Реализация функционала настроек
This commit is contained in:
parent
11237605b9
commit
b462a2b1ec
14 changed files with 466 additions and 243 deletions
|
|
@ -157,4 +157,20 @@ export class NetworkService {
|
|||
return error.response;
|
||||
}
|
||||
}
|
||||
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
return await axiosInstance.post(`/settings/clear_subnet`);
|
||||
} catch (error: any) {
|
||||
return error.response;
|
||||
}
|
||||
}
|
||||
static async clearNetwork(ipAddress: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
return await axiosInstance.post(`/settings/clear_wifi`);
|
||||
} catch (error: any) {
|
||||
return error.response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {buttonNames} from "../../../const/buttons/names";
|
|||
import {ModalMap} from "../../modalwindows/windowsType";
|
||||
import {IconComponent} from "../../../const/buttons/icons";
|
||||
import {useModalContext} from "../../../store";
|
||||
import {useNameSetting} from "../../../hooks/useSettings";
|
||||
|
||||
interface DeviceCardProps {
|
||||
device: DeviceUser;
|
||||
|
|
@ -74,8 +75,8 @@ const Value = styled.span`
|
|||
|
||||
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const toggleExpanded = () => setIsExpanded(!isExpanded);
|
||||
const { openModal } = useModalContext();
|
||||
const nameSetting = useNameSetting();
|
||||
|
||||
const openModalNote = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -87,7 +88,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
|||
|
||||
|
||||
return (
|
||||
<CardWrapper status={status} onClick={toggleExpanded}>
|
||||
<CardWrapper status={status} onClick={()=>setIsExpanded(!isExpanded)}>
|
||||
<CardHeader status={status}>
|
||||
<p>
|
||||
MAC: {device.mac}
|
||||
|
|
@ -100,7 +101,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
|||
<CardContent isExpanded={isExpanded}>
|
||||
<InfoRow>
|
||||
<Label>Имя:</Label>
|
||||
<Value>{device.hostname || device.ip}</Value>
|
||||
<Value>{device.hostname || (nameSetting === 'ip' ? device.ip : 'Неизвестно')}</Value>
|
||||
</InfoRow>
|
||||
|
||||
<InfoRow>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import {Setting, useSettingsStore} from "../../store/settingsStore";
|
||||
import {ChangeEvent} from "react";
|
||||
import {Setting} from "../../store/settings/settingsStore";
|
||||
import {useState} from "react";
|
||||
import styled, {useTheme} from "styled-components";
|
||||
import {IconComponent} from "../../const/buttons/icons";
|
||||
import {buttonNames} from "../../const/buttons/names";
|
||||
import SettingSelect from "./SettingSelect";
|
||||
import {MainButton} from "../UI/buttons/mainButton";
|
||||
import React from 'react';
|
||||
|
||||
const SettingBlock = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -65,24 +66,55 @@ const LabelCheckbox = styled.label`
|
|||
transform: translate(-50%, -50%);
|
||||
}
|
||||
`
|
||||
const InputWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
gap: 6px;
|
||||
`
|
||||
const ErrorText = styled.div`
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
interface SettingItemProps {
|
||||
setting: Setting;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
onValidationError?: (error: string | null) => void;
|
||||
}
|
||||
|
||||
export const SettingItem = ({ setting, value, onChange }: SettingItemProps) => {
|
||||
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { updateSetting } = useSettingsStore();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const newValue = setting.type === 'boolean'
|
||||
? (e.target as HTMLInputElement).checked
|
||||
: setting.type === 'number'
|
||||
? Number(e.target.value)
|
||||
: e.target.value;
|
||||
onChange(newValue);
|
||||
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value;
|
||||
let newError: string | null = null;
|
||||
|
||||
if (inputValue.trim() === '') {
|
||||
newError = 'Поле не может быть пустым';
|
||||
onChange(null);
|
||||
}
|
||||
else if (isNaN(Number(inputValue))) {
|
||||
newError = 'Пожалуйста, введите число';
|
||||
}
|
||||
else {
|
||||
const numValue = Number(inputValue);
|
||||
if (setting.min !== undefined && numValue < setting.min) {
|
||||
newError = `Значение не может быть меньше ${setting.min}`;
|
||||
}
|
||||
else if (setting.max !== undefined && numValue > setting.max) {
|
||||
newError = `Значение не может быть больше ${setting.max}`;
|
||||
}
|
||||
|
||||
if (!newError) {
|
||||
onChange(numValue);
|
||||
}
|
||||
}
|
||||
|
||||
setError(newError);
|
||||
onValidationError?.(newError);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -91,35 +123,48 @@ export const SettingItem = ({ setting, value, onChange }: SettingItemProps) => {
|
|||
|
||||
{setting.type === 'boolean' && (
|
||||
<LabelCheckbox>
|
||||
<StyledCheckbox />
|
||||
<StyledCheckbox
|
||||
checked={!!value}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<IconComponent name={buttonNames[19]} width="18" height="18" setColor={theme.primary} />
|
||||
</LabelCheckbox>
|
||||
)}
|
||||
|
||||
{setting.type === 'select' && setting.options && (
|
||||
<SettingSelect
|
||||
value={setting.value}
|
||||
value={value}
|
||||
options={setting.options}
|
||||
onChange={(value) => updateSetting(setting.id, value)}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{setting.type === 'text' && (
|
||||
<input
|
||||
type="text"
|
||||
value={setting.value}
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{setting.type === 'number' && (
|
||||
<input
|
||||
type="number"
|
||||
min={setting?.min}
|
||||
max={setting?.max}
|
||||
value={setting.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<InputWrapper>
|
||||
<input
|
||||
type="number"
|
||||
min={setting?.min}
|
||||
max={setting?.max}
|
||||
value={value === undefined || value === null ? '' : value}
|
||||
onChange={handleNumberChange}
|
||||
onKeyDown={(e) => {
|
||||
if (!/[0-9]/.test(e.key) &&
|
||||
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className={error ? 'error' : ''}
|
||||
/>
|
||||
{error && <ErrorText>{error}</ErrorText>}
|
||||
</InputWrapper>
|
||||
)}
|
||||
|
||||
{setting.type === 'button' && (
|
||||
|
|
@ -127,8 +172,11 @@ export const SettingItem = ({ setting, value, onChange }: SettingItemProps) => {
|
|||
type="button"
|
||||
size="small"
|
||||
title={setting.defaultValue || ''}
|
||||
onClick={setting.action}
|
||||
/>
|
||||
)}
|
||||
</SettingBlock>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export { SettingItem };
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
// components/modalContainers/NoteDevice.tsx
|
||||
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
|
||||
import { MainButton } from "../UI/buttons/mainButton";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import styled from "styled-components";
|
||||
import {Setting, useSettingsStore} from "../../store/settingsStore";
|
||||
import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
|
||||
import {SettingItem} from "./SettingItem";
|
||||
import {IconComponent} from "../../const/buttons/icons";
|
||||
import {buttonNames} from "../../const/buttons/names";
|
||||
|
|
@ -51,16 +50,31 @@ const SectionLine = styled.span`
|
|||
const CloseBtn = styled.span`
|
||||
cursor: pointer;
|
||||
`
|
||||
export const Settings = ({ onClose }: Props) => {
|
||||
const settingsConfig = useSettingsStore(state => state.settingsConfig);
|
||||
const settingsValues = useSettingsStore(state => state.settingsValues);
|
||||
const activeSection = useSettingsStore(state => state.activeSection);
|
||||
const pendingChanges = useSettingsStore(state => state.pendingChanges);
|
||||
const setActiveSection = useSettingsStore(state => state.setActiveSection);
|
||||
const updateSetting = useSettingsStore(state => state.updateSetting);
|
||||
const applyChanges = useSettingsStore(state => state.applyChanges);
|
||||
const discardChanges = useSettingsStore(state => state.discardChanges);
|
||||
const getSetting = useSettingsStore(state => state.getSetting);
|
||||
const initializeSettings = useSettingsStore(state => state.initializeSettings);
|
||||
|
||||
export const Settings = ({onClose}: Props) => {
|
||||
const {
|
||||
settingsConfig,
|
||||
settingsValues,
|
||||
activeSection,
|
||||
setActiveSection,
|
||||
initializeSettings,
|
||||
updateSetting
|
||||
} = useSettingsStore();
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleValidationError = useCallback((fieldId: string, error: string | null) => {
|
||||
setFieldErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
if (error) {
|
||||
newErrors[fieldId] = error;
|
||||
} else {
|
||||
delete newErrors[fieldId];
|
||||
}
|
||||
return newErrors;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const settingsBySection = useMemo(() => {
|
||||
return settingsConfig.reduce((acc, setting) => {
|
||||
|
|
@ -75,7 +89,7 @@ export const Settings = ({onClose}: Props) => {
|
|||
}, {} as Record<string, Setting[]>);
|
||||
}, [settingsConfig, settingsValues]);
|
||||
|
||||
const sections = Object.keys(settingsBySection);
|
||||
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeSettings();
|
||||
|
|
@ -87,17 +101,40 @@ export const Settings = ({onClose}: Props) => {
|
|||
}
|
||||
}, [sections, activeSection, setActiveSection]);
|
||||
|
||||
const handleSave = () => {
|
||||
onClose();
|
||||
};
|
||||
const handleSave = useCallback(() => {
|
||||
const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
|
||||
if (hasErrors) {
|
||||
alert('Исправьте ошибки перед сохранением');
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredFields = settingsConfig.filter(s => s.required);
|
||||
const emptyRequiredFields = requiredFields.filter(f => {
|
||||
const value = pendingChanges[f.id] ?? settingsValues[f.id] ?? f.defaultValue;
|
||||
return value == null || value === '';
|
||||
});
|
||||
|
||||
if (emptyRequiredFields.length > 0) {
|
||||
alert('Заполните все обязательные поля');
|
||||
return;
|
||||
}
|
||||
|
||||
applyChanges();
|
||||
onClose();
|
||||
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
discardChanges();
|
||||
setFieldErrors({});
|
||||
onClose();
|
||||
}, [discardChanges, onClose]);
|
||||
|
||||
return (
|
||||
<ModalContainer style={{ height: '60vh', width: '50vw'}}>
|
||||
<ModalTitle>
|
||||
<h3>Настройки</h3>
|
||||
<CloseBtn>
|
||||
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={() => onClose()} />
|
||||
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
|
||||
</CloseBtn>
|
||||
</ModalTitle>
|
||||
|
||||
|
|
@ -121,7 +158,8 @@ export const Settings = ({onClose}: Props) => {
|
|||
<SettingItem
|
||||
key={setting.id}
|
||||
setting={setting}
|
||||
value={settingsValues[setting.id] ?? setting.defaultValue}
|
||||
value={getSetting(setting.id).currentValue}
|
||||
onValidationError={(error) => handleValidationError(setting.id, error)}
|
||||
onChange={(value) => updateSetting(setting.id, value)}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -136,6 +174,7 @@ export const Settings = ({onClose}: Props) => {
|
|||
size="small"
|
||||
title='Сохранить'
|
||||
onClick={handleSave}
|
||||
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</ModalContainer>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
|
||||
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||
import {usePaginateSetting} from "../../../../../../hooks/useSettings";
|
||||
import Pagination from "../../../../../UI/Pagination";
|
||||
|
||||
const ScanBtn = styled.button`
|
||||
position: relative;
|
||||
|
|
@ -29,8 +31,29 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
const inactiveDevices = devices.filter(device => device.status !== 'online');
|
||||
|
||||
// Пагинация
|
||||
const paginateSetting = usePaginateSetting();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [devicesPerPage] = useState(10);
|
||||
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
|
||||
|
||||
useEffect(() => {
|
||||
setDevicesPerPage(paginateSetting);
|
||||
}, [paginateSetting]);
|
||||
|
||||
const indexOfLastDevice = currentPage * devicesPerPage;
|
||||
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
|
||||
const currentActiveDevices = activeDevices.slice(indexOfFirstDevice, indexOfLastDevice);
|
||||
const currentInactiveDevices = inactiveDevices.slice(indexOfFirstDevice, indexOfLastDevice);
|
||||
const totalPages = Math.ceil(devices.length / devicesPerPage);
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if(currentPage > 1) setCurrentPage(currentPage - 1);
|
||||
}
|
||||
const handleNextPage = () => {
|
||||
if(currentPage < totalPages) setCurrentPage(currentPage + 1);
|
||||
}
|
||||
const onPageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
|
@ -48,7 +71,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
{/* Колонка активных устройств */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные узлы ({activeDevices.length})</h3>
|
||||
{activeDevices.map((device, index) => (
|
||||
{currentActiveDevices.map((device, index) => (
|
||||
<DeviceCard
|
||||
key={index}
|
||||
status="online"
|
||||
|
|
@ -59,7 +82,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
{/* Колонка неактивных устройств */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные узлы ({inactiveDevices.length})</h3>
|
||||
{inactiveDevices.map((device, index) => (
|
||||
{currentInactiveDevices.map((device, index) => (
|
||||
<DeviceCard
|
||||
key={index}
|
||||
status="offline"
|
||||
|
|
@ -71,6 +94,15 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
) : scanInitiated ? (
|
||||
<p style={{ marginTop: '20px' }}>Узлы не найдены</p>
|
||||
) : null}
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
onPageChange={onPageChange}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPrevPage={handlePrevPage}
|
||||
onNextPage={handleNextPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {buttonNames} from "../../../../../../const/buttons/names";
|
|||
import {ModalMap} from "../../../../../modalwindows/windowsType";
|
||||
import {useModalContext} from "../../../../../../store";
|
||||
import Pagination from "../../../../../UI/Pagination";
|
||||
import {useSettingsStore} from "../../../../../../store/settingsStore";
|
||||
import {useSettingsStore} from "../../../../../../store/settings/settingsStore";
|
||||
import {useNameSetting, usePaginateSetting} from "../../../../../../hooks/useSettings";
|
||||
|
||||
const TableContainer = styled.div`
|
||||
width: 100%;
|
||||
|
|
@ -74,14 +75,10 @@ const TableScanner = ({devices}:TableScannerProps) => {
|
|||
currentNote: device.note || ''
|
||||
});
|
||||
};
|
||||
|
||||
const paginateSetting = useSettingsStore(state =>
|
||||
state.settingsValues['paginate-nodes'] ??
|
||||
state.settingsConfig.find(s => s.id === 'paginate-nodes')?.defaultValue ??
|
||||
15
|
||||
);
|
||||
const nameSetting = useNameSetting();
|
||||
|
||||
// Пагинация
|
||||
const paginateSetting = usePaginateSetting();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
|
||||
|
||||
|
|
@ -123,7 +120,7 @@ const TableScanner = ({devices}:TableScannerProps) => {
|
|||
{currentDevices.map((item, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
|
||||
<TableCell>{item.hostname || item.ip}</TableCell>
|
||||
<TableCell>{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>
|
||||
<TableCell>{item.vendor}</TableCell>
|
||||
<TableCell>{item.ip}</TableCell>
|
||||
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const ScanBtn = styled.button`
|
|||
|
||||
interface ListUsersProps {
|
||||
loading: boolean;
|
||||
handleScan: ()=>void;
|
||||
handleScan: (replay?: boolean) => void;
|
||||
devices: DeviceUser[];
|
||||
scanInitiated: boolean;
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
|
||||
return (
|
||||
<>
|
||||
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||
<ScanBtn onClick={()=>handleScan()} disabled={loading}>
|
||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'}
|
||||
</ScanBtn>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useIpStore } from "../../../../../store";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import { useState } from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import { NetworkService } from "../../../../../api/request";
|
||||
import ListUsers from "./modules/ListUsers";
|
||||
import TableUsers from "./modules/TableUsers";
|
||||
import DropdownType from "../../../../UI/Dropdown/DropdownType";
|
||||
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
|
||||
|
||||
type Option ={
|
||||
value: number,
|
||||
|
|
@ -136,9 +137,11 @@ export default function UserScanner() {
|
|||
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
|
||||
|
||||
// Сканирование
|
||||
const handleScan = async () => {
|
||||
const handleScan = async (replay:boolean) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if(!replay){
|
||||
setLoading(true);
|
||||
}
|
||||
setScanInitiated(true);
|
||||
const res = await NetworkService.scanUser(ipAddress);
|
||||
console.log(res);
|
||||
|
|
@ -151,6 +154,23 @@ export default function UserScanner() {
|
|||
}
|
||||
};
|
||||
|
||||
// Интервально сканирование
|
||||
const intervalToggle = useIntervalBoolean();
|
||||
const intervalTime = useIntervalTime();
|
||||
useEffect(() => {
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (intervalToggle) {
|
||||
intervalId = setInterval(()=>handleScan(true), intervalTime * 60 * 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
};
|
||||
}, [intervalToggle, intervalTime, handleScan]);
|
||||
|
||||
// Способ отображения
|
||||
const viewOptions: Option[] = [
|
||||
{ value: 1, label: 'Таблица' },
|
||||
|
|
@ -180,9 +200,9 @@ export default function UserScanner() {
|
|||
Сканирование узлов <ScanningText />
|
||||
</LoadingText>
|
||||
) : selectedView.value == 1 ? (
|
||||
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
|
||||
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
|
||||
):(
|
||||
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
|
||||
<ListUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
|||
20
FRONT/DEV/src/hooks/useSettings.ts
Normal file
20
FRONT/DEV/src/hooks/useSettings.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import {useSettingsStore} from "../store/settings/settingsStore";
|
||||
|
||||
export const useNameSetting = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('netnames-nodes')) as string;
|
||||
};
|
||||
|
||||
export const usePaginateSetting = () => {
|
||||
return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20));
|
||||
};
|
||||
|
||||
export const useIntervalBoolean = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean;
|
||||
};
|
||||
export const useIntervalTime = () => {
|
||||
return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60));
|
||||
};
|
||||
|
||||
export const useThemeSetting = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system';
|
||||
};
|
||||
25
FRONT/DEV/src/store/settings/config/appearanceSettings.ts
Normal file
25
FRONT/DEV/src/store/settings/config/appearanceSettings.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const appearanceSettings = [
|
||||
{
|
||||
id: 'theme',
|
||||
sectionId: 'Внешний вид',
|
||||
label: 'Цветовая тема',
|
||||
type: 'select',
|
||||
defaultValue: 'light',
|
||||
options: [
|
||||
{ value: 'light', label: 'Светлая' },
|
||||
{ value: 'dark', label: 'Тёмная' },
|
||||
{ value: 'system', label: 'Как в системе' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
sectionId: 'Внешний вид',
|
||||
label: 'Язык интерфейса',
|
||||
type: 'select',
|
||||
defaultValue: 'ru',
|
||||
options: [
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'en', label: 'Английский' },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
16
FRONT/DEV/src/store/settings/config/networkSettings.ts
Normal file
16
FRONT/DEV/src/store/settings/config/networkSettings.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const networkSettings = [
|
||||
{
|
||||
id: 'clean-network',
|
||||
type: 'button',
|
||||
label: 'Очистить данные сессии',
|
||||
sectionId: 'Точки доступа',
|
||||
defaultValue: 'Очистить',
|
||||
},
|
||||
{
|
||||
id: 'export-network',
|
||||
type: 'button',
|
||||
label: 'Экспорт данных точек доступа',
|
||||
sectionId: 'Точки доступа',
|
||||
defaultValue: 'Экспортировать',
|
||||
},
|
||||
] as const;
|
||||
59
FRONT/DEV/src/store/settings/config/nodesSettings.ts
Normal file
59
FRONT/DEV/src/store/settings/config/nodesSettings.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
export const nodesSettings = [
|
||||
{
|
||||
id: 'rescan-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Интервальное сканирование',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'interval-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Интервал сканирования узлов (мин)',
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
min: 1,
|
||||
max: 2880
|
||||
},
|
||||
{
|
||||
id: 'netnames-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Отображаемые имена при отсутствии',
|
||||
type: 'select',
|
||||
defaultValue: 'ip',
|
||||
options: [
|
||||
{ value: 'ip', label: 'IP адрес' },
|
||||
{ value: 'unknown', label: 'Неизвестно' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'paginate-nodes',
|
||||
type: 'number',
|
||||
label: 'Количество строк в таблице',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 20,
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
id: 'customize-nodes',
|
||||
type: 'button',
|
||||
label: 'Отображение узлов',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Настроить',
|
||||
},
|
||||
{
|
||||
id: 'clean-nodes',
|
||||
type: 'button',
|
||||
label: 'Очистить данные сессии',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Очистить',
|
||||
},
|
||||
{
|
||||
id: 'export-nodes',
|
||||
type: 'button',
|
||||
label: 'Экспорт данных узлов',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Экспортировать',
|
||||
},
|
||||
] as const;
|
||||
|
||||
129
FRONT/DEV/src/store/settings/settingsStore.ts
Normal file
129
FRONT/DEV/src/store/settings/settingsStore.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import {nodesSettings} from "./config/nodesSettings";
|
||||
import { networkSettings } from './config/networkSettings';
|
||||
import {appearanceSettings} from "./config/appearanceSettings";
|
||||
|
||||
type SettingValue = any;
|
||||
type SettingsDictionary = Record<string, SettingValue>;
|
||||
|
||||
interface SettingOption {
|
||||
readonly value: any;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export type Setting = {
|
||||
readonly id: string;
|
||||
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button';
|
||||
readonly label: string;
|
||||
readonly defaultValue: any;
|
||||
value?: string;
|
||||
readonly min?: number;
|
||||
readonly max?: number;
|
||||
readonly options?: any;
|
||||
readonly sectionId: string;
|
||||
readonly required?: boolean;
|
||||
readonly action?: () => void;
|
||||
};
|
||||
|
||||
type SettingsState = {
|
||||
settingsConfig: Setting[];
|
||||
settingsValues: SettingsDictionary;
|
||||
activeSection: string | null;
|
||||
pendingChanges: SettingsDictionary;
|
||||
setActiveSection: (sectionId: string | null) => void;
|
||||
updateSetting: (id: string, value: any) => void;
|
||||
initializeSettings: () => void;
|
||||
getSetting: (id: string) => Setting & { currentValue: any };
|
||||
resetSettings: () => void;
|
||||
applyChanges: () => void;
|
||||
discardChanges: () => void;
|
||||
getSettingValue: <T = any>(id: string) => T;
|
||||
getNumberSetting: (id: string, fallback?: number) => number;
|
||||
};
|
||||
|
||||
const defaultSettings: Setting[] = [
|
||||
...nodesSettings,
|
||||
...networkSettings,
|
||||
...appearanceSettings,
|
||||
];
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
settingsConfig: defaultSettings,
|
||||
settingsValues: {},
|
||||
activeSection: null,
|
||||
pendingChanges: {},
|
||||
|
||||
setActiveSection: (sectionId) => set({ activeSection: sectionId }),
|
||||
|
||||
updateSetting: (id, value) =>
|
||||
set((state) => ({
|
||||
pendingChanges: { ...state.pendingChanges, [id]: value }
|
||||
})),
|
||||
|
||||
applyChanges: () =>
|
||||
set((state) => ({
|
||||
settingsValues: { ...state.settingsValues, ...state.pendingChanges },
|
||||
pendingChanges: {}
|
||||
})),
|
||||
|
||||
discardChanges: () => set({ pendingChanges: {} }),
|
||||
|
||||
initializeSettings: () => {
|
||||
const { settingsValues, settingsConfig } = get();
|
||||
const newValues = { ...settingsValues };
|
||||
|
||||
settingsConfig.forEach(setting => {
|
||||
if (newValues[setting.id] === undefined) {
|
||||
newValues[setting.id] = setting.defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
set({ settingsValues: newValues });
|
||||
},
|
||||
|
||||
getSetting: (id) => {
|
||||
const state = get();
|
||||
const setting = state.settingsConfig.find(s => s.id === id);
|
||||
if (!setting) throw new Error(`Setting ${id} not found`);
|
||||
|
||||
const hasPendingChange = Object.prototype.hasOwnProperty.call(state.pendingChanges, id);
|
||||
const hasSavedValue = Object.prototype.hasOwnProperty.call(state.settingsValues, id);
|
||||
|
||||
const currentValue = hasPendingChange
|
||||
? state.pendingChanges[id]
|
||||
: hasSavedValue
|
||||
? state.settingsValues[id]
|
||||
: setting.defaultValue;
|
||||
|
||||
return {
|
||||
...setting,
|
||||
currentValue
|
||||
};
|
||||
},
|
||||
getSettingValue: (id: string) => {
|
||||
const state = get();
|
||||
return state.settingsValues[id] ??
|
||||
state.settingsConfig.find(s => s.id === id)?.defaultValue;
|
||||
},
|
||||
|
||||
getNumberSetting: (id: string, fallback = 0) => {
|
||||
const value = get().getSettingValue(id);
|
||||
return Number(value) || fallback;
|
||||
},
|
||||
|
||||
resetSettings: () => set({ settingsValues: {}, pendingChanges: {} }),
|
||||
}),
|
||||
{
|
||||
name: 'app-settings',
|
||||
partialize: (state) => ({
|
||||
settingsValues: state.settingsValues
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
state?.initializeSettings();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type SettingValue = any;
|
||||
type SettingsDictionary = Record<string, SettingValue>;
|
||||
|
||||
export type Setting = {
|
||||
id: string;
|
||||
type: 'boolean' | 'select' | 'text' | 'number' | 'button';
|
||||
label: string;
|
||||
defaultValue: any;
|
||||
value?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
options?: { value: any; label: string }[];
|
||||
sectionId: string;
|
||||
};
|
||||
|
||||
type SettingsState = {
|
||||
settingsConfig: Setting[];
|
||||
settingsValues: SettingsDictionary;
|
||||
activeSection: string | null;
|
||||
setActiveSection: (sectionId: string | null) => void;
|
||||
updateSetting: (id: string, value: any) => void;
|
||||
initializeSettings: () => void;
|
||||
getSetting: (id: string) => Setting & { currentValue: any };
|
||||
resetSettings: () => void;
|
||||
};
|
||||
|
||||
const defaultSettings: Setting[] = [
|
||||
// Узлы подсети
|
||||
{
|
||||
id: 'rescan-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Интервальное сканирование',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'interval-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Интервал сканирования узлов (мин)',
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
min: 1,
|
||||
max: 2880
|
||||
},
|
||||
{
|
||||
id: 'netnames-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Отображаемые имена при отсутствии',
|
||||
type: 'select',
|
||||
defaultValue: 'ip',
|
||||
options: [
|
||||
{ value: 'ip', label: 'IP адрес' },
|
||||
{ value: 'unknown', label: 'Неизвестно' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'paginate-nodes',
|
||||
type: 'number',
|
||||
label: 'Количество строк в таблице',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 20,
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
id: 'customize-nodes',
|
||||
type: 'button',
|
||||
label: 'Отображение узлов',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Настроить',
|
||||
},
|
||||
{
|
||||
id: 'clean-nodes',
|
||||
type: 'button',
|
||||
label: 'Очистить данные сессии',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Очистить',
|
||||
},
|
||||
{
|
||||
id: 'export-nodes',
|
||||
type: 'button',
|
||||
label: 'Экспорт данных узлов',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Экспортировать',
|
||||
},
|
||||
|
||||
// Точки доступа
|
||||
{
|
||||
id: 'clean-network',
|
||||
type: 'button',
|
||||
label: 'Очистить данные сессии',
|
||||
sectionId: 'Точки доступа',
|
||||
defaultValue: 'Очистить',
|
||||
},
|
||||
{
|
||||
id: 'export-network',
|
||||
type: 'button',
|
||||
label: 'Экспорт данных точек доступа',
|
||||
sectionId: 'Точки доступа',
|
||||
defaultValue: 'Экспортировать',
|
||||
},
|
||||
|
||||
// Внешний вид
|
||||
{
|
||||
id: 'theme',
|
||||
sectionId: 'Внешний вид',
|
||||
label: 'Цветовая тема',
|
||||
type: 'select',
|
||||
defaultValue: 'light',
|
||||
options: [
|
||||
{ value: 'light', label: 'Светлая' },
|
||||
{ value: 'dark', label: 'Тёмная' },
|
||||
{ value: 'system', label: 'Как в системе' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
sectionId: 'Внешний вид',
|
||||
label: 'Язык интерфейса',
|
||||
type: 'select',
|
||||
defaultValue: 'ru',
|
||||
options: [
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'en', label: 'Английский' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
settingsConfig: defaultSettings,
|
||||
settingsValues: {},
|
||||
activeSection: null,
|
||||
|
||||
setActiveSection: (sectionId) => set({ activeSection: sectionId }),
|
||||
|
||||
updateSetting: (id, value) =>
|
||||
set((state) => ({
|
||||
settingsValues: { ...state.settingsValues, [id]: value }
|
||||
})),
|
||||
|
||||
initializeSettings: () => {
|
||||
const { settingsValues, settingsConfig } = get();
|
||||
const newValues = { ...settingsValues };
|
||||
|
||||
settingsConfig.forEach(setting => {
|
||||
if (newValues[setting.id] === undefined) {
|
||||
newValues[setting.id] = setting.defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
set({ settingsValues: newValues });
|
||||
},
|
||||
|
||||
getSetting: (id) => {
|
||||
const setting = get().settingsConfig.find(s => s.id === id);
|
||||
if (!setting) throw new Error(`Setting ${id} not found`);
|
||||
return {
|
||||
...setting,
|
||||
currentValue: get().settingsValues[id] ?? setting.defaultValue
|
||||
};
|
||||
},
|
||||
|
||||
resetSettings: () => set({ settingsValues: {} }),
|
||||
}),
|
||||
{
|
||||
name: 'app-settings',
|
||||
partialize: (state) => ({
|
||||
settingsValues: state.settingsValues
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
state?.initializeSettings();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue