From 7df9560c00450a909c24ba8692c64208a7bb0dca Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 27 Mar 2024 10:21:46 +0100 Subject: [PATCH] [WIFI-13549] Fix for venues with large amount of APs Signed-off-by: Charles --- package-lock.json | 4 +- package.json | 2 +- src/components/DataTable/index.tsx | 240 +++++++++--------- src/hooks/Network/Inventory.ts | 49 ++++ .../EntityPage/Layout/InventoryCard/index.tsx | 113 ++++++++- .../InventoryCard/useEntityInventory.tsx | 21 ++ .../VenuePage/Layout/InventoryCard/index.tsx | 114 ++++++++- .../InventoryCard/useVenueInventory.tsx | 21 ++ 8 files changed, 427 insertions(+), 137 deletions(-) create mode 100644 src/pages/EntityPage/Layout/InventoryCard/useEntityInventory.tsx create mode 100644 src/pages/VenuePage/Layout/InventoryCard/useVenueInventory.tsx diff --git a/package-lock.json b/package-lock.json index ce53593b..742a2133 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wlan-cloud-owprov-ui", - "version": "3.0.2(2)", + "version": "3.0.2(3)", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wlan-cloud-owprov-ui", - "version": "3.0.2(2)", + "version": "3.0.2(3)", "license": "ISC", "dependencies": { "@chakra-ui/anatomy": "^2.1.1", diff --git a/package.json b/package.json index 4a8200f1..7839909e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wlan-cloud-owprov-ui", - "version": "3.0.2(2)", + "version": "3.0.2(3)", "description": "", "main": "index.tsx", "scripts": { diff --git a/src/components/DataTable/index.tsx b/src/components/DataTable/index.tsx index 8960b5c9..6ef7f3fa 100644 --- a/src/components/DataTable/index.tsx +++ b/src/components/DataTable/index.tsx @@ -23,6 +23,7 @@ import { Spinner, Heading, useBreakpoint, + TableContainer, } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { @@ -63,6 +64,7 @@ type DataTableProps = { isManual?: boolean; saveSettingsId?: string; showAllRows?: boolean; + disabledPaginationAutoReset?: boolean; }; type TableInstanceWithHooks = TableInstance & @@ -89,6 +91,7 @@ const DataTable = ({ showAllRows, onRowClick, isRowClickable, + disabledPaginationAutoReset, }: DataTableProps) => { const { t } = useTranslation(); const breakpoint = useBreakpoint(); @@ -141,6 +144,7 @@ const DataTable = ({ // @ts-ignore initialState: { sortBy, pagination: !hideControls, pageSize: queryPageSize }, manualPagination: isManual, + autoResetPage: !disabledPaginationAutoReset, pageCount: isManual && count !== undefined ? Math.ceil(count / queryPageSize) @@ -164,7 +168,9 @@ const DataTable = ({ }; useEffect(() => { - if (setPageInfo && pageIndex !== undefined) setPageInfo({ index: pageIndex, limit: queryPageSize }); + if (setPageInfo && pageIndex !== undefined) { + setPageInfo({ index: pageIndex, limit: queryPageSize }); + } }, [queryPageSize, pageIndex]); useEffect(() => { @@ -214,132 +220,134 @@ const DataTable = ({ // Render the UI for your table return ( <> - + - - - { - // @ts-ignore - headerGroups.map((group) => ( - - { - // @ts-ignore - group.headers.map((column) => ( - - )) - } - - )) - } - - {data.length > 0 && ( - - {page.map((row: Row) => { - prepareRow(row); - const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true; - const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined; - return ( - + +
-
- {column.render('Header')} - -
-
+ + { + // @ts-ignore + headerGroups.map((group) => ( + { // @ts-ignore - row.cells.map((cell) => ( - )) } - ); - })} - - )} -
( + { - e.stopPropagation(); - } - : undefined - } - cursor={ - // @ts-ignore - !cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick - ? 'pointer' - : undefined - } + width={column.customWidth ?? null} > - {cell.render('Cell')} - +
+ {column.render('Header')} + +
+
- {!isLoading && data.length === 0 && !hideEmptyListText && ( -
- {obj ? ( - - {t('common.no_obj_found', { obj })} - - ) : ( - - {t('common.empty_list')} - + )) + } + + {data.length > 0 && ( + + {page.map((row: Row) => { + prepareRow(row); + const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true; + const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined; + return ( + + { + // @ts-ignore + row.cells.map((cell) => ( + { + e.stopPropagation(); + } + : undefined + } + cursor={ + // @ts-ignore + !cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick + ? 'pointer' + : undefined + } + > + {cell.render('Cell')} + + )) + } + + ); + })} + )} -
- )} + + {!isLoading && data.length === 0 && !hideEmptyListText && ( +
+ {obj ? ( + + {t('common.no_obj_found', { obj })} + + ) : ( + + {t('common.empty_list')} + + )} +
+ )} +
{!hideControls && ( diff --git a/src/hooks/Network/Inventory.ts b/src/hooks/Network/Inventory.ts index c04e2673..02614ef3 100644 --- a/src/hooks/Network/Inventory.ts +++ b/src/hooks/Network/Inventory.ts @@ -6,6 +6,55 @@ import { AxiosError } from 'models/Axios'; import { PageInfo, SortInfo } from 'models/Table'; import { axiosProv } from 'utils/axiosInstances'; +export const useGetSelectInventoryPaginated = ({ + serialNumbers, + pageInfo, + sortInfo, +}: { + serialNumbers: string[]; + pageInfo?: PageInfo; + sortInfo?: SortInfo; +}) => { + const { t } = useTranslation(); + const toast = useToast(); + const paginatedSerials = pageInfo + ? serialNumbers.slice(pageInfo.limit * pageInfo.index, pageInfo.limit * (pageInfo.index + 1)) + : []; + let sortString = ''; + if (sortInfo && sortInfo.length > 0) { + sortString = `&orderBy=${sortInfo.map((info) => `${info.id}:${info.sort.charAt(0)}`).join(',')}`; + } + + return useQuery( + ['get-inventory-with-select', serialNumbers, pageInfo], + () => + paginatedSerials.length > 0 + ? axiosProv + .get(`inventory?withExtendedInfo=true&select=${paginatedSerials}${sortString}`) + .then(({ data }) => data.taglist) + : [], + { + staleTime: 30000, + keepPreviousData: true, + onError: (e: AxiosError) => { + if (!toast.isActive('get-inventory-tags-fetching-error')) + toast({ + id: 'get-inventory-tags-fetching-error', + title: t('common.error'), + description: t('crud.error_fetching_obj', { + obj: t('inventory.tags'), + e: e?.response?.data?.ErrorDescription, + }), + status: 'error', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + }, + }, + ); +}; + export const useGetInventoryTableSpecs = () => useQuery( ['get-inventory-table-spec'], diff --git a/src/pages/EntityPage/Layout/InventoryCard/index.tsx b/src/pages/EntityPage/Layout/InventoryCard/index.tsx index 4a257fe5..1d427ff4 100644 --- a/src/pages/EntityPage/Layout/InventoryCard/index.tsx +++ b/src/pages/EntityPage/Layout/InventoryCard/index.tsx @@ -3,20 +3,22 @@ import { Box, Heading, Spacer, useDisclosure } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import EntityInventoryActions from './Actions'; +import { useEntityInventory } from './useEntityInventory'; import Card from 'components/Card'; import CardHeader from 'components/Card/CardHeader'; +import DataTable from 'components/DataTable'; import ExportDevicesTableButton from 'components/ExportInventoryButton'; import FactoryResetModal from 'components/Modals/SubscriberDevice/FactoryResetModal'; import FirmwareUpgradeModal from 'components/Modals/SubscriberDevice/FirmwareUpgradeModal'; import WifiScanModal from 'components/Modals/SubscriberDevice/WifiScanModal'; -import InventoryTable from 'components/Tables/InventoryTable'; import ConfigurationPushModal from 'components/Tables/InventoryTable/ConfigurationPushModal'; import CreateTagModal from 'components/Tables/InventoryTable/CreateTagModal'; import EditTagModal from 'components/Tables/InventoryTable/EditTagModal'; import ImportDeviceCsvModal from 'components/Tables/InventoryTable/ImportDeviceCsvModal'; -import { useGetEntity } from 'hooks/Network/Entity'; import { usePushConfig } from 'hooks/Network/Inventory'; import { Device } from 'models/Device'; +import { InventoryTagApiResponse } from 'models/Inventory'; +import { Column } from 'models/Table'; type Props = { id: string; @@ -24,7 +26,7 @@ type Props = { const EntityInventoryCard = ({ id }: Props) => { const { t } = useTranslation(); - const getEntity = useGetEntity({ id }); + const { getEntity, getTags, setPageInfo } = useEntityInventory({ entityId: id }); const queryClient = useQueryClient(); const [tag, setTag] = React.useState(undefined); const [serialNumber, setSerialNumber] = React.useState(''); @@ -67,9 +69,90 @@ const EntityInventoryCard = ({ id }: Props) => { ); const refetchTags = React.useCallback(() => { + getEntity.refetch(); queryClient.invalidateQueries(['get-inventory-with-select']); }, []); + const columns: Column[] = React.useMemo( + () => [ + { + id: 'serialNumber', + Header: t('inventory.serial_number'), + Footer: '', + accessor: 'serialNumber', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + isMonospace: true, + // @ts-ignore + sortType: (rowA, rowB, currId) => { + const a = rowA.values[currId]; + const b = rowB.values[currId]; + + if (a && b) { + return a.localeCompare(b); + } + + return 0; + }, + }, + { + id: 'name', + Header: t('common.name'), + Footer: '', + accessor: 'name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + isMonospace: true, + }, + { + id: 'configuration', + Header: t('configurations.one'), + Footer: '', + accessor: 'extendedInfo.deviceConfiguration.name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + }, + { + id: 'description', + Header: t('common.description'), + Footer: '', + accessor: 'description', + }, + { + id: 'entity', + Header: t('entities.entity'), + Footer: '', + accessor: 'extendedInfo.entity.name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + }, + { + id: 'venue', + Header: t('venues.one'), + Footer: '', + accessor: 'extendedInfo.venue.name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + }, + { + id: 'actions', + Header: '', + Footer: '', + accessor: 'id', + // @ts-ignore + Cell: (cell) => actions(cell), + customWidth: '50px', + disableSortBy: true, + }, + ], + [], + ); + return ( @@ -85,12 +168,24 @@ const EntityInventoryCard = ({ id }: Props) => { /> - - + !['entity', 'venue', 'description'].find((ign) => col.id === ign))} + data={getTags.data ?? []} + isManual + obj={t('devices.title')} + sortBy={[ + { + id: 'serialNumber', + desc: false, + }, + ]} + count={getEntity.data?.devices.length ?? 0} + setPageInfo={setPageInfo} + minHeight="200px" + onRowClick={openEditModal} + isRowClickable={() => true} + disabledPaginationAutoReset /> { + const [pageInfo, setPageInfo] = React.useState(); + const getEntity = useGetEntity({ id: entityId }); + const getTags = useGetSelectInventoryPaginated({ pageInfo, serialNumbers: getEntity.data?.devices ?? [] }); + + return { + getEntity, + getTags, + pageInfo, + setPageInfo, + }; +}; diff --git a/src/pages/VenuePage/Layout/InventoryCard/index.tsx b/src/pages/VenuePage/Layout/InventoryCard/index.tsx index 55487f43..078424bb 100644 --- a/src/pages/VenuePage/Layout/InventoryCard/index.tsx +++ b/src/pages/VenuePage/Layout/InventoryCard/index.tsx @@ -3,20 +3,23 @@ import { Box, Heading, Spacer, useDisclosure } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import VenueInventoryActions from './Actions'; +import { useVenueInventory } from './useVenueInventory'; +import RefreshButton from 'components/Buttons/RefreshButton'; import Card from 'components/Card'; import CardHeader from 'components/Card/CardHeader'; +import DataTable from 'components/DataTable'; import ExportDevicesTableButton from 'components/ExportInventoryButton'; import FactoryResetModal from 'components/Modals/SubscriberDevice/FactoryResetModal'; import FirmwareUpgradeModal from 'components/Modals/SubscriberDevice/FirmwareUpgradeModal'; import WifiScanModal from 'components/Modals/SubscriberDevice/WifiScanModal'; -import InventoryTable from 'components/Tables/InventoryTable'; import ConfigurationPushModal from 'components/Tables/InventoryTable/ConfigurationPushModal'; import CreateTagModal from 'components/Tables/InventoryTable/CreateTagModal'; import EditTagModal from 'components/Tables/InventoryTable/EditTagModal'; import ImportDeviceCsvModal from 'components/Tables/InventoryTable/ImportDeviceCsvModal'; import { usePushConfig } from 'hooks/Network/Inventory'; -import { useGetVenue } from 'hooks/Network/Venues'; import { Device } from 'models/Device'; +import { InventoryTagApiResponse } from 'models/Inventory'; +import { Column } from 'models/Table'; type Props = { id: string; @@ -24,7 +27,7 @@ type Props = { const VenueInventoryCard = ({ id }: Props) => { const { t } = useTranslation(); - const getVenue = useGetVenue({ id }); + const { getVenue, getTags, setPageInfo } = useVenueInventory({ venueId: id }); const queryClient = useQueryClient(); const [tag, setTag] = React.useState(undefined); const [serialNumber, setSerialNumber] = React.useState(''); @@ -71,6 +74,86 @@ const VenueInventoryCard = ({ id }: Props) => { queryClient.invalidateQueries(['get-inventory-with-select']); }, []); + const columns: Column[] = React.useMemo( + () => [ + { + id: 'serialNumber', + Header: t('inventory.serial_number'), + Footer: '', + accessor: 'serialNumber', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + isMonospace: true, + // @ts-ignore + sortType: (rowA, rowB, currId) => { + const a = rowA.values[currId]; + const b = rowB.values[currId]; + + if (a && b) { + return a.localeCompare(b); + } + + return 0; + }, + }, + { + id: 'name', + Header: t('common.name'), + Footer: '', + accessor: 'name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + isMonospace: true, + }, + { + id: 'configuration', + Header: t('configurations.one'), + Footer: '', + accessor: 'extendedInfo.deviceConfiguration.name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + }, + { + id: 'description', + Header: t('common.description'), + Footer: '', + accessor: 'description', + }, + { + id: 'entity', + Header: t('entities.entity'), + Footer: '', + accessor: 'extendedInfo.entity.name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + }, + { + id: 'venue', + Header: t('venues.one'), + Footer: '', + accessor: 'extendedInfo.venue.name', + customMaxWidth: '200px', + customWidth: 'calc(15vh)', + customMinWidth: '150px', + }, + { + id: 'actions', + Header: '', + Footer: '', + accessor: 'id', + // @ts-ignore + Cell: (cell) => actions(cell), + customWidth: '50px', + disableSortBy: true, + }, + ], + [], + ); + return ( @@ -81,13 +164,26 @@ const VenueInventoryCard = ({ id }: Props) => { + - - + !['entity', 'venue', 'description'].find((ign) => col.id === ign))} + data={getTags.data ?? []} + isManual + obj={t('devices.title')} + sortBy={[ + { + id: 'serialNumber', + desc: false, + }, + ]} + count={getVenue.data?.devices.length ?? 0} + setPageInfo={setPageInfo} + minHeight="200px" + onRowClick={openEditModal} + isRowClickable={() => true} + disabledPaginationAutoReset /> { + const [pageInfo, setPageInfo] = React.useState(); + const getVenue = useGetVenue({ id: venueId }); + const getTags = useGetSelectInventoryPaginated({ pageInfo, serialNumbers: getVenue.data?.devices ?? [] }); + + return { + getVenue, + getTags, + pageInfo, + setPageInfo, + }; +};