Загрузка данных


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