Compare commits

...

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

674 changed files with 60086 additions and 253210 deletions

3
.gitignore vendored
View file

@ -6,5 +6,6 @@ __pycache__
.wheels
.vendor
.python3-deb
SRV/storage/devices/devices.json
SRV/storage/subnets/*
SRV/storage/wifi/wifi.json
build/

137
.woodpecker/.woodpecker.yml Normal file
View 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
View 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>

View file

@ -100,7 +100,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || device.ip}</Value>
<Value>{device.hostname || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>

View file

@ -87,7 +87,7 @@ const TableScanner = ({devices}:TableScannerProps) => {
{devices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<TableCell>{item.hostname || item.ip}</TableCell>
<TableCell>{item.hostname || 'Неизвестен'}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>

View file

@ -31,7 +31,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (

View file

@ -30,7 +30,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (

View file

@ -163,7 +163,7 @@ export default function UserScanner() {
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов в подсети</TitleWrapper>)}
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование пользователей в сети</TitleWrapper>)}
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
@ -177,7 +177,7 @@ export default function UserScanner() {
{loading ? (
<LoadingText>
Сканирование узлов в подсети<ScanningText />
Сканирование сети<ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />

View file

@ -161,7 +161,7 @@ export function WifiScanner() {
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование точек доступа Wi-Fi</TitleWrapper>
<TitleWrapper>Сканирование Wi-Fi сети</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
@ -186,11 +186,11 @@ export function WifiScanner() {
{loading ? (
<LoadingText>
Сканирование точек доступа <ScanningText/>
Сканирование сети <ScanningText/>
</LoadingText>
) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'}
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
)}
</Wrapper>

View file

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

View file

@ -115,7 +115,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
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(() => {
testSocket.close();
resolve(false);

View file

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Before After
Before After

61
FRONT/DEV/index.tsx Normal file
View 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 />);

4966
FRONT/DEV/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.4.1",
"electron": "^34.0.2",
"electron": "^39.2.3",
"electron-packager": "^17.1.2",
"esbuild-wasm": "^0.17.12",
"typescript": "^4.9.5",

11
FRONT/DEV/preload.js Normal file
View 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))
});

View file

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 459 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 579 B

After

Width:  |  Height:  |  Size: 579 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 966 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 906 B

Before After
Before After

View 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"
}

View 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": "Английский"
}

View file

@ -1,13 +1,13 @@
import axios, { AxiosResponse } from "axios";
const axiosWithoutCredentials = axios.create({
baseURL: `https://localhost:8001`,
baseURL: `http://localhost:8001`,
})
export class NetworkService {
static createAxiosInstance(ipAddress: string) {
return axios.create({
baseURL: `https://${ipAddress}:8001`,
baseURL: `http://${ipAddress}:8001`,
});
}
@ -83,10 +83,12 @@ export class NetworkService {
}
}
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices');
return await axiosInstance.post('/subnet/devices',{
subnet: ipScanning,
});
} catch (error: any) {
return error.response;
}
@ -157,4 +159,38 @@ export class NetworkService {
return error.response;
}
}
// Settings
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/clear_subnet`);
} catch (error: any) {
return error.response;
}
}
static async clearNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/clear_wifi`);
} catch (error: any) {
return error.response;
}
}
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_devices`);
} catch (error: any) {
return error.response;
}
}
static async exportNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_wifi`);
} catch (error: any) {
return error.response;
}
}
}

View file

@ -5,6 +5,7 @@ import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {IconComponent} from "../../../const/buttons/icons";
import {useModalContext} from "../../../store";
import {useNameSetting} from "../../../hooks/useSettings";
interface DeviceCardProps {
device: DeviceUser;
@ -74,8 +75,8 @@ const Value = styled.span`
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => setIsExpanded(!isExpanded);
const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
@ -87,7 +88,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
return (
<CardWrapper status={status} onClick={toggleExpanded}>
<CardWrapper status={status} onClick={()=>setIsExpanded(!isExpanded)}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
@ -100,7 +101,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || device.ip}</Value>
<Value>{device.hostname || (nameSetting === 'ip' ? device.ip : 'Неизвестно')}</Value>
</InfoRow>
<InfoRow>

View 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;

View file

@ -17,10 +17,10 @@ const Button = styled.button.withConfig({
color: white;
font-size: ${(props) =>
props.size === "small"
? "14px"
: props.size === "big"
? "16px"
: "14px"}; // По умолчанию
: props.size === "big"
? "18px"
: "16px"}; // По умолчанию
cursor: pointer;
display: flex;
justify-content: center;

View file

@ -3,12 +3,13 @@ import { Handle, Position } from '@xyflow/react';
import { Ip } from './style';
import { Row, IpTitle, IpContent } from './style';
import Input from '../../../../UI/inputs/input';
import { useModalContext, usePannableStore } from '../../../../../store';
import {useIpStore, useModalContext, usePannableStore} from '../../../../../store';
import { ModalMap } from '../../../../modalwindows/windowsType';
import { validateIPAddress, validateSameSubnet } from '../../../../../utils/nodeController/validator';
import {changeIp} from '../../../../../const/types/types'
import { IconComponent } from "../../../../../const/buttons/icons";
import { buttonNames } from "../../../../../const/buttons/names";
import {NetworkService} from "../../../../../api/request";
interface IIp {
ipType: string
@ -45,6 +46,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
const { toggleEditingMode } = usePannableStore();
const [isInput, setIsInput] = useState(false)
const { openModal } = useModalContext();
const {ipAddress} = useIpStore();
const [ipData, setIpData] = useState<changeIp>(
{
@ -146,7 +148,20 @@ export const IpNode = ({ data }: CustomNodeProps) => {
setIsInput(false)
};
const switchMethod = async ()=>{
const methodDCHP ={
"connection_id": ipData.connection,
"ipv4_updates": {
"ipv4.method": "auto",
}
}
try {
await NetworkService.saveIp(ipAddress, methodDCHP);
window.location.reload();
}catch (err){
console.error(err)
}
}
return (
<>
@ -170,7 +185,10 @@ export const IpNode = ({ data }: CustomNodeProps) => {
</Row>
) : (
<Row>
{typeMap[data.ipData.ipType] ==='Static' && <IconComponent name={buttonNames[18]} onClick={switchMethod} width='18' height='18' setColor='white'/>}
<IconComponent name={buttonNames[7]} onClick={handleClick} width='18' height='18' setColor='white'/>
</Row>
)}
</IpTitle>

View file

@ -8,6 +8,7 @@ import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
@ -19,53 +20,55 @@ interface Props {
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const { ipAddress } = useIpStore();
const {ipAddress} = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleSave = async () => {
try {
setIsLoading(true);
setError('')
setError('');
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if(res.status === 200) {
onClose()
}else{
setError(res.data.detail)
if (res.status === 200) {
onClose();
} else {
setError(res.data.detail);
}
} catch (error) {
console.error('Error connection:', error);
setError('Ошибка подключения к сети')
}finally {
console.error(t('connectionErrorLog'), error);
setError(t('connectionError'));
} finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{ height: '230px' }}>
<ModalContainer style={{height: '230px'}}>
<ModalTitle>
<h3>Подключение к сети</h3>
<h3>{t('networkConnection')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<Input
type="password"
placeholderText='Введите пароль...'
placeholderText={t('enterPassword')}
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
{error && (
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
{error}
</p>
)}
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
@ -73,12 +76,12 @@ export const ConnectNetwork = ({address, onClose}: Props) => {
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title='Подключиться'
title={t('connect')}
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
{isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
</ModalContainer>
);
};

View file

@ -1,4 +1,3 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
@ -6,6 +5,7 @@ import { useState } from "react";
import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
@ -20,6 +20,7 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => {
try {
@ -50,13 +51,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
return (
<ModalContainer style={{ height: '250px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
placeholderText={t('note-enter')}
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
@ -67,13 +68,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
<MainButton
type="button"
size="small"
title="Отмена"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
title={t('save')}
onClick={handleSave}
/>
</ButtonContainer>

View file

@ -6,6 +6,7 @@ import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
@ -20,6 +21,7 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => {
try {
@ -50,13 +52,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
return (
<ModalContainer style={{ height: '220px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
placeholderText={t('note-enter')}
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
@ -67,13 +69,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
<MainButton
type="button"
size="small"
title="Отмена"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
title={t('save')}
onClick={handleSave}
/>
</ButtonContainer>

View file

@ -3,6 +3,7 @@ import { MainButton } from "../../UI/buttons/mainButton";
import { useState } from "react";
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { validateIPAddress } from "../../../utils/nodeController/validator";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
onSave?: (data: string) => void;
@ -12,6 +13,7 @@ interface Props {
export const AddDevice = ({ onSave, onClose }: Props) => {
const [ipAddress, setIpAddress] = useState('')
const [error, setError] = useState(false)
const {t} = useTranslation('common');
const handleSubmit = () => {
if (!validateIPAddress(ipAddress)) {
@ -28,31 +30,39 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
return (
<ModalContainer style={{ height: '230px' }}>
{/* <form onSubmit={handleSubmit}> */}
<ModalTitle>
<h3>Добавление узла</h3>
<h3>{t('addNode')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<Input
type="text"
placeholderText='Введите адрес узла...'
placeholderText={t('enterNodeAddress')}
id="newIp"
value={ipAddress}
onChange={(value) => setIpAddress(value)}
/>
{error && (
<p style={{ color: 'red', marginTop: '5px' }}>
Некорректный адрес узла
{t('invalidAddress')}
</p>
)}
</ModalContent>
<ButtonContainer>
<MainButton type="button" size="small" title="Отмена" onClick={onClose} />
<MainButton type="submit" size="small" title="Сохранить" onClick={handleSubmit} />
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="submit"
size="small"
title={t('save')}
onClick={handleSubmit}
/>
</ButtonContainer>
{/* </form> */}
</ModalContainer>
)
}

