[WIFI-12261] Added system secrets on the system page

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-02-03 16:53:50 +01:00
parent 999680e94b
commit 227a51423d
18 changed files with 653 additions and 46 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.9.0(7)",
"version": "2.9.0(9)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -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!",

View File

@@ -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!",

View File

@@ -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!",

View File

@@ -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 !",

View File

@@ -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!",

View File

@@ -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}

View File

@@ -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) => (

View 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;

View 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;

View 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;

View 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;

View 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>
);
};

View File

@@ -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 = {

View File

@@ -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';

View File

@@ -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}
/>

View File

@@ -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;