[WIFI-12360] Custom script run fix

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-03-07 20:08:40 +01:00
parent 64f3ee797e
commit db4dfc93e8
17 changed files with 332 additions and 89 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ucentral-client", "name": "ucentral-client",
"version": "2.9.0(13)", "version": "2.9.0(16)",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ucentral-client", "name": "ucentral-client",
"version": "2.9.0(13)", "version": "2.9.0(16)",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@chakra-ui/icons": "^2.0.11", "@chakra-ui/icons": "^2.0.11",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ucentral-client", "name": "ucentral-client",
"version": "2.9.0(13)", "version": "2.9.0(16)",
"description": "", "description": "",
"private": true, "private": true,
"main": "index.tsx", "main": "index.tsx",

View File

@@ -44,12 +44,14 @@ const defaultProps = {
sortBy: [], sortBy: [],
}; };
export type DataTableProps = { export type DataTableProps<TValue> = {
columns: readonly Column<object>[]; columns: Column<TValue>[];
data: object[]; data: TValue[];
count?: number; count?: number;
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>; setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
isLoading?: boolean; isLoading?: boolean;
onRowClick?: (row: TValue) => void;
isRowClickable?: (row: TValue) => boolean;
obj?: string; obj?: string;
sortBy?: { id: string; desc: boolean }[]; sortBy?: { id: string; desc: boolean }[];
hiddenColumns?: string[]; hiddenColumns?: string[];
@@ -68,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> &
state: UsePaginationState<T>; state: UsePaginationState<T>;
}; };
const _DataTable = ({ const _DataTable = <TValue extends object>({
columns, columns,
data, data,
isLoading, isLoading,
@@ -84,7 +86,9 @@ const _DataTable = ({
isManual, isManual,
saveSettingsId, saveSettingsId,
showAllRows, showAllRows,
}: DataTableProps) => { onRowClick,
isRowClickable,
}: DataTableProps<TValue>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const textColor = useColorModeValue('gray.700', 'white'); const textColor = useColorModeValue('gray.700', 'white');
@@ -143,7 +147,7 @@ const _DataTable = ({
}, },
useSortBy, useSortBy,
usePagination, usePagination,
) as TableInstanceWithHooks<object>; ) as TableInstanceWithHooks<TValue>;
const handleGoToPage = (newPage: number) => { const handleGoToPage = (newPage: number) => {
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage)); if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
@@ -260,8 +264,10 @@ const _DataTable = ({
</Thead> </Thead>
{data.length > 0 && ( {data.length > 0 && (
<Tbody {...getTableBodyProps()}> <Tbody {...getTableBodyProps()}>
{page.map((row: Row) => { {page.map((row: Row<TValue>) => {
prepareRow(row); prepareRow(row);
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
return ( return (
<Tr <Tr
{...row.getRowProps()} {...row.getRowProps()}
@@ -269,6 +275,7 @@ const _DataTable = ({
_hover={{ _hover={{
backgroundColor: hoveredRowBg, backgroundColor: hoveredRowBg,
}} }}
onClick={onClick}
> >
{ {
// @ts-ignore // @ts-ignore
@@ -288,8 +295,26 @@ const _DataTable = ({
fontSize="14px" fontSize="14px"
// @ts-ignore // @ts-ignore
textAlign={cell.column.isCentered ? 'center' : undefined} textAlign={cell.column.isCentered ? 'center' : undefined}
fontFamily={
// @ts-ignore // @ts-ignore
fontFamily={cell.column.isMonospace ? 'monospace' : undefined} cell.column.isMonospace
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
: undefined
}
onClick={
// @ts-ignore
cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick)
? (e) => {
e.stopPropagation();
}
: undefined
}
cursor={
// @ts-ignore
!cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick
? 'pointer'
: undefined
}
> >
{cell.render('Cell')} {cell.render('Cell')}
</Td> </Td>
@@ -414,4 +439,4 @@ const _DataTable = ({
_DataTable.defaultProps = defaultProps; _DataTable.defaultProps = defaultProps;
export const DataTable = React.memo(_DataTable); export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable;

View File

@@ -249,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({
fontSize="14px" fontSize="14px"
// @ts-ignore // @ts-ignore
textAlign={cell.column.isCentered ? 'center' : undefined} textAlign={cell.column.isCentered ? 'center' : undefined}
fontFamily={
// @ts-ignore // @ts-ignore
fontFamily={cell.column.isMonospace ? 'monospace' : undefined} cell.column.isMonospace
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
: undefined
}
> >
{cell.render('Cell')} {cell.render('Cell')}
</Td> </Td>

View File

@@ -5,17 +5,22 @@ import {
AlertIcon, AlertIcon,
AlertTitle, AlertTitle,
Box, Box,
Button,
Flex,
FormControl, FormControl,
FormErrorMessage, FormErrorMessage,
FormLabel, FormLabel,
Textarea, Textarea,
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ClipboardText } from 'phosphor-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SaveButton } from '../../Buttons/SaveButton'; import { SaveButton } from '../../Buttons/SaveButton';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { FileInputButton } from 'components/Buttons/FileInputButton'; import { FileInputButton } from 'components/Buttons/FileInputButton';
import { useConfigureDevice } from 'hooks/Network/Commands'; import { useConfigureDevice } from 'hooks/Network/Commands';
import { useGetDevice } from 'hooks/Network/Devices';
import { AxiosError } from 'models/Axios';
export type ConfigureModalProps = { export type ConfigureModalProps = {
serialNumber: string; serialNumber: string;
@@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
const { t } = useTranslation(); const { t } = useTranslation();
const toast = useToast(); const toast = useToast();
const configure = useConfigureDevice({ serialNumber }); const configure = useConfigureDevice({ serialNumber });
const getDevice = useGetDevice({ serialNumber });
const [newConfig, setNewConfig] = React.useState(''); const [newConfig, setNewConfig] = React.useState('');
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setNewConfig(e.target.value); setNewConfig(e.target.value);
}, []); }, []);
const onImportConfiguration = () => {
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
};
const isValid = React.useMemo(() => { const isValid = React.useMemo(() => {
try { try {
JSON.parse(newConfig); JSON.parse(newConfig);
@@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
modalProps.onClose(); modalProps.onClose();
}, },
}); });
} catch (e) { } catch (e) {}
// console.log(e);
}
}; };
return ( return (
@@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
<AlertIcon /> <AlertIcon />
<Box> <Box>
<AlertTitle>{t('common.error')}</AlertTitle> <AlertTitle>{t('common.error')}</AlertTitle>
{ <AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription>
// @ts-ignore
<AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription>
}
</Box> </Box>
</Alert> </Alert>
)} )}
@@ -92,7 +98,8 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
</Alert> </Alert>
<FormControl isInvalid={!isValid && newConfig.length > 0}> <FormControl isInvalid={!isValid && newConfig.length > 0}>
<FormLabel>{t('configurations.one')}</FormLabel> <FormLabel>{t('configurations.one')}</FormLabel>
<Box mb={2} w="240px"> <Flex mb={2}>
<Box w="240px">
<FileInputButton <FileInputButton
value={newConfig} value={newConfig}
setValue={(v) => setNewConfig(v)} setValue={(v) => setNewConfig(v)}
@@ -101,6 +108,15 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
isStringFile isStringFile
/> />
</Box> </Box>
<Button
rightIcon={<ClipboardText size={20} />}
onClick={onImportConfiguration}
hidden={!getDevice.data}
ml={2}
>
Current Configuration
</Button>
</Flex>
<Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} /> <Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} />
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage> <FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
</FormControl> </FormControl>

View File

@@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
let requestData: { let requestData: {
[k: string]: unknown; [k: string]: unknown;
serialNumber: string; serialNumber: string;
script?: string;
timeout?: number | undefined; timeout?: number | undefined;
} = data; } = data;
if (requestData.script) {
requestData.script = btoa(requestData.script);
}
if (selectedScript === 'diagnostics') { if (selectedScript === 'diagnostics') {
requestData = { requestData = {
serialNumber: device?.serialNumber ?? '', serialNumber: device?.serialNumber ?? '',
@@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2)); setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']); queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']);
}, },
onError: (e) => {
if (axios.isAxiosError(e) && e.response?.data?.ErrorDescription) {
toast({
id: 'script-update-error',
title: t('common.error'),
description: e.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
}); });
if (!waitForResponse) { if (!waitForResponse) {
toast({ toast({

View File

@@ -110,6 +110,18 @@ export const useDeleteCommand = () => {
}); });
}; };
export const useGetSingleCommandHistory = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
useQuery(
['commands', serialNumber, commandId],
() =>
axiosGw
.get(`command/${commandId}?serialNumber=${serialNumber}`)
.then((response) => response.data as DeviceCommandHistory),
{
enabled: serialNumber !== undefined && serialNumber !== '' && commandId !== undefined && commandId !== '',
},
);
export type EventQueueResponse = { export type EventQueueResponse = {
UUID: string; UUID: string;
attachFile: number; attachFile: number;
@@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
queryClient.invalidateQueries(['commands', serialNumber]); queryClient.invalidateQueries(['commands', serialNumber]);
}, },
onError: (e) => { onError: (e) => {
queryClient.invalidateQueries(['commands', serialNumber]);
if (axios.isAxiosError(e)) { if (axios.isAxiosError(e)) {
toast({ toast({
id: 'script-error', id: 'script-error',

View File

@@ -1,6 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react'; import {
Box,
CircularProgress,
CircularProgressLabel,
Flex,
Heading,
Icon,
Text,
Tooltip,
VStack,
} from '@chakra-ui/react';
import { ArrowSquareDown, ArrowSquareUp, Clock } from 'phosphor-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card } from 'components/Containers/Card';
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
import { bytesString } from 'helpers/stringHelper'; import { bytesString } from 'helpers/stringHelper';
import { useGetDevicesStats } from 'hooks/Network/Devices'; import { useGetDevicesStats } from 'hooks/Network/Devices';
@@ -11,18 +23,19 @@ const SidebarDevices = () => {
const [lastTime, setLastTime] = React.useState<Date | undefined>(); const [lastTime, setLastTime] = React.useState<Date | undefined>();
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>(); const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
const getTime = () => { const time = React.useMemo(() => {
if (lastTime === undefined || lastUpdate === undefined) return null; if (lastTime === undefined || lastUpdate === undefined) return null;
const seconds = lastTime.getTime() - lastUpdate.getTime(); const seconds = lastTime.getTime() - lastUpdate.getTime();
return Math.max(0, Math.floor(seconds / 1000)); return Math.max(0, Math.floor(seconds / 1000));
}; }, [lastTime, lastUpdate]);
const refresh = () => { const circleColor = () => {
if (document.visibilityState !== 'hidden') { if (time === null) return 'gray.300';
getStats.refetch(); if (time < 10) return 'green.300';
} if (time < 30) return 'yellow.300';
return 'red.300';
}; };
React.useEffect(() => { React.useEffect(() => {
@@ -38,47 +51,60 @@ const SidebarDevices = () => {
}; };
}, []); }, []);
React.useEffect(() => {
document.addEventListener('visibilitychange', refresh);
return () => {
document.removeEventListener('visibilitychange', refresh);
};
}, []);
if (!getStats.data) return null; if (!getStats.data) return null;
return ( return (
<Card borderWidth="2px">
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
<CircularProgress
isIndeterminate
color={circleColor()}
position="absolute"
right="6px"
top="6px"
w="unset"
size={6}
thickness="14px"
>
<CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel>
</CircularProgress>
</Tooltip>
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
<Box position="absolute" right="8px" top="8px" w="unset" hidden>
<Clock size={16} />
</Box>
</Tooltip>
<VStack mb={-1}> <VStack mb={-1}>
<Flex flexDir="column" textAlign="center"> <Flex flexDir="column" textAlign="center">
<Heading size="md">{getStats.data.connectedDevices}</Heading> <Heading size="md">{getStats.data.connectedDevices}</Heading>
<Heading size="xs"> <Heading size="xs" display="flex" justifyContent="center">
{t('common.connected')} {t('devices.title')} <Text>
</Heading> {t('common.connected')} {t('devices.title')}{' '}
<Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400"> </Text>{' '}
({getStats.data.connectingDevices} {t('controller.devices.connecting')})
</Heading>
<Heading
size="xs"
mt={1}
fontStyle="italic"
fontWeight="normal"
color="gray.400"
hidden={getStats.data.rx === undefined || getStats.data.tx === undefined}
>
Rx: {bytesString(getStats.data.rx)}, Tx: {bytesString(getStats.data.tx)}
</Heading> </Heading>
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
<Heading size="md" textAlign="center" mt={2}> <Heading size="md" textAlign="center" mt={1}>
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
</Heading> </Heading>
</Tooltip> </Tooltip>
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading> <Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
<Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400"> <Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
{t('controller.stats.seconds_ago', { s: getTime() })} <Tooltip hasArrow label="Rx">
</Heading> <Flex alignItems="center" mr={1}>
<Icon as={ArrowSquareUp} weight="bold" boxSize={5} mt="1px" color="blue.400" />{' '}
{getStats.data.rx !== undefined ? bytesString(getStats.data.rx, 0) : '-'}
</Flex>
</Tooltip>
<Tooltip hasArrow label="Tx">
<Flex alignItems="center">
<Icon as={ArrowSquareDown} weight="bold" boxSize={5} mt="1px" color="purple.400" />{' '}
{getStats.data.tx !== undefined ? bytesString(getStats.data.tx, 0) : '-'}
</Flex>
</Tooltip>
</Flex>
</Flex> </Flex>
</VStack> </VStack>
</Card>
); );
}; };

View File

@@ -20,6 +20,7 @@ export interface Column<T> {
alwaysShow?: boolean; alwaysShow?: boolean;
Footer?: string; Footer?: string;
accessor?: string; accessor?: string;
stopPropagation?: boolean;
disableSortBy?: boolean; disableSortBy?: boolean;
hasPopover?: boolean; hasPopover?: boolean;
customMaxWidth?: string; customMaxWidth?: string;

View File

@@ -99,13 +99,14 @@ const DefaultConfigurationsList = () => {
<CardBody> <CardBody>
<Box overflowX="auto" w="100%"> <Box overflowX="auto" w="100%">
<LoadingOverlay isLoading={getConfigs.isFetching}> <LoadingOverlay isLoading={getConfigs.isFetching}>
<DataTable <DataTable<DefaultConfigurationResponse>
columns={columns as Column<object>[]} columns={columns}
saveSettingsId="firmware.table" saveSettingsId="firmware.table"
data={getConfigs.data ?? []} data={getConfigs.data ?? []}
obj={t('controller.configurations.title')} obj={t('controller.configurations.title')}
minHeight="200px" minHeight="200px"
sortBy={[{ id: 'name', desc: true }]} sortBy={[{ id: 'name', desc: true }]}
onRowClick={onViewDetails}
/> />
</LoadingOverlay> </LoadingOverlay>
</Box> </Box>

View File

@@ -35,7 +35,10 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
const parsedData = React.useMemo(() => { const parsedData = React.useMemo(() => {
if (!getStats.data && !getCustomStats.data) return undefined; if (!getStats.data && !getCustomStats.data) return undefined;
const data: Record<string, { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number }> = {}; const data: Record<
string,
{ tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number; removed?: boolean }
> = {};
const memoryData = { const memoryData = {
used: [] as number[], used: [] as number[],
buffered: [] as number[], buffered: [] as number[],
@@ -100,6 +103,18 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
maxRx: rxDelta, maxRx: rxDelta,
}; };
else { else {
if (data[inter.name] && !data[inter.name]?.removed && data[inter.name]?.recorded.length === 1) {
data[inter.name]?.tx.shift();
data[inter.name]?.rx.shift();
data[inter.name]?.recorded.shift();
// @ts-ignore
data[inter.name].maxRx = rxDelta;
// @ts-ignore
data[inter.name].maxTx = txDelta;
// @ts-ignore
data[inter.name].removed = true;
}
data[inter.name]?.rx.push(rxDelta); data[inter.name]?.rx.push(rxDelta);
data[inter.name]?.tx.push(txDelta); data[inter.name]?.tx.push(txDelta);
data[inter.name]?.recorded.push(stat.recorded); data[inter.name]?.recorded.push(stat.recorded);

View File

@@ -1,8 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { import {
Box, Box,
Button,
Center,
Heading, Heading,
HStack, HStack,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Portal, Portal,
Spacer, Spacer,
Tag, Tag,
@@ -12,10 +22,13 @@ import {
useBreakpoint, useBreakpoint,
useColorModeValue, useColorModeValue,
useDisclosure, useDisclosure,
useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import axios from 'axios';
import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react'; import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Masonry from 'react-masonry-css'; import Masonry from 'react-masonry-css';
import { useNavigate } from 'react-router-dom';
import DeviceDetails from './Details'; import DeviceDetails from './Details';
import DeviceLogsCard from './LogsCard'; import DeviceLogsCard from './LogsCard';
import DeviceNotes from './Notes'; import DeviceNotes from './Notes';
@@ -23,6 +36,7 @@ import RestrictionsCard from './RestrictionsCard';
import DeviceStatisticsCard from './StatisticsCard'; import DeviceStatisticsCard from './StatisticsCard';
import DeviceSummary from './Summary'; import DeviceSummary from './Summary';
import WifiAnalysisCard from './WifiAnalysis'; import WifiAnalysisCard from './WifiAnalysis';
import { DeleteButton } from 'components/Buttons/DeleteButton';
import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown'; import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown';
import { RefreshButton } from 'components/Buttons/RefreshButton'; import { RefreshButton } from 'components/Buttons/RefreshButton';
import { Card } from 'components/Containers/Card'; import { Card } from 'components/Containers/Card';
@@ -38,7 +52,7 @@ import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
import { TelemetryModal } from 'components/Modals/TelemetryModal'; import { TelemetryModal } from 'components/Modals/TelemetryModal';
import { TraceModal } from 'components/Modals/TraceModal'; import { TraceModal } from 'components/Modals/TraceModal';
import { WifiScanModal } from 'components/Modals/WifiScanModal'; import { WifiScanModal } from 'components/Modals/WifiScanModal';
import { useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices';
type Props = { type Props = {
serialNumber: string; serialNumber: string;
@@ -46,10 +60,16 @@ type Props = {
const DevicePageWrapper = ({ serialNumber }: Props) => { const DevicePageWrapper = ({ serialNumber }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const toast = useToast();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const navigate = useNavigate();
const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({
serialNumber,
});
const getDevice = useGetDevice({ serialNumber }); const getDevice = useGetDevice({ serialNumber });
const getStatus = useGetDeviceStatus({ serialNumber }); const getStatus = useGetDeviceStatus({ serialNumber });
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 }); const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 });
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
const scanModalProps = useDisclosure(); const scanModalProps = useDisclosure();
const resetModalProps = useDisclosure(); const resetModalProps = useDisclosure();
const eventQueueProps = useDisclosure(); const eventQueueProps = useDisclosure();
@@ -62,6 +82,35 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
// Sticky-top styles // Sticky-top styles
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
const handleDeleteClick = () =>
deleteDevice(serialNumber, {
onSuccess: () => {
toast({
id: `delete-device-success-${serialNumber}`,
title: t('common.success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
navigate('/devices');
},
onError: (e) => {
if (axios.isAxiosError(e)) {
toast({
id: `delete-device-error-${serialNumber}`,
title: t('common.error'),
description: e.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
});
const connectedTag = React.useMemo(() => { const connectedTag = React.useMemo(() => {
if (!getStatus.data) return null; if (!getStatus.data) return null;
@@ -148,6 +197,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<Spacer /> <Spacer />
<HStack spacing={2}> <HStack spacing={2}>
{breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />}
{getDevice?.data && ( {getDevice?.data && (
<DeviceActionDropdown <DeviceActionDropdown
// @ts-ignore // @ts-ignore
@@ -166,6 +216,33 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
isCompact isCompact
/> />
)} )}
<Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}>
<Box>
<PopoverTrigger>
<DeleteButton isCompact onClick={() => {}} />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} {serialNumber}
</PopoverHeader>
<PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onDeleteClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
<RefreshButton <RefreshButton
onClick={refresh} onClick={refresh}
isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching}
@@ -214,6 +291,33 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
size="md" size="md"
/> />
)} )}
<Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}>
<Box>
<PopoverTrigger>
<DeleteButton isCompact onClick={() => {}} />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} {serialNumber}
</PopoverHeader>
<PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onDeleteClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
<RefreshButton <RefreshButton
onClick={refresh} onClick={refresh}
isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching}

View File

@@ -3,6 +3,7 @@ import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chak
import { LockSimple } from 'phosphor-react'; import { LockSimple } from 'phosphor-react';
import ReactCountryFlag from 'react-country-flag'; import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Actions from './Actions'; import Actions from './Actions';
import DeviceListFirmwareButton from './FirmwareButton'; import DeviceListFirmwareButton from './FirmwareButton';
import AP from './icons/AP.png'; import AP from './icons/AP.png';
@@ -49,6 +50,7 @@ const BADGE_COLORS: Record<string, string> = {
const DeviceListCard = () => { const DeviceListCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [serialNumber, setSerialNumber] = React.useState<string>(''); const [serialNumber, setSerialNumber] = React.useState<string>('');
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined); const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined);
@@ -252,6 +254,7 @@ const DeviceListCard = () => {
Footer: '', Footer: '',
accessor: 'firmware', accessor: 'firmware',
Cell: (v) => firmwareCell(v.cell.row.original), Cell: (v) => firmwareCell(v.cell.row.original),
stopPropagation: true,
customWidth: '50px', customWidth: '50px',
disableSortBy: true, disableSortBy: true,
}, },
@@ -389,7 +392,7 @@ const DeviceListCard = () => {
</CardHeader> </CardHeader>
<CardBody p={4}> <CardBody p={4}>
<Box overflowX="auto" w="100%"> <Box overflowX="auto" w="100%">
<DataTable <DataTable<DeviceWithStatus>
columns={ columns={
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as { columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
id: string; id: string;
@@ -407,6 +410,8 @@ const DeviceListCard = () => {
// @ts-ignore // @ts-ignore
setPageInfo={setPageInfo} setPageInfo={setPageInfo}
saveSettingsId="gateway.devices.table" saveSettingsId="gateway.devices.table"
onRowClick={(device) => navigate(`devices/${device.serialNumber}`)}
isRowClickable={() => true}
/> />
</Box> </Box>
</CardBody> </CardBody>

View File

@@ -11,7 +11,15 @@ const UriCell = ({ uri }: Props) => {
return ( return (
<Box display="flex"> <Box display="flex">
<Button onClick={copy.onCopy} size="xs" colorScheme="teal" mr={2}> <Button
onClick={(e) => {
copy.onCopy();
e.stopPropagation();
}}
size="xs"
colorScheme="teal"
mr={2}
>
{copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')} {copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')}
</Button> </Button>
<Text my="auto">{uri}</Text> <Text my="auto">{uri}</Text>

View File

@@ -158,13 +158,14 @@ const FirmwareListTable = () => {
<CardBody p={4}> <CardBody p={4}>
<Box overflowX="auto" w="100%"> <Box overflowX="auto" w="100%">
<LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}> <LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}>
<DataTable <DataTable<Firmware>
columns={columns as Column<object>[]} columns={columns}
saveSettingsId="firmware.table" saveSettingsId="firmware.table"
data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []} data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []}
obj={t('analytics.firmware')} obj={t('analytics.firmware')}
minHeight="200px" minHeight="200px"
sortBy={[{ id: 'imageDate', desc: true }]} sortBy={[{ id: 'imageDate', desc: true }]}
onRowClick={(firmw) => handleViewDetailsClick(firmw)()}
/> />
</LoadingOverlay> </LoadingOverlay>
</Box> </Box>

View File

@@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react'; import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import ScriptTableActions from './Actions'; import ScriptTableActions from './Actions';
import CreateScriptButton from './CreateButton'; import CreateScriptButton from './CreateButton';
import useScriptsTable from './useScriptsTable'; import useScriptsTable from './useScriptsTable';
@@ -21,6 +22,7 @@ type Props = {
const ScriptTableCard = ({ onIdSelect }: Props) => { const ScriptTableCard = ({ onIdSelect }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { query, hiddenColumns } = useScriptsTable(); const { query, hiddenColumns } = useScriptsTable();
const { id } = useParams();
const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []); const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []);
const actionCell = React.useCallback( const actionCell = React.useCallback(
@@ -108,8 +110,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Box w="100%" h="300px" overflowY="auto"> <Box w="100%" h="300px" overflowY="auto">
<DataTable <DataTable<Script>
columns={columns as Column<object>[]} columns={columns}
saveSettingsId="apiKeys.profile.table" saveSettingsId="apiKeys.profile.table"
data={query.data ?? []} data={query.data ?? []}
obj={t('script.other')} obj={t('script.other')}
@@ -118,6 +120,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
hiddenColumns={hiddenColumns[0]} hiddenColumns={hiddenColumns[0]}
showAllRows showAllRows
hideControls hideControls
onRowClick={(script) => onIdSelect(script.id)}
isRowClickable={(script) => script.id !== id}
/> />
</Box> </Box>
</CardBody> </CardBody>

View File

@@ -25,10 +25,10 @@ const UserTable = () => {
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure(); const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers(); const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
const openEditModal = (editUser: User) => { const openEditModal = React.useCallback((editUser: User) => {
setEditId(editUser.id); setEditId(editUser.id);
openEdit(); openEdit();
}; }, []);
const memoizedActions = useCallback( const memoizedActions = useCallback(
(userActions: User) => ( (userActions: User) => (
@@ -99,7 +99,7 @@ const UserTable = () => {
]; ];
if (user?.userRole !== 'csr') if (user?.userRole !== 'csr')
baseColumns.push({ baseColumns.push({
id: 'user', id: 'actions',
Header: t('common.actions'), Header: t('common.actions'),
Footer: '', Footer: '',
accessor: 'Id', accessor: 'Id',
@@ -139,14 +139,15 @@ const UserTable = () => {
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Box overflowX="auto" w="100%"> <Box overflowX="auto" w="100%">
<DataTable <DataTable<User>
columns={columns as Column<object>[]} columns={columns}
data={users ?? []} data={users ?? []}
isLoading={isFetching} isLoading={isFetching}
obj={t('users.title')} obj={t('users.title')}
sortBy={[{ id: 'email', desc: false }]} sortBy={[{ id: 'email', desc: false }]}
hiddenColumns={hiddenColumns} hiddenColumns={hiddenColumns}
fullScreen fullScreen
onRowClick={openEditModal}
/> />
</Box> </Box>
</CardBody> </CardBody>