Merge pull request #168 from stephb9959/main

[WIFI-12413] Added toast on download trace/script result error
This commit is contained in:
Charles Bourque
2023-03-17 10:24:15 +01:00
committed by GitHub
35 changed files with 265 additions and 210 deletions

4
package-lock.json generated
View File

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

View File

@@ -1,13 +1,13 @@
{ {
"name": "ucentral-client", "name": "ucentral-client",
"version": "2.9.0(18)", "version": "2.9.0(22)",
"description": "", "description": "",
"private": true, "private": true,
"main": "index.tsx", "main": "index.tsx",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "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'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix", "lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
"clean": "rm -rf node_modules && rm -rf build" "clean": "rm -rf node_modules && rm -rf build"

View File

@@ -12,7 +12,14 @@ export interface AlertButtonProps extends ThemeProps {
label?: string; 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 { t } = useTranslation();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();

View File

@@ -11,7 +11,14 @@ export interface CreateButtonProps extends SpaceProps {
label?: string; 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 { t } = useTranslation();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();

View File

@@ -16,7 +16,7 @@ const _DeleteButton: React.FC<DeleteButtonProps> = ({
onClick, onClick,
isDisabled, isDisabled,
isLoading, isLoading,
isCompact, isCompact = true,
label, label,
ml, ml,
...props ...props

View File

@@ -51,7 +51,7 @@ const DeviceActionDropdown = ({
onOpenScriptModal, onOpenScriptModal,
onOpenRebootModal, onOpenRebootModal,
size, size,
isCompact, isCompact = true,
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const toast = useToast(); const toast = useToast();
@@ -163,7 +163,7 @@ const DeviceActionDropdown = ({
return ( return (
<Menu> <Menu>
<Tooltip label={t('commands.other')}> <Tooltip label={t('common.actions')}>
{size === undefined || isCompact ? ( {size === undefined || isCompact ? (
<MenuButton <MenuButton
as={IconButton} as={IconButton}
@@ -182,7 +182,7 @@ const DeviceActionDropdown = ({
isDisabled={isDisabled} isDisabled={isDisabled}
ml={2} ml={2}
> >
{t('commands.other')} {t('common.actions')}
</MenuButton> </MenuButton>
)} )}
</Tooltip> </Tooltip>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react'; import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { Pen } from 'phosphor-react'; import { Pen } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
export interface EditButtonProps { export interface EditButtonProps {
onClick: () => void; onClick: () => void;
@@ -11,7 +12,15 @@ export interface EditButtonProps {
ml?: string | number; 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(); const breakpoint = useBreakpoint();
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') { if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
@@ -24,12 +33,12 @@ const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, is
isDisabled={isDisabled} isDisabled={isDisabled}
{...props} {...props}
> >
{label} {label ?? t('common.edit')}
</Button> </Button>
); );
} }
return ( return (
<Tooltip label={label}> <Tooltip label={label ?? t('common.edit')} hasArrow>
<IconButton <IconButton
aria-label="edit" aria-label="edit"
colorScheme="gray" colorScheme="gray"

View File

@@ -17,7 +17,7 @@ const _RefreshButton: React.FC<RefreshButtonProps> = ({
onClick, onClick,
isDisabled, isDisabled,
isFetching, isFetching,
isCompact, isCompact = true,
ml, ml,
size, size,
...props ...props

View File

@@ -15,7 +15,7 @@ const _ResponsiveButton: React.FC<ResponsiveButtonProps> = ({
onClick, onClick,
isDisabled, isDisabled,
isLoading, isLoading,
isCompact, isCompact = true,
color, color,
label, label,
icon, icon,

View File

@@ -18,7 +18,7 @@ const _SaveButton: React.FC<SaveButtonProps> = ({
onClick, onClick,
isDisabled, isDisabled,
isLoading, isLoading,
isCompact, isCompact = true,
isDirty, isDirty,
dirtyCheck, dirtyCheck,
...props ...props

View File

@@ -20,7 +20,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
isDirty, isDirty,
isDisabled, isDisabled,
isLoading, isLoading,
isCompact, isCompact = true,
ml, ml,
...props ...props
}) => { }) => {

View File

@@ -16,7 +16,7 @@ const _WarningButton: React.FC<WarningButtonProps> = ({
onClick, onClick,
isDisabled, isDisabled,
isLoading, isLoading,
isCompact, isCompact = true,
label, label,
...props ...props
}) => { }) => {

View File

@@ -21,7 +21,7 @@ export const ColumnPicker = ({
hiddenColumns, hiddenColumns,
setHiddenColumns, setHiddenColumns,
size, size,
isCompact, isCompact = true,
}: ColumnPickerProps) => { }: ColumnPickerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getPref, setPref } = useAuth(); const { getPref, setPref } = useAuth();

View File

@@ -57,7 +57,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
if (isOpen) resetData(); if (isOpen) resetData();
}, [isOpen]); }, [isOpen]);
return ( return (
(<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside"> <Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
<ModalOverlay /> <ModalOverlay />
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}> <ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
<ModalHeader <ModalHeader
@@ -66,7 +66,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
<> <>
{csvData ? ( {csvData ? (
// @ts-ignore // @ts-ignore
(<CSVLink <CSVLink
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`} filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
data={csvData as object[]} data={csvData as object[]}
> >
@@ -77,7 +77,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
label={t('common.download')} label={t('common.download')}
onClick={() => {}} onClick={() => {}}
/> />
</CSVLink>) </CSVLink>
) : ( ) : (
<ResponsiveButton <ResponsiveButton
color="gray" color="gray"
@@ -118,6 +118,6 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
confirm={closeCancelAndForm} confirm={closeCancelAndForm}
cancel={closeConfirm} cancel={closeConfirm}
/> />
</Modal>) </Modal>
); );
}; };

View File

@@ -276,8 +276,11 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
const downloadScript = (serialNumber: string, commandId: string) => const downloadScript = (serialNumber: string, commandId: string) =>
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' }); axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), { const { t } = useTranslation();
const toast = useToast();
return useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
enabled: false, enabled: false,
onSuccess: (response) => { onSuccess: (response) => {
const blob = new Blob([response.data], { type: 'application/octet-stream' }); const blob = new Blob([response.data], { type: 'application/octet-stream' });
@@ -290,4 +293,27 @@ export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNum
link.download = filename; link.download = filename;
link.click(); 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',
});
}
},
}); });
};

View File

@@ -1,5 +1,6 @@
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { axiosGw } from 'constants/axiosInstances'; import { axiosGw } from 'constants/axiosInstances';
@@ -85,8 +86,11 @@ export const useTrace = ({ serialNumber, alertOnCompletion }: { serialNumber: st
export const downloadTrace = (serialNumber: string, commandId: string) => export const downloadTrace = (serialNumber: string, commandId: string) =>
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' }); axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), { const { t } = useTranslation();
const toast = useToast();
return useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
enabled: false, enabled: false,
onSuccess: (response) => { onSuccess: (response) => {
const blob = new Blob([response.data], { type: 'application/octet-stream' }); const blob = new Blob([response.data], { type: 'application/octet-stream' });
@@ -99,4 +103,27 @@ export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: st
link.download = filename; link.download = filename;
link.click(); 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',
});
}
},
}); });
};

View File

@@ -44,13 +44,13 @@ const CommandHistory = ({ serialNumber }: Props) => {
<Box textAlign="right" display="flex"> <Box textAlign="right" display="flex">
<Spacer /> <Spacer />
<HStack> <HStack>
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ColumnPicker <ColumnPicker
columns={columns as Column<unknown>[]} columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns} hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns} setHiddenColumns={setHiddenColumns}
preference="gateway.device.commandshistory.hiddenColumns" preference="gateway.device.commandshistory.hiddenColumns"
/> />
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<RefreshButton <RefreshButton
isCompact isCompact
isFetching={getCommands.isFetching} isFetching={getCommands.isFetching}

View File

@@ -99,16 +99,6 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
const actionCell = React.useCallback( const actionCell = React.useCallback(
(command: DeviceCommandHistory) => ( (command: DeviceCommandHistory) => (
<HStack> <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')}> <Tooltip label={t('crud.delete')}>
<IconButton <IconButton
aria-label={t('crud.delete')} aria-label={t('crud.delete')}
@@ -119,6 +109,16 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
isLoading={loadingDeleteSerial === command.UUID} isLoading={loadingDeleteSerial === command.UUID}
/> />
</Tooltip> </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> </HStack>
), ),
[loadingDeleteSerial], [loadingDeleteSerial],

View File

@@ -52,8 +52,6 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
<Heading size="md">{t('configurations.statistics')}</Heading> <Heading size="md">{t('configurations.statistics')}</Heading>
<Spacer /> <Spacer />
<HStack> <HStack>
<ViewLastStatsModal serialNumber={serialNumber} />
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<Select value={selected} onChange={onSelectInterface}> <Select value={selected} onChange={onSelectInterface}>
{parsedData?.interfaces {parsedData?.interfaces
? Object.keys(parsedData.interfaces).map((v) => ( ? Object.keys(parsedData.interfaces).map((v) => (
@@ -64,6 +62,8 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
: null} : null}
<option value="memory">{t('statistics.memory')}</option> <option value="memory">{t('statistics.memory')}</option>
</Select> </Select>
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ViewLastStatsModal serialNumber={serialNumber} />
<RefreshButton <RefreshButton
size="sm" size="sm"
onClick={refresh} onClick={refresh}

View File

@@ -9,12 +9,15 @@ import {
Button, Button,
Center, Center,
Heading, Heading,
IconButton,
Spinner, Spinner,
Tooltip,
useClipboard, useClipboard,
useColorMode, useColorMode,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { JsonViewer } from '@textea/json-viewer'; import { JsonViewer } from '@textea/json-viewer';
import { ListDashes } from 'phosphor-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RefreshButton } from 'components/Buttons/RefreshButton'; import { RefreshButton } from 'components/Buttons/RefreshButton';
import FormattedDate from 'components/InformationDisplays/FormattedDate'; import FormattedDate from 'components/InformationDisplays/FormattedDate';
@@ -43,9 +46,15 @@ const ViewCapabilitiesModal = ({ serialNumber }: Props) => {
return ( return (
<> <>
<Button onClick={onOpen} colorScheme="pink" mr={2}> <Tooltip label={t('controller.devices.capabilities')} hasArrow>
{t('controller.devices.capabilities')} <IconButton
</Button> aria-label={t('controller.devices.capabilities')}
icon={<ListDashes size={20} />}
onClick={onOpen}
colorScheme="pink"
mr={2}
/>
</Tooltip>
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
title={t('controller.devices.capabilities')} title={t('controller.devices.capabilities')}

View File

@@ -7,11 +7,14 @@ import {
AccordionPanel, AccordionPanel,
Box, Box,
Button, Button,
IconButton,
Tooltip,
useClipboard, useClipboard,
useColorMode, useColorMode,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { JsonViewer } from '@textea/json-viewer'; import { JsonViewer } from '@textea/json-viewer';
import { Barcode } from 'phosphor-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal } from 'components/Modals/Modal'; import { Modal } from 'components/Modals/Modal';
import { DeviceConfiguration } from 'models/Device'; import { DeviceConfiguration } from 'models/Device';
@@ -30,9 +33,14 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
return ( return (
<> <>
<Button onClick={onOpen} isDisabled={!configuration} colorScheme="purple"> <Tooltip label={t('configurations.one')} hasArrow>
{t('configurations.one')} <IconButton
</Button> aria-label={t('configurations.one')}
icon={<Barcode size={20} />}
onClick={onOpen}
colorScheme="purple"
/>
</Tooltip>
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
title={t('configurations.one')} title={t('configurations.one')}

View File

@@ -1,18 +1,15 @@
import * as React from 'react'; import * as React from 'react';
import { import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Box, Box,
Button, Button,
Center,
Heading, Heading,
HStack, HStack,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Portal, Portal,
Spacer, Spacer,
Tag, Tag,
@@ -62,6 +59,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const toast = useToast(); const toast = useToast();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const cancelRef = React.useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({ const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({
serialNumber, serialNumber,
@@ -197,7 +195,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<Spacer /> <Spacer />
<HStack spacing={2}> <HStack spacing={2}>
{breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />}
<DeleteButton isCompact onClick={onDeleteOpen} />
{getDevice?.data && ( {getDevice?.data && (
<DeviceActionDropdown <DeviceActionDropdown
// @ts-ignore // @ts-ignore
@@ -216,33 +214,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
isCompact isCompact
/> />
)} )}
<Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}>
<Box>
<PopoverTrigger>
<DeleteButton isCompact onClick={() => {}} />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} {serialNumber}
</PopoverHeader>
<PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onDeleteClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
<RefreshButton <RefreshButton
onClick={refresh} onClick={refresh}
isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching}
@@ -274,6 +245,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<Spacer /> <Spacer />
<HStack spacing={2}> <HStack spacing={2}>
<DeviceSearchBar /> <DeviceSearchBar />
<DeleteButton isCompact onClick={onDeleteOpen} />
{getDevice?.data && ( {getDevice?.data && (
<DeviceActionDropdown <DeviceActionDropdown
// @ts-ignore // @ts-ignore
@@ -289,35 +261,9 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
onOpenRebootModal={rebootModalProps.onOpen} onOpenRebootModal={rebootModalProps.onOpen}
onOpenScriptModal={scriptModal.openModal} onOpenScriptModal={scriptModal.openModal}
size="md" size="md"
isCompact
/> />
)} )}
<Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}>
<Box>
<PopoverTrigger>
<DeleteButton isCompact onClick={() => {}} />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} {serialNumber}
</PopoverHeader>
<PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onDeleteClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
<RefreshButton <RefreshButton
onClick={refresh} onClick={refresh}
isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching}
@@ -330,6 +276,24 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
</Card> </Card>
</Portal> </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} /> <WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} /> <FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} /> <FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />

View File

@@ -7,9 +7,11 @@ import {
Box, Box,
Button, Button,
Center, Center,
IconButton,
Tag, Tag,
TagLabel, TagLabel,
Text, Text,
Tooltip,
useDisclosure, useDisclosure,
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
@@ -58,9 +60,14 @@ const UpdateDbButton = () => {
return ( return (
<> <>
<Button colorScheme="teal" leftIcon={<Database size={20} />} onClick={onOpen}> <Tooltip label={t('firmware.last_db_update_title')}>
{t('firmware.last_db_update_title')} <IconButton
</Button> aria-label={t('firmware.last_db_update_title')}
colorScheme="teal"
icon={<Database size={20} />}
onClick={onOpen}
/>
</Tooltip>
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; 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 { Download } from 'phosphor-react';
import { CSVLink } from 'react-csv'; import { CSVLink } from 'react-csv';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -127,9 +127,9 @@ const LogsCard = () => {
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
data={downloadableLogs as object[]} data={downloadableLogs as object[]}
> >
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> <Tooltip label={t('logs.export')} hasArrow>
{t('logs.export')} <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
</Button> </Tooltip>
</CSVLink> </CSVLink>
</HStack> </HStack>
</CardHeader> </CardHeader>

View File

@@ -1,5 +1,19 @@
import * as React from 'react'; 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 { Download } from 'phosphor-react';
import { CSVLink } from 'react-csv'; import { CSVLink } from 'react-csv';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -128,9 +142,9 @@ const FmsLogsCard = () => {
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
data={downloadableLogs as object[]} data={downloadableLogs as object[]}
> >
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> <Tooltip label={t('logs.export')} hasArrow>
{t('logs.export')} <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
</Button> </Tooltip>
</CSVLink> </CSVLink>
</HStack> </HStack>
</CardHeader> </CardHeader>

View File

@@ -1,5 +1,19 @@
import * as React from 'react'; 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 { Download } from 'phosphor-react';
import { CSVLink } from 'react-csv'; import { CSVLink } from 'react-csv';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -128,9 +142,9 @@ const GeneralLogsCard = () => {
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
data={downloadableLogs as object[]} data={downloadableLogs as object[]}
> >
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> <Tooltip label={t('logs.export')} hasArrow>
{t('logs.export')} <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
</Button> </Tooltip>
</CSVLink> </CSVLink>
</HStack> </HStack>
</CardHeader> </CardHeader>

View File

@@ -1,5 +1,19 @@
import * as React from 'react'; 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 { Download } from 'phosphor-react';
import { CSVLink } from 'react-csv'; import { CSVLink } from 'react-csv';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -128,9 +142,9 @@ const SecLogsCard = () => {
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
data={downloadableLogs as object[]} data={downloadableLogs as object[]}
> >
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> <Tooltip label={t('logs.export')} hasArrow>
{t('logs.export')} <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
</Button> </Tooltip>
</CSVLink> </CSVLink>
</HStack> </HStack>
</CardHeader> </CardHeader>

View File

@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Box } from '@chakra-ui/react';
import ApiKeyTable from './Table'; import ApiKeyTable from './Table';
import { Card } from 'components/Containers/Card'; import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody'; import { CardBody } from 'components/Containers/Card/CardBody';
@@ -10,7 +11,9 @@ const ApiKeysCard = () => {
return ( return (
<Card p={4}> <Card p={4}>
<CardBody> <CardBody>
<ApiKeyTable userId={user?.id ?? ''} /> <Box w="100%">
<ApiKeyTable userId={user?.id ?? ''} />
</Box>
</CardBody> </CardBody>
</Card> </Card>
); );

View File

@@ -1,5 +1,6 @@
import * as React from 'react'; 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 { useTranslation } from 'react-i18next';
import SystemLoggingModal from './Modal'; import SystemLoggingModal from './Modal';
import { EndpointApiResponse } from 'hooks/Network/Endpoints'; import { EndpointApiResponse } from 'hooks/Network/Endpoints';
@@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => {
return ( return (
<> <>
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto"> <Tooltip label={t('system.logging')} hasArrow>
{t('system.logging')} <IconButton
</Button> 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} /> <SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
</> </>
); );

View File

@@ -22,6 +22,7 @@ import { useTranslation } from 'react-i18next';
import FormattedDate from '../../../components/InformationDisplays/FormattedDate'; import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
import SystemLoggingButton from './LoggingButton'; import SystemLoggingButton from './LoggingButton';
import SystemCertificatesTable from './SystemCertificatesTable'; import SystemCertificatesTable from './SystemCertificatesTable';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { Card } from 'components/Containers/Card'; import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody'; import { CardBody } from 'components/Containers/Card/CardBody';
import { compactSecondsToDetailed } from 'helpers/dateFormatting'; import { compactSecondsToDetailed } from 'helpers/dateFormatting';
@@ -70,16 +71,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
<Heading pt={0}>{endpoint.type}</Heading> <Heading pt={0}>{endpoint.type}</Heading>
<Spacer /> <Spacer />
<SystemLoggingButton endpoint={endpoint} token={token} /> <SystemLoggingButton endpoint={endpoint} token={token} />
<Button <RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
mt={1}
minWidth="112px"
colorScheme="blue"
rightIcon={<ArrowsClockwise />}
onClick={refresh}
isLoading={isFetchingSystem || isFetchingSubsystems}
>
{t('common.refresh')}
</Button>
</Box> </Box>
<CardBody> <CardBody>
<VStack w="100%"> <VStack w="100%">

View File

@@ -77,7 +77,7 @@ const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm',
return ( return (
<Menu> <Menu>
<Tooltip label={t('commands.other')}> <Tooltip label={t('common.actions')}>
<MenuButton <MenuButton
as={IconButton} as={IconButton}
aria-label="Commands" aria-label="Commands"

View File

@@ -1,11 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { AddIcon } from '@chakra-ui/icons'; import { useDisclosure } from '@chakra-ui/react';
import { Button, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SaveButton } from '../../../../components/Buttons/SaveButton'; import { SaveButton } from '../../../../components/Buttons/SaveButton';
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert'; import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
import { Modal } from '../../../../components/Modals/Modal'; import { Modal } from '../../../../components/Modals/Modal';
import CreateUserForm, { CreateUserFormValues } from './Form'; import CreateUserForm, { CreateUserFormValues } from './Form';
import { CreateButton } from 'components/Buttons/CreateButton';
import { useAuth } from 'contexts/AuthProvider'; import { useAuth } from 'contexts/AuthProvider';
import { useFormRef } from 'hooks/useFormRef'; import { useFormRef } from 'hooks/useFormRef';
@@ -25,16 +25,7 @@ const CreateUserModal = () => {
return ( return (
<> <>
<Button {user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
hidden={user?.userRole === 'csr'}
alignItems="center"
colorScheme="blue"
rightIcon={<AddIcon />}
onClick={onOpen}
ml={2}
>
{t('crud.create')}
</Button>
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onClose={closeModal} onClose={closeModal}

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react'; import { Avatar, Box, Flex, useDisclosure } from '@chakra-ui/react';
import { ArrowsClockwise } from 'phosphor-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker'; import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
@@ -9,6 +8,7 @@ import FormattedDate from '../../../components/InformationDisplays/FormattedDate
import CreateUserModal from './CreateUserModal'; import CreateUserModal from './CreateUserModal';
import EditUserModal from './EditUserModal'; import EditUserModal from './EditUserModal';
import UserActions from './UserActions'; import UserActions from './UserActions';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { Card } from 'components/Containers/Card'; import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody'; import { CardBody } from 'components/Containers/Card/CardBody';
import { CardHeader } from 'components/Containers/Card/CardHeader'; import { CardHeader } from 'components/Containers/Card/CardHeader';
@@ -125,15 +125,7 @@ const UserTable = () => {
preference="provisioning.userTable.hiddenColumns" preference="provisioning.userTable.hiddenColumns"
/> />
<CreateUserModal /> <CreateUserModal />
<Button <RefreshButton onClick={refreshUsers} isFetching={isFetching} ml={2} />
colorScheme="gray"
onClick={() => refreshUsers()}
rightIcon={<ArrowsClockwise />}
ml={2}
isLoading={isFetching}
>
{t('common.refresh')}
</Button>
</Box> </Box>
</Flex> </Flex>
</CardHeader> </CardHeader>

View File

@@ -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 CardComponent from './additions/card/Card';
import CardBodyComponent from './additions/card/CardBody'; import CardBodyComponent from './additions/card/CardBody';
import CardHeaderComponent from './additions/card/CardHeader'; import CardHeaderComponent from './additions/card/CardHeader';
@@ -37,4 +37,6 @@ const theme = extendTheme({
}, },
}); });
Tooltip.defaultProps = { ...Tooltip.defaultProps, hasArrow: true };
export default theme; export default theme;

View File

@@ -1,49 +1,9 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import { VitePWA } from 'vite-plugin-pwa';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [tsconfigPaths(), react()],
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',
},
],
},
}),
],
build: { build: {
outDir: './build', outDir: './build',
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,