From b462a2b1ec4d6f1e384d30284ac60bcbe2dbb44e Mon Sep 17 00:00:00 2001 From: test Date: Wed, 2 Jul 2025 17:38:09 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FRONT/DEV/src/api/request.ts | 16 ++ .../src/components/UI/Dropdown/DeviceCard.tsx | 7 +- .../src/components/settings/SettingItem.tsx | 96 +++++++--- .../DEV/src/components/settings/Settings.tsx | 75 ++++++-- .../usersScanner/modules/ListUsers.tsx | 40 +++- .../usersScanner/modules/TableScanner.tsx | 13 +- .../usersScanner/modules/TableUsers.tsx | 4 +- .../appModules/usersScanner/userScanner.tsx | 30 ++- FRONT/DEV/src/hooks/useSettings.ts | 20 ++ .../settings/config/appearanceSettings.ts | 25 +++ .../store/settings/config/networkSettings.ts | 16 ++ .../store/settings/config/nodesSettings.ts | 59 ++++++ FRONT/DEV/src/store/settings/settingsStore.ts | 129 +++++++++++++ FRONT/DEV/src/store/settingsStore.ts | 179 ------------------ 14 files changed, 466 insertions(+), 243 deletions(-) create mode 100644 FRONT/DEV/src/hooks/useSettings.ts create mode 100644 FRONT/DEV/src/store/settings/config/appearanceSettings.ts create mode 100644 FRONT/DEV/src/store/settings/config/networkSettings.ts create mode 100644 FRONT/DEV/src/store/settings/config/nodesSettings.ts create mode 100644 FRONT/DEV/src/store/settings/settingsStore.ts delete mode 100644 FRONT/DEV/src/store/settingsStore.ts diff --git a/FRONT/DEV/src/api/request.ts b/FRONT/DEV/src/api/request.ts index 99f1f22..183a621 100644 --- a/FRONT/DEV/src/api/request.ts +++ b/FRONT/DEV/src/api/request.ts @@ -157,4 +157,20 @@ export class NetworkService { return error.response; } } + static async clearSubnet(ipAddress: string): Promise { + 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 { + try { + const axiosInstance = this.createAxiosInstance(ipAddress); + return await axiosInstance.post(`/settings/clear_wifi`); + } catch (error: any) { + return error.response; + } + } } \ No newline at end of file diff --git a/FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx b/FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx index 0345346..17e53c7 100644 --- a/FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx +++ b/FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx @@ -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 = ({ 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 = ({ device, status }) => { return ( - + setIsExpanded(!isExpanded)}>

MAC: {device.mac} @@ -100,7 +101,7 @@ const DeviceCard: React.FC = ({ device, status }) => { - {device.hostname || device.ip} + {device.hostname || (nameSetting === 'ip' ? device.ip : 'Неизвестно')} diff --git a/FRONT/DEV/src/components/settings/SettingItem.tsx b/FRONT/DEV/src/components/settings/SettingItem.tsx index 1732b5c..f1f850c 100644 --- a/FRONT/DEV/src/components/settings/SettingItem.tsx +++ b/FRONT/DEV/src/components/settings/SettingItem.tsx @@ -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(null); - const handleChange = (e: ChangeEvent) => { - 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) => { + 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' && ( - + onChange(e.target.checked)} + /> )} {setting.type === 'select' && setting.options && ( updateSetting(setting.id, value)} + onChange={(value) => onChange(value)} /> )} {setting.type === 'text' && ( onChange(e.target.value)} /> )} {setting.type === 'number' && ( - + + { + if (!/[0-9]/.test(e.key) && + !['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { + e.preventDefault(); + } + }} + className={error ? 'error' : ''} + /> + {error && {error}} + )} {setting.type === 'button' && ( @@ -127,8 +172,11 @@ export const SettingItem = ({ setting, value, onChange }: SettingItemProps) => { type="button" size="small" title={setting.defaultValue || ''} + onClick={setting.action} /> )} ); -}; \ No newline at end of file +}); + +export { SettingItem }; \ No newline at end of file diff --git a/FRONT/DEV/src/components/settings/Settings.tsx b/FRONT/DEV/src/components/settings/Settings.tsx index 33ca81e..7d91170 100644 --- a/FRONT/DEV/src/components/settings/Settings.tsx +++ b/FRONT/DEV/src/components/settings/Settings.tsx @@ -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>({}); + + 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); }, [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 (

Настройки

- onClose()} /> + @@ -121,7 +158,8 @@ export const Settings = ({onClose}: Props) => { handleValidationError(setting.id, error)} onChange={(value) => updateSetting(setting.id, value)} /> ))} @@ -136,6 +174,7 @@ export const Settings = ({onClose}: Props) => { size="small" title='Сохранить' onClick={handleSave} + /> diff --git a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/ListUsers.tsx b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/ListUsers.tsx index 52f146e..9c865bf 100644 --- a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/ListUsers.tsx +++ b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/ListUsers.tsx @@ -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 {/* Колонка активных устройств */}

Активные узлы ({activeDevices.length})

- {activeDevices.map((device, index) => ( + {currentActiveDevices.map((device, index) => (

Неактивные узлы ({inactiveDevices.length})

- {inactiveDevices.map((device, index) => ( + {currentInactiveDevices.map((device, index) => ( Узлы не найдены

) : null} + {totalPages > 1 && ( + + )} ); }; diff --git a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableScanner.tsx b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableScanner.tsx index 6e98453..e1dfabb 100644 --- a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableScanner.tsx +++ b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableScanner.tsx @@ -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) => ( - {item.hostname || item.ip} + {item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')} {item.vendor} {item.ip} {item.open_ports.join(', ') || '-'} diff --git a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableUsers.tsx b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableUsers.tsx index 53f7773..5a3bc4f 100644 --- a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableUsers.tsx +++ b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/modules/TableUsers.tsx @@ -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 ( <> - + handleScan()} disabled={loading}> {devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'} diff --git a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/userScanner.tsx b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/userScanner.tsx index 19ca497..8a098b8 100644 --- a/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/userScanner.tsx +++ b/FRONT/DEV/src/components/workSpace/modules/appModules/usersScanner/userScanner.tsx @@ -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() { Сканирование узлов ) : selectedView.value == 1 ? ( - + handleScan(false)} devices={devices} scanInitiated={scanInitiated} /> ):( - + handleScan(false)} devices={devices} scanInitiated={scanInitiated} /> )} ); diff --git a/FRONT/DEV/src/hooks/useSettings.ts b/FRONT/DEV/src/hooks/useSettings.ts new file mode 100644 index 0000000..017af56 --- /dev/null +++ b/FRONT/DEV/src/hooks/useSettings.ts @@ -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'; +}; \ No newline at end of file diff --git a/FRONT/DEV/src/store/settings/config/appearanceSettings.ts b/FRONT/DEV/src/store/settings/config/appearanceSettings.ts new file mode 100644 index 0000000..f8bd3b4 --- /dev/null +++ b/FRONT/DEV/src/store/settings/config/appearanceSettings.ts @@ -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; \ No newline at end of file diff --git a/FRONT/DEV/src/store/settings/config/networkSettings.ts b/FRONT/DEV/src/store/settings/config/networkSettings.ts new file mode 100644 index 0000000..73d8584 --- /dev/null +++ b/FRONT/DEV/src/store/settings/config/networkSettings.ts @@ -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; \ No newline at end of file diff --git a/FRONT/DEV/src/store/settings/config/nodesSettings.ts b/FRONT/DEV/src/store/settings/config/nodesSettings.ts new file mode 100644 index 0000000..0ecd613 --- /dev/null +++ b/FRONT/DEV/src/store/settings/config/nodesSettings.ts @@ -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; + diff --git a/FRONT/DEV/src/store/settings/settingsStore.ts b/FRONT/DEV/src/store/settings/settingsStore.ts new file mode 100644 index 0000000..c58571e --- /dev/null +++ b/FRONT/DEV/src/store/settings/settingsStore.ts @@ -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; + +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: (id: string) => T; + getNumberSetting: (id: string, fallback?: number) => number; +}; + +const defaultSettings: Setting[] = [ + ...nodesSettings, + ...networkSettings, + ...appearanceSettings, +]; + +export const useSettingsStore = create()( + 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(); + } + } + ) +); \ No newline at end of file diff --git a/FRONT/DEV/src/store/settingsStore.ts b/FRONT/DEV/src/store/settingsStore.ts deleted file mode 100644 index 22b451f..0000000 --- a/FRONT/DEV/src/store/settingsStore.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - -type SettingValue = any; -type SettingsDictionary = Record; - -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()( - 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(); - } - } - ) -);