Compare commits
No commits in common. "v0.0.2" and "main" have entirely different histories.
|
|
@ -6,3 +6,6 @@ __pycache__
|
||||||
.wheels
|
.wheels
|
||||||
.vendor
|
.vendor
|
||||||
.python3-deb
|
.python3-deb
|
||||||
|
SRV/storage/subnets/*
|
||||||
|
SRV/storage/wifi/wifi.json
|
||||||
|
build/
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
debian/patches
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
series
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
2
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
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} не найдено")
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
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} не найдено")
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
1
|
|
||||||
2
|
|
||||||
137
.woodpecker/.woodpecker.yml
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
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 "Ошибка загрузки файла"
|
||||||
13
FRONT/BUILD/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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,10 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { NetworkService } from './src/api/request.ts';
|
|
||||||
import { useIpStore } from './src/store';
|
import { useIpStore } from './src/store';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAdminStore } from './src/store/adminStore.ts';
|
|
||||||
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
|
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function createWindow() {
|
||||||
win.show();
|
win.show();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
win.loadURL('http://localhost:3000');
|
win.loadURL('https://localhost:8001');
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/icons-ic": "^1.2.13",
|
"@iconify/icons-ic": "^1.2.13",
|
||||||
|
"@iconify/icons-ph": "^1.2.5",
|
||||||
"@iconify/react": "^5.2.0",
|
"@iconify/react": "^5.2.0",
|
||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"@xyflow/react": "^12.3.5",
|
"@xyflow/react": "^12.3.5",
|
||||||
|
|
|
||||||
|
|
@ -82,4 +82,79 @@ export class NetworkService {
|
||||||
return error.response;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
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;
|
border: none;
|
||||||
padding: ${(props) =>
|
padding: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "4px 8px"
|
? "10px 28px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "10px 20px"
|
? "10px 20px"
|
||||||
: "6px 10px"}; // По умолчанию
|
: "12px 20px"}; // По умолчанию
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: ${(props) =>
|
font-size: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "12px"
|
? "14px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "16px"
|
? "16px"
|
||||||
: "14px"}; // По умолчанию
|
: "14px"}; // По умолчанию
|
||||||
|
|
@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
|
||||||
? 'row-reverse'
|
? 'row-reverse'
|
||||||
: 'row'};
|
: 'row'};
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
transition: all .3s ease;
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: ${(props) => props.$hoverсolor};
|
background-color: ${(props) => props.$hoverсolor};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ type Props = {
|
||||||
placeholderText: string;
|
placeholderText: string;
|
||||||
id: string;
|
id: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (identifier: string, value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
type?: string;
|
type?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
|
@ -33,7 +33,7 @@ const StyledInput = styled.input`
|
||||||
border: 1px solid #adadad;
|
border: 1px solid #adadad;
|
||||||
}
|
}
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 14px;
|
font-size: 18px;
|
||||||
color: ${props=>props.theme.textColor}
|
color: ${props=>props.theme.textColor}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id,
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(id, value);
|
onChange(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,21 @@ import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface SignalSVGProps {
|
interface SignalSVGProps {
|
||||||
signal: number; // Принимаем signal как пропс
|
signal: number; // Принимаем signal как пропс
|
||||||
|
color: string;
|
||||||
|
size: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
|
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем цвета в зависимости от signal
|
// Устанавливаем цвета в зависимости от signal
|
||||||
const colors = {
|
const colors = {
|
||||||
'--st1-color': signal >= 1 ? 'white' : 'black',
|
'--st1-color': signal >= 1 ? color : 'black',
|
||||||
'--st2-color': signal >= 2 ? 'white' : 'black',
|
'--st2-color': signal >= 2 ? color : 'black',
|
||||||
'--st3-color': signal >= 3 ? 'white' : 'black',
|
'--st3-color': signal >= 3 ? color : 'black',
|
||||||
'--st4-color': signal >= 4 ? 'white' : 'black',
|
'--st4-color': signal >= 4 ? color : 'black',
|
||||||
'--st5-color': signal === 5 ? 'white' : 'black',
|
'--st5-color': signal === 5 ? color : 'black',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Применяем стили к SVG
|
// Применяем стили к SVG
|
||||||
|
|
@ -35,8 +37,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 296 235"
|
viewBox="0 0 296 235"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
width="20px" // Фиксированная ширина
|
width={size} // Фиксированная ширина
|
||||||
height="20px"
|
height={size}
|
||||||
>
|
>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
{`
|
{`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
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}
|
placeholderText={data.ipData.ipAdress}
|
||||||
id="newIp"
|
id="newIp"
|
||||||
value={ipData.newIp}
|
value={ipData.newIp}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('newIp', value)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
style={{fontSize: '12px', padding: '4px 10px'}}
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
|
@ -203,7 +203,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
placeholderText={data.ipData.ipGate}
|
placeholderText={data.ipData.ipGate}
|
||||||
id="newGate"
|
id="newGate"
|
||||||
value={ipData.newGate}
|
value={ipData.newGate}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('newGate', value)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
style={{fontSize: '12px', padding: '4px 10px'}}
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
|
@ -221,7 +221,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
placeholderText={data.ipData.ipDns.join(', ')}
|
placeholderText={data.ipData.ipDns.join(', ')}
|
||||||
id="newDns"
|
id="newDns"
|
||||||
value={ipData.newDns.join(', ')}
|
value={ipData.newDns.join(', ')}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={( value) => handleChange('newDns', value)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
style={{fontSize: '12px', padding: '4px 10px'}}
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export const NetWorkNode = ({ data }: CustomNodeProps) => {
|
||||||
|
|
||||||
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
|
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
|
||||||
<Status>
|
<Status>
|
||||||
<SignalSVG signal={signal} />
|
<SignalSVG signal={signal} color={'white'} size={'20px'} />
|
||||||
</Status>
|
</Status>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const Network = styled.div<Props>`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
overflow: hidden;
|
||||||
p{
|
p{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -29,8 +29,6 @@ export const TitleContainer = styled.div<Props>`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: ${(props) => props.$backgroundcolor};
|
background: ${(props) => props.$backgroundcolor};
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
color: white;
|
color: white;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
height: ${(props) => props.$height ? props.$height : 'unset'};
|
height: ${(props) => props.$height ? props.$height : 'unset'};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// 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='Введите адрес узла...'
|
placeholderText='Введите адрес узла...'
|
||||||
id="newIp"
|
id="newIp"
|
||||||
value={ipAddress}
|
value={ipAddress}
|
||||||
onChange={(id, value) => setIpAddress(value)}
|
onChange={(value) => setIpAddress(value)}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p style={{ color: 'red', marginTop: '5px' }}>
|
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
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,6 +110,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
try {
|
try {
|
||||||
const result = await NetworkService.saveIp(ipAddress, data);
|
const result = await NetworkService.saveIp(ipAddress, data);
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
|
if(ipAddress!=='localhost'){
|
||||||
setIpAddress(ipData.newIp)
|
setIpAddress(ipData.newIp)
|
||||||
|
|
||||||
const updatedDevices = devices.map(device =>
|
const updatedDevices = devices.map(device =>
|
||||||
|
|
@ -118,6 +119,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
: device
|
: device
|
||||||
);
|
);
|
||||||
useDevicesStore.setState({ devices: updatedDevices });
|
useDevicesStore.setState({ devices: updatedDevices });
|
||||||
|
}
|
||||||
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
placeholderText='Введите пароль администратора...'
|
placeholderText='Введите пароль администратора...'
|
||||||
id="password"
|
id="password"
|
||||||
value={user.password}
|
value={user.password}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalCon
|
||||||
import { ModalMap } from "./windowsType";
|
import { ModalMap } from "./windowsType";
|
||||||
import Loader from "../UI/loader";
|
import Loader from "../UI/loader";
|
||||||
import { ModalWindow } from "./style";
|
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 = {
|
type Props = {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
|
@ -35,7 +39,34 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
|
||||||
message={data.message}
|
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'] && (
|
{type === ModalMap['4'] && (
|
||||||
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
|
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const ModalContainer = styled.div<{height?: string}>`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
height: ${(props) => props.height ? props.height : '200px'};
|
height: ${(props) => props.height ? props.height : '230px'};
|
||||||
width: 520px;
|
width: 520px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -66,5 +66,5 @@ export const ButtonContainer = styled.div`
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 20px;
|
padding: 10px 20px 20px;
|
||||||
`
|
`
|
||||||
|
|
@ -5,5 +5,9 @@ export const ModalMap: ModalType = {
|
||||||
'2': 'loader',
|
'2': 'loader',
|
||||||
'3': 'connectionError',
|
'3': 'connectionError',
|
||||||
'4': 'addDevice',
|
'4': 'addDevice',
|
||||||
'5': 'sudo'
|
'5': 'sudo',
|
||||||
|
'6': 'noteDevice',
|
||||||
|
'7': 'noteWifi',
|
||||||
|
'8': 'connectNetwork',
|
||||||
|
'9': 'disconnectConfirm',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@ export const CreateUser = () => {
|
||||||
if (result.status === 200) { window.location.reload() }
|
if (result.status === 200) { window.location.reload() }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error:', err);
|
console.error('Error:', err);
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +145,7 @@ export const CreateUser = () => {
|
||||||
placeholderText='Введите пароль...'
|
placeholderText='Введите пароль...'
|
||||||
id="password"
|
id="password"
|
||||||
value={user.password}
|
value={user.password}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -154,7 +153,7 @@ export const CreateUser = () => {
|
||||||
placeholderText='Повторите пароль...'
|
placeholderText='Повторите пароль...'
|
||||||
id="repeatPassword"
|
id="repeatPassword"
|
||||||
value={repeatPassword}
|
value={repeatPassword}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('repeatPassword', value)}
|
||||||
style={{ marginTop: '10px' }}
|
style={{ marginTop: '10px' }}
|
||||||
/>
|
/>
|
||||||
{!passwordsMatch && (
|
{!passwordsMatch && (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
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)
|
setHasInited(false)
|
||||||
initializationAttempt.current = 0;
|
initializationAttempt.current = 0;
|
||||||
|
|
||||||
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
|
||||||
|
|
||||||
let connectionTimeout;
|
let connectionTimeout;
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,19 @@ export const ToolBar = () => {
|
||||||
{toolBarMenu && toolBarMenu.map((menu) => (
|
{toolBarMenu && toolBarMenu.map((menu) => (
|
||||||
<ToolBarButton menuId={menu.id} key={menu.id} />
|
<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>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
|
|
||||||
<Button>
|
<Button>
|
||||||
<IconContainer>
|
<IconContainer>
|
||||||
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />
|
<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;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
width: 210px;
|
||||||
p{
|
p{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +115,7 @@ export const DevicesBar = () => {
|
||||||
|
|
||||||
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
testSocket.close();
|
testSocket.close();
|
||||||
resolve(false);
|
resolve(false);
|
||||||
|
|
@ -196,12 +196,12 @@ export const DevicesBar = () => {
|
||||||
|
|
||||||
<DeviceBlock>
|
<DeviceBlock>
|
||||||
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
|
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
|
||||||
<p>Текущее устройство</p>
|
<p>Корневой адрес</p>
|
||||||
</DeviceControl>
|
</DeviceControl>
|
||||||
{currentDevice.hostname == '' ? (
|
{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>
|
</DeviceBlock>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ import { useToolBar } from "../../../../hooks/useToolBar"
|
||||||
import { Space } from "../../../../const/menus/sideBar/components"
|
import { Space } from "../../../../const/menus/sideBar/components"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div<{isOpen: boolean}>`
|
||||||
background-color: ${(props) => props.theme.secondary};
|
background-color: ${(props) => props.theme.secondary};
|
||||||
width: 250px;
|
width: 250px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
flex: 3;
|
flex: 3;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
|
||||||
|
transition: max-width .4s ease;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ToolSpace = () => {
|
export const ToolSpace = () => {
|
||||||
|
|
@ -15,12 +19,12 @@ export const ToolSpace = () => {
|
||||||
const name = toolBar?.current || ''
|
const name = toolBar?.current || ''
|
||||||
const { isActive, isOpen } = useToolBar(name)
|
const { isActive, isOpen } = useToolBar(name)
|
||||||
|
|
||||||
if (!isActive || !isOpen) {
|
if (!isActive) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container isOpen={isOpen}>
|
||||||
<Space name={name} />
|
<Space name={name} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { workSpaceNames } from "../../const/workspace/names";
|
||||||
import { Header } from "./modules/appModules/header";
|
import { Header } from "./modules/appModules/header";
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
import About from "./modules/appModules/about";
|
import About from "./modules/appModules/about";
|
||||||
|
import UserScanner from "./modules/appModules/usersScanner/userScanner";
|
||||||
|
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
|
||||||
|
|
||||||
const Section = styled.div`
|
const Section = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -75,6 +77,12 @@ export default function WorkSpace() {
|
||||||
from={"helloWindow"}
|
from={"helloWindow"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{workModule === workSpaceNames[5] && (
|
||||||
|
<UserScanner />
|
||||||
|
)}
|
||||||
|
{workModule === workSpaceNames[6] && (
|
||||||
|
<WifiScanner />
|
||||||
|
)}
|
||||||
{workModule === workSpaceNames[3] &&
|
{workModule === workSpaceNames[3] &&
|
||||||
<WorkSection>
|
<WorkSection>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,13 @@ import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
|
||||||
import edit from '@iconify/icons-ic/baseline-edit';
|
import edit from '@iconify/icons-ic/baseline-edit';
|
||||||
import checkCircle from '@iconify/icons-ic/round-check-circle';
|
import checkCircle from '@iconify/icons-ic/round-check-circle';
|
||||||
import cancel from '@iconify/icons-ic/baseline-cancel';
|
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 admin from '@iconify/icons-ic/baseline-admin-panel-settings';
|
||||||
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
|
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
|
||||||
import refresh from '@iconify/icons-ic/baseline-refresh';
|
import refresh from '@iconify/icons-ic/baseline-refresh';
|
||||||
import close from '@iconify/icons-ic/round-close';
|
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 = {
|
export const ICONS = {
|
||||||
[buttonNames[1]]: exitToApp,
|
[buttonNames[1]]: exitToApp,
|
||||||
|
|
@ -30,8 +33,10 @@ export const ICONS = {
|
||||||
[buttonNames[10]]: admin,
|
[buttonNames[10]]: admin,
|
||||||
[buttonNames[11]]: refresh,
|
[buttonNames[11]]: refresh,
|
||||||
[buttonNames[12]]: close,
|
[buttonNames[12]]: close,
|
||||||
[buttonNames[13]]: noAdmin
|
[buttonNames[13]]: noAdmin,
|
||||||
|
[buttonNames[14]]: userSearch,
|
||||||
|
[buttonNames[15]]: wifiFind,
|
||||||
|
[buttonNames[16]]: noteEdit,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type IconName = keyof typeof ICONS;
|
type IconName = keyof typeof ICONS;
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,8 @@ export const buttonNames: names ={
|
||||||
10: 'admin',
|
10: 'admin',
|
||||||
11: 'reset',
|
11: 'reset',
|
||||||
12: 'close',
|
12: 'close',
|
||||||
13: 'noAdmin'
|
13: 'noAdmin',
|
||||||
|
14: 'userSearch',
|
||||||
|
15: 'wifiFind',
|
||||||
|
16: 'noteEdit'
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const baseTheme = {
|
const baseTheme = {
|
||||||
accent: '#007ACC',
|
accent: '#007ACC',
|
||||||
|
successColor: "#4CAf50",
|
||||||
|
errorColor: "#F44336",
|
||||||
accentHover: '#4A90E2',
|
accentHover: '#4A90E2',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9,6 +11,10 @@ export const light = {
|
||||||
secondary: '#EDEDED',
|
secondary: '#EDEDED',
|
||||||
background: '#E8E8E8',
|
background: '#E8E8E8',
|
||||||
textColor: '#000000',
|
textColor: '#000000',
|
||||||
|
borderColor: '#404040',
|
||||||
|
scannerColor: '#007ACC',
|
||||||
|
offlineColor: '#ababab',
|
||||||
|
hoverBtn: '#d6d6d6',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dark = {
|
export const dark = {
|
||||||
|
|
@ -17,4 +23,8 @@ export const dark = {
|
||||||
secondary: '#252525',
|
secondary: '#252525',
|
||||||
background: '#3A3A3A',
|
background: '#3A3A3A',
|
||||||
textColor: '#FFFFFF',
|
textColor: '#FFFFFF',
|
||||||
|
borderColor: '#b6b6b6',
|
||||||
|
scannerColor: '#0E9C9D',
|
||||||
|
offlineColor: '#656565',
|
||||||
|
hoverBtn: '#4a4a4a',
|
||||||
};
|
};
|
||||||
|
|
@ -4,5 +4,7 @@ export const workSpaceNames: names ={
|
||||||
1: 'helloWindow',
|
1: 'helloWindow',
|
||||||
2: 'aboutFromWork',
|
2: 'aboutFromWork',
|
||||||
3: 'networkInterfaces',
|
3: 'networkInterfaces',
|
||||||
4: 'aboutFromHello'
|
4: 'aboutFromHello',
|
||||||
|
5: 'userScanner',
|
||||||
|
6: 'wifiScanner',
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ interface IpAddressState {
|
||||||
export const useIpStore = create<IpAddressState>()(
|
export const useIpStore = create<IpAddressState>()(
|
||||||
persist(
|
persist(
|
||||||
(set)=>({
|
(set)=>({
|
||||||
ipAddress: 'localhost',
|
ipAddress: '192:168.1.147',
|
||||||
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
|
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
51
FRONT/BUILD/resources/app/src/store/useDeviceStore.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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,6 +1,5 @@
|
||||||
import { useIpStore } from "../store";
|
import { useIpStore } from "../store";
|
||||||
import { useAdminStore } from "../store/adminStore";
|
import { useAdminStore } from "../store/adminStore";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { NetworkService } from "../api/request";
|
import { NetworkService } from "../api/request";
|
||||||
|
|
||||||
export const useCheckAdmin = () => {
|
export const useCheckAdmin = () => {
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
61
FRONT/DEV/index.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
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 />);
|
||||||
|
|
@ -21,7 +21,7 @@ function createWindow() {
|
||||||
win.show();
|
win.show();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
win.loadURL('http://localhost:3000');
|
win.loadURL('https://localhost:8001');
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
||||||
4966
FRONT/DEV/package-lock.json
generated
Normal file
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/icons-ic": "^1.2.13",
|
"@iconify/icons-ic": "^1.2.13",
|
||||||
|
"@iconify/icons-ph": "^1.2.5",
|
||||||
"@iconify/react": "^5.2.0",
|
"@iconify/react": "^5.2.0",
|
||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"@xyflow/react": "^12.3.5",
|
"@xyflow/react": "^12.3.5",
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"electron": "^34.0.2",
|
"electron": "^39.2.3",
|
||||||
"electron-packager": "^17.1.2",
|
"electron-packager": "^17.1.2",
|
||||||
"esbuild-wasm": "^0.17.12",
|
"esbuild-wasm": "^0.17.12",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
11
FRONT/DEV/preload.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
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))
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 459 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 966 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
186
FRONT/DEV/public/locales/en/common.json
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
192
FRONT/DEV/public/locales/ru/common.json
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
{
|
||||||
|
"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": "Английский"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
196
FRONT/DEV/src/api/request.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
FRONT/DEV/src/components/UI/Dropdown/DeviceCard.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
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;
|
||||||
130
FRONT/DEV/src/components/UI/Dropdown/DropdownType.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
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;
|
||||||
77
FRONT/DEV/src/components/UI/Pagination.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
||||||
|
|
@ -9,18 +9,18 @@ const Button = styled.button.withConfig({
|
||||||
border: none;
|
border: none;
|
||||||
padding: ${(props) =>
|
padding: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "4px 8px"
|
? "10px 28px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "10px 20px"
|
? "10px 20px"
|
||||||
: "6px 10px"}; // По умолчанию
|
: "12px 20px"}; // По умолчанию
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: ${(props) =>
|
font-size: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "12px"
|
|
||||||
: props.size === "big"
|
|
||||||
? "16px"
|
? "16px"
|
||||||
: "14px"}; // По умолчанию
|
: props.size === "big"
|
||||||
|
? "18px"
|
||||||
|
: "16px"}; // По умолчанию
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
|
||||||
? 'row-reverse'
|
? 'row-reverse'
|
||||||
: 'row'};
|
: 'row'};
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
transition: all .3s ease;
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: ${(props) => props.$hoverсolor};
|
background-color: ${(props) => props.$hoverсolor};
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ type Props = {
|
||||||
placeholderText: string;
|
placeholderText: string;
|
||||||
id: string;
|
id: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (identifier: string, value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
type?: string;
|
type?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
|
@ -33,7 +33,7 @@ const StyledInput = styled.input`
|
||||||
border: 1px solid #adadad;
|
border: 1px solid #adadad;
|
||||||
}
|
}
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 14px;
|
font-size: 18px;
|
||||||
color: ${props=>props.theme.textColor}
|
color: ${props=>props.theme.textColor}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id,
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(id, value);
|
onChange(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2,19 +2,21 @@ import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface SignalSVGProps {
|
interface SignalSVGProps {
|
||||||
signal: number; // Принимаем signal как пропс
|
signal: number; // Принимаем signal как пропс
|
||||||
|
color: string;
|
||||||
|
size: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
|
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем цвета в зависимости от signal
|
// Устанавливаем цвета в зависимости от signal
|
||||||
const colors = {
|
const colors = {
|
||||||
'--st1-color': signal >= 1 ? 'white' : 'black',
|
'--st1-color': signal >= 1 ? color : 'black',
|
||||||
'--st2-color': signal >= 2 ? 'white' : 'black',
|
'--st2-color': signal >= 2 ? color : 'black',
|
||||||
'--st3-color': signal >= 3 ? 'white' : 'black',
|
'--st3-color': signal >= 3 ? color : 'black',
|
||||||
'--st4-color': signal >= 4 ? 'white' : 'black',
|
'--st4-color': signal >= 4 ? color : 'black',
|
||||||
'--st5-color': signal === 5 ? 'white' : 'black',
|
'--st5-color': signal === 5 ? color : 'black',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Применяем стили к SVG
|
// Применяем стили к SVG
|
||||||
|
|
@ -35,8 +37,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 296 235"
|
viewBox="0 0 296 235"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
width="20px" // Фиксированная ширина
|
width={size} // Фиксированная ширина
|
||||||
height="20px"
|
height={size}
|
||||||
>
|
>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
{`
|
{`
|
||||||