View file

@ -2,6 +2,7 @@ import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer }
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={
message: string
@ -10,28 +11,37 @@ type Props={
export const ConnectionError = ({message}: Props)=>{
const {closeModal} = useModalContext()
const { setIpAddress } = useIpStore();
const {t} = useTranslation('common');
const close = ()=>{
setIpAddress('localhost')
closeModal()
}
return(
return (
<ModalContainer>
<ModalTitle>
<h3>Соединение прервано</h3>
<h3>{t('connectionLost')}</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Произошёл разрыв с сервером</h3>
<p>Пожалуйста, проверьте доступ к адресу и попробуйте снова.</p>
<h3>{t('serverDisconnected')}</h3>
<p>{t('checkAddressAndRetry')}</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Закрыть" onClick={close} size="small"/>
<MainButton title="Повторить" onClick={closeModal} size="small"/>
<MainButton
title={t('close')}
onClick={close}
size="small"
/>
<MainButton
title={t('retry')}
onClick={closeModal}
size="small"
/>
</ButtonContainer>
</ModalContainer>
)

View file

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

View file

@ -6,6 +6,7 @@ import {useState} from "react";
import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={
address: string,
@ -16,6 +17,7 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleDisconnect = async () => {
try {
@ -34,21 +36,32 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
return(
<ModalContainer>
<ModalTitle>
<h3>Отключение от Wi-Fi сети</h3>
<h3>{t('wifiDisconnect-title')}</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Подтверждение отключения</h3>
<h3>{t('wifiDisconnect-confirm')}</h3>
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Отменить" onClick={onClose} size="small"/>
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
<MainButton
title={t('cancel')}
onClick={onClose}
size="small"
/>
<MainButton
title={t('wifiDisconnect-disconnect')}
color={theme.errorColor}
hoverColor={theme.errorColor}
onClick={handleDisconnect}
size="small"
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
{isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
</ModalContainer>
)
}

View file

@ -8,6 +8,7 @@ import { useIpStore } from "../../../store";
import { MainButton } from "../../UI/buttons/mainButton";
import { useAdminStore } from "../../../store/adminStore";
import { useDevicesStore } from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props = {
ipData: changeIp
@ -52,6 +53,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
const { ipAddress, setIpAddress } = useIpStore();
const {isAdmin} = useAdminStore()
const { devices, addDevice, deleteDevice } = useDevicesStore();
const {t} = useTranslation('common');
useEffect(() => {
if (ipData && isAdmin) {
@ -146,40 +148,48 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
return (
<>
{loader && isAdmin ? (
<Loader loaderText="Идет проверка корректности IP адреса" />
<Loader loaderText={t('ipChange-checkingIp')} />
) : (
<ModalContainer {...(checkStatus ? { height: "unset" } : {})}>
<ModalTitle>
<h3>Изменение IP-Адреса</h3>
<h3>{t('ipChange-title')}</h3>
</ModalTitle>
{checkStatus ? (
<ModalContent>
<Information>
<h3>Общие сведения</h3>
<h3>{t('ipChange-generalInfo')}</h3>
<Info>
<p>Интерфейс: {ipData.device}</p>
<p>{t('ipChange-interface')}: {ipData.device}</p>
<p>--{'>'}</p>
<p>{ipData.connection}</p>
</Info>
<h3>Сведения об IP</h3>
<h3>{t('ipChange-ipInfo')}</h3>
<DataContainer>
<CurrentData>
<p>Текущий ip: {ipData.currentIp}</p>
<p>Текущий шлюз: {ipData.currentGate}</p>
<p>Текущий DNS: {ipData.currentDns}</p>
<p>{t('ipChange-currentIp')}: {ipData.currentIp}</p>
<p>{t('ipChange-currentGateway')}: {ipData.currentGate}</p>
<p>{t('ipChange-currentDns')}: {ipData.currentDns}</p>
</CurrentData>
<NewData>
<p>Новый ip: {ipData.newIp}</p>
<p>Новый шлюз: {ipData.newGate}</p>
<p>Новый DNS: {ipData.newDns}</p>
<p>{t('ipChange-newIp')}: {ipData.newIp}</p>
<p>{t('ipChange-newGateway')}: {ipData.newGate}</p>
<p>{t('ipChange-newDns')}: {ipData.newDns}</p>
</NewData>
</DataContainer>
</Information>
<ButtonContainer>
<MainButton title="Отмена" size='small' onClick={() => cancelIp(currentIp)}/>
<MainButton title="Сохранить" size='small' onClick={() => saveIp(newIP)}/>
<MainButton
title={t('cancel')}
size='small'
onClick={() => cancelIp(currentIp)}
/>
<MainButton
title={t('save')}
size='small'
onClick={() => saveIp(newIP)}
/>
</ButtonContainer>
</ModalContent>
) : (
@ -188,19 +198,21 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
<ModalText>
{isAdmin ? (
<>
<h3>Произошла ошибка проверки корректности</h3>
<h3>{t('ipChange-checkError')}</h3>
<ErrorMessage>{message}</ErrorMessage>
</>
):(
<h3>Для изменения сетевых настроек необходимо авторизироваться под администратором</h3>
<h3>{t('ipChange-adminRequired')}</h3>
)}
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Закрыть" size='small' onClick={smoothClose}/>
<MainButton
title={t('close')}
size='small'
onClick={smoothClose}
/>
</ButtonContainer>
</>
)}

View file

@ -9,6 +9,7 @@ import { useCheckAdmin } from "../../../utils/checkAdmin"
import { buttonNames } from "../../../const/buttons/names"
import { IconComponent } from "../../../const/buttons/icons"
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
onClose: () => void;
@ -17,9 +18,10 @@ interface Props {
export const RightsConfirm = ({ onClose }: Props) => {
const { ipAddress } = useIpStore();
const { isAdmin, setAdmin } = useAdminStore()
const { isAdmin } = useAdminStore()
const checkAdmin = useCheckAdmin();
const [error, setError] = useState(false)
const {t} = useTranslation('common');
const [success, setSuccess] = useState({
message: '',
status: false
@ -45,7 +47,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
setError(false)
setSuccess(prevData => ({
...prevData,
message: 'Успешный вход',
message: t('auth-successLogin'),
status: true
}));
await new Promise(resolve => setTimeout(resolve, 3000));
@ -54,7 +56,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
setError(true)
}
} catch (err) {
console.error('Error:', err);
console.error(t('auth-error'), err);
}
}
@ -63,7 +65,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
await checkAdmin();
setSuccess(prevData => ({
...prevData,
message: 'Успешный выход',
message: t('auth-successLogout'),
status: true
}));
await new Promise(resolve => setTimeout(resolve, 3000));
@ -76,15 +78,14 @@ export const RightsConfirm = ({ onClose }: Props) => {
!isAdmin ? (
<>
<ModalTitle>
<h3>Получение прав доступа</h3>
<h3>{t('auth-getAccess')}</h3>
</ModalTitle>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>Включить административный доступ</h3>
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>{t('auth-enableAdmin')}</h3>
<Input
type="password"
placeholderText='Введите пароль администратора...'
placeholderText={t('auth-enterAdminPass')}
id="password"
value={user.password}
onChange={(value) => handleChange('password', value)}
@ -92,44 +93,40 @@ export const RightsConfirm = ({ onClose }: Props) => {
{error && (
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
Неверный пароль оператора
{t('auth-wrongPass')}
</p>
)}
</ModalContent>
<ButtonContainer>
<MainButton size="small" title="Отмена" onClick={onClose} />
<MainButton size="small" title="Отправить" onClick={() => login(user)} />
<MainButton size="small" title={t('cancel')} onClick={onClose} />
<MainButton size="small" title={t('submit')} onClick={() => login(user)} />
</ButtonContainer>
</>
) : (
<>
<ModalTitle>
<h3>Переключиться на ограниченный доступ</h3>
<h3>{t('auth-switchToLimited')}</h3>
</ModalTitle>
<ModalContent>
<p>В данном режиме доступа ограничиваются полномочия администратора.</p>
<p>{t('auth-limitedModeDesc')}</p>
</ModalContent>
<ButtonContainer>
<MainButton size="small" title="Отмена" onClick={onClose} />
<MainButton size="small" title="Ограничить доступ" onClick={logout} />
<MainButton size="small" title={t('cancel')} onClick={onClose} />
<MainButton size="small" title={t('auth-restrictAccess')} onClick={logout} />
</ButtonContainer>
</>
)
):(
<ModalContainer style={{display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '15px'}}>
<IconComponent name={buttonNames[8]} width="50" height="50"/>
<h3>{success.message}</h3>
<p style={{fontSize: '12px'}}>Это окно можно закрыть, либо оно пропадёт само</p>
<MainButton size="small" title="Закрыть" onClick={onClose} style={{width: '30%'}}/>
<p style={{fontSize: '12px'}}>{t('auth-closeHint')}</p>
<MainButton size="small" title={t('close')} onClick={onClose} style={{width: '30%'}}/>
</ModalContainer>
)}
</ModalContainer>
)
}

View file

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

View file

@ -13,7 +13,7 @@ export const ModalWindow = styled.div`
height: 100%;
`
export const ModalContainer = styled.div<{height?: string}>`
export const ModalContainer = styled.div<{height?: string, width?:string}>`
border-radius: 12px;
background: ${(props)=>props.theme.secondary};
color: ${(props=>props.theme.textColor)};
@ -25,13 +25,14 @@ export const ModalContainer = styled.div<{height?: string}>`
justify-content: space-between;
gap: 15px;
height: ${(props) => props.height ? props.height : '230px'};
width: 520px;
width: ${(props) => props.width ? props.width : '520px'};
`
export const ModalTitle = styled.div`
padding: 20px;
border-bottom: 1px solid #9D9D9D;
display: flex;
align-items: center;
justify-content: space-between;
h3{

View file

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

View file

@ -9,6 +9,7 @@ import { IconComponent } from "../const/buttons/icons"
import { buttonNames } from "../const/buttons/names"
import ThemeButton from "./UI/buttons/toggletheme"
import About from "./workSpace/modules/appModules/about"
import {useTranslation} from "../utils/i18n/hooks";
const Wrapper = styled.div`
display: flex;
@ -73,6 +74,7 @@ export const CreateUser = () => {
const [repeatPassword, setRepeatPassword] = useState('');
const [passwordsMatch, setPasswordsMatch] = useState(true);
const [about, setAbout] = useState(false)
const {t} = useTranslation('common')
const [user, setUser] = useState({
username: 'operator',
@ -118,31 +120,27 @@ export const CreateUser = () => {
setAbout(true)
}
return (
<>
{!about ? (
<Wrapper>
<Container onSubmit={handleSubmit}>
<Title>
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent>
<h3>Настройка оператора узла</h3>
<IconComponent name={buttonNames[3]} width="52" height="52" />
<h3>{t('operatorConfig-title')}</h3>
{ipAddress != 'localhost' &&
<p>{ipAddress}</p>
}
</Title>
<Content>
<div>
<p>Перед началом работы необходимо создать пароль оператора приложения</p>
<p>{t('operatorConfig-instruction')}</p>
</div>
<div>
<Input
type="password"
placeholderText='Введите пароль...'
placeholderText={t('operatorConfig-passwordPlaceholder')}
id="password"
value={user.password}
onChange={(value) => handleChange('password', value)}
@ -150,7 +148,7 @@ export const CreateUser = () => {
<Input
type="password"
placeholderText='Повторите пароль...'
placeholderText={t('operatorConfig-repeatPasswordPlaceholder')}
id="repeatPassword"
value={repeatPassword}
onChange={(value) => handleChange('repeatPassword', value)}
@ -158,37 +156,45 @@ export const CreateUser = () => {
/>
{!passwordsMatch && (
<p style={{ color: 'red', marginTop: '5px' }}>
Пароли не совпадают!
{t('operatorConfig-passwordMismatch')}
</p>
)}
</div>
</Content>
<ButtonContainer>
<MainButton style={{ width: '100%' }} title="Отправить" type="submit" />
<MainButton
style={{ width: '100%' }}
title={t('operatorConfig-submit')}
type="submit"
/>
</ButtonContainer>
</Container>
<Menu>
{ipAddress != 'localhost' &&
<MainButton title="Назад" onClick={back} style={{marginRight: '10px'}}/>
<MainButton
title={t('common-back')}
onClick={back}
style={{marginRight: '10px'}}
/>
}
<ThemeButton />
<Button>
<IconContainer>
<IconComponent name={buttonNames[4]} width="30" height="30" onClick={toAbout}/>
<IconComponent
name={buttonNames[4]}
width="30"
height="30"
onClick={toAbout}
/>
</IconContainer>
</Button>
</Menu>
</Wrapper>
): (
) : (
<About from="reg" onBack={() => setAbout(false)} />
)}
</>
)
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
import {useTranslation} from "../../../../utils/i18n/hooks";
const Wrapper = styled.div`
width: 100%;
@ -49,30 +50,33 @@
`
export const HelloWindow = () => {
const {t} = useTranslation('common')
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
<img
src="./assets/images/logo.svg"
alt={t('main-logoAlt')}
style={{width: '152px'}}
/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
title={t('main-networkSettings')}
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
title={t('main-about')}
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={()=>setWorkModule(workSpaceNames[4])}
onClick={() => setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
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";
import {TableScanner} from "./TableScanner";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button`
position: relative;
@ -20,23 +20,24 @@ const ScanBtn = styled.button`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
handleScan: (replay?: boolean) => void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
<ScanBtn onClick={() => handleScan()} disabled={loading}>
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null}
</>
);

View file

@ -1,11 +1,15 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import { useState } from "react";
import {useEffect, useState, useTransition} from "react";
import { NetworkService } from "../../../../../api/request";
import ListUsers from "./modules/ListUsers";
import TableUsers from "./modules/TableUsers";
import DropdownType from "../../../../UI/Dropdown/DropdownType";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
import IpScanning from "./modules/IpScanning";
import {useTranslation} from "../../../../../utils/i18n/hooks";
type Option ={
value: number,
@ -51,7 +55,7 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 50px;
padding: 80px 50px 40px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
@ -59,13 +63,20 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
overflow-x: hidden;
position: relative;
`;
const TitleWrapper = styled.h2`
const TopWrapper = styled.div`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`
const TopTitle = styled.h2`
font-size: 32px;
width: max-content;
text-align: center;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
@ -77,7 +88,7 @@ const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.accent};
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@ -130,31 +141,65 @@ const ScanningText = styled.span`
export default function UserScanner() {
const {t} = useTranslation('common')
const { ipAddress } = useIpStore();
const [ipScanning, setIpScanning] = useState('');
const [loading, setLoading] = useState(false);
const [isValid, setIsValid] = useState(true);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
const validateIp = (ip: string) => {
const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
return regex.test(ip);
};
// Сканирование
const handleScan = async () => {
const handleScan = async (replay: boolean) => {
try {
if (ipScanning && !validateIp(ipScanning)) {
setIsValid(false);
return;
}
setIsValid(true);
if (!replay) {
setLoading(true);
}
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress);
console.log(res);
const res = await NetworkService.scanUser(ipAddress, ipScanning);
console.log(res)
setDevices(res.data.devices || []);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([])
setDevices([]);
} finally {
setLoading(false);
}
};
// Интервально сканирование
const intervalToggle = useIntervalBoolean();
const intervalTime = useIntervalTime();
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
if (intervalToggle) {
intervalId = setInterval(()=>handleScan(true), intervalTime * 60 * 1000);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [intervalToggle, intervalTime, handleScan]);
// Способ отображения
const viewOptions: Option[] = [
{ value: 1, label: 'Таблица' },
{ value: 2, label: 'Список' },
{ value: 1, label: t('userScanner-visible_table') },
{ value: 2, label: t('userScanner-visible_list') },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
@ -163,7 +208,18 @@ export default function UserScanner() {
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов в подсети</TitleWrapper>)}
<TopWrapper>
{(!scanInitiated || loading) && (<TopTitle>{t('userScanner-title')}</TopTitle>)}
<IpScanning
value={ipScanning}
setIpScanning={setIpScanning}
loading={loading}
isValid={isValid}
setIsValid={setIsValid}
/>
</TopWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
@ -172,17 +228,17 @@ export default function UserScanner() {
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
)}
{loading ? (
<LoadingText>
Сканирование узлов в подсети<ScanningText />
{t('userScanner-loading')} <ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
<ListUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);

View file

@ -5,6 +5,7 @@ import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store";
import {useTranslation} from "../../../../../utils/i18n/hooks";
// Анимации
const ring1 = keyframes`
@ -57,6 +58,7 @@ const TitleWrapper = styled.h2`
left: 50%;
transform: translateX(-50%);
font-size: 32px;
text-align: center;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
@ -137,14 +139,16 @@ const ScanBtn = styled.button<{scanInitiated: boolean}>`
export function WifiScanner() {
const { ipAddress } = useIpStore();
//const ipAddress = '192.168.1.148'
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const { t } = useTranslation('common');
const handleScan = async () => {
setWifiNetworks([]);
setCurrentNetwork(null)
setCurrentNetwork(null);
setLoading(true);
setScanInitiated(true);
try {
@ -152,7 +156,7 @@ export function WifiScanner() {
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
console.error(t('wifiScanner-scanError'), err);
setWifiNetworks([]);
} finally {
setLoading(false);
@ -161,17 +165,19 @@ export function WifiScanner() {
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование точек доступа Wi-Fi</TitleWrapper>
<TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
<span className="ring1 circel"></span>
<span className="ring2 circel"></span>
<span className="ring3 circel"></span>
{wifiNetworks.map((network) => (
{wifiNetworks.map((network, index) => (
<WifiDot
key={network.bssid}
network={network}
index={index}
total={wifiNetworks.length}
onSelect={setCurrentNetwork}
/>
))}
@ -186,11 +192,15 @@ export function WifiScanner() {
{loading ? (
<LoadingText>
Сканирование точек доступа <ScanningText/>
{t('wifiScanner-scanning')} <ScanningText/>
</LoadingText>
) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'}
<ScanBtn
onClick={handleScan}
disabled={loading}
scanInitiated={wifiNetworks.length > 0}
>
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
</ScanBtn>
)}
</Wrapper>

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