Merge pull request #175 from Telecominfraproject/main

Version 2.9.0(18)
This commit is contained in:
Charles Bourque
2023-03-22 15:10:41 +01:00
committed by GitHub
110 changed files with 7307 additions and 6641 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@@ -15,7 +15,7 @@ interface Props extends ThemeProps {
const defaultProps = {
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
label: undefined,
};

View File

@@ -15,7 +15,7 @@ const defaultProps = {
onClick: () => {},
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
label: undefined,
};

View File

@@ -15,7 +15,7 @@ interface Props {
const defaultProps = {
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
label: undefined,
ml: undefined,
};

View File

@@ -15,7 +15,7 @@ const defaultProps = {
label: 'Edit',
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
ml: undefined,
};

View File

@@ -14,7 +14,7 @@ interface Props {
const defaultProps = {
isDisabled: false,
isFetching: false,
isCompact: false,
isCompact: true,
ml: undefined,
};

View File

@@ -15,7 +15,7 @@ const ResponsiveButton = ({
onClick,
isDisabled,
isLoading,
isCompact,
isCompact = true,
color,
label,
icon,

View File

@@ -18,7 +18,7 @@ const defaultProps = {
onSave: undefined,
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
ml: undefined,
};

View File

@@ -17,7 +17,7 @@ interface Props {
const defaultProps = {
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
isDirty: false,
ml: undefined,
};

View File

@@ -15,7 +15,7 @@ interface Props extends ThemeProps {
const defaultProps = {
isDisabled: false,
isLoading: false,
isCompact: false,
isCompact: true,
label: undefined,
};

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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],
);

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ const DeviceActionDropdown = ({
return (
<Menu>
<Tooltip label={t('commands.other')}>
<Tooltip label={t('common.actions')}>
<MenuButton
as={IconButton}
aria-label="Commands"

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ const DeviceActionDropdown = ({
return (
<Menu>
<Tooltip label={t('commands.other')}>
<Tooltip label={t('common.actions')}>
<MenuButton
as={IconButton}
aria-label="Commands"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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,
};
};

View File

@@ -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[];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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' },
]}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ const EntityCard = ({ id }) => {
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isCompact={false}
isDisabled={!editing || !form.isValid || !form.dirty}
ml={2}
/>

View File

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

View File

@@ -78,6 +78,7 @@ const EntityDeviceTableWrapper = ({ entity }: Props) => {
ignoredColumns={['entity', 'venue']}
refreshId={refreshId}
actions={actions}
openDetailsModal={openEditModal}
/>
<EditTagModal
isOpen={isEditOpen}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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