Загрузка данных
import { ModelSelect } from '@/features/Settings/selects';
import { LocationFilters } from '@/features/filters/LocationFilters/LocationFilters';
import { AppConsts } from '@/shared';
import {
Button,
Column,
DateTimeField,
IconButton,
IpInput,
Row,
Select,
TextField,
Tooltip,
Typography,
} from '@/shared/components';
import { SearchSelect } from '@/shared/components/SearchSelect';
import { InputMask } from '@/shared/lib/InputMask';
import {
ChevronsDownIcon,
ChevronsUpIcon,
HelpCircleIcon,
} from '@sber-friend/flamingo-icons';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuditSideFilter } from '../controller';
export const UsersAuditSideFilter = () => {
const { t } = useTranslation('common');
const {
searchParams,
isEquipmentFiltersOpen,
refetch,
auditOptions,
smartRoomFilter,
additionalFilters,
baseFilters,
handleEquipmentFiltersToggle,
} = useAuditSideFilter();
const {
resetFilters,
handleChangeTimeFrom,
handleChange,
handleChangeTimeMax,
handleChangeUser,
} = baseFilters;
const {
modelIds,
macValue,
ipValue,
cityId,
floorId,
placeId,
regionId,
roomId,
isEquipmentFiltersAvailable,
equipmentModelType,
handleChangeModels,
handleMacChange,
handleIpChange,
setRegionId,
setCityId,
setPlaceId,
setFloorId,
setRoomId,
} = additionalFilters;
const sourceOptions = useMemo(() => {
return [
{ label: t('all'), value: '' },
{ label: t('monitoring'), value: 'smartroom-monitoring' },
{ label: t('authentication'), value: 'smartroom-auth' },
{
label: t('notifications:notifications'),
value: 'smartroom-notifications',
},
];
}, [searchParams.source]);
const { smartRoomId, smartRoomIdError, handleSmartRoomIdChange } =
smartRoomFilter;
const { isFetchingUsers, userOptions, typeOptions, setSearchUser } =
auditOptions;
return (
<Column minWidth={330} maxWidth={330} gridGap={16}>
<DateTimeField
label={t('timeFrom')}
onChange={handleChangeTimeFrom}
margin='none'
value={searchParams.tmMin && searchParams.tmMin * AppConsts.THOUSAND}
/>
<DateTimeField
label={t('timeTo')}
margin='none'
onChange={handleChangeTimeMax}
value={searchParams.tmMax && searchParams.tmMax * AppConsts.THOUSAND}
/>
<Select
label={t('type')}
onChange={handleChange('type')}
options={typeOptions || ''}
value={searchParams.type}
/>
<SearchSelect
label={t('user')}
placeholder={t('user:placeholders.searchByFullName')}
loading={isFetchingUsers}
value={searchParams?.userId || ''}
setValue={setSearchUser}
options={userOptions}
onChangeCallback={handleChangeUser}
/>
<Select
key='qwe'
label={t('source')}
onChange={handleChange('source')}
options={sourceOptions || ''}
value={searchParams.source}
/>
<TextField
label='Smartroom ID'
value={smartRoomId}
onChange={handleSmartRoomIdChange}
error={smartRoomIdError}
helperText={smartRoomIdError ? t('messages.invalidUUIDMessage') : ''}
/>
{isEquipmentFiltersAvailable && (
<>
<Row justifyContent='space-between'>
<Typography variant='h5'>{t('audit:equipmentParams')}</Typography>
<IconButton onClick={handleEquipmentFiltersToggle}>
{isEquipmentFiltersOpen ? (
<ChevronsUpIcon />
) : (
<ChevronsDownIcon />
)}
</IconButton>
</Row>
{isEquipmentFiltersOpen && equipmentModelType && (
<Column gridGap={16}>
<Row alignItems={'center'}>
<Typography variant='h5'>{t('location')}</Typography>
<Tooltip title={t('audit:locationHint')}>
<HelpCircleIcon fontSize='small' />
</Tooltip>
</Row>
<LocationFilters
isRooms
isRoomRequired
regionId={regionId}
setRegionId={setRegionId}
cityId={cityId}
setCityId={setCityId}
placeId={placeId}
setPlaceId={setPlaceId}
floorId={floorId}
setFloorId={setFloorId}
roomId={roomId}
setRoomId={setRoomId}
withDropdown={false}
onLocationIdsChange={() => {}}
/>
<Typography variant='h5'>{t('other')}</Typography>
<ModelSelect
multiple
isCreateButton={false}
type={equipmentModelType}
value={modelIds}
onChange={handleChangeModels}
/>
<TextField
fullWidth
label='MAC'
mask={InputMask.mac}
value={macValue}
onChange={handleMacChange}
/>
<IpInput
label='ip'
value={ipValue}
onChange={handleIpChange}
error={false}
helperText={undefined}
/>
</Column>
)}
</>
)}
<Column gridGap={8}>
<Button onClick={refetch} color='primary'>
{t('buttons.update')}
</Button>
<Button onClick={resetFilters} color='error'>
{t('buttons.reset')}
</Button>
</Column>
</Column>
);
};
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 ModelSelectValue = string | number | boolean | null;
type CommonProps = Omit<
AdditionalAutocompleteProps,
'onChange' | 'value' | 'multiple'
> & {
onChange: DeviceModelOnChange;
type: EquipmentModelType;
isCreateButton?: boolean;
};
type SingleProps = CommonProps & {
multiple?: false;
value?: ModelSelectValue;
initialModel?: DeviceModel | null;
setModel?: (model: DeviceModel | null) => void;
};
type MultipleProps = Omit<CommonProps, 'onChange'> & {
multiple: true;
value?: ModelSelectValue[];
onChange: (value: ModelSelectValue[]) => void;
initialModel?: DeviceModel | DeviceModel[] | null;
setModel?: (model: DeviceModel[]) => void;
};
type Props = SingleProps | MultipleProps;
export const ModelSelect = ({
value,
onChange,
error,
helperText,
required,
type,
initialModel,
multiple,
isCreateButton = true,
setModel,
onKeyDown,
handleClearButtonClick,
...rest
}: Props) => {
const { t } = useTranslation('common');
const [page, setPage] = useState(0);
const initialInputValue = Array.isArray(initialModel)
? undefined
: initialModel?.name;
const {
inputValue,
setInputValue,
debouncedValue,
isDebounceLoading,
onInputChange,
} = useAutocompleteDebounce(initialInputValue);
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,
multiple,
onInputChange,
handleClearButtonClick,
setPage,
setInputValue,
});
const info = Helpers.isEmpty(allItems) ? t('messages.addModel') : helperText;
console.log(inputValue);
return (
<Box display='flex' gridGap='8px'>
<Autocomplete
loading={isDataFetching || isDebounceLoading}
margin='none'
label={t('controller:model')}
options={options}
value={value}
multiple={multiple}
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 { DispatchStateAction } from '@/components/types';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
export type AutocompleteDebounceReturn = {
inputValue: string;
setInputValue: DispatchStateAction<string>;
debouncedValue: string;
isDebounceLoading: boolean;
onInputChange: (value: string) => void;
resetDebounce: (value?: string) => void;
};
const AUTOCOMPLETE_DEBOUNCE_STATE = {
debouncedValue: '',
isDebounceLoading: false,
inputValue: '',
hasUserInteracted: false,
debounceDuration: 1000,
} as const;
/**
* Хук для управления состоянием автокомплита с дебаунсом.
* Позволяет отложить обновление основного значения до завершения ввода пользователя.
*
* @example
* ```tsx
* const MyComponent = () => {
* const {
* inputValue,
* debouncedValue,
* isDebounceLoading,
* onInputChange
* } = useAutocompleteDebounce('initial value');
*
* useEffect(() => {
* // Вызывается только через 1 секунду после остановки ввода
* if (debouncedValue) {
* fetchSearchResults(debouncedValue);
* }
* }, [debouncedValue]);
*
* return (
* <div>
* <input
* type="text"
* value={inputValue}
* onChange={onInputChange}
* placeholder="Поиск..."
* />
* {isDebounceLoading && <Spinner />}
* </div>
* );
* };
* ```
*
* @param initialInputValue - Начальное значение для поля ввода (опционально)
* @returns Объект с состоянием и обработчиками
*/
export const useAutocompleteDebounce = (
initialInputValue?: string,
): AutocompleteDebounceReturn => {
const [debouncedValue, setDebouncedValue] = useState<string>(
AUTOCOMPLETE_DEBOUNCE_STATE.debouncedValue,
);
const [isDebounceLoading, setIsDebounceLoading] = useState<boolean>(
AUTOCOMPLETE_DEBOUNCE_STATE.isDebounceLoading,
);
const [inputValue, setInputValue] = useState<string>(
AUTOCOMPLETE_DEBOUNCE_STATE.inputValue,
);
const hasUserInteracted = useRef<boolean>(
AUTOCOMPLETE_DEBOUNCE_STATE.hasUserInteracted,
);
const debounce = useDebouncedCallback((value: string) => {
setDebouncedValue(value);
setIsDebounceLoading(AUTOCOMPLETE_DEBOUNCE_STATE.isDebounceLoading);
}, AUTOCOMPLETE_DEBOUNCE_STATE.debounceDuration);
const onInputChange = useCallback(
(value: string) => {
console.log(value);
setInputValue(value);
hasUserInteracted.current = true;
debounce(value);
setIsDebounceLoading(true);
},
[debounce],
);
const resetDebounce = useCallback(
(value = '') => {
debounce.cancel();
setInputValue(value);
setDebouncedValue(value);
setIsDebounceLoading(AUTOCOMPLETE_DEBOUNCE_STATE.isDebounceLoading);
},
[debounce],
);
useEffect(() => {
if (initialInputValue !== undefined && !hasUserInteracted.current) {
setInputValue(initialInputValue);
}
}, [initialInputValue]);
return {
inputValue,
setInputValue,
debouncedValue,
isDebounceLoading,
onInputChange,
resetDebounce,
};
};
import {
AdditionalAutocompleteProps,
AutocompleteOption,
} from '@/components/types';
import { AutocompleteRenderInputParams } from '@material-ui/lab';
import { Autocomplete as FlamingoAutocomplete } from '@sber-friend/flamingo-core';
import { TextField, TextFieldProps } from '@shared/components';
import { KeyboardEventHandler, memo, useCallback, useMemo } from 'react';
import { useAutocompleteController } from './controllers/useAutocompleteController/useAutocompleteController';
type AutocompleteValue = string | number | boolean | null;
export type AutocompleteProps = {
options: AutocompleteOption[];
error?: boolean | undefined;
value?: AutocompleteValue | AutocompleteValue[];
onChange: (value: AutocompleteValue | AutocompleteValue[]) => void;
label?: string;
disabled?: boolean;
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
} & Omit<AdditionalAutocompleteProps, 'onChange' | 'value'> & {
margin?: TextFieldProps['margin'];
'data-fui-tid'?: TextFieldProps['data-fui-tid'];
'data-testid'?: string;
};
export const Autocomplete = memo(
({
options,
onChange,
error,
helperText,
required,
value,
label,
disabled,
placeholder,
styleVariant,
onInputChange,
inputValue,
loading,
margin,
popupIcon,
onReset,
onClose,
open,
onOpen,
renderOption,
size,
onKeyDown,
ListboxComponent,
handleClearButtonClick,
multiple,
...rest
}: AutocompleteProps) => {
const handleRenderInput = useCallback(
(params: AutocompleteRenderInputParams) => (
<TextField
margin={margin ?? 'none'}
error={error}
required={required}
helperText={helperText}
label={label}
placeholder={placeholder}
{...params}
inputProps={{
...params.inputProps,
'data-testid': rest['data-testid']
? `${rest['data-testid']}-input`
: undefined,
}}
/>
),
[margin, error, required, helperText, label, placeholder, rest],
);
const { getOptionLabel, getOptionSelected, getAutocompleteValue } =
useAutocompleteController();
const autocompleteValue = useMemo(() => {
if (multiple) {
const values = Array.isArray(value) ? value : [];
return options.filter((option) => values.includes(option.value));
}
return getAutocompleteValue(options, value as string);
}, [getAutocompleteValue, multiple, options, value]);
const handleReset = useCallback(() => {
onChange(multiple ? [] : '');
}, [multiple, onChange]);
const handleChange = useCallback<
(
event: unknown,
value: AutocompleteOption | AutocompleteOption[] | null,
) => void
>(
(_, newValue) => {
if (Array.isArray(newValue)) {
onChange(newValue.map((option) => option.value));
return;
}
onChange(newValue ? newValue.value : '');
},
[onChange],
);
return (
<FlamingoAutocomplete
open={open}
styleVariant={styleVariant}
disabled={disabled}
value={autocompleteValue}
multiple={multiple}
getOptionSelected={getOptionSelected}
fullWidth
getOptionLabel={getOptionLabel}
onChange={handleChange}
onInputChange={onInputChange}
options={options}
renderInput={handleRenderInput}
onReset={onReset ?? handleReset}
inputValue={inputValue}
loading={loading}
customPopupIcon={!!popupIcon}
popupIcon={popupIcon}
onClose={onClose}
onOpen={onOpen}
renderOption={renderOption}
size={size}
onKeyDown={onKeyDown}
ListboxComponent={ListboxComponent}
handleClearButtonClick={handleClearButtonClick}
// @ts-ignore
data-testid={rest['data-testid']}
data-fui-tid={rest['data-fui-tid']}
/>
);
},
);