mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-31 18:27:45 +00:00
[WIFI-12435] [WIFI-12436] Device table added functionality and styling fixes
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
49
package-lock.json
generated
49
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(23)",
|
||||
"version": "2.10.0(5)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(23)",
|
||||
"version": "2.10.0(5)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@googlemaps/typescript-guards": "^2.0.3",
|
||||
"@react-spring/web": "^9.5.5",
|
||||
"@tanstack/react-query": "^4.12.0",
|
||||
"@tanstack/react-table": "^8.7.9",
|
||||
"@textea/json-viewer": "^2.10.0",
|
||||
"axios": "^1.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -3496,6 +3497,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.8.5.tgz",
|
||||
"integrity": "sha512-g/t21E/ICHvaCOJOhsDNr5QaB/6aDQEHFbx/YliwwU/CJThMqG+dS28vnToIBV/5MBgpeXoGRi2waDJVJlZrtg==",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.8.5.tgz",
|
||||
"integrity": "sha512-Xnwa1qxpgvSW7ozLiexmKp2PIYcLBiY/IizbdGriYCL6OOHuZ9baRhrrH51zjyz+61ly6K59rmt6AI/3RR+97Q==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@textea/json-viewer": {
|
||||
"version": "2.10.0",
|
||||
"license": "MIT",
|
||||
@@ -11823,6 +11855,19 @@
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@tanstack/react-table": {
|
||||
"version": "8.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.8.5.tgz",
|
||||
"integrity": "sha512-g/t21E/ICHvaCOJOhsDNr5QaB/6aDQEHFbx/YliwwU/CJThMqG+dS28vnToIBV/5MBgpeXoGRi2waDJVJlZrtg==",
|
||||
"requires": {
|
||||
"@tanstack/table-core": "8.8.5"
|
||||
}
|
||||
},
|
||||
"@tanstack/table-core": {
|
||||
"version": "8.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.8.5.tgz",
|
||||
"integrity": "sha512-Xnwa1qxpgvSW7ozLiexmKp2PIYcLBiY/IizbdGriYCL6OOHuZ9baRhrrH51zjyz+61ly6K59rmt6AI/3RR+97Q=="
|
||||
},
|
||||
"@textea/json-viewer": {
|
||||
"version": "2.10.0",
|
||||
"requires": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(23)",
|
||||
"version": "2.10.0(5)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
@@ -51,6 +51,7 @@
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"@tanstack/react-query": "^4.12.0",
|
||||
"@tanstack/react-table": "^8.7.9",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-table": "^7.8.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
|
||||
23
src/@tanstack.react-table.d.ts
vendored
Normal file
23
src/@tanstack.react-table.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { BoxProps } from '@chakra-ui/react';
|
||||
import '@tanstack/react-table';
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
stopPropagation?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
hasPopover?: boolean;
|
||||
customMaxWidth?: string;
|
||||
customMinWidth?: string;
|
||||
customWidth?: string;
|
||||
isMonospace?: boolean;
|
||||
isCentered?: boolean;
|
||||
columnSelectorOptions?: {
|
||||
label?: string;
|
||||
};
|
||||
headerOptions?: {
|
||||
tooltip?: string;
|
||||
};
|
||||
headerStyleProps?: BoxProps;
|
||||
}
|
||||
}
|
||||
62
src/components/DataTables/DataGrid/CellRow.tsx
Normal file
62
src/components/DataTables/DataGrid/CellRow.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import { Td, Tr } from '@chakra-ui/react';
|
||||
import { Row, flexRender } from '@tanstack/react-table';
|
||||
|
||||
export type DataGridCellRowProps<TValue extends object> = {
|
||||
row: Row<TValue>;
|
||||
onRowClick: ((row: TValue) => (() => void) | undefined) | undefined;
|
||||
rowStyle: {
|
||||
hoveredRowBg: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const DataGridCellRow = <TValue extends object>({
|
||||
row,
|
||||
rowStyle: { hoveredRowBg },
|
||||
onRowClick,
|
||||
}: DataGridCellRowProps<TValue>) => {
|
||||
const onClick = onRowClick ? onRowClick(row.original) : undefined;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={row.id}
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Td
|
||||
px={1}
|
||||
key={cell.id}
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
minWidth={cell.column.columnDef.meta?.customMinWidth ?? undefined}
|
||||
maxWidth={cell.column.columnDef.meta?.customMaxWidth ?? undefined}
|
||||
width={cell.column.columnDef.meta?.customWidth}
|
||||
textAlign={cell.column.columnDef.meta?.isCentered ? 'center' : undefined}
|
||||
fontFamily={
|
||||
cell.column.columnDef.meta?.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
cell.column.columnDef.meta?.stopPropagation || (cell.column.id === 'actions' && onClick)
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
cursor={
|
||||
!cell.column.columnDef.meta?.stopPropagation && cell.column.id !== 'actions' && onClick
|
||||
? 'pointer'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
82
src/components/DataTables/DataGrid/DataGridColumnPicker.tsx
Normal file
82
src/components/DataTables/DataGrid/DataGridColumnPicker.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { Box, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
|
||||
import { VisibilityState } from '@tanstack/react-table';
|
||||
import { FunnelSimple } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DataGridColumn } from './useDataGrid';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
export type DataGridColumnPickerProps<TValue extends object> = {
|
||||
preference: string;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
columnVisibility: VisibilityState;
|
||||
setColumnVisibility: (str: VisibilityState) => void;
|
||||
};
|
||||
|
||||
export const DataGridColumnPicker = <TValue extends object>({
|
||||
preference,
|
||||
columns,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
}: DataGridColumnPickerProps<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const { getPref, setPref } = useAuth();
|
||||
|
||||
const handleColumnClick = React.useCallback(
|
||||
(id: string) => {
|
||||
const newVisibility = { ...columnVisibility };
|
||||
newVisibility[id] = newVisibility[id] !== undefined ? !newVisibility[id] : false;
|
||||
const hiddenColumnsArray = Object.entries(newVisibility)
|
||||
.filter(([, value]) => !value)
|
||||
.map(([key]) => key);
|
||||
setPref({ preference, value: hiddenColumnsArray.join(',') });
|
||||
setColumnVisibility({ ...newVisibility });
|
||||
},
|
||||
[columnVisibility],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const savedPrefs = getPref(preference);
|
||||
|
||||
if (savedPrefs) {
|
||||
const savedHiddenColumns = savedPrefs.split(',');
|
||||
setColumnVisibility(savedHiddenColumns.reduce((acc, curr) => ({ ...acc, [curr]: false }), {}));
|
||||
} else {
|
||||
setColumnVisibility({});
|
||||
}
|
||||
}, [preference]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Menu closeOnSelect={false} isLazy>
|
||||
<Tooltip label={t('common.columns')} hasArrow>
|
||||
<MenuButton as={IconButton} icon={<FunnelSimple />} />
|
||||
</Tooltip>
|
||||
<MenuList maxH="200px" overflowY="auto">
|
||||
{columns
|
||||
.filter((col) => col.id && col.header)
|
||||
.map((column) => {
|
||||
const handleClick =
|
||||
column.id !== undefined ? () => handleColumnClick(column.id as unknown as string) : undefined;
|
||||
const id = column.id ?? uuid();
|
||||
let label = column.header?.toString() ?? 'Unrecognized column';
|
||||
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={id}
|
||||
as={Checkbox}
|
||||
isChecked={columnVisibility[id] === undefined || columnVisibility[id]}
|
||||
onChange={column.meta?.alwaysShow ? undefined : handleClick}
|
||||
isDisabled={column.meta?.alwaysShow}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
43
src/components/DataTables/DataGrid/HeaderRow.tsx
Normal file
43
src/components/DataTables/DataGrid/HeaderRow.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Th, Tooltip, Tr } from '@chakra-ui/react';
|
||||
import { HeaderGroup, flexRender } from '@tanstack/react-table';
|
||||
import { DataGridSortIcon } from './SortIcon';
|
||||
|
||||
export type DataGridHeaderRowProps<TValue extends object> = {
|
||||
headerGroup: HeaderGroup<TValue>;
|
||||
};
|
||||
|
||||
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
|
||||
<Tr p={0}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Th
|
||||
color="gray.400"
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
minWidth={header.column.columnDef.meta?.customMinWidth ?? undefined}
|
||||
maxWidth={header.column.columnDef.meta?.customMaxWidth ?? undefined}
|
||||
width={header.column.columnDef.meta?.customWidth}
|
||||
fontSize="sm"
|
||||
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
|
||||
cursor={header.column.getCanSort() ? 'pointer' : undefined}
|
||||
>
|
||||
<Flex display="flex" alignItems="center">
|
||||
{header.isPlaceholder ? null : (
|
||||
<Tooltip label={header.column.columnDef.meta?.headerOptions?.tooltip}>
|
||||
<Box
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
alignContent="center"
|
||||
width="100%"
|
||||
{...header.column.columnDef.meta?.headerStyleProps}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DataGridSortIcon sortInfo={header.column.getIsSorted()} canSort={header.column.getCanSort()} />
|
||||
</Flex>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
);
|
||||
124
src/components/DataTables/DataGrid/Input.tsx
Normal file
124
src/components/DataTables/DataGrid/Input.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import * as React from 'react';
|
||||
import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Tooltip,
|
||||
Flex,
|
||||
IconButton,
|
||||
Text,
|
||||
Select,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useContainerDimensions } from 'hooks/useContainerDimensions';
|
||||
|
||||
type Props<T extends object> = {
|
||||
table: Table<T>;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const DataGridControls = <T extends object>({ table, isDisabled }: Props<T>) => {
|
||||
const { t } = useTranslation();
|
||||
const { ref, dimensions } = useContainerDimensions({ precision: 100 });
|
||||
const isCompact = dimensions.width !== 0 && dimensions.width <= 800;
|
||||
|
||||
return (
|
||||
<Flex ref={ref} justifyContent="space-between" m={4} alignItems="center">
|
||||
<Flex>
|
||||
<Tooltip label={t('table.first_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to first page"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
isDisabled={isDisabled || !table.getCanPreviousPage()}
|
||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||
mr={4}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('table.previous_page')}>
|
||||
<IconButton
|
||||
aria-label="Previous page"
|
||||
onClick={() => table.previousPage()}
|
||||
isDisabled={isDisabled || !table.getCanPreviousPage()}
|
||||
icon={<ChevronLeftIcon h={6} w={6} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems="center">
|
||||
{isCompact ? null : (
|
||||
<>
|
||||
<Text flexShrink={0} mr={8}>
|
||||
{t('table.page')}{' '}
|
||||
<Text fontWeight="bold" as="span">
|
||||
{table.getState().pagination.pageIndex + 1}
|
||||
</Text>{' '}
|
||||
{t('common.of')}{' '}
|
||||
<Text fontWeight="bold" as="span">
|
||||
{table.getPageCount()}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text flexShrink={0}>{t('table.go_to_page')}</Text>{' '}
|
||||
<NumberInput
|
||||
ml={2}
|
||||
mr={8}
|
||||
w={28}
|
||||
min={1}
|
||||
max={table.getPageCount()}
|
||||
onChange={(_, numberValue) => {
|
||||
const newPage = numberValue ? numberValue - 1 : 0;
|
||||
table.setPageIndex(newPage);
|
||||
}}
|
||||
value={table.getState().pagination.pageIndex + 1}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</>
|
||||
)}
|
||||
<Select
|
||||
w={32}
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={(e) => {
|
||||
table.setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map((opt) => (
|
||||
<option key={uuid()} value={opt}>
|
||||
{t('common.show')} {opt}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<Tooltip label={t('table.next_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to next page"
|
||||
onClick={() => table.nextPage()}
|
||||
isDisabled={isDisabled || !table.getCanNextPage()}
|
||||
icon={<ChevronRightIcon h={6} w={6} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('table.last_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to last page"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
isDisabled={isDisabled || !table.getCanNextPage()}
|
||||
icon={<ArrowRightIcon h={3} w={3} />}
|
||||
ml={4}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataGridControls;
|
||||
23
src/components/DataTables/DataGrid/SortIcon.tsx
Normal file
23
src/components/DataTables/DataGrid/SortIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { SortDirection } from '@tanstack/react-table';
|
||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
||||
|
||||
export type DataGridSortIconProps = {
|
||||
sortInfo: false | SortDirection;
|
||||
canSort: boolean;
|
||||
};
|
||||
|
||||
export const DataGridSortIcon = ({ sortInfo, canSort }: DataGridSortIconProps) => {
|
||||
if (canSort) {
|
||||
if (sortInfo) {
|
||||
return sortInfo === 'desc' ? (
|
||||
<Icon ml={1} boxSize={3} as={ArrowDown} />
|
||||
) : (
|
||||
<Icon ml={1} boxSize={3} as={ArrowUp} />
|
||||
);
|
||||
}
|
||||
return <Icon ml={1} boxSize={3} as={Circle} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
189
src/components/DataTables/DataGrid/index.tsx
Normal file
189
src/components/DataTables/DataGrid/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Flex,
|
||||
HStack,
|
||||
Heading,
|
||||
LayoutProps,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Thead,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataGridCellRow } from './CellRow';
|
||||
import { DataGridColumnPicker } from './DataGridColumnPicker';
|
||||
import { DataGridHeaderRow } from './HeaderRow';
|
||||
import DataGridControls from './Input';
|
||||
import { DataGridColumn, UseDataGridReturn } from './useDataGrid';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||
|
||||
export type ColumnOptions = {
|
||||
isSortable?: boolean;
|
||||
};
|
||||
|
||||
export type DataGridOptions<TValue extends object> = {
|
||||
count?: number;
|
||||
isFullScreen?: boolean;
|
||||
isHidingControls?: boolean;
|
||||
isManual?: boolean;
|
||||
minimumHeight?: LayoutProps['minH'];
|
||||
onRowClick?: (row: TValue) => (() => void) | undefined;
|
||||
refetch?: () => void;
|
||||
};
|
||||
|
||||
export type DataGridProps<TValue extends object> = {
|
||||
controller: UseDataGridReturn;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
header: {
|
||||
title: string;
|
||||
objectListed: string;
|
||||
leftContent?: React.ReactNode;
|
||||
addButton?: React.ReactNode;
|
||||
otherButtons?: React.ReactNode;
|
||||
};
|
||||
data?: TValue[];
|
||||
isLoading?: boolean;
|
||||
options?: DataGridOptions<TValue>;
|
||||
};
|
||||
|
||||
export const DataGrid = <TValue extends object>({
|
||||
controller,
|
||||
columns,
|
||||
header,
|
||||
data = [],
|
||||
options = {},
|
||||
isLoading = false,
|
||||
}: DataGridProps<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
/*
|
||||
Table Styling
|
||||
*/
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||
|
||||
const minimumHeight: LayoutProps['minH'] = React.useMemo(() => {
|
||||
if (options.isFullScreen) {
|
||||
return { base: 'calc(100vh - 360px)', md: 'calc(100vh - 288px)' };
|
||||
}
|
||||
return options.minimumHeight ?? '300px';
|
||||
}, [options.isFullScreen, options.minimumHeight]);
|
||||
|
||||
/*
|
||||
Table Options
|
||||
*/
|
||||
const onRowClick = React.useMemo(() => options.onRowClick, [options.onRowClick]);
|
||||
|
||||
const pagination = React.useMemo(
|
||||
() => ({
|
||||
pageIndex: controller.pageInfo.pageIndex,
|
||||
pageSize: controller.pageInfo.pageSize,
|
||||
}),
|
||||
[controller.pageInfo.pageIndex, controller.pageInfo.pageSize],
|
||||
);
|
||||
|
||||
const pageCount = React.useMemo(() => {
|
||||
if (options.isManual && options.count) {
|
||||
return Math.ceil(options.count / pagination.pageSize);
|
||||
}
|
||||
return Math.ceil((data?.length ?? 0) / pagination.pageSize);
|
||||
}, [options.count, options.isManual, data?.length, pagination.pageSize]);
|
||||
|
||||
const tableOptions = React.useMemo(
|
||||
() => ({
|
||||
pageCount: pageCount > 0 ? pageCount : 1,
|
||||
initialState: { sorting: controller.sortBy, pagination },
|
||||
manualPagination: options.isManual,
|
||||
manualSorting: options.isManual,
|
||||
autoResetPageIndex: false,
|
||||
}),
|
||||
[options.isManual, controller.sortBy, pageCount],
|
||||
);
|
||||
|
||||
const table = useReactTable<TValue>({
|
||||
// react-table base functions
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
|
||||
// Table State
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting: controller.sortBy,
|
||||
columnVisibility: controller.columnVisibility,
|
||||
pagination,
|
||||
},
|
||||
|
||||
// Change Handlers
|
||||
onSortingChange: controller.setSortBy,
|
||||
onPaginationChange: controller.onPaginationChange,
|
||||
|
||||
// debugTable: true,
|
||||
|
||||
// Table Options
|
||||
...tableOptions,
|
||||
});
|
||||
|
||||
if (isLoading && data.length === 0) {
|
||||
return (
|
||||
<Center>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box w="100%">
|
||||
<Flex mb={2}>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
{header.leftContent}
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
<DataGridColumnPicker
|
||||
columns={columns}
|
||||
columnVisibility={controller.columnVisibility}
|
||||
setColumnVisibility={controller.setColumnVisibility}
|
||||
preference={`${controller.tableSettingsId}.hiddenColumns`}
|
||||
/>
|
||||
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||
</HStack>
|
||||
</Flex>
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{data?.length === 0 ? (
|
||||
<Center mt={8}>
|
||||
<Heading size="md">
|
||||
{header.objectListed ? t('common.no_obj_found', { obj: header.objectListed }) : t('common.empty_list')}
|
||||
</Heading>
|
||||
</Center>
|
||||
) : null}
|
||||
</TableContainer>
|
||||
</LoadingOverlay>
|
||||
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
82
src/components/DataTables/DataGrid/useDataGrid.ts
Normal file
82
src/components/DataTables/DataGrid/useDataGrid.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ColumnDef,
|
||||
OnChangeFn,
|
||||
PaginationState,
|
||||
SortingColumnDef,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
const getDefaultSettings = (settings?: string) => {
|
||||
let limit = 10;
|
||||
let index = 0;
|
||||
|
||||
if (settings) {
|
||||
const savedSizeSetting = localStorage.getItem(settings);
|
||||
if (savedSizeSetting) {
|
||||
try {
|
||||
limit = parseInt(savedSizeSetting, 10);
|
||||
} catch (e) {
|
||||
limit = 10;
|
||||
}
|
||||
}
|
||||
|
||||
const savedPageSetting = localStorage.getItem(`${settings}.page`);
|
||||
if (savedPageSetting) {
|
||||
try {
|
||||
index = parseInt(savedPageSetting, 10);
|
||||
} catch (e) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pageSize: limit,
|
||||
pageIndex: index,
|
||||
};
|
||||
};
|
||||
|
||||
export type DataGridColumn<T> = ColumnDef<T> & SortingColumnDef<T> & { id: string };
|
||||
|
||||
export type UseDataGridReturn = {
|
||||
tableSettingsId: string;
|
||||
pageInfo: PaginationState;
|
||||
columnVisibility: VisibilityState;
|
||||
setColumnVisibility: React.Dispatch<React.SetStateAction<VisibilityState>>;
|
||||
sortBy: SortingState;
|
||||
setSortBy: React.Dispatch<React.SetStateAction<SortingState>>;
|
||||
onPaginationChange: OnChangeFn<PaginationState>;
|
||||
};
|
||||
|
||||
export type UseDataGridProps = {
|
||||
tableSettingsId: string;
|
||||
defaultSortBy?: SortingState;
|
||||
};
|
||||
|
||||
export const useDataGrid = ({ tableSettingsId, defaultSortBy }: UseDataGridProps): UseDataGridReturn => {
|
||||
const [sortBy, setSortBy] = React.useState<SortingState>(defaultSortBy ?? []);
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||
const [pageInfo, setPageInfo] = React.useState<PaginationState>(getDefaultSettings(tableSettingsId));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tableSettingsId) {
|
||||
localStorage.setItem(`${tableSettingsId}.page`, String(pageInfo.pageIndex));
|
||||
if (tableSettingsId) localStorage.setItem(`${tableSettingsId}`, String(pageInfo.pageSize));
|
||||
}
|
||||
}, [pageInfo.pageIndex, pageInfo.pageSize]);
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
tableSettingsId,
|
||||
pageInfo,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
onPaginationChange: setPageInfo,
|
||||
}),
|
||||
[pageInfo, columnVisibility, sortBy],
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { compactDate, formatDaysAgo } from 'helpers/dateFormatting';
|
||||
import { compactDate, formatDaysAgo, formatDaysAgoCompact } from 'helpers/dateFormatting';
|
||||
|
||||
type Props = { date?: number; hidePrefix?: boolean };
|
||||
type Props = { date?: number; hidePrefix?: boolean; isCompact?: boolean };
|
||||
|
||||
const getDaysAgo = ({ date, hidePrefix }: { date?: number; hidePrefix?: boolean }) => {
|
||||
const getDaysAgo = ({ date, hidePrefix, isCompact }: { date?: number; hidePrefix?: boolean; isCompact?: boolean }) => {
|
||||
if (!date || date === 0) return '-';
|
||||
|
||||
if (isCompact)
|
||||
return hidePrefix ? formatDaysAgoCompact(date).split(' ').slice(1).join(' ') : formatDaysAgoCompact(date);
|
||||
return hidePrefix ? formatDaysAgo(date).split(' ').slice(1).join(' ') : formatDaysAgo(date);
|
||||
};
|
||||
|
||||
const FormattedDate = ({ date, hidePrefix }: Props) => (
|
||||
const FormattedDate = ({ date, hidePrefix, isCompact }: Props) => (
|
||||
<Tooltip hasArrow placement="top" label={compactDate(date ?? 0)}>
|
||||
{getDaysAgo({ date, hidePrefix })}
|
||||
{getDaysAgo({ date, hidePrefix, isCompact })}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { bytesString } from 'helpers/stringHelper';
|
||||
|
||||
const DataCell: React.FC<{ bytes?: number }> = ({ bytes }) => {
|
||||
type Props = { bytes?: number; showZerosAs?: string; boxProps?: BoxProps };
|
||||
const DataCell = ({ bytes, showZerosAs, boxProps }: Props) => {
|
||||
const data = useMemo(() => {
|
||||
if (bytes === undefined) return '-';
|
||||
|
||||
if (showZerosAs && bytes === 0) return showZerosAs;
|
||||
return bytesString(bytes);
|
||||
}, [bytes]);
|
||||
|
||||
return <div>{data}</div>;
|
||||
return <Box {...boxProps}>{data}</Box>;
|
||||
};
|
||||
|
||||
export default React.memo(DataCell);
|
||||
|
||||
17
src/components/TableCells/DurationCell/index.tsx
Normal file
17
src/components/TableCells/DurationCell/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { secondsDuration } from 'helpers/dateFormatting';
|
||||
|
||||
const DurationCell: React.FC<{ seconds?: number }> = ({ seconds }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (seconds === undefined) return '-';
|
||||
|
||||
return secondsDuration(seconds, t);
|
||||
}, [seconds]);
|
||||
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
export default React.memo(DurationCell);
|
||||
@@ -1,13 +1,20 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
|
||||
const NumberCell = ({ value }: { value?: number }) => {
|
||||
const data = useMemo(() => {
|
||||
type Props = {
|
||||
value?: number;
|
||||
boxProps?: BoxProps;
|
||||
showZerosAs?: string;
|
||||
};
|
||||
|
||||
const NumberCell = ({ value, boxProps, showZerosAs }: Props) => {
|
||||
const getData = () => {
|
||||
if (value === undefined) return '-';
|
||||
|
||||
if (value === 0 && showZerosAs) return showZerosAs;
|
||||
return value.toLocaleString();
|
||||
}, [value]);
|
||||
};
|
||||
|
||||
return <div>{data}</div>;
|
||||
return <Box {...boxProps}>{getData()}</Box>;
|
||||
};
|
||||
|
||||
export default React.memo(NumberCell);
|
||||
|
||||
@@ -126,3 +126,40 @@ export const dateForFilename = (dateString: number) => {
|
||||
date.getDate(),
|
||||
)}_${twoDigitNumber(date.getHours())}h${twoDigitNumber(date.getMinutes())}m${twoDigitNumber(date.getSeconds())}s`;
|
||||
};
|
||||
|
||||
export const formatDaysAgoCompact = (d1: number, d2: number = new Date().getTime()) => {
|
||||
try {
|
||||
const convertedTimestamp = unixToDateString(d1);
|
||||
const date = new Date(convertedTimestamp).getTime();
|
||||
const elapsed = date - d2;
|
||||
|
||||
for (const key of Object.keys(UNITS)) {
|
||||
if (
|
||||
Math.abs(elapsed) > UNITS[key as 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'] ||
|
||||
key === 'second'
|
||||
) {
|
||||
const result = RTF.format(
|
||||
Math.round(elapsed / UNITS[key as 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second']),
|
||||
key as Intl.RelativeTimeFormatUnit,
|
||||
);
|
||||
return result
|
||||
.replace(' years', 'y')
|
||||
.replace(' year', 'y')
|
||||
.replace(' months', 'm')
|
||||
.replace(' month', 'm')
|
||||
.replace(' days', 'd')
|
||||
.replace(' day', 'd')
|
||||
.replace(' hours', 'h')
|
||||
.replace(' hour', 'h')
|
||||
.replace(' minutes', 'm')
|
||||
.replace(' minute', 'm')
|
||||
.replace(' seconds', 's')
|
||||
.replace(' second', 's');
|
||||
}
|
||||
}
|
||||
|
||||
return compactDate(date);
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,23 +51,30 @@ export type DeviceWithStatus = {
|
||||
entity: string;
|
||||
firmware: string;
|
||||
fwUpdatePolicy: string;
|
||||
hasGPS: boolean;
|
||||
hasRADIUSSessions: number | boolean;
|
||||
ipAddress: string;
|
||||
lastConfigurationChange: number;
|
||||
lastConfigurationDownload: number;
|
||||
lastContact: number | string;
|
||||
lastFWUpdate: number;
|
||||
load: number;
|
||||
locale: string;
|
||||
location: string;
|
||||
macAddress: string;
|
||||
manufacturer: string;
|
||||
memoryUsed: number;
|
||||
messageCount: number;
|
||||
modified: number;
|
||||
notes: Note[];
|
||||
owner: string;
|
||||
sanity: number;
|
||||
started: number;
|
||||
restrictedDevice: boolean;
|
||||
rxBytes: number;
|
||||
serialNumber: string;
|
||||
subscriber: string;
|
||||
temperature: number;
|
||||
txBytes: number;
|
||||
venue: string;
|
||||
verifiedCertificate: string;
|
||||
|
||||
@@ -55,4 +55,5 @@ export const useGetTag = ({ serialNumber, onError }: { serialNumber?: string; on
|
||||
useQuery(['tag', serialNumber], () => getTag(serialNumber), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
onError,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
});
|
||||
|
||||
54
src/hooks/useContainerDimensions.ts
Normal file
54
src/hooks/useContainerDimensions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const roundToNearest = (num: number, precision: number) => {
|
||||
const factor = 1 / precision;
|
||||
return Math.round(num * factor) / factor;
|
||||
};
|
||||
|
||||
export type UseContainerDimensionsProps = {
|
||||
precision?: 10 | 100 | 1000;
|
||||
};
|
||||
|
||||
export const useContainerDimensions = ({ precision }: UseContainerDimensionsProps) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
const getDimensions = () => ({
|
||||
width: (ref && ref.current?.offsetWidth) || 0,
|
||||
height: (ref && ref.current?.offsetHeight) || 0,
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
const { width, height } = getDimensions();
|
||||
if (!precision) {
|
||||
if (dimensions.width !== width && dimensions.height !== height) setDimensions({ width, height });
|
||||
} else {
|
||||
const newDimensions = { width, height };
|
||||
newDimensions.width = roundToNearest(newDimensions.width, precision);
|
||||
newDimensions.height = roundToNearest(newDimensions.height, precision);
|
||||
if (newDimensions.width !== dimensions.width || newDimensions.height !== dimensions.height) {
|
||||
setDimensions(newDimensions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (ref.current) {
|
||||
handleResize();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [ref, dimensions]);
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
dimensions,
|
||||
ref,
|
||||
}),
|
||||
[dimensions.height, dimensions.width],
|
||||
);
|
||||
};
|
||||
@@ -28,7 +28,6 @@ const DeviceDetails = ({ serialNumber }: Props) => {
|
||||
? getDevice.data?.devicePassword
|
||||
: 'openwifi',
|
||||
);
|
||||
|
||||
const getPassword = () => {
|
||||
if (!getDevice.data) return '-';
|
||||
if (isShowingPassword) {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Icon,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Wrapper } from '@googlemaps/react-wrapper';
|
||||
import { Globe } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,13 +21,15 @@ import { useGetDeviceLastStats } from 'hooks/Network/Statistics';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
isCompact?: boolean;
|
||||
};
|
||||
|
||||
const LocationDisplayButton = ({ serialNumber }: Props) => {
|
||||
const LocationDisplayButton = ({ serialNumber, isCompact }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' });
|
||||
const getLastStats = useGetDeviceLastStats({ serialNumber });
|
||||
const iconColor = useColorModeValue('blue.500', 'blue.200');
|
||||
|
||||
const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => {
|
||||
if (!getLastStats.data?.gps) return undefined;
|
||||
@@ -38,9 +50,15 @@ const LocationDisplayButton = ({ serialNumber }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
|
||||
{t('locations.view_gps')}
|
||||
</Button>
|
||||
{isCompact ? (
|
||||
<Tooltip label={t('locations.view_gps')}>
|
||||
<Icon as={Globe} boxSize={6} onClick={onOpen} color={iconColor} cursor="pointer" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
|
||||
{t('locations.view_gps')}
|
||||
</Button>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}>
|
||||
<Box w="100%" h="100%">
|
||||
<Flex mb={4}>
|
||||
|
||||
19
src/pages/Devices/ListCard/GpsCell.tsx
Normal file
19
src/pages/Devices/ListCard/GpsCell.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Center } from '@chakra-ui/react';
|
||||
import { DeviceWithStatus } from 'hooks/Network/Devices';
|
||||
import LocationDisplayButton from 'pages/Device/LocationDisplayButton';
|
||||
|
||||
type Props = {
|
||||
device: DeviceWithStatus;
|
||||
};
|
||||
const DeviceTableGpsCell = ({ device }: Props) => {
|
||||
if (!device.hasGPS) return <Center>-</Center>;
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<LocationDisplayButton serialNumber={device.serialNumber} isCompact />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceTableGpsCell;
|
||||
40
src/pages/Devices/ListCard/ProvisioningStatusCell.tsx
Normal file
40
src/pages/Devices/ListCard/ProvisioningStatusCell.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from '@chakra-ui/react';
|
||||
import { DeviceWithStatus } from 'hooks/Network/Devices';
|
||||
import { useGetProvUi } from 'hooks/Network/Endpoints';
|
||||
import { useGetTag } from 'hooks/Network/Inventory';
|
||||
|
||||
type Props = {
|
||||
device: DeviceWithStatus;
|
||||
};
|
||||
const ProvisioningStatusCell = ({ device }: Props) => {
|
||||
const getProvUi = useGetProvUi();
|
||||
const getTag = useGetTag({ serialNumber: device.serialNumber });
|
||||
const goToProvUi = (dir: string) => `${getProvUi.data}/#/${dir}`;
|
||||
|
||||
if (getTag.data?.extendedInfo?.entity?.name) {
|
||||
return (
|
||||
<Link isExternal href={goToProvUi(`entity/${getTag.data?.entity}`)}>
|
||||
{getTag.data?.extendedInfo?.entity?.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (getTag.data?.extendedInfo?.venue?.name) {
|
||||
return (
|
||||
<Link isExternal href={goToProvUi(`venue/${getTag.data?.venue}`)}>
|
||||
{getTag.data?.extendedInfo?.venue?.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (getTag.data?.extendedInfo?.subscriber?.name) {
|
||||
return (
|
||||
<Link isExternal href={goToProvUi(`venue/${getTag.data?.subscriber}`)}>
|
||||
{getTag.data?.extendedInfo?.subscriber?.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>-</span>;
|
||||
};
|
||||
|
||||
export default ProvisioningStatusCell;
|
||||
17
src/pages/Devices/ListCard/Uptime.tsx
Normal file
17
src/pages/Devices/ListCard/Uptime.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import DurationCell from 'components/TableCells/DurationCell';
|
||||
import { DeviceWithStatus } from 'hooks/Network/Devices';
|
||||
|
||||
type Props = {
|
||||
device: DeviceWithStatus;
|
||||
};
|
||||
const DeviceUptimeCell = ({ device }: Props) => {
|
||||
if (!device.connected || device.started === 0) return <span>-</span>;
|
||||
|
||||
// Get the uptime in seconds from device.started which is UNIX timestamp
|
||||
const uptime = Math.floor(Date.now() / 1000 - device.started);
|
||||
|
||||
return <DurationCell seconds={uptime} />;
|
||||
};
|
||||
|
||||
export default DeviceUptimeCell;
|
||||
@@ -1,20 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { LockSimple } from 'phosphor-react';
|
||||
import { Box, Center, Image, Link, Tag, TagLabel, TagRightIcon, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
CheckCircle,
|
||||
Heart,
|
||||
HeartBreak,
|
||||
LockSimple,
|
||||
ThermometerCold,
|
||||
ThermometerHot,
|
||||
WarningCircle,
|
||||
} from 'phosphor-react';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Actions from './Actions';
|
||||
import DeviceListFirmwareButton from './FirmwareButton';
|
||||
import DeviceTableGpsCell from './GpsCell';
|
||||
import AP from './icons/AP.png';
|
||||
import IOT from './icons/IOT.png';
|
||||
import MESH from './icons/MESH.png';
|
||||
import SWITCH from './icons/SWITCH.png';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import ProvisioningStatusCell from './ProvisioningStatusCell';
|
||||
import DeviceUptimeCell from './Uptime';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
|
||||
import { DataTable } from 'components/DataTables/DataTable';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
import DeviceSearchBar from 'components/DeviceSearchBar';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { ConfigureModal } from 'components/Modals/ConfigureModal';
|
||||
@@ -30,8 +39,16 @@ import DataCell from 'components/TableCells/DataCell';
|
||||
import NumberCell from 'components/TableCells/NumberCell';
|
||||
import { DeviceWithStatus, useGetDeviceCount, useGetDevices } from 'hooks/Network/Devices';
|
||||
import { FirmwareAgeResponse, useGetFirmwareAges } from 'hooks/Network/Firmware';
|
||||
import { Column, PageInfo } from 'models/Table';
|
||||
|
||||
const fourDigitNumber = (v: number) => {
|
||||
if (v === 0) {
|
||||
return '0.00';
|
||||
}
|
||||
const str = v.toString();
|
||||
const fourthChar = str.charAt(3);
|
||||
if (fourthChar === '.') return `${str.slice(0, 3)}`;
|
||||
return `${str.slice(0, 4)}`;
|
||||
};
|
||||
const ICON_STYLE = { width: '24px', height: '24px', borderRadius: '20px' };
|
||||
|
||||
const ICONS = {
|
||||
@@ -52,8 +69,6 @@ const DeviceListCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [serialNumber, setSerialNumber] = React.useState<string>('');
|
||||
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||
const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined);
|
||||
const scanModalProps = useDisclosure();
|
||||
const resetModalProps = useDisclosure();
|
||||
const upgradeModalProps = useDisclosure();
|
||||
@@ -63,9 +78,13 @@ const DeviceListCard = () => {
|
||||
const configureModalProps = useDisclosure();
|
||||
const rebootModalProps = useDisclosure();
|
||||
const scriptModal = useScriptModal();
|
||||
const tableController = useDataGrid({ tableSettingsId: 'gateway.devices.table' });
|
||||
const getCount = useGetDeviceCount({ enabled: true });
|
||||
const getDevices = useGetDevices({
|
||||
pageInfo,
|
||||
pageInfo: {
|
||||
limit: tableController.pageInfo.pageSize,
|
||||
index: tableController.pageInfo.pageIndex,
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
const getAges = useGetFirmwareAges({
|
||||
@@ -171,7 +190,7 @@ const DeviceListCard = () => {
|
||||
const dataCell = React.useCallback(
|
||||
(v: number) => (
|
||||
<Box textAlign="right">
|
||||
<DataCell bytes={v} />
|
||||
<DataCell bytes={v} showZerosAs="-" />
|
||||
</Box>
|
||||
),
|
||||
[],
|
||||
@@ -185,12 +204,27 @@ const DeviceListCard = () => {
|
||||
),
|
||||
[],
|
||||
);
|
||||
const compactDateCell = React.useCallback(
|
||||
(v?: number | string, hidePrefix?: boolean) =>
|
||||
v !== undefined && typeof v === 'number' && v !== 0 ? (
|
||||
<FormattedDate date={v as number} hidePrefix={hidePrefix} isCompact />
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
[],
|
||||
);
|
||||
const firmwareCell = React.useCallback(
|
||||
(device: DeviceWithStatus & { age?: FirmwareAgeResponse }) => (
|
||||
<DeviceListFirmwareButton device={device} age={device.age} onOpenUpgrade={onOpenUpgradeModal} />
|
||||
),
|
||||
[getAges],
|
||||
);
|
||||
const provCell = React.useCallback(
|
||||
(device: DeviceWithStatus) =>
|
||||
device.subscriber || device.entity || device.venue ? <ProvisioningStatusCell device={device} /> : '-',
|
||||
[],
|
||||
);
|
||||
const uptimeCell = React.useCallback((device: DeviceWithStatus) => <DeviceUptimeCell device={device} />, []);
|
||||
const localeCell = React.useCallback(
|
||||
(device: DeviceWithStatus) => (
|
||||
<Tooltip label={`${device.locale !== '' ? `${device.locale} - ` : ''}${device.ipAddress}`} placement="top">
|
||||
@@ -198,13 +232,25 @@ const DeviceListCard = () => {
|
||||
{device.locale !== '' && device.ipAddress !== '' && (
|
||||
<ReactCountryFlag style={ICON_STYLE} countryCode={device.locale} svg />
|
||||
)}
|
||||
{` ${device.ipAddress}`}
|
||||
{` ${device.ipAddress.length > 0 ? device.ipAddress : '-'}`}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
),
|
||||
[],
|
||||
);
|
||||
const numberCell = React.useCallback((v?: number) => <NumberCell value={v !== undefined ? v : 0} />, []);
|
||||
const gpsCell = React.useCallback((device: DeviceWithStatus) => <DeviceTableGpsCell device={device} />, []);
|
||||
const numberCell = React.useCallback(
|
||||
(v?: number) => (
|
||||
<NumberCell
|
||||
value={v !== undefined ? v : 0}
|
||||
showZerosAs="-"
|
||||
boxProps={{
|
||||
textAlign: 'right',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
const actionCell = React.useCallback(
|
||||
(device: DeviceWithStatus) => (
|
||||
<Actions
|
||||
@@ -224,135 +270,358 @@ const DeviceListCard = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
const columns: Column<DeviceWithStatus>[] = React.useMemo(
|
||||
(): Column<DeviceWithStatus>[] => [
|
||||
const sanityCell = React.useCallback((device: DeviceWithStatus) => {
|
||||
if (!device.connected) return <Center>-</Center>;
|
||||
|
||||
let colorScheme = 'red';
|
||||
if (device.sanity >= 80) colorScheme = 'yellow';
|
||||
if (device.sanity === 100) colorScheme = 'green';
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Tag borderRadius="full" variant="subtle" colorScheme={colorScheme}>
|
||||
<TagLabel>{device.sanity}%</TagLabel>
|
||||
<TagRightIcon marginStart="0.1rem" as={colorScheme === 'green' ? Heart : HeartBreak} />
|
||||
</Tag>
|
||||
</Center>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const loadCell = React.useCallback((device: DeviceWithStatus) => {
|
||||
if (!device.connected) return <Center>-</Center>;
|
||||
|
||||
let colorScheme = 'red';
|
||||
if (device.load <= 20) colorScheme = 'yellow';
|
||||
if (device.load <= 5) colorScheme = 'green';
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Tag borderRadius="full" variant="subtle" colorScheme={colorScheme}>
|
||||
<TagLabel>{fourDigitNumber(device.load)}%</TagLabel>
|
||||
<TagRightIcon marginStart="0.1rem" as={colorScheme === 'green' ? CheckCircle : WarningCircle} />
|
||||
</Tag>
|
||||
</Center>
|
||||
);
|
||||
}, []);
|
||||
const memoryCell = React.useCallback((device: DeviceWithStatus) => {
|
||||
if (!device.connected) return <Center>-</Center>;
|
||||
|
||||
let colorScheme = 'red';
|
||||
if (device.memoryUsed <= 85) colorScheme = 'yellow';
|
||||
if (device.memoryUsed <= 60) colorScheme = 'green';
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Tag borderRadius="full" variant="subtle" colorScheme={colorScheme}>
|
||||
<TagLabel>{fourDigitNumber(device.memoryUsed)}%</TagLabel>
|
||||
<TagRightIcon marginStart="0.1rem" as={colorScheme === 'green' ? CheckCircle : WarningCircle} />
|
||||
</Tag>
|
||||
</Center>
|
||||
);
|
||||
}, []);
|
||||
const temperatureCell = React.useCallback((device: DeviceWithStatus) => {
|
||||
if (!device.connected || device.temperature === 0) return <Center>-</Center>;
|
||||
|
||||
let colorScheme = 'red';
|
||||
if (device.temperature <= 85) colorScheme = 'yellow';
|
||||
if (device.temperature <= 75) colorScheme = 'green';
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Tag borderRadius="full" variant="subtle" colorScheme={colorScheme}>
|
||||
<TagLabel>{fourDigitNumber(device.temperature)}°C</TagLabel>
|
||||
<TagRightIcon marginStart="0.1rem" as={colorScheme === 'green' ? ThermometerCold : ThermometerHot} />
|
||||
</Tag>
|
||||
</Center>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const columns: DataGridColumn<DeviceWithStatus>[] = React.useMemo(
|
||||
(): DataGridColumn<DeviceWithStatus>[] => [
|
||||
{
|
||||
id: 'badge',
|
||||
Header: '',
|
||||
Footer: '',
|
||||
accessor: 'badge',
|
||||
Cell: (v) => badgeCell(v.cell.row.original),
|
||||
customWidth: '35px',
|
||||
alwaysShow: true,
|
||||
disableSortBy: true,
|
||||
header: '',
|
||||
footer: '',
|
||||
accessorKey: 'badge',
|
||||
cell: (v) => badgeCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '35px',
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'serialNumber',
|
||||
Header: t('inventory.serial_number'),
|
||||
Footer: '',
|
||||
accessor: 'serialNumber',
|
||||
Cell: (v) => serialCell(v.cell.row.original),
|
||||
alwaysShow: true,
|
||||
customMaxWidth: '200px',
|
||||
customWidth: '130px',
|
||||
customMinWidth: '130px',
|
||||
disableSortBy: true,
|
||||
header: t('inventory.serial_number'),
|
||||
footer: '',
|
||||
accessorKey: 'serialNumber',
|
||||
cell: (v) => serialCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
alwaysShow: true,
|
||||
customMaxWidth: '200px',
|
||||
customWidth: '130px',
|
||||
customMinWidth: '130px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sanity',
|
||||
header: t('devices.sanity'),
|
||||
footer: '',
|
||||
cell: (v) => sanityCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
headerStyleProps: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: t('analytics.memory'),
|
||||
footer: '',
|
||||
cell: (v) => memoryCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
headerStyleProps: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'load',
|
||||
header: 'Load',
|
||||
footer: '',
|
||||
cell: (v) => loadCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
headerOptions: {
|
||||
tooltip: 'CPU Load',
|
||||
},
|
||||
headerStyleProps: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'temperature',
|
||||
header: 'Temp',
|
||||
footer: '',
|
||||
cell: (v) => temperatureCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
headerOptions: {
|
||||
tooltip: t('analytics.temperature'),
|
||||
},
|
||||
columnSelectorOptions: {
|
||||
label: t('analytics.temperature'),
|
||||
},
|
||||
headerStyleProps: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'firmware',
|
||||
Header: t('commands.revision'),
|
||||
Footer: '',
|
||||
accessor: 'firmware',
|
||||
Cell: (v) => firmwareCell(v.cell.row.original),
|
||||
stopPropagation: true,
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: t('commands.revision'),
|
||||
footer: '',
|
||||
accessorKey: 'firmware',
|
||||
cell: (v) => firmwareCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
stopPropagation: true,
|
||||
customWidth: '50px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'compatible',
|
||||
Header: t('common.type'),
|
||||
Footer: '',
|
||||
accessor: 'compatible',
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: t('common.type'),
|
||||
footer: '',
|
||||
accessorKey: 'compatible',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'IP',
|
||||
Header: 'IP',
|
||||
Footer: '',
|
||||
accessor: 'IP',
|
||||
Cell: (v) => localeCell(v.cell.row.original),
|
||||
disableSortBy: true,
|
||||
header: 'IP',
|
||||
footer: '',
|
||||
accessorKey: 'IP',
|
||||
cell: (v) => localeCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customMaxWidth: '140px',
|
||||
customWidth: '130px',
|
||||
customMinWidth: '130px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'provisioning',
|
||||
header: 'Provisioning',
|
||||
footer: '',
|
||||
cell: (v) => provCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
stopPropagation: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'radius',
|
||||
header: 'Rad',
|
||||
footer: '',
|
||||
accessorKey: 'hasRADIUSSessions',
|
||||
cell: (v) =>
|
||||
numberCell(
|
||||
typeof v.cell.row.original.hasRADIUSSessions === 'number' ? v.cell.row.original.hasRADIUSSessions : 0,
|
||||
),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
columnSelectorOptions: {
|
||||
label: 'Radius Sessions',
|
||||
},
|
||||
headerOptions: {
|
||||
tooltip: 'Current active radius sessions',
|
||||
},
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'GPS',
|
||||
header: 'GPS',
|
||||
footer: '',
|
||||
cell: (v) => gpsCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '32px',
|
||||
stopPropagation: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
header: t('system.uptime'),
|
||||
footer: '',
|
||||
cell: (v) => uptimeCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'lastContact',
|
||||
Header: t('analytics.last_contact'),
|
||||
Footer: '',
|
||||
accessor: 'lastContact',
|
||||
Cell: (v) => dateCell(v.cell.row.original.lastContact),
|
||||
disableSortBy: true,
|
||||
header: t('analytics.last_contact'),
|
||||
footer: '',
|
||||
accessorKey: 'lastContact',
|
||||
cell: (v) => dateCell(v.cell.row.original.lastContact),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'lastFWUpdate',
|
||||
Header: t('controller.devices.last_upgrade'),
|
||||
Footer: '',
|
||||
accessor: 'lastFWUpdate',
|
||||
Cell: (v) => dateCell(v.cell.row.original.lastFWUpdate),
|
||||
disableSortBy: true,
|
||||
header: t('controller.devices.last_upgrade'),
|
||||
footer: '',
|
||||
accessorKey: 'lastFWUpdate',
|
||||
cell: (v) => dateCell(v.cell.row.original.lastFWUpdate),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'rxBytes',
|
||||
Header: 'Rx',
|
||||
Footer: '',
|
||||
accessor: 'rxBytes',
|
||||
Cell: (v) => dataCell(v.cell.row.original.rxBytes),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: 'Rx',
|
||||
footer: '',
|
||||
accessorKey: 'rxBytes',
|
||||
cell: (v) => dataCell(v.cell.row.original.rxBytes),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '50px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'txBytes',
|
||||
Header: 'Tx',
|
||||
Footer: '',
|
||||
accessor: 'txBytes',
|
||||
Cell: (v) => dataCell(v.cell.row.original.txBytes),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: 'Tx',
|
||||
footer: '',
|
||||
accessorKey: 'txBytes',
|
||||
cell: (v) => dataCell(v.cell.row.original.txBytes),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2G',
|
||||
Header: '2G',
|
||||
Footer: '',
|
||||
accessor: 'associations_2G',
|
||||
Cell: (v) => numberCell(v.cell.row.original.associations_2G),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: '2G',
|
||||
footer: '',
|
||||
accessorKey: 'associations_2G',
|
||||
cell: (v) => numberCell(v.cell.row.original.associations_2G),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5G',
|
||||
Header: '5G',
|
||||
Footer: '',
|
||||
accessor: 'associations_5G',
|
||||
Cell: (v) => numberCell(v.cell.row.original.associations_5G),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: '5G',
|
||||
footer: '',
|
||||
accessorKey: 'associations_5G',
|
||||
cell: (v) => numberCell(v.cell.row.original.associations_5G),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6G',
|
||||
Header: '6G',
|
||||
Footer: '',
|
||||
accessor: 'associations_6G',
|
||||
Cell: (v) => numberCell(v.cell.row.original.associations_6G),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: '6G',
|
||||
footer: '',
|
||||
accessorKey: 'associations_6G',
|
||||
cell: (v) => numberCell(v.cell.row.original.associations_6G),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'certificateExpiryDate',
|
||||
Header: t('devices.certificate_expiry'),
|
||||
Footer: '',
|
||||
accessor: 'certificateExpiryDate',
|
||||
Cell: (v) => dateCell(v.cell.row.original.certificateExpiryDate, true),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
header: 'Exp',
|
||||
footer: '',
|
||||
accessorKey: 'certificateExpiryDate',
|
||||
cell: (v) => compactDateCell(v.cell.row.original.certificateExpiryDate, true),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
columnSelectorOptions: {
|
||||
label: 'Certificate Expiry',
|
||||
},
|
||||
headerOptions: {
|
||||
tooltip: 'Certificate Expiry Date',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'actions',
|
||||
Cell: (v) => actionCell(v.cell.row.original),
|
||||
customWidth: '50px',
|
||||
alwaysShow: true,
|
||||
disableSortBy: true,
|
||||
header: t('common.actions'),
|
||||
footer: '',
|
||||
accessorKey: 'actions',
|
||||
cell: (v) => actionCell(v.cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '50px',
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, firmwareCell],
|
||||
@@ -368,50 +637,27 @@ const DeviceListCard = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{getCount.data?.count} {t('devices.title')}
|
||||
</Heading>
|
||||
<DeviceSearchBar />
|
||||
<Spacer />
|
||||
<ColumnPicker
|
||||
columns={columns as Column<unknown>[]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="gateway.devices.table.hiddenColumns"
|
||||
/>
|
||||
<RefreshButton
|
||||
onClick={() => {
|
||||
getDevices.refetch();
|
||||
getCount.refetch();
|
||||
}}
|
||||
isCompact
|
||||
ml={2}
|
||||
isFetching={getCount.isFetching || getDevices.isFetching}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable<DeviceWithStatus>
|
||||
columns={
|
||||
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
|
||||
id: string;
|
||||
Header: string;
|
||||
Footer: string;
|
||||
accessor: string;
|
||||
}[]
|
||||
}
|
||||
data={data ?? []}
|
||||
<DataGrid<DeviceWithStatus>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: `${getCount.data?.count} ${t('devices.title')}`,
|
||||
objectListed: t('devices.title'),
|
||||
leftContent: <DeviceSearchBar />,
|
||||
}}
|
||||
columns={columns}
|
||||
data={data}
|
||||
isLoading={getCount.isFetching || getDevices.isFetching}
|
||||
isManual
|
||||
hiddenColumns={hiddenColumns}
|
||||
obj={t('devices.title')}
|
||||
count={getCount.data?.count || 0}
|
||||
// @ts-ignore
|
||||
setPageInfo={setPageInfo}
|
||||
saveSettingsId="gateway.devices.table"
|
||||
onRowClick={(device) => navigate(`devices/${device.serialNumber}`)}
|
||||
isRowClickable={() => true}
|
||||
options={{
|
||||
count: getCount.data?.count,
|
||||
isManual: true,
|
||||
onRowClick: (device) => () => navigate(`devices/${device.serialNumber}`),
|
||||
refetch: () => {
|
||||
getDevices.refetch();
|
||||
getCount.refetch();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
Reference in New Issue
Block a user