mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-11-01 18:57:46 +00:00
Merge pull request #170 from Telecominfraproject/main
Version 2.9.0(23)
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(13)",
|
||||
"version": "2.9.0(23)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(13)",
|
||||
"version": "2.9.0(23)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(13)",
|
||||
"version": "2.9.0(23)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"format": "prettier --write \"src/**/*.js\"",
|
||||
"format": "prettier --write \"src/**/*x.{ts,tsx,js,jsx}\"",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
|
||||
"clean": "rm -rf node_modules && rm -rf build"
|
||||
|
||||
@@ -12,7 +12,14 @@ export interface AlertButtonProps extends ThemeProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const _AlertButton: React.FC<AlertButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
||||
const _AlertButton: React.FC<AlertButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact = true,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
|
||||
@@ -11,7 +11,14 @@ export interface CreateButtonProps extends SpaceProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const _CreateButton: React.FC<CreateButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
||||
const _CreateButton: React.FC<CreateButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact = true,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const _DeleteButton: React.FC<DeleteButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
label,
|
||||
ml,
|
||||
...props
|
||||
|
||||
@@ -51,7 +51,7 @@ const DeviceActionDropdown = ({
|
||||
onOpenScriptModal,
|
||||
onOpenRebootModal,
|
||||
size,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
@@ -163,7 +163,7 @@ const DeviceActionDropdown = ({
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
{size === undefined || isCompact ? (
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
@@ -182,7 +182,7 @@ const DeviceActionDropdown = ({
|
||||
isDisabled={isDisabled}
|
||||
ml={2}
|
||||
>
|
||||
{t('commands.other')}
|
||||
{t('common.actions')}
|
||||
</MenuButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface EditButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -11,7 +12,15 @@ export interface EditButtonProps {
|
||||
ml?: string | number;
|
||||
}
|
||||
|
||||
const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, isLoading, isCompact, ...props }) => {
|
||||
const _EditButton: React.FC<EditButtonProps> = ({
|
||||
onClick,
|
||||
label,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact = true,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
|
||||
@@ -24,12 +33,12 @@ const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, is
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
{label ?? t('common.edit')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<Tooltip label={label ?? t('common.edit')} hasArrow>
|
||||
<IconButton
|
||||
aria-label="edit"
|
||||
colorScheme="gray"
|
||||
|
||||
@@ -17,7 +17,7 @@ const _RefreshButton: React.FC<RefreshButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isFetching,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
ml,
|
||||
size,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const _ResponsiveButton: React.FC<ResponsiveButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
color,
|
||||
label,
|
||||
icon,
|
||||
|
||||
@@ -18,7 +18,7 @@ const _SaveButton: React.FC<SaveButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
isDirty,
|
||||
dirtyCheck,
|
||||
...props
|
||||
|
||||
@@ -20,7 +20,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
|
||||
isDirty,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
ml,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ const _WarningButton: React.FC<WarningButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ColumnPicker = ({
|
||||
hiddenColumns,
|
||||
setHiddenColumns,
|
||||
size,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
}: ColumnPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { getPref, setPref } = useAuth();
|
||||
|
||||
@@ -44,12 +44,14 @@ const defaultProps = {
|
||||
sortBy: [],
|
||||
};
|
||||
|
||||
export type DataTableProps = {
|
||||
columns: readonly Column<object>[];
|
||||
data: object[];
|
||||
export type DataTableProps<TValue> = {
|
||||
columns: Column<TValue>[];
|
||||
data: TValue[];
|
||||
count?: number;
|
||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||
isLoading?: boolean;
|
||||
onRowClick?: (row: TValue) => void;
|
||||
isRowClickable?: (row: TValue) => boolean;
|
||||
obj?: string;
|
||||
sortBy?: { id: string; desc: boolean }[];
|
||||
hiddenColumns?: string[];
|
||||
@@ -68,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
||||
state: UsePaginationState<T>;
|
||||
};
|
||||
|
||||
const _DataTable = ({
|
||||
const _DataTable = <TValue extends object>({
|
||||
columns,
|
||||
data,
|
||||
isLoading,
|
||||
@@ -84,7 +86,9 @@ const _DataTable = ({
|
||||
isManual,
|
||||
saveSettingsId,
|
||||
showAllRows,
|
||||
}: DataTableProps) => {
|
||||
onRowClick,
|
||||
isRowClickable,
|
||||
}: DataTableProps<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
@@ -143,7 +147,7 @@ const _DataTable = ({
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
) as TableInstanceWithHooks<object>;
|
||||
) as TableInstanceWithHooks<TValue>;
|
||||
|
||||
const handleGoToPage = (newPage: number) => {
|
||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
|
||||
@@ -260,8 +264,10 @@ const _DataTable = ({
|
||||
</Thead>
|
||||
{data.length > 0 && (
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{page.map((row: Row) => {
|
||||
{page.map((row: Row<TValue>) => {
|
||||
prepareRow(row);
|
||||
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
|
||||
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
|
||||
return (
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
@@ -269,6 +275,7 @@ const _DataTable = ({
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
// @ts-ignore
|
||||
@@ -288,8 +295,26 @@ const _DataTable = ({
|
||||
fontSize="14px"
|
||||
// @ts-ignore
|
||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||
// @ts-ignore
|
||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
||||
fontFamily={
|
||||
// @ts-ignore
|
||||
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')}
|
||||
</Td>
|
||||
@@ -414,4 +439,4 @@ const _DataTable = ({
|
||||
|
||||
_DataTable.defaultProps = defaultProps;
|
||||
|
||||
export const DataTable = React.memo(_DataTable);
|
||||
export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable;
|
||||
|
||||
@@ -249,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({
|
||||
fontSize="14px"
|
||||
// @ts-ignore
|
||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||
// @ts-ignore
|
||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
||||
fontFamily={
|
||||
// @ts-ignore
|
||||
cell.column.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</Td>
|
||||
|
||||
@@ -5,17 +5,22 @@ import {
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Textarea,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { ClipboardText } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../Buttons/SaveButton';
|
||||
import { Modal } from '../Modal';
|
||||
import { FileInputButton } from 'components/Buttons/FileInputButton';
|
||||
import { useConfigureDevice } from 'hooks/Network/Commands';
|
||||
import { useGetDevice } from 'hooks/Network/Devices';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
export type ConfigureModalProps = {
|
||||
serialNumber: string;
|
||||
@@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const configure = useConfigureDevice({ serialNumber });
|
||||
const getDevice = useGetDevice({ serialNumber });
|
||||
|
||||
const [newConfig, setNewConfig] = React.useState('');
|
||||
|
||||
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setNewConfig(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onImportConfiguration = () => {
|
||||
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
|
||||
};
|
||||
const isValid = React.useMemo(() => {
|
||||
try {
|
||||
JSON.parse(newConfig);
|
||||
@@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
modalProps.onClose();
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// console.log(e);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
{
|
||||
// @ts-ignore
|
||||
<AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription>
|
||||
}
|
||||
<AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -92,15 +98,25 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
</Alert>
|
||||
<FormControl isInvalid={!isValid && newConfig.length > 0}>
|
||||
<FormLabel>{t('configurations.one')}</FormLabel>
|
||||
<Box mb={2} w="240px">
|
||||
<FileInputButton
|
||||
value={newConfig}
|
||||
setValue={(v) => setNewConfig(v)}
|
||||
refreshId="1"
|
||||
accept=".json"
|
||||
isStringFile
|
||||
/>
|
||||
</Box>
|
||||
<Flex mb={2}>
|
||||
<Box w="240px">
|
||||
<FileInputButton
|
||||
value={newConfig}
|
||||
setValue={(v) => setNewConfig(v)}
|
||||
refreshId="1"
|
||||
accept=".json"
|
||||
isStringFile
|
||||
/>
|
||||
</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} />
|
||||
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
@@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
||||
let requestData: {
|
||||
[k: string]: unknown;
|
||||
serialNumber: string;
|
||||
script?: string;
|
||||
timeout?: number | undefined;
|
||||
} = data;
|
||||
|
||||
if (requestData.script) {
|
||||
requestData.script = btoa(requestData.script);
|
||||
}
|
||||
|
||||
if (selectedScript === 'diagnostics') {
|
||||
requestData = {
|
||||
serialNumber: device?.serialNumber ?? '',
|
||||
@@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
||||
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
|
||||
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) {
|
||||
toast({
|
||||
|
||||
@@ -57,7 +57,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
if (isOpen) resetData();
|
||||
}, [isOpen]);
|
||||
return (
|
||||
(<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
<ModalHeader
|
||||
@@ -66,7 +66,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
<>
|
||||
{csvData ? (
|
||||
// @ts-ignore
|
||||
(<CSVLink
|
||||
<CSVLink
|
||||
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={csvData as object[]}
|
||||
>
|
||||
@@ -77,7 +77,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
label={t('common.download')}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</CSVLink>)
|
||||
</CSVLink>
|
||||
) : (
|
||||
<ResponsiveButton
|
||||
color="gray"
|
||||
@@ -118,6 +118,6 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
confirm={closeCancelAndForm}
|
||||
cancel={closeConfirm}
|
||||
/>
|
||||
</Modal>)
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
UUID: string;
|
||||
attachFile: number;
|
||||
@@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
},
|
||||
onError: (e) => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: 'script-error',
|
||||
@@ -263,14 +276,44 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const downloadScript = (serialNumber: string, commandId: string) =>
|
||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||
|
||||
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||
useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
||||
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
||||
enabled: false,
|
||||
onSuccess: (response) => {
|
||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `Script_${commandId}.tar.gz`;
|
||||
const headerLine =
|
||||
(response.headers['content-disposition'] as string | undefined) ??
|
||||
(response.headers['content-disposition'] as string | undefined);
|
||||
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Script_${commandId}.tar.gz`;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const bufferResponse = e.response?.data;
|
||||
let errorMessage = '';
|
||||
// If the response is a buffer, parse to JSON object
|
||||
if (bufferResponse instanceof ArrayBuffer) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||
errorMessage = json.ErrorDescription;
|
||||
}
|
||||
|
||||
toast({
|
||||
id: `script-download-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: errorMessage,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
@@ -89,3 +89,18 @@ export const useDeleteHealthChecks = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getDevicesWithHealthBetween = (
|
||||
context: QueryFunctionContext<[string, string, { lowerLimit: number; upperLimit: number }]>,
|
||||
) =>
|
||||
axiosGw
|
||||
.get(`devices?health=true&lowLimit=${context.queryKey[2].lowerLimit}&highLimit=${context.queryKey[2].upperLimit}`)
|
||||
.then((res) => res.data.serialNumbers as string[]);
|
||||
|
||||
export const useGetDevicesWithHealthBetween = ({
|
||||
lowerLimit,
|
||||
upperLimit,
|
||||
}: {
|
||||
lowerLimit: number;
|
||||
upperLimit: number;
|
||||
}) => useQuery(['devices', 'health', { lowerLimit, upperLimit }], getDevicesWithHealthBetween);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
|
||||
@@ -85,14 +86,44 @@ export const useTrace = ({ serialNumber, alertOnCompletion }: { serialNumber: st
|
||||
export const downloadTrace = (serialNumber: string, commandId: string) =>
|
||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||
|
||||
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||
useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
||||
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
||||
enabled: false,
|
||||
onSuccess: (response) => {
|
||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `Trace_${commandId}.pcap`;
|
||||
const headerLine =
|
||||
(response.headers['content-disposition'] as string | undefined) ??
|
||||
(response.headers['content-disposition'] as string | undefined);
|
||||
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Trace_${commandId}.pcap`;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const bufferResponse = e.response?.data;
|
||||
let errorMessage = '';
|
||||
// If the response is a buffer, parse to JSON object
|
||||
if (bufferResponse instanceof ArrayBuffer) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||
errorMessage = json.ErrorDescription;
|
||||
}
|
||||
|
||||
toast({
|
||||
id: `trace-download-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: errorMessage,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
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 { Card } from 'components/Containers/Card';
|
||||
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
||||
import { bytesString } from 'helpers/stringHelper';
|
||||
import { useGetDevicesStats } from 'hooks/Network/Devices';
|
||||
@@ -11,18 +23,19 @@ const SidebarDevices = () => {
|
||||
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
||||
|
||||
const getTime = () => {
|
||||
const time = React.useMemo(() => {
|
||||
if (lastTime === undefined || lastUpdate === undefined) return null;
|
||||
|
||||
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
||||
|
||||
return Math.max(0, Math.floor(seconds / 1000));
|
||||
};
|
||||
}, [lastTime, lastUpdate]);
|
||||
|
||||
const refresh = () => {
|
||||
if (document.visibilityState !== 'hidden') {
|
||||
getStats.refetch();
|
||||
}
|
||||
const circleColor = () => {
|
||||
if (time === null) return 'gray.300';
|
||||
if (time < 10) return 'green.300';
|
||||
if (time < 30) return 'yellow.300';
|
||||
return 'red.300';
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -38,47 +51,60 @@ const SidebarDevices = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('visibilitychange', refresh);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!getStats.data) return null;
|
||||
|
||||
return (
|
||||
<VStack mb={-1}>
|
||||
<Flex flexDir="column" textAlign="center">
|
||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||
<Heading size="xs">
|
||||
{t('common.connected')} {t('devices.title')}
|
||||
</Heading>
|
||||
<Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400">
|
||||
({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}
|
||||
<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"
|
||||
>
|
||||
Rx: {bytesString(getStats.data.rx)}, Tx: {bytesString(getStats.data.tx)}
|
||||
</Heading>
|
||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||
<Heading size="md" textAlign="center" mt={2}>
|
||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||
<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}>
|
||||
<Flex flexDir="column" textAlign="center">
|
||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||
<Heading size="xs" display="flex" justifyContent="center">
|
||||
<Text>
|
||||
{t('common.connected')} {t('devices.title')}{' '}
|
||||
</Text>{' '}
|
||||
</Heading>
|
||||
</Tooltip>
|
||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||
<Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400">
|
||||
{t('controller.stats.seconds_ago', { s: getTime() })}
|
||||
</Heading>
|
||||
</Flex>
|
||||
</VStack>
|
||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||
<Heading size="md" textAlign="center" mt={1}>
|
||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||
</Heading>
|
||||
</Tooltip>
|
||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||
<Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
|
||||
<Tooltip hasArrow label="Rx">
|
||||
<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>
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Column<T> {
|
||||
alwaysShow?: boolean;
|
||||
Footer?: string;
|
||||
accessor?: string;
|
||||
stopPropagation?: boolean;
|
||||
disableSortBy?: boolean;
|
||||
hasPopover?: boolean;
|
||||
customMaxWidth?: string;
|
||||
|
||||
@@ -99,13 +99,14 @@ const DefaultConfigurationsList = () => {
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<LoadingOverlay isLoading={getConfigs.isFetching}>
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<DefaultConfigurationResponse>
|
||||
columns={columns}
|
||||
saveSettingsId="firmware.table"
|
||||
data={getConfigs.data ?? []}
|
||||
obj={t('controller.configurations.title')}
|
||||
minHeight="200px"
|
||||
sortBy={[{ id: 'name', desc: true }]}
|
||||
onRowClick={onViewDetails}
|
||||
/>
|
||||
</LoadingOverlay>
|
||||
</Box>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { Modal } from 'components/Modals/Modal';
|
||||
import WifiScanResultDisplay from 'components/Modals/WifiScanModal/ResultDisplay';
|
||||
import { compactDate } from 'helpers/dateFormatting';
|
||||
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||
import { DeviceCommandHistory } from 'hooks/Network/Commands';
|
||||
import { DeviceCommandHistory, useGetSingleCommandHistory } from 'hooks/Network/Commands';
|
||||
import { WifiScanResult } from 'models/Device';
|
||||
|
||||
type Props = {
|
||||
@@ -39,9 +39,13 @@ type Props = {
|
||||
command?: DeviceCommandHistory;
|
||||
};
|
||||
|
||||
const CommandResultModal = ({ modalProps, command }: Props) => {
|
||||
const CommandResultModal = ({ modalProps, command: initialCommandInfo }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const { data: command } = useGetSingleCommandHistory({
|
||||
commandId: initialCommandInfo?.UUID ?? '',
|
||||
serialNumber: initialCommandInfo?.serialNumber ?? '',
|
||||
});
|
||||
|
||||
if (!command) return null;
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ const CommandHistory = ({ serialNumber }: Props) => {
|
||||
<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"
|
||||
/>
|
||||
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<RefreshButton
|
||||
isCompact
|
||||
isFetching={getCommands.isFetching}
|
||||
|
||||
@@ -99,16 +99,6 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
|
||||
const actionCell = React.useCallback(
|
||||
(command: DeviceCommandHistory) => (
|
||||
<HStack>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={onOpenDetails(command)}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
size="sm"
|
||||
isLoading={loadingDeleteSerial === command.UUID}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('crud.delete')}>
|
||||
<IconButton
|
||||
aria-label={t('crud.delete')}
|
||||
@@ -119,6 +109,16 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
|
||||
isLoading={loadingDeleteSerial === command.UUID}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={onOpenDetails(command)}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
size="sm"
|
||||
isLoading={loadingDeleteSerial === command.UUID}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
),
|
||||
[loadingDeleteSerial],
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ChartData,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
|
||||
|
||||
const getDivisionFactor = (maxBytes: number) => {
|
||||
if (maxBytes < 1024) {
|
||||
@@ -42,22 +44,28 @@ const InterfaceChart = ({ data }: Props) => {
|
||||
|
||||
const { factor, unit } = getDivisionFactor(data.maxTx);
|
||||
|
||||
const points = {
|
||||
const points: ChartData<'line', string[], string> = {
|
||||
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
|
||||
datasets: [
|
||||
{
|
||||
// Real 'Tx', but shown as 'Rx'
|
||||
label: 'Tx',
|
||||
data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)),
|
||||
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
||||
borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: 'start',
|
||||
},
|
||||
{
|
||||
// Real 'Rx', but shown as 'Tx'
|
||||
label: 'Rx',
|
||||
data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)),
|
||||
borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
|
||||
backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
|
||||
borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200
|
||||
backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: 'start',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -34,20 +34,29 @@ const DeviceMemoryChart = ({ data }: Props) => {
|
||||
{
|
||||
label: 'Free',
|
||||
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
||||
},
|
||||
{
|
||||
label: 'Buffered',
|
||||
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
|
||||
backgroundColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
|
||||
borderColor: colorMode === 'light' ? 'rgb(99, 179, 237, 1)' : 'rgb(190, 227, 248, 1)', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? 'rgb(99, 179, 237, 0.3)' : 'rgb(190, 227, 248, 0.3)', // blue-300 - blue-100
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: '+1',
|
||||
},
|
||||
{
|
||||
label: 'Cached',
|
||||
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
|
||||
backgroundColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
|
||||
borderColor: colorMode === 'light' ? 'rgb(237, 100, 166, 1)' : 'rgb(251, 182, 206, 1)', // pink-400 - pink-200
|
||||
backgroundColor: colorMode === 'light' ? 'rgb(237, 100, 166, 0.3)' : 'rgb(251, 182, 206, 0.3)', // pink-400 - pink-200
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: '+1',
|
||||
},
|
||||
{
|
||||
label: 'Buffered',
|
||||
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? 'rgb(255, 240, 31, 1)' : 'rgb(250, 240, 137, 1)', // yellow-400 - yellow-200
|
||||
backgroundColor: colorMode === 'light' ? 'rgb(255, 240, 31, 0.3)' : 'rgb(250, 240, 137, 0.3)', // yellow-400 - yellow-200
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: 'origin',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -52,8 +52,6 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
|
||||
<Heading size="md">{t('configurations.statistics')}</Heading>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<ViewLastStatsModal serialNumber={serialNumber} />
|
||||
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<Select value={selected} onChange={onSelectInterface}>
|
||||
{parsedData?.interfaces
|
||||
? Object.keys(parsedData.interfaces).map((v) => (
|
||||
@@ -64,6 +62,8 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
|
||||
: null}
|
||||
<option value="memory">{t('statistics.memory')}</option>
|
||||
</Select>
|
||||
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<ViewLastStatsModal serialNumber={serialNumber} />
|
||||
<RefreshButton
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
const [selected, setSelected] = React.useState('memory');
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
const [hasSelectedNew, setHasSelectedNew] = React.useState(false);
|
||||
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
||||
const onProgressChange = React.useCallback((newProgress: number) => {
|
||||
setProgress(newProgress);
|
||||
@@ -29,13 +30,17 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
});
|
||||
|
||||
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setHasSelectedNew(true);
|
||||
setSelected(event.target.value);
|
||||
};
|
||||
|
||||
const parsedData = React.useMemo(() => {
|
||||
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 = {
|
||||
used: [] as number[],
|
||||
buffered: [] as number[],
|
||||
@@ -56,7 +61,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
if (index === 0) {
|
||||
let updated = false;
|
||||
for (const inter of stat.data.interfaces ?? []) {
|
||||
if (!updated && selected === 'memory') {
|
||||
if (!hasSelectedNew && !updated && selected === 'memory') {
|
||||
updated = true;
|
||||
setSelected(inter.name);
|
||||
}
|
||||
@@ -100,6 +105,18 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
maxRx: rxDelta,
|
||||
};
|
||||
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]?.tx.push(txDelta);
|
||||
data[inter.name]?.recorded.push(stat.recorded);
|
||||
|
||||
@@ -9,12 +9,15 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
IconButton,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
useClipboard,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { ListDashes } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
@@ -43,9 +46,15 @@ const ViewCapabilitiesModal = ({ serialNumber }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onOpen} colorScheme="pink" mr={2}>
|
||||
{t('controller.devices.capabilities')}
|
||||
</Button>
|
||||
<Tooltip label={t('controller.devices.capabilities')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('controller.devices.capabilities')}
|
||||
icon={<ListDashes size={20} />}
|
||||
onClick={onOpen}
|
||||
colorScheme="pink"
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={t('controller.devices.capabilities')}
|
||||
|
||||
@@ -7,11 +7,14 @@ import {
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useClipboard,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { Barcode } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { DeviceConfiguration } from 'models/Device';
|
||||
@@ -30,9 +33,14 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onOpen} isDisabled={!configuration} colorScheme="purple">
|
||||
{t('configurations.one')}
|
||||
</Button>
|
||||
<Tooltip label={t('configurations.one')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('configurations.one')}
|
||||
icon={<Barcode size={20} />}
|
||||
onClick={onOpen}
|
||||
colorScheme="purple"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={t('configurations.one')}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
Box,
|
||||
Button,
|
||||
Heading,
|
||||
HStack,
|
||||
Portal,
|
||||
@@ -12,10 +19,13 @@ import {
|
||||
useBreakpoint,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Masonry from 'react-masonry-css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DeviceDetails from './Details';
|
||||
import DeviceLogsCard from './LogsCard';
|
||||
import DeviceNotes from './Notes';
|
||||
@@ -23,6 +33,7 @@ import RestrictionsCard from './RestrictionsCard';
|
||||
import DeviceStatisticsCard from './StatisticsCard';
|
||||
import DeviceSummary from './Summary';
|
||||
import WifiAnalysisCard from './WifiAnalysis';
|
||||
import { DeleteButton } from 'components/Buttons/DeleteButton';
|
||||
import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
@@ -38,7 +49,7 @@ import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
||||
import { TelemetryModal } from 'components/Modals/TelemetryModal';
|
||||
import { TraceModal } from 'components/Modals/TraceModal';
|
||||
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 = {
|
||||
serialNumber: string;
|
||||
@@ -46,10 +57,17 @@ type Props = {
|
||||
|
||||
const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const breakpoint = useBreakpoint();
|
||||
const cancelRef = React.useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({
|
||||
serialNumber,
|
||||
});
|
||||
const getDevice = useGetDevice({ serialNumber });
|
||||
const getStatus = useGetDeviceStatus({ serialNumber });
|
||||
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 });
|
||||
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
||||
const scanModalProps = useDisclosure();
|
||||
const resetModalProps = useDisclosure();
|
||||
const eventQueueProps = useDisclosure();
|
||||
@@ -62,6 +80,35 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
// Sticky-top styles
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
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(() => {
|
||||
if (!getStatus.data) return null;
|
||||
|
||||
@@ -148,6 +195,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />}
|
||||
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||
{getDevice?.data && (
|
||||
<DeviceActionDropdown
|
||||
// @ts-ignore
|
||||
@@ -197,6 +245,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<DeviceSearchBar />
|
||||
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||
{getDevice?.data && (
|
||||
<DeviceActionDropdown
|
||||
// @ts-ignore
|
||||
@@ -212,6 +261,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
onOpenRebootModal={rebootModalProps.onOpen}
|
||||
onOpenScriptModal={scriptModal.openModal}
|
||||
size="md"
|
||||
isCompact
|
||||
/>
|
||||
)}
|
||||
<RefreshButton
|
||||
@@ -226,6 +276,24 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
</Card>
|
||||
</Portal>
|
||||
)}
|
||||
<AlertDialog isOpen={isDeleteOpen} leastDestructiveRef={cancelRef} onClose={onDeleteClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{t('crud.delete')} {serialNumber}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button colorScheme="gray" mr="1" onClick={onDeleteClose} ref={cancelRef}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
||||
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
||||
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { useColorMode } from '@chakra-ui/react';
|
||||
import { CopyIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
useColorMode,
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Center,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
ListItem,
|
||||
Spinner,
|
||||
UnorderedList,
|
||||
useClipboard,
|
||||
Tooltip as ChakraTooltip,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
@@ -11,20 +28,57 @@ import {
|
||||
Legend,
|
||||
ChartData,
|
||||
ArcElement,
|
||||
ChartTypeRegistry,
|
||||
ScatterDataPoint,
|
||||
BubbleDataPoint,
|
||||
} from 'chart.js';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import { Pie, getElementAtEvent } from 'react-chartjs-2';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { ControllerDashboardHealth } from 'hooks/Network/Controller';
|
||||
import { useGetDevicesWithHealthBetween } from 'hooks/Network/HealthChecks';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
const LABEL_TO_LIMITS = {
|
||||
'100%': { lowerLimit: 100, upperLimit: 100, label: 'With 100% Health' },
|
||||
'>90%': { lowerLimit: 90, upperLimit: 99, label: 'Between 90% and 99%' },
|
||||
'>60%': { lowerLimit: 60, upperLimit: 89, label: 'Between 60% and 89%' },
|
||||
'<=60%': { lowerLimit: 0, upperLimit: 59, label: 'Between 0% and 59%' },
|
||||
} as const;
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement);
|
||||
|
||||
type Props = {
|
||||
data: ControllerDashboardHealth[];
|
||||
};
|
||||
|
||||
const OverallHealthPieChart = ({ data }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const { hasCopied, onCopy, setValue } = useClipboard('');
|
||||
const modalProps = useDisclosure();
|
||||
const [deviceCategory, setDeviceCategory] = React.useState<{ lowerLimit: number; upperLimit: number; label: string }>(
|
||||
LABEL_TO_LIMITS['100%'],
|
||||
);
|
||||
const serialNumbersFromCategory = useGetDevicesWithHealthBetween(deviceCategory);
|
||||
const chartRef =
|
||||
React.useRef<ChartJS<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>>(
|
||||
null,
|
||||
);
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (chartRef.current) {
|
||||
const element = getElementAtEvent(chartRef.current, event)?.[0];
|
||||
if (element && element.index !== undefined) {
|
||||
const label = chartRef.current?.data?.labels?.[element.index] as keyof typeof LABEL_TO_LIMITS | undefined;
|
||||
if (label && LABEL_TO_LIMITS[label]) {
|
||||
setDeviceCategory(LABEL_TO_LIMITS[label]);
|
||||
modalProps.onOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => {
|
||||
const totalDevices = data.reduce(
|
||||
@@ -85,7 +139,7 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
||||
}
|
||||
if (totalDevices['<60%'] > 0) {
|
||||
newData.push(totalDevices['<60%']);
|
||||
labels.push('<60%');
|
||||
labels.push('<=60%');
|
||||
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
|
||||
backgroundColor.push(color);
|
||||
borderColor.push(color);
|
||||
@@ -105,38 +159,108 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
||||
};
|
||||
}, [data, colorMode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (serialNumbersFromCategory.data) setValue(serialNumbersFromCategory.data.join(','));
|
||||
}, [serialNumbersFromCategory.data]);
|
||||
|
||||
return (
|
||||
<GraphStatDisplay
|
||||
title={t('controller.dashboard.overall_health')}
|
||||
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
||||
chart={
|
||||
<Pie
|
||||
data={parsedData}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
labels: {
|
||||
color: colorMode === 'dark' ? 'white' : undefined,
|
||||
<>
|
||||
<GraphStatDisplay
|
||||
title={t('controller.dashboard.overall_health')}
|
||||
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
||||
chart={
|
||||
<Pie
|
||||
// @ts-ignore
|
||||
ref={chartRef}
|
||||
data={parsedData}
|
||||
onClick={onClick}
|
||||
options={{
|
||||
onHover: (e, elements) => {
|
||||
const element = e.native?.target as unknown as { style: { cursor: string } };
|
||||
if (element && elements.length > 0) {
|
||||
element.style.cursor = 'pointer';
|
||||
} else if (element) {
|
||||
element.style.cursor = 'default';
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
labels: {
|
||||
color: colorMode === 'dark' ? 'white' : undefined,
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.label}: ${context.formattedValue} (${Math.round(
|
||||
// @ts-ignore
|
||||
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
||||
)}%)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.label}: ${context.formattedValue} (${Math.round(
|
||||
// @ts-ignore
|
||||
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
||||
)}%)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={t('controller.dashboard.overall_health')}
|
||||
{...modalProps}
|
||||
options={{
|
||||
modalSize: 'sm',
|
||||
}}
|
||||
topRightButtons={
|
||||
<ChakraTooltip label={hasCopied ? `${t('common.copied')}!` : t('common.copy')} hasArrow closeOnClick={false}>
|
||||
<IconButton
|
||||
aria-label={t('common.copy')}
|
||||
icon={<CopyIcon h={5} w={5} />}
|
||||
onClick={onCopy}
|
||||
colorScheme="teal"
|
||||
hidden={!serialNumbersFromCategory.data || serialNumbersFromCategory.data.length === 0}
|
||||
/>
|
||||
</ChakraTooltip>
|
||||
}
|
||||
>
|
||||
{serialNumbersFromCategory.isFetching ? (
|
||||
<Center my={8}>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
) : (
|
||||
<Box>
|
||||
{serialNumbersFromCategory.error ? (
|
||||
<Alert mb={4} status="error">
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{(serialNumbersFromCategory.error as AxiosError).response?.data.ErrorDescription}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{serialNumbersFromCategory.data ? (
|
||||
<Box>
|
||||
<Heading size="md" mb={4}>
|
||||
{serialNumbersFromCategory.data.length} {t('devices.title')} {deviceCategory.label}
|
||||
</Heading>
|
||||
<Box maxH="70vh" overflowY="auto" overflowX="hidden">
|
||||
<UnorderedList pl={2}>
|
||||
{serialNumbersFromCategory.data
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((device) => (
|
||||
<ListItem key={device} fontFamily="mono">
|
||||
<Link href={`#/devices/${device}`}>{device}</Link>
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chak
|
||||
import { LockSimple } 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 AP from './icons/AP.png';
|
||||
@@ -49,6 +50,7 @@ const BADGE_COLORS: Record<string, string> = {
|
||||
|
||||
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);
|
||||
@@ -252,6 +254,7 @@ const DeviceListCard = () => {
|
||||
Footer: '',
|
||||
accessor: 'firmware',
|
||||
Cell: (v) => firmwareCell(v.cell.row.original),
|
||||
stopPropagation: true,
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
},
|
||||
@@ -389,7 +392,7 @@ const DeviceListCard = () => {
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
<DataTable<DeviceWithStatus>
|
||||
columns={
|
||||
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
|
||||
id: string;
|
||||
@@ -407,6 +410,8 @@ const DeviceListCard = () => {
|
||||
// @ts-ignore
|
||||
setPageInfo={setPageInfo}
|
||||
saveSettingsId="gateway.devices.table"
|
||||
onRowClick={(device) => navigate(`devices/${device.serialNumber}`)}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
IconButton,
|
||||
Tag,
|
||||
TagLabel,
|
||||
Text,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
@@ -58,9 +60,14 @@ const UpdateDbButton = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button colorScheme="teal" leftIcon={<Database size={20} />} onClick={onOpen}>
|
||||
{t('firmware.last_db_update_title')}
|
||||
</Button>
|
||||
<Tooltip label={t('firmware.last_db_update_title')}>
|
||||
<IconButton
|
||||
aria-label={t('firmware.last_db_update_title')}
|
||||
colorScheme="teal"
|
||||
icon={<Database size={20} />}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -11,7 +11,15 @@ const UriCell = ({ uri }: Props) => {
|
||||
|
||||
return (
|
||||
<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')}
|
||||
</Button>
|
||||
<Text my="auto">{uri}</Text>
|
||||
|
||||
@@ -158,13 +158,14 @@ const FirmwareListTable = () => {
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}>
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<Firmware>
|
||||
columns={columns}
|
||||
saveSettingsId="firmware.table"
|
||||
data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []}
|
||||
obj={t('analytics.firmware')}
|
||||
minHeight="200px"
|
||||
sortBy={[{ id: 'imageDate', desc: true }]}
|
||||
onRowClick={(firmw) => handleViewDetailsClick(firmw)()}
|
||||
/>
|
||||
</LoadingOverlay>
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Box, Flex, HStack, IconButton, Select, Spacer, Table, Text, Th, Thead, Tooltip, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -127,9 +127,9 @@ const LogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -128,9 +142,9 @@ const FmsLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -128,9 +142,9 @@ const GeneralLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -128,9 +142,9 @@ const SecLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ApiKeyTable from './Table';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
@@ -10,7 +11,9 @@ const ApiKeysCard = () => {
|
||||
return (
|
||||
<Card p={4}>
|
||||
<CardBody>
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
<Box w="100%">
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ScriptTableActions from './Actions';
|
||||
import CreateScriptButton from './CreateButton';
|
||||
import useScriptsTable from './useScriptsTable';
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { query, hiddenColumns } = useScriptsTable();
|
||||
const { id } = useParams();
|
||||
|
||||
const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []);
|
||||
const actionCell = React.useCallback(
|
||||
@@ -108,8 +110,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box w="100%" h="300px" overflowY="auto">
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<Script>
|
||||
columns={columns}
|
||||
saveSettingsId="apiKeys.profile.table"
|
||||
data={query.data ?? []}
|
||||
obj={t('script.other')}
|
||||
@@ -118,6 +120,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||
hiddenColumns={hiddenColumns[0]}
|
||||
showAllRows
|
||||
hideControls
|
||||
onRowClick={(script) => onIdSelect(script.id)}
|
||||
isRowClickable={(script) => script.id !== id}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Article } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemLoggingModal from './Modal';
|
||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||
@@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto">
|
||||
{t('system.logging')}
|
||||
</Button>
|
||||
<Tooltip label={t('system.logging')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('system.logging')}
|
||||
colorScheme="teal"
|
||||
type="button"
|
||||
my="auto"
|
||||
onClick={modalProps.onOpen}
|
||||
icon={<Article size={20} />}
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
|
||||
import SystemLoggingButton from './LoggingButton';
|
||||
import SystemCertificatesTable from './SystemCertificatesTable';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { compactSecondsToDetailed } from 'helpers/dateFormatting';
|
||||
@@ -70,16 +71,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
<Heading pt={0}>{endpoint.type}</Heading>
|
||||
<Spacer />
|
||||
<SystemLoggingButton endpoint={endpoint} token={token} />
|
||||
<Button
|
||||
mt={1}
|
||||
minWidth="112px"
|
||||
colorScheme="blue"
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
onClick={refresh}
|
||||
isLoading={isFetchingSystem || isFetchingSubsystems}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
|
||||
</Box>
|
||||
<CardBody>
|
||||
<VStack w="100%">
|
||||
|
||||
@@ -77,7 +77,7 @@ const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm',
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import CreateUserForm, { CreateUserFormValues } from './Form';
|
||||
import { CreateButton } from 'components/Buttons/CreateButton';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
|
||||
@@ -25,16 +25,7 @@ const CreateUserModal = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'csr'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
import { Avatar, Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
|
||||
@@ -9,6 +8,7 @@ import FormattedDate from '../../../components/InformationDisplays/FormattedDate
|
||||
import CreateUserModal from './CreateUserModal';
|
||||
import EditUserModal from './EditUserModal';
|
||||
import UserActions from './UserActions';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
@@ -25,10 +25,10 @@ const UserTable = () => {
|
||||
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
|
||||
|
||||
const openEditModal = (editUser: User) => {
|
||||
const openEditModal = React.useCallback((editUser: User) => {
|
||||
setEditId(editUser.id);
|
||||
openEdit();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const memoizedActions = useCallback(
|
||||
(userActions: User) => (
|
||||
@@ -99,7 +99,7 @@ const UserTable = () => {
|
||||
];
|
||||
if (user?.userRole !== 'csr')
|
||||
baseColumns.push({
|
||||
id: 'user',
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
@@ -125,28 +125,21 @@ const UserTable = () => {
|
||||
preference="provisioning.userTable.hiddenColumns"
|
||||
/>
|
||||
<CreateUserModal />
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={() => refreshUsers()}
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
ml={2}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<RefreshButton onClick={refreshUsers} isFetching={isFetching} ml={2} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<User>
|
||||
columns={columns}
|
||||
data={users ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('users.title')}
|
||||
sortBy={[{ id: 'email', desc: false }]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
fullScreen
|
||||
onRowClick={openEditModal}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
|
||||
import { extendTheme, Tooltip, type ThemeConfig } from '@chakra-ui/react';
|
||||
import CardComponent from './additions/card/Card';
|
||||
import CardBodyComponent from './additions/card/CardBody';
|
||||
import CardHeaderComponent from './additions/card/CardHeader';
|
||||
@@ -37,4 +37,6 @@ const theme = extendTheme({
|
||||
},
|
||||
});
|
||||
|
||||
Tooltip.defaultProps = { ...Tooltip.defaultProps, hasArrow: true };
|
||||
|
||||
export default theme;
|
||||
|
||||
@@ -1,49 +1,9 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
/* other options */
|
||||
},
|
||||
manifest: {
|
||||
name: 'OpenWiFi Controller App',
|
||||
short_name: 'OpenWiFiController',
|
||||
description: 'OpenWiFi Controller App',
|
||||
theme_color: '#000000',
|
||||
icons: [
|
||||
{
|
||||
src: 'android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-384x384.png',
|
||||
sizes: '384x384',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
build: {
|
||||
outDir: './build',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
|
||||
Reference in New Issue
Block a user