[WIFI-11875] Added timestamps selection for device commands, logs and healthchecks

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2022-12-05 13:03:45 -05:00
parent a8f53de511
commit db642782b0
12 changed files with 386 additions and 33 deletions

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.8.0(36)",
"version": "2.8.0(37)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -57,6 +57,29 @@ export const useGetCommandHistory = ({
onError,
});
const getCommandsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () =>
axiosGw
.get(`commands?serialNumber=${serialNumber}&startDate=${start}&endDate=${end}`)
.then((response) => response.data) as Promise<{
commands: DeviceCommandHistory[];
}>;
export const useGetCommandHistoryWithTimestamps = ({
serialNumber,
start,
end,
onError,
}: {
serialNumber?: string;
start?: number;
end?: number;
onError?: (e: AxiosError) => void;
}) =>
useQuery(['commands', serialNumber, { start, end }], getCommandsWithTimestamps(serialNumber, start, end), {
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
staleTime: 1000 * 60,
onError,
});
const deleteCommandHistory = async (id: string) => axiosGw.delete(`command/${id}`);
export const useDeleteCommand = () => {
const queryClient = useQueryClient();

View File

@@ -40,7 +40,31 @@ export const useDeleteLogs = () => {
return useMutation(deleteLogs, {
onSuccess: () => {
queryClient.invalidateQueries('devicelogs');
queryClient.invalidateQueries(['devicelogs']);
},
});
};
const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () =>
axiosGw
.get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}`)
.then((response) => response.data) as Promise<{
values: DeviceLog[];
serialNumber: string;
}>;
export const useGetDeviceLogsWithTimestamps = ({
serialNumber,
start,
end,
onError,
}: {
serialNumber?: string;
start?: number;
end?: number;
onError?: (e: AxiosError) => void;
}) =>
useQuery(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), {
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
staleTime: 1000 * 60,
onError,
});

View File

@@ -33,6 +33,30 @@ export const useGetHealthChecks = ({
onError,
});
const getHealthChecksWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () =>
axiosGw
.get(`device/${serialNumber}/healthchecks?startDate=${start}&endDate=${end}`)
.then((response) => response.data) as Promise<{
values: HealthCheck[];
serialNumber: string;
}>;
export const useGetHealthChecksWithTimestamps = ({
serialNumber,
start,
end,
onError,
}: {
serialNumber?: string;
start?: number;
end?: number;
onError?: (e: AxiosError) => void;
}) =>
useQuery(['healthchecks', serialNumber, { start, end }], getHealthChecksWithTimestamps(serialNumber, start, end), {
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
staleTime: 1000 * 60,
onError,
});
const deleteHealthChecks = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) =>
axiosGw.delete(`device/${serialNumber}/healthchecks?endDate=${endDate}`);
export const useDeleteHealthChecks = () => {
@@ -40,7 +64,7 @@ export const useDeleteHealthChecks = () => {
return useMutation(deleteHealthChecks, {
onSuccess: () => {
queryClient.invalidateQueries('healthchecks');
queryClient.invalidateQueries(['healthchecks']);
},
});
};

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { Box, Button, Center, Heading } from '@chakra-ui/react';
import { Box, Button, Center, Heading, HStack, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import HistoryDatePickers from '../DatePickers';
import CommandResultModal from './ResultModal';
import useCommandHistoryTable from './useCommandHistoryTable';
import { RefreshButton } from 'components/Buttons/RefreshButton';
@@ -15,24 +16,48 @@ const CommandHistory = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { getCommands, columns, selectedCommand, detailsModalProps } = useCommandHistoryTable({ serialNumber, limit });
const { time, setTime, getCustomCommands, getCommands, columns, selectedCommand, detailsModalProps } =
useCommandHistoryTable({ serialNumber, limit });
const raiseLimit = () => {
setLimit(limit + 25);
};
const noMoreAvailable = getCommands.data !== undefined && getCommands.data.commands.length < limit;
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
};
const onClear = () => {
setTime(undefined);
};
const noMoreAvailable =
getCustomCommands.data || (getCommands.data !== undefined && getCommands.data.commands.length < limit);
const data = React.useMemo(() => {
if (getCustomCommands.data) return getCustomCommands.data.commands.sort((a, b) => b.submitted - a.submitted);
if (getCommands.data) return getCommands.data.commands;
return [];
}, [getCustomCommands.data, getCommands.data]);
return (
<Box>
<Box textAlign="right">
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns}
preference="gateway.device.commandshistory.hiddenColumns"
/>
<RefreshButton isCompact isFetching={getCommands.isFetching} onClick={getCommands.refetch} ml={2} />
<Box textAlign="right" display="flex">
<Spacer />
<HStack>
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns}
preference="gateway.device.commandshistory.hiddenColumns"
/>
<RefreshButton
isCompact
isFetching={getCommands.isFetching}
onClick={getCommands.refetch}
colorScheme="blue"
/>
</HStack>
</Box>
<Box overflowY="auto" h="300px">
<DataTable
@@ -44,16 +69,16 @@ const CommandHistory = ({ serialNumber }: Props) => {
accessor: string;
}[]
}
data={getCommands.data?.commands ?? []}
isLoading={getCommands.isFetching}
data={data}
isLoading={getCommands.isFetching || getCustomCommands.isFetching}
hiddenColumns={hiddenColumns}
obj={t('controller.devices.commands')}
// @ts-ignore
hideControls
showAllRows
/>
{getCommands.data !== undefined && (
<Center mt={2}>
{data !== undefined && (
<Center mt={2} hidden={getCustomCommands.data !== undefined}>
{!noMoreAvailable || getCommands.isFetching ? (
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getCommands.isFetching}>
{t('controller.devices.show_more')}

View File

@@ -4,7 +4,12 @@ import { MagnifyingGlass, Trash } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { uppercaseFirstLetter } from 'helpers/stringHelper';
import { DeviceCommandHistory, useDeleteCommand, useGetCommandHistory } from 'hooks/Network/Commands';
import {
DeviceCommandHistory,
useDeleteCommand,
useGetCommandHistory,
useGetCommandHistoryWithTimestamps,
} from 'hooks/Network/Commands';
import { AxiosError } from 'models/Axios';
import { Column } from 'models/Table';
@@ -15,6 +20,12 @@ type Props = {
const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
const { t } = useTranslation();
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
const getCustomCommands = useGetCommandHistoryWithTimestamps({
serialNumber,
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
});
const getCommands = useGetCommandHistory({ serialNumber, limit });
const deleteCommand = useDeleteCommand();
const [selectedCommand, setSelectedCommand] = React.useState<DeviceCommandHistory | undefined>();
@@ -185,8 +196,11 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
return {
columns,
getCommands,
getCustomCommands,
selectedCommand,
detailsModalProps,
time,
setTime,
};
};

View File

@@ -0,0 +1,188 @@
import * as React from 'react';
import {
Box,
Button,
Flex,
Heading,
HStack,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Spacer,
Tooltip,
useBreakpoint,
} from '@chakra-ui/react';
import { Clock, Prohibit } from 'phosphor-react';
import ReactDatePicker from 'react-datepicker';
import { useTranslation } from 'react-i18next';
import { CloseButton } from 'components/Buttons/CloseButton';
import { SaveButton } from 'components/Buttons/SaveButton';
const CustomInputButton = React.forwardRef(
({ value, onClick }: { value: string; onClick: () => void }, ref: React.LegacyRef<HTMLButtonElement>) => (
<Button colorScheme="gray" size="sm" onClick={onClick} ref={ref} mt={1}>
{value}
</Button>
),
);
const getStart = () => {
const date = new Date();
date.setHours(date.getHours() - 1);
return date;
};
type Props = {
defaults?: { start: Date; end: Date };
setTime: (start: Date, end: Date) => void;
onClear: () => void;
};
const HistoryDatePickers = ({ defaults, setTime, onClear }: Props) => {
const { t } = useTranslation();
const [start, setStart] = React.useState<Date>(defaults?.start ?? getStart());
const [end, setEnd] = React.useState<Date>(defaults?.end ?? new Date());
const breakpoint = useBreakpoint();
const onStartChange = (newDate: Date) => {
setStart(newDate);
};
const onEndChange = (newDate: Date) => {
setEnd(newDate);
};
const clear = (onClose: () => void) => () => {
onClear();
onClose();
};
const onSave = (onClose: () => void) => () => {
onClose();
setTime(start, end);
};
const width = (isOpen: boolean) => {
if (isOpen) {
return breakpoint === 'base' ? '360px' : '460px';
}
return undefined;
};
React.useEffect(() => {
setStart(defaults?.start ?? getStart());
setEnd(defaults?.end ?? new Date());
}, [defaults]);
return (
<Popover>
{({ isOpen, onClose }) => (
<>
<PopoverTrigger>
<Box>
<Tooltip label={t('controller.crud.choose_time')}>
<IconButton aria-label={t('controller.crud.choose_time')} icon={<Clock />} />
</Tooltip>
</Box>
</PopoverTrigger>
<PopoverContent w={width(isOpen)}>
<PopoverArrow />
<PopoverHeader display="flex">
<Heading size="sm" my="auto">
{t('controller.crud.choose_time')}
</Heading>
<Spacer />
<HStack>
<Tooltip label={t('controller.crud.clear_time')}>
<IconButton
colorScheme="red"
aria-label={t('controller.crud.clear_time')}
onClick={clear(onClose)}
icon={<Prohibit />}
/>
</Tooltip>
<SaveButton onClick={onSave(onClose)} isCompact />
<CloseButton onClick={onClose} />
</HStack>
</PopoverHeader>
<PopoverBody>
{breakpoint === 'base' ? (
<Box>
<Flex>
<Heading size="sm" my="auto" mr={2}>
{t('system.start')}:{' '}
</Heading>
<Box w="170px">
<ReactDatePicker
selected={start}
onChange={onStartChange}
timeInputLabel={`${t('common.time')}: `}
dateFormat="dd/MM/yyyy hh:mm aa"
timeFormat="p"
showTimeSelect
// @ts-ignore
customInput={<CustomInputButton />}
/>
</Box>
</Flex>
<Flex>
<Heading size="sm" my="auto" mr={4}>
{t('common.end')}:{' '}
</Heading>
<Box w="170px">
<ReactDatePicker
selected={end}
onChange={onEndChange}
timeInputLabel={`${t('common.time')}: `}
dateFormat="dd/MM/yyyy hh:mm aa"
timeFormat="p"
showTimeSelect
// @ts-ignore
customInput={<CustomInputButton />}
/>
</Box>
</Flex>
</Box>
) : (
<Flex>
<Heading size="sm" my="auto" mr={2}>
{t('system.start')}:{' '}
</Heading>
<Box w="170px">
<ReactDatePicker
selected={start}
onChange={onStartChange}
timeInputLabel={`${t('common.time')}: `}
dateFormat="dd/MM/yyyy hh:mm aa"
timeFormat="p"
showTimeSelect
// @ts-ignore
customInput={<CustomInputButton />}
/>
</Box>
<Heading size="sm" my="auto" mr={2}>
{t('common.end')}:{' '}
</Heading>
<Box w="170px">
<ReactDatePicker
selected={end}
onChange={onEndChange}
timeInputLabel={`${t('common.time')}: `}
dateFormat="dd/MM/yyyy hh:mm aa"
timeFormat="p"
showTimeSelect
// @ts-ignore
customInput={<CustomInputButton />}
/>
</Box>
</Flex>
)}
</PopoverBody>
</PopoverContent>
</>
)}
</Popover>
);
};
export default HistoryDatePickers;

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import HistoryDatePickers from '../DatePickers';
import DeleteHealthChecksModal from './DeleteModal';
import useHealthCheckTable from './useHealthCheckTable';
import { RefreshButton } from 'components/Buttons/RefreshButton';
@@ -15,19 +16,35 @@ const HealthCheckHistory = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { getHealthChecks, columns } = useHealthCheckTable({ serialNumber, limit });
const { time, setTime, getCustomHealthChecks, getHealthChecks, columns } = useHealthCheckTable({
serialNumber,
limit,
});
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
};
const onClear = () => {
setTime(undefined);
};
const raiseLimit = () => {
setLimit(limit + 25);
};
const noMoreAvailable = getHealthChecks.data !== undefined && getHealthChecks.data.values.length < limit;
const data = React.useMemo(() => {
if (getCustomHealthChecks.data) return getCustomHealthChecks.data.values.sort((a, b) => b.recorded - a.recorded);
if (getHealthChecks.data) return getHealthChecks.data.values;
return [];
}, [getHealthChecks.data, getCustomHealthChecks.data]);
return (
<Box>
<Flex>
<Spacer />
<HStack>
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
@@ -35,7 +52,13 @@ const HealthCheckHistory = ({ serialNumber }: Props) => {
preference="gateway.device.healthchecks.hiddenColumns"
/>
<DeleteHealthChecksModal serialNumber={serialNumber} />
<RefreshButton isCompact isFetching={getHealthChecks.isFetching} onClick={getHealthChecks.refetch} ml={2} />
<RefreshButton
isCompact
isFetching={getHealthChecks.isFetching || getCustomHealthChecks.isFetching}
onClick={getHealthChecks.refetch}
ml={2}
colorScheme="blue"
/>
</HStack>
</Flex>
<Box overflowY="auto" h="300px">
@@ -48,8 +71,8 @@ const HealthCheckHistory = ({ serialNumber }: Props) => {
accessor: string;
}[]
}
data={getHealthChecks.data?.values ?? []}
isLoading={getHealthChecks.isFetching}
data={data}
isLoading={getHealthChecks.isFetching || getCustomHealthChecks.isFetching}
hiddenColumns={hiddenColumns}
obj={t('controller.devices.healthchecks')}
// @ts-ignore
@@ -57,7 +80,7 @@ const HealthCheckHistory = ({ serialNumber }: Props) => {
showAllRows
/>
{getHealthChecks.data !== undefined && (
<Center mt={2}>
<Center mt={2} hidden={getCustomHealthChecks.data !== undefined}>
{!noMoreAvailable || getHealthChecks.isFetching ? (
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getHealthChecks.isFetching}>
{t('controller.devices.show_more')}

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { Badge, Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { HealthCheck, useGetHealthChecks } from 'hooks/Network/HealthChecks';
import { HealthCheck, useGetHealthChecks, useGetHealthChecksWithTimestamps } from 'hooks/Network/HealthChecks';
import { Column } from 'models/Table';
type Props = {
@@ -13,6 +13,12 @@ type Props = {
const useHealthCheckTable = ({ serialNumber, limit }: Props) => {
const { t } = useTranslation();
const getHealthChecks = useGetHealthChecks({ serialNumber, limit });
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
const getCustomHealthChecks = useGetHealthChecksWithTimestamps({
serialNumber,
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
});
const dateCell = React.useCallback(
(v: number) => (
@@ -80,6 +86,9 @@ const useHealthCheckTable = ({ serialNumber, limit }: Props) => {
return {
columns,
getHealthChecks,
getCustomHealthChecks,
time,
setTime,
};
};

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import HistoryDatePickers from '../DatePickers';
import DeleteLogModal from './DeleteModal';
import useDeviceLogsTable from './useDeviceLogsTable';
import { RefreshButton } from 'components/Buttons/RefreshButton';
@@ -15,19 +16,32 @@ const LogHistory = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { getLogs, columns } = useDeviceLogsTable({ serialNumber, limit });
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit });
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
};
const onClear = () => {
setTime(undefined);
};
const raiseLimit = () => {
setLimit(limit + 25);
};
const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit;
const data = React.useMemo(() => {
if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded);
if (getLogs.data) return getLogs.data.values;
return [];
}, [getLogs.data, getCustomLogs.data]);
return (
<Box>
<Flex>
<Spacer />
<HStack>
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
@@ -35,7 +49,7 @@ const LogHistory = ({ serialNumber }: Props) => {
preference="gateway.device.logs.hiddenColumns"
/>
<DeleteLogModal serialNumber={serialNumber} />
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} ml={2} />
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
</HStack>
</Flex>
<Box overflowY="auto" h="300px">
@@ -48,8 +62,8 @@ const LogHistory = ({ serialNumber }: Props) => {
accessor: string;
}[]
}
data={getLogs.data?.values ?? []}
isLoading={getLogs.isFetching}
data={data}
isLoading={getLogs.isFetching || getCustomLogs.isFetching}
hiddenColumns={hiddenColumns}
obj={t('controller.devices.logs')}
// @ts-ignore
@@ -57,7 +71,7 @@ const LogHistory = ({ serialNumber }: Props) => {
showAllRows
/>
{getLogs.data !== undefined && (
<Center mt={2}>
<Center mt={2} hidden={getCustomLogs.data !== undefined}>
{!noMoreAvailable || getLogs.isFetching ? (
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}>
{t('controller.devices.show_more')}

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { DeviceLog, useGetDeviceLogs } from 'hooks/Network/DeviceLogs';
import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs';
import { Column } from 'models/Table';
type Props = {
@@ -13,6 +13,12 @@ type Props = {
const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
const { t } = useTranslation();
const getLogs = useGetDeviceLogs({ serialNumber, limit });
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
const getCustomLogs = useGetDeviceLogsWithTimestamps({
serialNumber,
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
});
const dateCell = React.useCallback(
(v: number) => (
@@ -76,6 +82,9 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
return {
columns,
getLogs,
getCustomLogs,
time,
setTime,
};
};