mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-11-02 03:27:56 +00:00
Merge pull request #175 from Telecominfraproject/main
Version 2.9.0(18)
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.9.0(10)",
|
||||
"version": "2.9.0(18)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.9.0(10)",
|
||||
"version": "2.9.0(18)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.9.0(10)",
|
||||
"version": "2.9.0(18)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ interface Props extends ThemeProps {
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
label: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const defaultProps = {
|
||||
onClick: () => {},
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
label: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
label: undefined,
|
||||
ml: undefined,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const defaultProps = {
|
||||
label: 'Edit',
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
ml: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
isFetching: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
ml: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const ResponsiveButton = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
color,
|
||||
label,
|
||||
icon,
|
||||
|
||||
@@ -18,7 +18,7 @@ const defaultProps = {
|
||||
onSave: undefined,
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
ml: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
isDirty: false,
|
||||
ml: undefined,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props extends ThemeProps {
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
isCompact: false,
|
||||
isCompact: true,
|
||||
label: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, useBreakpoint } from '@chakra-ui/react';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { FunnelSimple } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
interface Props {
|
||||
type ColumnPickerProps = {
|
||||
preference: string;
|
||||
columns: Column[];
|
||||
columns: Column<unknown>[];
|
||||
hiddenColumns: string[];
|
||||
setHiddenColumns: (str: string[]) => void;
|
||||
}
|
||||
defaultHiddenColumns?: string[];
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
isCompact?: boolean;
|
||||
};
|
||||
|
||||
const ColumnPicker = ({ preference, columns, hiddenColumns, setHiddenColumns }: Props) => {
|
||||
const ColumnPicker = ({
|
||||
preference,
|
||||
columns,
|
||||
hiddenColumns,
|
||||
setHiddenColumns,
|
||||
defaultHiddenColumns = [],
|
||||
size,
|
||||
isCompact = true,
|
||||
}: ColumnPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { getPref, setPref } = useAuth();
|
||||
const breakpoint = useBreakpoint();
|
||||
@@ -28,14 +49,16 @@ const ColumnPicker = ({ preference, columns, hiddenColumns, setHiddenColumns }:
|
||||
|
||||
useEffect(() => {
|
||||
const savedPrefs = getPref(preference);
|
||||
setHiddenColumns(savedPrefs ? savedPrefs.split(',') : []);
|
||||
setHiddenColumns(savedPrefs ? savedPrefs.split(',') : defaultHiddenColumns);
|
||||
}, []);
|
||||
|
||||
if (breakpoint === 'base' || breakpoint === 'sm') {
|
||||
if (isCompact || breakpoint === 'base' || breakpoint === 'sm') {
|
||||
return (
|
||||
<Menu closeOnSelect={false}>
|
||||
<MenuButton as={IconButton} icon={<FunnelSimple />} />
|
||||
<MenuList maxH="200px" overflowY="auto">
|
||||
<Tooltip label={t('common.columns')} hasArrow>
|
||||
<MenuButton as={IconButton} size={size} icon={<FunnelSimple />} />
|
||||
</Tooltip>
|
||||
<MenuList maxH="210px" overflowY="auto">
|
||||
{columns.map((column) => (
|
||||
<MenuItem key={uuid()} isDisabled={column.alwaysShow} onClick={() => handleColumnClick(column.id)}>
|
||||
<Checkbox
|
||||
@@ -53,7 +76,7 @@ const ColumnPicker = ({ preference, columns, hiddenColumns, setHiddenColumns }:
|
||||
|
||||
return (
|
||||
<Menu closeOnSelect={false}>
|
||||
<MenuButton as={Button} rightIcon={<FunnelSimple />} minWidth="120px">
|
||||
<MenuButton as={Button} size={size} rightIcon={<FunnelSimple />} minWidth="120px">
|
||||
{t('common.columns')}
|
||||
</MenuButton>
|
||||
<MenuList maxH="200px" overflowY="auto">
|
||||
|
||||
@@ -98,6 +98,7 @@ const AlgorithmPicker = ({ algorithms, value, onChange, onRemove, isDisabled, op
|
||||
colorScheme="red"
|
||||
onClick={onRemove}
|
||||
icon={<Trash size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
mt={1}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -23,9 +23,10 @@ const DeviceRulesAlgorithms = ({ algorithms, value, setValue, isDisabled }: Prop
|
||||
}
|
||||
};
|
||||
const onAdd = () => {
|
||||
if (algorithms?.[0]) {
|
||||
const unusedAlgos = algorithms?.filter((a) => !value?.find((v) => v.name === a.shortName));
|
||||
if (unusedAlgos?.[0]) {
|
||||
const newValues = value ? [...value] : [];
|
||||
newValues.push({ name: algorithms[0].shortName, parameters: '' });
|
||||
newValues.push({ name: unusedAlgos[0].shortName, parameters: '' });
|
||||
setValue([...newValues]);
|
||||
}
|
||||
};
|
||||
@@ -62,7 +63,7 @@ const DeviceRulesAlgorithms = ({ algorithms, value, setValue, isDisabled }: Prop
|
||||
/>
|
||||
))}
|
||||
<Center>
|
||||
<Button onClick={onAdd} mb={2}>
|
||||
<Button onClick={onAdd} mb={2} isDisabled={!algorithms || value.length >= algorithms.length}>
|
||||
{t('crud.add')} {t('rrm.algorithm')}
|
||||
</Button>
|
||||
</Center>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, Box, Flex, FormControl, FormLabel, Select, UseDisclosureReturn } from '@chakra-ui/react';
|
||||
import { Alert, Box, Flex, UseDisclosureReturn } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import DeviceRulesAlgorithms from './Algorithms';
|
||||
import { CUSTOM_RRM, DEFAULT_RRM_CRON, isCustomRrm, isValidCustomRrm, RRM_VALUE } from './helper';
|
||||
import { CUSTOM_RRM, isCustomRrm, isValidCustomRrm, RRM_VALUE } from './helper';
|
||||
import RrmProviderPicker from './ProviderPicker';
|
||||
import RrmScheduler from './Scheduler';
|
||||
import RrmTypePicker from './TypePicker';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { RrmAlgorithm, RrmProvider } from 'hooks/Network/Rrm';
|
||||
import { RrmProviderCompleteInformation } from 'hooks/Network/Rrm';
|
||||
|
||||
const extractValueFromProps: (value: unknown) => RRM_VALUE = (value: unknown) => {
|
||||
try {
|
||||
@@ -36,26 +36,16 @@ type Props = {
|
||||
modalProps: UseDisclosureReturn;
|
||||
value: unknown;
|
||||
onChange: (v: RRM_VALUE) => void;
|
||||
algorithms?: RrmAlgorithm[];
|
||||
provider?: RrmProvider;
|
||||
providers?: RrmProviderCompleteInformation[];
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const EditRrmForm = ({ value, modalProps, onChange, algorithms, provider, isDisabled }: Props) => {
|
||||
const EditRrmForm = ({ value, modalProps, onChange, providers, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [newValue, setNewValue] = React.useState<RRM_VALUE>(extractValueFromProps(value));
|
||||
|
||||
const options = [
|
||||
{ label: t('common.custom'), value: 'custom' },
|
||||
{ label: t('common.no'), value: 'no' },
|
||||
{ label: t('common.inherit'), value: 'inherit' },
|
||||
];
|
||||
|
||||
const isCustom = isCustomRrm(newValue);
|
||||
|
||||
const onVendorChange = (vendor: string) => {
|
||||
if (isCustomRrm(newValue)) setNewValue({ ...newValue, vendor });
|
||||
};
|
||||
const onAlgoChange = (v: { name: string; parameters: string }[]) => {
|
||||
if (isCustomRrm(newValue)) setNewValue({ ...newValue, algorithms: v });
|
||||
};
|
||||
@@ -63,23 +53,6 @@ const EditRrmForm = ({ value, modalProps, onChange, algorithms, provider, isDisa
|
||||
if (isCustomRrm(newValue)) setNewValue({ ...newValue, schedule });
|
||||
};
|
||||
|
||||
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (e.target.value === 'custom') {
|
||||
setNewValue({
|
||||
vendor: provider?.vendorShortname ?? '',
|
||||
schedule: DEFAULT_RRM_CRON,
|
||||
algorithms: [
|
||||
{
|
||||
name: algorithms?.[0]?.shortName ?? '',
|
||||
parameters: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (e.target.value === 'no' || e.target.value === 'inherit') {
|
||||
setNewValue(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (isCustomRrm(newValue)) {
|
||||
onChange({ ...newValue, schedule: `0 ${newValue.schedule}` });
|
||||
@@ -109,11 +82,7 @@ const EditRrmForm = ({ value, modalProps, onChange, algorithms, provider, isDisa
|
||||
onClose={modalProps.onClose}
|
||||
topRightButtons={
|
||||
<Box mr={2}>
|
||||
<SaveButton
|
||||
isCompact
|
||||
onClick={onSave}
|
||||
isDisabled={isDisabled || !isValid || (isCustom && (!provider || !algorithms))}
|
||||
/>
|
||||
<SaveButton isCompact onClick={onSave} isDisabled={isDisabled || !isValid || (isCustom && !providers)} />
|
||||
</Box>
|
||||
}
|
||||
options={{
|
||||
@@ -121,52 +90,38 @@ const EditRrmForm = ({ value, modalProps, onChange, algorithms, provider, isDisa
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{isCustom && (!provider || !algorithms) && <Alert status="error">{t('rrm.cant_save_custom')}</Alert>}
|
||||
{isCustom && !providers && <Alert status="error">{t('rrm.cant_save_custom')}</Alert>}
|
||||
<Flex mb={2}>
|
||||
<FormControl isRequired w="unset" mr={2}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal" _disabled={{ opacity: 0.8 }}>
|
||||
{t('common.mode')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={isCustom ? 'custom' : newValue}
|
||||
onChange={onSelectChange}
|
||||
borderRadius="15px"
|
||||
fontSize="sm"
|
||||
_disabled={{ opacity: 0.8, cursor: 'not-allowed' }}
|
||||
border="2px solid"
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option value={option.value} key={uuid()}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<RrmTypePicker
|
||||
value={isCustom ? 'custom' : newValue}
|
||||
onChange={setNewValue}
|
||||
providers={providers}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Flex>
|
||||
{isCustomRrm(newValue) && (
|
||||
<>
|
||||
<Flex my={1}>
|
||||
<RrmProviderPicker
|
||||
providers={provider ? [provider] : []}
|
||||
providers={providers ?? []}
|
||||
value={newValue.vendor}
|
||||
setValue={onVendorChange}
|
||||
setValue={setNewValue}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Flex>
|
||||
<Box my={1}>
|
||||
<DeviceRulesAlgorithms
|
||||
algorithms={algorithms}
|
||||
algorithms={providers?.find((p) => p.rrm.vendorShortname === newValue.vendor)?.algorithms ?? []}
|
||||
value={newValue.algorithms}
|
||||
setValue={onAlgoChange}
|
||||
isDisabled={isDisabled || !provider}
|
||||
isDisabled={isDisabled || !providers}
|
||||
/>
|
||||
</Box>
|
||||
<Flex my={1}>
|
||||
<RrmScheduler
|
||||
value={newValue.schedule}
|
||||
setValue={onScheduleChange}
|
||||
isDisabled={isDisabled || !provider}
|
||||
isDisabled={isDisabled || !providers}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@@ -2,26 +2,40 @@ import * as React from 'react';
|
||||
import { Alert, Box, Flex, FormControl, FormLabel, Link, Select, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DEFAULT_RRM_CRON, RRM_VALUE } from './helper';
|
||||
import { InfoPopover } from 'components/InfoPopover';
|
||||
import { RrmProvider } from 'hooks/Network/Rrm';
|
||||
import { RrmProviderCompleteInformation } from 'hooks/Network/Rrm';
|
||||
|
||||
type Props = {
|
||||
providers: RrmProvider[];
|
||||
setValue: (v: string) => void;
|
||||
setValue: (v: RRM_VALUE) => void;
|
||||
value?: string;
|
||||
isDisabled?: boolean;
|
||||
providers?: RrmProviderCompleteInformation[];
|
||||
};
|
||||
|
||||
const RrmProviderPicker = ({ providers, value, setValue, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const options = providers.map((p) => ({ label: p.vendor, value: p.vendorShortname }));
|
||||
const options = providers?.map((p) => ({ label: p.rrm.vendor, value: p.rrm.vendorShortname })) ?? [];
|
||||
|
||||
const provider = providers.find((p) => p.vendorShortname === value);
|
||||
const provider = providers?.find((p) => p.rrm.vendorShortname === value);
|
||||
|
||||
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setValue(e.target.value);
|
||||
const found = providers?.find((p) => p.rrm.vendorShortname === e.target.value);
|
||||
if (found) {
|
||||
setValue({
|
||||
vendor: found.rrm.vendorShortname ?? '',
|
||||
schedule: DEFAULT_RRM_CRON,
|
||||
algorithms: [
|
||||
{
|
||||
name: found.algorithms?.[0]?.shortName ?? '',
|
||||
parameters: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (providers.length === 0 || !value) {
|
||||
if (providers?.length === 0 || !value || !providers) {
|
||||
return (
|
||||
<FormControl isRequired w="unset" mr={2}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal" _disabled={{ opacity: 0.8 }}>
|
||||
@@ -61,13 +75,19 @@ const RrmProviderPicker = ({ providers, value, setValue, isDisabled }: Props) =>
|
||||
>
|
||||
<Box>
|
||||
<Text display="flex">
|
||||
{t('rrm.version')}: {provider?.version}
|
||||
{t('rrm.version')}: {provider?.rrm.version}
|
||||
</Text>
|
||||
<Text display="flex">
|
||||
{t('common.details')}:
|
||||
<Link href={provider?.about} isExternal ml={1}>
|
||||
{provider?.about}
|
||||
</Link>
|
||||
{provider?.rrm.about.includes('http') ? (
|
||||
<Link href={provider?.rrm.about} isExternal ml={1}>
|
||||
{provider?.rrm.about}
|
||||
</Link>
|
||||
) : (
|
||||
<Text ml={1} fontWeight="bold">
|
||||
{provider?.rrm.about}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</InfoPopover>
|
||||
|
||||
67
src/components/CustomFields/RrmFormField/TypePicker.tsx
Normal file
67
src/components/CustomFields/RrmFormField/TypePicker.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import { FormControl, FormLabel, Select } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DEFAULT_RRM_CRON, RRM_VALUE } from './helper';
|
||||
import { RrmProviderCompleteInformation } from 'hooks/Network/Rrm';
|
||||
|
||||
type Props = {
|
||||
value: 'custom' | 'no' | 'inherit';
|
||||
onChange: (v: RRM_VALUE) => void;
|
||||
providers?: RrmProviderCompleteInformation[];
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const RrmTypePicker = ({ value, onChange, providers, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = [
|
||||
{ label: t('common.custom'), value: 'custom' },
|
||||
{ label: t('common.no'), value: 'no' },
|
||||
{ label: t('common.inherit'), value: 'inherit' },
|
||||
];
|
||||
|
||||
const onRrmTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (e.target.value === 'custom' && providers?.[0]) {
|
||||
onChange({
|
||||
vendor: providers?.[0]?.rrm.vendorShortname ?? '',
|
||||
schedule: DEFAULT_RRM_CRON,
|
||||
algorithms: [
|
||||
{
|
||||
name: providers?.[0]?.algorithms?.[0]?.shortName ?? '',
|
||||
parameters: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (e.target.value === 'no' || e.target.value === 'inherit') {
|
||||
onChange(e.target.value);
|
||||
} else {
|
||||
onChange('no');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl isRequired w="unset" mr={2}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal" _disabled={{ opacity: 0.8 }}>
|
||||
{t('common.mode')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onRrmTypeChange}
|
||||
borderRadius="15px"
|
||||
fontSize="sm"
|
||||
_disabled={{ opacity: 0.8, cursor: 'not-allowed' }}
|
||||
border="2px solid"
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option value={option.value} key={uuid()}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default RrmTypePicker;
|
||||
@@ -3,8 +3,8 @@ import { Button, FormControl, FormErrorMessage, FormLabel, useDisclosure } from
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import EditRrmForm from './Form';
|
||||
import { isCustomRrm } from './helper';
|
||||
import { useGetRrmAlgorithms, useGetRrmProvider } from 'hooks/Network/Rrm';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
import { useRrm } from 'hooks/useRrm';
|
||||
|
||||
type Props = {
|
||||
namePrefix?: string;
|
||||
@@ -16,8 +16,7 @@ const RrmFormField = ({ namePrefix = 'deviceRules', isDisabled }: Props) => {
|
||||
const name = `${namePrefix}.rrm`;
|
||||
const { value, isError, error, onChange } = useFastField({ name });
|
||||
const modalProps = useDisclosure();
|
||||
const { data: provider, isLoading: isLoadingProvider } = useGetRrmProvider();
|
||||
const { data: algos, isLoading: isLoadingAlgos } = useGetRrmAlgorithms();
|
||||
const rrm = useRrm();
|
||||
|
||||
const displayedValue = React.useMemo(() => {
|
||||
try {
|
||||
@@ -47,7 +46,7 @@ const RrmFormField = ({ namePrefix = 'deviceRules', isDisabled }: Props) => {
|
||||
colorScheme="blue"
|
||||
mt={2}
|
||||
ml={1}
|
||||
isLoading={isLoadingProvider || isLoadingAlgos}
|
||||
isLoading={rrm.getProviders.isFetching}
|
||||
>
|
||||
{displayedValue}
|
||||
</Button>
|
||||
@@ -56,8 +55,7 @@ const RrmFormField = ({ namePrefix = 'deviceRules', isDisabled }: Props) => {
|
||||
value={value}
|
||||
modalProps={modalProps}
|
||||
onChange={onChange}
|
||||
algorithms={algos}
|
||||
provider={provider}
|
||||
providers={rrm.getProviders.data}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -45,15 +45,18 @@ const defaultProps = {
|
||||
sortBy: [],
|
||||
};
|
||||
|
||||
type DataTableProps = {
|
||||
columns: readonly Column<object>[];
|
||||
data: object[];
|
||||
type DataTableProps<TValue> = {
|
||||
columns: Column<TValue>[];
|
||||
data: TValue[];
|
||||
count?: number;
|
||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||
isLoading?: boolean;
|
||||
onRowClick?: (row: TValue) => void;
|
||||
isRowClickable?: (row: TValue) => boolean;
|
||||
obj?: string;
|
||||
sortBy?: { id: string; desc: boolean }[];
|
||||
hiddenColumns?: string[];
|
||||
hideEmptyListText?: boolean;
|
||||
hideControls?: boolean;
|
||||
minHeight?: string | number;
|
||||
fullScreen?: boolean;
|
||||
@@ -68,7 +71,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
||||
state: UsePaginationState<T>;
|
||||
};
|
||||
|
||||
const DataTable = ({
|
||||
const DataTable = <TValue extends object>({
|
||||
columns,
|
||||
data,
|
||||
isLoading,
|
||||
@@ -78,16 +81,19 @@ const DataTable = ({
|
||||
sortBy,
|
||||
hiddenColumns,
|
||||
hideControls,
|
||||
hideEmptyListText,
|
||||
count,
|
||||
setPageInfo,
|
||||
isManual,
|
||||
saveSettingsId,
|
||||
showAllRows,
|
||||
}: DataTableProps) => {
|
||||
onRowClick,
|
||||
isRowClickable,
|
||||
}: DataTableProps<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||
const getPageSize = () => {
|
||||
try {
|
||||
if (showAllRows) return 1000000;
|
||||
@@ -142,7 +148,7 @@ const DataTable = ({
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
) as TableInstanceWithHooks<object>;
|
||||
) as TableInstanceWithHooks<TValue>;
|
||||
|
||||
const handleGoToPage = (newPage: number) => {
|
||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
|
||||
@@ -259,8 +265,10 @@ const DataTable = ({
|
||||
</Thead>
|
||||
{data.length > 0 && (
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{page.map((row: Row) => {
|
||||
{page.map((row: Row<TValue>) => {
|
||||
prepareRow(row);
|
||||
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
|
||||
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
|
||||
return (
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
@@ -268,6 +276,7 @@ const DataTable = ({
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
// @ts-ignore
|
||||
@@ -287,8 +296,26 @@ const DataTable = ({
|
||||
fontSize="14px"
|
||||
// @ts-ignore
|
||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||
// @ts-ignore
|
||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
||||
fontFamily={
|
||||
// @ts-ignore
|
||||
cell.column.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
// @ts-ignore
|
||||
cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick)
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
cursor={
|
||||
// @ts-ignore
|
||||
!cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick
|
||||
? 'pointer'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</Td>
|
||||
@@ -300,7 +327,7 @@ const DataTable = ({
|
||||
</Tbody>
|
||||
)}
|
||||
</Table>
|
||||
{!isLoading && data.length === 0 && (
|
||||
{!isLoading && data.length === 0 && !hideEmptyListText && (
|
||||
<Center>
|
||||
{obj ? (
|
||||
<Heading size="md" pt={12}>
|
||||
@@ -413,4 +440,4 @@ const DataTable = ({
|
||||
|
||||
DataTable.defaultProps = defaultProps;
|
||||
|
||||
export default React.memo(DataTable);
|
||||
export default DataTable;
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Column } from 'models/Table';
|
||||
interface ObjectArrayFieldModalOptions {
|
||||
buttonLabel?: string;
|
||||
modalTitle?: string;
|
||||
onFormSubmit?: (value: any) => object;
|
||||
}
|
||||
|
||||
interface Props extends FieldInputProps<object[]> {
|
||||
@@ -41,24 +42,22 @@ interface Props extends FieldInputProps<object[]> {
|
||||
schema: (t: (e: string) => string, useDefault?: boolean) => object;
|
||||
}
|
||||
|
||||
const ObjectArrayFieldInput = (
|
||||
{
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
isError,
|
||||
error,
|
||||
fields,
|
||||
isRequired,
|
||||
isHidden,
|
||||
schema,
|
||||
columns,
|
||||
isDisabled,
|
||||
hideLabel,
|
||||
options
|
||||
}: Props
|
||||
) => {
|
||||
const ObjectArrayFieldInput: React.FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
isError,
|
||||
error,
|
||||
fields,
|
||||
isRequired,
|
||||
isHidden,
|
||||
schema,
|
||||
columns,
|
||||
isDisabled,
|
||||
hideLabel,
|
||||
options,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [tempValue, setTempValue] = useState<object[]>([]);
|
||||
@@ -76,7 +75,7 @@ const ObjectArrayFieldInput = (
|
||||
};
|
||||
|
||||
const removeAction = useCallback(
|
||||
(cell) => (
|
||||
(cell: { row: { index: number } }) => (
|
||||
<Tooltip hasArrow label={t('common.remove')} placement="top">
|
||||
<IconButton
|
||||
aria-label="Remove Object"
|
||||
@@ -132,7 +131,11 @@ const ObjectArrayFieldInput = (
|
||||
validateOnMount
|
||||
onSubmit={(data, { setSubmitting, resetForm }) => {
|
||||
setSubmitting(true);
|
||||
setTempValue([...tempValue, data]);
|
||||
if (!options.onFormSubmit) {
|
||||
setTempValue([...tempValue, data]);
|
||||
} else {
|
||||
setTempValue([...tempValue, options.onFormSubmit(data)]);
|
||||
}
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
}}
|
||||
|
||||
@@ -5,27 +5,29 @@ import useFastField from 'hooks/useFastField';
|
||||
import { FieldProps } from 'models/Form';
|
||||
|
||||
interface Props extends FieldProps, LayoutProps {
|
||||
formatValue?: (value: string) => string;
|
||||
hideButton?: boolean;
|
||||
}
|
||||
|
||||
const StringField = ({
|
||||
const StringField: React.FC<Props> = ({
|
||||
name,
|
||||
isDisabled = false,
|
||||
label,
|
||||
hideButton = false,
|
||||
isRequired = false,
|
||||
element,
|
||||
formatValue,
|
||||
isArea = false,
|
||||
emptyIsUndefined = false,
|
||||
definitionKey,
|
||||
...props
|
||||
}: Props) => {
|
||||
}) => {
|
||||
const { value, error, isError, onChange, onBlur } = useFastField<string | undefined>({ name });
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
if (emptyIsUndefined && e.target.value.length === 0) onChange(undefined);
|
||||
else onChange(e.target.value);
|
||||
else onChange(formatValue ? formatValue(e.target.value) : e.target.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Modal, ModalOverlay, ModalContent, ModalBody, useDisclosure, Flex } from '@chakra-ui/react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
useDisclosure,
|
||||
Flex,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SubscriberSearchDisplayTable from './Table';
|
||||
@@ -19,9 +28,9 @@ const SubscriberSearchModal = ({ operatorId }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button alignItems="center" colorScheme="blue" rightIcon={<MagnifyingGlass />} onClick={onOpen} ml={2}>
|
||||
{t('common.search')}
|
||||
</Button>
|
||||
<Tooltip label={t('common.search')} hasArrow>
|
||||
<IconButton aria-label={t('common.search')} icon={<MagnifyingGlass />} onClick={onOpen} colorScheme="teal" />
|
||||
</Tooltip>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '90%', md: '900px', lg: '1000px', xl: '80%' }}>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, Modal, ModalOverlay, ModalContent, ModalBody, Center, Spinner } from '@chakra-ui/react';
|
||||
import { Modal, ModalOverlay, ModalContent, ModalBody, Center, Spinner } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateSubscriberDeviceStep0 from './MultiStepForm/Step0';
|
||||
import CreateSubscriberDeviceStep1 from './MultiStepForm/Step1';
|
||||
import CreateSubscriberDeviceStep2 from './MultiStepForm/Step2';
|
||||
import CreateSubscriberDeviceStep3 from './MultiStepForm/Step3';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import StepButton from 'components/Buttons/StepButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
@@ -141,16 +141,7 @@ const CreateSubscriberDeviceModal = ({ refresh, operatorId, subscriberId, device
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'CSR'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '90%', md: '900px', lg: '1000px', xl: '80%' }}>
|
||||
|
||||
@@ -26,24 +26,33 @@ import {
|
||||
Heading,
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
// @ts-ignore
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTable, usePagination, useSortBy, Row } from 'react-table';
|
||||
import {
|
||||
useTable,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
Row,
|
||||
UsePaginationInstanceProps,
|
||||
UseSortByInstanceProps,
|
||||
UsePaginationState,
|
||||
} from 'react-table';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import SortIcon from './SortIcon';
|
||||
import { isColumnSorted, isSortedDesc, onSortClick } from './utils';
|
||||
import LoadingOverlay from 'components/LoadingOverlay';
|
||||
import { Column, PageInfo, SortInfo } from 'models/Table';
|
||||
|
||||
interface Props {
|
||||
columns: Column[];
|
||||
data: object[];
|
||||
interface Props<TValue> {
|
||||
columns: Column<TValue>[];
|
||||
data: TValue[];
|
||||
count?: number;
|
||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||
sortInfo: SortInfo;
|
||||
setSortInfo: React.Dispatch<React.SetStateAction<SortInfo>>;
|
||||
isLoading?: boolean;
|
||||
obj: string;
|
||||
onRowClick?: (row: TValue) => void;
|
||||
isRowClickable?: (row: TValue) => boolean;
|
||||
sortBy?: { id: string; desc: boolean }[];
|
||||
hiddenColumns?: string[];
|
||||
hideControls?: boolean;
|
||||
@@ -53,6 +62,12 @@ interface Props {
|
||||
saveSettingsId?: string;
|
||||
}
|
||||
|
||||
type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
||||
UsePaginationInstanceProps<T> &
|
||||
UseSortByInstanceProps<T> & {
|
||||
state: UsePaginationState<T>;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
count: undefined,
|
||||
setPageInfo: undefined,
|
||||
@@ -66,7 +81,7 @@ const defaultProps = {
|
||||
saveSettingsId: undefined,
|
||||
};
|
||||
|
||||
const SortableDataTable = ({
|
||||
const SortableDataTable = <TValue extends object>({
|
||||
columns,
|
||||
data,
|
||||
isLoading,
|
||||
@@ -82,7 +97,9 @@ const SortableDataTable = ({
|
||||
setPageInfo,
|
||||
isManual,
|
||||
saveSettingsId,
|
||||
}: Props) => {
|
||||
onRowClick,
|
||||
isRowClickable,
|
||||
}: Props<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||
@@ -112,21 +129,23 @@ const SortableDataTable = ({
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable(
|
||||
{
|
||||
// @ts-ignore
|
||||
columns,
|
||||
data,
|
||||
data, // @ts-ignore
|
||||
initialState: { sortBy, pagination: !hideControls, pageSize: queryPageSize },
|
||||
manualPagination: isManual,
|
||||
pageCount: isManual && count !== undefined ? Math.ceil(count / queryPageSize) : undefined,
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
);
|
||||
) as TableInstanceWithHooks<TValue>;
|
||||
|
||||
useEffect(() => {
|
||||
if (setPageInfo && pageIndex !== undefined) setPageInfo({ index: pageIndex, limit: queryPageSize });
|
||||
}, [queryPageSize, pageIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
if (saveSettingsId) localStorage.setItem(saveSettingsId, pageSize);
|
||||
setQueryPageSize(pageSize);
|
||||
}, [pageSize]);
|
||||
@@ -170,48 +189,44 @@ const SortableDataTable = ({
|
||||
<LoadingOverlay isLoading={isManual !== undefined && isManual && isLoading !== undefined && isLoading}>
|
||||
<Table {...getTableProps()} size="small" textColor={textColor} w="100%">
|
||||
<Thead fontSize="14px">
|
||||
{
|
||||
// @ts-ignore
|
||||
headerGroups.map((group) => (
|
||||
<Tr {...group.getHeaderGroupProps()} key={uuid()}>
|
||||
{
|
||||
{headerGroups.map((group) => (
|
||||
<Tr {...group.getHeaderGroupProps()}>
|
||||
{group.headers.map((column) => (
|
||||
<Th
|
||||
color="gray.400"
|
||||
{...column.getHeaderProps()}
|
||||
// @ts-ignore
|
||||
group.headers.map((column) => (
|
||||
<Th
|
||||
color="gray.400"
|
||||
{...column.getHeaderProps()}
|
||||
minWidth={column.customMinWidth ?? null}
|
||||
// @ts-ignore
|
||||
maxWidth={column.customMaxWidth ?? null}
|
||||
// @ts-ignore
|
||||
width={column.customWidth ?? null}
|
||||
>
|
||||
<div
|
||||
onClick={() => onSortClick(column.id, sortInfo, setSortInfo)}
|
||||
style={{ alignContent: 'center', overflow: 'hidden', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{column.render('Header')}
|
||||
<SortIcon
|
||||
// @ts-ignore
|
||||
minWidth={column.customMinWidth ?? null}
|
||||
isSorted={isColumnSorted(column.id, sortInfo)}
|
||||
// @ts-ignore
|
||||
maxWidth={column.customMaxWidth ?? null}
|
||||
isSortedDesc={isSortedDesc(column.id, sortInfo)}
|
||||
// @ts-ignore
|
||||
width={column.customWidth ?? null}
|
||||
>
|
||||
<div
|
||||
onClick={() => onSortClick(column.id, sortInfo, setSortInfo)}
|
||||
style={{ alignContent: 'center', overflow: 'hidden', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{column.render('Header')}
|
||||
<SortIcon
|
||||
// @ts-ignore
|
||||
isSorted={isColumnSorted(column.id, sortInfo)}
|
||||
// @ts-ignore
|
||||
isSortedDesc={isSortedDesc(column.id, sortInfo)}
|
||||
// @ts-ignore
|
||||
canSort={column.canSort}
|
||||
/>
|
||||
</div>
|
||||
</Th>
|
||||
))
|
||||
}
|
||||
</Tr>
|
||||
))
|
||||
}
|
||||
canSort={column.canSort}
|
||||
/>
|
||||
</div>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
{data.length > 0 && (
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{page.map((row: Row) => {
|
||||
{page.map((row: Row<TValue>) => {
|
||||
prepareRow(row);
|
||||
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
|
||||
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
|
||||
return (
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
@@ -219,6 +234,7 @@ const SortableDataTable = ({
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
// @ts-ignore
|
||||
@@ -238,8 +254,26 @@ const SortableDataTable = ({
|
||||
fontSize="14px"
|
||||
// @ts-ignore
|
||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||
// @ts-ignore
|
||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
||||
fontFamily={
|
||||
// @ts-ignore
|
||||
cell.column.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
// @ts-ignore
|
||||
cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick)
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
cursor={
|
||||
// @ts-ignore
|
||||
!cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick
|
||||
? 'pointer'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</Td>
|
||||
@@ -300,7 +334,7 @@ const SortableDataTable = ({
|
||||
w={28}
|
||||
min={1}
|
||||
max={pageOptions.length}
|
||||
onChange={(_, numberValue) => {
|
||||
onChange={(_: unknown, numberValue: number) => {
|
||||
const newPage = numberValue ? numberValue - 1 : 0;
|
||||
gotoPage(newPage);
|
||||
}}
|
||||
@@ -317,7 +351,7 @@ const SortableDataTable = ({
|
||||
<Select
|
||||
w={32}
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -66,7 +66,7 @@ const DeviceActionDropdown = ({
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateConfigurationForm from './Form';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
@@ -51,9 +51,7 @@ const CreateConfigurationModal = ({ refresh, entityId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button alignItems="center" colorScheme="blue" rightIcon={<AddIcon />} onClick={openModal} ml={2}>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
<CreateButton onClick={openModal} ml={2} />
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '90%', md: '900px', lg: '1000px', xl: '80%' }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import DataTable from 'components/DataTable';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
@@ -14,6 +15,9 @@ const propTypes = {
|
||||
const ConfigurationsTable = ({ select, actions }) => {
|
||||
const { t } = useTranslation();
|
||||
const { data: configurations, isFetching } = useGetSelectConfigurations({ select });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleGoToPage = (configuration) => navigate(`/configuration/${configuration.id}`);
|
||||
|
||||
const dateCell = useCallback((cell, key) => <FormattedDate date={cell.row.values[key]} key={uuid()} />, []);
|
||||
const typesCell = useCallback((cell) => cell.row.values.deviceTypes.join(', '), []);
|
||||
@@ -32,20 +36,13 @@ const ConfigurationsTable = ({ select, actions }) => {
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
Header: t('common.description'),
|
||||
id: 'deviceTypes',
|
||||
Header: t('configurations.device_types'),
|
||||
Footer: '',
|
||||
accessor: 'description',
|
||||
accessor: 'deviceTypes',
|
||||
Cell: ({ cell }) => typesCell(cell),
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
Header: t('common.created'),
|
||||
Footer: '',
|
||||
accessor: 'created',
|
||||
Cell: ({ cell }) => dateCell(cell, 'created'),
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
customMaxWidth: '150px',
|
||||
},
|
||||
{
|
||||
id: 'modified',
|
||||
@@ -57,17 +54,15 @@ const ConfigurationsTable = ({ select, actions }) => {
|
||||
customWidth: '150px',
|
||||
},
|
||||
{
|
||||
id: 'deviceTypes',
|
||||
Header: t('configurations.device_types'),
|
||||
id: 'description',
|
||||
Header: t('common.description'),
|
||||
Footer: '',
|
||||
accessor: 'deviceTypes',
|
||||
Cell: ({ cell }) => typesCell(cell),
|
||||
accessor: 'description',
|
||||
disableSortBy: true,
|
||||
customMaxWidth: '150px',
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
Header: t('common.actions'),
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
customWidth: '80px',
|
||||
@@ -86,14 +81,18 @@ const ConfigurationsTable = ({ select, actions }) => {
|
||||
columns={columns}
|
||||
data={configurations ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('configurations.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
obj={t('configurations.title')}
|
||||
minHeight="200px"
|
||||
hideControls
|
||||
showAllRows
|
||||
onRowClick={handleGoToPage}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateContactForm from './Form';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
@@ -47,16 +47,7 @@ const CreateContactModal = ({ refresh, entityId, isVenue, onCreate }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'CSR'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" initialFocusRef={initialRef}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -13,6 +13,7 @@ const propTypes = {
|
||||
ignoredColumns: PropTypes.arrayOf(PropTypes.string),
|
||||
refreshId: PropTypes.number,
|
||||
disabledIds: PropTypes.arrayOf(PropTypes.string),
|
||||
openDetailsModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@@ -21,7 +22,7 @@ const defaultProps = {
|
||||
disabledIds: [],
|
||||
};
|
||||
|
||||
const ContactTable = ({ actions, select, ignoredColumns, refreshId, disabledIds }) => {
|
||||
const ContactTable = ({ actions, select, ignoredColumns, refreshId, disabledIds, openDetailsModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { data: venues, isFetching, refetch } = useGetSelectContacts({ t, toast, select });
|
||||
@@ -75,8 +76,8 @@ const ContactTable = ({ actions, select, ignoredColumns, refreshId, disabledIds
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
Header: t('common.actions'),
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
customWidth: '80px',
|
||||
@@ -106,6 +107,10 @@ const ContactTable = ({ actions, select, ignoredColumns, refreshId, disabledIds
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
showAllRows
|
||||
hideControls
|
||||
onRowClick={openDetailsModal}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ const DeviceActionDropdown = ({
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
|
||||
@@ -11,9 +11,19 @@ import {
|
||||
useBoolean,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Popover,
|
||||
Box,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverArrow,
|
||||
PopoverCloseButton,
|
||||
PopoverHeader,
|
||||
PopoverBody,
|
||||
PopoverFooter,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { ArrowSquareOut, PaperPlaneTilt } from 'phosphor-react';
|
||||
import { ArrowSquareOut, PaperPlaneTilt, Trash } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DeviceActionDropdown from './ActionDropdown';
|
||||
@@ -26,7 +36,7 @@ import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useGetDeviceConfigurationOverrides } from 'hooks/Network/ConfigurationOverride';
|
||||
import useGetDeviceTypes from 'hooks/Network/DeviceTypes';
|
||||
import { useGetGatewayUi } from 'hooks/Network/Endpoints';
|
||||
import { useGetTag } from 'hooks/Network/Inventory';
|
||||
import { useDeleteTag, useGetTag } from 'hooks/Network/Inventory';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
const propTypes = {
|
||||
@@ -59,10 +69,24 @@ const EditTagModal = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [editing, setEditing] = useBoolean();
|
||||
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const toast = useToast();
|
||||
const [form, setForm] = useState({});
|
||||
const [configuration, setConfiguration] = useState(null);
|
||||
const { mutateAsync: deleteTag, isLoading: isDeleting } = useDeleteTag({
|
||||
name: tag?.name,
|
||||
refreshTable: refresh,
|
||||
onClose,
|
||||
});
|
||||
|
||||
const handleDeleteClick = () =>
|
||||
deleteTag(tag.serialNumber, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const { data: gwUi } = useGetGatewayUi();
|
||||
const formRef = useCallback(
|
||||
(node) => {
|
||||
@@ -125,11 +149,33 @@ const EditTagModal = ({
|
||||
title={t('crud.edit_obj', { obj: tag?.name ?? tag?.serialNumber })}
|
||||
right={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!editing || !form.isValid || (configuration !== null && !configuration.__form.isValid)}
|
||||
/>
|
||||
<Popover isOpen={isDeleteOpen} onOpen={onDeleteOpen} onClose={onDeleteClose}>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isDeleteOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label="Open Device Delete" colorScheme="red" icon={<Trash size={20} />} />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent fontSize="md" fontWeight="normal">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {tag?.name}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>{t('crud.delete_confirm', { obj: t('inventory.tag_one') })}</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onDeleteClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DeviceActionDropdown
|
||||
device={tag}
|
||||
isDisabled={editing}
|
||||
@@ -156,6 +202,13 @@ const EditTagModal = ({
|
||||
onClick={handlePushConfig}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!editing || !form.isValid || (configuration !== null && !configuration.__form.isValid)}
|
||||
hidden={!editing}
|
||||
ml={2}
|
||||
/>
|
||||
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
|
||||
<CloseButton ml={2} onClick={closeModal} />
|
||||
</>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
useBreakpoint,
|
||||
Button,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody, Tooltip, IconButton } from '@chakra-ui/react';
|
||||
import { UploadSimple } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -36,7 +26,6 @@ const defaultProps = {
|
||||
|
||||
const ImportDeviceCsvModal = ({ refresh, deviceClass, parent }) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
const [refreshId, setRefreshId] = useState(uuid());
|
||||
// 0: explanation, file import and file analysis
|
||||
// 1: testing the serial number list with the API
|
||||
@@ -91,22 +80,6 @@ const ImportDeviceCsvModal = ({ refresh, deviceClass, parent }) => {
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const getButton = () => {
|
||||
if (breakpoint !== 'base' && breakpoint !== 'sm') {
|
||||
return (
|
||||
<Button ml={2} colorScheme="blue" onClick={openModal} rightIcon={<UploadSimple size={20} />}>
|
||||
{t('devices.import_batch_tags')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={t('devices.import_batch_tags')}>
|
||||
<IconButton ml={2} colorScheme="blue" onClick={openModal} icon={<UploadSimple size={20} />} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const closeModal = () => (isCloseable ? onClose() : openConfirm());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
@@ -116,7 +89,9 @@ const ImportDeviceCsvModal = ({ refresh, deviceClass, parent }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{getButton()}
|
||||
<Tooltip label={t('devices.import_batch_tags')}>
|
||||
<IconButton ml={2} colorScheme="teal" onClick={openModal} icon={<UploadSimple size={20} />} />
|
||||
</Tooltip>
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -18,6 +18,7 @@ const propTypes = {
|
||||
refreshId: PropTypes.number,
|
||||
onlyUnassigned: PropTypes.bool,
|
||||
minHeight: PropTypes.instanceOf(Object),
|
||||
openDetailsModal: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@@ -46,6 +47,7 @@ const InventoryTable = ({
|
||||
actions,
|
||||
refreshId,
|
||||
minHeight,
|
||||
openDetailsModal,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
@@ -122,6 +124,16 @@ const InventoryTable = ({
|
||||
customWidth: 'calc(15vh)',
|
||||
customMinWidth: '150px',
|
||||
isMonospace: true,
|
||||
sortType: (rowA, rowB, id) => {
|
||||
const a = rowA.values[id];
|
||||
const b = rowB.values[id];
|
||||
|
||||
if (a && b) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
@@ -171,11 +183,12 @@ const InventoryTable = ({
|
||||
if (actions !== null || addAction || removeAction) {
|
||||
baseColumns.push({
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Header: '',
|
||||
Footer: '',
|
||||
accessor: 'id',
|
||||
Cell: ({ cell }) => deviceActions(cell),
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
});
|
||||
}
|
||||
return baseColumns;
|
||||
@@ -193,15 +206,17 @@ const InventoryTable = ({
|
||||
isLoading={isFetchingCount || isFetchingTags}
|
||||
isManual={!isManual}
|
||||
obj={t('devices.title')}
|
||||
count={count || 0}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'serialNumber',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
count={count || 0}
|
||||
setPageInfo={setPageInfo}
|
||||
minHeight={minHeight ?? '200px'}
|
||||
onRowClick={openDetailsModal}
|
||||
isRowClickable={openDetailsModal ? () => true : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -222,6 +237,8 @@ const InventoryTable = ({
|
||||
count={count || 0}
|
||||
setPageInfo={setPageInfo}
|
||||
minHeight={minHeight ?? '200px'}
|
||||
onRowClick={openDetailsModal}
|
||||
isRowClickable={openDetailsModal ? () => true : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateLocationForm from './Form';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
|
||||
const propTypes = {
|
||||
@@ -22,7 +21,6 @@ const defaultProps = {
|
||||
|
||||
const CreateLocationModal = ({ refresh, entityId }) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const { form, formRef } = useFormRef();
|
||||
@@ -36,16 +34,7 @@ const CreateLocationModal = ({ refresh, entityId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'CSR'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
<CreateButton onClick={onOpen} ml={2} />
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -13,6 +13,7 @@ const propTypes = {
|
||||
ignoredColumns: PropTypes.arrayOf(PropTypes.string),
|
||||
refreshId: PropTypes.number,
|
||||
disabledIds: PropTypes.arrayOf(PropTypes.string),
|
||||
openDetailsModal: PropTypes.func.isRequired,
|
||||
};
|
||||
const defaultProps = {
|
||||
ignoredColumns: [],
|
||||
@@ -20,7 +21,7 @@ const defaultProps = {
|
||||
disabledIds: [],
|
||||
};
|
||||
|
||||
const LocationTable = ({ actions, select, refreshId, ignoredColumns, disabledIds }) => {
|
||||
const LocationTable = ({ actions, select, refreshId, ignoredColumns, disabledIds, openDetailsModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { data: venues, isFetching, refetch } = useGetSelectLocations({ t, toast, select });
|
||||
@@ -81,7 +82,7 @@ const LocationTable = ({ actions, select, refreshId, ignoredColumns, disabledIds
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
@@ -112,6 +113,8 @@ const LocationTable = ({ actions, select, refreshId, ignoredColumns, disabledIds
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
onRowClick={openDetailsModal}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -10,17 +10,13 @@ import { useGetResources } from 'hooks/Network/Resources';
|
||||
const propTypes = {
|
||||
actions: PropTypes.func.isRequired,
|
||||
select: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
refreshId: PropTypes.number.isRequired,
|
||||
openDetailsModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ResourcesTable = ({ select, actions, refreshId }) => {
|
||||
const ResourcesTable = ({ select, actions, openDetailsModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const {
|
||||
data: resources,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useGetResources({
|
||||
const { data: resources, isFetching } = useGetResources({
|
||||
t,
|
||||
toast,
|
||||
select,
|
||||
@@ -70,7 +66,7 @@ const ResourcesTable = ({ select, actions, refreshId }) => {
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Header: '',
|
||||
Footer: '',
|
||||
accessor: 'id',
|
||||
customWidth: '80px',
|
||||
@@ -83,10 +79,6 @@ const ResourcesTable = ({ select, actions, refreshId }) => {
|
||||
return baseColumns;
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshId > 0) refetch();
|
||||
}, [refreshId]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
@@ -100,6 +92,10 @@ const ResourcesTable = ({ select, actions, refreshId }) => {
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
showAllRows
|
||||
hideControls
|
||||
onRowClick={openDetailsModal}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Column, DeviceCell } from 'models/Table';
|
||||
|
||||
interface Props {
|
||||
actions: (cell: DeviceCell) => React.ReactElement;
|
||||
onOpenDetails: (device: Device) => void;
|
||||
operatorId: string;
|
||||
subscriberId?: string;
|
||||
setDevices?: React.Dispatch<React.SetStateAction<Device[]>>;
|
||||
@@ -26,16 +27,17 @@ const defaultProps = {
|
||||
minHeight: undefined,
|
||||
};
|
||||
|
||||
const SubscriberDeviceTable = ({
|
||||
const SubscriberDeviceTable: React.FC<Props> = ({
|
||||
actions,
|
||||
operatorId,
|
||||
onOpenDetails,
|
||||
subscriberId = '',
|
||||
setDevices,
|
||||
ignoredColumns,
|
||||
refreshId,
|
||||
disabledIds,
|
||||
minHeight,
|
||||
}: Props) => {
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { data: subscriberDevices, isFetching, refetch } = useGetSubscriberDevices({ operatorId, subscriberId });
|
||||
|
||||
@@ -44,7 +46,7 @@ const SubscriberDeviceTable = ({
|
||||
|
||||
// Columns array. This array contains your table headings and accessors which maps keys from data array
|
||||
const columns = React.useMemo(
|
||||
(): Column[] => [
|
||||
(): Column<Device>[] => [
|
||||
{
|
||||
id: 'serialNumber',
|
||||
Header: t('inventory.serial_number'),
|
||||
@@ -90,7 +92,7 @@ const SubscriberDeviceTable = ({
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
@@ -126,6 +128,8 @@ const SubscriberDeviceTable = ({
|
||||
},
|
||||
]}
|
||||
minHeight={minHeight ?? '200px'}
|
||||
onRowClick={onOpenDetails}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateSubscriberForm from './Form';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
|
||||
@@ -18,7 +17,6 @@ const propTypes = {
|
||||
};
|
||||
const CreateSubscriberModal = ({ refresh, operatorId }) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { form, formRef } = useFormRef();
|
||||
const { isOpen, isConfirmOpen, onOpen, closeConfirm, closeModal, closeCancelAndForm } = useFormModal({
|
||||
isDirty: form?.dirty,
|
||||
@@ -26,16 +24,7 @@ const CreateSubscriberModal = ({ refresh, operatorId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'CSR'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
<CreateButton onClick={onOpen} ml={2} />
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import DataTable from 'components/DataTable';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
@@ -33,6 +34,8 @@ const SubscriberTable = ({ actions, operatorId, refreshId, disabledIds }) => {
|
||||
countParams: { operatorId },
|
||||
getParams: { operatorId },
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const handleGoToClick = (subscriber) => navigate(`/subscriber/${subscriber.id}`);
|
||||
|
||||
const actionCell = useCallback((cell) => actions(cell), [actions]);
|
||||
const memoizedDate = useCallback((cell, key) => <FormattedDate date={cell.row.values[key]} key={uuid()} />, []);
|
||||
@@ -89,7 +92,7 @@ const SubscriberTable = ({ actions, operatorId, refreshId, disabledIds }) => {
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
@@ -118,6 +121,8 @@ const SubscriberTable = ({ actions, operatorId, refreshId, disabledIds }) => {
|
||||
setPageInfo={setPageInfo}
|
||||
saveSettingsId="operator.subscribers.table"
|
||||
minHeight="200px"
|
||||
onRowClick={handleGoToClick}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { axiosRrm } from 'utils/axiosInstances';
|
||||
import { QueryFunctionContext, useQuery } from '@tanstack/react-query';
|
||||
import { EndpointApiResponse } from './Endpoints';
|
||||
import { axiosProv, axiosRrm } from 'utils/axiosInstances';
|
||||
|
||||
export type RrmProvider = {
|
||||
vendor: string;
|
||||
@@ -7,10 +8,6 @@ export type RrmProvider = {
|
||||
version: string;
|
||||
about: string;
|
||||
};
|
||||
export const useGetRrmProvider = () =>
|
||||
useQuery(['get-rrm-provider'], () => axiosRrm.get('provider').then(({ data }) => data), {
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
export type RrmAlgorithm = {
|
||||
name: string;
|
||||
@@ -20,11 +17,70 @@ export type RrmAlgorithm = {
|
||||
parameterSamples: string;
|
||||
helper: string;
|
||||
};
|
||||
export const useGetRrmAlgorithms = () =>
|
||||
|
||||
export interface RrmProviderCompleteInformation extends EndpointApiResponse {
|
||||
algorithms: RrmAlgorithm[];
|
||||
rrm: RrmProvider;
|
||||
}
|
||||
|
||||
const getRrmProvider = async (
|
||||
endpoint: EndpointApiResponse & { algorithms: RrmAlgorithm[] },
|
||||
): Promise<RrmProviderCompleteInformation> =>
|
||||
axiosRrm
|
||||
.get(`${endpoint.uri}/api/v1/provider`)
|
||||
.then(({ data }: { data: RrmProvider }) => ({
|
||||
...endpoint,
|
||||
rrm: data,
|
||||
}))
|
||||
.catch(() => ({
|
||||
...endpoint,
|
||||
rrm: {
|
||||
vendor: '',
|
||||
vendorShortname: '',
|
||||
version: '',
|
||||
about: '',
|
||||
},
|
||||
}));
|
||||
|
||||
const getRrmAlgorithms = async (
|
||||
endpoint: EndpointApiResponse,
|
||||
): Promise<EndpointApiResponse & { algorithms: RrmAlgorithm[] }> =>
|
||||
axiosRrm
|
||||
.get(`${endpoint.uri}/api/v1/algorithms`)
|
||||
.then(({ data }: { data: RrmAlgorithm[] }) => ({
|
||||
algorithms: data,
|
||||
...endpoint,
|
||||
}))
|
||||
.catch(() => ({
|
||||
algorithms: [] as RrmAlgorithm[],
|
||||
...endpoint,
|
||||
}));
|
||||
|
||||
const getAllAlgorithms = async (context: QueryFunctionContext<[string, string, EndpointApiResponse[]]>) => {
|
||||
const promises = context.queryKey[2].map((endpoint) => getRrmAlgorithms(endpoint));
|
||||
const providersWithAlgorithms: (EndpointApiResponse & { algorithms: RrmAlgorithm[] })[] = await Promise.all(promises);
|
||||
const promises2 = providersWithAlgorithms.map((provider) => getRrmProvider(provider));
|
||||
const completeProviders: RrmProviderCompleteInformation[] = await Promise.all(promises2);
|
||||
|
||||
return completeProviders;
|
||||
};
|
||||
|
||||
export const useGetAllRrmAlgorithms = ({ endpoints }: { endpoints: EndpointApiResponse[] }) =>
|
||||
useQuery(['rrm', 'providers', endpoints], getAllAlgorithms, {
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
export const useGetRrmProviders = () =>
|
||||
useQuery(
|
||||
['get-rrm-algorithms'],
|
||||
() => axiosRrm.get('algorithms').then(({ data }: { data: RrmAlgorithm[] }) => data),
|
||||
['rrm', 'providers'],
|
||||
() =>
|
||||
axiosProv
|
||||
.get('systemConfiguration?entries=rrm.providers')
|
||||
.then(({ data }: { data: { parameterName: string; parameterValue: string }[] }) => {
|
||||
const providers = data.find((entry) => entry.parameterName === 'rrm.providers');
|
||||
return providers?.parameterValue?.split(',').map((provider) => provider.trim()) ?? [];
|
||||
}),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
26
src/hooks/useRrm.ts
Normal file
26
src/hooks/useRrm.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import * as React from 'react';
|
||||
import { EndpointApiResponse, useGetEndpoints } from './Network/Endpoints';
|
||||
import { useGetAllRrmAlgorithms, useGetRrmProviders } from './Network/Rrm';
|
||||
|
||||
export const useRrm = () => {
|
||||
const getProviders = useGetRrmProviders();
|
||||
const getEndpoints = useGetEndpoints({ onSuccess: () => {} });
|
||||
|
||||
const providersWithEndpointInfo = React.useMemo(() => {
|
||||
if (getProviders.data && getEndpoints.data) {
|
||||
const providers = getProviders.data.map((provider) => {
|
||||
const endpoint = getEndpoints.data.find(({ type }) => type === provider);
|
||||
return endpoint ?? null;
|
||||
});
|
||||
return providers.filter((provider) => provider !== null) as EndpointApiResponse[];
|
||||
}
|
||||
return [];
|
||||
}, [getProviders.data, getEndpoints.data]);
|
||||
|
||||
const getCompleteProviders = useGetAllRrmAlgorithms({ endpoints: providersWithEndpointInfo ?? [] });
|
||||
|
||||
return {
|
||||
getProviders: getCompleteProviders,
|
||||
};
|
||||
};
|
||||
@@ -17,6 +17,7 @@ export type SortInfo = { id: string; sort: 'asc' | 'dsc' }[];
|
||||
export interface Column<T> {
|
||||
id: string;
|
||||
Header: string;
|
||||
stopPropagation?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
Footer?: string;
|
||||
accessor?: string;
|
||||
@@ -28,3 +29,11 @@ export interface Column<T> {
|
||||
isMonospace?: boolean;
|
||||
Cell?: ({ cell }: { cell: { row: { original: T } } }) => React.ReactElement | string | JSX.Element;
|
||||
}
|
||||
|
||||
export type TableOptions = {
|
||||
isLoading?: boolean;
|
||||
pageInfo?: PageInfo;
|
||||
setPageInfo?: (v: PageInfo) => void;
|
||||
minHeight?: number | string;
|
||||
ignoreColumns?: string[];
|
||||
};
|
||||
|
||||
@@ -32,7 +32,13 @@ const AddSubsectionModal = ({ editing, activeSubs, addSub }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton label={t('configurations.add_subsection')} onClick={onOpen} isDisabled={!editing} ml={2} />
|
||||
<CreateButton
|
||||
label={t('configurations.add_subsection')}
|
||||
onClick={onOpen}
|
||||
isDisabled={!editing}
|
||||
ml={2}
|
||||
isCompact
|
||||
/>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="sm" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Box,
|
||||
Heading,
|
||||
IconButton,
|
||||
Text,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
useBoolean,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Flask } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { uppercaseFirstLetter } from 'utils/stringHelper';
|
||||
|
||||
const configurationSections = ['globals', 'unit', 'metrics', 'services', 'radios', 'interfaces', 'third-party'];
|
||||
|
||||
const transformComputedConfigToEditable = (
|
||||
config: Record<string, unknown>,
|
||||
defaultConfiguration: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
description: string;
|
||||
weight: number;
|
||||
configuration: object;
|
||||
}
|
||||
>,
|
||||
) => {
|
||||
const configurations = [];
|
||||
|
||||
try {
|
||||
for (const [section, value] of Object.entries(config)) {
|
||||
if (configurationSections.includes(section)) {
|
||||
const configuration = {
|
||||
name: uppercaseFirstLetter(section),
|
||||
description: defaultConfiguration[section]?.description ?? '',
|
||||
weight: defaultConfiguration[section]?.weight ?? 1,
|
||||
configuration: {},
|
||||
};
|
||||
configuration.configuration[section] = value;
|
||||
configurations.push(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(configurations, null, 4);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
activeConfigurations: string[];
|
||||
defaultConfiguration: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
description: string;
|
||||
weight: number;
|
||||
configuration: object;
|
||||
}
|
||||
>;
|
||||
setConfig: (newConfig: object) => void;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
|
||||
const ExpertModeButton = ({ activeConfigurations, defaultConfiguration, setConfig, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [error, { on, off }] = useBoolean();
|
||||
const [tempValue, setTempValue] = React.useState('');
|
||||
|
||||
const saveValue = () => {
|
||||
try {
|
||||
const final = JSON.parse(transformComputedConfigToEditable(JSON.parse(tempValue), defaultConfiguration));
|
||||
|
||||
if (final) {
|
||||
const newVal = final.map((conf: { configuration: object }) => ({
|
||||
...conf,
|
||||
configuration: JSON.stringify(conf.configuration),
|
||||
}));
|
||||
setConfig(newVal);
|
||||
onClose();
|
||||
}
|
||||
} catch (e) {
|
||||
on();
|
||||
}
|
||||
};
|
||||
|
||||
const onTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setTempValue(e.target.value);
|
||||
try {
|
||||
const json = JSON.parse(e.target.value);
|
||||
const res = transformComputedConfigToEditable(json, defaultConfiguration);
|
||||
if (res) off();
|
||||
else on();
|
||||
} catch {
|
||||
on();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
const newConfig: Record<string, unknown> = {};
|
||||
for (const [section, value] of Object.entries(defaultConfiguration)) {
|
||||
if (activeConfigurations.includes(section)) {
|
||||
newConfig[section] = value.configuration;
|
||||
}
|
||||
}
|
||||
setTempValue(JSON.stringify(newConfig, null, 4));
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip label={t('configurations.expert_name')}>
|
||||
<IconButton
|
||||
ml={2}
|
||||
aria-label={t('configurations.expert_name')}
|
||||
colorScheme="purple"
|
||||
onClick={onOpen}
|
||||
icon={<Flask size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('configurations.expert_name')}
|
||||
topRightButtons={<SaveButton onClick={saveValue} isDisabled={tempValue.length === 0 || error} />}
|
||||
>
|
||||
<Box>
|
||||
<Alert my={2} colorScheme="red">
|
||||
<AlertIcon />
|
||||
<Text ml={2}>{t('configurations.import_warning')}</Text>
|
||||
</Alert>
|
||||
<Heading size="sm">{t('configurations.expert_name_explanation')}</Heading>
|
||||
<Textarea h="512px" value={tempValue} onChange={onTextAreaChange} mt={2} />
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpertModeButton;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { UploadSimple } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,24 +13,9 @@ const propTypes = {
|
||||
const ImportConfigurationButton = ({ setConfig, isDisabled }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
const getButton = () => {
|
||||
if (breakpoint !== 'base' && breakpoint !== 'sm') {
|
||||
return (
|
||||
<Button
|
||||
ml={2}
|
||||
colorScheme="blue"
|
||||
onClick={onOpen}
|
||||
rightIcon={<UploadSimple size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{t('configurations.import_file')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<Tooltip label={t('configurations.import_file')}>
|
||||
<IconButton
|
||||
ml={2}
|
||||
@@ -40,12 +25,6 @@ const ImportConfigurationButton = ({ setConfig, isDisabled }) => {
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getButton()}
|
||||
<ImportConfigurationModal isOpen={isOpen} onClose={onClose} setValue={setConfig} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Textarea,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { testInterfacesString } from 'constants/formTests';
|
||||
|
||||
const propTypes = {
|
||||
config: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
setConfig: PropTypes.func.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const InterfaceExpertForm = ({ config, setConfig, onClose, editing }) => {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState(JSON.stringify({ interfaces: config }, null, 2));
|
||||
|
||||
const onChange = (e) => setValue(e.target.value);
|
||||
const isInvalid = useMemo(() => !testInterfacesString(value), [value]);
|
||||
|
||||
const save = () => {
|
||||
setConfig(value);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} isOpen size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
<ModalHeader
|
||||
title={t('configurations.expert_name')}
|
||||
right={
|
||||
<>
|
||||
<SaveButton onClick={save} isDisabled={isInvalid || !editing} />
|
||||
<CloseButton ml={2} onClick={onClose} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
<FormControl isInvalid={isInvalid} isRequired isDisabled={!editing}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal" _disabled={{ opacity: 0.8 }}>
|
||||
{t('configurations.interfaces_instruction')}
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
borderRadius="15px"
|
||||
fontSize="sm"
|
||||
h="360px"
|
||||
type="text"
|
||||
_disabled={{ opacity: 0.8, cursor: 'not-allowed' }}
|
||||
/>
|
||||
<FormErrorMessage>{t('form.invalid_interfaces')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
InterfaceExpertForm.propTypes = propTypes;
|
||||
export default React.memo(InterfaceExpertForm, isEqual);
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useField } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import InterfaceExpertForm from './Form';
|
||||
import EditButton from 'components/Buttons/EditButton';
|
||||
|
||||
const propTypes = {
|
||||
editing: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const InterfaceExpertModal = ({ editing }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [{ value }, , { setValue }] = useField('configuration');
|
||||
|
||||
const setConfig = (v) => {
|
||||
setValue(JSON.parse(v).interfaces);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditButton label={t('configurations.expert_name')} onClick={onOpen} />
|
||||
{isOpen && <InterfaceExpertForm config={value} onClose={onClose} setConfig={setConfig} editing={editing} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InterfaceExpertModal.propTypes = propTypes;
|
||||
export default InterfaceExpertModal;
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useField } from 'formik';
|
||||
import { Box, Heading, SimpleGrid, Switch, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INTERFACE_IPV4_DHCP_SCHEMA } from '../../interfacesConstants';
|
||||
import DhcpLeaseIpV4 from './DhcpLeaseIpV4';
|
||||
import ConfigurationSubSectionToggle from 'components/CustomFields/ConfigurationSubSection';
|
||||
import { INTERFACE_IPV4_DHCP_LEASE_SCHEMA, INTERFACE_IPV4_DHCP_SCHEMA } from '../../interfacesConstants';
|
||||
import StaticLeaseOffsetField from './StaticLeaseOffsetField';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import ObjectArrayFieldModal from 'components/FormFields/ObjectArrayFieldModal';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
@@ -14,56 +16,189 @@ type Props = {
|
||||
|
||||
const DhcpIpV4 = ({ namePrefix, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [{ value }] = useField(`${namePrefix}.dhcp`);
|
||||
const { value, onChange } = useFastField({ name: `${namePrefix}.dhcp` });
|
||||
const { value: ipv4 } = useFastField<{ subnet?: string } | undefined>({ name: `${namePrefix}` });
|
||||
const { onChange: onLeaseChange } = useFastField({ name: `${namePrefix}.dhcp-lease` });
|
||||
|
||||
const isEnabled = useMemo(() => value !== undefined, [value]);
|
||||
const isEnabled = value !== undefined;
|
||||
|
||||
const defaultValue = useMemo(() => INTERFACE_IPV4_DHCP_SCHEMA(t, true).cast(), []);
|
||||
const fieldsToDestroy = useMemo(() => [`${namePrefix}.dhcp-lease`], []);
|
||||
const defaultValue = INTERFACE_IPV4_DHCP_SCHEMA(t, true).cast();
|
||||
|
||||
const onToggle = () => {
|
||||
if (isEnabled) {
|
||||
onChange(undefined);
|
||||
onLeaseChange(undefined);
|
||||
} else {
|
||||
onChange(defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
const leasesField = useMemo(
|
||||
() => (
|
||||
<SimpleGrid minChildWidth="260px" spacing={4}>
|
||||
<StringField
|
||||
name="macaddr"
|
||||
label="MAC Address"
|
||||
definitionKey="interface.ipv4.dhcp-lease.macaddr"
|
||||
isRequired
|
||||
w="220px"
|
||||
formatValue={(v) => {
|
||||
const r = /([a-f0-9]{2})([a-f0-9]{2})/i;
|
||||
let str = v.replace(/[^a-f0-9]/gi, '');
|
||||
|
||||
while (r.test(str)) {
|
||||
str = str.replace(r, `$1:$2`);
|
||||
}
|
||||
|
||||
str = str.slice(0, 17);
|
||||
|
||||
return str;
|
||||
}}
|
||||
mr={4}
|
||||
/>
|
||||
<StaticLeaseOffsetField subnet={ipv4?.subnet} />
|
||||
<Box mr={4} w="180px">
|
||||
<StringField
|
||||
name="lease-time"
|
||||
label="lease-time"
|
||||
definitionKey="interface.ipv4.dhcp-lease.lease-time"
|
||||
isRequired
|
||||
/>
|
||||
</Box>
|
||||
<ToggleField
|
||||
name="publish-hostname"
|
||||
label="publish-hostname"
|
||||
definitionKey="interface.ipv4.dhcp-lease.publish-hostname"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
),
|
||||
[ipv4],
|
||||
);
|
||||
const leasesCols = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'macaddr',
|
||||
Header: 'macaddr',
|
||||
Footer: '',
|
||||
accessor: 'macaddr',
|
||||
customWidth: '100px',
|
||||
},
|
||||
{
|
||||
id: 'static-lease-offset',
|
||||
Header: 'static-lease-offset',
|
||||
Footer: '',
|
||||
accessor: 'static-lease-offset',
|
||||
customWidth: '100px',
|
||||
},
|
||||
{
|
||||
id: 'lease-time',
|
||||
Header: 'lease-time',
|
||||
Footer: '',
|
||||
accessor: 'lease-time',
|
||||
customWidth: '100px',
|
||||
},
|
||||
{
|
||||
id: 'publish-hostname',
|
||||
Header: 'publish-hostname',
|
||||
Footer: '',
|
||||
accessor: 'publish-hostname',
|
||||
customWidth: '100px',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ConfigurationSubSectionToggle
|
||||
name={`${namePrefix}.dhcp`}
|
||||
label="Enabled DHCP"
|
||||
fieldsToDestroy={fieldsToDestroy}
|
||||
defaultValue={defaultValue}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<Box mt={4}>
|
||||
<Heading size="md" display="flex">
|
||||
<Text pt={1}>DHCPv4</Text>
|
||||
<Switch
|
||||
pt={1}
|
||||
onChange={onToggle}
|
||||
isChecked={isEnabled}
|
||||
borderRadius="15px"
|
||||
size="lg"
|
||||
mx={2}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
{isEnabled ? (
|
||||
<Box>
|
||||
<ObjectArrayFieldModal
|
||||
name={`${namePrefix}.dhcp-leases`}
|
||||
label="Reserved Addresses"
|
||||
fields={leasesField}
|
||||
columns={leasesCols}
|
||||
schema={INTERFACE_IPV4_DHCP_LEASE_SCHEMA}
|
||||
isDisabled={isDisabled}
|
||||
hideLabel
|
||||
emptyIsUndefined
|
||||
isRequired
|
||||
options={{
|
||||
modalTitle: 'Reserved Addresses',
|
||||
buttonLabel: 'Manage Reserved Addresses',
|
||||
onFormSubmit: (v: {
|
||||
__temp_ip?: string;
|
||||
secondMacAddress: string;
|
||||
'lease-time': string;
|
||||
'publish-hostname': boolean;
|
||||
}) => {
|
||||
const newObj = v;
|
||||
// Delete temp ip from the object
|
||||
delete newObj.__temp_ip;
|
||||
|
||||
return newObj;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</Heading>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mt={2} w="100%">
|
||||
<NumberField
|
||||
name={`${namePrefix}.dhcp.lease-first`}
|
||||
label="dhcp.lease-first"
|
||||
label="lease-first"
|
||||
definitionKey="interface.ipv4.dhcp.lease-first"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<NumberField
|
||||
name={`${namePrefix}.dhcp.lease-count`}
|
||||
label="dhcp.lease-count"
|
||||
label="lease-count"
|
||||
definitionKey="interface.ipv4.dhcp.lease-count"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.dhcp.lease-time`}
|
||||
label="dhcp.lease-time"
|
||||
label="lease-time"
|
||||
definitionKey="interface.ipv4.dhcp.lease-time"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.dhcp.relay-server`}
|
||||
label="dhcp.relay-server"
|
||||
label="relay-server"
|
||||
definitionKey="interface.ipv4.dhcp.relay-server"
|
||||
isDisabled={isDisabled}
|
||||
emptyIsUndefined
|
||||
/>
|
||||
<DhcpLeaseIpV4 namePrefix={namePrefix} isDisabled={isDisabled} />
|
||||
</>
|
||||
<StringField
|
||||
name={`${namePrefix}.circuit-id-format`}
|
||||
label="circuit-id-format"
|
||||
definitionKey="interface.ipv4.dhcp.circuit-id-format"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.dhcp.remote-id-format`}
|
||||
label="remote-id-format"
|
||||
definitionKey="interface.ipv4.dhcp.remote-id-format"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useField } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INTERFACE_IPV4_DHCP_LEASE_SCHEMA } from '../../interfacesConstants';
|
||||
import ConfigurationSubSectionToggle from 'components/CustomFields/ConfigurationSubSection';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
namePrefix: string;
|
||||
};
|
||||
|
||||
const DhcpLeaseIpV4 = ({ namePrefix, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [{ value }] = useField(`${namePrefix}.dhcp-lease`);
|
||||
|
||||
const isEnabled = useMemo(() => value !== undefined, [value]);
|
||||
|
||||
const defaultValue = useMemo(() => INTERFACE_IPV4_DHCP_LEASE_SCHEMA(t, true).cast(), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigurationSubSectionToggle
|
||||
name={`${namePrefix}.dhcp-lease`}
|
||||
label="Enable DHCP-LEASE"
|
||||
defaultValue={defaultValue}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<StringField
|
||||
name={`${namePrefix}.dhcp-lease.macaddr`}
|
||||
label="dhcp-lease.macaddr"
|
||||
definitionKey="interface.ipv4.dhcp-lease.macaddr"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.dhcp-lease.lease-time`}
|
||||
label="dhcp-lease.lease-time"
|
||||
definitionKey="interface.ipv4.dhcp-lease.lease-time"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<NumberField
|
||||
name={`${namePrefix}.dhcp-lease.static-lease-offset`}
|
||||
label="dhcp-lease.static-lease-offset"
|
||||
definitionKey="interface.ipv4.dhcp-lease.static-lease-offset"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<ToggleField
|
||||
name={`${namePrefix}.dhcp-lease.publish-hostname`}
|
||||
label="dhcp-lease.publish-hostname"
|
||||
definitionKey="interface.ipv4.dhcp-lease.publish-hostname"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DhcpLeaseIpV4);
|
||||
@@ -4,7 +4,7 @@ import { INTERFACE_IPV4_PORT_FORWARD_SCHEMA, INTERFACE_IPV4_SCHEMA } from '../..
|
||||
import LockedIpv4 from './LockedIpv4';
|
||||
import StaticIpV4 from './StaticIpV4';
|
||||
import ConfigurationResourcePicker from 'components/CustomFields/ConfigurationResourcePicker';
|
||||
import ObjectArrayFieldModal, { ObjectArrayFieldModalOptions } from 'components/FormFields/ObjectArrayFieldModal';
|
||||
import ObjectArrayFieldModal from 'components/FormFields/ObjectArrayFieldModal';
|
||||
import { PortRangeField } from 'components/FormFields/PortRangeField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
@@ -79,7 +79,7 @@ const IpV4Form = ({ isEnabled, isDisabled, namePrefix, ipv4, role, onToggle, onC
|
||||
],
|
||||
[],
|
||||
);
|
||||
const portOpts: ObjectArrayFieldModalOptions = useMemo(
|
||||
const portOpts = useMemo(
|
||||
() => ({
|
||||
buttonLabel: 'IPv4 Port Forwarding',
|
||||
modalTitle: 'IPv4 Port Forwarding',
|
||||
@@ -139,9 +139,7 @@ const IpV4Form = ({ isEnabled, isDisabled, namePrefix, ipv4, role, onToggle, onC
|
||||
{variableBlockId ? (
|
||||
<LockedIpv4 variableBlockId={variableBlockId} />
|
||||
) : (
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={ipv4 === 'static' ? 8 : undefined} mt={2} w="100%">
|
||||
<StaticIpV4 namePrefix={namePrefix} isEnabled={ipv4 === 'static'} isDisabled={isDisabled} />
|
||||
</SimpleGrid>
|
||||
<StaticIpV4 namePrefix={namePrefix} isEnabled={ipv4 === 'static'} isDisabled={isDisabled} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { SimpleGrid } from '@chakra-ui/react';
|
||||
import DhcpIpV4 from './DhcpIpV4';
|
||||
import CreatableSelectField from 'components/FormFields/CreatableSelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
@@ -15,31 +16,35 @@ const IpV4 = ({ isDisabled, namePrefix, isEnabled }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<StringField
|
||||
name={`${namePrefix}.subnet`}
|
||||
label="subnet"
|
||||
definitionKey="interface.ipv4.subnet"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.gateway`}
|
||||
label="gateway"
|
||||
definitionKey="interface.ipv4.gateway"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<ToggleField
|
||||
name={`${namePrefix}.send-hostname`}
|
||||
label="send-hostname"
|
||||
definitionKey="interface.ipv4.send-hostname"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<CreatableSelectField
|
||||
name={`${namePrefix}.use-dns`}
|
||||
label="use-dns"
|
||||
definitionKey="interface.ipv4.use-dns"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mt={2} w="100%">
|
||||
<StringField
|
||||
name={`${namePrefix}.subnet`}
|
||||
label="subnet"
|
||||
definitionKey="interface.ipv4.subnet"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.gateway`}
|
||||
label="gateway"
|
||||
definitionKey="interface.ipv4.gateway"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<ToggleField
|
||||
name={`${namePrefix}.send-hostname`}
|
||||
label="send-hostname"
|
||||
definitionKey="interface.ipv4.send-hostname"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<CreatableSelectField
|
||||
name={`${namePrefix}.use-dns`}
|
||||
label="use-dns"
|
||||
definitionKey="interface.ipv4.use-dns"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<DhcpIpV4 namePrefix={namePrefix} isDisabled={isDisabled} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
import { FormControl, FormErrorMessage, FormLabel, Input } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import { testIpv4 } from 'constants/formTests';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
subnet?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const StaticLeaseOffsetField = ({ subnet, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { onChange: onOffsetChange } = useFastField<number>({ name: 'static-lease-offset' });
|
||||
const { value, onChange, onBlur } = useFastField<string>({ name: '__temp_ip' });
|
||||
|
||||
const isSubnetValid = testIpv4(subnet);
|
||||
const onIpChange = (v: string) => {
|
||||
onChange(v);
|
||||
|
||||
if (testIpv4(v)) {
|
||||
const ipEnding = v.split('.').pop()?.split('/')[0];
|
||||
const newOffset = Number(ipEnding) - Number(subnet?.split('.').pop()?.split('/')[0]);
|
||||
|
||||
if (newOffset > 0) {
|
||||
onOffsetChange(newOffset);
|
||||
} else {
|
||||
onOffsetChange(-newOffset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!value && subnet && isSubnetValid) {
|
||||
onIpChange(`${subnet?.split('.').slice(0, 3).join('.')}.${Number(subnet.split('.').pop()?.split('/')[0]) + 1}`);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
if (!subnet || !isSubnetValid) {
|
||||
return (
|
||||
<NumberField
|
||||
name="static-lease-offset"
|
||||
label="dhcp-lease.static-lease-offset"
|
||||
definitionKey="interface.ipv4.dhcp-lease.static-lease-offset"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
w="200px"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const currOffset = value
|
||||
? Number(value.split('.').pop()?.split('/')[0]) - Number(subnet.split('.').pop()?.split('/')[0])
|
||||
: NaN;
|
||||
const isIpValid = testIpv4(value);
|
||||
|
||||
return (
|
||||
<FormControl isRequired isDisabled={isDisabled} isInvalid={!testIpv4(value) || currOffset <= 0}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal" _disabled={{ opacity: 0.8 }}>
|
||||
IP Address
|
||||
</FormLabel>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onIpChange(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
borderRadius="15px"
|
||||
fontSize="sm"
|
||||
autoComplete="off"
|
||||
border="2px solid"
|
||||
_disabled={{ opacity: 0.8, cursor: 'not-allowed' }}
|
||||
w="160px"
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{isIpValid && currOffset <= 0
|
||||
? `Offset${Number.isNaN(currOffset) ? '' : ` (${currOffset})`} with subnet needs to be bigger than 0`
|
||||
: t('form.invalid_ipv4')}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(StaticLeaseOffsetField);
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormControl, FormLabel, Switch } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Heading, SimpleGrid, Switch, Text } from '@chakra-ui/react';
|
||||
import { getIn, useFormikContext } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INTERFACE_IPV6_DHCP_SCHEMA } from '../../interfacesConstants';
|
||||
@@ -7,15 +7,7 @@ import CreatableSelectField from 'components/FormFields/CreatableSelectField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
|
||||
const DhcpIpV6 = (
|
||||
{
|
||||
editing,
|
||||
index
|
||||
}: {
|
||||
editing: boolean
|
||||
index: number
|
||||
}
|
||||
) => {
|
||||
const DhcpIpV6: React.FC<{ editing: boolean; index: number }> = ({ editing, index }) => {
|
||||
const { t } = useTranslation();
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
|
||||
@@ -27,24 +19,27 @@ const DhcpIpV6 = (
|
||||
}
|
||||
};
|
||||
|
||||
const isEnabled = useMemo(
|
||||
() => getIn(values, `configuration[${index}].ipv6.dhcpv6`) !== undefined,
|
||||
[getIn(values, `configuration[${index}].ipv6.dhcpv6`)],
|
||||
);
|
||||
const isEnabled = !!getIn(values, `configuration[${index}].ipv6.dhcpv6`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl isDisabled={!editing}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
Enable DHCPv6
|
||||
</FormLabel>
|
||||
<Switch onChange={onEnabledChange} isChecked={isEnabled} borderRadius="15px" size="lg" isDisabled={!editing} />
|
||||
</FormControl>
|
||||
<Heading size="md" display="flex">
|
||||
<Text pt={1}>DHCPv6</Text>
|
||||
<Switch
|
||||
pt={1}
|
||||
onChange={onEnabledChange}
|
||||
isChecked={isEnabled}
|
||||
borderRadius="15px"
|
||||
size="lg"
|
||||
mx={2}
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
</Heading>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={2} mt={2} w="100%">
|
||||
<SelectField
|
||||
name={`configuration[${index}].ipv6.dhcpv6.mode`}
|
||||
label="dhcpv6.mode"
|
||||
label="mode"
|
||||
definitionKey="interface.ipv6.dhcpv6.mode"
|
||||
options={[
|
||||
{ value: 'hybrid', label: 'hybrid' },
|
||||
@@ -57,18 +52,18 @@ const DhcpIpV6 = (
|
||||
/>
|
||||
<CreatableSelectField
|
||||
name={`configuration[${index}].ipv6.dhcpv6.announce-dns`}
|
||||
label="dhcpv6.announce-dns"
|
||||
label="announce-dns"
|
||||
definitionKey="interface.ipv6.dhcpv6.announce-dns"
|
||||
emptyIsUndefined
|
||||
/>
|
||||
<StringField
|
||||
name={`configuration[${index}].ipv6.dhcpv6.filter-prefix`}
|
||||
label="dhcpv6.filter-prefix"
|
||||
label="filter-prefix"
|
||||
definitionKey="interface.ipv6.dhcpv6.filter-prefix"
|
||||
isDisabled={!editing}
|
||||
emptyIsUndefined
|
||||
/>
|
||||
</>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,29 +5,20 @@ import { INTERFACE_IPV6_PORT_FORWARD_SCHEMA, INTERFACE_IPV6_TRAFFIC_ALLOW_SCHEMA
|
||||
import DhcpIpV6 from './DhcpIpV6';
|
||||
import CreatableSelectField from 'components/FormFields/CreatableSelectField';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import ObjectArrayFieldModal, { ObjectArrayFieldModalOptions } from 'components/FormFields/ObjectArrayFieldModal';
|
||||
import ObjectArrayFieldModal from 'components/FormFields/ObjectArrayFieldModal';
|
||||
import { PortRangeField } from 'components/FormFields/PortRangeField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ArrayCell from 'components/TableCells/ArrayCell';
|
||||
|
||||
const IpV6 = (
|
||||
{
|
||||
editing,
|
||||
index,
|
||||
ipv6,
|
||||
role,
|
||||
onToggle,
|
||||
onChange
|
||||
}: {
|
||||
editing: boolean
|
||||
index: number
|
||||
ipv6: string
|
||||
role: string
|
||||
onToggle: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
) => {
|
||||
const IpV6: React.FC<{
|
||||
editing: boolean;
|
||||
index: number;
|
||||
ipv6: string;
|
||||
role: string;
|
||||
onToggle: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
}> = ({ editing, index, ipv6, role, onToggle, onChange }) => {
|
||||
const arrCell = useCallback((cell, key) => <ArrayCell arr={cell.row.values[key]} key={uuid()} />, []);
|
||||
|
||||
const portFields = useMemo(
|
||||
@@ -89,7 +80,7 @@ const IpV6 = (
|
||||
],
|
||||
[],
|
||||
);
|
||||
const portOpts: ObjectArrayFieldModalOptions = useMemo(
|
||||
const portOpts = useMemo(
|
||||
() => ({
|
||||
buttonLabel: 'IPv6 Port Forwarding',
|
||||
modalTitle: 'IPv6 Port Forwarding',
|
||||
@@ -177,7 +168,7 @@ const IpV6 = (
|
||||
[],
|
||||
);
|
||||
|
||||
const trafficOpts: ObjectArrayFieldModalOptions = useMemo(
|
||||
const trafficOpts = useMemo(
|
||||
() => ({
|
||||
buttonLabel: 'IPv6 Traffic management',
|
||||
modalTitle: 'IPv6 Traffic management',
|
||||
@@ -187,7 +178,7 @@ const IpV6 = (
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading size="md" display="flex">
|
||||
<Heading size="md" display="flex" mt={2}>
|
||||
<Text pt={1}>IpV6</Text>
|
||||
<Switch
|
||||
onChange={onToggle}
|
||||
@@ -234,9 +225,9 @@ const IpV6 = (
|
||||
</Flex>
|
||||
)}
|
||||
</Heading>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={2} mt={2} w="100%">
|
||||
{ipv6 === 'static' && (
|
||||
<>
|
||||
{ipv6 === 'static' && (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={2} mt={2} w="100%">
|
||||
<StringField
|
||||
name={`configuration[${index}].ipv6.subnet`}
|
||||
label="subnet"
|
||||
@@ -257,10 +248,11 @@ const IpV6 = (
|
||||
definitionKey="interface.ipv6.prefix-size"
|
||||
acceptEmptyValue
|
||||
/>
|
||||
<DhcpIpV6 editing={editing} index={index} />
|
||||
</>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</SimpleGrid>
|
||||
|
||||
<DhcpIpV6 editing={editing} index={index} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -74,7 +74,6 @@ const AdvancedSettings: React.FC<{ editing: boolean; namePrefix: string }> = ({
|
||||
definitionKey="interface.ssid.services"
|
||||
isDisabled={!editing}
|
||||
options={[
|
||||
{ value: 'captive', label: 'captive' },
|
||||
{ value: 'radius-gw-proxy', label: 'radius-gw-proxy' },
|
||||
{ value: 'wifi-steering', label: 'wifi-steering' },
|
||||
]}
|
||||
|
||||
@@ -59,7 +59,7 @@ const SingleSsid = ({ editing, index, namePrefix, remove }) => {
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mt={2}>
|
||||
<StringField
|
||||
name={`${namePrefix}.name`}
|
||||
label="name"
|
||||
label="SSID"
|
||||
definitionKey="interface.ssid.name"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import InternalFormAccess from '../common/InternalFormAccess';
|
||||
import SectionGeneralCard from '../common/SectionGeneralCard';
|
||||
import InterfaceExpertModal from './InterfaceExpertModal';
|
||||
import Interfaces from './Interfaces';
|
||||
import { INTERFACES_SCHEMA } from './interfacesConstants';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
@@ -91,12 +90,7 @@ const InterfaceSection = ({ editing, setSection, sectionInformation, removeSub }
|
||||
<VStack spacing={4}>
|
||||
<SectionGeneralCard
|
||||
editing={editing}
|
||||
buttons={
|
||||
<>
|
||||
<InterfaceExpertModal editing={editing} />
|
||||
<DeleteButton ml={2} onClick={removeUnit} isDisabled={!editing} />
|
||||
</>
|
||||
}
|
||||
buttons={<DeleteButton ml={2} onClick={removeUnit} isDisabled={!editing} />}
|
||||
/>
|
||||
<FieldArray name="configuration">
|
||||
{(arrayHelpers) => (
|
||||
|
||||
@@ -466,18 +466,20 @@ export const INTERFACE_IPV4_DHCP_SCHEMA = (t, useDefault = false) => {
|
||||
export const INTERFACE_IPV4_DHCP_LEASE_SCHEMA = (t, useDefault = false) => {
|
||||
const shape = object()
|
||||
.shape({
|
||||
macaddr: string().required(t('form.required')).default(''),
|
||||
macaddr: string()
|
||||
.required(t('form.required'))
|
||||
.test('test-first-mac', t('form.invalid_mac_uc'), testUcMac)
|
||||
.default('00:11:22:33:44:55'),
|
||||
'static-lease-offset': number().required(t('form.required')).positive().integer().default(undefined),
|
||||
'lease-time': string()
|
||||
.required(t('form.required'))
|
||||
.test('ipv4_dhcp-lease.lease-time', t('form.invalid_lease_time'), testLeaseTime)
|
||||
.default('6h'),
|
||||
'static-lease-offset': number().required(t('form.required')).positive().integer().default(1),
|
||||
'publish-hostname': bool().required(t('form.required')).default(true),
|
||||
})
|
||||
.default({
|
||||
macaddr: '',
|
||||
macaddr: '00:11:22:33:44:55',
|
||||
'lease-time': '6h',
|
||||
'static-lease-offset': 1,
|
||||
'publish-hostname': true,
|
||||
});
|
||||
|
||||
@@ -535,17 +537,30 @@ export const INTERFACE_IPV4_SCHEMA = (t, useDefault = false) => {
|
||||
addressing: string().required(t('form.required')).default('dynamic'),
|
||||
subnet: string().when('addressing', {
|
||||
is: 'dynamic',
|
||||
then: string().nullable(),
|
||||
then: string(),
|
||||
otherwise: string()
|
||||
.test('test-ipv4-subnet', t('form.invalid_ipv4'), testIpv4)
|
||||
.test('test-ipv4-subnet-static-d', t('form.invalid_static_ipv4_d'), testStaticIpv4ClassD)
|
||||
.test('test-ipv4-subnet-static-e', t('form.invalid_static_ipv4_e'), testStaticIpv4ClassE)
|
||||
.default(''),
|
||||
.test('test-ipv4-subnet', t('form.invalid_ipv4'), (v) => {
|
||||
if (v === 'auto/24') return true;
|
||||
return testIpv4(v);
|
||||
})
|
||||
.test('test-ipv4-subnet-static-d', t('form.invalid_static_ipv4_d'), (v) => {
|
||||
if (v === 'auto/24') return true;
|
||||
return testStaticIpv4ClassD(v);
|
||||
})
|
||||
.test('test-ipv4-subnet-static-e', t('form.invalid_static_ipv4_e'), (v) => {
|
||||
if (v === 'auto/24') return true;
|
||||
return testStaticIpv4ClassE(v);
|
||||
})
|
||||
.required(t('form.required'))
|
||||
.default('192.168.1.1/24'),
|
||||
}),
|
||||
gateway: string().when('addressing', {
|
||||
is: 'dynamic',
|
||||
then: string().nullable(),
|
||||
otherwise: string().default(''),
|
||||
otherwise: string()
|
||||
.test('test-ipv4-subnet', t('form.invalid_ipv4'), testIpv4)
|
||||
.required(t('form.required'))
|
||||
.default('192.168.1.1'),
|
||||
}),
|
||||
'send-hostname': bool().when('addressing', {
|
||||
is: 'dynamic',
|
||||
@@ -563,7 +578,6 @@ export const INTERFACE_IPV4_SCHEMA = (t, useDefault = false) => {
|
||||
otherwise: array().of(object()).default([]),
|
||||
}),
|
||||
dhcp: INTERFACE_IPV4_DHCP_SCHEMA(t, useDefault),
|
||||
'dhcp-lease': INTERFACE_IPV4_DHCP_LEASE_SCHEMA(t, useDefault),
|
||||
});
|
||||
|
||||
return useDefault ? shape : shape.nullable().default(undefined);
|
||||
|
||||
@@ -31,10 +31,28 @@ const Health = ({ editing }: Props) => {
|
||||
isRequired
|
||||
w={24}
|
||||
/>
|
||||
<ToggleField name="configuration.health.dhcp-local" label="dhcp-local" isRequired defaultValue />
|
||||
<ToggleField name="configuration.health.dhcp-remote" label="dhcp-remote" isRequired />
|
||||
<ToggleField name="configuration.health.dns-local" label="dns-local" isRequired defaultValue />
|
||||
<ToggleField name="configuration.health.dns-remote" label="dns-remote" isRequired defaultValue />
|
||||
<ToggleField
|
||||
name="configuration.health.dhcp-local"
|
||||
label="dhcp-local"
|
||||
isRequired
|
||||
defaultValue
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
<ToggleField name="configuration.health.dhcp-remote" label="dhcp-remote" isRequired isDisabled={!editing} />
|
||||
<ToggleField
|
||||
name="configuration.health.dns-local"
|
||||
label="dns-local"
|
||||
isRequired
|
||||
defaultValue
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
<ToggleField
|
||||
name="configuration.health.dns-remote"
|
||||
label="dns-remote"
|
||||
isRequired
|
||||
defaultValue
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import { EVENT_TYPES_OPTIONS } from './metricsConstants';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import MultiSelectField from 'components/FormFields/MultiSelectField';
|
||||
|
||||
type Props = {
|
||||
editing: boolean;
|
||||
};
|
||||
|
||||
const Realtime = ({ editing }: Props) => (
|
||||
<Card variant="widget" mb={4}>
|
||||
<CardHeader>
|
||||
<Heading size="md" borderBottom="1px solid">
|
||||
Realtime
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={8} mt={2} w="100%">
|
||||
<MultiSelectField
|
||||
name="configuration.realtime.types"
|
||||
label="types"
|
||||
definitionKey="metrics.realtime.types"
|
||||
options={EVENT_TYPES_OPTIONS}
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default React.memo(Realtime);
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import { EVENT_TYPES_OPTIONS } from './metricsConstants';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import MultiSelectField from 'components/FormFields/MultiSelectField';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
|
||||
type Props = {
|
||||
editing: boolean;
|
||||
};
|
||||
|
||||
const Telemetry = ({ editing }: Props) => (
|
||||
<Card variant="widget" mb={4}>
|
||||
<CardHeader>
|
||||
<Heading size="md" borderBottom="1px solid">
|
||||
Telemetry
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={8} mt={2} w="100%">
|
||||
<NumberField
|
||||
name="configuration.telemetry.interval"
|
||||
label="interval"
|
||||
definitionKey="metrics.telemetry.interval"
|
||||
unit="s"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
w={24}
|
||||
/>
|
||||
<MultiSelectField
|
||||
name="configuration.telemetry.types"
|
||||
label="types"
|
||||
definitionKey="metrics.telemetry.types"
|
||||
options={EVENT_TYPES_OPTIONS}
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default React.memo(Telemetry);
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
|
||||
type Props = {
|
||||
editing: boolean;
|
||||
};
|
||||
|
||||
const WifiScan = ({ editing }: Props) => (
|
||||
<Card variant="widget" mb={4}>
|
||||
<CardHeader>
|
||||
<Heading size="md" borderBottom="1px solid">
|
||||
WiFi Scan
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mb={8} mt={2} w="100%">
|
||||
<NumberField
|
||||
name="configuration.wifi-scan.interval"
|
||||
label="interval"
|
||||
definitionKey="metrics.wifi-scan.interval"
|
||||
unit="s"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
w={24}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default React.memo(WifiScan);
|
||||
@@ -11,8 +11,11 @@ import SubSectionPicker from '../common/SubSectionPicker';
|
||||
import DhcpSnooping from './DhcpSnooping';
|
||||
import Health from './Health';
|
||||
import { getSubSectionDefaults, METRICS_SCHEMA } from './metricsConstants';
|
||||
import Realtime from './Realtime';
|
||||
import Statistics from './Statistics';
|
||||
import Telemetry from './Telemetry';
|
||||
import WifiFrames from './WifiFrames';
|
||||
import WifiScan from './WifiScan';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
import { ConfigurationSectionShape } from 'constants/propShapes';
|
||||
|
||||
@@ -108,15 +111,26 @@ const MetricsSection = ({ editing, setSection, sectionInformation, removeSub })
|
||||
subsectionPicker={
|
||||
<SubSectionPicker
|
||||
editing={editing}
|
||||
subsections={['statistics', 'health', 'wifi-frames', 'dhcp-snooping']}
|
||||
subsections={[
|
||||
'dhcp-snooping',
|
||||
'health',
|
||||
'realtime',
|
||||
'statistics',
|
||||
'telemetry',
|
||||
'wifi-frames',
|
||||
'wifi-scan',
|
||||
]}
|
||||
onSubsectionsChange={(sub) => onSubsectionsChange(sub, setFieldValue)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{isSubSectionActive('statistics') && <Statistics editing={editing} />}
|
||||
{isSubSectionActive('health') && <Health editing={editing} />}
|
||||
{isSubSectionActive('wifi-frames') && <WifiFrames editing={editing} />}
|
||||
{isSubSectionActive('dhcp-snooping') && <DhcpSnooping editing={editing} />}
|
||||
{isSubSectionActive('health') && <Health editing={editing} />}
|
||||
{isSubSectionActive('realtime') && <Realtime editing={editing} />}
|
||||
{isSubSectionActive('statistics') && <Statistics editing={editing} />}
|
||||
{isSubSectionActive('telemetry') && <Telemetry editing={editing} />}
|
||||
{isSubSectionActive('wifi-frames') && <WifiFrames editing={editing} />}
|
||||
{isSubSectionActive('wifi-scan') && <WifiScan editing={editing} />}
|
||||
</Masonry>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,69 @@
|
||||
import { object, number, string, array, bool } from 'yup';
|
||||
|
||||
const EVENT_TYPES = [
|
||||
'ssh',
|
||||
'health',
|
||||
'health.dns',
|
||||
'health.dhcp',
|
||||
'health.radius',
|
||||
'health.memory',
|
||||
'client',
|
||||
'client.join',
|
||||
'client.leave',
|
||||
'client.key-mismatch',
|
||||
'wifi',
|
||||
'wifi.start',
|
||||
'wifi.stop',
|
||||
'wired',
|
||||
'wired.carrier-up',
|
||||
'wired.carrier-down',
|
||||
'unit',
|
||||
'unit-boot-up',
|
||||
];
|
||||
|
||||
export const EVENT_TYPES_OPTIONS = EVENT_TYPES.map((type) => ({
|
||||
label: type,
|
||||
value: type,
|
||||
}));
|
||||
|
||||
export const METRICS_WIFISCAN_SCHEMA = (t, useDefault = false) =>
|
||||
useDefault
|
||||
? object().shape({
|
||||
interval: number().required(t('form.required')).moreThan(59).lessThan(1000).default(60),
|
||||
})
|
||||
: object()
|
||||
.shape({
|
||||
interval: number().required(t('form.required')).moreThan(59).lessThan(1000).default(60),
|
||||
})
|
||||
.nullable()
|
||||
.default(undefined);
|
||||
|
||||
export const METRICS_REALTIME_SCHEMA = (t, useDefault = false) =>
|
||||
useDefault
|
||||
? object().shape({
|
||||
types: array().of(string()).required(t('form.required')).min(1, t('form.required')).default([]),
|
||||
})
|
||||
: object()
|
||||
.shape({
|
||||
types: array().of(string()).required(t('form.required')).min(1, t('form.required')).default([]),
|
||||
})
|
||||
.nullable()
|
||||
.default(undefined);
|
||||
|
||||
export const METRICS_TELEMETRY_SCHEMA = (t, useDefault = false) =>
|
||||
useDefault
|
||||
? object().shape({
|
||||
interval: number().required(t('form.required')).moreThan(59).lessThan(1000).default(60),
|
||||
types: array().of(string()).required(t('form.required')).min(1, t('form.required')).default([]),
|
||||
})
|
||||
: object()
|
||||
.shape({
|
||||
interval: number().required(t('form.required')).moreThan(59).lessThan(1000).default(60),
|
||||
types: array().of(string()).required(t('form.required')).min(1, t('form.required')).default([]),
|
||||
})
|
||||
.nullable()
|
||||
.default(undefined);
|
||||
|
||||
export const METRICS_STATISTICS_SCHEMA = (t, useDefault = false) =>
|
||||
useDefault
|
||||
? object().shape({
|
||||
@@ -68,6 +132,9 @@ export const METRICS_SCHEMA = (t, useDefault = false) =>
|
||||
health: METRICS_HEALTH_SCHEMA(t, useDefault),
|
||||
'wifi-frames': METRICS_WIFI_FRAMES_SCHEMA(t, useDefault),
|
||||
'dhcp-snooping': METRICS_DHCP_SNOOPING_SCHEMA(t, useDefault),
|
||||
realtime: METRICS_REALTIME_SCHEMA(t, useDefault),
|
||||
telemetry: METRICS_TELEMETRY_SCHEMA(t, useDefault),
|
||||
'wifi-scan': METRICS_WIFISCAN_SCHEMA(t, useDefault),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -81,6 +148,12 @@ export const getSubSectionDefaults = (t, sub) => {
|
||||
return METRICS_WIFI_FRAMES_SCHEMA(t, true).cast();
|
||||
case 'dhcp-snooping':
|
||||
return METRICS_DHCP_SNOOPING_SCHEMA(t, true).cast();
|
||||
case 'telemetry':
|
||||
return METRICS_TELEMETRY_SCHEMA(t, true).cast();
|
||||
case 'realtime':
|
||||
return METRICS_REALTIME_SCHEMA(t, true).cast();
|
||||
case 'wifi-scan':
|
||||
return METRICS_WIFISCAN_SCHEMA(t, true).cast();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ const CaptiveConfiguration = ({ editing }: { editing: boolean }) => {
|
||||
</Select>
|
||||
</Box>
|
||||
</CardHeader>
|
||||
<CardBody pb={8} pt={2} display="block">
|
||||
<CardBody pb={2} pt={2} display="block">
|
||||
{
|
||||
// Basic Fields
|
||||
}
|
||||
@@ -120,6 +120,11 @@ const CaptiveConfiguration = ({ editing }: { editing: boolean }) => {
|
||||
emptyIsUndefined={mode !== 'uam'}
|
||||
isRequired={mode === 'uam'}
|
||||
/>
|
||||
<CreatableSelectField
|
||||
{...fieldProps('walled-garden-ipaddr')}
|
||||
placeholder="Example: 192.168.0.1"
|
||||
emptyIsUndefined
|
||||
/>
|
||||
<FileInputFieldModal
|
||||
{...fieldProps('web-root')}
|
||||
fileName="configuration.captive.web-root-filename"
|
||||
|
||||
@@ -11,6 +11,7 @@ export const SERVICES_CAPTIVE_SCHEMA = (t, useDefault = false) => {
|
||||
then: array().of(string()).min(1, t('form.required')),
|
||||
})
|
||||
.default(undefined),
|
||||
'walled-garden-ipaddr': array().of(string()).default(undefined),
|
||||
'web-root': string().default(undefined),
|
||||
'idle-timeout': number().required(t('form.required')).positive().lessThan(65535).integer().default(600),
|
||||
'session-timeout': number().positive().lessThan(65535).integer().default(undefined),
|
||||
@@ -303,11 +304,7 @@ export const SERVICES_IEEE8021X_SCHEMA = (t, useDefault = false) => {
|
||||
};
|
||||
export const SERVICES_RADIUS_PROXY_SCHEMA = (t, useDefault = false) => {
|
||||
const shape = object().shape({
|
||||
realms: array()
|
||||
.of(SERVICES_REALMS_SCHEMA(t, useDefault))
|
||||
.required(t('form.required'))
|
||||
.min(1, t('form.required'))
|
||||
.default([]),
|
||||
realms: array().of(SERVICES_REALMS_SCHEMA(t, useDefault)).required(t('form.required')).default([]),
|
||||
});
|
||||
|
||||
return useDefault ? shape : shape.nullable().default(undefined);
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { CheckCircle, WarningOctagon } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
@@ -30,7 +20,6 @@ const defaultProps = {
|
||||
const ViewConfigErrorsModal = ({ errors, activeConfigurations, isDisabled }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const breakpoint = useBreakpoint();
|
||||
const errorAmount =
|
||||
errors.globals.length +
|
||||
errors.unit.length +
|
||||
@@ -42,28 +31,20 @@ const ViewConfigErrorsModal = ({ errors, activeConfigurations, isDisabled }) =>
|
||||
|
||||
return (
|
||||
<>
|
||||
{breakpoint !== 'base' && breakpoint !== 'sm' ? (
|
||||
<Button
|
||||
<Tooltip
|
||||
label={`${errorAmount} ${errorAmount === 1 ? t('common.error') : t('common.errors')}`}
|
||||
hasArrow
|
||||
shouldWrapChildren
|
||||
>
|
||||
<IconButton
|
||||
colorScheme={errorAmount === 0 ? 'green' : 'red'}
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
rightIcon={errorAmount === 0 ? <CheckCircle size={20} /> : <WarningOctagon size={20} />}
|
||||
icon={errorAmount === 0 ? <CheckCircle size={20} /> : <WarningOctagon size={20} />}
|
||||
isDisabled={isDisabled || errorAmount === 0}
|
||||
>
|
||||
{errorAmount} {errorAmount === 1 ? t('common.error') : t('common.errors')}
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip label={`${errorAmount} ${errorAmount === 1 ? t('common.error') : t('common.errors')}`}>
|
||||
<IconButton
|
||||
colorScheme={errorAmount === 0 ? 'green' : 'red'}
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
icon={errorAmount === 0 ? <CheckCircle size={20} /> : <WarningOctagon size={20} />}
|
||||
isDisabled={isDisabled || errorAmount === 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { CheckCircle, WarningOctagon } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
@@ -30,7 +20,6 @@ const defaultProps = {
|
||||
const ViewConfigWarningsModal = ({ warnings, activeConfigurations, isDisabled }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const breakpoint = useBreakpoint();
|
||||
const warningsAmount =
|
||||
warnings.globals.length +
|
||||
warnings.unit.length +
|
||||
@@ -42,27 +31,19 @@ const ViewConfigWarningsModal = ({ warnings, activeConfigurations, isDisabled })
|
||||
|
||||
return (
|
||||
<>
|
||||
{breakpoint !== 'base' && breakpoint !== 'sm' ? (
|
||||
<Button
|
||||
<Tooltip
|
||||
label={`${warningsAmount} ${warningsAmount === 1 ? t('common.warning') : t('common.warnings')}`}
|
||||
hasArrow
|
||||
shouldWrapChildren
|
||||
>
|
||||
<IconButton
|
||||
colorScheme={warningsAmount === 0 ? 'green' : 'yellow'}
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
rightIcon={warningsAmount === 0 ? <CheckCircle size={20} /> : <WarningOctagon size={20} />}
|
||||
icon={warningsAmount === 0 ? <CheckCircle size={20} /> : <WarningOctagon size={20} />}
|
||||
isDisabled={isDisabled || warningsAmount === 0}
|
||||
>
|
||||
{warningsAmount} {warningsAmount === 1 ? t('common.warning') : t('common.warnings')}
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip label={`${warningsAmount} ${warningsAmount === 1 ? t('common.warning') : t('common.warnings')}`}>
|
||||
<IconButton
|
||||
colorScheme={warningsAmount === 0 ? 'green' : 'yellow'}
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
icon={warningsAmount === 0 ? <CheckCircle size={20} /> : <WarningOctagon size={20} />}
|
||||
isDisabled={isDisabled || warningsAmount === 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { ArrowsOut } from 'phosphor-react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -34,11 +24,10 @@ interface Props {
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const ViewJsonConfigModal = ({ configurations, activeConfigurations, isDisabled }: Props) => {
|
||||
const ViewJsonConfigModal: React.FC<Props> = ({ configurations, activeConfigurations, isDisabled }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const interfaces = useInterfacesJsonDisplay({ interfaces: configurations.interfaces?.configuration, isOpen });
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
const configStringToDisplay = () => {
|
||||
try {
|
||||
@@ -71,30 +60,17 @@ const ViewJsonConfigModal = ({ configurations, activeConfigurations, isDisabled
|
||||
|
||||
return (
|
||||
<>
|
||||
{breakpoint !== 'base' && breakpoint !== 'sm' ? (
|
||||
<Button
|
||||
<Tooltip label={t('common.view_json')}>
|
||||
<IconButton
|
||||
aria-label="Show JSON Configuration"
|
||||
colorScheme="gray"
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
rightIcon={<ArrowsOut size={20} />}
|
||||
icon={<ArrowsOut size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
ml={2}
|
||||
>
|
||||
{t('common.view_json')}
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip label={t('common.view_json')}>
|
||||
<IconButton
|
||||
aria-label="Show JSON Configuration"
|
||||
colorScheme="gray"
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
icon={<ArrowsOut size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
ml={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddSubsectionModal from './AddSubsectionModal';
|
||||
import ExpertModeButton from './ExpertModeButton';
|
||||
import GlobalsSection from './GlobalsSection';
|
||||
import { GLOBALS_SCHEMA } from './GlobalsSection/globalsConstants';
|
||||
import ImportConfigurationButton from './ImportConfigurationButton';
|
||||
@@ -384,6 +385,22 @@ const ConfigurationSectionsCard = ({ configId, editing, setSections, label, onDe
|
||||
activeConfigurations={activeConfigurations}
|
||||
isDisabled={isFetching}
|
||||
/>
|
||||
<ExpertModeButton
|
||||
defaultConfiguration={{
|
||||
globals: globals.data,
|
||||
unit: unit.data,
|
||||
metrics: metrics.data,
|
||||
services: services.data,
|
||||
radios: radios.data,
|
||||
interfaces: interfaces.data,
|
||||
'third-party': thirdParty.data,
|
||||
}}
|
||||
activeConfigurations={activeConfigurations}
|
||||
isDisabled={!editing}
|
||||
setConfig={importConfig}
|
||||
/>
|
||||
<ImportConfigurationButton isDisabled={!editing} setConfig={importConfig} />
|
||||
<AddSubsectionModal editing={editing} activeSubs={activeConfigurations} addSub={addSubsection} />
|
||||
<ViewJsonConfigModal
|
||||
configurations={{
|
||||
globals: globals.data,
|
||||
@@ -397,8 +414,6 @@ const ConfigurationSectionsCard = ({ configId, editing, setSections, label, onDe
|
||||
activeConfigurations={activeConfigurations}
|
||||
isDisabled={isFetching}
|
||||
/>
|
||||
<ImportConfigurationButton isDisabled={!editing} setConfig={importConfig} />
|
||||
<AddSubsectionModal editing={editing} activeSubs={activeConfigurations} addSub={addSubsection} />
|
||||
{onDelete && <DeleteButton isDisabled={!editing} onClick={onDelete} ml={2} />}
|
||||
</Box>
|
||||
</CardHeader>
|
||||
|
||||
@@ -149,7 +149,6 @@ const ConfigurationCard = ({ id }) => {
|
||||
<SaveButton
|
||||
onClick={handleSubmitClick}
|
||||
isLoading={updateEntity.isLoading}
|
||||
isCompact={false}
|
||||
isDisabled={
|
||||
!editing || !form.isValid || sections.invalidValues.length > 0 || (!form.dirty && !sections.isDirty)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ const EntityCard = ({ id }) => {
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isCompact={false}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
ml={2}
|
||||
/>
|
||||
|
||||
@@ -41,7 +41,13 @@ const EntityContactTableWrapper = ({ entity }) => {
|
||||
<Box textAlign="right" mb={2}>
|
||||
<CreateContactModal refresh={refreshEntity} entityId={entity.id} />
|
||||
</Box>
|
||||
<ContactTable select={entity.contacts} actions={actions} refreshId={refreshId} ignoredColumns={['entity']} />
|
||||
<ContactTable
|
||||
select={entity.contacts}
|
||||
actions={actions}
|
||||
refreshId={refreshId}
|
||||
ignoredColumns={['entity']}
|
||||
openDetailsModal={openEditModal}
|
||||
/>
|
||||
<EditContactModal isOpen={isEditOpen} onClose={closeEdit} contact={contact} refresh={refetchLocations} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -78,6 +78,7 @@ const EntityDeviceTableWrapper = ({ entity }: Props) => {
|
||||
ignoredColumns={['entity', 'venue']}
|
||||
refreshId={refreshId}
|
||||
actions={actions}
|
||||
openDetailsModal={openEditModal}
|
||||
/>
|
||||
<EditTagModal
|
||||
isOpen={isEditOpen}
|
||||
|
||||
@@ -41,7 +41,13 @@ const EntityLocationTableWrapper = ({ entity }) => {
|
||||
<Box textAlign="right" mb={2}>
|
||||
<CreateLocationModal refresh={refreshEntity} entityId={entity.id} />
|
||||
</Box>
|
||||
<LocationTable select={entity.locations} actions={actions} refreshId={refreshId} ignoredColumns={['entity']} />
|
||||
<LocationTable
|
||||
select={entity.locations}
|
||||
actions={actions}
|
||||
refreshId={refreshId}
|
||||
ignoredColumns={['entity']}
|
||||
openDetailsModal={openEditModal}
|
||||
/>
|
||||
<EditLocationModal isOpen={isEditOpen} onClose={closeEdit} location={location} refresh={refetchLocations} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,10 @@ const EntityResourcesTableWrapper = ({ entity }) => {
|
||||
setResource(newResource);
|
||||
openEdit();
|
||||
};
|
||||
const openDetailsModalFromTable = (openedResource) => {
|
||||
setResource(openedResource);
|
||||
openEdit();
|
||||
};
|
||||
|
||||
const refreshEntity = () => queryClient.invalidateQueries(['get-entity', entity.id]);
|
||||
|
||||
@@ -40,7 +44,13 @@ const EntityResourcesTableWrapper = ({ entity }) => {
|
||||
<Box textAlign="right" mb={2}>
|
||||
<CreateResourceModal refresh={refreshEntity} entityId={entity.id} />
|
||||
</Box>
|
||||
<ResourceTable select={entity.variables} actions={actions} refreshId={refreshId} ignoredColumns={['entity']} />
|
||||
<ResourceTable
|
||||
select={entity.variables}
|
||||
actions={actions}
|
||||
refreshId={refreshId}
|
||||
ignoredColumns={['entity']}
|
||||
openDetailsModal={openDetailsModalFromTable}
|
||||
/>
|
||||
<EditResourceModal isOpen={isEditOpen} onClose={closeEdit} resource={resource} refresh={refreshTable} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { Device } from 'models/Device';
|
||||
import { PageInfo, SortInfo } from 'models/Table';
|
||||
|
||||
const InventoryTable = () => {
|
||||
const InventoryTable: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>(undefined);
|
||||
const [onlyUnassigned, setOnlyUnassigned] = useBoolean(false);
|
||||
@@ -146,6 +146,7 @@ const InventoryTable = () => {
|
||||
customWidth: 'calc(15vh)',
|
||||
customMinWidth: '150px',
|
||||
disableSortBy: true,
|
||||
stopPropagation: true,
|
||||
},
|
||||
{
|
||||
id: 'venue',
|
||||
@@ -157,6 +158,7 @@ const InventoryTable = () => {
|
||||
customWidth: 'calc(15vh)',
|
||||
customMinWidth: '150px',
|
||||
disableSortBy: true,
|
||||
stopPropagation: true,
|
||||
},
|
||||
{
|
||||
id: 'subscriber',
|
||||
@@ -231,6 +233,7 @@ const InventoryTable = () => {
|
||||
</FormControl>
|
||||
<ColumnPicker
|
||||
columns={columns}
|
||||
defaultHiddenColumns={['subscriber']}
|
||||
hiddenColumns={hiddenColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="provisioning.inventoryTable.hiddenColumns"
|
||||
@@ -242,8 +245,15 @@ const InventoryTable = () => {
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<SortableDataTable
|
||||
columns={onlyUnassigned ? columns.filter((col) => col.id !== 'entity' && col.id !== 'venue') : columns}
|
||||
<SortableDataTable<Device>
|
||||
columns={
|
||||
onlyUnassigned
|
||||
? columns.filter(
|
||||
(col) =>
|
||||
col.id !== 'entity' && col.id !== 'venue' && !hiddenColumns.find((hidden) => hidden === col.id),
|
||||
)
|
||||
: columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id))
|
||||
}
|
||||
data={tags ?? []}
|
||||
isLoading={isFetchingCount || isFetchingTags}
|
||||
isManual
|
||||
@@ -255,6 +265,8 @@ const InventoryTable = () => {
|
||||
setPageInfo={setPageInfo}
|
||||
fullScreen
|
||||
saveSettingsId="inventory.table"
|
||||
onRowClick={openEditModal}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -131,9 +145,9 @@ const FmsLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -134,9 +148,9 @@ const GeneralLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, HStack, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Box, Flex, HStack, IconButton, Spacer, Table, Text, Th, Thead, Tooltip, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -124,9 +124,9 @@ const NotificationsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -131,9 +145,9 @@ const SecLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
operatorId: string;
|
||||
}
|
||||
|
||||
const OperatorDevicesTab = ({ operatorId }: Props) => {
|
||||
const OperatorDevicesTab: React.FC<Props> = ({ operatorId }) => {
|
||||
const { refreshId, refresh } = useRefreshId();
|
||||
const { obj: subscriberDevice, openModal, isOpen, onClose } = useObjectModal();
|
||||
const [serialNumber, setSerialNumber] = useState<string>('');
|
||||
@@ -54,7 +54,13 @@ const OperatorDevicesTab = ({ operatorId }: Props) => {
|
||||
<Box w="250px">
|
||||
<SubscriberDeviceSearch operatorId={operatorId} onClick={openModal} />
|
||||
</Box>
|
||||
<SubscriberDeviceTable operatorId={operatorId} actions={actions} refreshId={refreshId} minHeight="270px" />
|
||||
<SubscriberDeviceTable
|
||||
operatorId={operatorId}
|
||||
onOpenDetails={openModal}
|
||||
actions={actions}
|
||||
refreshId={refreshId}
|
||||
minHeight="270px"
|
||||
/>
|
||||
<EditSubscriberDeviceModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -11,9 +11,10 @@ const propTypes = {
|
||||
operatorId: PropTypes.string.isRequired,
|
||||
refreshId: PropTypes.number.isRequired,
|
||||
actions: PropTypes.func.isRequired,
|
||||
openDetailsModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ServiceClassTable = ({ operatorId, refreshId, actions }) => {
|
||||
const ServiceClassTable = ({ operatorId, refreshId, actions, openDetailsModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: serviceClasses,
|
||||
@@ -95,6 +96,8 @@ const ServiceClassTable = ({ operatorId, refreshId, actions }) => {
|
||||
]}
|
||||
isLoading={isFetching}
|
||||
minHeight="200px"
|
||||
onRowClick={openDetailsModal}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const ServiceClassTab = ({ operatorId }) => {
|
||||
<Box textAlign="right" mb={2}>
|
||||
<CreateServiceModal operatorId={operatorId} refresh={refresh} />
|
||||
</Box>
|
||||
<ServiceClassTable operatorId={operatorId} actions={actions} refreshId={refreshId} />
|
||||
<ServiceClassTable operatorId={operatorId} actions={actions} refreshId={refreshId} openDetailsModal={openModal} />
|
||||
<EditServiceClassModal serviceClass={serviceClass} isOpen={isOpen} onClose={onClose} refresh={refresh} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,11 +29,12 @@ const OperatorDetailsCard = ({ id }) => {
|
||||
<Heading size="md">{operator?.name}</Heading>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<DeleteOperatorButton isDisabled={editing || isFetching} operator={operator} />
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isCompact={false}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
hidden={!editing}
|
||||
ml={2}
|
||||
/>
|
||||
<ToggleEditButton
|
||||
@@ -43,7 +44,6 @@ const OperatorDetailsCard = ({ id }) => {
|
||||
isDirty={formRef.dirty}
|
||||
ml={2}
|
||||
/>
|
||||
<DeleteOperatorButton isDisabled={editing || isFetching} operator={operator} />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} isDisabled={editing} ml={2} />
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box, Flex, Heading, Spacer } from '@chakra-ui/react';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Actions from './Actions';
|
||||
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||
@@ -12,7 +13,7 @@ import ColumnPicker from 'components/ColumnPicker';
|
||||
import DataTable from 'components/DataTable';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import CreateOperatorModal from 'components/Modals/Operator/CreateOperatorModal';
|
||||
import { useGetOperatorCount, useGetOperators } from 'hooks/Network/Operators';
|
||||
import { OperatorApiResponse, useGetOperatorCount, useGetOperators } from 'hooks/Network/Operators';
|
||||
import useControlledTable from 'hooks/useControlledTable';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
@@ -29,6 +30,9 @@ const OperatorsTable = () => {
|
||||
useGet: useGetOperators as (props: unknown) => UseQueryResult,
|
||||
});
|
||||
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleGoToClick = (operator: { id: string }) => navigate(`/operators/${operator.id}`);
|
||||
|
||||
const memoizedActions = useCallback(
|
||||
(cell) => <Actions cell={cell.row} refreshTable={refetchCount} key={uuid()} />,
|
||||
@@ -36,8 +40,8 @@ const OperatorsTable = () => {
|
||||
);
|
||||
const memoizedDate = useCallback((cell, key) => <FormattedDate date={cell.row.values[key]} key={uuid()} />, []);
|
||||
|
||||
const columns: Column[] = useMemo(
|
||||
(): Column[] => [
|
||||
const columns: Column<OperatorApiResponse>[] = useMemo(
|
||||
(): Column<OperatorApiResponse>[] => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: t('common.name'),
|
||||
@@ -97,7 +101,7 @@ const OperatorsTable = () => {
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
<DataTable<OperatorApiResponse>
|
||||
columns={
|
||||
columns as {
|
||||
id: string;
|
||||
@@ -106,22 +110,24 @@ const OperatorsTable = () => {
|
||||
accessor: string;
|
||||
}[]
|
||||
}
|
||||
data={operators ?? []}
|
||||
isLoading={isFetching}
|
||||
isManual
|
||||
hiddenColumns={hiddenColumns}
|
||||
obj={t('operator.other')}
|
||||
data={(operators as unknown as OperatorApiResponse[]) ?? []}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
isLoading={isFetching}
|
||||
isManual
|
||||
hiddenColumns={hiddenColumns}
|
||||
obj={t('operator.other')}
|
||||
count={count || 0}
|
||||
// @ts-ignore
|
||||
setPageInfo={setPageInfo}
|
||||
fullScreen
|
||||
saveSettingsId="operators.table"
|
||||
onRowClick={handleGoToClick}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ApiKeyTable from './Table';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
@@ -10,7 +11,9 @@ const ApiKeysCard = () => {
|
||||
return (
|
||||
<Card p={4}>
|
||||
<CardBody>
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
<Box w="100%">
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
|
||||
import { Wrench } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSendEmailResetSubscriber, useSuspendSubscriber } from 'hooks/Network/Subscribers';
|
||||
import useMutationResult from 'hooks/useMutationResult';
|
||||
@@ -12,13 +12,7 @@ interface Props {
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const SubscriberActions = (
|
||||
{
|
||||
subscriber,
|
||||
refresh,
|
||||
isDisabled
|
||||
}: Props
|
||||
) => {
|
||||
const SubscriberActions: React.FC<Props> = ({ subscriber, refresh, isDisabled }) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutateAsync: suspend } = useSuspendSubscriber({ id: subscriber?.id ?? '' });
|
||||
const { mutateAsync: resetPassword } = useSendEmailResetSubscriber({ id: subscriber?.id ?? '' });
|
||||
@@ -41,9 +35,9 @@ const SubscriberActions = (
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton as={Button} rightIcon={<ChevronDownIcon />} ml={2} isDisabled={isDisabled}>
|
||||
{t('common.actions')}
|
||||
</MenuButton>
|
||||
<Tooltip label={t('common.actions')} aria-label={t('common.actions')} hasArrow>
|
||||
<MenuButton as={IconButton} icon={<Wrench size={20} />} ml={2} isDisabled={isDisabled} />
|
||||
</Tooltip>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleSuspendClick}>
|
||||
{subscriber?.suspended ? t('users.stop_suspension') : t('users.suspend')}
|
||||
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const SubscriberCard = ({ id }: Props) => {
|
||||
const SubscriberCard: React.FC<Props> = ({ id }) => {
|
||||
const [editing, setEditing] = useBoolean();
|
||||
const { data: subscriber, refetch, isFetching } = useGetSubscriber({ id });
|
||||
const { form, formRef } = useFormRef();
|
||||
@@ -41,11 +41,12 @@ const SubscriberCard = ({ id }: Props) => {
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<Box>
|
||||
<DeleteVenuePopover isDisabled={editing || isFetching} subscriber={subscriber} />
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isCompact={false}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
hidden={!editing}
|
||||
ml={2}
|
||||
/>
|
||||
<ToggleEditButton
|
||||
@@ -55,9 +56,8 @@ const SubscriberCard = ({ id }: Props) => {
|
||||
isDirty={form.dirty}
|
||||
ml={2}
|
||||
/>
|
||||
<DeleteVenuePopover isDisabled={editing || isFetching} subscriber={subscriber} />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} isDisabled={editing} ml={2} />
|
||||
<Actions subscriber={subscriber} refresh={refetch} isDisabled={editing} />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} isDisabled={editing} ml={2} />
|
||||
</Box>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
subscriberId: string;
|
||||
}
|
||||
|
||||
const OperatorDevicesTab = ({ operatorId, subscriberId }: Props) => {
|
||||
const OperatorDevicesTab: React.FC<Props> = ({ operatorId, subscriberId }) => {
|
||||
const { t } = useTranslation();
|
||||
const { refreshId, refresh } = useRefreshId();
|
||||
const [serialNumber, setSerialNumber] = useState<string>('');
|
||||
@@ -70,6 +70,7 @@ const OperatorDevicesTab = ({ operatorId, subscriberId }: Props) => {
|
||||
operatorId={operatorId}
|
||||
subscriberId={subscriberId}
|
||||
actions={actions}
|
||||
onOpenDetails={openModal}
|
||||
refreshId={refreshId}
|
||||
minHeight="380px"
|
||||
setDevices={setDevices}
|
||||
|
||||
@@ -58,7 +58,7 @@ const SystemSecretsTable = () => {
|
||||
columns={columns as Column<object>[]}
|
||||
saveSettingsId="system.secrets.table"
|
||||
data={getSecrets.data ?? []}
|
||||
obj={t('keys.other')}
|
||||
obj={t('system.secrets')}
|
||||
sortBy={[{ id: 'key', desc: false }]}
|
||||
showAllRows
|
||||
hideControls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Article } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemLoggingModal from './Modal';
|
||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||
@@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto">
|
||||
{t('system.logging')}
|
||||
</Button>
|
||||
<Tooltip label={t('system.logging')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('system.logging')}
|
||||
colorScheme="teal"
|
||||
type="button"
|
||||
my="auto"
|
||||
onClick={modalProps.onOpen}
|
||||
icon={<Article size={20} />}
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ArrowsClockwise } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemLoggingButton from './LoggingButton';
|
||||
import SystemCertificatesTable from './SystemCertificatesTable';
|
||||
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
@@ -70,16 +71,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
<Heading pt={0}>{endpoint.type}</Heading>
|
||||
<Spacer />
|
||||
<SystemLoggingButton endpoint={endpoint} token={token} />
|
||||
<Button
|
||||
mt={1}
|
||||
minWidth="112px"
|
||||
colorScheme="blue"
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
onClick={refresh}
|
||||
isLoading={isFetchingSystem || isFetchingSubsystems}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
|
||||
</Box>
|
||||
<CardBody>
|
||||
<VStack w="100%">
|
||||
|
||||
@@ -77,7 +77,7 @@ const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm',
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateUserForm from './Form';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
@@ -47,16 +47,7 @@ const CreateUserModal = ({ requirements, refreshUsers }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'CSR'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user