This commit is contained in:
commit 1672f54e02
22 changed files with 366 additions and 89 deletions

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false) setHasInited(false)
initializationAttempt.current = 0; initializationAttempt.current = 0;
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`); const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
let connectionTimeout; let connectionTimeout;
setSocket(newSocket); setSocket(newSocket);

View file

@ -115,7 +115,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => { const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`); const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
testSocket.close(); testSocket.close();
resolve(false); resolve(false);

View file

@ -1,13 +1,13 @@
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
const axiosWithoutCredentials = axios.create({ const axiosWithoutCredentials = axios.create({
baseURL: `https://localhost:8001`, baseURL: `http://localhost:8001`,
}) })
export class NetworkService { export class NetworkService {
static createAxiosInstance(ipAddress: string) { static createAxiosInstance(ipAddress: string) {
return axios.create({ return axios.create({
baseURL: `https://${ipAddress}:8001`, baseURL: `http://${ipAddress}:8001`,
}); });
} }
@ -86,7 +86,9 @@ export class NetworkService {
static async scanUser(ipAddress: string): Promise<AxiosResponse> { static async scanUser(ipAddress: string): Promise<AxiosResponse> {
try { try {
const axiosInstance = this.createAxiosInstance(ipAddress); const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices'); return await axiosInstance.post('/subnet/devices',{
subnet: ''
});
} catch (error: any) { } catch (error: any) {
return error.response; return error.response;
} }
@ -157,6 +159,8 @@ export class NetworkService {
return error.response; return error.response;
} }
} }
// Settings
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> { static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
try { try {
const axiosInstance = this.createAxiosInstance(ipAddress); const axiosInstance = this.createAxiosInstance(ipAddress);
@ -173,4 +177,12 @@ export class NetworkService {
return error.response; return error.response;
} }
} }
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export`);
} catch (error: any) {
return error.response;
}
}
} }

View file

@ -0,0 +1,131 @@
import React, {useEffect, useState} from 'react';
import {ButtonContainer, ModalContainer, ModalContent, ModalTitle} from "../style";
import {MainButton} from "../../UI/buttons/mainButton";
import {ModalMap} from "../windowsType";
import {useModalContext} from "../../../store";
import styled from "styled-components";
import { useSettingsStore } from '../../../store/settings/settingsStore';
const StyledTableList = styled.ul`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 0 40px;
list-style-type: none;
`
const StyledTableItem = styled.li`
width: 160px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
background-color: ${props => props.theme.accent};
color: ${props => props.theme.primary};
border: 1px solid ${props => props.theme.primary};
&:first-child {
border-radius: 16px 0 0 16px;
}
&:last-child {
border-radius: 0 16px 16px 0;
}
&.off{
background-color: ${props => props.theme.offlineColor};
}
`
const CustomizeNodes = () => {
const { openModal } = useModalContext();
const pendingChanges = useSettingsStore(state => state.pendingChanges);
const updateSetting = useSettingsStore(state => state.updateSetting);
const getSetting = useSettingsStore(state => state.getSetting);
const initializeSettings = useSettingsStore(state => state.initializeSettings);
const [dragIndex, setDragIndex] = useState<number | null>(null);
// Получаем текущие значения из хранилища
const initialColumns = getSetting('table-columns-order').currentValue as string[];
const disabledColumns = getSetting('table-columns-disabled').currentValue as number[];
const items = pendingChanges['table-columns-order'] ?? initialColumns;
const disabledItems = pendingChanges['table-columns-disabled'] ?? disabledColumns;
// Drag & Drop
const handleDragStart = (index: number) => {
setDragIndex(index);
};
const handleDragOver = (index: number) => {
if (dragIndex === null || index === dragIndex) return;
const newItems = [...items];
const [movedItem] = newItems.splice(dragIndex, 1);
newItems.splice(index, 0, movedItem);
updateSetting('table-columns-order', newItems);
setDragIndex(index);
};
const handleDrop = () => {
setDragIndex(null);
};
// Переключение состояния
const handleSwitch = (index: number) => {
const newDisabledItems = disabledItems.includes(index)
? disabledItems.filter((i: number) => i !== index)
: [...disabledItems, index];
updateSetting('table-columns-disabled', newDisabledItems);
};
// Закрытие с сохранением
const handleSave = () => {
openModal(ModalMap['10']);
};
useEffect(() => {
initializeSettings();
}, [initializeSettings]);
return (
<ModalContainer style={{ height: '35vh', width: '80vw'}}>
<ModalTitle>
<h3>Кастомизация таблицы узлов подсети</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', height: "100%", gap: '24px', padding: 0 }}>
<StyledTableList>
{items.map((item:string, index:number) => (
<StyledTableItem
key={index}
className={disabledItems.includes(index) ? 'off' : ''}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => {
e.preventDefault();
handleDragOver(index);
}}
onDragEnd={handleDrop}
onClick={() => handleSwitch(index)}
>
{item}
</StyledTableItem>
))}
</StyledTableList>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title='Назад'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};
export default CustomizeNodes;

View file

@ -8,6 +8,7 @@ import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork"; import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm"; import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
import {Settings} from "../settings/Settings"; import {Settings} from "../settings/Settings";
import CustomizeNodes from "./modalContainers/customizeNodes";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,
@ -80,6 +81,9 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
onClose={closeLoader} onClose={closeLoader}
/> />
)} )}
{type === ModalMap['11'] && (
<CustomizeNodes />
)}
</ModalWindow > </ModalWindow >
} }
</> </>

View file

@ -32,6 +32,7 @@ export const ModalTitle = styled.div`
padding: 20px; padding: 20px;
border-bottom: 1px solid #9D9D9D; border-bottom: 1px solid #9D9D9D;
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
h3{ h3{

View file

@ -10,4 +10,6 @@ export const ModalMap: ModalType = {
'7': 'noteWifi', '7': 'noteWifi',
'8': 'connectNetwork', '8': 'connectNetwork',
'9': 'disconnectConfirm', '9': 'disconnectConfirm',
"10": 'settings',
'11': 'customNodes',
} }

View file

@ -6,6 +6,8 @@ import {buttonNames} from "../../const/buttons/names";
import SettingSelect from "./SettingSelect"; import SettingSelect from "./SettingSelect";
import {MainButton} from "../UI/buttons/mainButton"; import {MainButton} from "../UI/buttons/mainButton";
import React from 'react'; import React from 'react';
import {useModalContext} from "../../store";
import {ModalMap} from "../modalwindows/windowsType";
const SettingBlock = styled.div` const SettingBlock = styled.div`
display: flex; display: flex;
@ -19,6 +21,7 @@ const SettingBlock = styled.div`
font-size: 16px; font-size: 16px;
outline: none; outline: none;
background-color: ${props => props.theme.primary}; background-color: ${props => props.theme.primary};
color: ${props => props.theme.textColor};
border: 1px solid ${props => props.theme.borderColor}; border: 1px solid ${props => props.theme.borderColor};
padding: 6px 8px; padding: 6px 8px;
border-radius: 4px; border-radius: 4px;
@ -73,7 +76,7 @@ const InputWrapper = styled.div`
gap: 6px; gap: 6px;
` `
const ErrorText = styled.div` const ErrorText = styled.div`
color: red; color: ${props => props.theme.errorColor};
font-size: 12px; font-size: 12px;
`; `;
@ -87,7 +90,17 @@ interface SettingItemProps {
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => { const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
const theme = useTheme(); const theme = useTheme();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { openModal } = useModalContext();
const handleButtonAction = () => {
if (setting.id === 'customize-nodes') {
openModal(ModalMap['11']);
} else if (setting.action) {
setting.action();
}
};
// Изменение числа
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value; const inputValue = e.target.value;
let newError: string | null = null; let newError: string | null = null;
@ -172,9 +185,10 @@ const SettingItem = React.memo(({ setting, value, onChange, onValidationError }:
type="button" type="button"
size="small" size="small"
title={setting.defaultValue || ''} title={setting.defaultValue || ''}
onClick={setting.action} onClick={handleButtonAction}
/> />
)} )}
{setting.type === 'hidden' && null}
</SettingBlock> </SettingBlock>
); );
}); });

