mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 17:32:20 +00:00
[WIFI-12261] Added system secrets on the system page
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(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