mirror of
				https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
				synced 2025-10-30 17:57:46 +00:00 
			
		
		
		
	[WIFI-12360] Custom script run fix
Signed-off-by: Charles <charles.bourque96@gmail.com>
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(16)", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "ucentral-client", | ||||
|       "version": "2.9.0(13)", | ||||
|       "version": "2.9.0(16)", | ||||
|       "license": "ISC", | ||||
|       "dependencies": { | ||||
|         "@chakra-ui/icons": "^2.0.11", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "ucentral-client", | ||||
|   "version": "2.9.0(13)", | ||||
|   "version": "2.9.0(16)", | ||||
|   "description": "", | ||||
|   "private": true, | ||||
|   "main": "index.tsx", | ||||
|   | ||||
| @@ -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} | ||||
|                             fontFamily={ | ||||
|                               // @ts-ignore | ||||
|                             fontFamily={cell.column.isMonospace ? 'monospace' : undefined} | ||||
|                               cell.column.isMonospace | ||||
|                                 ? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace' | ||||
|                                 : undefined | ||||
|                             } | ||||
|                             onClick={ | ||||
|                               // @ts-ignore | ||||
|                               cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick) | ||||
|                                 ? (e) => { | ||||
|                                     e.stopPropagation(); | ||||
|                                   } | ||||
|                                 : undefined | ||||
|                             } | ||||
|                             cursor={ | ||||
|                               // @ts-ignore | ||||
|                               !cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick | ||||
|                                 ? 'pointer' | ||||
|                                 : undefined | ||||
|                             } | ||||
|                           > | ||||
|                             {cell.render('Cell')} | ||||
|                           </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} | ||||
|                             fontFamily={ | ||||
|                               // @ts-ignore | ||||
|                             fontFamily={cell.column.isMonospace ? 'monospace' : undefined} | ||||
|                               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,7 +98,8 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | ||||
|         </Alert> | ||||
|         <FormControl isInvalid={!isValid && newConfig.length > 0}> | ||||
|           <FormLabel>{t('configurations.one')}</FormLabel> | ||||
|           <Box mb={2} w="240px"> | ||||
|           <Flex mb={2}> | ||||
|             <Box w="240px"> | ||||
|               <FileInputButton | ||||
|                 value={newConfig} | ||||
|                 setValue={(v) => setNewConfig(v)} | ||||
| @@ -101,6 +108,15 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | ||||
|                 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({ | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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 ( | ||||
|     <Card borderWidth="2px"> | ||||
|       <Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}> | ||||
|         <CircularProgress | ||||
|           isIndeterminate | ||||
|           color={circleColor()} | ||||
|           position="absolute" | ||||
|           right="6px" | ||||
|           top="6px" | ||||
|           w="unset" | ||||
|           size={6} | ||||
|           thickness="14px" | ||||
|         > | ||||
|           <CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel> | ||||
|         </CircularProgress> | ||||
|       </Tooltip> | ||||
|       <Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}> | ||||
|         <Box position="absolute" right="8px" top="8px" w="unset" hidden> | ||||
|           <Clock size={16} /> | ||||
|         </Box> | ||||
|       </Tooltip> | ||||
|       <VStack mb={-1}> | ||||
|         <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} | ||||
|         > | ||||
|           Rx: {bytesString(getStats.data.rx)}, Tx: {bytesString(getStats.data.tx)} | ||||
|           <Heading size="xs" display="flex" justifyContent="center"> | ||||
|             <Text> | ||||
|               {t('common.connected')} {t('devices.title')}{' '} | ||||
|             </Text>{' '} | ||||
|           </Heading> | ||||
|           <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> | ||||
|           <Heading size="md" textAlign="center" mt={2}> | ||||
|             <Heading size="md" textAlign="center" mt={1}> | ||||
|               {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} | ||||
|             </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 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> | ||||
|   | ||||
| @@ -35,7 +35,10 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | ||||
|   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[], | ||||
| @@ -100,6 +103,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); | ||||
|   | ||||
| @@ -1,8 +1,18 @@ | ||||
| import * as React from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   Center, | ||||
|   Heading, | ||||
|   HStack, | ||||
|   Popover, | ||||
|   PopoverArrow, | ||||
|   PopoverBody, | ||||
|   PopoverCloseButton, | ||||
|   PopoverContent, | ||||
|   PopoverFooter, | ||||
|   PopoverHeader, | ||||
|   PopoverTrigger, | ||||
|   Portal, | ||||
|   Spacer, | ||||
|   Tag, | ||||
| @@ -12,10 +22,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 +36,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 +52,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 +60,16 @@ type Props = { | ||||
|  | ||||
| const DevicePageWrapper = ({ serialNumber }: Props) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const toast = useToast(); | ||||
|   const breakpoint = useBreakpoint(); | ||||
|   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 +82,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 +197,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | ||||
|             <Spacer /> | ||||
|             <HStack spacing={2}> | ||||
|               {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} | ||||
|  | ||||
|               {getDevice?.data && ( | ||||
|                 <DeviceActionDropdown | ||||
|                   // @ts-ignore | ||||
| @@ -166,6 +216,33 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | ||||
|                   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 | ||||
|                 onClick={refresh} | ||||
|                 isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} | ||||
| @@ -214,6 +291,33 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | ||||
|                     size="md" | ||||
|                   /> | ||||
|                 )} | ||||
|                 <Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}> | ||||
|                   <Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}> | ||||
|                     <Box> | ||||
|                       <PopoverTrigger> | ||||
|                         <DeleteButton isCompact onClick={() => {}} /> | ||||
|                       </PopoverTrigger> | ||||
|                     </Box> | ||||
|                   </Tooltip> | ||||
|                   <PopoverContent> | ||||
|                     <PopoverArrow /> | ||||
|                     <PopoverCloseButton /> | ||||
|                     <PopoverHeader> | ||||
|                       {t('crud.delete')} {serialNumber} | ||||
|                     </PopoverHeader> | ||||
|                     <PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody> | ||||
|                     <PopoverFooter> | ||||
|                       <Center> | ||||
|                         <Button colorScheme="gray" mr="1" onClick={onDeleteClose}> | ||||
|                           {t('common.cancel')} | ||||
|                         </Button> | ||||
|                         <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}> | ||||
|                           {t('common.yes')} | ||||
|                         </Button> | ||||
|                       </Center> | ||||
|                     </PopoverFooter> | ||||
|                   </PopoverContent> | ||||
|                 </Popover> | ||||
|                 <RefreshButton | ||||
|                   onClick={refresh} | ||||
|                   isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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,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> | ||||
|   | ||||
| @@ -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', | ||||
| @@ -139,14 +139,15 @@ const UserTable = () => { | ||||
|         </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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Charles
					Charles