View file

@ -6,8 +6,6 @@ import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
import {SettingItem} from "./SettingItem"; import {SettingItem} from "./SettingItem";
import {IconComponent} from "../../const/buttons/icons"; import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names"; import {buttonNames} from "../../const/buttons/names";
import {ModalMap} from "../modalwindows/windowsType";
import {workSpaceNames} from "../../const/workspace/names";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@ -129,7 +127,7 @@ export const Settings = ({ onClose }: Props) => {
}, [discardChanges, onClose]); }, [discardChanges, onClose]);
return ( return (
<ModalContainer style={{ height: '60vh', width: '50vw'}}> <ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle> <ModalTitle>
<h3>Настройки</h3> <h3>Настройки</h3>
<CloseBtn> <CloseBtn>
@ -168,6 +166,13 @@ export const Settings = ({ onClose }: Props) => {
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton
type="button"
size="small"
title='Отменить'
onClick={handleCancel}
/>
<MainButton <MainButton
type="button" type="button"
size="small" size="small"

View file

@ -1,17 +1,21 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useMemo, useState} from 'react';
import { DeviceUser } from '../../../../../../store/useDeviceStore'; import { DeviceUser } from '../../../../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components'; import styled from 'styled-components';
import {IconComponent} from "../../../../../../const/buttons/icons"; import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names"; import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType"; import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store"; import {useModalContext} from "../../../../../../store";
import Pagination from "../../../../../UI/Pagination"; import Pagination from "../../../../../UI/Pagination";
import {useSettingsStore} from "../../../../../../store/settings/settingsStore"; import {
import {useNameSetting, usePaginateSetting} from "../../../../../../hooks/useSettings"; useDisabledColumns,
useNameSetting,
useOrdersColumns,
usePaginateSetting
} from "../../../../../../hooks/useSettings";
interface TableScannerProps {
devices: DeviceUser[];
}
const TableContainer = styled.div` const TableContainer = styled.div`
width: 100%; width: 100%;
@ -67,6 +71,7 @@ const StatusBadge = styled.span<{ status:string }>`
const TableScanner = ({devices}:TableScannerProps) => { const TableScanner = ({devices}:TableScannerProps) => {
const { openModal } = useModalContext(); const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => { const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
e.stopPropagation(); e.stopPropagation();
@ -75,7 +80,27 @@ const TableScanner = ({devices}:TableScannerProps) => {
currentNote: device.note || '' currentNote: device.note || ''
}); });
}; };
const nameSetting = useNameSetting();
// Применение настроек отображения
const ordersColumns = useOrdersColumns();
const disabledColumns = useDisabledColumns();
const columnVisibility = useMemo(() => {
return {
status: !disabledColumns.includes(ordersColumns.indexOf('Онлайн')),
name: !disabledColumns.includes(ordersColumns.indexOf('Имя')),
vendor: !disabledColumns.includes(ordersColumns.indexOf('Производитель')),
ip: !disabledColumns.includes(ordersColumns.indexOf('IP адрес')),
ports: !disabledColumns.includes(ordersColumns.indexOf('Открытые порты')),
activity: !disabledColumns.includes(ordersColumns.indexOf('Последняя активность')),
note: !disabledColumns.includes(ordersColumns.indexOf('Заметка')),
action: !disabledColumns.includes(ordersColumns.indexOf('Действие'))
};
}, [ordersColumns, disabledColumns]);
const sortedColumns = useMemo(() => {
return ordersColumns.filter(column =>
!disabledColumns.includes(ordersColumns.indexOf(column))
);
}, [ordersColumns, disabledColumns]);
// Пагинация // Пагинация
const paginateSetting = usePaginateSetting(); const paginateSetting = usePaginateSetting();
@ -106,31 +131,41 @@ const TableScanner = ({devices}:TableScannerProps) => {
<StyledTable> <StyledTable>
<thead> <thead>
<tr> <tr>
<TableHeaderCell></TableHeaderCell> {columnVisibility.status && <TableHeaderCell></TableHeaderCell>}
<TableHeaderCell>Имя</TableHeaderCell> {columnVisibility.name && <TableHeaderCell>Имя</TableHeaderCell>}
<TableHeaderCell>Производитель</TableHeaderCell> {columnVisibility.vendor && <TableHeaderCell>Производитель</TableHeaderCell>}
<TableHeaderCell>IP адрес</TableHeaderCell> {columnVisibility.ip && <TableHeaderCell>IP адрес</TableHeaderCell>}
<TableHeaderCell>Открытые порты</TableHeaderCell> {columnVisibility.ports && <TableHeaderCell>Открытые порты</TableHeaderCell>}
<TableHeaderCell>Последняя активность</TableHeaderCell> {columnVisibility.activity && <TableHeaderCell>Последняя активность</TableHeaderCell>}
<TableHeaderCell>Заметка</TableHeaderCell> {columnVisibility.note && <TableHeaderCell>Заметка</TableHeaderCell>}
<TableHeaderCell></TableHeaderCell> {columnVisibility.action && <TableHeaderCell></TableHeaderCell>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{currentDevices.map((item, i) => ( {currentDevices.map((item, i) => (
<TableRow key={i}> <TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell> {columnVisibility.status && (
<TableCell>{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell> <TableCell><StatusBadge status={item.status} /></TableCell>
<TableCell>{item.vendor}</TableCell> )}
<TableCell>{item.ip}</TableCell> {columnVisibility.name && (
<TableCell>
{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}
</TableCell>
)}
{columnVisibility.vendor && <TableCell>{item.vendor}</TableCell>}
{columnVisibility.ip && <TableCell>{item.ip}</TableCell>}
{columnVisibility.ports && (
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell> <TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell> )}
<TableCell>{item.note || '-'}</TableCell> {columnVisibility.activity && <TableCell>{item.last_seen}</TableCell>}
{columnVisibility.note && <TableCell>{item.note || '-'}</TableCell>}
{columnVisibility.action && (
<TableCell> <TableCell>
<button onClick={(e) => openModalNote(e, item)}> <button onClick={(e) => openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" /> <IconComponent name={buttonNames[16]} width="26" height="26" />
</button> </button>
</TableCell> </TableCell>
)}
</TableRow> </TableRow>
))} ))}
</tbody> </tbody>

View file

@ -144,7 +144,7 @@ export default function UserScanner() {
} }
setScanInitiated(true); setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress); const res = await NetworkService.scanUser(ipAddress);
console.log(res); console.log(res)
setDevices(res.data.devices || []); setDevices(res.data.devices || []);
} catch (err) { } catch (err) {
console.error("Ошибка сканирования сети:", err); console.error("Ошибка сканирования сети:", err);

View file

@ -137,7 +137,7 @@ const ScanBtn = styled.button<{scanInitiated: boolean}>`
export function WifiScanner() { export function WifiScanner() {
//const { ipAddress } = useIpStore(); //const { ipAddress } = useIpStore();
const ipAddress = '192.168.1.147' const ipAddress = '192.168.1.148'
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false); const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore(); const { wifiNetworks, setWifiNetworks } = useNetworkStore();

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false) setHasInited(false)
initializationAttempt.current = 0; initializationAttempt.current = 0;
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`); const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
let connectionTimeout; let connectionTimeout;
setSocket(newSocket); setSocket(newSocket);

View file

@ -69,7 +69,7 @@ export const ToolBar = () => {
</IconContainer> </IconContainer>
</Button> </Button>
<ThemeButton /> {/*<ThemeButton />*/}
<Button> <Button>
<IconContainer> <IconContainer>

View file

@ -115,7 +115,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => { const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`); const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
testSocket.close(); testSocket.close();
resolve(false); resolve(false);

View file

@ -2,4 +2,8 @@ interface Window {
electron: { electron: {
quitApp: () => void; quitApp: () => void;
}; };
electronAPI: {
showSaveDialog: (options: Electron.SaveDialogOptions) => Promise<Electron.SaveDialogReturnValue>;
writeFile: (path: string, data: string) => Promise<{ success: boolean }>;
};
} }

