Compare commits
No commits in common. "main" and "v0.0.2" have entirely different histories.
721 changed files with 209030 additions and 65200 deletions
1
.pc/.quilt_patches
Normal file
1
.pc/.quilt_patches
Normal file
|
|
@ -0,0 +1 @@
|
|||
debian/patches
|
||||
1
.pc/.quilt_series
Normal file
1
.pc/.quilt_series
Normal file
|
|
@ -0,0 +1 @@
|
|||
series
|
||||
1
.pc/.version
Normal file
1
.pc/.version
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
75
.pc/1/SRV/services/ip_service.py
Normal file
75
.pc/1/SRV/services/ip_service.py
Normal 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} не найдено")
|
||||
112
.pc/2/SRV/services/ip_service.py
Normal file
112
.pc/2/SRV/services/ip_service.py
Normal 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
2
.pc/applied-patches
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
1
|
||||
2
|
||||
|
|
@ -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 "Ошибка загрузки файла"
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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'}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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' }}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -110,17 +110,15 @@ 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()
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ export const ModalContainer = styled.div<{height?: string}>`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
height: ${(props) => props.height ? props.height : '230px'};
|
||||
gap: 15px;
|
||||
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;
|
||||
`
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
h1{
|
||||
font-size: 52px;
|
||||
}
|
||||
`
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
export const HelloWindow = () => {
|
||||
h1{
|
||||
font-size: 52px;
|
||||
}
|
||||
`
|
||||
|
||||
const { setWorkModule } = useWorkSpaceStore()
|
||||
export const HelloWindow = () => {
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Container className="unselectable">
|
||||
<Title>
|
||||
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
|
||||
</Title>
|
||||
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>
|
||||
)
|
||||
}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -93,7 +93,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
|||
|
||||
newSocket.onmessage = (event) => {
|
||||
const newData = JSON.parse(event.data);
|
||||
|
||||
|
||||
setData(() => [
|
||||
newData.activation,
|
||||
newData.deactivation,
|
||||
|
|
@ -126,7 +126,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
|||
const activationData = data[0].interfaces;
|
||||
|
||||
const wifi = activationData.find((el: any) => el.type === 'WIFI');
|
||||
|
||||
|
||||
setInterfaces(newInterfaces);
|
||||
setEdgeData(newEdgeData);
|
||||
if (wifi) { setWifi(wifi); }
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'])} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,5 @@ export const buttonNames: names ={
|
|||
10: 'admin',
|
||||
11: 'reset',
|
||||
12: 'close',
|
||||
13: 'noAdmin',
|
||||
14: 'userSearch',
|
||||
15: 'wifiFind',
|
||||
16: 'noteEdit'
|
||||
13: 'noAdmin'
|
||||
}
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -4,7 +4,5 @@ export const workSpaceNames: names ={
|
|||
1: 'helloWindow',
|
||||
2: 'aboutFromWork',
|
||||
3: 'networkInterfaces',
|
||||
4: 'aboutFromHello',
|
||||
5: 'userScanner',
|
||||
6: 'wifiScanner',
|
||||
4: 'aboutFromHello'
|
||||
}
|
||||
|
|
@ -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 }),
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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 />);
|
||||
4966
FRONT/DEV/package-lock.json
generated
4966
FRONT/DEV/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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))
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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": "Английский"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 >
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
9
FRONT/DEV/src/electron.d.ts
vendored
9
FRONT/DEV/src/electron.d.ts
vendored
|
|
@ -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 }>;
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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];
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
2305
FRONT/DEV/yarn.lock
2305
FRONT/DEV/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -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`
|
||||
73
README.md
73
README.md
|
|
@ -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
2
SRV/.gitignore
vendored
|
|
@ -6,5 +6,3 @@ __pycache__
|
|||
.wheels
|
||||
.vendor
|
||||
.python3-deb
|
||||
storage/subnets/
|
||||
storage/wifi/
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -31,8 +7,4 @@ router.include_router(ufw.router, prefix="/ufw", tags=["UFW"])
|
|||
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"])
|
||||
router.include_router(auth.router, prefix="/auth", tags=["Auth"])
|
||||
|
|
@ -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="Ошибка при проверке файла")
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
108
SRV/api/v1/ip.py
108
SRV/api/v1/ip.py
|
|
@ -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)}")
|
||||
|
|
@ -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="Не удалось получить имя хоста")
|
||||
|
|
@ -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)))
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue