mirror of
				https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
				synced 2025-10-31 02:07:45 +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", |   "name": "ucentral-client", | ||||||
|   "version": "2.9.0(13)", |   "version": "2.9.0(16)", | ||||||
|   "lockfileVersion": 2, |   "lockfileVersion": 2, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "ucentral-client", |       "name": "ucentral-client", | ||||||
|       "version": "2.9.0(13)", |       "version": "2.9.0(16)", | ||||||
|       "license": "ISC", |       "license": "ISC", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@chakra-ui/icons": "^2.0.11", |         "@chakra-ui/icons": "^2.0.11", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "ucentral-client", |   "name": "ucentral-client", | ||||||
|   "version": "2.9.0(13)", |   "version": "2.9.0(16)", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "main": "index.tsx", |   "main": "index.tsx", | ||||||
|   | |||||||
| @@ -44,12 +44,14 @@ const defaultProps = { | |||||||
|   sortBy: [], |   sortBy: [], | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type DataTableProps = { | export type DataTableProps<TValue> = { | ||||||
|   columns: readonly Column<object>[]; |   columns: Column<TValue>[]; | ||||||
|   data: object[]; |   data: TValue[]; | ||||||
|   count?: number; |   count?: number; | ||||||
|   setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>; |   setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>; | ||||||
|   isLoading?: boolean; |   isLoading?: boolean; | ||||||
|  |   onRowClick?: (row: TValue) => void; | ||||||
|  |   isRowClickable?: (row: TValue) => boolean; | ||||||
|   obj?: string; |   obj?: string; | ||||||
|   sortBy?: { id: string; desc: boolean }[]; |   sortBy?: { id: string; desc: boolean }[]; | ||||||
|   hiddenColumns?: string[]; |   hiddenColumns?: string[]; | ||||||
| @@ -68,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> & | |||||||
|     state: UsePaginationState<T>; |     state: UsePaginationState<T>; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| const _DataTable = ({ | const _DataTable = <TValue extends object>({ | ||||||
|   columns, |   columns, | ||||||
|   data, |   data, | ||||||
|   isLoading, |   isLoading, | ||||||
| @@ -84,7 +86,9 @@ const _DataTable = ({ | |||||||
|   isManual, |   isManual, | ||||||
|   saveSettingsId, |   saveSettingsId, | ||||||
|   showAllRows, |   showAllRows, | ||||||
| }: DataTableProps) => { |   onRowClick, | ||||||
|  |   isRowClickable, | ||||||
|  | }: DataTableProps<TValue>) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|   const textColor = useColorModeValue('gray.700', 'white'); |   const textColor = useColorModeValue('gray.700', 'white'); | ||||||
| @@ -143,7 +147,7 @@ const _DataTable = ({ | |||||||
|     }, |     }, | ||||||
|     useSortBy, |     useSortBy, | ||||||
|     usePagination, |     usePagination, | ||||||
|   ) as TableInstanceWithHooks<object>; |   ) as TableInstanceWithHooks<TValue>; | ||||||
|  |  | ||||||
|   const handleGoToPage = (newPage: number) => { |   const handleGoToPage = (newPage: number) => { | ||||||
|     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage)); |     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage)); | ||||||
| @@ -260,8 +264,10 @@ const _DataTable = ({ | |||||||
|             </Thead> |             </Thead> | ||||||
|             {data.length > 0 && ( |             {data.length > 0 && ( | ||||||
|               <Tbody {...getTableBodyProps()}> |               <Tbody {...getTableBodyProps()}> | ||||||
|                 {page.map((row: Row) => { |                 {page.map((row: Row<TValue>) => { | ||||||
|                   prepareRow(row); |                   prepareRow(row); | ||||||
|  |                   const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true; | ||||||
|  |                   const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined; | ||||||
|                   return ( |                   return ( | ||||||
|                     <Tr |                     <Tr | ||||||
|                       {...row.getRowProps()} |                       {...row.getRowProps()} | ||||||
| @@ -269,6 +275,7 @@ const _DataTable = ({ | |||||||
|                       _hover={{ |                       _hover={{ | ||||||
|                         backgroundColor: hoveredRowBg, |                         backgroundColor: hoveredRowBg, | ||||||
|                       }} |                       }} | ||||||
|  |                       onClick={onClick} | ||||||
|                     > |                     > | ||||||
|                       { |                       { | ||||||
|                         // @ts-ignore |                         // @ts-ignore | ||||||
| @@ -288,8 +295,26 @@ const _DataTable = ({ | |||||||
|                             fontSize="14px" |                             fontSize="14px" | ||||||
|                             // @ts-ignore |                             // @ts-ignore | ||||||
|                             textAlign={cell.column.isCentered ? 'center' : undefined} |                             textAlign={cell.column.isCentered ? 'center' : undefined} | ||||||
|  |                             fontFamily={ | ||||||
|                               // @ts-ignore |                               // @ts-ignore | ||||||
|                             fontFamily={cell.column.isMonospace ? 'monospace' : undefined} |                               cell.column.isMonospace | ||||||
|  |                                 ? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace' | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|  |                             onClick={ | ||||||
|  |                               // @ts-ignore | ||||||
|  |                               cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick) | ||||||
|  |                                 ? (e) => { | ||||||
|  |                                     e.stopPropagation(); | ||||||
|  |                                   } | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|  |                             cursor={ | ||||||
|  |                               // @ts-ignore | ||||||
|  |                               !cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick | ||||||
|  |                                 ? 'pointer' | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|                           > |                           > | ||||||
|                             {cell.render('Cell')} |                             {cell.render('Cell')} | ||||||
|                           </Td> |                           </Td> | ||||||
| @@ -414,4 +439,4 @@ const _DataTable = ({ | |||||||
|  |  | ||||||
| _DataTable.defaultProps = defaultProps; | _DataTable.defaultProps = defaultProps; | ||||||
|  |  | ||||||
| export const DataTable = React.memo(_DataTable); | export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable; | ||||||
|   | |||||||
| @@ -249,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({ | |||||||
|                             fontSize="14px" |                             fontSize="14px" | ||||||
|                             // @ts-ignore |                             // @ts-ignore | ||||||
|                             textAlign={cell.column.isCentered ? 'center' : undefined} |                             textAlign={cell.column.isCentered ? 'center' : undefined} | ||||||
|  |                             fontFamily={ | ||||||
|                               // @ts-ignore |                               // @ts-ignore | ||||||
|                             fontFamily={cell.column.isMonospace ? 'monospace' : undefined} |                               cell.column.isMonospace | ||||||
|  |                                 ? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace' | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|                           > |                           > | ||||||
|                             {cell.render('Cell')} |                             {cell.render('Cell')} | ||||||
|                           </Td> |                           </Td> | ||||||
|   | |||||||
| @@ -5,17 +5,22 @@ import { | |||||||
|   AlertIcon, |   AlertIcon, | ||||||
|   AlertTitle, |   AlertTitle, | ||||||
|   Box, |   Box, | ||||||
|  |   Button, | ||||||
|  |   Flex, | ||||||
|   FormControl, |   FormControl, | ||||||
|   FormErrorMessage, |   FormErrorMessage, | ||||||
|   FormLabel, |   FormLabel, | ||||||
|   Textarea, |   Textarea, | ||||||
|   useToast, |   useToast, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
|  | import { ClipboardText } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { SaveButton } from '../../Buttons/SaveButton'; | import { SaveButton } from '../../Buttons/SaveButton'; | ||||||
| import { Modal } from '../Modal'; | import { Modal } from '../Modal'; | ||||||
| import { FileInputButton } from 'components/Buttons/FileInputButton'; | import { FileInputButton } from 'components/Buttons/FileInputButton'; | ||||||
| import { useConfigureDevice } from 'hooks/Network/Commands'; | import { useConfigureDevice } from 'hooks/Network/Commands'; | ||||||
|  | import { useGetDevice } from 'hooks/Network/Devices'; | ||||||
|  | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
| export type ConfigureModalProps = { | export type ConfigureModalProps = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const configure = useConfigureDevice({ serialNumber }); |   const configure = useConfigureDevice({ serialNumber }); | ||||||
|  |   const getDevice = useGetDevice({ serialNumber }); | ||||||
|  |  | ||||||
|   const [newConfig, setNewConfig] = React.useState(''); |   const [newConfig, setNewConfig] = React.useState(''); | ||||||
|  |  | ||||||
|   const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { |   const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|     setNewConfig(e.target.value); |     setNewConfig(e.target.value); | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|  |   const onImportConfiguration = () => { | ||||||
|  |     setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : ''); | ||||||
|  |   }; | ||||||
|   const isValid = React.useMemo(() => { |   const isValid = React.useMemo(() => { | ||||||
|     try { |     try { | ||||||
|       JSON.parse(newConfig); |       JSON.parse(newConfig); | ||||||
| @@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|           modalProps.onClose(); |           modalProps.onClose(); | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     } catch (e) { |     } catch (e) {} | ||||||
|       // console.log(e); |  | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|             <AlertIcon /> |             <AlertIcon /> | ||||||
|             <Box> |             <Box> | ||||||
|               <AlertTitle>{t('common.error')}</AlertTitle> |               <AlertTitle>{t('common.error')}</AlertTitle> | ||||||
|               { |               <AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription> | ||||||
|                 // @ts-ignore |  | ||||||
|                 <AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription> |  | ||||||
|               } |  | ||||||
|             </Box> |             </Box> | ||||||
|           </Alert> |           </Alert> | ||||||
|         )} |         )} | ||||||
| @@ -92,7 +98,8 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|         </Alert> |         </Alert> | ||||||
|         <FormControl isInvalid={!isValid && newConfig.length > 0}> |         <FormControl isInvalid={!isValid && newConfig.length > 0}> | ||||||
|           <FormLabel>{t('configurations.one')}</FormLabel> |           <FormLabel>{t('configurations.one')}</FormLabel> | ||||||
|           <Box mb={2} w="240px"> |           <Flex mb={2}> | ||||||
|  |             <Box w="240px"> | ||||||
|               <FileInputButton |               <FileInputButton | ||||||
|                 value={newConfig} |                 value={newConfig} | ||||||
|                 setValue={(v) => setNewConfig(v)} |                 setValue={(v) => setNewConfig(v)} | ||||||
| @@ -101,6 +108,15 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|                 isStringFile |                 isStringFile | ||||||
|               /> |               /> | ||||||
|             </Box> |             </Box> | ||||||
|  |             <Button | ||||||
|  |               rightIcon={<ClipboardText size={20} />} | ||||||
|  |               onClick={onImportConfiguration} | ||||||
|  |               hidden={!getDevice.data} | ||||||
|  |               ml={2} | ||||||
|  |             > | ||||||
|  |               Current Configuration | ||||||
|  |             </Button> | ||||||
|  |           </Flex> | ||||||
|           <Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} /> |           <Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} /> | ||||||
|           <FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage> |           <FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage> | ||||||
|         </FormControl> |         </FormControl> | ||||||
|   | |||||||
| @@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => { | |||||||
|     let requestData: { |     let requestData: { | ||||||
|       [k: string]: unknown; |       [k: string]: unknown; | ||||||
|       serialNumber: string; |       serialNumber: string; | ||||||
|  |       script?: string; | ||||||
|       timeout?: number | undefined; |       timeout?: number | undefined; | ||||||
|     } = data; |     } = data; | ||||||
|  |  | ||||||
|  |     if (requestData.script) { | ||||||
|  |       requestData.script = btoa(requestData.script); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (selectedScript === 'diagnostics') { |     if (selectedScript === 'diagnostics') { | ||||||
|       requestData = { |       requestData = { | ||||||
|         serialNumber: device?.serialNumber ?? '', |         serialNumber: device?.serialNumber ?? '', | ||||||
| @@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => { | |||||||
|         setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2)); |         setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2)); | ||||||
|         queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']); |         queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']); | ||||||
|       }, |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         if (axios.isAxiosError(e) && e.response?.data?.ErrorDescription) { | ||||||
|  |           toast({ | ||||||
|  |             id: 'script-update-error', | ||||||
|  |             title: t('common.error'), | ||||||
|  |             description: e.response?.data?.ErrorDescription, | ||||||
|  |             status: 'error', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|     }); |     }); | ||||||
|     if (!waitForResponse) { |     if (!waitForResponse) { | ||||||
|       toast({ |       toast({ | ||||||
|   | |||||||
| @@ -110,6 +110,18 @@ export const useDeleteCommand = () => { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const useGetSingleCommandHistory = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => | ||||||
|  |   useQuery( | ||||||
|  |     ['commands', serialNumber, commandId], | ||||||
|  |     () => | ||||||
|  |       axiosGw | ||||||
|  |         .get(`command/${commandId}?serialNumber=${serialNumber}`) | ||||||
|  |         .then((response) => response.data as DeviceCommandHistory), | ||||||
|  |     { | ||||||
|  |       enabled: serialNumber !== undefined && serialNumber !== '' && commandId !== undefined && commandId !== '', | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  |  | ||||||
| export type EventQueueResponse = { | export type EventQueueResponse = { | ||||||
|   UUID: string; |   UUID: string; | ||||||
|   attachFile: number; |   attachFile: number; | ||||||
| @@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => { | |||||||
|       queryClient.invalidateQueries(['commands', serialNumber]); |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|     }, |     }, | ||||||
|     onError: (e) => { |     onError: (e) => { | ||||||
|  |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|       if (axios.isAxiosError(e)) { |       if (axios.isAxiosError(e)) { | ||||||
|         toast({ |         toast({ | ||||||
|           id: 'script-error', |           id: 'script-error', | ||||||
|   | |||||||
| @@ -1,6 +1,18 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react'; | import { | ||||||
|  |   Box, | ||||||
|  |   CircularProgress, | ||||||
|  |   CircularProgressLabel, | ||||||
|  |   Flex, | ||||||
|  |   Heading, | ||||||
|  |   Icon, | ||||||
|  |   Text, | ||||||
|  |   Tooltip, | ||||||
|  |   VStack, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { ArrowSquareDown, ArrowSquareUp, Clock } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Card } from 'components/Containers/Card'; | ||||||
| import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; | import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; | ||||||
| import { bytesString } from 'helpers/stringHelper'; | import { bytesString } from 'helpers/stringHelper'; | ||||||
| import { useGetDevicesStats } from 'hooks/Network/Devices'; | import { useGetDevicesStats } from 'hooks/Network/Devices'; | ||||||
| @@ -11,18 +23,19 @@ const SidebarDevices = () => { | |||||||
|   const [lastTime, setLastTime] = React.useState<Date | undefined>(); |   const [lastTime, setLastTime] = React.useState<Date | undefined>(); | ||||||
|   const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>(); |   const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>(); | ||||||
|  |  | ||||||
|   const getTime = () => { |   const time = React.useMemo(() => { | ||||||
|     if (lastTime === undefined || lastUpdate === undefined) return null; |     if (lastTime === undefined || lastUpdate === undefined) return null; | ||||||
|  |  | ||||||
|     const seconds = lastTime.getTime() - lastUpdate.getTime(); |     const seconds = lastTime.getTime() - lastUpdate.getTime(); | ||||||
|  |  | ||||||
|     return Math.max(0, Math.floor(seconds / 1000)); |     return Math.max(0, Math.floor(seconds / 1000)); | ||||||
|   }; |   }, [lastTime, lastUpdate]); | ||||||
|  |  | ||||||
|   const refresh = () => { |   const circleColor = () => { | ||||||
|     if (document.visibilityState !== 'hidden') { |     if (time === null) return 'gray.300'; | ||||||
|       getStats.refetch(); |     if (time < 10) return 'green.300'; | ||||||
|     } |     if (time < 30) return 'yellow.300'; | ||||||
|  |     return 'red.300'; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
| @@ -38,47 +51,60 @@ const SidebarDevices = () => { | |||||||
|     }; |     }; | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     document.addEventListener('visibilitychange', refresh); |  | ||||||
|  |  | ||||||
|     return () => { |  | ||||||
|       document.removeEventListener('visibilitychange', refresh); |  | ||||||
|     }; |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   if (!getStats.data) return null; |   if (!getStats.data) return null; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|  |     <Card borderWidth="2px"> | ||||||
|  |       <Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}> | ||||||
|  |         <CircularProgress | ||||||
|  |           isIndeterminate | ||||||
|  |           color={circleColor()} | ||||||
|  |           position="absolute" | ||||||
|  |           right="6px" | ||||||
|  |           top="6px" | ||||||
|  |           w="unset" | ||||||
|  |           size={6} | ||||||
|  |           thickness="14px" | ||||||
|  |         > | ||||||
|  |           <CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel> | ||||||
|  |         </CircularProgress> | ||||||
|  |       </Tooltip> | ||||||
|  |       <Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}> | ||||||
|  |         <Box position="absolute" right="8px" top="8px" w="unset" hidden> | ||||||
|  |           <Clock size={16} /> | ||||||
|  |         </Box> | ||||||
|  |       </Tooltip> | ||||||
|       <VStack mb={-1}> |       <VStack mb={-1}> | ||||||
|         <Flex flexDir="column" textAlign="center"> |         <Flex flexDir="column" textAlign="center"> | ||||||
|           <Heading size="md">{getStats.data.connectedDevices}</Heading> |           <Heading size="md">{getStats.data.connectedDevices}</Heading> | ||||||
|         <Heading size="xs"> |           <Heading size="xs" display="flex" justifyContent="center"> | ||||||
|           {t('common.connected')} {t('devices.title')} |             <Text> | ||||||
|         </Heading> |               {t('common.connected')} {t('devices.title')}{' '} | ||||||
|         <Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400"> |             </Text>{' '} | ||||||
|           ({getStats.data.connectingDevices} {t('controller.devices.connecting')}) |  | ||||||
|         </Heading> |  | ||||||
|         <Heading |  | ||||||
|           size="xs" |  | ||||||
|           mt={1} |  | ||||||
|           fontStyle="italic" |  | ||||||
|           fontWeight="normal" |  | ||||||
|           color="gray.400" |  | ||||||
|           hidden={getStats.data.rx === undefined || getStats.data.tx === undefined} |  | ||||||
|         > |  | ||||||
|           Rx: {bytesString(getStats.data.rx)}, Tx: {bytesString(getStats.data.tx)} |  | ||||||
|           </Heading> |           </Heading> | ||||||
|           <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> |           <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> | ||||||
|           <Heading size="md" textAlign="center" mt={2}> |             <Heading size="md" textAlign="center" mt={1}> | ||||||
|               {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} |               {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} | ||||||
|             </Heading> |             </Heading> | ||||||
|           </Tooltip> |           </Tooltip> | ||||||
|           <Heading size="xs">{t('controller.devices.average_uptime')}</Heading> |           <Heading size="xs">{t('controller.devices.average_uptime')}</Heading> | ||||||
|         <Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400"> |           <Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}> | ||||||
|           {t('controller.stats.seconds_ago', { s: getTime() })} |             <Tooltip hasArrow label="Rx"> | ||||||
|         </Heading> |               <Flex alignItems="center" mr={1}> | ||||||
|  |                 <Icon as={ArrowSquareUp} weight="bold" boxSize={5} mt="1px" color="blue.400" />{' '} | ||||||
|  |                 {getStats.data.rx !== undefined ? bytesString(getStats.data.rx, 0) : '-'} | ||||||
|  |               </Flex> | ||||||
|  |             </Tooltip> | ||||||
|  |             <Tooltip hasArrow label="Tx"> | ||||||
|  |               <Flex alignItems="center"> | ||||||
|  |                 <Icon as={ArrowSquareDown} weight="bold" boxSize={5} mt="1px" color="purple.400" />{' '} | ||||||
|  |                 {getStats.data.tx !== undefined ? bytesString(getStats.data.tx, 0) : '-'} | ||||||
|  |               </Flex> | ||||||
|  |             </Tooltip> | ||||||
|  |           </Flex> | ||||||
|         </Flex> |         </Flex> | ||||||
|       </VStack> |       </VStack> | ||||||
|  |     </Card> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ export interface Column<T> { | |||||||
|   alwaysShow?: boolean; |   alwaysShow?: boolean; | ||||||
|   Footer?: string; |   Footer?: string; | ||||||
|   accessor?: string; |   accessor?: string; | ||||||
|  |   stopPropagation?: boolean; | ||||||
|   disableSortBy?: boolean; |   disableSortBy?: boolean; | ||||||
|   hasPopover?: boolean; |   hasPopover?: boolean; | ||||||
|   customMaxWidth?: string; |   customMaxWidth?: string; | ||||||
|   | |||||||
| @@ -99,13 +99,14 @@ const DefaultConfigurationsList = () => { | |||||||
|       <CardBody> |       <CardBody> | ||||||
|         <Box overflowX="auto" w="100%"> |         <Box overflowX="auto" w="100%"> | ||||||
|           <LoadingOverlay isLoading={getConfigs.isFetching}> |           <LoadingOverlay isLoading={getConfigs.isFetching}> | ||||||
|             <DataTable |             <DataTable<DefaultConfigurationResponse> | ||||||
|               columns={columns as Column<object>[]} |               columns={columns} | ||||||
|               saveSettingsId="firmware.table" |               saveSettingsId="firmware.table" | ||||||
|               data={getConfigs.data ?? []} |               data={getConfigs.data ?? []} | ||||||
|               obj={t('controller.configurations.title')} |               obj={t('controller.configurations.title')} | ||||||
|               minHeight="200px" |               minHeight="200px" | ||||||
|               sortBy={[{ id: 'name', desc: true }]} |               sortBy={[{ id: 'name', desc: true }]} | ||||||
|  |               onRowClick={onViewDetails} | ||||||
|             /> |             /> | ||||||
|           </LoadingOverlay> |           </LoadingOverlay> | ||||||
|         </Box> |         </Box> | ||||||
|   | |||||||
| @@ -35,7 +35,10 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|   const parsedData = React.useMemo(() => { |   const parsedData = React.useMemo(() => { | ||||||
|     if (!getStats.data && !getCustomStats.data) return undefined; |     if (!getStats.data && !getCustomStats.data) return undefined; | ||||||
|  |  | ||||||
|     const data: Record<string, { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number }> = {}; |     const data: Record< | ||||||
|  |       string, | ||||||
|  |       { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number; removed?: boolean } | ||||||
|  |     > = {}; | ||||||
|     const memoryData = { |     const memoryData = { | ||||||
|       used: [] as number[], |       used: [] as number[], | ||||||
|       buffered: [] as number[], |       buffered: [] as number[], | ||||||
| @@ -100,6 +103,18 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|               maxRx: rxDelta, |               maxRx: rxDelta, | ||||||
|             }; |             }; | ||||||
|           else { |           else { | ||||||
|  |             if (data[inter.name] && !data[inter.name]?.removed && data[inter.name]?.recorded.length === 1) { | ||||||
|  |               data[inter.name]?.tx.shift(); | ||||||
|  |               data[inter.name]?.rx.shift(); | ||||||
|  |               data[inter.name]?.recorded.shift(); | ||||||
|  |               // @ts-ignore | ||||||
|  |               data[inter.name].maxRx = rxDelta; | ||||||
|  |               // @ts-ignore | ||||||
|  |               data[inter.name].maxTx = txDelta; | ||||||
|  |               // @ts-ignore | ||||||
|  |               data[inter.name].removed = true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             data[inter.name]?.rx.push(rxDelta); |             data[inter.name]?.rx.push(rxDelta); | ||||||
|             data[inter.name]?.tx.push(txDelta); |             data[inter.name]?.tx.push(txDelta); | ||||||
|             data[inter.name]?.recorded.push(stat.recorded); |             data[inter.name]?.recorded.push(stat.recorded); | ||||||
|   | |||||||
| @@ -1,8 +1,18 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { | ||||||
|   Box, |   Box, | ||||||
|  |   Button, | ||||||
|  |   Center, | ||||||
|   Heading, |   Heading, | ||||||
|   HStack, |   HStack, | ||||||
|  |   Popover, | ||||||
|  |   PopoverArrow, | ||||||
|  |   PopoverBody, | ||||||
|  |   PopoverCloseButton, | ||||||
|  |   PopoverContent, | ||||||
|  |   PopoverFooter, | ||||||
|  |   PopoverHeader, | ||||||
|  |   PopoverTrigger, | ||||||
|   Portal, |   Portal, | ||||||
|   Spacer, |   Spacer, | ||||||
|   Tag, |   Tag, | ||||||
| @@ -12,10 +22,13 @@ import { | |||||||
|   useBreakpoint, |   useBreakpoint, | ||||||
|   useColorModeValue, |   useColorModeValue, | ||||||
|   useDisclosure, |   useDisclosure, | ||||||
|  |   useToast, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
|  | import axios from 'axios'; | ||||||
| import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react'; | import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import Masonry from 'react-masonry-css'; | import Masonry from 'react-masonry-css'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
| import DeviceDetails from './Details'; | import DeviceDetails from './Details'; | ||||||
| import DeviceLogsCard from './LogsCard'; | import DeviceLogsCard from './LogsCard'; | ||||||
| import DeviceNotes from './Notes'; | import DeviceNotes from './Notes'; | ||||||
| @@ -23,6 +36,7 @@ import RestrictionsCard from './RestrictionsCard'; | |||||||
| import DeviceStatisticsCard from './StatisticsCard'; | import DeviceStatisticsCard from './StatisticsCard'; | ||||||
| import DeviceSummary from './Summary'; | import DeviceSummary from './Summary'; | ||||||
| import WifiAnalysisCard from './WifiAnalysis'; | import WifiAnalysisCard from './WifiAnalysis'; | ||||||
|  | import { DeleteButton } from 'components/Buttons/DeleteButton'; | ||||||
| import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown'; | import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown'; | ||||||
| import { RefreshButton } from 'components/Buttons/RefreshButton'; | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| @@ -38,7 +52,7 @@ import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | |||||||
| import { TelemetryModal } from 'components/Modals/TelemetryModal'; | import { TelemetryModal } from 'components/Modals/TelemetryModal'; | ||||||
| import { TraceModal } from 'components/Modals/TraceModal'; | import { TraceModal } from 'components/Modals/TraceModal'; | ||||||
| import { WifiScanModal } from 'components/Modals/WifiScanModal'; | import { WifiScanModal } from 'components/Modals/WifiScanModal'; | ||||||
| import { useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; | import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -46,10 +60,16 @@ type Props = { | |||||||
|  |  | ||||||
| const DevicePageWrapper = ({ serialNumber }: Props) => { | const DevicePageWrapper = ({ serialNumber }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |   const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({ | ||||||
|  |     serialNumber, | ||||||
|  |   }); | ||||||
|   const getDevice = useGetDevice({ serialNumber }); |   const getDevice = useGetDevice({ serialNumber }); | ||||||
|   const getStatus = useGetDeviceStatus({ serialNumber }); |   const getStatus = useGetDeviceStatus({ serialNumber }); | ||||||
|   const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 }); |   const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 }); | ||||||
|  |   const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure(); | ||||||
|   const scanModalProps = useDisclosure(); |   const scanModalProps = useDisclosure(); | ||||||
|   const resetModalProps = useDisclosure(); |   const resetModalProps = useDisclosure(); | ||||||
|   const eventQueueProps = useDisclosure(); |   const eventQueueProps = useDisclosure(); | ||||||
| @@ -62,6 +82,35 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|   // Sticky-top styles |   // Sticky-top styles | ||||||
|   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; |   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; | ||||||
|   const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); |   const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); | ||||||
|  |  | ||||||
|  |   const handleDeleteClick = () => | ||||||
|  |     deleteDevice(serialNumber, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         toast({ | ||||||
|  |           id: `delete-device-success-${serialNumber}`, | ||||||
|  |           title: t('common.success'), | ||||||
|  |           status: 'success', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |         navigate('/devices'); | ||||||
|  |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         if (axios.isAxiosError(e)) { | ||||||
|  |           toast({ | ||||||
|  |             id: `delete-device-error-${serialNumber}`, | ||||||
|  |             title: t('common.error'), | ||||||
|  |             description: e.response?.data?.ErrorDescription, | ||||||
|  |             status: 'error', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|   const connectedTag = React.useMemo(() => { |   const connectedTag = React.useMemo(() => { | ||||||
|     if (!getStatus.data) return null; |     if (!getStatus.data) return null; | ||||||
|  |  | ||||||
| @@ -148,6 +197,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|             <Spacer /> |             <Spacer /> | ||||||
|             <HStack spacing={2}> |             <HStack spacing={2}> | ||||||
|               {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} |               {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} | ||||||
|  |  | ||||||
|               {getDevice?.data && ( |               {getDevice?.data && ( | ||||||
|                 <DeviceActionDropdown |                 <DeviceActionDropdown | ||||||
|                   // @ts-ignore |                   // @ts-ignore | ||||||
| @@ -166,6 +216,33 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|                   isCompact |                   isCompact | ||||||
|                 /> |                 /> | ||||||
|               )} |               )} | ||||||
|  |               <Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}> | ||||||
|  |                 <Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}> | ||||||
|  |                   <Box> | ||||||
|  |                     <PopoverTrigger> | ||||||
|  |                       <DeleteButton isCompact onClick={() => {}} /> | ||||||
|  |                     </PopoverTrigger> | ||||||
|  |                   </Box> | ||||||
|  |                 </Tooltip> | ||||||
|  |                 <PopoverContent> | ||||||
|  |                   <PopoverArrow /> | ||||||
|  |                   <PopoverCloseButton /> | ||||||
|  |                   <PopoverHeader> | ||||||
|  |                     {t('crud.delete')} {serialNumber} | ||||||
|  |                   </PopoverHeader> | ||||||
|  |                   <PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody> | ||||||
|  |                   <PopoverFooter> | ||||||
|  |                     <Center> | ||||||
|  |                       <Button colorScheme="gray" mr="1" onClick={onDeleteClose}> | ||||||
|  |                         {t('common.cancel')} | ||||||
|  |                       </Button> | ||||||
|  |                       <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}> | ||||||
|  |                         {t('common.yes')} | ||||||
|  |                       </Button> | ||||||
|  |                     </Center> | ||||||
|  |                   </PopoverFooter> | ||||||
|  |                 </PopoverContent> | ||||||
|  |               </Popover> | ||||||
|               <RefreshButton |               <RefreshButton | ||||||
|                 onClick={refresh} |                 onClick={refresh} | ||||||
|                 isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} |                 isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} | ||||||
| @@ -214,6 +291,33 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|                     size="md" |                     size="md" | ||||||
|                   /> |                   /> | ||||||
|                 )} |                 )} | ||||||
|  |                 <Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}> | ||||||
|  |                   <Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}> | ||||||
|  |                     <Box> | ||||||
|  |                       <PopoverTrigger> | ||||||
|  |                         <DeleteButton isCompact onClick={() => {}} /> | ||||||
|  |                       </PopoverTrigger> | ||||||
|  |                     </Box> | ||||||
|  |                   </Tooltip> | ||||||
|  |                   <PopoverContent> | ||||||
|  |                     <PopoverArrow /> | ||||||
|  |                     <PopoverCloseButton /> | ||||||
|  |                     <PopoverHeader> | ||||||
|  |                       {t('crud.delete')} {serialNumber} | ||||||
|  |                     </PopoverHeader> | ||||||
|  |                     <PopoverBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</PopoverBody> | ||||||
|  |                     <PopoverFooter> | ||||||
|  |                       <Center> | ||||||
|  |                         <Button colorScheme="gray" mr="1" onClick={onDeleteClose}> | ||||||
|  |                           {t('common.cancel')} | ||||||
|  |                         </Button> | ||||||
|  |                         <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}> | ||||||
|  |                           {t('common.yes')} | ||||||
|  |                         </Button> | ||||||
|  |                       </Center> | ||||||
|  |                     </PopoverFooter> | ||||||
|  |                   </PopoverContent> | ||||||
|  |                 </Popover> | ||||||
|                 <RefreshButton |                 <RefreshButton | ||||||
|                   onClick={refresh} |                   onClick={refresh} | ||||||
|                   isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} |                   isFetching={getDevice.isFetching || getHealth.isFetching || getStatus.isFetching} | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chak | |||||||
| import { LockSimple } from 'phosphor-react'; | import { LockSimple } from 'phosphor-react'; | ||||||
| import ReactCountryFlag from 'react-country-flag'; | import ReactCountryFlag from 'react-country-flag'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
| import Actions from './Actions'; | import Actions from './Actions'; | ||||||
| import DeviceListFirmwareButton from './FirmwareButton'; | import DeviceListFirmwareButton from './FirmwareButton'; | ||||||
| import AP from './icons/AP.png'; | import AP from './icons/AP.png'; | ||||||
| @@ -49,6 +50,7 @@ const BADGE_COLORS: Record<string, string> = { | |||||||
|  |  | ||||||
| const DeviceListCard = () => { | const DeviceListCard = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|   const [serialNumber, setSerialNumber] = React.useState<string>(''); |   const [serialNumber, setSerialNumber] = React.useState<string>(''); | ||||||
|   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); |   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); | ||||||
|   const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined); |   const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined); | ||||||
| @@ -252,6 +254,7 @@ const DeviceListCard = () => { | |||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'firmware', |         accessor: 'firmware', | ||||||
|         Cell: (v) => firmwareCell(v.cell.row.original), |         Cell: (v) => firmwareCell(v.cell.row.original), | ||||||
|  |         stopPropagation: true, | ||||||
|         customWidth: '50px', |         customWidth: '50px', | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
| @@ -389,7 +392,7 @@ const DeviceListCard = () => { | |||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody p={4}> |       <CardBody p={4}> | ||||||
|         <Box overflowX="auto" w="100%"> |         <Box overflowX="auto" w="100%"> | ||||||
|           <DataTable |           <DataTable<DeviceWithStatus> | ||||||
|             columns={ |             columns={ | ||||||
|               columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as { |               columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as { | ||||||
|                 id: string; |                 id: string; | ||||||
| @@ -407,6 +410,8 @@ const DeviceListCard = () => { | |||||||
|             // @ts-ignore |             // @ts-ignore | ||||||
|             setPageInfo={setPageInfo} |             setPageInfo={setPageInfo} | ||||||
|             saveSettingsId="gateway.devices.table" |             saveSettingsId="gateway.devices.table" | ||||||
|  |             onRowClick={(device) => navigate(`devices/${device.serialNumber}`)} | ||||||
|  |             isRowClickable={() => true} | ||||||
|           /> |           /> | ||||||
|         </Box> |         </Box> | ||||||
|       </CardBody> |       </CardBody> | ||||||
|   | |||||||
| @@ -11,7 +11,15 @@ const UriCell = ({ uri }: Props) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box display="flex"> |     <Box display="flex"> | ||||||
|       <Button onClick={copy.onCopy} size="xs" colorScheme="teal" mr={2}> |       <Button | ||||||
|  |         onClick={(e) => { | ||||||
|  |           copy.onCopy(); | ||||||
|  |           e.stopPropagation(); | ||||||
|  |         }} | ||||||
|  |         size="xs" | ||||||
|  |         colorScheme="teal" | ||||||
|  |         mr={2} | ||||||
|  |       > | ||||||
|         {copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')} |         {copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')} | ||||||
|       </Button> |       </Button> | ||||||
|       <Text my="auto">{uri}</Text> |       <Text my="auto">{uri}</Text> | ||||||
|   | |||||||
| @@ -158,13 +158,14 @@ const FirmwareListTable = () => { | |||||||
|       <CardBody p={4}> |       <CardBody p={4}> | ||||||
|         <Box overflowX="auto" w="100%"> |         <Box overflowX="auto" w="100%"> | ||||||
|           <LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}> |           <LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}> | ||||||
|             <DataTable |             <DataTable<Firmware> | ||||||
|               columns={columns as Column<object>[]} |               columns={columns} | ||||||
|               saveSettingsId="firmware.table" |               saveSettingsId="firmware.table" | ||||||
|               data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []} |               data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []} | ||||||
|               obj={t('analytics.firmware')} |               obj={t('analytics.firmware')} | ||||||
|               minHeight="200px" |               minHeight="200px" | ||||||
|               sortBy={[{ id: 'imageDate', desc: true }]} |               sortBy={[{ id: 'imageDate', desc: true }]} | ||||||
|  |               onRowClick={(firmw) => handleViewDetailsClick(firmw)()} | ||||||
|             /> |             /> | ||||||
|           </LoadingOverlay> |           </LoadingOverlay> | ||||||
|         </Box> |         </Box> | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react'; | import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useParams } from 'react-router-dom'; | ||||||
| import ScriptTableActions from './Actions'; | import ScriptTableActions from './Actions'; | ||||||
| import CreateScriptButton from './CreateButton'; | import CreateScriptButton from './CreateButton'; | ||||||
| import useScriptsTable from './useScriptsTable'; | import useScriptsTable from './useScriptsTable'; | ||||||
| @@ -21,6 +22,7 @@ type Props = { | |||||||
| const ScriptTableCard = ({ onIdSelect }: Props) => { | const ScriptTableCard = ({ onIdSelect }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { query, hiddenColumns } = useScriptsTable(); |   const { query, hiddenColumns } = useScriptsTable(); | ||||||
|  |   const { id } = useParams(); | ||||||
|  |  | ||||||
|   const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []); |   const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []); | ||||||
|   const actionCell = React.useCallback( |   const actionCell = React.useCallback( | ||||||
| @@ -108,8 +110,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => { | |||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody> |       <CardBody> | ||||||
|         <Box w="100%" h="300px" overflowY="auto"> |         <Box w="100%" h="300px" overflowY="auto"> | ||||||
|           <DataTable |           <DataTable<Script> | ||||||
|             columns={columns as Column<object>[]} |             columns={columns} | ||||||
|             saveSettingsId="apiKeys.profile.table" |             saveSettingsId="apiKeys.profile.table" | ||||||
|             data={query.data ?? []} |             data={query.data ?? []} | ||||||
|             obj={t('script.other')} |             obj={t('script.other')} | ||||||
| @@ -118,6 +120,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => { | |||||||
|             hiddenColumns={hiddenColumns[0]} |             hiddenColumns={hiddenColumns[0]} | ||||||
|             showAllRows |             showAllRows | ||||||
|             hideControls |             hideControls | ||||||
|  |             onRowClick={(script) => onIdSelect(script.id)} | ||||||
|  |             isRowClickable={(script) => script.id !== id} | ||||||
|           /> |           /> | ||||||
|         </Box> |         </Box> | ||||||
|       </CardBody> |       </CardBody> | ||||||
|   | |||||||
| @@ -25,10 +25,10 @@ const UserTable = () => { | |||||||
|   const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure(); |   const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure(); | ||||||
|   const { data: users, refetch: refreshUsers, isFetching } = useGetUsers(); |   const { data: users, refetch: refreshUsers, isFetching } = useGetUsers(); | ||||||
|  |  | ||||||
|   const openEditModal = (editUser: User) => { |   const openEditModal = React.useCallback((editUser: User) => { | ||||||
|     setEditId(editUser.id); |     setEditId(editUser.id); | ||||||
|     openEdit(); |     openEdit(); | ||||||
|   }; |   }, []); | ||||||
|  |  | ||||||
|   const memoizedActions = useCallback( |   const memoizedActions = useCallback( | ||||||
|     (userActions: User) => ( |     (userActions: User) => ( | ||||||
| @@ -99,7 +99,7 @@ const UserTable = () => { | |||||||
|     ]; |     ]; | ||||||
|     if (user?.userRole !== 'csr') |     if (user?.userRole !== 'csr') | ||||||
|       baseColumns.push({ |       baseColumns.push({ | ||||||
|         id: 'user', |         id: 'actions', | ||||||
|         Header: t('common.actions'), |         Header: t('common.actions'), | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'Id', |         accessor: 'Id', | ||||||
| @@ -139,14 +139,15 @@ const UserTable = () => { | |||||||
|         </CardHeader> |         </CardHeader> | ||||||
|         <CardBody> |         <CardBody> | ||||||
|           <Box overflowX="auto" w="100%"> |           <Box overflowX="auto" w="100%"> | ||||||
|             <DataTable |             <DataTable<User> | ||||||
|               columns={columns as Column<object>[]} |               columns={columns} | ||||||
|               data={users ?? []} |               data={users ?? []} | ||||||
|               isLoading={isFetching} |               isLoading={isFetching} | ||||||
|               obj={t('users.title')} |               obj={t('users.title')} | ||||||
|               sortBy={[{ id: 'email', desc: false }]} |               sortBy={[{ id: 'email', desc: false }]} | ||||||
|               hiddenColumns={hiddenColumns} |               hiddenColumns={hiddenColumns} | ||||||
|               fullScreen |               fullScreen | ||||||
|  |               onRowClick={openEditModal} | ||||||
|             /> |             /> | ||||||
|           </Box> |           </Box> | ||||||
|         </CardBody> |         </CardBody> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Charles
					Charles