View file

@ -8,6 +8,11 @@ export const usePaginateSetting = () => {
return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20)); return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20));
}; };
export const useThemeSettings = () => {
return useSettingsStore(state => state.getSettingValue('theme'));
};
//Интервальное сканирование под узлов
export const useIntervalBoolean = () => { export const useIntervalBoolean = () => {
return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean; return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean;
}; };
@ -15,6 +20,15 @@ export const useIntervalTime = () => {
return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60)); return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60));
}; };
// Кастомизация таблиц
export const useOrdersColumns = () => {
return useSettingsStore(state => state.getSettingValue('table-columns-order')) as string[];
};
export const useDisabledColumns = () => {
return useSettingsStore(state => state.getSettingValue('table-columns-disabled')) as number[];
};
export const useThemeSetting = () => { export const useThemeSetting = () => {
return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system'; return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system';
}; };

View file

@ -9,7 +9,7 @@ interface IpAddressState {
export const useIpStore = create<IpAddressState>()( export const useIpStore = create<IpAddressState>()(
persist( persist(
(set)=>({ (set)=>({
ipAddress: '192:168.1.147', ipAddress: '192:168.1.0',
setIpAddress: (newIp: string) => set({ ipAddress: newIp }), setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
}), }),
{ {

View file

@ -1,10 +1,25 @@
export const networkSettings = [ import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNetworkSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{ {
id: 'clean-network', id: 'clean-network',
type: 'button', type: 'button',
label: 'Очистить данные сессии', label: 'Очистить данные сессии',
sectionId: 'Точки доступа', sectionId: 'Точки доступа',
defaultValue: 'Очистить', defaultValue: 'Очистить',
action: async ()=>{
try {
const res = await NetworkService.clearNetwork(ipAddress);
console.log(res);
}catch (e){
console.error(e);
alert('Данные не были удалены: '+ e)
}
},
}, },
{ {
id: 'export-network', id: 'export-network',
@ -14,3 +29,6 @@ export const networkSettings = [
defaultValue: 'Экспортировать', defaultValue: 'Экспортировать',
}, },
] as const; ] as const;
};
export const networkSettings = createNetworkSettings();

View file

@ -1,4 +1,11 @@
export const nodesSettings = [ import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNodesSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{ {
id: 'rescan-nodes', id: 'rescan-nodes',
sectionId: 'Узлы подсети', sectionId: 'Узлы подсети',
@ -47,13 +54,67 @@ export const nodesSettings = [
label: 'Очистить данные сессии', label: 'Очистить данные сессии',
sectionId: 'Узлы подсети', sectionId: 'Узлы подсети',
defaultValue: 'Очистить', defaultValue: 'Очистить',
action: async ()=>{
try {
const res = await NetworkService.clearSubnet(ipAddress);
console.log(res)
}catch (e){
console.error(e);
alert('Данные не были удалены: '+ e)
}
},
}, },
{ {
id: 'export-nodes', id: "export-nodes",
type: 'button', type: "button",
label: 'Экспорт данных узлов', label: "Экспорт данных узлов",
sectionId: 'Узлы подсети', sectionId: "Узлы подсети",
defaultValue: 'Экспортировать', defaultValue: "Экспортировать",
}, action: async () => {
] as const; try {
const response = await NetworkService.exportDevices(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: "Сохранить данные узлов",
defaultPath: "nodes_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert("Данные успешно экспортированы!");
}
} catch (e: any) {
alert("Ошибка экспорта: " + e.message);
}
},
},
{
id: 'table-columns-order',
type: 'hidden',
label: '',
sectionId: 'Узлы подсети',
defaultValue: [
'Онлайн',
'Имя',
'Производитель',
'IP адрес',
'Открытые порты',
'Последняя активность',
'Заметка',
'Действие'
]
},
{
id: 'table-columns-disabled',
label: '',
sectionId: 'Узлы подсети',
type: 'hidden',
defaultValue: []
}
] as const;
};
export const nodesSettings = createNodesSettings();

View file

@ -7,14 +7,9 @@ import {appearanceSettings} from "./config/appearanceSettings";
type SettingValue = any; type SettingValue = any;
type SettingsDictionary = Record<string, SettingValue>; type SettingsDictionary = Record<string, SettingValue>;
interface SettingOption {
readonly value: any;
readonly label: string;
}
export type Setting = { export type Setting = {
readonly id: string; readonly id: string;
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button'; readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button' | 'hidden';
readonly label: string; readonly label: string;
readonly defaultValue: any; readonly defaultValue: any;
value?: string; value?: string;

View file

@ -3,10 +3,10 @@ import { ThemeProvider as StyledTheme } from 'styled-components';
import { light, dark} from '../const/theme/theme'; import { light, dark} from '../const/theme/theme';
import { GlobalStyle } from '../const/theme/globalStyles'; import { GlobalStyle } from '../const/theme/globalStyles';
import { themeNames, ThemName } from '../const/theme/names'; import { themeNames, ThemName } from '../const/theme/names';
import {useThemeSettings} from "../hooks/useSettings";
type ThemeContextType = { type ThemeContextType = {
theme: ThemName; theme: ThemName;
toggleTheme: () => void;
}; };
const ThemeContext = createContext<ThemeContextType | undefined>(undefined); const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
@ -23,32 +23,13 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const getSystemTheme = () => const getSystemTheme = () =>
window.matchMedia('(prefers-color-scheme: dark)').matches ? themeNames[2] : themeNames[1]; window.matchMedia('(prefers-color-scheme: dark)').matches ? themeNames[2] : themeNames[1];
const getInitialTheme = () => { const getInitialTheme = useThemeSettings();
let savedTheme = localStorage.getItem('theme') as ThemName;
if (!savedTheme) {
savedTheme = themeNames[3]; // Устанавливаем значение по умолчанию
localStorage.setItem('theme', savedTheme); // Сохраняем без JSON.stringify
}
return savedTheme;
};
const [theme, setTheme] = useState<ThemName>(getInitialTheme); const [theme, setTheme] = useState<ThemName>(getInitialTheme);
const toggleTheme = () => { useEffect(() => {
setTheme((prev) => { setTheme(getInitialTheme)
let newTheme: ThemName; }, [getInitialTheme]);
if (prev === themeNames[3]) {
newTheme = themeNames[1];
} else if (prev === themeNames[1]) {
newTheme = themeNames[2];
} else {
newTheme = themeNames[3];
}
localStorage.setItem('theme', newTheme);
return newTheme;
});
};
useEffect(() => { useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
@ -68,8 +49,8 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const currentTheme = theme === themeNames[3] ? getSystemTheme() : theme; const currentTheme = theme === themeNames[3] ? getSystemTheme() : theme;
return ( return (
<ThemeContext.Provider value={{ theme, toggleTheme }}> <ThemeContext.Provider value={{ theme }}>
<StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark, toggleTheme }}> <StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark }}>
<GlobalStyle /> <GlobalStyle />
{children} {children}
</StyledTheme> </StyledTheme>