mirror of
				https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
				synced 2025-10-29 18:02:31 +00:00 
			
		
		
		
	Merge pull request #161 from stephb9959/main
[WIFI-12261] Added system secrets on the system page
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(7)", | ||||
|   "version": "2.9.0(9)", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "ucentral-client", | ||||
|       "version": "2.9.0(7)", | ||||
|       "version": "2.9.0(9)", | ||||
|       "license": "ISC", | ||||
|       "dependencies": { | ||||
|         "@chakra-ui/icons": "^2.0.11", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "ucentral-client", | ||||
|   "version": "2.9.0(7)", | ||||
|   "version": "2.9.0(9)", | ||||
|   "description": "", | ||||
|   "private": true, | ||||
|   "main": "index.tsx", | ||||
|   | ||||
| @@ -1037,6 +1037,7 @@ | ||||
| 	}, | ||||
| 	"system": { | ||||
| 		"backend_logs": "Back-End-Protokolle", | ||||
| 		"configuration": "Aufbau", | ||||
| 		"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden", | ||||
| 		"endpoint": "Endpunkt", | ||||
| 		"hostname": "Hostname", | ||||
| @@ -1047,9 +1048,10 @@ | ||||
| 		"os": "Betriebssystem", | ||||
| 		"processors": "Prozessoren", | ||||
| 		"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden", | ||||
| 		"secrets": "Systemgeheimnisse", | ||||
| 		"secrets": "Geheimnisse", | ||||
| 		"secrets_create": "Geheimnis erstellen", | ||||
| 		"secrets_one": "Systemgeheimnis", | ||||
| 		"secrets_one": "Geheimnis", | ||||
| 		"services": "dienstleistungen", | ||||
| 		"start": "Start", | ||||
| 		"subsystems": "Subsysteme", | ||||
| 		"success_reload": "Reload-Befehl erfolgreich gesendet!", | ||||
|   | ||||
| @@ -1037,6 +1037,7 @@ | ||||
| 	}, | ||||
| 	"system": { | ||||
| 		"backend_logs": "Back-End Logs", | ||||
| 		"configuration": "Configuration", | ||||
| 		"could_not_retrieve": "Error: could not retrieve {{name}} system information", | ||||
| 		"endpoint": "Endpoint", | ||||
| 		"hostname": "Host Name", | ||||
| @@ -1047,9 +1048,10 @@ | ||||
| 		"os": "Operating System", | ||||
| 		"processors": "Processors", | ||||
| 		"reload_chosen_subsystems": "Reload Chosen Subsystems", | ||||
| 		"secrets": "System Secrets", | ||||
| 		"secrets": "Secrets", | ||||
| 		"secrets_create": "Create Secret", | ||||
| 		"secrets_one": "System Secret", | ||||
| 		"secrets_one": "Secret", | ||||
| 		"services": "Services", | ||||
| 		"start": "Start", | ||||
| 		"subsystems": "Subsystems", | ||||
| 		"success_reload": "Successfully sent reload command!", | ||||
|   | ||||
| @@ -1037,6 +1037,7 @@ | ||||
| 	}, | ||||
| 	"system": { | ||||
| 		"backend_logs": "Registros de back-end", | ||||
| 		"configuration": "Configuración", | ||||
| 		"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ", | ||||
| 		"endpoint": "punto final", | ||||
| 		"hostname": "Nombre de host", | ||||
| @@ -1047,9 +1048,10 @@ | ||||
| 		"os": "sistema operativo", | ||||
| 		"processors": "Procesadores", | ||||
| 		"reload_chosen_subsystems": "Recargar subsistemas elegidos", | ||||
| 		"secrets": "Secretos del sistema", | ||||
| 		"secrets": "Misterios", | ||||
| 		"secrets_create": "Crear secreto", | ||||
| 		"secrets_one": "Secreto del sistema", | ||||
| 		"secrets_one": "secreto", | ||||
| 		"services": "Servicios", | ||||
| 		"start": "comienzo", | ||||
| 		"subsystems": "Subsistemas", | ||||
| 		"success_reload": "¡Comando de recarga enviado con éxito!", | ||||
|   | ||||
| @@ -1037,6 +1037,7 @@ | ||||
| 	}, | ||||
| 	"system": { | ||||
| 		"backend_logs": "Journaux principaux", | ||||
| 		"configuration": "Configuration", | ||||
| 		"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ", | ||||
| 		"endpoint": "Point final", | ||||
| 		"hostname": "nom d'hôte", | ||||
| @@ -1047,9 +1048,10 @@ | ||||
| 		"os": "Système opérateur", | ||||
| 		"processors": "Processeurs", | ||||
| 		"reload_chosen_subsystems": "Recharger les sous-systèmes choisis", | ||||
| 		"secrets": "Secrets du système", | ||||
| 		"secrets": "Secrets", | ||||
| 		"secrets_create": "Créer un secret", | ||||
| 		"secrets_one": "Code secret du système", | ||||
| 		"secrets_one": "Secret", | ||||
| 		"services": "Prestations de service", | ||||
| 		"start": "Début", | ||||
| 		"subsystems": "Sous-systèmes", | ||||
| 		"success_reload": "Commande de rechargement envoyée avec succès !", | ||||
|   | ||||
| @@ -1037,6 +1037,7 @@ | ||||
| 	}, | ||||
| 	"system": { | ||||
| 		"backend_logs": "Registros de back-end", | ||||
| 		"configuration": "Configuração", | ||||
| 		"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema", | ||||
| 		"endpoint": "Ponto final", | ||||
| 		"hostname": "Nome de anfitrião", | ||||
| @@ -1047,9 +1048,10 @@ | ||||
| 		"os": "Sistema Operacional", | ||||
| 		"processors": "Processadores", | ||||
| 		"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos", | ||||
| 		"secrets": "Segredos do sistema", | ||||
| 		"secrets": "Segredos", | ||||
| 		"secrets_create": "Criar Segredo", | ||||
| 		"secrets_one": "Segredo do sistema", | ||||
| 		"secrets_one": "Segredo", | ||||
| 		"services": "Serviços", | ||||
| 		"start": "Começar", | ||||
| 		"subsystems": "Subsistemas", | ||||
| 		"success_reload": "Comando de recarga enviado com sucesso!", | ||||
|   | ||||
| @@ -53,6 +53,7 @@ export type DataTableProps = { | ||||
|   obj?: string; | ||||
|   sortBy?: { id: string; desc: boolean }[]; | ||||
|   hiddenColumns?: string[]; | ||||
|   hideEmptyListText?: boolean; | ||||
|   hideControls?: boolean; | ||||
|   minHeight?: string | number; | ||||
|   fullScreen?: boolean; | ||||
| @@ -77,6 +78,7 @@ const _DataTable = ({ | ||||
|   sortBy, | ||||
|   hiddenColumns, | ||||
|   hideControls, | ||||
|   hideEmptyListText, | ||||
|   count, | ||||
|   setPageInfo, | ||||
|   isManual, | ||||
| @@ -86,6 +88,7 @@ const _DataTable = ({ | ||||
|   const { t } = useTranslation(); | ||||
|   const breakpoint = useBreakpoint(); | ||||
|   const textColor = useColorModeValue('gray.700', 'white'); | ||||
|   const hoveredRowBg = useColorModeValue('gray.100', 'gray.600'); | ||||
|   const getPageSize = () => { | ||||
|     try { | ||||
|       if (showAllRows) return 1000000; | ||||
| @@ -142,6 +145,10 @@ const _DataTable = ({ | ||||
|     usePagination, | ||||
|   ) as TableInstanceWithHooks<object>; | ||||
|  | ||||
|   const handleGoToPage = (newPage: number) => { | ||||
|     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage)); | ||||
|     gotoPage(newPage); | ||||
|   }; | ||||
|   const handleNextPage = () => { | ||||
|     nextPage(); | ||||
|     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1)); | ||||
| @@ -256,7 +263,13 @@ const _DataTable = ({ | ||||
|                 {page.map((row: Row) => { | ||||
|                   prepareRow(row); | ||||
|                   return ( | ||||
|                     <Tr {...row.getRowProps()} key={uuid()}> | ||||
|                     <Tr | ||||
|                       {...row.getRowProps()} | ||||
|                       key={uuid()} | ||||
|                       _hover={{ | ||||
|                         backgroundColor: hoveredRowBg, | ||||
|                       }} | ||||
|                     > | ||||
|                       { | ||||
|                         // @ts-ignore | ||||
|                         row.cells.map((cell) => ( | ||||
| @@ -288,7 +301,7 @@ const _DataTable = ({ | ||||
|               </Tbody> | ||||
|             )} | ||||
|           </Table> | ||||
|           {!isLoading && data.length === 0 && ( | ||||
|           {!isLoading && data.length === 0 && !hideEmptyListText && ( | ||||
|             <Center> | ||||
|               {obj ? ( | ||||
|                 <Heading size="md" pt={12}> | ||||
| @@ -309,7 +322,7 @@ const _DataTable = ({ | ||||
|             <Tooltip label={t('table.first_page')}> | ||||
|               <IconButton | ||||
|                 aria-label="Go to first page" | ||||
|                 onClick={() => gotoPage(0)} | ||||
|                 onClick={() => handleGoToPage(0)} | ||||
|                 isDisabled={!canPreviousPage} | ||||
|                 icon={<ArrowLeftIcon h={3} w={3} />} | ||||
|                 mr={4} | ||||
| @@ -347,7 +360,7 @@ const _DataTable = ({ | ||||
|                   max={pageOptions.length} | ||||
|                   onChange={(_: unknown, numberValue: number) => { | ||||
|                     const newPage = numberValue ? numberValue - 1 : 0; | ||||
|                     gotoPage(newPage); | ||||
|                     handleGoToPage(newPage); | ||||
|                   }} | ||||
|                   defaultValue={pageIndex + 1} | ||||
|                 > | ||||
| @@ -386,7 +399,7 @@ const _DataTable = ({ | ||||
|             <Tooltip label={t('table.last_page')}> | ||||
|               <IconButton | ||||
|                 aria-label="Go to last page" | ||||
|                 onClick={() => gotoPage(pageCount - 1)} | ||||
|                 onClick={() => handleGoToPage(pageCount - 1)} | ||||
|                 isDisabled={!canNextPage} | ||||
|                 icon={<ArrowRightIcon h={3} w={3} />} | ||||
|                 ml={4} | ||||
|   | ||||
| @@ -100,6 +100,7 @@ const SortableDataTable: React.FC<Props> = ({ | ||||
| }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const breakpoint = useBreakpoint(); | ||||
|   const hoveredRowBg = useColorModeValue('gray.100', 'gray.600'); | ||||
|   const textColor = useColorModeValue('gray.700', 'white'); | ||||
|   const getPageSize = () => { | ||||
|     const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined; | ||||
| @@ -223,7 +224,13 @@ const SortableDataTable: React.FC<Props> = ({ | ||||
|                 {page.map((row: Row) => { | ||||
|                   prepareRow(row); | ||||
|                   return ( | ||||
|                     <Tr {...row.getRowProps()} key={uuid()}> | ||||
|                     <Tr | ||||
|                       {...row.getRowProps()} | ||||
|                       key={uuid()} | ||||
|                       _hover={{ | ||||
|                         backgroundColor: hoveredRowBg, | ||||
|                       }} | ||||
|                     > | ||||
|                       { | ||||
|                         // @ts-ignore | ||||
|                         row.cells.map((cell) => ( | ||||
|   | ||||
							
								
								
									
										139
									
								
								src/pages/SystemPage/SystemSecrets/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/pages/SystemPage/SystemSecrets/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| import React from 'react'; | ||||
| import { CopyIcon } from '@chakra-ui/icons'; | ||||
| import { | ||||
|   IconButton, | ||||
|   Tooltip, | ||||
|   Popover, | ||||
|   PopoverArrow, | ||||
|   PopoverBody, | ||||
|   PopoverCloseButton, | ||||
|   PopoverContent, | ||||
|   PopoverFooter, | ||||
|   PopoverHeader, | ||||
|   PopoverTrigger, | ||||
|   Center, | ||||
|   Box, | ||||
|   Button, | ||||
|   useDisclosure, | ||||
|   HStack, | ||||
|   Text, | ||||
|   useClipboard, | ||||
| } from '@chakra-ui/react'; | ||||
| import { Eye, Trash } from 'phosphor-react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import EditSecretButton from './EditButton'; | ||||
| import { Secret, useDeleteSystemSecret } from 'hooks/Network/Secrets'; | ||||
|  | ||||
| interface Props { | ||||
|   secret: Secret; | ||||
|   isDisabled?: boolean; | ||||
| } | ||||
|  | ||||
| const SystemSecretActions = ({ secret, isDisabled }: Props) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||
|   const deleteSecret = useDeleteSystemSecret(); | ||||
|   const { hasCopied, onCopy } = useClipboard(secret.value); | ||||
|  | ||||
|   const handleDeleteClick = React.useCallback(() => { | ||||
|     deleteSecret.mutate(secret.key, { | ||||
|       onSuccess: () => { | ||||
|         onClose(); | ||||
|       }, | ||||
|     }); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <HStack mx="auto"> | ||||
|       <Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}> | ||||
|         <Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}> | ||||
|           <Box> | ||||
|             <PopoverTrigger> | ||||
|               <IconButton | ||||
|                 aria-label="delete-device" | ||||
|                 colorScheme="red" | ||||
|                 icon={<Trash size={20} />} | ||||
|                 size="sm" | ||||
|                 isDisabled={isDisabled} | ||||
|               /> | ||||
|             </PopoverTrigger> | ||||
|           </Box> | ||||
|         </Tooltip> | ||||
|         <PopoverContent w="340px"> | ||||
|           <PopoverArrow /> | ||||
|           <PopoverCloseButton /> | ||||
|           <PopoverHeader> | ||||
|             {t('crud.delete')} {secret.key} | ||||
|           </PopoverHeader> | ||||
|           <PopoverBody> | ||||
|             <Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('system.secrets_one') })}</Text> | ||||
|           </PopoverBody> | ||||
|           <PopoverFooter> | ||||
|             <Center> | ||||
|               <Button colorScheme="gray" mr="1" onClick={onClose}> | ||||
|                 {t('common.cancel')} | ||||
|               </Button> | ||||
|               <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteSecret.isLoading}> | ||||
|                 {t('common.yes')} | ||||
|               </Button> | ||||
|             </Center> | ||||
|           </PopoverFooter> | ||||
|         </PopoverContent> | ||||
|       </Popover> | ||||
|       <Tooltip | ||||
|         label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`} | ||||
|         hasArrow | ||||
|         closeOnClick={false} | ||||
|       > | ||||
|         <IconButton | ||||
|           aria-label={t('common.copy')} | ||||
|           icon={<CopyIcon h={5} w={5} />} | ||||
|           onClick={onCopy} | ||||
|           size="sm" | ||||
|           colorScheme="teal" | ||||
|           mr={2} | ||||
|         /> | ||||
|       </Tooltip> | ||||
|       <EditSecretButton secret={secret} /> | ||||
|       <Popover> | ||||
|         <Tooltip label={`${t('common.view')} ${t('system.secrets_one')}`} hasArrow closeOnClick={false}> | ||||
|           <Box> | ||||
|             <PopoverTrigger> | ||||
|               <IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" /> | ||||
|             </PopoverTrigger> | ||||
|           </Box> | ||||
|         </Tooltip> | ||||
|         <PopoverContent w="560px"> | ||||
|           <PopoverArrow /> | ||||
|           <PopoverCloseButton /> | ||||
|           <PopoverHeader> | ||||
|             {t('common.view')} {secret.key} | ||||
|             <Tooltip | ||||
|               label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`} | ||||
|               hasArrow | ||||
|               closeOnClick={false} | ||||
|             > | ||||
|               <IconButton | ||||
|                 aria-label={t('common.copy')} | ||||
|                 icon={<CopyIcon h={4} w={4} />} | ||||
|                 onClick={onCopy} | ||||
|                 size="xs" | ||||
|                 colorScheme="teal" | ||||
|                 ml={2} | ||||
|               /> | ||||
|             </Tooltip> | ||||
|           </PopoverHeader> | ||||
|           <PopoverBody> | ||||
|             <Text whiteSpace="break-spaces"> | ||||
|               <Center> | ||||
|                 <pre style={{ fontFamily: 'monospace' }}>{secret.value}</pre> | ||||
|               </Center> | ||||
|             </Text> | ||||
|           </PopoverBody> | ||||
|         </PopoverContent> | ||||
|       </Popover> | ||||
|     </HStack> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SystemSecretActions; | ||||
							
								
								
									
										118
									
								
								src/pages/SystemPage/SystemSecrets/CreateButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/pages/SystemPage/SystemSecrets/CreateButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| import * as React from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   Input, | ||||
|   Textarea, | ||||
|   useDisclosure, | ||||
|   useToast, | ||||
| } from '@chakra-ui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { CreateButton } from '../../../components/Buttons/CreateButton'; | ||||
| import { SaveButton } from '../../../components/Buttons/SaveButton'; | ||||
| import { Modal } from '../../../components/Modals/Modal'; | ||||
| import { useCreateSystemSecret } from 'hooks/Network/Secrets'; | ||||
| import { AxiosError } from 'models/Axios'; | ||||
|  | ||||
| type FormValues = { | ||||
|   key: string; | ||||
|   value: string; | ||||
| }; | ||||
|  | ||||
| const DEFAULT_FORM_VALUES: FormValues = { | ||||
|   key: '', | ||||
|   value: '', | ||||
| }; | ||||
|  | ||||
| const SystemSecretCreateButton = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const toast = useToast(); | ||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||
|   const [form, setForm] = React.useState<FormValues>(DEFAULT_FORM_VALUES); | ||||
|   const [isNameChanged, setIsNameChanged] = React.useState(false); | ||||
|   const [isValueChanged, setIsValueChanged] = React.useState(false); | ||||
|   const create = useCreateSystemSecret(); | ||||
|  | ||||
|   const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     setForm({ ...form, key: e.target.value }); | ||||
|     if (!isNameChanged) setIsNameChanged(true); | ||||
|   }; | ||||
|   const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||
|     setForm({ ...form, value: e.target.value }); | ||||
|     if (!isValueChanged) setIsValueChanged(true); | ||||
|   }; | ||||
|  | ||||
|   const isNameError = form.key.length === 0; | ||||
|   const isValueError = form.value.length === 0; | ||||
|  | ||||
|   const onSubmit = () => { | ||||
|     create.mutate(form, { | ||||
|       onSuccess: () => { | ||||
|         toast({ | ||||
|           id: 'create-system-secret-success', | ||||
|           title: t('common.success'), | ||||
|           description: t('crud.success_update_obj', { | ||||
|             obj: t('system.secrets_one'), | ||||
|           }), | ||||
|           status: 'success', | ||||
|           duration: 5000, | ||||
|           isClosable: true, | ||||
|           position: 'top-right', | ||||
|         }); | ||||
|         onClose(); | ||||
|       }, | ||||
|       onError: (e) => { | ||||
|         toast({ | ||||
|           id: 'create-system-secret-error', | ||||
|           title: t('common.error'), | ||||
|           description: (e as AxiosError)?.response?.data?.ErrorDescription, | ||||
|           status: 'error', | ||||
|           duration: 5000, | ||||
|           isClosable: true, | ||||
|           position: 'top-right', | ||||
|         }); | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleOpenClick = () => { | ||||
|     setIsNameChanged(false); | ||||
|     setIsValueChanged(false); | ||||
|     setForm(DEFAULT_FORM_VALUES); | ||||
|     onOpen(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <CreateButton onClick={handleOpenClick} isCompact /> | ||||
|       <Modal | ||||
|         isOpen={isOpen} | ||||
|         onClose={onClose} | ||||
|         title={t('system.secrets_create')} | ||||
|         topRightButtons={ | ||||
|           <SaveButton onClick={onSubmit} isDisabled={isNameError || isValueError} isLoading={create.isLoading} /> | ||||
|         } | ||||
|         options={{ | ||||
|           modalSize: 'sm', | ||||
|         }} | ||||
|       > | ||||
|         <Box> | ||||
|           <FormControl mb={2} isInvalid={isNameError && isNameChanged}> | ||||
|             <FormLabel>{t('common.name')}</FormLabel> | ||||
|             <Input value={form.key} onChange={onKeyChange} /> | ||||
|             <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||
|           </FormControl> | ||||
|           <FormControl mb={2} isInvalid={isValueError && isValueChanged}> | ||||
|             <FormLabel>{t('common.value')}</FormLabel> | ||||
|             <Textarea value={form.value} onChange={onValueChange} rows={2} /> | ||||
|             <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||
|           </FormControl> | ||||
|         </Box> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SystemSecretCreateButton; | ||||
							
								
								
									
										146
									
								
								src/pages/SystemPage/SystemSecrets/EditButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/pages/SystemPage/SystemSecrets/EditButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| import * as React from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   Center, | ||||
|   FormControl, | ||||
|   FormErrorMessage, | ||||
|   FormLabel, | ||||
|   IconButton, | ||||
|   Input, | ||||
|   Popover, | ||||
|   PopoverArrow, | ||||
|   PopoverBody, | ||||
|   PopoverCloseButton, | ||||
|   PopoverContent, | ||||
|   PopoverFooter, | ||||
|   PopoverHeader, | ||||
|   PopoverTrigger, | ||||
|   Text, | ||||
|   Textarea, | ||||
|   Tooltip, | ||||
|   useDisclosure, | ||||
|   useToast, | ||||
| } from '@chakra-ui/react'; | ||||
| import { Pencil } from 'phosphor-react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Secret, useUpdateSystemSecret } from 'hooks/Network/Secrets'; | ||||
| import { AxiosError } from 'models/Axios'; | ||||
|  | ||||
| type FormValues = { | ||||
|   key: string; | ||||
|   value: string; | ||||
| }; | ||||
|  | ||||
| type Props = { | ||||
|   secret: Secret; | ||||
| }; | ||||
|  | ||||
| const EditSecretButton = ({ secret }: Props) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const toast = useToast(); | ||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||
|   const [form, setForm] = React.useState<FormValues>({ | ||||
|     key: secret.key, | ||||
|     value: secret.value, | ||||
|   }); | ||||
|   const [isNameChanged, setIsNameChanged] = React.useState(false); | ||||
|   const [isValueChanged, setIsValueChanged] = React.useState(false); | ||||
|   const update = useUpdateSystemSecret(); | ||||
|  | ||||
|   const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     setForm({ ...form, key: e.target.value }); | ||||
|     if (!isNameChanged) setIsNameChanged(true); | ||||
|   }; | ||||
|   const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||
|     setForm({ ...form, value: e.target.value }); | ||||
|     if (!isValueChanged) setIsValueChanged(true); | ||||
|   }; | ||||
|  | ||||
|   const isNameError = form.key.length === 0; | ||||
|   const isValueError = form.value.length === 0; | ||||
|  | ||||
|   const onSubmit = () => { | ||||
|     update.mutate(form, { | ||||
|       onSuccess: () => { | ||||
|         toast({ | ||||
|           id: 'create-system-secret-success', | ||||
|           title: t('common.success'), | ||||
|           status: 'success', | ||||
|           duration: 5000, | ||||
|           isClosable: true, | ||||
|           position: 'top-right', | ||||
|         }); | ||||
|         onClose(); | ||||
|       }, | ||||
|       onError: (e) => { | ||||
|         toast({ | ||||
|           id: 'create-system-secret-error', | ||||
|           title: t('common.error'), | ||||
|           description: (e as AxiosError)?.response?.data?.ErrorDescription, | ||||
|           status: 'error', | ||||
|           duration: 5000, | ||||
|           isClosable: true, | ||||
|           position: 'top-right', | ||||
|         }); | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleOpenClick = () => { | ||||
|     setIsNameChanged(false); | ||||
|     setIsValueChanged(false); | ||||
|     setForm({ | ||||
|       key: secret.key, | ||||
|       value: secret.value, | ||||
|     }); | ||||
|     onOpen(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Popover isOpen={isOpen} onOpen={handleOpenClick} onClose={onClose}> | ||||
|       <Tooltip hasArrow label={t('crud.edit')} placement="top" isDisabled={isOpen}> | ||||
|         <Box> | ||||
|           <PopoverTrigger> | ||||
|             <IconButton aria-label="delete-device" colorScheme="blue" icon={<Pencil size={20} />} size="sm" /> | ||||
|           </PopoverTrigger> | ||||
|         </Box> | ||||
|       </Tooltip> | ||||
|       <PopoverContent w="340px"> | ||||
|         <PopoverArrow /> | ||||
|         <PopoverCloseButton /> | ||||
|         <PopoverHeader> | ||||
|           {t('crud.edit')} {secret.key} | ||||
|         </PopoverHeader> | ||||
|         <PopoverBody> | ||||
|           <Text whiteSpace="break-spaces"> | ||||
|             <Box> | ||||
|               <FormControl mb={2} isInvalid={isNameError && isNameChanged}> | ||||
|                 <FormLabel>{t('common.name')}</FormLabel> | ||||
|                 <Input value={form.key} onChange={onKeyChange} /> | ||||
|                 <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||
|               </FormControl> | ||||
|               <FormControl mb={2} isInvalid={isValueError && isValueChanged}> | ||||
|                 <FormLabel>{t('common.value')}</FormLabel> | ||||
|                 <Textarea value={form.value} onChange={onValueChange} rows={2} /> | ||||
|                 <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||
|               </FormControl> | ||||
|             </Box> | ||||
|           </Text> | ||||
|         </PopoverBody> | ||||
|         <PopoverFooter> | ||||
|           <Center> | ||||
|             <Button colorScheme="gray" mr="1" onClick={onClose}> | ||||
|               {t('common.cancel')} | ||||
|             </Button> | ||||
|             <Button colorScheme="blue" ml="1" onClick={onSubmit} isLoading={update.isLoading}> | ||||
|               {t('common.save')} | ||||
|             </Button> | ||||
|           </Center> | ||||
|         </PopoverFooter> | ||||
|       </PopoverContent> | ||||
|     </Popover> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default EditSecretButton; | ||||
							
								
								
									
										70
									
								
								src/pages/SystemPage/SystemSecrets/Table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/pages/SystemPage/SystemSecrets/Table.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import * as React from 'react'; | ||||
| import { Box } from '@chakra-ui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { DataTable } from '../../../components/DataTables/DataTable'; | ||||
| import SystemSecretActions from './Actions'; | ||||
| import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets'; | ||||
| import { Column } from 'models/Table'; | ||||
|  | ||||
| const SystemSecretsTable = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const getSecrets = useGetAllSystemSecrets(); | ||||
|   const getDictionary = useGetSystemSecretsDictionary(); | ||||
|  | ||||
|   const descriptionCell = React.useCallback( | ||||
|     (secret: Secret) => { | ||||
|       if (!getDictionary.data) return '-'; | ||||
|  | ||||
|       return getDictionary.data.find((d) => d.key === secret.key)?.description ?? '-'; | ||||
|     }, | ||||
|     [getDictionary.data], | ||||
|   ); | ||||
|  | ||||
|   const actionCell = React.useCallback((secret: Secret) => <SystemSecretActions secret={secret} />, []); | ||||
|  | ||||
|   const columns = React.useMemo( | ||||
|     (): Column<Secret>[] => [ | ||||
|       { | ||||
|         id: 'key', | ||||
|         Header: t('common.name'), | ||||
|         Footer: '', | ||||
|         accessor: 'key', | ||||
|         alwaysShow: true, | ||||
|         customWidth: '120px', | ||||
|       }, | ||||
|       { | ||||
|         id: 'description', | ||||
|         Header: t('common.description'), | ||||
|         Footer: '', | ||||
|         Cell: ({ cell }) => descriptionCell(cell.row.original), | ||||
|         accessor: 'description', | ||||
|         hasPopover: true, | ||||
|       }, | ||||
|       { | ||||
|         id: 'actions', | ||||
|         Header: t('common.actions'), | ||||
|         Footer: '', | ||||
|         Cell: (v) => actionCell(v.cell.row.original), | ||||
|         disableSortBy: true, | ||||
|         customWidth: '120px', | ||||
|         alwaysShow: true, | ||||
|       }, | ||||
|     ], | ||||
|     [t, descriptionCell], | ||||
|   ); | ||||
|   return ( | ||||
|     <Box w="100%"> | ||||
|       <DataTable | ||||
|         columns={columns as Column<object>[]} | ||||
|         saveSettingsId="system.secrets.table" | ||||
|         data={getSecrets.data ?? []} | ||||
|         obj={t('keys.other')} | ||||
|         sortBy={[{ id: 'key', desc: false }]} | ||||
|         showAllRows | ||||
|         hideControls | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SystemSecretsTable; | ||||
							
								
								
									
										53
									
								
								src/pages/SystemPage/SystemSecrets/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/pages/SystemPage/SystemSecrets/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import * as React from 'react'; | ||||
| import { | ||||
|   BackgroundProps, | ||||
|   Box, | ||||
|   EffectProps, | ||||
|   Heading, | ||||
|   InteractivityProps, | ||||
|   LayoutProps, | ||||
|   PositionProps, | ||||
|   SpaceProps, | ||||
|   Spacer, | ||||
| } from '@chakra-ui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import SystemSecretCreateButton from './CreateButton'; | ||||
| import SystemSecretsTable from './Table'; | ||||
| import { Card } from 'components/Containers/Card'; | ||||
| import { CardBody } from 'components/Containers/Card/CardBody'; | ||||
| import { CardHeader } from 'components/Containers/Card/CardHeader'; | ||||
| import { useAuth } from 'contexts/AuthProvider'; | ||||
|  | ||||
| export interface SystemSecretsCardProps | ||||
|   extends LayoutProps, | ||||
|     SpaceProps, | ||||
|     BackgroundProps, | ||||
|     InteractivityProps, | ||||
|     PositionProps, | ||||
|     EffectProps {} | ||||
|  | ||||
| export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { user } = useAuth(); | ||||
|  | ||||
|   if (!user || user.userRole !== 'root') { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box px={4} py={4}> | ||||
|       <Card variant="widget" {...props}> | ||||
|         <CardHeader> | ||||
|           <Heading size="md" my="auto"> | ||||
|             {t('system.secrets')} | ||||
|           </Heading> | ||||
|           <Spacer /> | ||||
|           <SystemSecretCreateButton /> | ||||
|         </CardHeader> | ||||
|         <CardBody p={4}> | ||||
|           <SystemSecretsTable /> | ||||
|         </CardBody> | ||||
|       </Card> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
| @@ -21,8 +21,8 @@ import axios from 'axios'; | ||||
| import { FloppyDisk } from 'phosphor-react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import { Modal } from '../../../../components/Modals/Modal'; | ||||
| import { LoadingOverlay } from 'components/LoadingOverlay'; | ||||
| import { Modal } from 'components/Modals/Modal'; | ||||
| import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System'; | ||||
|  | ||||
| type Props = { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import React, { useCallback } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { DataTable } from 'components/DataTables/DataTable'; | ||||
| import { DataTable } from '../../../components/DataTables/DataTable'; | ||||
| import { compactDate } from 'helpers/dateFormatting'; | ||||
| import { Column } from 'models/Table'; | ||||
|  | ||||
|   | ||||
| @@ -19,11 +19,11 @@ import { | ||||
| import { MultiValue, Select } from 'chakra-react-select'; | ||||
| import { ArrowsClockwise } from 'phosphor-react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import FormattedDate from '../../../components/InformationDisplays/FormattedDate'; | ||||
| import SystemLoggingButton from './LoggingButton'; | ||||
| import SystemCertificatesTable from './SystemCertificatesTable'; | ||||
| import { Card } from 'components/Containers/Card'; | ||||
| import { CardBody } from 'components/Containers/Card/CardBody'; | ||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||
| import { compactSecondsToDetailed } from 'helpers/dateFormatting'; | ||||
| import { EndpointApiResponse } from 'hooks/Network/Endpoints'; | ||||
| import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System'; | ||||
| @@ -65,7 +65,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Card> | ||||
|       <Card variant="widget"> | ||||
|         <Box display="flex" mb={2}> | ||||
|           <Heading pt={0}>{endpoint.type}</Heading> | ||||
|           <Spacer /> | ||||
| @@ -73,7 +73,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | ||||
|           <Button | ||||
|             mt={1} | ||||
|             minWidth="112px" | ||||
|             colorScheme="gray" | ||||
|             colorScheme="blue" | ||||
|             rightIcon={<ArrowsClockwise />} | ||||
|             onClick={refresh} | ||||
|             isLoading={isFetchingSystem || isFetchingSubsystems} | ||||
| @@ -179,7 +179,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | ||||
|                   ml={2} | ||||
|                   onClick={handleReloadClick} | ||||
|                   icon={<ArrowsClockwise size={20} />} | ||||
|                   colorScheme="gray" | ||||
|                   colorScheme="blue" | ||||
|                   isLoading={isReloading} | ||||
|                   isDisabled={subs.length === 0} | ||||
|                 /> | ||||
|   | ||||
| @@ -1,27 +1,47 @@ | ||||
| import React from 'react'; | ||||
| import { Heading, SimpleGrid, Spacer } from '@chakra-ui/react'; | ||||
| import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import { RefreshButton } from '../../components/Buttons/RefreshButton'; | ||||
| import { SystemSecretsCard } from './SystemSecrets'; | ||||
| import SystemTile from './SystemTile'; | ||||
| import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||
| import { Card } from 'components/Containers/Card'; | ||||
| import { CardHeader } from 'components/Containers/Card/CardHeader'; | ||||
| import { axiosSec } from 'constants/axiosInstances'; | ||||
| import { useAuth } from 'contexts/AuthProvider'; | ||||
| import { useGetEndpoints } from 'hooks/Network/Endpoints'; | ||||
|  | ||||
| const SystemPage = () => { | ||||
| const getDefaultTabIndex = () => { | ||||
|   const index = localStorage.getItem('system-tab-index') || '0'; | ||||
|   try { | ||||
|     return parseInt(index, 10); | ||||
|   } catch { | ||||
|     return 0; | ||||
|   } | ||||
| }; | ||||
| type Props = { | ||||
|   isOnlySec?: boolean; | ||||
| }; | ||||
|  | ||||
| const SystemPage = ({ isOnlySec }: Props) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { token } = useAuth(); | ||||
|   const { token, user } = useAuth(); | ||||
|   const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} }); | ||||
|   const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex()); | ||||
|   const handleTabChange = (index: number) => { | ||||
|     setTabIndex(index); | ||||
|     localStorage.setItem('system-tab-index', index.toString()); | ||||
|   }; | ||||
|  | ||||
|   const isRoot = user && user.userRole === 'root'; | ||||
|  | ||||
|   const endpointsList = React.useMemo(() => { | ||||
|     if (!endpoints || !token) return null; | ||||
|     if (!token || (!isOnlySec && !endpoints)) return null; | ||||
|  | ||||
|     const endpointList = [...endpoints]; | ||||
|     const endpointList = endpoints ? [...endpoints] : []; | ||||
|     endpointList.push({ | ||||
|       uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', | ||||
|       type: 'owsec', | ||||
|       type: isOnlySec ? '' : 'owsec', | ||||
|       id: 0, | ||||
|       vendor: 'owsec', | ||||
|       authenticationType: '', | ||||
| @@ -37,20 +57,51 @@ const SystemPage = () => { | ||||
|   }, [endpoints, token]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Card mb={4} py={2} px={4}> | ||||
|         <CardHeader> | ||||
|           <Heading size="md" my="auto"> | ||||
|             {t('controller.firmware.endpoints')} | ||||
|           </Heading> | ||||
|           <Spacer /> | ||||
|           <RefreshButton onClick={refetch} isFetching={isFetching} /> | ||||
|         </CardHeader> | ||||
|       </Card> | ||||
|       <SimpleGrid minChildWidth="500px" spacing="20px" mb={3}> | ||||
|         {endpointsList} | ||||
|       </SimpleGrid> | ||||
|     </> | ||||
|     <Card p={0}> | ||||
|       <Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy> | ||||
|         <TabList> | ||||
|           <CardHeader> | ||||
|             <Tab>{t('system.services')}</Tab> | ||||
|             <Tab hidden={!isRoot}>{t('system.configuration')}</Tab> | ||||
|           </CardHeader> | ||||
|         </TabList> | ||||
|         <TabPanels> | ||||
|           <TabPanel p={0}> | ||||
|             <Box | ||||
|               borderLeft="1px solid" | ||||
|               borderRight="1px solid" | ||||
|               borderBottom="1px solid" | ||||
|               borderColor="var(--chakra-colors-chakra-border-color)" | ||||
|               borderBottomLeftRadius="15px" | ||||
|               borderBottomRightRadius="15px" | ||||
|             > | ||||
|               {!isOnlySec && ( | ||||
|                 <CardHeader px={4} pt={4}> | ||||
|                   <Spacer /> | ||||
|                   <RefreshButton onClick={refetch} isFetching={isFetching} /> | ||||
|                 </CardHeader> | ||||
|               )} | ||||
|               <SimpleGrid minChildWidth="500px" spacing="20px" p={4}> | ||||
|                 {endpointsList} | ||||
|               </SimpleGrid> | ||||
|             </Box> | ||||
|           </TabPanel> | ||||
|           <TabPanel p={0}> | ||||
|             <Box | ||||
|               borderLeft="1px solid" | ||||
|               borderRight="1px solid" | ||||
|               borderBottom="1px solid" | ||||
|               borderColor="var(--chakra-colors-chakra-border-color)" | ||||
|               borderBottomLeftRadius="15px" | ||||
|               borderBottomRightRadius="15px" | ||||
|             > | ||||
|               <SystemSecretsCard /> | ||||
|             </Box> | ||||
|           </TabPanel> | ||||
|         </TabPanels> | ||||
|       </Tabs> | ||||
|     </Card> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SystemPage; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Charles Bourque
					Charles Bourque