Загрузка данных
import { useDeviceRepository } from '@/domains/equipment/repositories/device.repository';
import { normalizeSipUriForPayload } from '@/domains/equipment/repositories/helpers/normalizeSipUriForPayload';
import { usePreservedSipUri } from '@/domains/equipment/repositories/helpers/usePreservedSipUri';
import { DeviceModel } from '@/shared/api/controller/client';
import { yupResolver } from '@hookform/resolvers/yup';
import { QueryStatus } from '@reduxjs/toolkit/dist/query';
import { Button, Drawer, DrawerActions } from '@shared/components';
import { useCloseDrawer } from '@shared/hooks/drawer/useCloseDrawer';
import { useSelectDrawerType } from '@shared/hooks/drawer/useSelectDrawerType';
import {
EventActionTypes,
useSendClickStreamEvent,
} from '@shared/hooks/useSendClickStreamEvent';
import { DrawerTypes } from '@shared/model/drawer/drawer.types';
import { useSelectActiveController } from '@shared/store/сontrollers/сontrollers.selectors';
import { useSyncControlPage } from '@widgets/Monitoring/hooks';
import { memo, useEffect, useState } from 'react';
import {
FormProvider,
Resolver,
SubmitHandler,
useForm,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CreateDeviceForm } from '../CreateDeviceForm';
import { CreateDeviceFormData } from '../model/types';
import { createDeviceFormSchema } from '../validation/validator';
export const CreateDeviceDrawer = memo(() => {
const { t } = useTranslation('common');
const { sendEvent } = useSendClickStreamEvent(EventActionTypes.createDevice);
const drawerType = useSelectDrawerType();
const isVisible = drawerType[DrawerTypes.createDevice];
const closeDrawer = useCloseDrawer();
const activeController = useSelectActiveController();
const [model, setModel] = useState<DeviceModel | null>(null);
const { preserveSipUri, getPreservedSipUri, resetPreservedSipUri } =
usePreservedSipUri();
const {
saveDevice,
createDeviceStatus,
isCreateDeviceLoading,
resetCreateDevice,
} = useDeviceRepository();
const defaultValues: CreateDeviceFormData = {
name: '',
description: '',
ip: '',
mac: '',
controlPage: '',
typeName: '',
manufacturerName: '',
sipUri: '',
hasSipUri: null,
modelId: '',
hardwareId: '',
type: 'ethernet',
bidirectional: false,
ieee8021x: 'MAB',
protocolIp: 'dhcp',
firmware: null,
serialNumber: null,
inventoryNumber: null,
ipAvLan: '',
localAddress: undefined,
port: undefined,
netInterface: false,
netInterfaceInfo: [],
disableMonitoring: false,
};
const methods = useForm<CreateDeviceFormData>({
mode: 'onBlur',
reValidateMode: 'onChange',
defaultValues,
resolver: yupResolver(
createDeviceFormSchema,
) as Resolver<CreateDeviceFormData>,
});
useSyncControlPage(defaultValues.ip, methods);
useEffect(() => {
const currentHasSipUri = methods.getValues('hasSipUri');
if (currentHasSipUri) {
preserveSipUri(methods.getValues('sipUri'));
}
if (model) {
const modelHasSipUri = model.type?.hasSipUri ?? false;
methods.setValue('manufacturerName', model.manufacturer?.name);
methods.setValue('typeName', model.type?.name);
methods.setValue('hasSipUri', modelHasSipUri);
if (modelHasSipUri) {
methods.setValue('sipUri', getPreservedSipUri());
}
return;
}
methods.setValue('modelId', '');
methods.resetField('manufacturerName');
methods.resetField('typeName');
methods.setValue('hasSipUri', null);
}, [model]);
useEffect(() => {
if (isVisible) {
methods.reset(defaultValues);
resetPreservedSipUri();
}
}, [isVisible]);
useEffect(() => {
if (createDeviceStatus === QueryStatus.fulfilled) {
closeDrawer(DrawerTypes.createDevice);
methods.reset(defaultValues);
resetPreservedSipUri();
resetCreateDevice();
sendEvent();
}
}, [createDeviceStatus]);
const submitHandler: SubmitHandler<CreateDeviceFormData> = (values) => {
if (activeController?.id) {
saveDevice(normalizeSipUriForPayload(values), activeController.id);
}
};
const actions: DrawerActions = {
actionPrimary: (
<Button
data-fui-tid={`${DrawerTypes.createDevice}-actionPrimary`}
onClick={methods.handleSubmit(submitHandler)}
loading={isCreateDeviceLoading}
disabled={!methods.formState.isValid}
>
{t('buttons.save')}
</Button>
),
};
return (
<FormProvider {...methods}>
<Drawer
type={DrawerTypes.createDevice}
actions={actions}
title={t('drawerTitle.createDevice')}
>
<CreateDeviceForm setModel={setModel} />
</Drawer>
</FormProvider>
);
});
import { DisableMonitoringButton } from '@/entities/Monitoring';
import { ModelSelect } from '@/features/Settings/selects';
import { EquipmentModel } from '@/features/Settings/selects/ModelSelect';
import { DeviceModel } from '@/shared/api/controller/client';
import { TestIdsConsts } from '@/shared/consts/TestIdsConsts';
import { isMultilineValue } from '@/utils';
import { InterfaceSelect } from '@/widgets/Devices/ui/InterfaceSelect/InterfaceSelect';
import { EquipmentNetInterfaceListForm } from '@/widgets/Monitoring/components/EquipmentNetInterfaceListForm/EquipmentNetInterfaceListForm';
import {
IpInput,
SegmentButtonProps,
Switch,
TextField,
} from '@shared/components';
import { MonitoringConsts } from '@shared/consts/MonitoringConsts';
import { useFieldValidation } from '@shared/hooks';
import { InputMask } from '@shared/lib/InputMask';
import { ProtocolIpSelect } from '@widgets/Devices/ui/ProtocolIpSelect/ProtocolIpSelect';
import { Select8021X } from '@widgets/Devices/ui/Select802-1X/Select802-1X';
import { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
AVLAN_VALUES,
BASIC_VALUES,
ETHERNET_VALUES,
OTHER_VALUES,
} from './model/formNames';
import { CreateDeviceFormData } from './model/types';
import { createDeviceFormSchema } from './validation/validator';
type Props = {
setModel?: (model: DeviceModel | null) => void;
};
/**
* @requires refactoring
*/
export const CreateDeviceForm = ({ setModel }: Props) => {
const { t } = useTranslation();
const { control, watch, setValue, trigger } =
useFormContext<CreateDeviceFormData>();
const { isRequiredField } = useFieldValidation(createDeviceFormSchema);
const interfaceType = watch(OTHER_VALUES.type);
const hasSipUri = watch(OTHER_VALUES.hasSipUri);
const isEthernet = interfaceType === 'ethernet';
const isAvLan = interfaceType === 'av-lan';
useEffect(() => {
if (interfaceType !== 'ethernet') {
setValue(ETHERNET_VALUES.ip, '');
setValue(ETHERNET_VALUES.port, undefined);
setValue(ETHERNET_VALUES.ieee8021x, undefined);
setValue(ETHERNET_VALUES.protocolIp, 'dhcp');
}
if (interfaceType !== 'av-lan') {
setValue(AVLAN_VALUES.ipAvLan, '');
setValue(OTHER_VALUES.netInterface, false);
setValue(OTHER_VALUES.netInterfaceInfo, []);
}
if (interfaceType !== 'ethernet' && interfaceType !== 'av-lan') {
setValue(ETHERNET_VALUES.mac, '');
setValue(OTHER_VALUES.netInterface, false);
setValue(OTHER_VALUES.netInterfaceInfo, []);
}
trigger(OTHER_VALUES.type);
}, [interfaceType]);
useEffect(() => {
if (!hasSipUri) {
setValue(OTHER_VALUES.sipUri, '');
}
}, [hasSipUri]);
const getTestId = TestIdsConsts.getEquipmentTestId('create');
return (
<>
{Object.values(BASIC_VALUES).map((inputName) => (
<Controller
key={inputName}
name={inputName}
control={control}
render={({
field: { value, name, ...rest },
fieldState: { error },
}) => (
<TextField
data-fui-tid={getTestId(inputName)}
fullWidth
label={t(`controller:${name}`)}
error={!!error?.message}
helperText={t(error?.message as string)}
multiline={isMultilineValue(name)}
inputProps={{
'data-testid': name,
}}
required={isRequiredField(name)}
value={value}
limitedSymbols={
MonitoringConsts.EQUIPMENT_FIELDS_LIMITS[name] ?? undefined
}
{...rest}
/>
)}
/>
))}
<Controller
name={OTHER_VALUES.modelId}
control={control}
render={({ field: { name, ...rest }, fieldState: { error } }) => (
<ModelSelect
data-fui-tid={getTestId(name)}
type={EquipmentModel.device}
data-testid={name}
error={!!error?.message}
setModel={setModel}
handleClearButtonClick={() => setModel?.(null)}
helperText={t(error?.message as string)}
required={isRequiredField(name)}
{...rest}
/>
)}
/>
<Controller
name={OTHER_VALUES.typeName}
control={control}
render={({ field: { value, name } }) => (
<TextField
data-fui-tid={getTestId(name)}
label={t(`controller:${name}`)}
value={value}
readOnly
/>
)}
/>
{hasSipUri && (
<Controller
name={OTHER_VALUES.sipUri}
control={control}
render={({ field: { name, ...rest }, fieldState: { error } }) => (
<TextField
data-fui-tid={getTestId(name)}
placeholder={t('equipment:sipUriPlaceholder')}
fullWidth
label='SIP URI'
error={!!error?.message}
helperText={t(error?.message as string)}
inputProps={{
'data-testid': name,
}}
required={isRequiredField(name)}
limitedSymbols={
MonitoringConsts.EQUIPMENT_FIELDS_LIMITS[name] ?? undefined
}
{...rest}
/>
)}
/>
)}
<Controller
name={OTHER_VALUES.manufacturerName}
control={control}
render={({ field: { value, name } }) => (
<TextField
data-fui-tid={getTestId(name)}
label={t(`controller:${name}`)}
value={value}
readOnly
/>
)}
/>
<Controller
name={OTHER_VALUES.bidirectional}
control={control}
render={({ field: { value, name, ...rest } }) => (
<Switch
data-fui-tid={getTestId(name)}
inputProps={{
'data-testid': name,
}}
label={t(`device:${name}`)}
checked={!!value}
required={isRequiredField(name)}
{...rest}
/>
)}
/>
<Controller
name={OTHER_VALUES.type}
control={control}
render={({ field: { name, ...rest }, fieldState: { error } }) => (
<InterfaceSelect
data-fui-tid={getTestId(name)}
data-testid={name}
required={isRequiredField(name)}
error={!!error?.message}
helperText={t(error?.message as string)}
{...rest}
/>
)}
/>
{isEthernet && (
<>
{Object.values(ETHERNET_VALUES).map((inputName) => {
const isIp = inputName === 'ip';
const isMac = inputName === 'mac';
const protocolIp = inputName === 'protocolIp';
const ieee8021x = inputName === 'ieee8021x';
const mask = (() => {
if (isMac) {
return InputMask.mac;
}
return undefined;
})();
if (protocolIp) {
return (
<Controller
name={ETHERNET_VALUES.protocolIp}
control={control}
render={({
field: { name, ...rest },
fieldState: { error },
}) => (
<ProtocolIpSelect
data-fui-tid={getTestId(inputName)}
error={!!error?.message}
helperText={t(error?.message as string)}
required
{...rest}
/>
)}
/>
);
}
if (ieee8021x) {
return (
<Controller
name={ETHERNET_VALUES.ieee8021x}
control={control}
render={({
field: { name, ...rest },
fieldState: { error },
}) => (
<Select8021X
data-fui-tid={getTestId(inputName)}
error={!!error?.message}
helperText={t(error?.message as string)}
required
{...rest}
/>
)}
/>
);
}
return (
<Controller
key={inputName}
name={inputName}
control={control}
render={({
field: { value, name, ...rest },
fieldState: { error },
}) => {
if (isIp) {
return (
<IpInput
data-fui-tid={getTestId(inputName)}
value={(value as string) ?? ''}
error={!!error?.message}
helperText={t(error?.message as string)}
label={name}
// HACK: use updateDeviceSchema to check required ip
required={(() => {
switch (inputName) {
case 'ip':
return true;
default:
isRequiredField(name);
}
})()}
{...rest}
/>
);
}
return (
<TextField
data-fui-tid={getTestId(inputName)}
fullWidth
label={t(`controller:${name}`)}
error={!!error?.message}
helperText={t(error?.message as string)}
multiline={isMultilineValue(name)}
inputProps={{
'data-testid': name,
}}
// HACK: use updateDeviceSchema to check required mac
required={(() => {
switch (inputName) {
case 'mac':
return true;
default:
return isRequiredField(name);
}
})()}
value={value}
mask={mask}
limitedSymbols={
MonitoringConsts.EQUIPMENT_FIELDS_LIMITS[name] ??
undefined
}
{...rest}
/>
);
}}
/>
);
})}
</>
)}
{isAvLan && (
<>
{Object.values(AVLAN_VALUES).map((inputName) => {
const isMac = inputName === 'mac';
const isIpAvLan = inputName === 'ipAvLan';
return (
<Controller
key={inputName}
name={inputName}
control={control}
render={({
field: { value, name, ...rest },
fieldState: { error },
}) => {
if (isIpAvLan) {
return (
<IpInput
data-fui-tid={getTestId(inputName)}
value={(value as string) ?? ''}
error={!!error?.message}
helperText={t(error?.message as string)}
label={name}
required
{...rest}
/>
);
}
return (
<TextField
data-fui-tid={getTestId(inputName)}
fullWidth
label={t(`controller:${name}`)}
error={!!error?.message}
helperText={t(error?.message as string)}
multiline={isMultilineValue(name)}
inputProps={{
'data-testid': name,
}}
// HACK: use updateDeviceSchema to check required mac
required={(() => {
switch (inputName) {
case 'mac':
return false;
default:
return isRequiredField(name);
}
})()}
value={value}
mask={isMac ? InputMask.mac : undefined}
{...rest}
/>
);
}}
/>
);
})}
</>
)}
{isEthernet && (
<>
<Controller
name={OTHER_VALUES.netInterface}
control={control}
render={({ field: { onChange, value, name } }) => (
<Switch
data-fui-tid={getTestId(name)}
margin='normal'
label={t(`controller:${name}`)}
checked={!!value}
onChange={onChange}
required={isRequiredField(name)}
/>
)}
/>
<EquipmentNetInterfaceListForm
name={OTHER_VALUES.netInterfaceInfo}
netInterfaceName={OTHER_VALUES.netInterface}
isRequiredField={isRequiredField}
/>
</>
)}
<Controller
name={OTHER_VALUES.disableMonitoring}
control={control}
render={({ field: { onChange, value } }) => {
const handleChange: SegmentButtonProps['onChange'] = (
_,
newValue,
) => {
onChange(newValue);
};
return (
<DisableMonitoringButton value={!!value} onChange={handleChange} />
);
}}
/>
</>
);
};
import { useAutocompleteDebounce } from '@/shared/hooks';
import { Helpers } from '@/shared/lib';
import { Autocomplete } from '@components/UI/autocomplete/Autocomplete';
import {
AdditionalAutocompleteProps,
DeviceModelOnChange,
} from '@components/types';
import { PlusIcon } from '@sber-friend/flamingo-icons';
import { DeviceModel } from '@shared/api/controller/client';
import { Box, IconButton } from '@shared/components';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { EquipmentModelType } from '../../lib';
import { useGetModels } from '../hooks/useGetModels';
import { useModelSelectActions } from '../hooks/useHelpers';
import { useVirtualization } from '../hooks/useVirtualization';
type Props = Omit<AdditionalAutocompleteProps, 'onChange'> & {
onChange: DeviceModelOnChange;
type: EquipmentModelType;
initialModel?: DeviceModel | null;
setModel?: (model: DeviceModel | null) => void;
isCreateButton?: boolean;
};
export const ModelSelect = ({
value,
onChange,
error,
helperText,
required,
type,
initialModel,
isCreateButton = true,
setModel,
onKeyDown,
handleClearButtonClick,
...rest
}: Props) => {
const { t } = useTranslation();
const [page, setPage] = useState(0);
const {
inputValue,
setInputValue,
debouncedValue,
isDebounceLoading,
onInputChange,
} = useAutocompleteDebounce(initialModel?.name);
const { models, total, status, isDataFetching } = useGetModels({
debouncedValue,
type,
page,
});
const { allItems, VirtualizedListbox, options } = useVirtualization({
initialModel,
models,
total,
status,
page,
setPage,
});
const {
handleChange,
handleOpenCreateModalDrawer,
handleInputChange,
handleBackSpace,
} = useModelSelectActions({
allItems,
type,
setModel,
onChange,
onInputChange,
handleClearButtonClick,
setPage,
setInputValue,
});
const info = Helpers.isEmpty(allItems)
? t('common:messages.addModel')
: helperText;
return (
<Box display='flex' gridGap='8px'>
<Autocomplete
loading={isDataFetching || isDebounceLoading}
margin='none'
label={t('controller:model')}
options={options}
value={value}
onChange={handleChange}
inputValue={inputValue}
error={error}
helperText={info}
required={required}
disabled={!options.length}
ListboxComponent={VirtualizedListbox}
onKeyDown={handleBackSpace}
handleClearButtonClick={() => {
handleClearButtonClick?.();
setInputValue('');
}}
onInputChange={handleInputChange}
//@ts-ignore
data-testId={rest['data-testid']}
{...rest}
/>
{isCreateButton && (
<Box marginTop='20px'>
<IconButton
variant='square'
contained
onClick={handleOpenCreateModalDrawer}
>
<PlusIcon />
</IconButton>
</Box>
)}
</Box>
);
};
import { DeviceModel } from '@/shared/api/controller/client';
import { AutocompleteDebounceReturn } from '@/shared/hooks';
import { useSetDrawerType } from '@/shared/hooks/drawer/useSetDrawerType';
import { AutocompleteOnChange, DeviceModelOnChange } from '@components/types';
import { DrawerTypes } from '@shared/model/drawer/drawer.types';
import { ChangeEvent, KeyboardEvent } from 'react';
import { EquipmentModel, EquipmentModelType } from '../../lib';
type Params = {
allItems: DeviceModel[];
type: EquipmentModelType;
setModel?: (model: DeviceModel | null) => void;
onChange: DeviceModelOnChange;
onInputChange?: AutocompleteDebounceReturn['onInputChange'];
handleClearButtonClick?: () => void;
setPage: (page: number) => void;
setInputValue: (value: string) => void;
};
export const useModelSelectActions = ({
allItems,
type,
setModel,
onChange,
onInputChange,
handleClearButtonClick,
setPage,
setInputValue,
}: Params) => {
const setDrawerType = useSetDrawerType();
const handleOpenCreateModalDrawer = () => {
if (type === EquipmentModel.controller) {
setDrawerType(DrawerTypes.createControllerModel);
return;
}
if (type === EquipmentModel.padlet) {
setDrawerType(DrawerTypes.createPadletModel);
return;
}
setDrawerType(DrawerTypes.createDeviceModel);
};
const handleChange: AutocompleteOnChange = (value) => {
const model = allItems.find((model) => model.id === value);
if (model) {
setInputValue(model?.name || '');
onChange(value);
setModel?.(model);
}
};
const handleInputChange = (
_: ChangeEvent<HTMLInputElement>,
value: string,
) => {
onInputChange?.(value);
setPage(0);
};
const handleBackSpace = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Backspace') {
handleClearButtonClick?.();
setModel?.(null);
}
};
return {
handleChange,
handleOpenCreateModalDrawer,
handleInputChange,
handleBackSpace,
};
};