Compare commits

...

No commits in common. "main" and "v0.0.2" have entirely different histories.
main ... v0.0.2

721 changed files with 209030 additions and 65200 deletions

1
.pc/.quilt_patches Normal file
View file

@ -0,0 +1 @@
debian/patches

1
.pc/.quilt_series Normal file
View file

@ -0,0 +1 @@
series

1
.pc/.version Normal file
View file

@ -0,0 +1 @@
2

View file

@ -0,0 +1,75 @@
import asyncio
from typing import Dict, Any
from sdbus_async.networkmanager import (
NetworkManager,
NetworkManagerSettings,
NetworkConnectionSettings,
NetworkDeviceGeneric,
)
import sdbus
from sdbus_async.networkmanager.settings import ConnectionProfile
from services.arp_service import NetworkScanner
class IPService:
"""
Сервис проверки доступности IP-адреса и применения настроек сети
"""
def __init__(self):
self.arp_scanner = NetworkScanner()
self.bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(self.bus)
def _reset_bus(self):
""" Перезапускает соединение с системной шиной D-Bus """
new_bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(new_bus)
if hasattr(self, 'bus') and self.bus is not None:
self.bus = None
self.bus = new_bus
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
""" Проверяет доступность IP-адреса перед его применением """
self._reset_bus()
clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
if any(client['ip'] == new_ip for client in clients):
return {"available": False, "message": f"IP address {new_ip} уже занят."}
return {"available": True, "message": "Network profile settings are available for use."}
async def apply_ip_config(self, args: Dict[str, Any]) -> None:
""" Применяет проверенные настройки IP """
self._reset_bus()
nm = NetworkManager()
settings = NetworkManagerSettings()
connection_paths = await settings.get_connections_by_id(args["connection_id"])
if not connection_paths:
raise ValueError(f"Подключение {args['connection_id']} не найдено")
connection = NetworkConnectionSettings(connection_paths[0])
profile = await connection.get_profile()
settings_dict = profile.to_settings_dict()
for key_path, value in args["ipv4_updates"].items():
section, *props = key_path.split('.')
current = settings_dict[section]
for prop in props[:-1]:
current = current.setdefault(prop, {})
current[props[-1]] = value
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Обновлены настройки для {args['connection_id']}:")
interface_name = settings_dict['connection']['interface-name']
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path)
if await device.interface == interface_name:
await device.disconnect()
print(f"Отключён от {interface_name}")
await nm.activate_connection(connection=connection_paths[0], device=device_path)
print(f"Переподключён к {args['connection_id']}")
return
raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")

View file

@ -0,0 +1,112 @@
from sdbus_async.networkmanager import NetworkManager, NetworkManagerSettings, NetworkConnectionSettings, NetworkDeviceGeneric, ActiveConnection
import asyncio
from sdbus_async.networkmanager.settings import ConnectionProfile
from typing import Dict, Any
import sdbus
from services.arp_service import NetworkScanner
class IPService:
"""
Сервис проверки доступности IP-адреса и применения настроек сети
"""
def __init__(self):
self.arp_scanner = NetworkScanner()
self.bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(self.bus)
def _reset_bus(self):
""" Перезапускает соединение с системной шиной D-Bus """
new_bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(new_bus)
if hasattr(self, 'bus') and self.bus is not None:
self.bus = None
self.bus = new_bus
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
""" Проверяет доступность IP-адреса перед его применением """
self._reset_bus()
clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
if any(client['ip'] == new_ip for client in clients):
return {"available": False, "message": f"IP address {new_ip} уже доступен."}
return {"available": True, "message": "Network profile settings are available for use."}
async def apply_ip_config(self, args: Dict[str, Any]) -> None:
""" Применяет проверенные настройки IP """
self._reset_bus()
nm = NetworkManager(self.bus)
settings = NetworkManagerSettings(self.bus)
connection_paths = await settings.get_connections_by_id(args["connection_id"])
if not connection_paths:
raise ValueError(f"Подключение {args['connection_id']} не найдено")
connection = NetworkConnectionSettings(connection_paths[0], self.bus)
profile = await connection.get_profile()
settings_dict = profile.to_settings_dict()
# Обновляем настройки IPv4
for key_path, value in args["ipv4_updates"].items():
section, *props = key_path.split('.')
current = settings_dict[section]
for prop in props[:-1]:
current = current.setdefault(prop, {})
current[props[-1]] = value
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Обновлены настройки для {args['connection_id']}:")
# Проверяем interface-name
interface_name = settings_dict['connection'].get('interface-name')
if not interface_name:
# Ищем устройство, связанное с connection_id через активное соединение
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path, self.bus)
active_connection = await device.active_connection
if active_connection != '/':
active_conn = ActiveConnection(active_connection, self.bus)
conn_path = await active_conn.connection # Путь к настройкам соединения
if conn_path != '/':
conn_settings = NetworkConnectionSettings(conn_path, self.bus)
active_profile = await conn_settings.get_profile()
if active_profile.to_settings_dict()['connection']['id'] == args["connection_id"]:
interface_name = await device.interface
settings_dict['connection']['interface-name'] = interface_name
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
break
if not interface_name:
# Если не нашли активное соединение, пробуем получить интерфейс из настроек устройства
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path, self.bus)
applied_connection = await device.get_applied_connection()
connection_settings = applied_connection[0]
conn_id = connection_settings.get('connection', {}).get('id', ('s', ''))[1]
if conn_id == args["connection_id"]:
interface_name = await device.interface
settings_dict['connection']['interface-name'] = interface_name
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
break
if not interface_name:
raise ValueError(f"Не удалось определить interface-name для {args['connection_id']}")
# Переподключаем устройство
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path, self.bus)
if await device.interface == interface_name:
await device.disconnect()
print(f"Отключён от {interface_name}")
await nm.activate_connection(connection=connection_paths[0], device=device_path)
print(f"Переподключён к {args['connection_id']}")
return
raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")

2
.pc/applied-patches Normal file
View file

@ -0,0 +1,2 @@
1
2

View file

