From 587b22dcb31f71cfd5a32dc8f17ed7f4307fa2bd Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 14 Jan 2025 12:30:23 +0100 Subject: [PATCH] Enable server-side pagination hardware_types Updated queries for `hardware_types` to support server-side pagination. This change ensures data fetching is paginated on the backend while maintaining the current client-side logic. A future update will optimize the respective tables to fully leverage pagination, avoiding an immediate rewrite of the client-side implementation. Part of #779 Signed-off-by: Omar --- frontend/src/api/schema.graphql | 72 +++++++++++++++++-- .../components/HardwareTypesTable.test.tsx | 59 +++++++++------ .../src/components/HardwareTypesTable.tsx | 55 +++++++++----- frontend/src/forms/CreateSystemModel.tsx | 16 +++-- frontend/src/forms/UpdateHardwareType.tsx | 19 +++-- frontend/src/pages/HardwareType.tsx | 18 ++--- frontend/src/pages/HardwareTypes.tsx | 17 +++-- frontend/src/pages/SystemModelCreate.tsx | 7 +- 8 files changed, 189 insertions(+), 74 deletions(-) diff --git a/frontend/src/api/schema.graphql b/frontend/src/api/schema.graphql index 4daa75c02..e82935dbb 100644 --- a/frontend/src/api/schema.graphql +++ b/frontend/src/api/schema.graphql @@ -1186,6 +1186,27 @@ enum HardwareTypePartNumberSortField { PART_NUMBER } +":hardware_type_part_number connection" +type HardwareTypePartNumberConnection { + "Total count on all pages" + count: Int + + "Page information" + pageInfo: PageInfo! + + ":hardware_type_part_number edges" + edges: [HardwareTypePartNumberEdge!] +} + +":hardware_type_part_number edge" +type HardwareTypePartNumberEdge { + "Cursor" + cursor: String! + + ":hardware_type_part_number node" + node: HardwareTypePartNumber! +} + input HardwareTypePartNumberFilterPartNumber { isNil: Boolean eq: String @@ -1301,6 +1322,27 @@ enum HardwareTypeSortField { NAME } +":hardware_type connection" +type HardwareTypeConnection { + "Total count on all pages" + count: Int + + "Page information" + pageInfo: PageInfo! + + ":hardware_type edges" + edges: [HardwareTypeEdge!] +} + +":hardware_type edge" +type HardwareTypeEdge { + "Cursor" + cursor: String! + + ":hardware_type node" + node: HardwareType! +} + input HardwareTypeFilterName { isNil: Boolean eq: String @@ -1395,12 +1437,18 @@ type HardwareType implements Node { "A filter to limit the results" filter: HardwareTypePartNumberFilterInput - "The number of records to return." - limit: Int + "The number of records to return from the beginning. Maximum 250" + first: Int - "The number of records to skip." - offset: Int - ): [HardwareTypePartNumber!]! + "Show records before the specified keyset." + before: String + + "Show records after the specified keyset." + after: String + + "The number of records to return to the end. Maximum 250" + last: Int + ): HardwareTypePartNumberConnection! } "The result of the :set_device_led_behavior mutation" @@ -2879,7 +2927,19 @@ type RootQueryType { "A filter to limit the results" filter: HardwareTypeFilterInput - ): [HardwareType!]! + + "The number of records to return from the beginning. Maximum 250" + first: Int + + "Show records before the specified keyset." + before: String + + "Show records after the specified keyset." + after: String + + "The number of records to return to the end. Maximum 250" + last: Int + ): HardwareTypeConnection "Returns a single system model." systemModel("The id of the record" id: ID!): SystemModel diff --git a/frontend/src/components/HardwareTypesTable.test.tsx b/frontend/src/components/HardwareTypesTable.test.tsx index 560568d9e..6b97c7309 100644 --- a/frontend/src/components/HardwareTypesTable.test.tsx +++ b/frontend/src/components/HardwareTypesTable.test.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2024 SECO Mind Srl + Copyright 2024-2025 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,15 +29,14 @@ import type { HardwareTypesTable_getHardwareTypes_Query } from "api/__generated_ import HardwareTypesTable from "./HardwareTypesTable"; const GET_HARDWARE_TYPES_QUERY = graphql` - query HardwareTypesTable_getHardwareTypes_Query @relay_test_operation { - hardwareTypes { - ...HardwareTypesTable_HardwareTypesFragment - } + query HardwareTypesTable_getHardwareTypes_Query($first: Int, $after: String) + @relay_test_operation { + ...HardwareTypesTable_HardwareTypesFragment } `; const ComponentWithQuery = () => { - const { hardwareTypes } = + const hardwareTypes = useLazyLoadQuery( GET_HARDWARE_TYPES_QUERY, {}, @@ -50,16 +49,30 @@ type HardwareType = { handle: string; name: string; partNumbers: { - id: string; - partNumber: string; - }[]; + edges: { + node: { + id: string; + partNumber: string; + }; + }[]; + }; }; const renderComponent = (hardwareTypes: HardwareType[] = []) => { const relayEnvironment = createMockEnvironment(); - relayEnvironment.mock.queueOperationResolver((_operation) => ({ - data: { hardwareTypes }, + + relayEnvironment.mock.queueOperationResolver(() => ({ + data: { + hardwareTypes: { + edges: hardwareTypes.map((model) => ({ + node: model, + })), + }, + }, })); + + relayEnvironment.mock.queuePendingOperation(GET_HARDWARE_TYPES_QUERY, {}); + renderWithProviders(, { relayEnvironment }); }; @@ -81,7 +94,9 @@ it("renders Hardware Type data", () => { id: "HW-ID", handle: "hw-handle", name: "HW name", - partNumbers: [{ id: "HW-PN1", partNumber: "HW-PN1" }], + partNumbers: { + edges: [{ node: { id: "HW-PN1", partNumber: "HW-PN1" } }], + }, }, ]); @@ -100,10 +115,12 @@ it("renders multiple Part Numbers separated by comma", () => { id: "HW-ID", handle: "hw-handle", name: "HW name", - partNumbers: [ - { id: "HW-PN1", partNumber: "HW-PN1" }, - { id: "HW-PN2", partNumber: "HW-PN2" }, - ], + partNumbers: { + edges: [ + { node: { id: "HW-PN1", partNumber: "HW-PN1" } }, + { node: { id: "HW-PN2", partNumber: "HW-PN2" } }, + ], + }, }, ]); @@ -116,10 +133,12 @@ it("renders Hardware Type data in correct columns", () => { id: "HW-ID", handle: "hw-handle", name: "HW name", - partNumbers: [ - { id: "HW-PN1", partNumber: "HW-PN1" }, - { id: "HW-PN2", partNumber: "HW-PN2" }, - ], + partNumbers: { + edges: [ + { node: { id: "HW-PN1", partNumber: "HW-PN1" } }, + { node: { id: "HW-PN2", partNumber: "HW-PN2" } }, + ], + }, }, ]); diff --git a/frontend/src/components/HardwareTypesTable.tsx b/frontend/src/components/HardwareTypesTable.tsx index f969c200f..85a17c346 100644 --- a/frontend/src/components/HardwareTypesTable.tsx +++ b/frontend/src/components/HardwareTypesTable.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021-2024 SECO Mind Srl + Copyright 2021-2025 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,30 +20,47 @@ import React from "react"; import { FormattedMessage } from "react-intl"; -import { graphql, useFragment } from "react-relay/hooks"; +import { graphql, usePaginationFragment } from "react-relay/hooks"; -import Table, { createColumnHelper } from "components/Table"; -import { Link, Route } from "Navigation"; +import type { HardwareTypesTable_PaginationQuery } from "api/__generated__/HardwareTypesTable_PaginationQuery.graphql"; import type { HardwareTypesTable_HardwareTypesFragment$key, HardwareTypesTable_HardwareTypesFragment$data, } from "api/__generated__/HardwareTypesTable_HardwareTypesFragment.graphql"; +import Table, { createColumnHelper } from "components/Table"; +import { Link, Route } from "Navigation"; + // We use graphql fields below in columns configuration /* eslint-disable relay/unused-fields */ const HARDWARE_TYPES_TABLE_FRAGMENT = graphql` - fragment HardwareTypesTable_HardwareTypesFragment on HardwareType - @relay(plural: true) { - id - handle - name - partNumbers { - partNumber + fragment HardwareTypesTable_HardwareTypesFragment on RootQueryType + @refetchable(queryName: "HardwareTypesTable_PaginationQuery") { + hardwareTypes(first: $first, after: $after) + @connection(key: "HardwareTypesTable_hardwareTypes") { + edges { + node { + id + handle + name + partNumbers { + edges { + node { + partNumber + } + } + } + } + } } } `; -type TableRecord = HardwareTypesTable_HardwareTypesFragment$data[number]; +type TableRecord = NonNullable< + NonNullable< + HardwareTypesTable_HardwareTypesFragment$data["hardwareTypes"] + >["edges"] +>[number]["node"]; const columnHelper = createColumnHelper(); const columns = [ @@ -81,7 +98,7 @@ const columns = [ /> ), cell: ({ getValue }) => - getValue().map(({ partNumber }, index) => ( + getValue().edges?.map(({ node: { partNumber } }, index) => ( {index > 0 && ", "} {partNumber} @@ -96,12 +113,14 @@ type Props = { }; const HardwareTypesTable = ({ className, hardwareTypesRef }: Props) => { - const hardwareTypes = useFragment( - HARDWARE_TYPES_TABLE_FRAGMENT, - hardwareTypesRef, - ); + const { data } = usePaginationFragment< + HardwareTypesTable_PaginationQuery, + HardwareTypesTable_HardwareTypesFragment$key + >(HARDWARE_TYPES_TABLE_FRAGMENT, hardwareTypesRef); + + const tableData = data.hardwareTypes?.edges?.map((edge) => edge.node) ?? []; - return ; + return
; }; export default HardwareTypesTable; diff --git a/frontend/src/forms/CreateSystemModel.tsx b/frontend/src/forms/CreateSystemModel.tsx index 2fcd4d89d..a96acfa3b 100644 --- a/frontend/src/forms/CreateSystemModel.tsx +++ b/frontend/src/forms/CreateSystemModel.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021-2024 SECO Mind Srl + Copyright 2021-2025 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import { FormattedMessage, useIntl } from "react-intl"; import { graphql, useFragment } from "react-relay/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; +import type { CreateSystemModel_OptionsFragment$key } from "api/__generated__/CreateSystemModel_OptionsFragment.graphql"; + import Button from "components/Button"; import CloseButton from "components/CloseButton"; import Col from "components/Col"; @@ -35,13 +37,15 @@ import Spinner from "components/Spinner"; import Stack from "components/Stack"; import { systemModelHandleSchema, messages, yup } from "forms"; -import type { CreateSystemModel_OptionsFragment$key } from "api/__generated__/CreateSystemModel_OptionsFragment.graphql"; - const CREATE_SYSTEM_MODEL_FRAGMENT = graphql` fragment CreateSystemModel_OptionsFragment on RootQueryType { hardwareTypes { - id - name + edges { + node { + id + name + } + } } tenantInfo { defaultLocale @@ -303,7 +307,7 @@ const CreateSystemModelForm = ({ defaultMessage: "Select a Hardware Type", })} - {hardwareTypes.map((hardwareType) => ( + {hardwareTypes?.edges?.map(({ node: hardwareType }) => ( diff --git a/frontend/src/forms/UpdateHardwareType.tsx b/frontend/src/forms/UpdateHardwareType.tsx index bb7891e04..d14bede81 100644 --- a/frontend/src/forms/UpdateHardwareType.tsx +++ b/frontend/src/forms/UpdateHardwareType.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021-2024 SECO Mind Srl + Copyright 2021-2025 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import { FormattedMessage } from "react-intl"; import { graphql, useFragment } from "react-relay/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; +import type { UpdateHardwareType_HardwareTypeFragment$key } from "api/__generated__/UpdateHardwareType_HardwareTypeFragment.graphql"; + import Button from "components/Button"; import Col from "components/Col"; import Form from "components/Form"; @@ -33,14 +35,17 @@ import Spinner from "components/Spinner"; import Stack from "components/Stack"; import { hardwareTypeHandleSchema, messages, yup } from "forms"; -import type { UpdateHardwareType_HardwareTypeFragment$key } from "api/__generated__/UpdateHardwareType_HardwareTypeFragment.graphql"; - const UPDATE_HARDWARE_TYPE_FRAGMENT = graphql` fragment UpdateHardwareType_HardwareTypeFragment on HardwareType { name handle partNumbers { - partNumber + count + edges { + node { + partNumber + } + } } } `; @@ -127,10 +132,10 @@ const UpdateHardwareTypeForm = ({ name: hardwareType.name, handle: hardwareType.handle, partNumbers: - hardwareType.partNumbers.length > 0 - ? hardwareType.partNumbers.map(({ partNumber }) => ({ + hardwareType.partNumbers?.count && hardwareType.partNumbers.count > 0 + ? hardwareType.partNumbers.edges?.map(({ node: { partNumber } }) => ({ value: partNumber, - })) + })) ?? [] : [{ value: "" }], // default with at least one empty part number }), [hardwareType.name, hardwareType.handle, hardwareType.partNumbers], diff --git a/frontend/src/pages/HardwareType.tsx b/frontend/src/pages/HardwareType.tsx index a89df0cd1..8e02952ed 100644 --- a/frontend/src/pages/HardwareType.tsx +++ b/frontend/src/pages/HardwareType.tsx @@ -22,6 +22,7 @@ import { Suspense, useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { ErrorBoundary } from "react-error-boundary"; import { + ConnectionHandler, graphql, useMutation, usePreloadedQuery, @@ -137,15 +138,16 @@ const HardwareTypeContent = ({ hardwareType }: HardwareTypeContentProps) => { } const root = store.getRoot(); - const hardwareTypes = root.getLinkedRecords("hardwareTypes"); - if (hardwareTypes) { - root.setLinkedRecords( - hardwareTypes.filter( - (hardwareType) => hardwareType.getDataID() !== hardwareTypeId, - ), - "hardwareTypes", - ); + + const connection = ConnectionHandler.getConnection( + root, + "HardwareTypesTable_hardwareTypes", + ); + + if (connection) { + ConnectionHandler.deleteNode(connection, hardwareTypeId); } + store.delete(hardwareTypeId); }, }); diff --git a/frontend/src/pages/HardwareTypes.tsx b/frontend/src/pages/HardwareTypes.tsx index 8eb76acf2..e081b8455 100644 --- a/frontend/src/pages/HardwareTypes.tsx +++ b/frontend/src/pages/HardwareTypes.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021-2024 SECO Mind Srl + Copyright 2021-2025 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import { graphql, usePreloadedQuery, useQueryLoader } from "react-relay/hooks"; import type { PreloadedQuery } from "react-relay/hooks"; import type { HardwareTypes_getHardwareTypes_Query } from "api/__generated__/HardwareTypes_getHardwareTypes_Query.graphql"; + import Button from "components/Button"; import Center from "components/Center"; import HardwareTypesTable from "components/HardwareTypesTable"; @@ -34,10 +35,11 @@ import Spinner from "components/Spinner"; import { Link, Route } from "Navigation"; const GET_HARDWARE_TYPES_QUERY = graphql` - query HardwareTypes_getHardwareTypes_Query { - hardwareTypes { - ...HardwareTypesTable_HardwareTypesFragment + query HardwareTypes_getHardwareTypes_Query($first: Int, $after: String) { + hardwareTypes(first: $first, after: $after) { + count } + ...HardwareTypesTable_HardwareTypesFragment } `; @@ -48,7 +50,7 @@ interface HardwareTypesContentProps { const HardwareTypesContent = ({ getHardwareTypesQuery, }: HardwareTypesContentProps) => { - const { hardwareTypes } = usePreloadedQuery( + const hardwareTypes = usePreloadedQuery( GET_HARDWARE_TYPES_QUERY, getHardwareTypesQuery, ); @@ -71,7 +73,7 @@ const HardwareTypesContent = ({ - {hardwareTypes.length === 0 ? ( + {hardwareTypes.hardwareTypes?.count === 0 ? ( { ); const fetchHardwareTypes = useCallback( - () => getHardwareTypes({}, { fetchPolicy: "store-and-network" }), + () => + getHardwareTypes({ first: 10_000 }, { fetchPolicy: "store-and-network" }), [getHardwareTypes], ); diff --git a/frontend/src/pages/SystemModelCreate.tsx b/frontend/src/pages/SystemModelCreate.tsx index cd37fe640..fe3fe651e 100644 --- a/frontend/src/pages/SystemModelCreate.tsx +++ b/frontend/src/pages/SystemModelCreate.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021-2024 SECO Mind Srl + Copyright 2021-2025 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,11 +28,13 @@ import { useQueryLoader, } from "react-relay/hooks"; import type { PreloadedQuery } from "react-relay/hooks"; + import type { SystemModelCreate_getOptions_Query, SystemModelCreate_getOptions_Query$data, } from "api/__generated__/SystemModelCreate_getOptions_Query.graphql"; import type { SystemModelCreate_createSystemModel_Mutation } from "api/__generated__/SystemModelCreate_createSystemModel_Mutation.graphql"; + import Alert from "components/Alert"; import Button from "components/Button"; import Center from "components/Center"; @@ -47,6 +49,7 @@ const CREATE_SYSTEM_MODEL_PAGE_QUERY = graphql` query SystemModelCreate_getOptions_Query { hardwareTypes { __typename + count } ...CreateSystemModel_OptionsFragment } @@ -190,7 +193,7 @@ const SystemModelWrapper = ({ getCreateSystemModelOptionsQuery, ); const { hardwareTypes } = systemModelOptions; - if (hardwareTypes.length === 0) { + if (hardwareTypes?.count === 0) { return ; } return ;