@ -1,137 +0,0 @@
when:
event: manual
steps:
checkout:
image: debian:latest
commands:
# ПОДГОТОВКА ГИТА И КЛОНИРОВАНИЕ GONETS
- apt update
- apt install -y git git-lfs
- git lfs install
# УСТАНОВКА НЕОБХОДИМЫХ ЗАВИСИМОСТЕЙ НА СИСТЕМЕ
- apt update
- apt install -y python3 curl findutils grep sed openssh-server tree libnss3 libnss3-dev libnspr4 libnspr4-dev libasound2 libasound2-dev python3.11-venv python3-pip python3-poetry nodejs npm debhelper devscripts build-essential fakeroot dh-make dpkg-dev cmake libgirepository1.0-dev gobject-introspection libcairo2-dev pkg-config python3-dev libdbus-1-dev
- python3 --version
- node -v
- npm -v
raspberry
# ПОДГОТОВКА СПИСКА PYTHON ЗАВИСИМОСТЕЙ GONETS
- rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
- rm -rf /root/.local/share/pypoetry
- rm -rf /root/.poetry
- rm -rf /usr/bin/poetry
- pip install pipx
- pipx ensurepath
- pipx install poetry
- echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
- . /root/.bashrc
- poetry --version
- cd SRV
- poetry env activate
- poetry add poetry-plugin-export
- poetry install
- poetry self update
- rm -f requirements.txt
- poetry run poetry export -f requirements.txt --output requirements.txt --without-hashes
# ПОДГОТОВКА И СБОРКА FRONTEND ЧАСТИ GONETS
- pwd
- cd ..
- pwd
- cd FRONT/DEV
- pwd
- npm install
- npm run package
- cd dist/gonets-linux-x64/resources/app
- pwd
- npm run build
- sed -i 's#href="/assets/#href="./assets/#g' dist/index.html
- sed -i 's#src="/assets/#src="./assets/#g' dist/index.html
- cd ../../../../..
- pwd
- rm -rf BUILD/*
- cp -r DEV/dist/gonets-linux-x64/* BUILD/
- rm -rf DEV
# ИНИЦИАЛИЗАЦИЯ DEBIAN КОНФИГУРАЦИЙ ДЛЯ СБОРКИ
- cd ..
- pwd
- cd ..
- pwd
- mv gonets gonets-0.1
- cd gonets-0.1
- pwd
- ls
- DEBEMAIL="devteam@gossys.dev" DEBFULLNAME="GOSSYS LLC" dh_make --single -y --createorig
- cd debian/
- rm -f *.ex
- rm -f README.*
# КЛОНИРОВАНИЕ НЕОБХОДИМЫХ ШАБЛОНОВ КОНФИГОВ ДЕБИАНА ДЛЯ СБОРКИ
- mkdir -p /woodpecker/src/dev.gossys.dev/HybrydMain/templates
- git clone https://s.galanskiy:${GONETS}@dev.gossys.dev/s.galanskiy/gonets-debian-template.git /woodpecker/src/dev.gossys.dev/HybrydMain/templates
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/changelog /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/control /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/copyright /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/install /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/postinst /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/postrm /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/rules /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
# СБОРКА ДЕБИАН ПАКЕТА
- cd ..
- pwd
- dpkg-buildpackage -us -uc -b
# ИЗВЛЕЧЕНИЕ ВЕРСИИ ПАКЕТА ДЛЯ КОММИТА
- VERSION=$(dpkg-parsechangelog -l./debian/changelog --show-field Version)
- echo "VERSION=$VERSION" > version.env
# КОММИТ И ТЕГ НА ВЕТКУ DEVELOP
- . ./version.env
- git config user.name "s.galanskiy"
- git config user.email "s.galanskiy@gossys.dev"
- git fetch --all --prune
- git branch -r
- git checkout -b "release/$VERSION"
- git commit -am "Релиз $VERSION"
- git tag -f "v$VERSION"
- git push "https://s.galanskiy:b4785198d18200dfed812dc4299575f736255bd7@dev.gossys.dev/HybrydMain/gonets.git" "release/$VERSION"
- git push -f https://s.galanskiy:b4785198d18200dfed812dc4299575f736255bd7@dev.gossys.dev/HybrydMain/gonets.git "v$VERSION"
# СОЗДАНИЕ И ЗАГРУЗКА РЕЛИЗА ЧЕРЕЗ API
- |
VERSION=$(dpkg-parsechangelog -l./debian/changelog --show-field Version)
echo "VERSION=$VERSION" > version.env
# Задать переменные
. ./version.env
TAG="v$VERSION"
DEB_PATH=$(find .. -maxdepth 1 -type f -name "gonets_*_amd64.deb" | head -n 1)
if [ ! -f "$DEB_PATH" ]; then
echo ".deb файл не найден по пути: $DEB_PATH"
exit 1
fi
RELEASE_JSON=$(curl -s -X POST "https://dev.gossys.dev/api/v1/repos/HybrydMain/gonets/releases" \
-H "Authorization: token b4785198d18200dfed812dc4299575f736255bd7" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"Релиз $TAG\", \"body\": \"Сборка .deb пакета для версии $TAG\", \"draft\": false, \"prerelease\": true}")
RELEASE_ID=$(echo "$RELEASE_JSON" | grep '"id":' | head -n1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
if [ -z "$RELEASE_ID" ]; then
echo "Не удалось получить ID релиза из ответа:"
echo "$RELEASE_JSON"
exit 1
fi
curl -s -X POST \
-H "Authorization: token b4785198d18200dfed812dc4299575f736255bd7" \
-F "attachment=@$DEB_PATH" \
"https://dev.gossys.dev/api/v1/repos/HybrydMain/gonets/releases/$RELEASE_ID/assets" || echo "Ошибка загрузки файла"

View file

@ -1,13 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GoNets</title>
<link rel="icon" type="image/x-icon" href="/assets/icon-4fEsvohP.png" />
<script type="module" crossorigin src="/assets/index-QSWEVNb0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DWFkiel1.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -1,8 +1,10 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { NetworkService } from './src/api/request.ts';
import { useIpStore } from './src/store';
import { useEffect } from 'react';
import { useAdminStore } from './src/store/adminStore.ts';
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
import {

View file

@ -21,7 +21,7 @@ function createWindow() {
win.show();
if (process.env.NODE_ENV === 'development') {
win.loadURL('https://localhost:8001');
win.loadURL('http://localhost:3000');
win.webContents.openDevTools();
} else {
win.loadFile(path.join(__dirname, 'dist/index.html'));

View file

@ -11,7 +11,6 @@
},
"dependencies": {
"@iconify/icons-ic": "^1.2.13",
"@iconify/icons-ph": "^1.2.5",
"@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5",

View file

@ -82,79 +82,4 @@ export class NetworkService {
return error.response;
}
}
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices');
} catch (error: any) {
return error.response;
}
}
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/wifi');
} catch (error: any) {
return error.response;
}
}
static async addNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/${type}/${address}/note`, {
note: note
});
} catch (error: any) {
return error.response;
}
}
static async editNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.put(`/subnet/${type}/${address}/note`, {
note: note
});
} catch (error: any) {
return error.response;
}
}
static async deleteNote(ipAddress: string, address:string, type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.delete(`/subnet/${type}/${address}/note`);
} catch (error: any) {
return error.response;
}
}
static async statusNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get(`/subnet/wifi/status`);
} catch (error: any) {
return error.response;
}
}
static async connectNetwork(ipAddress: string, password:string, address: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/connect`, {
ssid: address,
password: password,
});
} catch (error: any) {
return error.response;
}
}
static async disconnectNetwork(ipAddress: string, ssid: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
ssid: ssid,
});
} catch (error: any) {
return error.response;
}
}
}

View file

@ -1,137 +0,0 @@
import React, {useState} from 'react';
import styled, { css } from 'styled-components';
import { DeviceUser } from "../../../store/useDeviceStore";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {IconComponent} from "../../../const/buttons/icons";
import {useModalContext} from "../../../store";
interface DeviceCardProps {
device: DeviceUser;
status: 'online' | 'offline';
}
const CardWrapper = styled.div<{ status: 'online' | 'offline' }>`
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 16px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
`;
const CardHeader = styled.div<{ status: 'online' | 'offline' }>`
background: ${({ status, theme }) =>
status === 'online' ? theme.successColor : theme.offlineColor};
color: white;
padding: 16px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
`;
const CardContent = styled.div<{ isExpanded: boolean }>`
max-height: 0;
opacity: 0;
padding: 0 16px;
transition: all 0.3s ease;
color: #333;
font-size: 14px;
display: grid;
gap: 12px;
${({ isExpanded }) => isExpanded && css`
max-height: 500px;
opacity: 1;
padding: 16px;
`}
`;
const InfoRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Label = styled.span`
font-weight: 500;
color: #666;
`;
const Value = styled.span`
color: #333;
`;
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => setIsExpanded(!isExpanded);
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<CardWrapper status={status} onClick={toggleExpanded}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
<span onClick={openModalNote}>
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
</span>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>Производитель:</Label>
<Value>{device.vendor || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>IP адрес:</Label>
<Value>{device.ip}</Value>
</InfoRow>
{device.open_ports?.length > 0 && (
<InfoRow>
<Label>Открытые порты:</Label>
<Value>{device.open_ports.join(', ')}</Value>
</InfoRow>
)}
<InfoRow>
<Label>Последняя активность:</Label>
<Value>{device.last_seen}</Value>
</InfoRow>
<InfoRow>
<Label>Заметка:</Label>
<Value>{device.note || '-'}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -1,130 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import styled from "styled-components";
import {buttonNames} from "../../../const/buttons/names";
import {IconComponent} from "../../../const/buttons/icons";
type Option = {
value: number,
label: string,
}
interface DropdownTypeProps {
options: Option[];
label?: string;
onChange?: (selectedItem: Option) => void;
value: Option | null;
}
const DropdownWrapper = styled.div`
position: absolute;
top: 18px;
right:20px;
display: inline-block;
background-color: ${({theme}) => theme.background};
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
`
const Label = styled.p`
color: ${({theme}) => theme.textColor};
font-size: 16px;
`
const Dropdown = styled.div`
background-color: ${({theme}) => theme.primary};
border-radius: 15px;
overflow: hidden;
transition: all .3s ease;
cursor: pointer;
z-index: 10;
margin-top: 6px;
`
const DropdownHeader = styled.div`
display: flex;
justify-content: space-between;
gap: 14px;
padding: 12px 24px;
font-size: 16px;
align-items: center;
height: 100%;
min-width: 200px;
`
const DropdownContent = styled.div<{$isOpen: boolean}>`
max-height: ${({$isOpen}) => $isOpen ? '500px' : '0'};
opacity: ${({$isOpen}) => $isOpen ? 1 : 0};
transition: all .3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
`
const DropdownRow = styled.button`
padding: 12px 24px;
background-color: transparent;
text-align: start;
font-size: 16px;
border: none;
width: 100%;
color: ${({theme}) => theme.textColor};
transition: all .3s ease;
cursor: pointer;
&:hover {
background-color: ${({theme}) => theme.background};
}
`
const DropdownArrow = styled.div<{ $isOpen: boolean }>`
transition: transform 0.3s ease;
transform: rotate(${({ $isOpen }) => ($isOpen ? "90deg" : "0deg")});
`;
const DropdownType = ({options, value, onChange, label}: DropdownTypeProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
function toggleOpen() {
setIsOpen((prev) => !prev);
}
function handleSelect(option: Option) {
onChange?.(option);
setIsOpen(false);
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<DropdownWrapper ref={dropdownRef}>
<Label>{label}</Label>
<Dropdown>
<DropdownHeader onClick={toggleOpen}>
<p>{value?.label || 'Select an option'}</p>
<DropdownArrow $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} />
</DropdownArrow>
</DropdownHeader>
<DropdownContent $isOpen={isOpen}>
{options.map((option, i) => (
<DropdownRow key={i} onClick={() => handleSelect(option)}>
{option.label}
</DropdownRow>
))}
</DropdownContent>
</Dropdown>
</DropdownWrapper>
);
};
export default DropdownType;

View file

@ -9,15 +9,15 @@ const Button = styled.button.withConfig({
border: none;
padding: ${(props) =>
props.size === "small"
? "10px 28px"
? "4px 8px"
: props.size === "big"
? "10px 20px"
: "12px 20px"}; // По умолчанию
: "6px 10px"}; // По умолчанию
border-radius: 8px;
color: white;
font-size: ${(props) =>
props.size === "small"
? "14px"
? "12px"
: props.size === "big"
? "16px"
: "14px"}; // По умолчанию
@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
? 'row-reverse'
: 'row'};
gap: 10px;
transition: all .3s ease;
&:hover{
background-color: ${(props) => props.$hoverсolor};
}

View file

@ -5,7 +5,7 @@ type Props = {
placeholderText: string;
id: string;
value?: string;
onChange?: (value: string) => void;
onChange?: (identifier: string, value: string) => void;
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
type?: string;
multiple?: boolean;
@ -33,7 +33,7 @@ const StyledInput = styled.input`
border: 1px solid #adadad;
}
width: 100%;
font-size: 18px;
font-size: 14px;
color: ${props=>props.theme.textColor}
`;
@ -41,7 +41,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id,
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
if (onChange) {
onChange(value);
onChange(id, value);
}
};

View file

@ -2,21 +2,19 @@ import React, { useEffect, useRef } from 'react';
interface SignalSVGProps {
signal: number; // Принимаем signal как пропс
color: string;
size: string;
}
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
const svgRef = useRef<SVGSVGElement | null>(null);
useEffect(() => {
// Устанавливаем цвета в зависимости от signal
const colors = {
'--st1-color': signal >= 1 ? color : 'black',
'--st2-color': signal >= 2 ? color : 'black',
'--st3-color': signal >= 3 ? color : 'black',
'--st4-color': signal >= 4 ? color : 'black',
'--st5-color': signal === 5 ? color : 'black',
'--st1-color': signal >= 1 ? 'white' : 'black',
'--st2-color': signal >= 2 ? 'white' : 'black',
'--st3-color': signal >= 3 ? 'white' : 'black',
'--st4-color': signal >= 4 ? 'white' : 'black',
'--st5-color': signal === 5 ? 'white' : 'black',
};
// Применяем стили к SVG
@ -37,8 +35,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
y="0px"
viewBox="0 0 296 235"
xmlSpace="preserve"
width={size} // Фиксированная ширина
height={size}
width="20px" // Фиксированная ширина
height="20px"
>
<style type="text/css">
{`

View file

@ -1,109 +0,0 @@
import React from 'react';
import { DeviceUser } from '../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components';
import {IconComponent} from "../../../const/buttons/icons";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {useModalContext} from "../../../store";
const TableContainer = styled.div`
width: 100%;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textSecondary};
background: ${({ theme }) => theme.primary};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const TableRow = styled.tr<{ clickable?: boolean }>`
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.background};
}
`;
const TableCell = styled.td`
padding: 16px 24px;
font-size: 14px;
color: ${({ theme }) => theme.text};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
display: inline-block;
border-radius:4px;
`
const TableScanner = ({devices}:TableScannerProps) => {
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Имя</TableHeaderCell>
<TableHeaderCell>Производитель</TableHeaderCell>
<TableHeaderCell>IP адрес</TableHeaderCell>
<TableHeaderCell>Открытые порты</TableHeaderCell>
<TableHeaderCell>Последняя активность</TableHeaderCell>
<TableHeaderCell>Заметка</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</tr>
</thead>
<tbody>
{devices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<TableCell>{item.hostname || 'Неизвестен'}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell>
<TableCell>{item.note || '-'}</TableCell>
<TableCell>
<span onClick={(e)=>openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</span>
</TableCell>
</TableRow>
))}
</tbody>
</StyledTable>
</TableContainer>
);
};
export default TableScanner;

View file

@ -184,7 +184,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipAdress}
id="newIp"
value={ipData.newIp}
onChange={(value) => handleChange('newIp', value)}
onChange={(id, value) => handleChange(id, value)}
onBlur={handleBlur}
onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}}
@ -203,7 +203,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipGate}
id="newGate"
value={ipData.newGate}
onChange={(value) => handleChange('newGate', value)}
onChange={(id, value) => handleChange(id, value)}
onBlur={handleBlur}
onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}}
@ -221,7 +221,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipDns.join(', ')}
id="newDns"
value={ipData.newDns.join(', ')}
onChange={( value) => handleChange('newDns', value)}
onChange={(id, value) => handleChange(id, value)}
onBlur={handleBlur}
onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}}

View file

@ -73,7 +73,7 @@ export const NetWorkNode = ({ data }: CustomNodeProps) => {
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
<Status>
<SignalSVG signal={signal} color={'white'} size={'20px'} />
<SignalSVG signal={signal} />
</Status>
) : (
<div></div>

View file

@ -18,7 +18,7 @@ export const Network = styled.div<Props>`
flex-direction: column;
gap: 10px;
visibility: visible;
overflow: hidden;
p{
margin: 0;
}
@ -29,6 +29,8 @@ export const TitleContainer = styled.div<Props>`
justify-content: space-between;
align-items: center;
background: ${(props) => props.$backgroundcolor};
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: white;
padding: 15px;
height: ${(props) => props.$height ? props.$height : 'unset'};

View file

@ -1,84 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
interface Props {
address: string;
onClose: () => void;
}
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const { ipAddress } = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSave = async () => {
try {
setIsLoading(true);
setError('')
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if(res.status === 200) {
onClose()
}else{
setError(res.data.detail)
}
} catch (error) {
console.error('Error connection:', error);
setError('Ошибка подключения к сети')
}finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{ height: '230px' }}>
<ModalTitle>
<h3>Подключение к сети</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="password"
placeholderText='Введите пароль...'
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
{error}
</p>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title='Подключиться'
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
</ModalContainer>
);
};

View file

@ -1,82 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore();
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedDevices = devices.map(device =>
device.mac === address
? { ...device, note: note.trim() }
: device
);
setDevices(updatedDevices);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '250px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,82 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedNetworks = wifiNetworks.map(network =>
network.ssid === address
? { ...network, note: note.trim() }
: { ...network }
);
setWifiNetworks(updatedNetworks);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '220px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -39,7 +39,7 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
placeholderText='Введите адрес узла...'
id="newIp"
value={ipAddress}
onChange={(value) => setIpAddress(value)}
onChange={(id, value) => setIpAddress(value)}
/>
{error && (
<p style={{ color: 'red', marginTop: '5px' }}>

View file

@ -1,54 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useState} from "react";
import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
type Props={
address: string,
onClose: () => void
}
export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const handleDisconnect = async () => {
try {
setIsLoading(true);
const res = await NetworkService.disconnectNetwork(ipAddress, address);
if(res.status === 200) {
onClose()
}
} catch (error) {
console.error('Error connection:', error);
}finally {
setIsLoading(false);
}
};
return(
<ModalContainer>
<ModalTitle>
<h3>Отключение от Wi-Fi сети</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Подтверждение отключения</h3>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Отменить" onClick={onClose} size="small"/>
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
</ModalContainer>
)
}

View file

@ -110,16 +110,14 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
try {
const result = await NetworkService.saveIp(ipAddress, data);
if (result.status === 200) {
if(ipAddress!=='localhost'){
setIpAddress(ipData.newIp)
setIpAddress(ipData.newIp)
const updatedDevices = devices.map(device =>
device.address === ipData.currentIp
? { ...device, address: ipData.newIp }
: device
);
useDevicesStore.setState({ devices: updatedDevices });
}
const updatedDevices = devices.map(device =>
device.address === ipData.currentIp
? { ...device, address: ipData.newIp }
: device
);
useDevicesStore.setState({ devices: updatedDevices });
onClose()
}

View file

@ -87,7 +87,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
placeholderText='Введите пароль администратора...'
id="password"
value={user.password}
onChange={(value) => handleChange('password', value)}
onChange={(id, value) => handleChange(id, value)}
/>
{error && (

View file

@ -3,10 +3,6 @@ import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalCon
import { ModalMap } from "./windowsType";
import Loader from "../UI/loader";
import { ModalWindow } from "./style";
import {NoteDevice} from "./modalContainers/NoteDevice";
import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
type Props = {
isOpen: boolean,
@ -39,34 +35,7 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
message={data.message}
/>
}
{type === ModalMap['6'] && (
<NoteDevice
type={'devices'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['7'] && (
<NoteWifi
type={'wifi'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['8'] && (
<ConnectNetwork
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['9'] && (
<DisconnectConfirm
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['4'] && (
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
)}

View file

@ -24,7 +24,7 @@ export const ModalContainer = styled.div<{height?: string}>`
flex-direction: column;
justify-content: space-between;
gap: 15px;
height: ${(props) => props.height ? props.height : '230px'};
height: ${(props) => props.height ? props.height : '200px'};
width: 520px;
`
@ -66,5 +66,5 @@ export const ButtonContainer = styled.div`
justify-content: flex-end;
display: flex;
gap: 10px;
padding: 10px 20px 20px;
padding: 20px;
`

View file

@ -5,9 +5,5 @@ export const ModalMap: ModalType = {
'2': 'loader',
'3': 'connectionError',
'4': 'addDevice',
'5': 'sudo',
'6': 'noteDevice',
'7': 'noteWifi',
'8': 'connectNetwork',
'9': 'disconnectConfirm',
'5': 'sudo'
}

View file

@ -101,6 +101,7 @@ export const CreateUser = () => {
if (result.status === 200) { window.location.reload() }
} catch (err) {
console.error('Error:', err);
} finally {
}
}
@ -145,7 +146,7 @@ export const CreateUser = () => {
placeholderText='Введите пароль...'
id="password"
value={user.password}
onChange={(value) => handleChange('password', value)}
onChange={(id, value) => handleChange(id, value)}
/>
<Input
@ -153,7 +154,7 @@ export const CreateUser = () => {
placeholderText='Повторите пароль...'
id="repeatPassword"
value={repeatPassword}
onChange={(value) => handleChange('repeatPassword', value)}
onChange={(id, value) => handleChange(id, value)}
style={{ marginTop: '10px' }}
/>
{!passwordsMatch && (

View file

@ -1,82 +1,82 @@
import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg');
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
`
const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg');
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
`
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 52px;
margin-bottom: 100px;
`
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 52px;
margin-bottom: 100px;
`
const ButtonWrapper = styled.div`
display: flex;
gap: 75px;
flex-direction: column;
`
const ButtonWrapper = styled.div`
display: flex;
gap: 75px;
flex-direction: column;
`
const ButtonContainer = styled.div`
display: flex;
gap: 15px;
flex-direction: column;
`
const ButtonContainer = styled.div`
display: flex;
gap: 15px;
flex-direction: column;
`
const Title = styled.div`
display: flex;
align-items: flex-end;
gap: 10px;
const Title = styled.div`
display: flex;
align-items: flex-end;
gap: 10px;
h1{
font-size: 52px;
}
`
export const HelloWindow = () => {
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={()=>setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
h1{
font-size: 52px;
}
`
export const HelloWindow = () => {
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={()=>setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
}

View file

@ -1,73 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online');
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (
<div style={{
display: 'flex',
width: '70%',
gap: '20px',
}}>
{/* Колонка активных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные устройства ({activeDevices.length})</h3>
{activeDevices.map((device, index) => (
<DeviceCard
key={index}
status="online"
device={device}
/>
))}
</div>
{/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные устройства ({inactiveDevices.length})</h3>
{inactiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="offline"
device={device}
/>
))}
</div>
</div>
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -1,45 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import TableScanner from "../../../../../UI/tables/TableScanner";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -1,191 +0,0 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import { 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";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
width: max-content;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.accent};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
export default function UserScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
// Сканирование
const handleScan = async () => {
try {
setLoading(true);
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress);
console.log(res);
setDevices(res.data.devices || []);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([])
} finally {
setLoading(false);
}
};
// Способ отображения
const viewOptions: Option[] = [
{ value: 1, label: 'Таблица' },
{ value: 2, label: 'Список' },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
setSelectedView(option);
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование пользователей в сети</TitleWrapper>)}
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
)}
{loading ? (
<LoadingText>
Сканирование сети<ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);
}

View file

@ -1,199 +0,0 @@
import styled, { keyframes } from "styled-components";
import React, { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store";
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:center ;
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: scale(1);
.circel {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
const ScanBtn = styled.button<{scanInitiated: boolean}>`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: transform .4s ease;
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")};
&:hover{
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")} scale(1.03);
}
`
export function WifiScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const handleScan = async () => {
setWifiNetworks([]);
setCurrentNetwork(null)
setLoading(true);
setScanInitiated(true);
try {
const res = await NetworkService.scanWifi(ipAddress);
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setWifiNetworks([]);
} finally {
setLoading(false);
}
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование Wi-Fi сети</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
<span className="ring1 circel"></span>
<span className="ring2 circel"></span>
<span className="ring3 circel"></span>
{wifiNetworks.map((network) => (
<WifiDot
key={network.bssid}
network={network}
onSelect={setCurrentNetwork}
/>
))}
{currentNetwork && (
<ModalWifiScaner
onClose={() => setCurrentNetwork(null)}
network={currentNetwork}
/>
)}
</UserLocate>
{loading ? (
<LoadingText>
Сканирование сети <ScanningText/>
</LoadingText>
) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
)}
</Wrapper>
);
}

View file

@ -1,225 +0,0 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import {WifiNetworkScanner} from "./WifiDot";
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useIpStore, useModalContext} from "../../../../../../store";
import {Status} from "../../../../../customeNodes/nodes/networkInterface/networkNode/style";
import SignalSVG from "../../../../../UI/signalSvg";
import { useTheme } from 'styled-components';
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
import {NetworkService} from "../../../../../../api/request";
const Modal = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
background-color: ${(props) => props.theme.secondary};
border-radius: 24px;
padding: 32px 28px;
bottom: -40%;
left: 220px;
position: absolute;
z-index: 20;
min-width: 300px;
&:hover .close-btn{
opacity: 1;
visibility: visible;
}
`
const CloseBtn = styled.div`
position: absolute;
top: -20px;
right: -20px;
cursor: pointer;
background-color: ${(props) => props.theme.primary};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
transition: all .4s ease;
opacity: 0;
visibility: visible;
&:hover {
background-color: ${(props) => props.theme.hoverBtn};
}
`
const ModalTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
line-height: 1;
margin-bottom: 32px;
h4{
font-weight: normal;
}
.functionalIcons{
display: flex;
justify-content: center;
align-items: center;
gap: 18px;
}
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
justify-content: space-between;
align-items: center;
width: 100%;
`
const ModalRow = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
gap: 12px;
width: 100%;
font-size: 14px;
h6{
margin: 0;
padding: 0;
font-size: 1em;
}
p{
max-width:180px;
text-align: right;
}
`
const ModalConnect = styled.button`
color: ${(props) => props.theme.primary};
padding: 10px 12px;
font-size: 18px;
background-color: ${(props) => props.theme.accent};
border-radius: 12px;
margin-top: 4px;
border: none;
outline: none;
width: 100%;
cursor: pointer;
transition: .4s ease;
&:hover{
background-color: ${(props) => props.theme.accentHover};
}
`
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
const { openModal } = useModalContext();
const { ipAddress } = useIpStore();
const theme = useTheme()
const [signal, setSignal] = useState(1);
const { wifiNetworks } = useNetworkStore();
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const getStatus = async () => {
try {
const response = await NetworkService.statusNetwork(ipAddress);
const { status, ssid } = response.data;
setIsConnected(
!!network?.ssid &&
ssid === network.ssid &&
status === 'connected'
);
} catch (error) {
setIsConnected(false);
}
};
getStatus();
}, [network?.ssid, ipAddress]);
const currentNetwork = wifiNetworks.find(n => n.bssid === network.bssid) || network;
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['7'], {
address: currentNetwork.ssid,
currentNote: currentNetwork.note || ''
});
};
const openModalNetwork= (e: React.MouseEvent) => {
e.stopPropagation();
if(isConnected){
openModal(ModalMap['9'], {
address: currentNetwork.ssid,
});
}else{
openModal(ModalMap['8'], {
address: currentNetwork.ssid,
});
}
};
useEffect(() => {
const perc = Number(network.signal);
switch (true) {
case perc >= -50: setSignal(5); break;
case perc >= -60: setSignal(4); break;
case perc >= -70: setSignal(3); break;
case perc >= -80: setSignal(2); break;
default: setSignal(1); break;
}
}, [network.signal])
return (
<Modal>
<CloseBtn className={'close-btn'} onClick={onClose}>
<IconComponent name={buttonNames[12]} width="30" height="30" />
</CloseBtn>
<ModalTitle>
<h4>{network.ssid}</h4>
<div className={'functionalIcons'}>
<span onClick={openModalNote} style={{cursor: "pointer"}}>
<IconComponent name={buttonNames[16]} width="30" height="30" />
</span>
<Status>
<SignalSVG signal={signal} size={'36px'} color={theme.accent} />
</Status>
</div>
</ModalTitle>
<ModalContent>
<ModalRow>
<h6>BSSID:</h6>
<p>{network.bssid}</p>
</ModalRow>
<ModalRow>
<h6>Производитель:</h6>
<p>{network.vendor}</p>
</ModalRow>
<ModalRow>
<h6>Сигнал:</h6>
<p>{network.signal} dBm</p>
</ModalRow>
<ModalRow>
<h6>Канал:</h6>
<p>{network.channel}</p>
</ModalRow>
<ModalRow>
<h6>Шифрование:</h6>
<p>{network.encryption}</p>
</ModalRow>
<ModalRow>
<h6>Частота:</h6>
<p>{network.frequency}</p>
</ModalRow>
<ModalRow>
<h6>Скрытая сеть:</h6>
<p>{network.hidden ? 'Да' : 'Нет'}</p>
</ModalRow>
<ModalRow>
<h6>Заметка:</h6>
<p>{currentNetwork.note ? currentNetwork.note : '-'}</p>
</ModalRow>
<ModalConnect onClick={openModalNetwork}>{isConnected ? 'Отключиться' : 'Подключиться'}</ModalConnect>
</ModalContent>
</Modal>
);
};
export default ModalWifiScaner;

View file

@ -1,79 +0,0 @@
import React from 'react';
import styled from "styled-components";
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
//Точка вайфай
const Dot = styled.div<{ top: string; left: string }>`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
transform: translate(-50%, -50%);
user-select: none;
transition: .4s;
text-wrap: nowrap;
&:hover {
transform: translate(-50%, -50%) scale(1.1);
}
`;
const WifiDot = ({ network, onSelect }: { network: WifiNetworkScanner, onSelect: (n: WifiNetworkScanner) => void }) => {
const getPositionInRing = (bssid: string, ringSize: number) => {
const hash = bssid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const randomAngle = (hash % 360) * (Math.PI / 180);
const innerRadius = ringSize * 0.4;
const outerRadius = ringSize * 0.5;
const randomValue = (hash % 1000) / 1000;
const randomRadius = Math.sqrt(innerRadius * innerRadius + randomValue * (outerRadius * outerRadius - innerRadius * innerRadius));
return {
x: Math.cos(randomAngle) * randomRadius,
y: Math.sin(randomAngle) * randomRadius
};
};
const getPosition = (signal: number, bssid: string) => {
let strength: 1 | 2 | 3;
if (signal >= -50) {
strength = 3;
} else if (signal >= -70) {
strength = 2;
} else if (signal >= -90) {
strength = 1;
} else {
strength = 1;
}
const ringSizes = { 1: 550, 2: 400, 3: 250 };
const ringSize = ringSizes[strength];
const offset = getPositionInRing(bssid, ringSize);
return {
top: `calc(50% - ${offset.y}px)`,
left: `calc(50% - ${offset.x}px)`
};
};
const position = getPosition(network.signal, network.bssid);
return (
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
{network.ssid}
</Dot>
);
};
export default WifiDot;

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false)
initializationAttempt.current = 0;
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
let connectionTimeout;
setSocket(newSocket);
@ -192,13 +192,13 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
}, [nodes, loader])
const onEdgesChange = useCallback(
(changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
(changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
);
const onConnect = useCallback(
(connection: any) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
(connection: any) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
const handleNodesChange = (changes: any) => {
@ -254,29 +254,29 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
}, [isNullPos]);
return (
<>
<>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
style={{ background: '#F7F9FB' }}
minZoom={0}
maxZoom={4}
fitView
fitViewOptions={{ padding: 0.1 }}
attributionPosition="bottom-left"
snapToGrid
snapGrid={[20, 20]}
panOnDrag={!isPanDisabled}
nodeTypes={nodeTypes}
proOptions={proOptions}
>
<Background gap={20} size={1} bgColor={theme.background} />
<Controls fitViewOptions={{ padding: 0.1 }} />
</ReactFlow>
</>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
style={{ background: '#F7F9FB' }}
minZoom={0}
maxZoom={4}
fitView
fitViewOptions={{ padding: 0.1 }}
attributionPosition="bottom-left"
snapToGrid
snapGrid={[20, 20]}
panOnDrag={!isPanDisabled}
nodeTypes={nodeTypes}
proOptions={proOptions}
>
<Background gap={20} size={1} bgColor={theme.background} />
<Controls fitViewOptions={{ padding: 0.1 }} />
</ReactFlow>
</>
);
};

View file

@ -50,19 +50,10 @@ export const ToolBar = () => {
{toolBarMenu && toolBarMenu.map((menu) => (
<ToolBarButton menuId={menu.id} key={menu.id} />
))}
<Button>
<IconContainer>
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[5])} />
</IconContainer>
</Button>
<Button>
<IconContainer>
<IconComponent name={buttonNames[15]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[6])} />
</IconContainer>
</Button>
</ButtonContainer>
<ButtonContainer>
<Button>
<IconContainer>
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />

View file

@ -18,7 +18,7 @@ const Container = styled.div`
align-items: center;
gap: 15px;
padding: 20px;
width: 210px;
p{
margin: 0;
}
@ -115,12 +115,12 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => {
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
const timeout = setTimeout(() => {
testSocket.close();
resolve(false);
}, 500
);
testSocket.close();
resolve(false);
}, 500
);
testSocket.onopen = () => {
clearTimeout(timeout);
@ -196,12 +196,12 @@ export const DevicesBar = () => {
<DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Корневой адрес</p>
<p>Текущее устройство</p>
</DeviceControl>
{currentDevice.hostname == '' ? (
<Text className="paddingP">Название: Загрузка<AnimDot /></Text>
<Text className="paddingP">Адрес: Загрузка<AnimDot /></Text>
) : (
<p className="paddingP">Название: {currentDevice.hostname}</p>
<p className="paddingP">Адрес: {currentDevice.hostname}</p>
)}
</DeviceBlock>

View file

@ -3,15 +3,11 @@ import { useToolBar } from "../../../../hooks/useToolBar"
import { Space } from "../../../../const/menus/sideBar/components"
import styled from "styled-components"
const Container = styled.div<{isOpen: boolean}>`
const Container = styled.div`
background-color: ${(props) => props.theme.secondary};
width: 250px;
font-size: 14px;
flex: 3;
overflow: hidden;
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
transition: max-width .4s ease;
`
export const ToolSpace = () => {
@ -19,12 +15,12 @@ export const ToolSpace = () => {
const name = toolBar?.current || ''
const { isActive, isOpen } = useToolBar(name)
if (!isActive) {
if (!isActive || !isOpen) {
return null
}
return (
<Container isOpen={isOpen}>
<Container>
<Space name={name} />
</Container>
)

View file

@ -9,8 +9,6 @@ import { workSpaceNames } from "../../const/workspace/names";
import { Header } from "./modules/appModules/header";
import { ReactFlowProvider } from "@xyflow/react";
import About from "./modules/appModules/about";
import UserScanner from "./modules/appModules/usersScanner/userScanner";
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
const Section = styled.div`
display: flex;
@ -77,12 +75,6 @@ export default function WorkSpace() {
from={"helloWindow"}
/>
)}
{workModule === workSpaceNames[5] && (
<UserScanner />
)}
{workModule === workSpaceNames[6] && (
<WifiScanner />
)}
{workModule === workSpaceNames[3] &&
<WorkSection>
<Header />

View file

@ -12,13 +12,10 @@ import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
import edit from '@iconify/icons-ic/baseline-edit';
import checkCircle from '@iconify/icons-ic/round-check-circle';
import cancel from '@iconify/icons-ic/baseline-cancel';
import userSearch from '@iconify/icons-ic/baseline-person-search';
import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
import refresh from '@iconify/icons-ic/baseline-refresh';
import close from '@iconify/icons-ic/round-close';
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
import noteEdit from '@iconify/icons-ph/note-pencil';
export const ICONS = {
[buttonNames[1]]: exitToApp,
@ -33,10 +30,8 @@ export const ICONS = {
[buttonNames[10]]: admin,
[buttonNames[11]]: refresh,
[buttonNames[12]]: close,
[buttonNames[13]]: noAdmin,
[buttonNames[14]]: userSearch,
[buttonNames[15]]: wifiFind,
[buttonNames[16]]: noteEdit,
[buttonNames[13]]: noAdmin
} as const;
type IconName = keyof typeof ICONS;

View file

@ -13,8 +13,5 @@ export const buttonNames: names ={
10: 'admin',
11: 'reset',
12: 'close',
13: 'noAdmin',
14: 'userSearch',
15: 'wifiFind',
16: 'noteEdit'
13: 'noAdmin'
}

View file

@ -1,7 +1,5 @@
const baseTheme = {
accent: '#007ACC',
successColor: "#4CAf50",
errorColor: "#F44336",
accentHover: '#4A90E2',
}
@ -11,10 +9,6 @@ export const light = {
secondary: '#EDEDED',
background: '#E8E8E8',
textColor: '#000000',
borderColor: '#404040',
scannerColor: '#007ACC',
offlineColor: '#ababab',
hoverBtn: '#d6d6d6',
};
export const dark = {
@ -23,8 +17,4 @@ export const dark = {
secondary: '#252525',
background: '#3A3A3A',
textColor: '#FFFFFF',
borderColor: '#b6b6b6',
scannerColor: '#0E9C9D',
offlineColor: '#656565',
hoverBtn: '#4a4a4a',
};

View file

@ -4,7 +4,5 @@ export const workSpaceNames: names ={
1: 'helloWindow',
2: 'aboutFromWork',
3: 'networkInterfaces',
4: 'aboutFromHello',
5: 'userScanner',
6: 'wifiScanner',
4: 'aboutFromHello'
}

View file

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

View file

@ -1,51 +0,0 @@
import { create } from 'zustand';
export interface DeviceUser {
ip: string;
mac: string;
hostname: string;
vendor: string;
open_ports: string[];
status: string;
last_seen: string;
note: string;
}
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
interface NetworkStore {
devices: DeviceUser[];
setDevices: (devices: DeviceUser[]) => void;
addDevice: (device: DeviceUser) => void;
wifiNetworks: WifiNetworkScanner[];
setWifiNetworks: (networks: WifiNetworkScanner[]) => void;
addWifiNetwork: (network: WifiNetworkScanner) => void;
clearAll: () => void;
}
const useNetworkStore = create<NetworkStore>((set) => ({
devices: [],
setDevices: (devices) => set({ devices }),
addDevice: (device) =>
set((state) => ({ devices: [...state.devices, device] })),
wifiNetworks: [],
setWifiNetworks: (networks) => set({ wifiNetworks: networks }),
addWifiNetwork: (network) =>
set((state) => ({ wifiNetworks: [...state.wifiNetworks, network] })),
clearAll: () => set({ devices: [], wifiNetworks: [] })
}));
export default useNetworkStore;

View file

@ -1,5 +1,6 @@
import { useIpStore } from "../store";
import { useAdminStore } from "../store/adminStore";
import { useEffect } from "react";
import { NetworkService } from "../api/request";
export const useCheckAdmin = () => {

View file

@ -1,61 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { useIpStore } from './src/store';
import { useEffect } from 'react';
import { useCheckAdmin } from './src/utils/checkAdmin';
import {
ModalProvider,
useModalContext,
ThemeProvider
} from './src/store';
import WindowComponent from './src/components/modalwindows/modalWindow';
import Router from './src/router';
import { I18nProvider } from "./src/utils/i18n/provider";
import { useLanguage } from "./src/hooks/useSettings";
const AppWithProviders = () => {
const currentLanguage = useLanguage();
return (
<I18nProvider initialLanguage={currentLanguage}>
<ThemeProvider>
<HashRouter>
<ModalProvider>
<MainComponent />
</ModalProvider>
</HashRouter>
</ThemeProvider>
</I18nProvider>
);
};
const MainComponent = () => {
const checkAdmin = useCheckAdmin();
const { ipAddress } = useIpStore();
useEffect(() => {
const checkAdminStatus = async () => {
await checkAdmin();
};
checkAdminStatus();
}, [ipAddress]);
const { isOpen, modalType, modalData } = useModalContext();
return (
<>
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
<Router />
</>
);
};
const container = document.getElementById('root');
if (!container) {
throw new Error('Root element with id "root" not found');
}
const root = createRoot(container);
root.render(<AppWithProviders />);

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
quitApp: () => ipcRenderer.send('quit-app'),
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
writeFile: (path, data) => ipcRenderer.invoke('write-file', { path, data }),
onThemeChange: (callback) => ipcRenderer.on('theme-update', (event, theme) => callback(theme))
});

View file

@ -1,186 +0,0 @@
{
"userScanner-title": "Subnet Node Scanning",
"userScanner-visible": "Display Method",
"userScanner-loading": "Scanning nodes",
"userScanner-refresh": "Refresh List",
"userScanner-scan": "Scan Nodes",
"userScanner-notFound": "Nodes not found",
"tableScanner-online": "Online",
"tableScanner-name": "Name",
"tableScanner-vendor": "Vendor",
"tableScanner-ip": "IP Address",
"tableScanner-ports": "Open Ports",
"tableScanner-activity": "Last Activity",
"tableScanner-note": "Note",
"tableScanner-action": "Action",
"tableScanner-unknown": "Unknown",
"tableScanner-noData": "No data to display",
"listUsers-activeNodes": "Active Nodes",
"listUsers-inactiveNodes": "Inactive Nodes",
"ipInput-placeholder": "Enter IP",
"ipInput-error": "Enter a valid IP address",
"userScanner-visible_table": "Table",
"userScanner-visible_list": "List",
"wifiScanner-title": "Wi-Fi Access Point Scanning",
"wifiScanner-scanning": "Scanning access points",
"wifiScanner-scanError": "Network scanning error",
"wifiScanner-refresh": "Refresh List",
"wifiScanner-scan": "Scan Access Points",
"wifiModal-bssid": "BSSID",
"wifiModal-vendor": "Vendor",
"wifiModal-signal": "Signal",
"wifiModal-channel": "Channel",
"wifiModal-encryption": "Encryption",
"wifiModal-frequency": "Frequency",
"wifiModal-hidden": "Hidden Network",
"wifiModal-note": "Note",
"wifiModal-connect": "Connect",
"wifiModal-disconnect": "Disconnect",
"wifiModal-unknown": "Unknown",
"wifiModal-none": "None",
"common-yes": "Yes",
"common-no": "No",
"addNode": "Add Node",
"enterNodeAddress": "Enter node address...",
"connectionLost": "Connection Lost",
"serverDisconnected": "Server connection interrupted",
"checkAddressAndRetry": "Please check the address availability and try again",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"retry": "Retry",
"connect": "Connect",
"submit": "Submit",
"common-back": "Back",
"error": "Error",
"networkConnection": "Network Connection",
"enterPassword": "Enter password...",
"connectingAttempt": "Attempting to connect to the network",
"connectionError": "Network connection error",
"connectionErrorLog": "Connection error: ",
"customizeTable-title": "Subnet Node Table Customization",
"wifiDisconnect-title": "Wi-Fi Network Disconnection",
"wifiDisconnect-confirm": "Confirm Disconnection",
"wifiDisconnect-message1": "You are about to disconnect from the network - ",
"wifiDisconnect-message2": " All active connections will be interrupted.",
"wifiDisconnect-disconnect": "Disconnect",
"wifiDisconnect-progress": "Disconnecting from the network",
"ipChange-title": "IP Address Change",
"ipChange-checkingIp": "Checking IP address validity",
"ipChange-generalInfo": "General Information",
"ipChange-interface": "Interface",
"ipChange-ipInfo": "IP Information",
"ipChange-currentIp": "Current IP",
"ipChange-currentGateway": "Current Gateway",
"ipChange-currentDns": "Current DNS",
"ipChange-newIp": "New IP",
"ipChange-newGateway": "New Gateway",
"ipChange-newDns": "New DNS",
"ipChange-checkError": "An error occurred while checking validity",
"ipChange-adminRequired": "To change network settings, you must log in as an administrator",
"note-edit": "Edit Note",
"note-add": "Add Note",
"note-enter": "Enter note...",
"auth-successLogin": "Successful Login",
"auth-successLogout": "Successful Logout",
"auth-getAccess": "Obtaining Access Rights",
"auth-enableAdmin": "Enable Administrative Access",
"auth-enterAdminPass": "Enter administrator password...",
"auth-wrongPass": "Incorrect operator password",
"auth-switchToLimited": "Switch to Limited Access",
"auth-limitedModeDesc": "In this mode, administrator privileges are restricted.",
"auth-restrictAccess": "Restrict Access",
"auth-closeHint": "This window can be closed, or it will disappear on its own",
"operatorConfig-title": "Node Operator Configuration",
"operatorConfig-instruction": "Before starting, you need to create an application operator password",
"operatorConfig-passwordPlaceholder": "Enter password...",
"operatorConfig-repeatPasswordPlaceholder": "Repeat password...",
"operatorConfig-passwordMismatch": "Passwords do not match!",
"operatorConfig-submit": "Submit",
"about-logoAlt": "Gossys Logo",
"about-title": "About the Application",
"about-description": "GoNets is a minimally viable cross-platform product (MVP) for demonstrating our approach to managing PC settings in a corporate environment.",
"about-featuresTitle": "Features",
"about-feature1": "Visualization of active network resources",
"about-feature2": "Configuration of local and remote network resource parameters",
"about-feature3": "Management of resource interaction with the GoNets Client",
"about-disclaimer": "The software is for demonstration and home use only.",
"about-goToPro": "Go to GoNets-PRO",
"about-homepage": "Homepage",
"about-documentation": "Documentation",
"about-license": "License",
"about-copyright": "Developed and maintained by Hybrid Office Systems LLC, 2024-2025. All rights reserved.",
"main-logoAlt": "Application Logo",
"main-networkSettings": "Network Settings",
"main-about": "About the Program",
"nodes-availableNodes": "Available Nodes",
"nodes-rootAddress": "Root Address",
"nodes-name": "Name",
"nodes-loading": "Loading",
"nodes-additionalNodes": "Additional Nodes",
"nodes-node": "Node",
"nodes-address": "Address",
"nodes-connectionError": "Failed to connect to the server at the address ",
"settings-nodesSection": "Subnet Nodes",
"settings-intervalScanning": "Interval Scanning",
"settings-scanInterval": "Node Scan Interval (min)",
"settings-displayNames": "Display Names When Missing",
"settings-ipAddress": "IP Address",
"settings-unknown": "Unknown",
"settings-rowsPerPage": "Rows Per Table",
"settings-displaySettings": "Nodes Display",
"settings-configure": "Configure",
"settings-clearSessionData": "Clear Session Data",
"settings-clear": "Clear",
"settings-clearFailed": "Data was not deleted: ",
"settings-exportData": "Export Nodes Data",
"settings-export": "Export",
"settings-saveNodesTitle": "Save Nodes Data",
"settings-exportSuccess": "Data exported successfully!",
"settings-exportError": "Export error: ",
"settings-status": "Online",
"settings-name": "Name",
"settings-vendor": "Manufacturer",
"settings-openPorts": "Open Ports",
"settings-lastActivity": "Last Activity",
"settings-note": "Note",
"settings-action": "Action",
"settings-title": "Settings",
"settings-resetSettings": "Reset Settings",
"settings-resetConfirm": "Are you sure you want to reset all settings to default values?",
"settings-cancel": "Cancel",
"settings-save": "Save",
"settings-fixErrors": "Fix errors before saving",
"settings-fillRequired": "Fill in all required fields",
"settings-dataNotDeleted": "Data was not deleted: ",
"settings-saveNetworksTitle": "Save Networks Data",
"settings-fieldRequired": "Field cannot be empty",
"settings-enterNumber": "Please enter a number",
"settings-valueTooSmall": "Value cannot be less than ",
"settings-valueTooLarge": "Value cannot be greater than ",
"settings-sections-subnetNodes": "Subnet Nodes",
"settings-sections-accessPoints": "Access Points",
"settings-sections-appearance": "Appearance",
"settings-exportNetworkData": "Export Access Points Data",
"settings-colorTheme": "Color Theme",
"settings-light": "Light",
"settings-dark": "Dark",
"settings-system": "As in System",
"settings-interfaceLanguage": "Interface Language",
"settings-russian": "Russian",
"settings-english": "English"
}

View file

@ -1,192 +0,0 @@
{
"userScanner-title": "Сканирование узлов подсети",
"userScanner-visible": "Способ отображения",
"userScanner-loading": "Cканирование узлов",
"userScanner-refresh": "Обновить список",
"userScanner-scan": "Сканировать узлы",
"userScanner-notFound": "Узлы не найдены",
"tableScanner-online": "Онлайн",
"tableScanner-name": "Имя",
"tableScanner-vendor": "Производитель",
"tableScanner-ip": "IP адрес",
"tableScanner-ports": "Открытые порты",
"tableScanner-activity": "Последняя активность",
"tableScanner-note": "Заметка",
"tableScanner-action": "Действие",
"tableScanner-unknown": "Неизвестно",
"tableScanner-noData": "Нет данных для отображения",
"listUsers-activeNodes": "Активные узлы",
"listUsers-inactiveNodes": "Неактивные узлы",
"ipInput-placeholder": "Введите IP",
"ipInput-error": "Введите корректный IP-адрес",
"userScanner-visible_table": "Таблица",
"userScanner-visible_list": "Список",
"wifiScanner-title": "Сканирование точек доступа Wi-Fi",
"wifiScanner-scanning": "Сканирование точек доступа",
"wifiScanner-scanError": "Ошибка сканирования сети",
"wifiScanner-refresh": "Обновить список",
"wifiScanner-scan": "Сканировать точки доступа",
"wifiModal-bssid": "BSSID",
"wifiModal-vendor": "Производитель",
"wifiModal-signal": "Сигнал",
"wifiModal-channel": "Канал",
"wifiModal-encryption": "Шифрование",
"wifiModal-frequency": "Частота",
"wifiModal-hidden": "Скрытая сеть",
"wifiModal-note": "Заметка",
"wifiModal-connect": "Подключиться",
"wifiModal-disconnect": "Отключиться",
"wifiModal-unknown": "Неизвестно",
"wifiModal-none": "Отсутствует",
"common-yes": "Да",
"common-no": "Нет",
"addNode": "Добавление узла",
"enterNodeAddress": "Введите адрес узла...",
"connectionLost": "Соединение прервано",
"serverDisconnected": "Произошёл разрыв с сервером",
"checkAddressAndRetry": "Пожалуйста, проверьте доступ к адресу и попробуйте снова",
"cancel": "Отменить",
"close": "Закрыть",
"save": "Сохранить",
"retry": "Повторить",
"connect": "Подключиться",
"common-back": "Назад",
"submit": "Подтвердить",
"error": "Ошибка",
"networkConnection": "Подключение к сети",
"enterPassword": "Введите пароль...",
"connectingAttempt": "Попытка подключения к сети",
"connectionError": "Ошибка подключения к сети",
"connectionErrorLog": "Ошибка подключения: ",
"customizeTable-title": "Кастомизация таблицы узлов подсети",
"wifiDisconnect-title": "Отключение от Wi-Fi сети",
"wifiDisconnect-confirm": "Подтверждение отключения",
"wifiDisconnect-message1": "Вы собираетесь отключиться от сети - ",
"wifiDisconnect-message2": " Все активные подключения будут прерваны.",
"wifiDisconnect-disconnect": "Отключиться",
"wifiDisconnect-progress": "Отключение от сети",
"ipChange-title": "Изменение IP-Адреса",
"ipChange-checkingIp": "Идет проверка корректности IP адреса",
"ipChange-generalInfo": "Общие сведения",
"ipChange-interface": "Интерфейс",
"ipChange-ipInfo": "Сведения об IP",
"ipChange-currentIp": "Текущий ip",
"ipChange-currentGateway": "Текущий шлюз",
"ipChange-currentDns": "Текущий DNS",
"ipChange-newIp": "Новый ip",
"ipChange-newGateway": "Новый шлюз",
"ipChange-newDns": "Новый DNS",
"ipChange-checkError": "Произошла ошибка проверки корректности",
"ipChange-adminRequired": "Для изменения сетевых настроек необходимо авторизироваться под администратором",
"note-edit": "Редактирование заметки",
"note-add": "Добавление заметки",
"note-enter": "Введите заметку...",
"auth-successLogin": "Успешный вход",
"auth-successLogout": "Успешный выход",
"auth-getAccess": "Получение прав доступа",
"auth-enableAdmin": "Включить административный доступ",
"auth-enterAdminPass": "Введите пароль администратора...",
"auth-wrongPass": "Неверный пароль оператора",
"auth-switchToLimited": "Переключиться на ограниченный доступ",
"auth-limitedModeDesc": "В данном режиме доступа ограничиваются полномочия администратора.",
"auth-restrictAccess": "Ограничить доступ",
"auth-closeHint": "Это окно можно закрыть, либо оно пропадёт само",
"operatorConfig-title": "Настройка оператора узла",
"operatorConfig-instruction": "Перед началом работы необходимо создать пароль оператора приложения",
"operatorConfig-passwordPlaceholder": "Введите пароль...",
"operatorConfig-repeatPasswordPlaceholder": "Повторите пароль...",
"operatorConfig-passwordMismatch": "Пароли не совпадают!",
"operatorConfig-submit": "Отправить",
"about-logoAlt": "Логотип Gossys",
"about-title": "О приложении",
"about-description": "ГоNets - минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.",
"about-featuresTitle": "Возможности",
"about-feature1": "Визуализация активных сетевых ресурсов",
"about-feature2": "Настройка параметров локальных и удалённых сетевых ресурсов",
"about-feature3": "Управление взаимодействием ресурсов с Клиентом ГoNets",
"about-disclaimer": "Программное обеспечение исключительно для ознакомления и домашнего использования.",
"about-goToPro": "Перейти на ГоNets-PRO",
"about-homepage": "Домашняя страница",
"about-documentation": "Документация",
"about-license": "Лицензия",
"about-copyright": "Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.",
"main-logoAlt": "Логотип приложения",
"main-networkSettings": "Настройки сети",
"main-about": "О программе",
"nodes-availableNodes": "Доступные узлы",
"nodes-rootAddress": "Корневой адрес",
"nodes-name": "Имя",
"nodes-loading": "Загрузка",
"nodes-additionalNodes": "Дополнительные узлы",
"nodes-node": "Узел",
"nodes-address": "Адрес",
"nodes-connectionError": "Не удалось подключиться к серверу по адресу ",
"settings-nodesSection": "Узлы подсети",
"settings-intervalScanning": "Интервальное сканирование",
"settings-scanInterval": "Интервал сканирования узлов (мин)",
"settings-displayNames": "Отображаемые имена при отсутствии",
"settings-ipAddress": "IP адрес",
"settings-unknown": "Неизвестно",
"settings-rowsPerPage": "Количество строк в таблице",
"settings-displaySettings": "Отображение узлов",
"settings-configure": "Настроить",
"settings-clearSessionData": "Очистить данные сессии",
"settings-clear": "Очистить",
"settings-clearFailed": "Данные не были удалены: ",
"settings-exportData": "Экспорт данных узлов",
"settings-export": "Экспортировать",
"settings-saveNodesTitle": "Сохранить данные узлов",
"settings-exportSuccess": "Данные успешно экспортированы!",
"settings-exportError": "Ошибка экспорта: ",
"settings-status": "Онлайн",
"settings-name": "Имя",
"settings-vendor": "Производитель",
"settings-openPorts": "Открытые порты",
"settings-lastActivity": "Последняя активность",
"settings-note": "Заметка",
"settings-action": "Действие",
"settings-title": "Настройки",
"settings-resetSettings": "Сбросить настройки",
"settings-resetConfirm": "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?",
"settings-cancel": "Отменить",
"settings-save": "Сохранить",
"settings-fixErrors": "Исправьте ошибки перед сохранением",
"settings-fillRequired": "Заполните все обязательные поля",
"settings-dataNotDeleted": "Данные не были удалены: ",
"settings-saveNetworksTitle": "Сохранить данные сетей",
"settings-fieldRequired": "Поле не может быть пустым",
"settings-enterNumber": "Пожалуйста, введите число",
"settings-valueTooSmall": "Значение не может быть меньше ",
"settings-valueTooLarge": "Значение не может быть больше ",
"settings-sections-subnetNodes": "Узлы подсети",
"settings-sections-accessPoints": "Точки доступа",
"settings-sections-appearance": "Внешний вид",
"settings-exportNetworkData": "Экспорт данных точек доступа",
"settings-colorTheme": "Цветовая тема",
"settings-light": "Светлая",
"settings-dark": "Тёмная",
"settings-system": "Как в системе",
"settings-interfaceLanguage": "Язык интерфейса",
"settings-russian": "Русский",
"settings-english": "Английский"
}

View file

@ -1,196 +0,0 @@
import axios, { AxiosResponse } from "axios";
const axiosWithoutCredentials = axios.create({
baseURL: `http://localhost:8001`,
})
export class NetworkService {
static createAxiosInstance(ipAddress: string) {
return axios.create({
baseURL: `http://${ipAddress}:8001`,
});
}
static async checkIp(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/ip/check_ip', data);
} catch (error: any) {
return error.response;
}
}
static async saveIp(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/ip/apply_ip', data);
} catch (error: any) {
return error.response;
}
}
static async getHostname(): Promise<AxiosResponse>{
try{
const response = await axiosWithoutCredentials.get('/network/get_hostname')
return response
} catch (error: any){
return error.response
}
}
static async createUser(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/auth/register', data);
} catch (error: any) {
return error.response;
}
}
static async login(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/auth/login', data);
} catch (error: any) {
return error.response;
}
}
static async logout(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/auth/logout');
} catch (error: any) {
return error.response;
}
}
static async checkServerConf(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/auth/check_json');
} catch (error: any) {
return error.response;
}
}
static async whoami(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/auth/me');
} catch (error: any) {
return error.response;
}
}
static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/subnet/devices',{
subnet: ipScanning,
});
} catch (error: any) {
return error.response;
}
}
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/wifi');
} catch (error: any) {
return error.response;
}
}
static async addNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/${type}/${address}/note`, {
note: note
});
} catch (error: any) {
return error.response;
}
}
static async editNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.put(`/subnet/${type}/${address}/note`, {
note: note
});
} catch (error: any) {
return error.response;
}
}
static async deleteNote(ipAddress: string, address:string, type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.delete(`/subnet/${type}/${address}/note`);
} catch (error: any) {
return error.response;
}
}
static async statusNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get(`/subnet/wifi/status`);
} catch (error: any) {
return error.response;
}
}
static async connectNetwork(ipAddress: string, password:string, address: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/connect`, {
ssid: address,
password: password,
});
} catch (error: any) {
return error.response;
}
}
static async disconnectNetwork(ipAddress: string, ssid: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
ssid: ssid,
});
} catch (error: any) {
return error.response;
}
}
// Settings
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;
}
}
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_devices`);
} catch (error: any) {
return error.response;
}
}
static async exportNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_wifi`);
} catch (error: any) {
return error.response;
}
}
}

View file

@ -1,138 +0,0 @@
import React, {useState} from 'react';
import styled, { css } from 'styled-components';
import { DeviceUser } from "../../../store/useDeviceStore";
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;
status: 'online' | 'offline';
}
const CardWrapper = styled.div<{ status: 'online' | 'offline' }>`
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 16px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
`;
const CardHeader = styled.div<{ status: 'online' | 'offline' }>`
background: ${({ status, theme }) =>
status === 'online' ? theme.successColor : theme.offlineColor};
color: white;
padding: 16px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
`;
const CardContent = styled.div<{ isExpanded: boolean }>`
max-height: 0;
opacity: 0;
padding: 0 16px;
transition: all 0.3s ease;
color: #333;
font-size: 14px;
display: grid;
gap: 12px;
${({ isExpanded }) => isExpanded && css`
max-height: 500px;
opacity: 1;
padding: 16px;
`}
`;
const InfoRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Label = styled.span`
font-weight: 500;
color: #666;
`;
const Value = styled.span`
color: #333;
`;
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
const [isExpanded, setIsExpanded] = useState(false);
const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<CardWrapper status={status} onClick={()=>setIsExpanded(!isExpanded)}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
<span onClick={openModalNote}>
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
</span>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || (nameSetting === 'ip' ? device.ip : 'Неизвестно')}</Value>
</InfoRow>
<InfoRow>
<Label>Производитель:</Label>
<Value>{device.vendor || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>IP адрес:</Label>
<Value>{device.ip}</Value>
</InfoRow>
{device.open_ports?.length > 0 && (
<InfoRow>
<Label>Открытые порты:</Label>
<Value>{device.open_ports.join(', ')}</Value>
</InfoRow>
)}
<InfoRow>
<Label>Последняя активность:</Label>
<Value>{device.last_seen}</Value>
</InfoRow>
<InfoRow>
<Label>Заметка:</Label>
<Value>{device.note || '-'}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -1,130 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import styled from "styled-components";
import {buttonNames} from "../../../const/buttons/names";
import {IconComponent} from "../../../const/buttons/icons";
type Option = {
value: number,
label: string,
}
interface DropdownTypeProps {
options: Option[];
label?: string;
onChange?: (selectedItem: Option) => void;
value: Option | null;
}
const DropdownWrapper = styled.div`
position: absolute;
top: 18px;
right:20px;
display: inline-block;
background-color: ${({theme}) => theme.background};
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
`
const Label = styled.p`
color: ${({theme}) => theme.textColor};
font-size: 16px;
`
const Dropdown = styled.div`
background-color: ${({theme}) => theme.primary};
border-radius: 15px;
overflow: hidden;
transition: all .3s ease;
cursor: pointer;
z-index: 10;
margin-top: 6px;
`
const DropdownHeader = styled.div`
display: flex;
justify-content: space-between;
gap: 14px;
padding: 12px 24px;
font-size: 16px;
align-items: center;
height: 100%;
min-width: 200px;
`
const DropdownContent = styled.div<{$isOpen: boolean}>`
max-height: ${({$isOpen}) => $isOpen ? '500px' : '0'};
opacity: ${({$isOpen}) => $isOpen ? 1 : 0};
transition: all .3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
`
const DropdownRow = styled.button`
padding: 12px 24px;
background-color: transparent;
text-align: start;
font-size: 16px;
border: none;
width: 100%;
color: ${({theme}) => theme.textColor};
transition: all .3s ease;
cursor: pointer;
&:hover {
background-color: ${({theme}) => theme.background};
}
`
const DropdownArrow = styled.div<{ $isOpen: boolean }>`
transition: transform 0.3s ease;
transform: rotate(${({ $isOpen }) => ($isOpen ? "90deg" : "0deg")});
`;
const DropdownType = ({options, value, onChange, label}: DropdownTypeProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
function toggleOpen() {
setIsOpen((prev) => !prev);
}
function handleSelect(option: Option) {
onChange?.(option);
setIsOpen(false);
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<DropdownWrapper ref={dropdownRef}>
<Label>{label}</Label>
<Dropdown>
<DropdownHeader onClick={toggleOpen}>
<p>{value?.label || 'Select an option'}</p>
<DropdownArrow $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} />
</DropdownArrow>
</DropdownHeader>
<DropdownContent $isOpen={isOpen}>
{options.map((option, i) => (
<DropdownRow key={i} onClick={() => handleSelect(option)}>
{option.label}
</DropdownRow>
))}
</DropdownContent>
</Dropdown>
</DropdownWrapper>
);
};
export default DropdownType;

View file

@ -1,77 +0,0 @@
import React from 'react';
import styled from "styled-components";
import {buttonNames} from "../../const/buttons/names";
import {IconComponent} from "../../const/buttons/icons";
const PaginateWrapper = styled.div`
width: 100%;
margin-top: 24px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
`
const Page = styled.button<{ active?: boolean }>`
width: 36px;
height: 36px;
background: ${(props) => props.active ? props.theme.borderColor : props.theme.primary};
color: ${(props) => props.active ? props.theme.primary : props.theme.textColor};
border: 1px solid ${(props => props.theme.borderColor)};
font-size: 16px;
cursor: pointer;
border-radius: 4px;
transition: .4s ease;
&:hover {
background: ${(props) => props.active ? props.theme.textColor : props.theme.background};;
}
`
interface PaginationProps {
currentPage: number;
totalPages: number;
onPrevPage: () => void;
onNextPage: () => void;
onPageChange: (page: number) => void;
}
const Pagination = ({currentPage, totalPages, onPrevPage, onNextPage, onPageChange}: PaginationProps) => {
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
return (
<PaginateWrapper>
<Page
onClick={onPrevPage}
disabled={currentPage === 1}
>
<IconComponent
width="20"
height="20"
name={buttonNames[17]}
/>
</Page>
{pageNumbers.map((page, i) => (
<Page
key={i}
onClick={() => onPageChange(page)}
active={currentPage === page}
>
{page}
</Page>
))}
<Page
onClick={onNextPage}
disabled={currentPage === totalPages}
>
<IconComponent
width="20"
height="20"
name={buttonNames[6]}
/>
</Page>
</PaginateWrapper>
);
};
export default Pagination;

View file

@ -1,87 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
onClose: () => void;
}
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const {ipAddress} = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleSave = async () => {
try {
setIsLoading(true);
setError('');
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if (res.status === 200) {
onClose();
} else {
setError(res.data.detail);
}
} catch (error) {
console.error(t('connectionErrorLog'), error);
setError(t('connectionError'));
} finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{height: '230px'}}>
<ModalTitle>
<h3>{t('networkConnection')}</h3>
</ModalTitle>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<Input
type="password"
placeholderText={t('enterPassword')}
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
{error && (
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
{error}
</p>
)}
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title={t('connect')}
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
</ModalContainer>
);
};

View file

@ -1,83 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedDevices = devices.map(device =>
device.mac === address
? { ...device, note: note.trim() }
: device
);
setDevices(updatedDevices);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '250px' }}>
<ModalTitle>
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText={t('note-enter')}
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title={t('save')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,84 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedNetworks = wifiNetworks.map(network =>
network.ssid === address
? { ...network, note: note.trim() }
: { ...network }
);
setWifiNetworks(updatedNetworks);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '220px' }}>
<ModalTitle>
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText={t('note-enter')}
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title={t('save')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,143 +0,0 @@
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';
import {useTranslation} from "../../../utils/i18n/hooks";
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 {t} = useTranslation('common');
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]);
const getItemText = (item: string) => {
if (item === 'Онлайн') return t('settings-status');
if (item === 'Имя') return t('settings-name');
if (item === 'Производитель') return t('settings-vendor');
if (item === 'IP адрес') return t('settings-ipAddress');
if (item === 'Открытые порты') return t('settings-openPorts');
if (item === 'Последняя активность') return t('settings-lastActivity');
if (item === 'Заметка') return t('settings-note');
if (item === 'Действие') return t('settings-action');
return item;
};
return (
<ModalContainer style={{ height: '35vh', width: '80vw'}}>
<ModalTitle>
<h3>{t('customizeTable-title')}</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)}
>
{getItemText(item)}
</StyledTableItem>
))}
</StyledTableList>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('common-back')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};
export default CustomizeNodes;

View file

@ -1,67 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useState} from "react";
import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={
address: string,
onClose: () => void
}
export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleDisconnect = async () => {
try {
setIsLoading(true);
const res = await NetworkService.disconnectNetwork(ipAddress, address);
if(res.status === 200) {
onClose()
}
} catch (error) {
console.error('Error connection:', error);
}finally {
setIsLoading(false);
}
};
return(
<ModalContainer>
<ModalTitle>
<h3>{t('wifiDisconnect-title')}</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>{t('wifiDisconnect-confirm')}</h3>
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton
title={t('cancel')}
onClick={onClose}
size="small"
/>
<MainButton
title={t('wifiDisconnect-disconnect')}
color={theme.errorColor}
hoverColor={theme.errorColor}
onClick={handleDisconnect}
size="small"
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
</ModalContainer>
)
}

View file

@ -1,91 +0,0 @@
import { useModalContext } from "../../store";
import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalContainers";
import { ModalMap } from "./windowsType";
import Loader from "../UI/loader";
import { ModalWindow } from "./style";
import {NoteDevice} from "./modalContainers/NoteDevice";
import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
import {Settings} from "../settings/Settings";
import CustomizeNodes from "./modalContainers/customizeNodes";
type Props = {
isOpen: boolean,
type: string
data: any;
}
export default function WindowComponent({ isOpen, type, data }: Props) {
const { closeModal, closeLoader } = useModalContext()
return (
<>
{isOpen &&
<ModalWindow>
{type == ModalMap['1'] && data &&
<IpConfirm
ipData={data}
onClose={closeModal}
smoothClose={closeLoader}
/>
}
{type == ModalMap['2'] &&
<Loader loaderText={data} />
}
{type === ModalMap['3'] &&
<ConnectionError
message={data.message}
/>
}
{type === ModalMap['6'] && (
<NoteDevice
type={'devices'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['7'] && (
<NoteWifi
type={'wifi'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['8'] && (
<ConnectNetwork
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['9'] && (
<DisconnectConfirm
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['4'] && (
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
)}
{type === ModalMap['5'] && (
<RightsConfirm onClose={closeLoader} />
)}
{type === ModalMap['10'] && (
<Settings
onClose={closeLoader}
/>
)}
{type === ModalMap['11'] && (
<CustomizeNodes />
)}
</ModalWindow >
}
</>
)
}

View file

@ -1,226 +0,0 @@
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';
import {useModalContext} from "../../store";
import {ModalMap} from "../modalwindows/windowsType";
import {useTranslation} from "../../utils/i18n/hooks";
const SettingBlock = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
margin-bottom: 18px;
font-size: 18px;
width: 90%;
input{
font-size: 16px;
outline: none;
background-color: ${props => props.theme.primary};
color: ${props => props.theme.textColor};
border: 1px solid ${props => props.theme.borderColor};
padding: 6px 8px;
border-radius: 4px;
}
input[type="number"]{
width: 80px;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
`
const StyledCheckbox = styled.input.attrs({ type: 'checkbox' })`
width: 22px !important;
height: 22px !important;
appearance: none;
-webkit-appearance: none;
position: relative;
cursor: pointer;
overflow: hidden;
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: ${props => props.theme.primary};
top: 0;
left: 0;
}
&:checked::after {
background: ${props => props.theme.accent};
}
`;
const LabelCheckbox = styled.label`
position: relative;
cursor: pointer;
padding: 0;
svg{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`
const InputWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 6px;
`
const ErrorText = styled.div`
color: ${props => props.theme.errorColor};
font-size: 12px;
`;
interface SettingItemProps {
setting: Setting;
value: any;
onChange: (value: any) => void;
onValidationError?: (error: string | null) => void;
}
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
const theme = useTheme();
const { t } = useTranslation('common');
const [error, setError] = useState<string | null>(null);
const { openModal } = useModalContext();
const handleButtonAction = () => {
if (setting.id === 'customize-nodes') {
openModal(ModalMap['11']);
} else if (setting.action) {
// Создаем обертку для действия с переводами
const actionWithTranslations = async () => {
try {
// Временно заменяем alert на версию с переводами
const originalAlert = window.alert;
window.alert = (message: string) => {
// Применяем переводы к сообщениям
let translatedMessage = message;
if (message.startsWith('settings-dataNotDeleted')) {
translatedMessage = t('settings-dataNotDeleted') + message.replace('settings-dataNotDeleted', '');
} else if (message === 'settings-exportSuccess') {
translatedMessage = t('settings-exportSuccess');
} else if (message.startsWith('settings-exportError')) {
translatedMessage = t('settings-exportError') + message.replace('settings-exportError', '');
}
originalAlert(translatedMessage);
};
if (setting.action) {
await setting.action();
}
// Восстанавливаем оригинальный alert
window.alert = originalAlert;
} catch (error) {
console.error('Action error:', error);
}
};
actionWithTranslations();
}
};
// Изменение числа
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
let newError: string | null = null;
if (inputValue.trim() === '') {
newError = t('settings-fieldRequired');
onChange(null);
}
else if (isNaN(Number(inputValue))) {
newError = t('settings-enterNumber');
}
else {
const numValue = Number(inputValue);
if (setting.min !== undefined && numValue < setting.min) {
newError = t('settings-valueTooSmall') + setting.min;
}
else if (setting.max !== undefined && numValue > setting.max) {
newError = t('settings-valueTooLarge') + setting.max;
}
if (!newError) {
onChange(numValue);
}
}
setError(newError);
onValidationError?.(newError);
};
return (
<SettingBlock>
<label>{setting.label}</label>
{setting.type === 'boolean' && (
<LabelCheckbox>
<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={value}
options={setting.options}
onChange={(value) => onChange(value)}
/>
)}
{setting.type === 'text' && (
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)}
{setting.type === 'number' && (
<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' && (
<MainButton
type="button"
size="small"
title={setting.defaultValue || ''}
onClick={handleButtonAction}
/>
)}
{setting.type === 'hidden' && null}
</SettingBlock>
);
});
export { SettingItem };

View file

@ -1,118 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { IconComponent } from '../../const/buttons/icons';
import {buttonNames} from "../../const/buttons/names";
interface SettingSelectProps {
value: any;
options: { value: any; label: string }[];
onChange: (value: any) => void;
disabled?: boolean;
}
const SelectWrapper = styled.div`
position: relative;
width: 100%;
max-width: 180px;
`;
const SelectHeader = styled.div<{ $isOpen: boolean; $disabled?: boolean }>`
padding: 6px 8px;
border: 1px solid ${props => props.theme.borderColor};
border-radius: 4px;
background-color: ${props => props.theme.primary};
cursor: ${props => props.$disabled ? 'not-allowed' : 'pointer'};
display: flex;
justify-content: space-between;
align-items: center;
opacity: ${props => props.$disabled ? 0.6 : 1};
font-size: 16px;
`;
const SelectOptions = styled.div<{ $isOpen: boolean }>`
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
background: ${props => props.theme.primary};
border: 1px solid ${props => props.theme.borderColor || '#ccc'};
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 10;
display: ${props => props.$isOpen ? 'block' : 'none'};
`;
const OptionItem = styled.div<{ $isSelected: boolean }>`
padding: 6px 8px;
cursor: pointer;
background-color: ${props => props.$isSelected ? (props.theme.accent) : 'transparent'};
color: ${props => props.$isSelected ? (props.theme.primary) : 'inherit'};
`;
const ArrowIcon = styled.span<{ $isOpen: boolean }>`
transition: transform 0.2s;
transform: rotate(${props => props.$isOpen ? '90deg' : '0deg'});
display: flex;
align-items: center;
`;
const SettingSelect: React.FC<SettingSelectProps> = ({value, options, onChange, disabled = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const selectRef = useRef<HTMLDivElement>(null);
const selectedOption = options.find(opt => opt.value === value);
const toggleDropdown = () => {
if (!disabled) {
setIsOpen(!isOpen);
}
};
const handleSelect = (optionValue: any) => {
onChange(optionValue);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<SelectWrapper ref={selectRef}>
<SelectHeader
$isOpen={isOpen}
onClick={toggleDropdown}
$disabled={disabled}
>
<span>{selectedOption?.label || 'Select...'}</span>
<ArrowIcon $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} width="16" height="16" />
</ArrowIcon>
</SelectHeader>
<SelectOptions $isOpen={isOpen}>
{options.map((option) => (
<OptionItem
key={option.value}
$isSelected={value === option.value}
onClick={() => handleSelect(option.value)}
>
{option.label}
</OptionItem>
))}
</SelectOptions>
</SelectWrapper>
);
};
export default SettingSelect;

View file

@ -1,264 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
import { MainButton } from "../UI/buttons/mainButton";
import {useCallback, useEffect, useMemo, useState} from "react";
import styled, {useTheme} from "styled-components";
import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
import {SettingItem} from "./SettingItem";
import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names";
import {useTranslation} from "../../utils/i18n/hooks";
interface Props {
onClose: () => void;
}
const SectionsSidebar = styled.ul`
display: flex;
justify-content: start;
align-items: start;
flex-direction: column;
gap: 4px;
width: 20%;
min-width: 180px;
list-style-type: none;
padding: 0;
margin: 0;
`
const SectionsBTN = styled.li`
font-size: 18px;
border: none;
cursor: pointer;
width: 100%;
padding:10px 16px;
box-sizing: border-box;
border-radius: 0 12px 12px 0;
&.active{
background-color: ${(props) => props.theme.accent};
color: ${props => props.theme.primary};
}
`
const SectionActive = styled.div`
width: 80%;
`
const SectionLine = styled.span`
height: 100%;
width: 1px;
background-color: #9D9D9D;
`
const CloseBtn = styled.span`
cursor: pointer;
`
const SettingsButtons = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
span{
display: flex;
gap: 12px;
}
`
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 resetToDefaults = useSettingsStore(state => state.resetToDefaults);
const initializeSettings = useSettingsStore(state => state.initializeSettings);
const updateSettingsConfig = useSettingsStore(state => state.updateSettingsConfig);
const theme = useTheme()
const { t, language } = useTranslation('common');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [currentLanguage, setCurrentLanguage] = useState(language);
// Проверка действий
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) => {
// Применяем переводы к настройкам
const translatedSetting = {
...setting,
sectionId: t(setting.sectionId),
label: t(setting.label),
defaultValue: typeof setting.defaultValue === 'string' && setting.defaultValue.startsWith('settings-') ? t(setting.defaultValue) : setting.defaultValue,
options: setting.options ? setting.options.map((option: any) => ({
...option,
label: option.label.startsWith('settings-') ? t(option.label) : option.label
})) : setting.options,
value: settingsValues[setting.id] ?? setting.defaultValue
};
if (!acc[translatedSetting.sectionId]) {
acc[translatedSetting.sectionId] = [];
}
acc[translatedSetting.sectionId].push(translatedSetting);
return acc;
}, {} as Record<string, Setting[]>);
}, [settingsConfig, settingsValues, t]);
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
useEffect(() => {
updateSettingsConfig();
initializeSettings();
}, [updateSettingsConfig, initializeSettings]);
useEffect(() => {
if (sections.length > 0 && !activeSection) {
setActiveSection(sections[0]);
}
}, [sections, activeSection, setActiveSection]);
// Обновляем текущий язык при изменении
useEffect(() => {
setCurrentLanguage(language);
}, [language]);
const handleSave = useCallback(() => {
const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
if (hasErrors) {
alert(t('settings-fixErrors'));
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(t('settings-fillRequired'));
return;
}
// Проверяем, изменился ли язык
const newLanguage = pendingChanges['language'] ?? settingsValues['language'];
const languageChanged = newLanguage && newLanguage !== currentLanguage;
applyChanges();
onClose();
// Если язык изменился, перезагружаем приложение
if (languageChanged) {
setTimeout(() => {
window.location.reload();
}, 100);
}
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose, t, currentLanguage]);
const handleCancel = useCallback(() => {
discardChanges();
setFieldErrors({});
onClose();
}, [discardChanges, onClose]);
const handleReset = () => {
if (window.confirm(t('settings-resetConfirm'))) {
resetToDefaults();
}
};
// Обработка нажатий клавиш
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter')
handleSave();
if (e.key === 'Escape')
handleCancel()
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleSave, handleCancel]);
return (
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle>
<h3>{t('settings-title')}</h3>
<CloseBtn>
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
</CloseBtn>
</ModalTitle>
<ModalContent style={{ display: 'flex', height:"100%", gap: '24px', padding:0 }}>
<SectionsSidebar>
{sections.map((sectionId) => (
<SectionsBTN
key={sectionId}
className={activeSection === sectionId ? 'active' : ''}
onClick={()=>setActiveSection(sectionId)}
>
{sectionId}
</SectionsBTN>
))}
</SectionsSidebar>
<SectionLine/>
<SectionActive>
{activeSection && settingsBySection[activeSection] && (
<>
{settingsBySection[activeSection].map((setting) => (
<SettingItem
key={setting.id}
setting={setting}
value={getSetting(setting.id).currentValue}
onValidationError={(error) => handleValidationError(setting.id, error)}
onChange={(value) => updateSetting(setting.id, value)}
/>
))}
</>
)}
</SectionActive>
</ModalContent>
<ButtonContainer>
<SettingsButtons>
<MainButton
type="button"
size="small"
title={t('settings-resetSettings')}
onClick={handleReset}
color={theme.offlineColor}
hoverColor={theme.borderColor}
/>
<span>
<MainButton
type="button"
size="small"
title={t('settings-cancel')}
onClick={handleCancel}
color={theme.offlineColor}
hoverColor={theme.borderColor}
/>
<MainButton
type="button"
size="small"
title={t('settings-save')}
onClick={handleSave}
/>
</span>
</SettingsButtons>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,252 +0,0 @@
import { MainButton } from "../../../UI/buttons/mainButton"
import { workSpaceNames } from "../../../../const/workspace/names"
import { useWorkSpaceStore } from "../../../../store"
import styled, {useTheme} from "styled-components"
import ThemeButton from "../../../UI/buttons/toggletheme"
import {buttonNames} from "../../../../const/buttons/names";
import {IconComponent} from "../../../../const/buttons/icons";
import {useTranslation} from "../../../../utils/i18n/hooks";
type Props = {
from: 'reg' | 'helloWindow' | 'workSpace'
onBack?: () => void
}
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background: ${(props => props.theme.background)};
padding: 50px;
box-sizing: border-box;
`
const Content = styled.div`
max-width: 1200px;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
padding: 40px 50px;
background: ${(props => props.theme.primary)};
border-radius: 18px;
position: relative;
`
const Listing = styled.div`
display: flex;
flex-direction: column;
background-color: ${(props => props.theme.secondary)};
border-radius: 10px;
padding: 24px 20px;
p{
font-weight: bold;
font-size: 24px;
}
`
const List = styled.ul`
display: flex;
flex-direction: column;
margin: 10px 0;
padding: 0;
list-style-type: none;
font-size: 18px;
gap: 4px;
& li:before {
color: ${(props => props.theme.accent)};
content: "•";
font-size: 28px;
width: .8em;
display: inline-block;
position: relative;
top: 2px;
}
`
const Title = styled.div`
font-size: 20px;
`
const SubTitle = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
background-color:rgba(255, 248, 225, 0.6);
border-left: #ff9800 solid 4px;
border-radius: 0 8px 8px 0;
padding: 20px 12px;
a{
text-decoration: none;
color: ${(props => props.theme.errorColor)};
font-weight: 500;
font-size: 18px;
width: max-content;
}
p{
font-size: 18px;
color: ${(props=>props.theme.textColor)};
}
.icon{
width: 32px;
height: 32px;
fill: #ff9800;
}
`
const Links = styled.div`
display: flex;
justify-content: start;
gap: 20px;
`
const LinkBlock = styled.a`
text-decoration: none;
color: ${(props => props.theme.accent)};;
`
const Copyright = styled.div`
width: 100%;
text-align: center;
color: ${(props => props.theme.offlineColor)};
`
const ButtonContainer = styled.div`
position: absolute;
top: 12px;
right: 14px;
`
const AboutLine = styled.hr`
border: ${(props => props.theme.hoverBtn)} 1px solid;
width: 100%;
`
const MainTitle = styled.div`
width: 100%;
display: flex;
gap: 15px;
align-items: center;
justify-content: space-between;
padding-top: 10px;
box-sizing: border-box;
img{
width: 50px;
height: 50px;
}
.version{
text-wrap: nowrap;
background-color: ${(props => props.theme.hoverBtn)};
color: ${(props => props.theme.borderColor)};
border-radius: 20px;
font-size: 18px;
padding: 10px 18px;
line-height: 1;
}
.title{
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
}
`
export default function About({ from, onBack }: Props) {
const { setWorkModule } = useWorkSpaceStore()
const {t} = useTranslation('common')
const getBackHandler = () => {
switch (from) {
case 'reg':
return onBack
case 'helloWindow':
return () => setWorkModule(workSpaceNames[1])
case 'workSpace':
return () => setWorkModule(workSpaceNames[3])
default:
return undefined
}
}
const backHandler = getBackHandler()
const showBackButton = from !== 'reg' || (from === 'reg' && onBack)
return (
<Wrapper>
<Content>
<MainTitle>
<div className={'title'}>
<img src="./assets/images/gossys.png" alt={t('about-logoAlt')} />
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}>
<h1>{t('about-title')}</h1>
</div>
</div>
<p className={'version'}>v 0.0.3</p>
</MainTitle>
<AboutLine/>
<Title>
<p>{t('about-description')}</p>
</Title>
<Listing>
<p>{t('about-featuresTitle')}:</p>
<List>
<li>{t('about-feature1')}</li>
<li>{t('about-feature2')}</li>
<li>{t('about-feature3')}</li>
</List>
</Listing>
<SubTitle>
<svg className="icon" viewBox="0 0 24 24">
<path d="M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2"/>
</svg>
<p>{t('about-disclaimer')}</p>
<a href={'https://gossys.dev/pages/projects/gonets'}>
{t('about-goToPro')}
</a>
</SubTitle>
<Links>
<LinkBlock href={'https://gossys.dev/pages/projects/gonets'}>
{t('about-homepage')}
</LinkBlock>
<LinkBlock href={'https://wiki.gossys.dev/ru/gonets'}>
{t('about-documentation')}
</LinkBlock>
<LinkBlock href={'https://wiki.gossys.dev/ru/lic/GosysPUB'}>
{t('about-license')}
</LinkBlock>
</Links>
<Copyright>
{t('about-copyright')}
</Copyright>
<ButtonContainer>
{showBackButton && backHandler && (
<IconComponent
width="36"
height="36"
name={buttonNames[12]}
onClick={backHandler}
/>
)}
{from == 'reg' &&
<ThemeButton />
}
</ButtonContainer>
</Content>
</Wrapper>
)
}

View file

@ -1,86 +0,0 @@
import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
import {useTranslation} from "../../../../utils/i18n/hooks";
const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg');
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
`
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 52px;
margin-bottom: 100px;
`
const ButtonWrapper = styled.div`
display: flex;
gap: 75px;
flex-direction: column;
`
const ButtonContainer = styled.div`
display: flex;
gap: 15px;
flex-direction: column;
`
const Title = styled.div`
display: flex;
align-items: flex-end;
gap: 10px;
h1{
font-size: 52px;
}
`
export const HelloWindow = () => {
const {t} = useTranslation('common')
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img
src="./assets/images/logo.svg"
alt={t('main-logoAlt')}
style={{width: '152px'}}
/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title={t('main-networkSettings')}
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title={t('main-about')}
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
}

View file

@ -1,85 +0,0 @@
import React, {useState} from 'react';
import styled, {keyframes} from 'styled-components';
import {useTranslation} from "../../../../../../utils/i18n/hooks";
interface Props {
value: string;
setIpScanning: (value: string) => void;
loading: boolean;
isValid: boolean;
setIsValid: (value: boolean) => void;
}
const Input = styled.input`
height: 40px;
width: 210px;
font-size: 20px;
text-align: center;
background-color: transparent;
border: none;
outline: none;
border-bottom: 2px solid ${props => props.theme.offlineColor};
color: ${props => props.theme.textColor};
transition: all 0.3s ease;
&:focus {
border-bottom: 2px solid ${props => props.theme.accent};
width: 270px;
}
&::placeholder {
color: ${props => props.theme.offlineColor};
transition: all 0.3s ease;
}
&:focus::placeholder {
opacity: 0;
}
&:disabled {
opacity: 0;
}
`
const errorVisible = keyframes`
0%{opacity: 0}
100%{opacity: 1}
`;
const ErrorMessage = styled.span`
margin-top: 6px;
color: ${props=>props.theme.errorColor};
animation: ${errorVisible} .4s linear;
`
const IpScanning = ({value, setIpScanning, loading, isValid, setIsValid}: Props) => {
const {t} = useTranslation('common')
const validateInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
const allowedKeys = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'.', '/',
'Backspace', 'Delete',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'
];
if (!allowedKeys.includes(e.key)) {
e.preventDefault();
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIpScanning(e.target.value);
}
return (
<>
<Input
value={value}
onChange={handleChange}
placeholder={t('ipInput-placeholder')}
onKeyDown={validateInput}
disabled={loading}
/>
{!isValid && !loading && (
<ErrorMessage>{t('ipInput-error')}</ErrorMessage>
)}
</>
);
};
export default IpScanning;

View file

@ -1,115 +0,0 @@
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";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online');
// Пагинация
const paginateSetting = usePaginateSetting();
const [currentPage, setCurrentPage] = useState(1);
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 (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn>
{devices.length > 0 ? (
<div style={{
display: 'flex',
width: '70%',
gap: '20px',
}}>
{/* Колонка активных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>
{t('listUsers-activeNodes')} ({activeDevices.length})
</h3>
{currentActiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="online"
device={device}
/>
))}
</div>
{/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>
{t('listUsers-inactiveNodes')} ({inactiveDevices.length})
</h3>
{currentInactiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="offline"
device={device}
/>
))}
</div>
</div>
) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null}
{totalPages > 1 && (
<Pagination
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
)}
</>
);
};
export default ListUsers;

View file

@ -1,268 +0,0 @@
import React, {useEffect, useMemo, useState} from 'react';
import { DeviceUser } from '../../../../../../store/useDeviceStore';
import styled from 'styled-components';
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
import Pagination from "../../../../../UI/Pagination";
import {
useDisabledColumns,
useNameSetting,
useOrdersColumns,
usePaginateSetting
} from "../../../../../../hooks/useSettings";
interface TableScannerProps {
devices: DeviceUser[];
}
const TableContainer = styled.div`
width: 100%;
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textColor};
background: ${({ theme }) => theme.primary};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const TableRow = styled.tr<{ clickable?: boolean }>`
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.background};
}
`;
const TableCell = styled.td`
padding: 16px 24px;
font-size: 14px;
color: ${({ theme }) => theme.text};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
button{
background-color: transparent;
border: none;
cursor: pointer;
}
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
display: inline-block;
border-radius:4px;
`
export const TableScanner = ({ devices }: TableScannerProps) => {
const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const ordersColumns = useOrdersColumns();
const disabledColumns = useDisabledColumns();
const paginateSetting = usePaginateSetting();
const {t} = useTranslation('common');
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
console.log(ordersColumns);
useEffect(() => {
setDevicesPerPage(paginateSetting);
}, [paginateSetting]);
const openModalNote = (e: React.MouseEvent, device: DeviceUser) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
const defaultOrder = useMemo(() => ([
t('settings-status'),
t('settings-name'),
t('settings-vendor'),
t('settings-ipAddress'),
t('settings-openPorts'),
t('settings-lastActivity'),
t('settings-note'),
t('settings-action'),
]), [t]);
const normalizedOrders = useMemo(
() => (Array.isArray(ordersColumns) && ordersColumns.length ? ordersColumns : defaultOrder),
[ordersColumns, defaultOrder]
);
const normalizedDisabled = useMemo(
() => (Array.isArray(disabledColumns) ? disabledColumns : []),
[disabledColumns]
);
const visibleColumns = useMemo(() => {
const normalizeColumnName = (column: string) => column?.toLowerCase();
const columnToKey = (column: string) => {
const col = normalizeColumnName(column);
switch (col) {
case 'settings-status':
case normalizeColumnName(t('settings-status')):
case 'онлайн':
case 'status':
return 'status';
case 'settings-name':
case normalizeColumnName(t('settings-name')):
case 'имя':
case 'name':
return 'name';
case 'settings-vendor':
case normalizeColumnName(t('settings-vendor')):
case 'производитель':
case 'vendor':
return 'vendor';
case 'settings-ipaddress':
case normalizeColumnName(t('settings-ipAddress')):
case 'ip адрес':
case 'ip address':
case 'ip':
return 'ip';
case 'settings-openports':
case normalizeColumnName(t('settings-openPorts')):
case 'открытые порты':
case 'open ports':
case 'ports':
return 'ports';
case 'settings-lastactivity':
case normalizeColumnName(t('settings-lastActivity')):
case 'последняя активность':
case 'last activity':
case 'activity':
return 'activity';
case 'settings-note':
case normalizeColumnName(t('settings-note')):
case 'заметка':
case 'note':
return 'note';
case 'settings-action':
case normalizeColumnName(t('settings-action')):
case 'действие':
case 'action':
return 'action';
default:
return '';
}
};
return normalizedOrders
.map((column, index) => ({ column, index }))
.filter(({ index }) => !normalizedDisabled.includes(index))
.map(({ column }) => columnToKey(column))
.filter(Boolean);
}, [normalizedOrders, normalizedDisabled, t]);
// Пагинация
const indexOfLastDevice = currentPage * devicesPerPage;
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
const currentDevices = devices.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);
};
// Рендер ячейки таблицы
const renderTableCell = (column: string, item: DeviceUser) => {
switch (column) {
case 'status':
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
case 'name':
return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>;
case 'vendor':
return <TableCell key="vendor">{item.vendor}</TableCell>;
case 'ip':
return <TableCell key="ip">{item.ip}</TableCell>;
case 'ports':
return <TableCell key="ports">{item.open_ports.join(', ') || '-'}</TableCell>;
case 'activity':
return <TableCell key="activity">{item.last_seen}</TableCell>;
case 'note':
return <TableCell key="note">{item.note || '-'}</TableCell>;
case 'action':
return (
<TableCell key="action">
<button onClick={(e) => openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</button>
</TableCell>
);
default:
return null;
}
};
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
{visibleColumns.map(column => {
switch (column) {
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
case 'name': return <TableHeaderCell key="name">{t('settings-name')}</TableHeaderCell>;
case 'vendor': return <TableHeaderCell key="vendor">{t('settings-vendor')}</TableHeaderCell>;
case 'ip': return <TableHeaderCell key="ip">{t('settings-ipAddress')}</TableHeaderCell>;
case 'ports': return <TableHeaderCell key="ports">{t('settings-openPorts')}</TableHeaderCell>;
case 'activity': return <TableHeaderCell key="activity">{t('settings-lastActivity')}</TableHeaderCell>;
case 'note': return <TableHeaderCell key="note">{t('settings-note')}</TableHeaderCell>;
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
default: return null;
}
})}
</tr>
</thead>
<tbody>
{currentDevices.map((item, index) => (
<TableRow key={index}>
{visibleColumns.map(column => renderTableCell(column, item))}
</TableRow>
))}
</tbody>
</StyledTable>
{totalPages > 1 && (
<Pagination
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
)}
</TableContainer>
);
};

View file

@ -1,46 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import {TableScanner} from "./TableScanner";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: (replay?: boolean) => void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
return (
<>
<ScanBtn onClick={() => handleScan()} disabled={loading}>
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -1,247 +0,0 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import {useEffect, useState, useTransition} 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";
import IpScanning from "./modules/IpScanning";
import {useTranslation} from "../../../../../utils/i18n/hooks";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 80px 50px 40px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`;
const TopWrapper = styled.div`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`
const TopTitle = styled.h2`
font-size: 32px;
width: max-content;
text-align: center;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
export default function UserScanner() {
const {t} = useTranslation('common')
const { ipAddress } = useIpStore();
const [ipScanning, setIpScanning] = useState('');
const [loading, setLoading] = useState(false);
const [isValid, setIsValid] = useState(true);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
const validateIp = (ip: string) => {
const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
return regex.test(ip);
};
// Сканирование
const handleScan = async (replay: boolean) => {
try {
if (ipScanning && !validateIp(ipScanning)) {
setIsValid(false);
return;
}
setIsValid(true);
if (!replay) {
setLoading(true);
}
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress, ipScanning);
console.log(res)
setDevices(res.data.devices || []);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([]);
} finally {
setLoading(false);
}
};
// Интервально сканирование
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: t('userScanner-visible_table') },
{ value: 2, label: t('userScanner-visible_list') },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
setSelectedView(option);
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TopWrapper>
{(!scanInitiated || loading) && (<TopTitle>{t('userScanner-title')}</TopTitle>)}
<IpScanning
value={ipScanning}
setIpScanning={setIpScanning}
loading={loading}
isValid={isValid}
setIsValid={setIsValid}
/>
</TopWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
)}
{loading ? (
<LoadingText>
{t('userScanner-loading')} <ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);
}

View file

@ -1,209 +0,0 @@
import styled, { keyframes } from "styled-components";
import React, { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store";
import {useTranslation} from "../../../../../utils/i18n/hooks";
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:center ;
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
text-align: center;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: scale(1);
.circel {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
const ScanBtn = styled.button<{scanInitiated: boolean}>`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: transform .4s ease;
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")};
&:hover{
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")} scale(1.03);
}
`
export function WifiScanner() {
const { ipAddress } = useIpStore();
//const ipAddress = '192.168.1.148'
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const { t } = useTranslation('common');
const handleScan = async () => {
setWifiNetworks([]);
setCurrentNetwork(null);
setLoading(true);
setScanInitiated(true);
try {
const res = await NetworkService.scanWifi(ipAddress);
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error(t('wifiScanner-scanError'), err);
setWifiNetworks([]);
} finally {
setLoading(false);
}
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
<span className="ring1 circel"></span>
<span className="ring2 circel"></span>
<span className="ring3 circel"></span>
{wifiNetworks.map((network, index) => (
<WifiDot
key={network.bssid}
network={network}
index={index}
total={wifiNetworks.length}
onSelect={setCurrentNetwork}
/>
))}
{currentNetwork && (
<ModalWifiScaner
onClose={() => setCurrentNetwork(null)}
network={currentNetwork}
/>
)}
</UserLocate>
{loading ? (
<LoadingText>
{t('wifiScanner-scanning')} <ScanningText/>
</LoadingText>
) : (
<ScanBtn
onClick={handleScan}
disabled={loading}
scanInitiated={wifiNetworks.length > 0}
>
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
</ScanBtn>
)}
</Wrapper>
);
}

View file

@ -1,228 +0,0 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import {WifiNetworkScanner} from "./WifiDot";
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useIpStore, useModalContext} from "../../../../../../store";
import {Status} from "../../../../../customeNodes/nodes/networkInterface/networkNode/style";
import SignalSVG from "../../../../../UI/signalSvg";
import { useTheme } from 'styled-components';
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
import {NetworkService} from "../../../../../../api/request";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const Modal = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
background-color: ${(props) => props.theme.secondary};
border-radius: 24px;
padding: 32px 28px;
bottom: -40%;
left: 220px;
position: absolute;
z-index: 20;
min-width: 300px;
&:hover .close-btn{
opacity: 1;
visibility: visible;
}
`
const CloseBtn = styled.div`
position: absolute;
top: -20px;
right: -20px;
cursor: pointer;
background-color: ${(props) => props.theme.primary};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
transition: all .4s ease;
opacity: 0;
visibility: visible;
&:hover {
background-color: ${(props) => props.theme.hoverBtn};
}
`
const ModalTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
line-height: 1;
margin-bottom: 32px;
h4{
font-weight: normal;
}
.functionalIcons{
display: flex;
justify-content: center;
align-items: center;
gap: 18px;
}
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
justify-content: space-between;
align-items: center;
width: 100%;
`
const ModalRow = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
gap: 12px;
width: 100%;
font-size: 14px;
h6{
margin: 0;
padding: 0;
font-size: 1em;
}
p{
max-width:180px;
text-align: right;
}
`
const ModalConnect = styled.button`
color: ${(props) => props.theme.primary};
padding: 10px 12px;
font-size: 18px;
background-color: ${(props) => props.theme.accent};
border-radius: 12px;
margin-top: 4px;
border: none;
outline: none;
width: 100%;
cursor: pointer;
transition: .4s ease;
&:hover{
background-color: ${(props) => props.theme.accentHover};
}
`
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
const { openModal } = useModalContext();
const { ipAddress } = useIpStore();
const theme = useTheme()
const [signal, setSignal] = useState(1);
const { wifiNetworks } = useNetworkStore();
const [isConnected, setIsConnected] = useState(false);
const { t } = useTranslation('common');
useEffect(() => {
const getStatus = async () => {
try {
const response = await NetworkService.statusNetwork(ipAddress);
const { status, ssid } = response.data;
setIsConnected(
!!network?.ssid &&
ssid === network.ssid &&
status === 'connected'
);
} catch (error) {
setIsConnected(false);
}
};
getStatus();
}, [network?.ssid, ipAddress]);
const currentNetwork = wifiNetworks.find(n => n.bssid === network.bssid) || network;
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['7'], {
address: currentNetwork.ssid,
currentNote: currentNetwork.note || ''
});
};
const openModalNetwork= (e: React.MouseEvent) => {
e.stopPropagation();
if(isConnected){
openModal(ModalMap['9'], {
address: currentNetwork.ssid,
});
}else{
openModal(ModalMap['8'], {
address: currentNetwork.ssid,
});
}
};
useEffect(() => {
const perc = Number(network.signal);
switch (true) {
case perc >= -50: setSignal(5); break;
case perc >= -60: setSignal(4); break;
case perc >= -70: setSignal(3); break;
case perc >= -80: setSignal(2); break;
default: setSignal(1); break;
}
}, [network.signal])
return (
<Modal>
<CloseBtn className={'close-btn'} onClick={onClose}>
<IconComponent name={buttonNames[12]} width="30" height="30" />
</CloseBtn>
<ModalTitle>
<h4>{network.ssid}</h4>
<div className={'functionalIcons'}>
<span onClick={openModalNote} style={{cursor: "pointer"}}>
<IconComponent name={buttonNames[16]} width="30" height="30" />
</span>
<Status>
<SignalSVG signal={signal} size={'36px'} color={theme.accent} />
</Status>
</div>
</ModalTitle>
<ModalContent>
<ModalRow>
<h6>{t('wifiModal-bssid')}:</h6>
<p>{network.bssid}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-vendor')}:</h6>
<p>{network.vendor || t('wifiModal-unknown')}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-signal')}:</h6>
<p>{network.signal} dBm</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-channel')}:</h6>
<p>{network.channel}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-encryption')}:</h6>
<p>{network.encryption || t('wifiModal-none')}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-frequency')}:</h6>
<p>{network.frequency} MHz</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-hidden')}:</h6>
<p>{network.hidden ? t('common-yes') : t('common-no')}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-note')}:</h6>
<p>{currentNetwork.note || '-'}</p>
</ModalRow>
<ModalConnect onClick={openModalNetwork}>
{isConnected ? t('wifiModal-disconnect') : t('wifiModal-connect')}
</ModalConnect>
</ModalContent>
</Modal>
);
};
export default ModalWifiScaner;

View file

@ -1,81 +0,0 @@
import React from 'react';
import styled from "styled-components";
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
//Точка вайфай
const Dot = styled.div<{ top: string; left: string }>`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
transform: translate(-50%, -50%);
user-select: none;
transition: .4s;
text-wrap: nowrap;
&:hover {
transform: translate(-50%, -50%) scale(1.1);
}
`;
const getPositionInRing = (index: number, total: number, ringSize: number) => {
const angle = (index * (2 * Math.PI / total)) + (Math.PI / total * (index % 2));
const innerRadius = ringSize * 0.4;
const outerRadius = ringSize * 0.5;
const radius = innerRadius + (outerRadius - innerRadius) * (0.3 + 0.7 * (index % 10) / 10);
return {
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius
};
};
const WifiDot = ({ network, index, total, onSelect }: {
network: WifiNetworkScanner,
index: number,
total: number,
onSelect: (n: WifiNetworkScanner) => void
}) => {
const getPosition = (signal: number, index: number, total: number) => {
let strength: 1 | 2 | 3;
if (signal >= -50) {
strength = 3;
} else if (signal >= -70) {
strength = 2;
} else {
strength = 1;
}
const ringSizes = { 1: 550, 2: 400, 3: 250 };
const ringSize = ringSizes[strength];
const offset = getPositionInRing(index, total, ringSize);
return {
top: `calc(50% + ${offset.y}px)`,
left: `calc(50% + ${offset.x}px)`
};
};
const position = getPosition(network.signal, index, total);
return (
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
{network.ssid}
</Dot>
);
};
export default WifiDot;

View file

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

View file

@ -1,37 +0,0 @@
import {useSettingsStore} from "../store/settings/settingsStore";
import {defaultLanguage, Language} from "../utils/i18n/config";
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 useThemeSettings = () => {
return useSettingsStore(state => state.getSettingValue('theme'));
};
//Интервальное сканирование под узлов
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 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 useLanguage = () => {
return useSettingsStore(
state => state.getSettingValue<Language>('language')
) ?? defaultLanguage;
};

View file

@ -1,30 +0,0 @@
const createAppearanceSettings = () => {
return [
{
id: 'theme',
sectionId: 'settings-sections-appearance',
label: 'settings-colorTheme',
type: 'select',
defaultValue: 'light',
options: [
{ value: 'light', label: 'settings-light' },
{ value: 'dark', label: 'settings-dark' },
{ value: 'system', label: 'settings-system' }
],
},
{
id: 'language',
sectionId: 'settings-sections-appearance',
label: 'settings-interfaceLanguage',
type: 'select',
defaultValue: 'ru',
options: [
{ value: 'ru', label: 'settings-russian' },
{ value: 'en', label: 'settings-english' },
],
},
] as const;
};
export { createAppearanceSettings };

View file

@ -1,55 +0,0 @@
import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNetworkSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{
id: 'clean-network',
type: 'button',
label: 'settings-clearSessionData',
sectionId: 'settings-sections-accessPoints',
defaultValue: 'settings-clear',
action: async ()=>{
try {
const res = await NetworkService.clearNetwork(ipAddress);
console.log(res);
}catch (e){
console.error(e);
alert('settings-dataNotDeleted' + e)
}
},
},
{
id: 'export-network',
type: 'button',
label: 'settings-exportNetworkData',
sectionId: 'settings-sections-accessPoints',
defaultValue: 'settings-export',
action: async () => {
try {
const response = await NetworkService.exportNetwork(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: 'settings-saveNetworksTitle',
defaultPath: "network_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert('settings-exportSuccess');
}
} catch (e: any) {
alert('settings-exportError' + e.message);
}
},
},
] as const;
};
export { createNetworkSettings };

View file

@ -1,119 +0,0 @@
import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNodesSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{
id: 'rescan-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-intervalScanning',
type: 'boolean',
defaultValue: false,
},
{
id: 'interval-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-scanInterval',
type: 'number',
defaultValue: 60,
min: 1,
max: 2880
},
{
id: 'netnames-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-displayNames',
type: 'select',
defaultValue: 'ip',
options: [
{ value: 'ip', label: 'settings-ipAddress' },
{ value: 'unknown', label: 'settings-unknown' },
],
},
{
id: 'paginate-nodes',
type: 'number',
label: 'settings-rowsPerPage',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 20,
min: 1,
},
{
id: 'customize-nodes',
type: 'button',
label: 'settings-displaySettings',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 'settings-configure',
},
{
id: 'clean-nodes',
type: 'button',
label: 'settings-clearSessionData',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 'settings-clear',
action: async ()=>{
try {
const res = await NetworkService.clearSubnet(ipAddress);
console.log(res)
}catch (e){
console.error(e);
alert('settings-dataNotDeleted' + e)
}
},
},
{
id: "export-nodes",
type: "button",
label: "settings-exportData",
sectionId: "settings-sections-subnetNodes",
defaultValue: "settings-export",
action: async () => {
try {
const response = await NetworkService.exportDevices(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: "settings-saveNodesTitle",
defaultPath: "nodes_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert("settings-exportSuccess");
}
} catch (e: any) {
alert("settings-exportError" + e.message);
}
},
},
{
id: 'table-columns-order',
type: 'hidden',
label: '',
sectionId: 'settings-sections-subnetNodes',
defaultValue: [
'settings-status',
'settings-name',
'settings-vendor',
'settings-ipAddress',
'settings-openPorts',
'settings-lastActivity',
'settings-note',
'settings-action'
]
},
{
id: 'table-columns-disabled',
label: '',
sectionId: 'settings-sections-subnetNodes',
type: 'hidden',
defaultValue: []
}
] as const;
};
export { createNodesSettings };

View file

@ -1,139 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {createNodesSettings} from "./config/nodesSettings";
import { createNetworkSettings } from './config/networkSettings';
import {createAppearanceSettings} from "./config/appearanceSettings";
type SettingValue = any;
type SettingsDictionary = Record<string, SettingValue>;
export type Setting = {
readonly id: string;
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button' | 'hidden';
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;
resetToDefaults: () => void;
getSettingValue: <T = any>(id: string) => T;
getNumberSetting: (id: string, fallback?: number) => number;
updateSettingsConfig: () => void;
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set, get) => ({
settingsConfig: [],
settingsValues: {},
activeSection: null,
pendingChanges: {},
setActiveSection: (sectionId) => set({ activeSection: sectionId }),
updateSetting: (id, value) =>
set((state) => ({
pendingChanges: { ...state.pendingChanges, [id]: value }
})),
resetToDefaults: () => set({
settingsValues: Object.fromEntries(
get().settingsConfig.map(setting => [
setting.id,
setting.defaultValue
])
),
pendingChanges: {}
}),
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 });
},
updateSettingsConfig: () => {
const defaultSettings: Setting[] = [
...createNodesSettings(),
...createNetworkSettings(),
...createAppearanceSettings(),
];
set({ settingsConfig: defaultSettings });
},
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();
}
}
)
);

View file

@ -1,51 +0,0 @@
import { create } from 'zustand';
export interface DeviceUser {
ip: string;
mac: string;
hostname: string;
vendor: string;
open_ports: string[];
status: string;
last_seen: string;
note: string;
}
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
interface NetworkStore {
devices: DeviceUser[];
setDevices: (devices: DeviceUser[]) => void;
addDevice: (device: DeviceUser) => void;
wifiNetworks: WifiNetworkScanner[];
setWifiNetworks: (networks: WifiNetworkScanner[]) => void;
addWifiNetwork: (network: WifiNetworkScanner) => void;
clearAll: () => void;
}
const useNetworkStore = create<NetworkStore>((set) => ({
devices: [],
setDevices: (devices) => set({ devices }),
addDevice: (device) =>
set((state) => ({ devices: [...state.devices, device] })),
wifiNetworks: [],
setWifiNetworks: (networks) => set({ wifiNetworks: networks }),
addWifiNetwork: (network) =>
set((state) => ({ wifiNetworks: [...state.wifiNetworks, network] })),
clearAll: () => set({ devices: [], wifiNetworks: [] })
}));
export default useNetworkStore;

View file

@ -1,11 +0,0 @@
export const supportedLanguages = {
en: 'English',
ru: 'Русский',
} as const;
export type Language = keyof typeof supportedLanguages;
export const defaultLanguage = 'ru';
export const namespaces = ['common', 'components'] as const;
export type Namespace = (typeof namespaces)[number];

View file

@ -1,11 +0,0 @@
import { Namespace } from './config';
import { useI18n } from './provider';
export const useTranslation = (ns?: Namespace) => {
const { t, language } = useI18n();
return {
t: (key: string) => t(key, ns),
language,
};
};

View file

@ -1,92 +0,0 @@
'use client'
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { defaultLanguage, Language, Namespace, namespaces, supportedLanguages } from './config';
type Translation = Record<string, string>;
type NamespaceTranslations = Record<Namespace, Translation>;
type LanguageTranslations = Record<Language, NamespaceTranslations>;
const I18nContext = createContext<{
language: Language;
t: (key: string, namespace?: Namespace) => string;
}>({
language: defaultLanguage,
t: (key: string) => key,
});
interface I18nProviderProps {
children: React.ReactNode;
initialLanguage: Language;
}
export const I18nProvider = ({ children, initialLanguage }: I18nProviderProps) => {
const [language, setLanguage] = useState<Language>(initialLanguage);
const [translations, setTranslations] = useState<LanguageTranslations>({} as LanguageTranslations);
// Загрузка переводов
useEffect(() => {
const loadTranslations = async () => {
const newTranslations: LanguageTranslations = { ...translations };
await Promise.all(
(Object.keys(supportedLanguages) as Language[]).map(async (lang) => {
if (!newTranslations[lang]) {
newTranslations[lang] = {} as NamespaceTranslations;
await Promise.all(
namespaces.map(async (ns) => {
try {
const res = await fetch(`/locales/${lang}/${ns}.json`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
newTranslations[lang][ns] = data;
} catch (e) {
console.error(`Failed to load ${lang}/${ns} translations`, e);
newTranslations[lang][ns] = {};
}
})
);
}
})
);
setTranslations(newTranslations);
};
loadTranslations();
}, []);
useEffect(() => {
setLanguage(initialLanguage);
}, [initialLanguage]);
const t = useMemo(() => {
return (key: string, namespace: Namespace = 'common') => {
return translations[language]?.[namespace]?.[key] || key;
};
}, [language, translations]);
const contextValue = useMemo(() => ({
language,
t,
}), [language, t]);
return (
<I18nContext.Provider value={contextValue}>
{children}
</I18nContext.Provider>
);
};
export const useI18n = () => {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useI18n must be used within I18nProvider');
}
return context;
};

File diff suppressed because it is too large Load diff

View file

@ -1,56 +0,0 @@
## Клиент приложения ГоNets
### Общие сведения
ГоNets - приложение для работы с сетевыми ресурсами в удобном и понятном графическом виде. Данное приложение разделено на серверную и клиентскую часть. Сервер сканирует, обрабатывает и преобразует информацию по сетям в простой и понятный JSON формат, который в свою очередь приходит на клиент, где данные проходят ряд обработок и формируются в графическое отображение.
Клиент приложения ГоNets написан на языке Typescript с использованием фреймворка React. Для более понятного отображения информации была выбрана библиотека ReatFlow, позволяющая легко и просто выстраивать различные графы, схемы, блоки и их связи. Так же, для того, чтобы приложение запускалось как десктопное был выбран метод обёртки собранной (build) версии в облочку Electron.
### Зависимости клиенсткой части
Далее будет представлен список необходимых npm зависимостей для разработки приложения. Уточню, что необходимы они исключительно во время разработки и сборки, собранная версия работает спокойно без них.
Зависимости при разработки:
```
"dependencies": {
"@iconify/icons-ic": "^1.2.13",
"@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5",
"axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-flow-renderer": "^10.3.17",
"react-redux": "^9.2.0",
"react-router-dom": "^7.0.2",
"styled-components": "^6.1.13",
"zustand": "^5.0.3"
},
"devDependencies": {
"@iconify-json/ic": "^1.2.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.4.1",
"electron": "^34.0.2",
"electron-packager": "^17.1.2",
"esbuild-wasm": "^0.17.12",
"typescript": "^4.9.5",
"vite": "^6.3.4"
}
```
### Запуск и сборка
Для того, чтобы работать с исходным кодом, необходимо склонировать проект с ветки frontend, перейти в директорию `FRONT/DEV/` и выполнить установку зависимостей с помощью `npm install`, возможно потребуется сделать после этого `npm audit --fix` Весь исходный код, находится непосредственно в директории `DEV`, собранная же версия в директории `BUILD`.
Для сборки приложения и обёртки его в электрон необходимо выполнить команду `npm run package`, далее перейти в созданную после команды директорию `dist`, от туда в `gonets-for-linux-64x/resources/app`, здесь нужно выполнить `npm run build`, в появившейся директории `dist` отредактировать файл `index.html`, таким образом, что везде где стоит `/assets` нужно прописать `./assets`. Проверить запуск приложения, можно прописав в терминале `./gonets` из директории `FRONT/DEV/dist/gonets-for-linux-64x`, где и лежит исполняемвый файл.
После удачного билда можно переместить всё содержимое `FRONT/DEV/dist/gonets-for-linux-64x/` в `/FRONT/BUILD`
Дополнительно отмечу, что входной точкой/файлом настройки самого электрона является файл `main.js`

View file

@ -1,72 +1,3 @@
<p align="center"><img src="assets/gonets_logo.svg" alt="Логотип ГоNets" width="15%"></p>
<p align="center"><img src="assets/gonets_text.svg" alt="Написание ГоNets" width="70%"></p>
# ОГЛАВЛЕНИЕ
- [Ссылки](#cссылки:)
- [Что это?](#что-это?)
- [Почему MVP?](#почему-mvp?)
- [Текущий статус Приложения](#текущий-статус-приложения)
- [Стабильные версии:](#стабильные-версии:)
- [Правила наименования](#правила-наименования)
- [Коды компонентов](#коды-компонентов)
- [Нумерация версий](#нумерация-версий)
## Ссылки:
Здесь Вы найдете неполный список ссылок на официальные ресурсы:
- [WEB-страница приложения](https://gonets.gossys.dev)
- [Гибридная Офисная Система](https://hybryd.ru)
- [HELPDESK](https://helpdesk.gossys.dev/)
- [Раздел Базы знаний](https://helpdesk.gossys.dev/knowledgebase.php?category=17)
- [О Разработчике](https://gossys.dev/)
- [Документация](https://wiki.gossys.dev/docs/gonets)
## Что это?
**ГоNets** - это кроссплатформенное **MVP** - приложение для управления сетевыми настройками локального и удаленных Клиентов и проведения востребованных Пользователями и Администраторами сетевых операций.
Все это происходит с использованием нового, интуитивно понятного интерфейса, с полным интерактивным взаимодействием.
<p align="center"><img src="assets/work_scr.jpg" alt="Экран ГоNets" width="90%"></p>
## Почему MVP?
**ГоNets** - приложение, созданное для тестирования данного интерфейса, применительно к управлению настройками операционной системы. На основе Вашей реакции нами будут приняты решения о дальнейшей модернизации проекта, либо его полного перехода на более привычный интерфейс.
## Текущий статус Приложения
Текущий статус Приложения - В разработке. Чего еще ожидать от приложения, размещенного на Гит? Одновременно с этим - оно находится и в тестировании. Подробную информацию Вы можете получить в в разделах Задачи и Проекты.
### Стабильные версии:
- Версия для Deb-based: GNB-DEB64-0.0.3
- Версия для RPM-based: В разработке
- Версия для Windows: В разработке
- Версия для MacOS (Intel): В разработке
- Версия для MacOS (ARM): В разработке
## Правила наименования
Общий шаблон наименования - **CODE-ARCH-NUM-SPEC**, где:
- **CODE** - Код компонента (См. таблицу ниже);
- **ARCH** - Архитектура компонента в соответствии с международной классификацией;
- **NUM** - Номер версии, в соответствии с "Правилами нумерации версий";
- **SPEC** - Дополнительное буквенное обозначение в общепринятом формате. (Например RC, Beta, PreRelease)
### Коды компонентов
|Наименование|Описание|Код|
|---|---|---|
|**Project GoNets (Проект, Проект GoNets, GoNets, Программный комплекс, ПК)**| Программный комплекс вцелом.| ***GNP*** |
|**Server (Сервер)**| Программный компонент, отвечающий за получение и обработку данных, формирующий данные для последующего отображения|***GNS***|
|**Server Client (Серверный Клиент)**| Программный компонент, отвечающий за сбор данных с ПК или Сервера и передающий их для обработки|***GNK***|
|**Client (Клиент)** |Программный компонент, отвечающий за визуализацию и взаимодействие с Пользователем|***GNC***|
|**Bundle (Объединенное приложение)**| Версия приложения **ГоNets**, собранная из нескольких программных компонентов, объединенная одним инсталлятором для обспечения полного функционала|***GNB***|
### Нумерация версий
В проекте действует следующая система нумерации версий:
**XX.YY.ZZ** - где XX.YY.ZZ, обозначают:
- **ZZ**: текущий номер стабильного билда;
- **YY**: текущий номер минорной версии (Версии с полной обратной совместимостью с предыдущими, без кардинальных отличий в интерфейсе или функционале.)
- **XX**: Текущий номер мажорной версии (Версии с частичной или отсутствующей обратной совместимостью с предыдущими, либо с кардинальными отличиями в интерфейсе или функционале.)
# GoNets
Общий репозиторий для GoNets начиная с версии 0.0.1

2
SRV/.gitignore vendored
View file

@ -6,5 +6,3 @@ __pycache__
.wheels
.vendor
.python3-deb
storage/subnets/
storage/wifi/

View file

@ -1,29 +1,5 @@
"""
Маршрутизация API-версии v1 для GONETS.
Модуль агрегирует и монтирует роутеры отдельных доменных областей под общим
корневым роутером. Каждый подключенный модуль предоставляет набор эндпоинтов
со своим префиксом и тегами для группировки в документации FastAPI.
Структура маршрутов:
- `/ufw` (UFW): управление правилами и состоянием Uncomplicated Firewall.
- `/ip` (IP): проверки и применение IP-конфигураций для сетевых профилей.
- `/network` (Network): получение и визуализация состояния интерфейсов,
управление позициями UI-контейнеров.
- `/auth` (Auth): регистрация, аутентификация (JWT), сессии.
- `/subnet` (Subnet): сканирование подсетей, заметки к устройствам,
взаимодействие с Wi-Fi (скан/подключение/отключение/статус).
- `WebSocket`: обработчики взаимодействия по WS (без префикса, путь внутри
модуля), периодическая трансляция сетевых данных.
- `/settings` (Settings): сервисные операции над локальными JSON-хранилищами
(очистка данных подсетей/wi-fi и пр.).
Назначение:
- Централизованная точка подключения доменных роутеров к приложению.
- Единообразие тегов и префиксов для автогенерации схемы и документации.
"""
from fastapi import APIRouter
from .v1 import ufw, websocket, ip, network, auth, subnet, settings
from api.v1 import ufw, websocket, ip, network, auth
router = APIRouter()
@ -32,7 +8,3 @@ router.include_router(websocket.router, tags=["WebSocket"])
router.include_router(ip.router, prefix="/ip", tags=["IP"])
router.include_router(network.router, prefix="/network", tags=["Network"])
router.include_router(auth.router, prefix="/auth", tags=["Auth"])
router.include_router(subnet.router, prefix="/subnet", tags=["Subnet"])
router.include_router(settings.router, prefix="/settings", tags=["Settings"])

View file

@ -1,10 +1,9 @@
# api/v1/auth.py
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
from services.auth_service import AuthService
from models.auth import UserCreate, UserLogin, UserResponse, Token
from storage.json_storage import JsonStorage
from utils.i18n import get_translations, _
from babel.support import Translations
import logging
import os
@ -15,24 +14,10 @@ router = APIRouter()
auth_service = AuthService()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate, translations: Translations = Depends(get_translations)):
"""Регистрирует нового пользователя.
Создаёт нового пользователя на основе предоставленных данных и возвращает
информацию о зарегистрированном пользователе.
Args:
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
translations: Объект переводов для текущего языка.
Returns:
UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin).
Raises:
HTTPException: Если пользователь уже существует (status_code=400) или произошла
внутренняя ошибка (status_code=500).
"""
async def register(user: UserCreate):
"""Регистрирует нового пользователя"""
try:
db_user = auth_service.register(user)
logger.info(f"Пользователь {user.username} успешно зарегистрирован")
@ -40,116 +25,62 @@ async def register(user: UserCreate, translations: Translations = Depends(get_tr
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Error in register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось зарегистрировать пользователя"))
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя")
@router.post("/login", response_model=Token)
async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations)):
"""Авторизует пользователя и создаёт сессию.
Проверяет учетные данные пользователя и возвращает токен и session_id для
последующей авторизации.
Args:
user (UserLogin): Объект с данными для входа (username, password).
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns:
Token: Объект с access_token и session_id для авторизации.
Raises:
HTTPException: Если учетные данные неверны (status_code=401) или произошла
внутренняя ошибка (status_code=500).
"""
async def login(user: UserLogin, request: Request):
"""Авторизует пользователя и создаёт сессию"""
try:
client_id = request.client.host
logger.debug(f"Попытка входа в систему клиента {client_id}")
logger.debug(f"Попытка авторизации для client_id {client_id}")
result = auth_service.login(user, client_id)
logger.info(f"Успешная авторизация для клиента {client_id}, Идентификатор сессии: {result['session_id']}")
logger.info(f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}")
return result
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка при авторизации: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось авторизовать пользователя"))
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
@router.get("/me")
async def get_current_user(request: Request, translations: Translations = Depends(get_translations)):
"""Возвращает данные текущего пользователя и токен.
Проверяет активную сессию по client_id и возвращает информацию о пользователе
и токен для дальнейшего использования.
Args:
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и
"token_type" (str, всегда "bearer").
Raises:
HTTPException: Если сессия не найдена или истекла (status_code=401) или
произошла внутренняя ошибка (status_code=500).
"""
async def get_current_user(request: Request):
"""Возвращает данные текущего пользователя и токен"""
try:
client_id = request.client.host
logger.debug(f"Запрос /me для клиента {client_id}")
logger.debug(f"Запрос /me для client_id {client_id}")
result = auth_service.get_user_by_session(client_id)
return {
"user": result["user"],
"access_token": result["access_token"],
"token_type": translations.gettext("bearer"),
"token_type": "bearer"
}
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка при get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить данные пользователя"))
logger.error(f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
@router.get("/logout")
async def logout(request: Request, translations: Translations = Depends(get_translations)):
"""Выход из системы, удаление сессии.
Удаляет активную сессию пользователя по client_id.
Args:
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns:
dict: Пустой словарь или сообщение об успешном выходе.
Raises:
HTTPException: Если сессия не найдена (status_code=401) или произошла
внутренняя ошибка (status_code=500).
"""
async def logout(request: Request):
"""Выход из системы, удаление сессии"""
try:
client_id = request.client.host
logger.debug(f"Запрос на выход из системы для клиента {client_id}")
logger.debug(f"Запрос logout для client_id {client_id}")
return auth_service.logout(client_id)
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Не удалось выйти из системы: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось выйти из системы"))
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось выйти из системы")
@router.get("/check_json")
async def check_users_file(translations: Translations = Depends(get_translations)):
"""Проверяет, существует ли файл users.json и содержит ли он пользователей.
Возвращает информацию о состоянии файла и наличии пользователей.
Returns:
dict: Словарь с ключами:
- "exists" (bool): Существует ли файл.
- "has_users" (bool): Содержит ли файл пользователей.
- "message" (str): Сообщение с описанием результата.
Raises:
HTTPException: Если произошла ошибка при проверке файла (status_code=500).
"""
async def check_users_file():
"""Проверяет, существует ли файл users.json и содержит ли он пользователей"""
try:
file_path = "/etc/gonets/users.json"
storage = JsonStorage(file_path=file_path)
@ -159,7 +90,7 @@ async def check_users_file(translations: Translations = Depends(get_translations
return {
"exists": False,
"has_users": False,
"message": translations.gettext("Файл users.json не существует"),
"message": "Файл users.json не существует"
}
data = storage._read_data()
@ -169,15 +100,15 @@ async def check_users_file(translations: Translations = Depends(get_translations
return {
"exists": True,
"has_users": True,
"message": translations.gettext("Файл users.json существует и содержит пользователей"),
"message": "Файл users.json существует и содержит пользователей"
}
else:
logger.info(f"Файл {file_path} существует, но не содержит пользователей")
return {
"exists": True,
"has_users": False,
"message": translations.gettext("Файл users.json существует, но не содержит пользователей"),
"message": "Файл users.json существует, но не содержит пользователей"
}
except Exception as e:
logger.error(f"Ошибка при проверке файла: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при проверке файла"))
logger.error(f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Ошибка при проверке файла")

View file

@ -31,9 +31,7 @@ class SuccessResponse(BaseModel):
@router.get(
"/zones",
response_model=List[str],
responses={500: {
"model": ErrorResponse
}},
responses={500: {"model": ErrorResponse}},
summary="Get all available zones",
)
async def get_zones() -> List[str]:
@ -55,9 +53,7 @@ async def get_zones() -> List[str]:
@router.get(
"/zones/default",
response_model=str,
responses={500: {
"model": ErrorResponse
}},
responses={500: {"model": ErrorResponse}},
summary="Get default zone",
)
async def get_default_zone() -> str:
@ -79,14 +75,7 @@ async def get_default_zone() -> str:
@router.put(
"/zones/default/{zone}",
response_model=SuccessResponse,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Set default zone",
)
async def set_default_zone(
@ -105,8 +94,9 @@ async def set_default_zone(
firewalld.set_default_zone(zone)
return SuccessResponse(message=f"Default zone set to {zone}")
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -117,18 +107,10 @@ async def set_default_zone(
@router.get(
"/zones/{zone}",
response_model=FirewallZone,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Get zone configuration",
)
async def get_zone(zone: str = Path(...,
description="Zone name")) -> FirewallZone:
async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZone:
"""
Get detailed configuration for a specific zone.
@ -140,13 +122,16 @@ async def get_zone(zone: str = Path(...,
"""
try:
zone_info = firewalld.get_zone_settings(zone)
return FirewallZone(name=zone,
target=FirewallTarget.DEFAULT,
interfaces=[],
**zone_info)
return FirewallZone(
name=zone,
target=FirewallTarget.DEFAULT,
interfaces=[],
**zone_info
)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -157,13 +142,10 @@ async def get_zone(zone: str = Path(...,
@router.get(
"/zones/{zone}/services",
response_model=List[str],
responses={500: {
"model": ErrorResponse
}},
responses={500: {"model": ErrorResponse}},
summary="Get services in zone",
)
async def get_services(zone: str = Path(...,
description="Zone name")) -> List[str]:
async def get_services(zone: str = Path(..., description="Zone name")) -> List[str]:
"""
Get all services in a zone.
@ -185,14 +167,7 @@ async def get_services(zone: str = Path(...,
@router.post(
"/zones/{zone}/services",
response_model=SuccessResponse,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Add service to zone",
)
async def add_service(
@ -211,11 +186,11 @@ async def add_service(
"""
try:
firewalld.add_service(zone, rule.name, rule.timeout)
return SuccessResponse(
message=f"Service {rule.name} added to zone {zone}")
return SuccessResponse(message=f"Service {rule.name} added to zone {zone}")
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -226,19 +201,12 @@ async def add_service(
@router.delete(
"/zones/{zone}/services/{service}",
response_model=SuccessResponse,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Remove service from zone",
)
async def remove_service(
zone: str = Path(..., description="Zone name"),
service: str = Path(..., description="Service name"),
zone: str = Path(..., description="Zone name"),
service: str = Path(..., description="Service name"),
) -> SuccessResponse:
"""
Remove a service from a zone.
@ -252,8 +220,7 @@ async def remove_service(
"""
try:
firewalld.remove_service(zone, service)
return SuccessResponse(
message=f"Service {service} removed from zone {zone}")
return SuccessResponse(message=f"Service {service} removed from zone {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -269,13 +236,10 @@ async def remove_service(
@router.get(
"/zones/{zone}/ports",
response_model=List[str],
responses={500: {
"model": ErrorResponse
}},
responses={500: {"model": ErrorResponse}},
summary="Get ports in zone",
)
async def get_ports(zone: str = Path(...,
description="Zone name")) -> List[str]:
async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]:
"""
Get all ports in a zone.
@ -298,15 +262,9 @@ async def get_ports(zone: str = Path(...,
"/zones/{zone}/ports",
response_model=SuccessResponse,
responses={
400: {
"model": ErrorResponse
},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
},
400: {"model": ErrorResponse},
404: {"model": ErrorResponse},
500: {"model": ErrorResponse},
},
summary="Add port to zone",
)
@ -327,11 +285,10 @@ async def add_port(
try:
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
return SuccessResponse(
message=
f"Port {rule.port}/{rule.protocol.value} added to zone {zone}")
message=f"Port {rule.port}/{rule.protocol.value} added to zone {zone}"
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e))
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -342,14 +299,7 @@ async def add_port(
@router.delete(
"/zones/{zone}/ports",
response_model=SuccessResponse,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Remove port from zone",
)
async def remove_port(
@ -369,11 +319,12 @@ async def remove_port(
try:
firewalld.remove_port(zone, rule.port, rule.protocol.value)
return SuccessResponse(
message=
f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}")
message=f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}"
)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -384,13 +335,10 @@ async def remove_port(
@router.get(
"/zones/{zone}/sources",
response_model=List[str],
responses={500: {
"model": ErrorResponse
}},
responses={500: {"model": ErrorResponse}},
summary="Get sources in zone",
)
async def get_sources(zone: str = Path(...,
description="Zone name")) -> List[str]:
async def get_sources(zone: str = Path(..., description="Zone name")) -> List[str]:
"""
Get all sources in a zone.
@ -412,14 +360,7 @@ async def get_sources(zone: str = Path(...,
@router.post(
"/zones/{zone}/sources",
response_model=SuccessResponse,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Add source to zone",
)
async def add_source(
@ -438,11 +379,11 @@ async def add_source(
"""
try:
firewalld.add_source(zone, rule.address)
return SuccessResponse(
message=f"Source {rule.address} added to zone {zone}")
return SuccessResponse(message=f"Source {rule.address} added to zone {zone}")
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -453,14 +394,7 @@ async def add_source(
@router.delete(
"/zones/{zone}/sources/{source}",
response_model=SuccessResponse,
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Remove source from zone",
)
async def remove_source(
@ -479,8 +413,7 @@ async def remove_source(
"""
try:
firewalld.remove_source(zone, source)
return SuccessResponse(
message=f"Source {source} removed from zone {zone}")
return SuccessResponse(message=f"Source {source} removed from zone {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -496,9 +429,7 @@ async def remove_source(
@router.post(
"/reload",
response_model=SuccessResponse,
responses={500: {
"model": ErrorResponse
}},
responses={500: {"model": ErrorResponse}},
summary="Reload firewall rules",
)
async def reload_firewall() -> SuccessResponse:

View file

@ -1,111 +1,37 @@
from typing import Dict, Any
from fastapi import APIRouter, Body, HTTPException, Depends
from fastapi import APIRouter, Body, HTTPException
from services.ip_service import IPService
from utils.i18n import get_translations
from babel.support import Translations
router = APIRouter()
ip_service = IPService()
@router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Проверяет доступность IP-адреса перед его применением.
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
и возвращает результат проверки.
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса.
translations: Объект переводов для текущего языка.
Returns:
Dict[str, Any]: Словарь с ключами:
- "available" (bool): Доступен ли IP-адрес.
- "message" (str): Сообщение о результате проверки.
Raises:
HTTPException: Если произошла внутренняя ошибка (status_code=500).
"""
async def check_ip_config(data: Dict[str, Any] = Body(...)):
""" Проверяет доступность IP-адреса """
try:
result = await ip_service.check_ip_config(data, translations)
result = await ip_service.check_ip_config(data)
return result
except Exception as e:
raise HTTPException(
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
@router.post("/apply_ip")
async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Применяет проверенные IP-настройки к указанному сетевому профилю.
Обновляет настройки соединения через NetworkManager, отключает и
переподключает устройство к сети.
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "success" (bool): Успешность операции.
- "message" (str): Сообщение об успешном применении настроек.
Raises:
HTTPException: Если подключение не найдено (status_code=404) или
произошла внутренняя ошибка (status_code=500).
"""
async def apply_ip_config(data: Dict[str, Any] = Body(...)):
""" Применяет проверенные IP-настройки """
try:
await ip_service.apply_ip_config(data, translations)
return {
"success": True,
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
}
await ip_service.apply_ip_config(data)
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
except ValueError as e:
raise HTTPException(status_code=404, detail=translations.gettext(str(e)))
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
@router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Применяет проверенные IP-настройки для отката сетевого профиля.
Используется для сохранения и отката настроек сетевого профиля к предыдущему
состоянию через NetworkManager.
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "success" (bool): Успешность операции.
- "message" (str): Сообщение об успешном применении настроек.
Raises:
HTTPException: Если подключение не найдено (status_code=404) или
произошла внутренняя ошибка (status_code=500).
"""
async def save_ip_config(data: Dict[str, Any] = Body(...)):
""" Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно) """
try:
await ip_service.apply_ip_config(data, translations)
return {
"success": True,
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
}
await ip_service.apply_ip_config(data)
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
except ValueError as e:
raise HTTPException(status_code=404, detail=translations.gettext(str(e)))
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")

View file

@ -1,16 +1,9 @@
from fastapi import APIRouter, Body, HTTPException, Depends
from fastapi import APIRouter, Body, HTTPException
import logging
from typing import Dict, Tuple, List, Union
from services.network_service import NetworkService
from models.network import (
NetworkInterfacesContainer,
Activation,
Deactivation,
NetworkInterface,
WiFiDevice,
)
from utils.i18n import get_translations, _, _load_translations
from babel.support import Translations
from models.network import NetworkInterfacesContainer, Activation, Deactivation, NetworkInterface, WiFiDevice
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@ -18,208 +11,118 @@ logger = logging.getLogger(__name__)
router = APIRouter()
network_service = NetworkService()
class ContainerPositionManager:
"""Класс для управления позициями и состоянием контейнеров.
Хранит и обновляет координаты (x, y) и состояние (isOpen) для контейнеров
активации и деактивации.
class ContainerPositionManager:
"""
Класс для управления позициями и состоянием контейнеров
"""
def __init__(self):
"""Инициализирует менеджер позиций контейнеров.
Создаёт начальные позиции и состояния для контейнеров "activation" и
"deactivation" с координатами (0, 0) и закрытым состоянием (False).
"""
self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
"activation": {
"x": 0,
"y": 0,
"isOpen": False
},
"deactivation": {
"x": 0,
"y": 0,
"isOpen": False
},
'activation': {'x': 0, 'y': 0, 'isOpen': False},
'deactivation': {'x': 0, 'y': 0, 'isOpen': False}
}
def get_position_and_state(self, container_name: str, translations: Translations) -> Tuple[int, int, bool]:
"""Получает текущие координаты и состояние указанного контейнера.
Args:
container_name (str): Имя контейнера ("activation" или "deactivation").
translations: Объект переводов для текущего языка.
Returns:
Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen).
Raises:
ValueError: Если указан неверный идентификатор контейнера.
"""
def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
container = self.positions.get(container_name)
if not container:
raise ValueError(translations.gettext("Неверное имя контейнера: {container_name}").format(container_name=container_name))
return container["x"], container["y"], container["isOpen"]
raise ValueError(f"Неверное имя контейнера: {container_name}")
return container['x'], container['y'], container['isOpen']
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
"""Обновляет позицию и состояние контейнера по его идентификатору.
container_name = 'activation' if container_id == 1 else 'deactivation'
self.positions[container_name] = {'x': x, 'y': y, 'isOpen': isOpen}
Args:
container_id (int): Идентификатор контейнера (1 для "activation", 2 для "deactivation").
x (int): Новая координата X.
y (int): Новая координата Y.
isOpen (bool): Новое состояние контейнера (открыт/закрыт).
Raises:
ValueError: Если container_id не соответствует ни одному контейнеру.
"""
container_name = "activation" if container_id == 1 else "deactivation"
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
container_manager = ContainerPositionManager()
async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]:
"""Возвращает текущие сетевые данные в виде словаря.
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные,
и возвращает их в формате Activation и Deactivation с учётом позиций и состояний
контейнеров.
Args:
translations: Объект переводов для текущего языка.
Returns:
Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими
данные о соответствующих контейнерах.
Raises:
HTTPException: Если произошла ошибка при получении данных (status_code=500).
async def get_network_data() -> Dict[str, Dict]:
"""
logger.debug("Calling get_network_data()")
Возвращает текущие сетевые данные в виде словаря
"""
logger.debug("Вызов get_network_data()")
try:
network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state("activation", translations)
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state("deactivation", translations)
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state('activation')
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state('deactivation')
activation = Activation(
id=1,
interfaces=activation_interfaces,
container_x=activation_x,
container_y=activation_y,
isOpen=activation_is_open,
isOpen=activation_is_open
)
deactivation = Deactivation(
id=2,
interfaces=deactivation_interfaces,
container_x=deactivation_x,
container_y=deactivation_y,
isOpen=deactivation_is_open,
isOpen=deactivation_is_open
)
return {
"activation": activation.model_dump(),
"deactivation": deactivation.model_dump(),
"deactivation": deactivation.model_dump()
}
except Exception as e:
logger.error(f"Error in get_network_data: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить сетевые данные"))
logger.error(f"Ошибка в get_network_data: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
def _split_interfaces(
interfaces: List[Union[NetworkInterface, WiFiDevice]],
) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
"""Разделяет интерфейсы на активные и неактивные.
Args:
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список сетевых интерфейсов.
Returns:
Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
Кортеж из списков активных и неактивных интерфейсов.
def _split_interfaces(interfaces: List[Union[NetworkInterface, WiFiDevice]]) -> Tuple[
List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
"""
Разделяет интерфейсы на активные и неактивные
"""
activation_interfaces = []
deactivation_interfaces = []
for interface in interfaces:
if interface.device_state == "100":
if interface.device_state == '100':
activation_interfaces.append(interface)
else:
deactivation_interfaces.append(interface)
return activation_interfaces, deactivation_interfaces
@router.post("/update_container_position")
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = Depends(get_translations)):
"""Обновляет позицию и состояние контейнера.
Принимает данные о новом положении и состоянии контейнера и обновляет их
в менеджере позиций.
Args:
data (Dict[str, Union[int, bool]]): Словарь с ключами:
- "container_id" (int): Идентификатор контейнера (1 или 2).
- "container_x" (int): Новая координата X.
- "container_y" (int): Новая координата Y.
- "isOpen" (bool): Новое состояние контейнера (открыт/закрыт).
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "success" (bool): Успешность операции.
- "message" (str): Сообщение об успешном обновлении.
Raises:
HTTPException: Если отсутствуют обязательные поля (status_code=400) или
типы данных неверны.
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)):
"""
Обновляет позицию и состояние контейнера
"""
try:
required_keys = [
"container_id", "container_x", "container_y", "isOpen"
]
required_keys = ["container_id", "container_x", "container_y", "isOpen"]
if not all(key in data for key in required_keys):
raise ValueError(
translations.gettext("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
)
raise ValueError("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
container_id = data["container_id"]
container_x = data["container_x"]
container_y = data["container_y"]
is_open = data["isOpen"]
if (not isinstance(container_id, int)
or not isinstance(container_x, int)
or not isinstance(container_y, int)
or not isinstance(is_open, bool)):
raise ValueError(translations.gettext("Неверный тип данных в запросе"))
# Проверка типов
if not isinstance(container_id, int) or not isinstance(container_x, int) or not isinstance(container_y,
int) or not isinstance(
is_open, bool):
raise ValueError("Неверный тип данных в запросе")
container_manager.update_position(container_id, container_x, container_y, is_open)
return {
"success": True,
"message": translations.gettext("Позиция и состояние контейнера обновлены")
}
return {"success": True, "message": "Позиция и состояние контейнера обновлены"}
except Exception as e:
logger.error(f"Error updating container position: {e}")
raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e)))
logger.error(f"Ошибка обновления позиции контейнера: {e}")
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
@router.get("/get_hostname")
async def get_hostname(translations: Translations = Depends(get_translations)):
"""Возвращает имя хоста системы.
Выполняет запрос к сервису для получения имени хоста.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключом "hostname" и значением имени хоста.
Raises:
HTTPException: Если произошла ошибка при получении имени хоста (status_code=500).
"""
async def get_hostname():
try:
hostname_info = await network_service.get_hostname()
return hostname_info
except Exception as e:
logger.error(f"Error in get_hostname: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить имя хоста"))
logger.error(f"Ошибка в get_hostname: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")

View file

@ -1,209 +0,0 @@
import os
import logging
import subprocess
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Response, Request, Body
from starlette.responses import FileResponse
from services.settings_service import SettingsService
from utils.i18n import get_translations
from babel.support import Translations
from models.subnet import SubnetScanRequest # Предполагается, что модель определена
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
router = APIRouter()
settings_service = SettingsService()
@router.post("/clear_subnet")
def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети, сохранённые в devices.json.
Удаляет все записи об узлах подсети, оставляя файл пустым.
Args:
subnet (Optional[str]): Подсеть в формате CIDR (например, "10.1.1.0/24").
Если None, очищает все подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
Raises:
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_subnet(subnet)
if success:
if subnet:
return {"message": translations.gettext("Данные узлов подсети {subnet} успешно очищены").format(subnet=subnet)}
else:
return {"message": translations.gettext("Данные всех подсетей успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные узлов подсети"))
except Exception as e:
logger.error(f"Error clearing subnet data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_wifi")
def clear_wifi(translations: Translations = Depends(get_translations)):
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
Удаляет все записи о точках доступа, оставляя файл пустым.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
Raises:
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_wifi()
if success:
return {"message": translations.gettext("Данные точек доступа Wi-Fi успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные точек доступа Wi-Fi"))
except Exception as e:
logger.error(f"Error clearing wifi data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_all_data")
def clear_all_data(translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
Удаляет все записи из devices.json и wifi.json.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
Raises:
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_all_data()
if success:
return {"message": translations.gettext("Все сетевые данные успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить все сетевые данные"))
except Exception as e:
logger.error(f"Error clearing all data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
WIFI_PATH = "storage/wifi/wifi.json"
@router.post("/export_devices", response_class=FileResponse)
async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON для указанной подсети.
Args:
request (SubnetScanRequest): Объект с полем subnet в формате CIDR.
translations: Объект переводов для текущего языка.
Returns:
FileResponse: Файл <subnet>.json для скачивания.
Raises:
HTTPException: Если файл не найден (status_code=404).
"""
subnet = request.subnet
file_path = f"storage/subnets/{subnet.replace('/', '_')}.json"
if not os.path.exists(file_path):
logger.warning(f"Export file not found: {file_path}")
raise HTTPException(status_code=404, detail=translations.gettext("Файл для подсети {subnet} не найден").format(subnet=subnet))
logger.info(f"Exporting file: {file_path}")
return FileResponse(
path=file_path,
filename=f"{subnet.replace('/', '_')}.json",
media_type="application/json"
)
@router.get("/export_wifi", response_class=FileResponse)
async def export_wifi(translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON.
Args:
translations: Объект переводов для текущего языка.
Returns:
FileResponse: Файл wifi.json для скачивания.
Raises:
HTTPException: Если файл не найден (status_code=404).
"""
if not os.path.exists(WIFI_PATH):
logger.warning(f"Export file not found: {WIFI_PATH}")
raise HTTPException(status_code=404, detail=translations.gettext("Файл не найден"))
logger.info(f"Exporting file: {WIFI_PATH}")
return FileResponse(
path=WIFI_PATH,
filename="wifi.json",
media_type="application/json"
)
@router.post("/set_language")
def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations)):
"""
Устанавливает куку locale для смены языка пользователя.
Args:
locale (str): Используемый язык.
response (Response): Запрос.
translations: Объект переводов текущего языка.
Returns:
dict: Сообщение об успешной установке языка.
"""
lc_messages_path = os.path.join(os.path.dirname(__file__), '..', '..', 'locales', locale, 'LC_MESSAGES')
if not os.path.isdir(lc_messages_path):
raise HTTPException(
status_code=400,
detail=translations.gettext("Язык '{locale}' не поддерживается.").format(locale=locale)
)
has_mo_or_po = any(f.endswith('.mo') or f.endswith('.po') for f in os.listdir(lc_messages_path))
if not has_mo_or_po:
raise HTTPException(
status_code=400,
detail=translations.gettext("Язык '{locale}' не поддерживается (нет файлов переводов).").format(locale=locale)
)
response.set_cookie(key="locale", value=locale, path="/", httponly=True)
return {"message": translations.gettext("Язык успешно установлен: {locale}").format(locale=locale)}
@router.get("/get_system_language")
async def get_system_language(request: Request, translations: Translations = Depends(get_translations)):
"""Выводит текущий язык, установленный в системе на основе locale.
Выполняет команду 'locale' в системе и извлекает параметры LANG
Args:
request (Request): Объект запроса для доступа к параметрам.
translations: Объект переводов текущего языка.
Returns:
dict: Словарь с текущим языком системы.
Raises:
HTTPException: Если не удалось извлечь системный язык (status_code=500).
"""
try:
result = subprocess.run(['locale'], capture_output=True, text=True, check=True)
output = result.stdout
lang = next((line.split('=')[1].strip() for line in output.splitlines() if line.startswith('LANG=')), None)
if lang:
return {"language": lang}
else:
logger.warning(f"No locale found: {output}")
return {"language": 'unknown'}
except subprocess.CalledProcessError as e:
logger.error(f"Error executing locale command: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось определить язык в системе"))
except Exception as e:
logger.error(f"Unexpected error getting system language: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при определении языка в системе {error}").format(error=str(e)))

View file

@ -1,352 +0,0 @@
from fastapi import APIRouter, HTTPException, Body, Depends
from models.subnet import SubnetScanResult, SubnetScanRequest
from models.wifi import WifiScanResult
from models.request import NoteRequest, WifiConnectRequest, WifiDisconnectRequest
from services.subnet_service import SubnetService, get_subnet_service
from services.wifi_service import WifiService
from services.connect_service import WifiConnectionService
from utils.i18n import get_translations, _
from babel.support import Translations
import logging
router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.post("/devices", response_model=SubnetScanResult)
def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Сканирует локальную подсеть и возвращает данные об устройствах.
Выполняет сканирование указанной подсети для обнаружения активных устройств.
Args:
request (SubnetScanRequest): Объект с полем subnet.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
SubnetScanResult: Объект с результатами сканирования (устройства и их данные).
Raises:
HTTPException: Если произошла ошибка сканирования (status_code=500).
"""
try:
# Если subnet пустая строка, определяем автоматически
subnet = request.subnet.strip() if request.subnet else None
logger.info(f"Requested subnet scan for: {subnet or 'auto-detected'}")
result = subnet_service.scan_subnet(subnet)
if not result.devices and result.error:
logger.warning(f"No devices found in subnet {subnet or 'auto-detected'}")
return result
except Exception as e:
logger.error(f"Error in scan_subnet: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования подсети: {error}").format(error=str(e)))
@router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них.
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
Args:
timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30.
translations: Объект переводов для текущего языка.
Returns:
WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей).
Raises:
HTTPException: Если произошла ошибка сканирования Wi-Fi (status_code=500).
"""
try:
wifi_service = WifiService()
result = wifi_service.scan_wifi(timeout=timeout)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования Wi-Fi: {error}").format(error=str(e)))
@router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Добавляет заметку для устройства по MAC-адресу.
Привязывает заметку к устройству, идентифицируемому по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном добавлении заметки.
Raises:
HTTPException: Если устройство не найдено (status_code=404), заметка уже
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
success, result = subnet_service.add_note(mac, request.note, translations)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} уже существует").format(mac=mac))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' добавлена для устройства {mac}").format(note=request.note, mac=mac)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/devices/{mac}/note")
def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Обновляет заметку для устройства по MAC-адресу.
Изменяет существующую заметку, привязанную к устройству.
Args:
mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для нового текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном обновлении заметки.
Raises:
HTTPException: Если устройство не найдено (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
success, result = subnet_service.update_note(mac, request.note, translations)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' обновлена для устройства {mac}").format(note=request.note, mac=mac)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/devices/{mac}/note")
def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Удаляет заметку для устройства по MAC-адресу.
Удаляет привязанную заметку для указанного устройства.
Args:
mac (str): MAC-адрес устройства.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном удалении заметки.
Raises:
HTTPException: Если устройство не найдено (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
success, result = subnet_service.delete_note(mac, translations)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка удалена для устройства {mac}").format(mac=mac)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/{ssid}/note")
def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Добавляет заметку для сети Wi-Fi по SSID.
Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID.
Args:
ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для текста заметки.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном добавлении заметки.
Raises:
HTTPException: Если сеть не найдена (status_code=404), заметка уже
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
wifi_service = WifiService()
success, result = wifi_service.add_note(ssid, request.note)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} уже существует").format(ssid=ssid))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' добавлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/wifi/{ssid}/note")
def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Обновляет заметку для сети Wi-Fi по SSID.
Изменяет существующую заметку, привязанную к Wi-Fi сети.
Args:
ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для нового текста заметки.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном обновлении заметки.
Raises:
HTTPException: Если сеть не найдена (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
wifi_service = WifiService()
success, result = wifi_service.update_note(ssid, request.note)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' обновлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/wifi/{ssid}/note")
def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)):
"""Удаляет заметку для сети Wi-Fi по SSID.
Удаляет привязанную заметку для указанной Wi-Fi сети.
Args:
ssid (str): Идентификатор Wi-Fi сети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном удалении заметки.
Raises:
HTTPException: Если сеть не найдена (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
wifi_service = WifiService()
success, result = wifi_service.delete_note(ssid)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка удалена для сети с SSID {ssid}").format(ssid=ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/connect")
def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)):
"""Подключается к Wi-Fi сети по SSID и паролю.
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
учетных данных.
Args:
request (WifiConnectRequest): Объект с полями "ssid" и "password".
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном подключении.
Raises:
HTTPException: Если подключение не удалось (status_code=400).
"""
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.connect(request.ssid, request.password)
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"message": translations.gettext("Успешно подключено к {ssid}").format(ssid=request.ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка подключения к Wi-Fi: {error}").format(error=str(e)))
@router.post("/wifi/disconnect")
def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)):
"""Отключается от Wi-Fi сети по указанному SSID.
Выполняет отключение от указанной Wi-Fi сети.
Args:
request (WifiDisconnectRequest): Объект с полем "ssid".
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном отключении.
Raises:
HTTPException: Если отключение не удалось (status_code=400).
"""
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.disconnect(request.ssid)
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"message": translations.gettext(message or "Успешно отключено от {ssid}").format(ssid=request.ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка отключения от Wi-Fi: {error}").format(error=str(e)))
@router.get("/wifi/status")
def get_wifi_status(translations: Translations = Depends(get_translations)):
"""Проверяет статус текущего подключения к Wi-Fi.
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "status" (str): Состояние ("connected" или другое).
- "ssid" (str, optional): SSID текущей сети, если подключено.
Raises:
HTTPException: Если проверка статуса не удалась (status_code=400).
"""
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.get_status()
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"status": translations.gettext("connected"), "ssid": message}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка проверки статуса Wi-Fi: {error}").format(error=str(e)))

Some files were not shown because too many files have changed in this diff Show more