Compare commits
No commits in common. "develop" and "main" have entirely different histories.
5
.gitignore
vendored
|
|
@ -6,5 +6,6 @@ __pycache__
|
||||||
.wheels
|
.wheels
|
||||||
.vendor
|
.vendor
|
||||||
.python3-deb
|
.python3-deb
|
||||||
SRV/storage/devices/devices.json
|
SRV/storage/subnets/*
|
||||||
SRV/storage/wifi/wifi.json
|
SRV/storage/wifi/wifi.json
|
||||||
|
build/
|
||||||
|
|
|
||||||
137
.woodpecker/.woodpecker.yml
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
when:
|
||||||
|
event: manual
|
||||||
|
|
||||||
|
steps:
|
||||||
|
checkout:
|
||||||
|
image: debian:latest
|
||||||
|
commands:
|
||||||
|
# ПОДГОТОВКА ГИТА И КЛОНИРОВАНИЕ GONETS
|
||||||
|
- apt update
|
||||||
|
- apt install -y git git-lfs
|
||||||
|
- git lfs install
|
||||||
|
|
||||||
|
# УСТАНОВКА НЕОБХОДИМЫХ ЗАВИСИМОСТЕЙ НА СИСТЕМЕ
|
||||||
|
- apt update
|
||||||
|
- apt install -y python3 curl findutils grep sed openssh-server tree libnss3 libnss3-dev libnspr4 libnspr4-dev libasound2 libasound2-dev python3.11-venv python3-pip python3-poetry nodejs npm debhelper devscripts build-essential fakeroot dh-make dpkg-dev cmake libgirepository1.0-dev gobject-introspection libcairo2-dev pkg-config python3-dev libdbus-1-dev
|
||||||
|
- python3 --version
|
||||||
|
- node -v
|
||||||
|
- npm -v
|
||||||
|
raspberry
|
||||||
|
|
||||||
|
# ПОДГОТОВКА СПИСКА PYTHON ЗАВИСИМОСТЕЙ GONETS
|
||||||
|
- rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
|
||||||
|
- rm -rf /root/.local/share/pypoetry
|
||||||
|
- rm -rf /root/.poetry
|
||||||
|
- rm -rf /usr/bin/poetry
|
||||||
|
- pip install pipx
|
||||||
|
- pipx ensurepath
|
||||||
|
- pipx install poetry
|
||||||
|
- echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
|
||||||
|
- . /root/.bashrc
|
||||||
|
- poetry --version
|
||||||
|
- cd SRV
|
||||||
|
- poetry env activate
|
||||||
|
- poetry add poetry-plugin-export
|
||||||
|
- poetry install
|
||||||
|
- poetry self update
|
||||||
|
- rm -f requirements.txt
|
||||||
|
- poetry run poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||||||
|
|
||||||
|
# ПОДГОТОВКА И СБОРКА FRONTEND ЧАСТИ GONETS
|
||||||
|
- pwd
|
||||||
|
- cd ..
|
||||||
|
- pwd
|
||||||
|
- cd FRONT/DEV
|
||||||
|
- pwd
|
||||||
|
- npm install
|
||||||
|
- npm run package
|
||||||
|
- cd dist/gonets-linux-x64/resources/app
|
||||||
|
- pwd
|
||||||
|
- npm run build
|
||||||
|
- sed -i 's#href="/assets/#href="./assets/#g' dist/index.html
|
||||||
|
- sed -i 's#src="/assets/#src="./assets/#g' dist/index.html
|
||||||
|
- cd ../../../../..
|
||||||
|
- pwd
|
||||||
|
- rm -rf BUILD/*
|
||||||
|
- cp -r DEV/dist/gonets-linux-x64/* BUILD/
|
||||||
|
- rm -rf DEV
|
||||||
|
|
||||||
|
# ИНИЦИАЛИЗАЦИЯ DEBIAN КОНФИГУРАЦИЙ ДЛЯ СБОРКИ
|
||||||
|
- cd ..
|
||||||
|
- pwd
|
||||||
|
- cd ..
|
||||||
|
- pwd
|
||||||
|
- mv gonets gonets-0.1
|
||||||
|
- cd gonets-0.1
|
||||||
|
- pwd
|
||||||
|
- ls
|
||||||
|
- DEBEMAIL="devteam@gossys.dev" DEBFULLNAME="GOSSYS LLC" dh_make --single -y --createorig
|
||||||
|
- cd debian/
|
||||||
|
- rm -f *.ex
|
||||||
|
- rm -f README.*
|
||||||
|
|
||||||
|
# КЛОНИРОВАНИЕ НЕОБХОДИМЫХ ШАБЛОНОВ КОНФИГОВ ДЕБИАНА ДЛЯ СБОРКИ
|
||||||
|
- mkdir -p /woodpecker/src/dev.gossys.dev/HybrydMain/templates
|
||||||
|
- git clone https://s.galanskiy:${GONETS}@dev.gossys.dev/s.galanskiy/gonets-debian-template.git /woodpecker/src/dev.gossys.dev/HybrydMain/templates
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/changelog /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/control /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/copyright /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/install /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/postinst /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/postrm /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
- mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/rules /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
|
||||||
|
|
||||||
|
# СБОРКА ДЕБИАН ПАКЕТА
|
||||||
|
- cd ..
|
||||||
|
- pwd
|
||||||
|
- dpkg-buildpackage -us -uc -b
|
||||||
|
|
||||||
|
# ИЗВЛЕЧЕНИЕ ВЕРСИИ ПАКЕТА ДЛЯ КОММИТА
|
||||||
|
- VERSION=$(dpkg-parsechangelog -l./debian/changelog --show-field Version)
|
||||||
|
- echo "VERSION=$VERSION" > version.env
|
||||||
|
|
||||||
|
# КОММИТ И ТЕГ НА ВЕТКУ DEVELOP
|
||||||
|
- . ./version.env
|
||||||
|
- git config user.name "s.galanskiy"
|
||||||
|
- git config user.email "s.galanskiy@gossys.dev"
|
||||||
|
- git fetch --all --prune
|
||||||
|
- git branch -r
|
||||||
|
- git checkout -b "release/$VERSION"
|
||||||
|
- git commit -am "Релиз $VERSION"
|
||||||
|
- git tag -f "v$VERSION"
|
||||||
|
- git push "https://s.galanskiy:b4785198d18200dfed812dc4299575f736255bd7@dev.gossys.dev/HybrydMain/gonets.git" "release/$VERSION"
|
||||||
|
- git push -f https://s.galanskiy:b4785198d18200dfed812dc4299575f736255bd7@dev.gossys.dev/HybrydMain/gonets.git "v$VERSION"
|
||||||
|
|
||||||
|
# СОЗДАНИЕ И ЗАГРУЗКА РЕЛИЗА ЧЕРЕЗ API
|
||||||
|
- |
|
||||||
|
VERSION=$(dpkg-parsechangelog -l./debian/changelog --show-field Version)
|
||||||
|
echo "VERSION=$VERSION" > version.env
|
||||||
|
|
||||||
|
# Задать переменные
|
||||||
|
. ./version.env
|
||||||
|
TAG="v$VERSION"
|
||||||
|
|
||||||
|
DEB_PATH=$(find .. -maxdepth 1 -type f -name "gonets_*_amd64.deb" | head -n 1)
|
||||||
|
|
||||||
|
if [ ! -f "$DEB_PATH" ]; then
|
||||||
|
echo ".deb файл не найден по пути: $DEB_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_JSON=$(curl -s -X POST "https://dev.gossys.dev/api/v1/repos/HybrydMain/gonets/releases" \
|
||||||
|
-H "Authorization: token b4785198d18200dfed812dc4299575f736255bd7" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"$TAG\", \"name\": \"Релиз $TAG\", \"body\": \"Сборка .deb пакета для версии $TAG\", \"draft\": false, \"prerelease\": true}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | grep '"id":' | head -n1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Не удалось получить ID релиза из ответа:"
|
||||||
|
echo "$RELEASE_JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token b4785198d18200dfed812dc4299575f736255bd7" \
|
||||||
|
-F "attachment=@$DEB_PATH" \
|
||||||
|
"https://dev.gossys.dev/api/v1/repos/HybrydMain/gonets/releases/$RELEASE_ID/assets" || echo "Ошибка загрузки файла"
|
||||||
13
FRONT/BUILD/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GoNets</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/assets/icon-4fEsvohP.png" />
|
||||||
|
<script type="module" crossorigin src="/assets/index-QSWEVNb0.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-DWFkiel1.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -100,7 +100,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
||||||
<CardContent isExpanded={isExpanded}>
|
<CardContent isExpanded={isExpanded}>
|
||||||
<InfoRow>
|
<InfoRow>
|
||||||
<Label>Имя:</Label>
|
<Label>Имя:</Label>
|
||||||
<Value>{device.hostname || device.ip}</Value>
|
<Value>{device.hostname || 'Неизвестно'}</Value>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
|
|
||||||
<InfoRow>
|
<InfoRow>
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ const TableScanner = ({devices}:TableScannerProps) => {
|
||||||
{devices.map((item, i) => (
|
{devices.map((item, i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
|
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
|
||||||
<TableCell>{item.hostname || item.ip}</TableCell>
|
<TableCell>{item.hostname || 'Неизвестен'}</TableCell>
|
||||||
<TableCell>{item.vendor}</TableCell>
|
<TableCell>{item.vendor}</TableCell>
|
||||||
<TableCell>{item.ip}</TableCell>
|
<TableCell>{item.ip}</TableCell>
|
||||||
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
|
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScanBtn onClick={handleScan} disabled={loading}>
|
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
|
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
|
||||||
</ScanBtn>
|
</ScanBtn>
|
||||||
|
|
||||||
{devices.length > 0 ? (
|
{devices.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScanBtn onClick={handleScan} disabled={loading}>
|
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
|
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
|
||||||
</ScanBtn>
|
</ScanBtn>
|
||||||
|
|
||||||
{devices.length > 0 ? (
|
{devices.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ export default function UserScanner() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||||
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов в подсети</TitleWrapper>)}
|
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование пользователей в сети</TitleWrapper>)}
|
||||||
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||||
<span className="center"></span>
|
<span className="center"></span>
|
||||||
<span className="ring1"></span>
|
<span className="ring1"></span>
|
||||||
|
|
@ -177,7 +177,7 @@ export default function UserScanner() {
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingText>
|
<LoadingText>
|
||||||
Сканирование узлов в подсети<ScanningText />
|
Сканирование сети<ScanningText />
|
||||||
</LoadingText>
|
</LoadingText>
|
||||||
) : selectedView.value == 1 ? (
|
) : selectedView.value == 1 ? (
|
||||||
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
|
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export function WifiScanner() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||||
<TitleWrapper>Сканирование точек доступа Wi-Fi</TitleWrapper>
|
<TitleWrapper>Сканирование Wi-Fi сети</TitleWrapper>
|
||||||
|
|
||||||
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||||
<span className="center circel"></span>
|
<span className="center circel"></span>
|
||||||
|
|
@ -186,11 +186,11 @@ export function WifiScanner() {
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingText>
|
<LoadingText>
|
||||||
Сканирование точек доступа <ScanningText/>
|
Сканирование сети <ScanningText/>
|
||||||
</LoadingText>
|
</LoadingText>
|
||||||
) : (
|
) : (
|
||||||
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
|
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
|
||||||
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'}
|
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
|
||||||
</ScanBtn>
|
</ScanBtn>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
||||||
setHasInited(false)
|
setHasInited(false)
|
||||||
initializationAttempt.current = 0;
|
initializationAttempt.current = 0;
|
||||||
|
|
||||||
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
|
||||||
|
|
||||||
let connectionTimeout;
|
let connectionTimeout;
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ export const DevicesBar = () => {
|
||||||
|
|
||||||
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
testSocket.close();
|
testSocket.close();
|
||||||
resolve(false);
|
resolve(false);
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
61
FRONT/DEV/index.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
import { useIpStore } from './src/store';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useCheckAdmin } from './src/utils/checkAdmin';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModalProvider,
|
||||||
|
useModalContext,
|
||||||
|
ThemeProvider
|
||||||
|
} from './src/store';
|
||||||
|
|
||||||
|
import WindowComponent from './src/components/modalwindows/modalWindow';
|
||||||
|
import Router from './src/router';
|
||||||
|
import { I18nProvider } from "./src/utils/i18n/provider";
|
||||||
|
import { useLanguage } from "./src/hooks/useSettings";
|
||||||
|
|
||||||
|
const AppWithProviders = () => {
|
||||||
|
const currentLanguage = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<I18nProvider initialLanguage={currentLanguage}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<HashRouter>
|
||||||
|
<ModalProvider>
|
||||||
|
<MainComponent />
|
||||||
|
</ModalProvider>
|
||||||
|
</HashRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MainComponent = () => {
|
||||||
|
const checkAdmin = useCheckAdmin();
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAdminStatus = async () => {
|
||||||
|
await checkAdmin();
|
||||||
|
};
|
||||||
|
checkAdminStatus();
|
||||||
|
}, [ipAddress]);
|
||||||
|
|
||||||
|
const { isOpen, modalType, modalData } = useModalContext();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
|
||||||
|
<Router />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
if (!container) {
|
||||||
|
throw new Error('Root element with id "root" not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<AppWithProviders />);
|
||||||
4966
FRONT/DEV/package-lock.json
generated
Normal file
|
|
@ -30,7 +30,7 @@
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"electron": "^34.0.2",
|
"electron": "^39.2.3",
|
||||||
"electron-packager": "^17.1.2",
|
"electron-packager": "^17.1.2",
|
||||||
"esbuild-wasm": "^0.17.12",
|
"esbuild-wasm": "^0.17.12",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
11
FRONT/DEV/preload.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
send: (channel, data) => ipcRenderer.send(channel, data),
|
||||||
|
on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
|
||||||
|
quitApp: () => ipcRenderer.send('quit-app'),
|
||||||
|
|
||||||
|
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
||||||
|
writeFile: (path, data) => ipcRenderer.invoke('write-file', { path, data }),
|
||||||
|
onThemeChange: (callback) => ipcRenderer.on('theme-update', (event, theme) => callback(theme))
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 459 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 966 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
186
FRONT/DEV/public/locales/en/common.json
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
{
|
||||||
|
"userScanner-title": "Subnet Node Scanning",
|
||||||
|
"userScanner-visible": "Display Method",
|
||||||
|
"userScanner-loading": "Scanning nodes",
|
||||||
|
"userScanner-refresh": "Refresh List",
|
||||||
|
"userScanner-scan": "Scan Nodes",
|
||||||
|
"userScanner-notFound": "Nodes not found",
|
||||||
|
"tableScanner-online": "Online",
|
||||||
|
"tableScanner-name": "Name",
|
||||||
|
"tableScanner-vendor": "Vendor",
|
||||||
|
"tableScanner-ip": "IP Address",
|
||||||
|
"tableScanner-ports": "Open Ports",
|
||||||
|
"tableScanner-activity": "Last Activity",
|
||||||
|
"tableScanner-note": "Note",
|
||||||
|
"tableScanner-action": "Action",
|
||||||
|
"tableScanner-unknown": "Unknown",
|
||||||
|
"tableScanner-noData": "No data to display",
|
||||||
|
"listUsers-activeNodes": "Active Nodes",
|
||||||
|
"listUsers-inactiveNodes": "Inactive Nodes",
|
||||||
|
"ipInput-placeholder": "Enter IP",
|
||||||
|
"ipInput-error": "Enter a valid IP address",
|
||||||
|
"userScanner-visible_table": "Table",
|
||||||
|
"userScanner-visible_list": "List",
|
||||||
|
|
||||||
|
"wifiScanner-title": "Wi-Fi Access Point Scanning",
|
||||||
|
"wifiScanner-scanning": "Scanning access points",
|
||||||
|
"wifiScanner-scanError": "Network scanning error",
|
||||||
|
"wifiScanner-refresh": "Refresh List",
|
||||||
|
"wifiScanner-scan": "Scan Access Points",
|
||||||
|
"wifiModal-bssid": "BSSID",
|
||||||
|
"wifiModal-vendor": "Vendor",
|
||||||
|
"wifiModal-signal": "Signal",
|
||||||
|
"wifiModal-channel": "Channel",
|
||||||
|
"wifiModal-encryption": "Encryption",
|
||||||
|
"wifiModal-frequency": "Frequency",
|
||||||
|
"wifiModal-hidden": "Hidden Network",
|
||||||
|
"wifiModal-note": "Note",
|
||||||
|
"wifiModal-connect": "Connect",
|
||||||
|
"wifiModal-disconnect": "Disconnect",
|
||||||
|
"wifiModal-unknown": "Unknown",
|
||||||
|
"wifiModal-none": "None",
|
||||||
|
"common-yes": "Yes",
|
||||||
|
"common-no": "No",
|
||||||
|
|
||||||
|
"addNode": "Add Node",
|
||||||
|
"enterNodeAddress": "Enter node address...",
|
||||||
|
"connectionLost": "Connection Lost",
|
||||||
|
"serverDisconnected": "Server connection interrupted",
|
||||||
|
"checkAddressAndRetry": "Please check the address availability and try again",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"close": "Close",
|
||||||
|
"save": "Save",
|
||||||
|
"retry": "Retry",
|
||||||
|
"connect": "Connect",
|
||||||
|
"submit": "Submit",
|
||||||
|
"common-back": "Back",
|
||||||
|
"error": "Error",
|
||||||
|
"networkConnection": "Network Connection",
|
||||||
|
"enterPassword": "Enter password...",
|
||||||
|
"connectingAttempt": "Attempting to connect to the network",
|
||||||
|
"connectionError": "Network connection error",
|
||||||
|
"connectionErrorLog": "Connection error: ",
|
||||||
|
"customizeTable-title": "Subnet Node Table Customization",
|
||||||
|
|
||||||
|
"wifiDisconnect-title": "Wi-Fi Network Disconnection",
|
||||||
|
"wifiDisconnect-confirm": "Confirm Disconnection",
|
||||||
|
"wifiDisconnect-message1": "You are about to disconnect from the network - ",
|
||||||
|
"wifiDisconnect-message2": " All active connections will be interrupted.",
|
||||||
|
"wifiDisconnect-disconnect": "Disconnect",
|
||||||
|
"wifiDisconnect-progress": "Disconnecting from the network",
|
||||||
|
|
||||||
|
"ipChange-title": "IP Address Change",
|
||||||
|
"ipChange-checkingIp": "Checking IP address validity",
|
||||||
|
"ipChange-generalInfo": "General Information",
|
||||||
|
"ipChange-interface": "Interface",
|
||||||
|
"ipChange-ipInfo": "IP Information",
|
||||||
|
"ipChange-currentIp": "Current IP",
|
||||||
|
"ipChange-currentGateway": "Current Gateway",
|
||||||
|
"ipChange-currentDns": "Current DNS",
|
||||||
|
"ipChange-newIp": "New IP",
|
||||||
|
"ipChange-newGateway": "New Gateway",
|
||||||
|
"ipChange-newDns": "New DNS",
|
||||||
|
"ipChange-checkError": "An error occurred while checking validity",
|
||||||
|
"ipChange-adminRequired": "To change network settings, you must log in as an administrator",
|
||||||
|
|
||||||
|
"note-edit": "Edit Note",
|
||||||
|
"note-add": "Add Note",
|
||||||
|
"note-enter": "Enter note...",
|
||||||
|
|
||||||
|
"auth-successLogin": "Successful Login",
|
||||||
|
"auth-successLogout": "Successful Logout",
|
||||||
|
"auth-getAccess": "Obtaining Access Rights",
|
||||||
|
"auth-enableAdmin": "Enable Administrative Access",
|
||||||
|
"auth-enterAdminPass": "Enter administrator password...",
|
||||||
|
"auth-wrongPass": "Incorrect operator password",
|
||||||
|
"auth-switchToLimited": "Switch to Limited Access",
|
||||||
|
"auth-limitedModeDesc": "In this mode, administrator privileges are restricted.",
|
||||||
|
"auth-restrictAccess": "Restrict Access",
|
||||||
|
"auth-closeHint": "This window can be closed, or it will disappear on its own",
|
||||||
|
|
||||||
|
"operatorConfig-title": "Node Operator Configuration",
|
||||||
|
"operatorConfig-instruction": "Before starting, you need to create an application operator password",
|
||||||
|
"operatorConfig-passwordPlaceholder": "Enter password...",
|
||||||
|
"operatorConfig-repeatPasswordPlaceholder": "Repeat password...",
|
||||||
|
"operatorConfig-passwordMismatch": "Passwords do not match!",
|
||||||
|
"operatorConfig-submit": "Submit",
|
||||||
|
|
||||||
|
"about-logoAlt": "Gossys Logo",
|
||||||
|
"about-title": "About the Application",
|
||||||
|
"about-description": "GoNets is a minimally viable cross-platform product (MVP) for demonstrating our approach to managing PC settings in a corporate environment.",
|
||||||
|
"about-featuresTitle": "Features",
|
||||||
|
"about-feature1": "Visualization of active network resources",
|
||||||
|
"about-feature2": "Configuration of local and remote network resource parameters",
|
||||||
|
"about-feature3": "Management of resource interaction with the GoNets Client",
|
||||||
|
"about-disclaimer": "The software is for demonstration and home use only.",
|
||||||
|
"about-goToPro": "Go to GoNets-PRO",
|
||||||
|
"about-homepage": "Homepage",
|
||||||
|
"about-documentation": "Documentation",
|
||||||
|
"about-license": "License",
|
||||||
|
"about-copyright": "Developed and maintained by Hybrid Office Systems LLC, 2024-2025. All rights reserved.",
|
||||||
|
|
||||||
|
"main-logoAlt": "Application Logo",
|
||||||
|
"main-networkSettings": "Network Settings",
|
||||||
|
"main-about": "About the Program",
|
||||||
|
|
||||||
|
"nodes-availableNodes": "Available Nodes",
|
||||||
|
"nodes-rootAddress": "Root Address",
|
||||||
|
"nodes-name": "Name",
|
||||||
|
"nodes-loading": "Loading",
|
||||||
|
"nodes-additionalNodes": "Additional Nodes",
|
||||||
|
"nodes-node": "Node",
|
||||||
|
"nodes-address": "Address",
|
||||||
|
"nodes-connectionError": "Failed to connect to the server at the address ",
|
||||||
|
|
||||||
|
"settings-nodesSection": "Subnet Nodes",
|
||||||
|
"settings-intervalScanning": "Interval Scanning",
|
||||||
|
"settings-scanInterval": "Node Scan Interval (min)",
|
||||||
|
"settings-displayNames": "Display Names When Missing",
|
||||||
|
"settings-ipAddress": "IP Address",
|
||||||
|
"settings-unknown": "Unknown",
|
||||||
|
"settings-rowsPerPage": "Rows Per Table",
|
||||||
|
"settings-displaySettings": "Nodes Display",
|
||||||
|
"settings-configure": "Configure",
|
||||||
|
"settings-clearSessionData": "Clear Session Data",
|
||||||
|
"settings-clear": "Clear",
|
||||||
|
"settings-clearFailed": "Data was not deleted: ",
|
||||||
|
"settings-exportData": "Export Nodes Data",
|
||||||
|
"settings-export": "Export",
|
||||||
|
"settings-saveNodesTitle": "Save Nodes Data",
|
||||||
|
"settings-exportSuccess": "Data exported successfully!",
|
||||||
|
"settings-exportError": "Export error: ",
|
||||||
|
"settings-status": "Online",
|
||||||
|
"settings-name": "Name",
|
||||||
|
"settings-vendor": "Manufacturer",
|
||||||
|
"settings-openPorts": "Open Ports",
|
||||||
|
"settings-lastActivity": "Last Activity",
|
||||||
|
"settings-note": "Note",
|
||||||
|
"settings-action": "Action",
|
||||||
|
|
||||||
|
"settings-title": "Settings",
|
||||||
|
"settings-resetSettings": "Reset Settings",
|
||||||
|
"settings-resetConfirm": "Are you sure you want to reset all settings to default values?",
|
||||||
|
"settings-cancel": "Cancel",
|
||||||
|
"settings-save": "Save",
|
||||||
|
"settings-fixErrors": "Fix errors before saving",
|
||||||
|
"settings-fillRequired": "Fill in all required fields",
|
||||||
|
"settings-dataNotDeleted": "Data was not deleted: ",
|
||||||
|
"settings-saveNetworksTitle": "Save Networks Data",
|
||||||
|
"settings-fieldRequired": "Field cannot be empty",
|
||||||
|
"settings-enterNumber": "Please enter a number",
|
||||||
|
"settings-valueTooSmall": "Value cannot be less than ",
|
||||||
|
"settings-valueTooLarge": "Value cannot be greater than ",
|
||||||
|
|
||||||
|
"settings-sections-subnetNodes": "Subnet Nodes",
|
||||||
|
"settings-sections-accessPoints": "Access Points",
|
||||||
|
"settings-sections-appearance": "Appearance",
|
||||||
|
|
||||||
|
"settings-exportNetworkData": "Export Access Points Data",
|
||||||
|
"settings-colorTheme": "Color Theme",
|
||||||
|
"settings-light": "Light",
|
||||||
|
"settings-dark": "Dark",
|
||||||
|
"settings-system": "As in System",
|
||||||
|
"settings-interfaceLanguage": "Interface Language",
|
||||||
|
"settings-russian": "Russian",
|
||||||
|
"settings-english": "English"
|
||||||
|
}
|
||||||
192
FRONT/DEV/public/locales/ru/common.json
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
{
|
||||||
|
"userScanner-title": "Сканирование узлов подсети",
|
||||||
|
"userScanner-visible": "Способ отображения",
|
||||||
|
"userScanner-loading": "Cканирование узлов",
|
||||||
|
"userScanner-refresh": "Обновить список",
|
||||||
|
"userScanner-scan": "Сканировать узлы",
|
||||||
|
"userScanner-notFound": "Узлы не найдены",
|
||||||
|
"tableScanner-online": "Онлайн",
|
||||||
|
"tableScanner-name": "Имя",
|
||||||
|
"tableScanner-vendor": "Производитель",
|
||||||
|
"tableScanner-ip": "IP адрес",
|
||||||
|
"tableScanner-ports": "Открытые порты",
|
||||||
|
"tableScanner-activity": "Последняя активность",
|
||||||
|
"tableScanner-note": "Заметка",
|
||||||
|
"tableScanner-action": "Действие",
|
||||||
|
"tableScanner-unknown": "Неизвестно",
|
||||||
|
"tableScanner-noData": "Нет данных для отображения",
|
||||||
|
"listUsers-activeNodes": "Активные узлы",
|
||||||
|
"listUsers-inactiveNodes": "Неактивные узлы",
|
||||||
|
"ipInput-placeholder": "Введите IP",
|
||||||
|
"ipInput-error": "Введите корректный IP-адрес",
|
||||||
|
"userScanner-visible_table": "Таблица",
|
||||||
|
"userScanner-visible_list": "Список",
|
||||||
|
|
||||||
|
"wifiScanner-title": "Сканирование точек доступа Wi-Fi",
|
||||||
|
"wifiScanner-scanning": "Сканирование точек доступа",
|
||||||
|
"wifiScanner-scanError": "Ошибка сканирования сети",
|
||||||
|
"wifiScanner-refresh": "Обновить список",
|
||||||
|
"wifiScanner-scan": "Сканировать точки доступа",
|
||||||
|
"wifiModal-bssid": "BSSID",
|
||||||
|
"wifiModal-vendor": "Производитель",
|
||||||
|
"wifiModal-signal": "Сигнал",
|
||||||
|
"wifiModal-channel": "Канал",
|
||||||
|
"wifiModal-encryption": "Шифрование",
|
||||||
|
"wifiModal-frequency": "Частота",
|
||||||
|
"wifiModal-hidden": "Скрытая сеть",
|
||||||
|
"wifiModal-note": "Заметка",
|
||||||
|
"wifiModal-connect": "Подключиться",
|
||||||
|
"wifiModal-disconnect": "Отключиться",
|
||||||
|
"wifiModal-unknown": "Неизвестно",
|
||||||
|
"wifiModal-none": "Отсутствует",
|
||||||
|
"common-yes": "Да",
|
||||||
|
"common-no": "Нет",
|
||||||
|
|
||||||
|
"addNode": "Добавление узла",
|
||||||
|
"enterNodeAddress": "Введите адрес узла...",
|
||||||
|
"connectionLost": "Соединение прервано",
|
||||||
|
"serverDisconnected": "Произошёл разрыв с сервером",
|
||||||
|
"checkAddressAndRetry": "Пожалуйста, проверьте доступ к адресу и попробуйте снова",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"save": "Сохранить",
|
||||||
|
"retry": "Повторить",
|
||||||
|
"connect": "Подключиться",
|
||||||
|
"common-back": "Назад",
|
||||||
|
"submit": "Подтвердить",
|
||||||
|
"error": "Ошибка",
|
||||||
|
"networkConnection": "Подключение к сети",
|
||||||
|
"enterPassword": "Введите пароль...",
|
||||||
|
"connectingAttempt": "Попытка подключения к сети",
|
||||||
|
"connectionError": "Ошибка подключения к сети",
|
||||||
|
"connectionErrorLog": "Ошибка подключения: ",
|
||||||
|
"customizeTable-title": "Кастомизация таблицы узлов подсети",
|
||||||
|
|
||||||
|
"wifiDisconnect-title": "Отключение от Wi-Fi сети",
|
||||||
|
"wifiDisconnect-confirm": "Подтверждение отключения",
|
||||||
|
"wifiDisconnect-message1": "Вы собираетесь отключиться от сети - ",
|
||||||
|
"wifiDisconnect-message2": " Все активные подключения будут прерваны.",
|
||||||
|
"wifiDisconnect-disconnect": "Отключиться",
|
||||||
|
"wifiDisconnect-progress": "Отключение от сети",
|
||||||
|
|
||||||
|
"ipChange-title": "Изменение IP-Адреса",
|
||||||
|
"ipChange-checkingIp": "Идет проверка корректности IP адреса",
|
||||||
|
"ipChange-generalInfo": "Общие сведения",
|
||||||
|
"ipChange-interface": "Интерфейс",
|
||||||
|
"ipChange-ipInfo": "Сведения об IP",
|
||||||
|
"ipChange-currentIp": "Текущий ip",
|
||||||
|
"ipChange-currentGateway": "Текущий шлюз",
|
||||||
|
"ipChange-currentDns": "Текущий DNS",
|
||||||
|
"ipChange-newIp": "Новый ip",
|
||||||
|
"ipChange-newGateway": "Новый шлюз",
|
||||||
|
"ipChange-newDns": "Новый DNS",
|
||||||
|
"ipChange-checkError": "Произошла ошибка проверки корректности",
|
||||||
|
"ipChange-adminRequired": "Для изменения сетевых настроек необходимо авторизироваться под администратором",
|
||||||
|
|
||||||
|
"note-edit": "Редактирование заметки",
|
||||||
|
"note-add": "Добавление заметки",
|
||||||
|
"note-enter": "Введите заметку...",
|
||||||
|
|
||||||
|
"auth-successLogin": "Успешный вход",
|
||||||
|
"auth-successLogout": "Успешный выход",
|
||||||
|
"auth-getAccess": "Получение прав доступа",
|
||||||
|
"auth-enableAdmin": "Включить административный доступ",
|
||||||
|
"auth-enterAdminPass": "Введите пароль администратора...",
|
||||||
|
"auth-wrongPass": "Неверный пароль оператора",
|
||||||
|
"auth-switchToLimited": "Переключиться на ограниченный доступ",
|
||||||
|
"auth-limitedModeDesc": "В данном режиме доступа ограничиваются полномочия администратора.",
|
||||||
|
"auth-restrictAccess": "Ограничить доступ",
|
||||||
|
"auth-closeHint": "Это окно можно закрыть, либо оно пропадёт само",
|
||||||
|
|
||||||
|
"operatorConfig-title": "Настройка оператора узла",
|
||||||
|
"operatorConfig-instruction": "Перед началом работы необходимо создать пароль оператора приложения",
|
||||||
|
"operatorConfig-passwordPlaceholder": "Введите пароль...",
|
||||||
|
"operatorConfig-repeatPasswordPlaceholder": "Повторите пароль...",
|
||||||
|
"operatorConfig-passwordMismatch": "Пароли не совпадают!",
|
||||||
|
"operatorConfig-submit": "Отправить",
|
||||||
|
|
||||||
|
"about-logoAlt": "Логотип Gossys",
|
||||||
|
"about-title": "О приложении",
|
||||||
|
"about-description": "ГоNets - минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.",
|
||||||
|
"about-featuresTitle": "Возможности",
|
||||||
|
"about-feature1": "Визуализация активных сетевых ресурсов",
|
||||||
|
"about-feature2": "Настройка параметров локальных и удалённых сетевых ресурсов",
|
||||||
|
"about-feature3": "Управление взаимодействием ресурсов с Клиентом ГoNets",
|
||||||
|
"about-disclaimer": "Программное обеспечение исключительно для ознакомления и домашнего использования.",
|
||||||
|
"about-goToPro": "Перейти на ГоNets-PRO",
|
||||||
|
"about-homepage": "Домашняя страница",
|
||||||
|
"about-documentation": "Документация",
|
||||||
|
"about-license": "Лицензия",
|
||||||
|
"about-copyright": "Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.",
|
||||||
|
|
||||||
|
"main-logoAlt": "Логотип приложения",
|
||||||
|
"main-networkSettings": "Настройки сети",
|
||||||
|
"main-about": "О программе",
|
||||||
|
|
||||||
|
"nodes-availableNodes": "Доступные узлы",
|
||||||
|
"nodes-rootAddress": "Корневой адрес",
|
||||||
|
"nodes-name": "Имя",
|
||||||
|
"nodes-loading": "Загрузка",
|
||||||
|
"nodes-additionalNodes": "Дополнительные узлы",
|
||||||
|
"nodes-node": "Узел",
|
||||||
|
"nodes-address": "Адрес",
|
||||||
|
"nodes-connectionError": "Не удалось подключиться к серверу по адресу ",
|
||||||
|
|
||||||
|
"settings-nodesSection": "Узлы подсети",
|
||||||
|
"settings-intervalScanning": "Интервальное сканирование",
|
||||||
|
"settings-scanInterval": "Интервал сканирования узлов (мин)",
|
||||||
|
"settings-displayNames": "Отображаемые имена при отсутствии",
|
||||||
|
"settings-ipAddress": "IP адрес",
|
||||||
|
"settings-unknown": "Неизвестно",
|
||||||
|
"settings-rowsPerPage": "Количество строк в таблице",
|
||||||
|
"settings-displaySettings": "Отображение узлов",
|
||||||
|
"settings-configure": "Настроить",
|
||||||
|
"settings-clearSessionData": "Очистить данные сессии",
|
||||||
|
"settings-clear": "Очистить",
|
||||||
|
"settings-clearFailed": "Данные не были удалены: ",
|
||||||
|
"settings-exportData": "Экспорт данных узлов",
|
||||||
|
"settings-export": "Экспортировать",
|
||||||
|
"settings-saveNodesTitle": "Сохранить данные узлов",
|
||||||
|
"settings-exportSuccess": "Данные успешно экспортированы!",
|
||||||
|
"settings-exportError": "Ошибка экспорта: ",
|
||||||
|
"settings-status": "Онлайн",
|
||||||
|
"settings-name": "Имя",
|
||||||
|
"settings-vendor": "Производитель",
|
||||||
|
"settings-openPorts": "Открытые порты",
|
||||||
|
"settings-lastActivity": "Последняя активность",
|
||||||
|
"settings-note": "Заметка",
|
||||||
|
"settings-action": "Действие",
|
||||||
|
|
||||||
|
"settings-title": "Настройки",
|
||||||
|
"settings-resetSettings": "Сбросить настройки",
|
||||||
|
"settings-resetConfirm": "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?",
|
||||||
|
"settings-cancel": "Отменить",
|
||||||
|
"settings-save": "Сохранить",
|
||||||
|
"settings-fixErrors": "Исправьте ошибки перед сохранением",
|
||||||
|
"settings-fillRequired": "Заполните все обязательные поля",
|
||||||
|
"settings-dataNotDeleted": "Данные не были удалены: ",
|
||||||
|
"settings-saveNetworksTitle": "Сохранить данные сетей",
|
||||||
|
"settings-fieldRequired": "Поле не может быть пустым",
|
||||||
|
"settings-enterNumber": "Пожалуйста, введите число",
|
||||||
|
"settings-valueTooSmall": "Значение не может быть меньше ",
|
||||||
|
"settings-valueTooLarge": "Значение не может быть больше ",
|
||||||
|
|
||||||
|
"settings-sections-subnetNodes": "Узлы подсети",
|
||||||
|
"settings-sections-accessPoints": "Точки доступа",
|
||||||
|
"settings-sections-appearance": "Внешний вид",
|
||||||
|
|
||||||
|
"settings-exportNetworkData": "Экспорт данных точек доступа",
|
||||||
|
"settings-colorTheme": "Цветовая тема",
|
||||||
|
"settings-light": "Светлая",
|
||||||
|
"settings-dark": "Тёмная",
|
||||||
|
"settings-system": "Как в системе",
|
||||||
|
"settings-interfaceLanguage": "Язык интерфейса",
|
||||||
|
"settings-russian": "Русский",
|
||||||
|
"settings-english": "Английский"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
|
||||||
const axiosWithoutCredentials = axios.create({
|
const axiosWithoutCredentials = axios.create({
|
||||||
baseURL: `https://localhost:8001`,
|
baseURL: `http://localhost:8001`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export class NetworkService {
|
export class NetworkService {
|
||||||
static createAxiosInstance(ipAddress: string) {
|
static createAxiosInstance(ipAddress: string) {
|
||||||
return axios.create({
|
return axios.create({
|
||||||
baseURL: `https://${ipAddress}:8001`,
|
baseURL: `http://${ipAddress}:8001`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,10 +83,12 @@ export class NetworkService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
|
static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
|
||||||
try {
|
try {
|
||||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
return await axiosInstance.get('/subnet/devices');
|
return await axiosInstance.post('/subnet/devices',{
|
||||||
|
subnet: ipScanning,
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return error.response;
|
return error.response;
|
||||||
}
|
}
|
||||||
|
|
@ -157,4 +159,38 @@ export class NetworkService {
|
||||||
return error.response;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import {buttonNames} from "../../../const/buttons/names";
|
||||||
import {ModalMap} from "../../modalwindows/windowsType";
|
import {ModalMap} from "../../modalwindows/windowsType";
|
||||||
import {IconComponent} from "../../../const/buttons/icons";
|
import {IconComponent} from "../../../const/buttons/icons";
|
||||||
import {useModalContext} from "../../../store";
|
import {useModalContext} from "../../../store";
|
||||||
|
import {useNameSetting} from "../../../hooks/useSettings";
|
||||||
|
|
||||||
interface DeviceCardProps {
|
interface DeviceCardProps {
|
||||||
device: DeviceUser;
|
device: DeviceUser;
|
||||||
|
|
@ -74,8 +75,8 @@ const Value = styled.span`
|
||||||
|
|
||||||
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const toggleExpanded = () => setIsExpanded(!isExpanded);
|
|
||||||
const { openModal } = useModalContext();
|
const { openModal } = useModalContext();
|
||||||
|
const nameSetting = useNameSetting();
|
||||||
|
|
||||||
const openModalNote = (e: React.MouseEvent) => {
|
const openModalNote = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -87,7 +88,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardWrapper status={status} onClick={toggleExpanded}>
|
<CardWrapper status={status} onClick={()=>setIsExpanded(!isExpanded)}>
|
||||||
<CardHeader status={status}>
|
<CardHeader status={status}>
|
||||||
<p>
|
<p>
|
||||||
MAC: {device.mac}
|
MAC: {device.mac}
|
||||||
|
|
@ -100,7 +101,7 @@ const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
||||||
<CardContent isExpanded={isExpanded}>
|
<CardContent isExpanded={isExpanded}>
|
||||||
<InfoRow>
|
<InfoRow>
|
||||||
<Label>Имя:</Label>
|
<Label>Имя:</Label>
|
||||||
<Value>{device.hostname || device.ip}</Value>
|
<Value>{device.hostname || (nameSetting === 'ip' ? device.ip : 'Неизвестно')}</Value>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
|
|
||||||
<InfoRow>
|
<InfoRow>
|
||||||
77
FRONT/DEV/src/components/UI/Pagination.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from "styled-components";
|
||||||
|
import {buttonNames} from "../../const/buttons/names";
|
||||||
|
import {IconComponent} from "../../const/buttons/icons";
|
||||||
|
|
||||||
|
const PaginateWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
const Page = styled.button<{ active?: boolean }>`
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: ${(props) => props.active ? props.theme.borderColor : props.theme.primary};
|
||||||
|
color: ${(props) => props.active ? props.theme.primary : props.theme.textColor};
|
||||||
|
border: 1px solid ${(props => props.theme.borderColor)};
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
transition: .4s ease;
|
||||||
|
&:hover {
|
||||||
|
background: ${(props) => props.active ? props.theme.textColor : props.theme.background};;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPrevPage: () => void;
|
||||||
|
onNextPage: () => void;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination = ({currentPage, totalPages, onPrevPage, onNextPage, onPageChange}: PaginationProps) => {
|
||||||
|
|
||||||
|
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaginateWrapper>
|
||||||
|
<Page
|
||||||
|
onClick={onPrevPage}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
name={buttonNames[17]}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
{pageNumbers.map((page, i) => (
|
||||||
|
<Page
|
||||||
|
key={i}
|
||||||
|
onClick={() => onPageChange(page)}
|
||||||
|
active={currentPage === page}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Page>
|
||||||
|
))}
|
||||||
|
<Page
|
||||||
|
onClick={onNextPage}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
name={buttonNames[6]}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
</PaginateWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
||||||
|
|
@ -17,10 +17,10 @@ const Button = styled.button.withConfig({
|
||||||
color: white;
|
color: white;
|
||||||
font-size: ${(props) =>
|
font-size: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "14px"
|
? "16px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "16px"
|
? "18px"
|
||||||
: "14px"}; // По умолчанию
|
: "16px"}; // По умолчанию
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -3,12 +3,13 @@ import { Handle, Position } from '@xyflow/react';
|
||||||
import { Ip } from './style';
|
import { Ip } from './style';
|
||||||
import { Row, IpTitle, IpContent } from './style';
|
import { Row, IpTitle, IpContent } from './style';
|
||||||
import Input from '../../../../UI/inputs/input';
|
import Input from '../../../../UI/inputs/input';
|
||||||
import { useModalContext, usePannableStore } from '../../../../../store';
|
import {useIpStore, useModalContext, usePannableStore} from '../../../../../store';
|
||||||
import { ModalMap } from '../../../../modalwindows/windowsType';
|
import { ModalMap } from '../../../../modalwindows/windowsType';
|
||||||
import { validateIPAddress, validateSameSubnet } from '../../../../../utils/nodeController/validator';
|
import { validateIPAddress, validateSameSubnet } from '../../../../../utils/nodeController/validator';
|
||||||
import {changeIp} from '../../../../../const/types/types'
|
import {changeIp} from '../../../../../const/types/types'
|
||||||
import { IconComponent } from "../../../../../const/buttons/icons";
|
import { IconComponent } from "../../../../../const/buttons/icons";
|
||||||
import { buttonNames } from "../../../../../const/buttons/names";
|
import { buttonNames } from "../../../../../const/buttons/names";
|
||||||
|
import {NetworkService} from "../../../../../api/request";
|
||||||
|
|
||||||
interface IIp {
|
interface IIp {
|
||||||
ipType: string
|
ipType: string
|
||||||
|
|
@ -45,6 +46,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
const { toggleEditingMode } = usePannableStore();
|
const { toggleEditingMode } = usePannableStore();
|
||||||
const [isInput, setIsInput] = useState(false)
|
const [isInput, setIsInput] = useState(false)
|
||||||
const { openModal } = useModalContext();
|
const { openModal } = useModalContext();
|
||||||
|
const {ipAddress} = useIpStore();
|
||||||
|
|
||||||
const [ipData, setIpData] = useState<changeIp>(
|
const [ipData, setIpData] = useState<changeIp>(
|
||||||
{
|
{
|
||||||
|
|
@ -146,7 +148,20 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
|
|
||||||
setIsInput(false)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -170,7 +185,10 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<IconComponent name={buttonNames[7]} onClick={handleClick} width='18' height='18' setColor='white'/>
|
<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>
|
</IpTitle>
|
||||||
|
|
@ -8,6 +8,7 @@ import useNetworkStore from "../../../store/useDeviceStore";
|
||||||
import RouterLoader from "../../UI/routerLoader";
|
import RouterLoader from "../../UI/routerLoader";
|
||||||
import {useTheme} from "styled-components";
|
import {useTheme} from "styled-components";
|
||||||
import {useIpStore} from "../../../store";
|
import {useIpStore} from "../../../store";
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
address: string;
|
address: string;
|
||||||
|
|
@ -19,53 +20,55 @@ interface Props {
|
||||||
export const ConnectNetwork = ({address, onClose}: Props) => {
|
export const ConnectNetwork = ({address, onClose}: Props) => {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { ipAddress } = useIpStore();
|
const {ipAddress} = useIpStore();
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError('')
|
setError('');
|
||||||
const res = await NetworkService.connectNetwork(ipAddress, password, address);
|
const res = await NetworkService.connectNetwork(ipAddress, password, address);
|
||||||
if(res.status === 200) {
|
if (res.status === 200) {
|
||||||
onClose()
|
onClose();
|
||||||
}else{
|
} else {
|
||||||
setError(res.data.detail)
|
setError(res.data.detail);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connection:', error);
|
console.error(t('connectionErrorLog'), error);
|
||||||
setError('Ошибка подключения к сети')
|
setError(t('connectionError'));
|
||||||
}finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContainer style={{ height: '230px' }}>
|
<ModalContainer style={{height: '230px'}}>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Подключение к сети</h3>
|
<h3>{t('networkConnection')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholderText='Введите пароль...'
|
placeholderText={t('enterPassword')}
|
||||||
value={password}
|
value={password}
|
||||||
id='deviceNote'
|
id='deviceNote'
|
||||||
onChange={(value) => setPassword(value)}
|
onChange={(value) => setPassword(value)}
|
||||||
/>
|
/>
|
||||||
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
{error && (
|
||||||
{error}
|
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
|
||||||
</p>
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton
|
<MainButton
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
title="Отмена"
|
title={t('cancel')}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
<MainButton
|
<MainButton
|
||||||
|
|
@ -73,12 +76,12 @@ export const ConnectNetwork = ({address, onClose}: Props) => {
|
||||||
size="small"
|
size="small"
|
||||||
color={theme.successColor}
|
color={theme.successColor}
|
||||||
hoverColor={theme.successColor}
|
hoverColor={theme.successColor}
|
||||||
title='Подключиться'
|
title={t('connect')}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
/>
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|
||||||
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
|
{isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// components/modalContainers/NoteDevice.tsx
|
|
||||||
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
|
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
|
||||||
import { MainButton } from "../../UI/buttons/mainButton";
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
import Input from "../../UI/inputs/input";
|
import Input from "../../UI/inputs/input";
|
||||||
|
|
@ -6,6 +5,7 @@ import { useState } from "react";
|
||||||
import {useIpStore} from "../../../store";
|
import {useIpStore} from "../../../store";
|
||||||
import {NetworkService} from "../../../api/request";
|
import {NetworkService} from "../../../api/request";
|
||||||
import useNetworkStore from "../../../store/useDeviceStore";
|
import useNetworkStore from "../../../store/useDeviceStore";
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
address: string;
|
address: string;
|
||||||
|
|
@ -20,6 +20,7 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
const [note, setNote] = useState(currentNote || '');
|
const [note, setNote] = useState(currentNote || '');
|
||||||
const { ipAddress } = useIpStore();
|
const { ipAddress } = useIpStore();
|
||||||
const { devices, setDevices } = useNetworkStore();
|
const { devices, setDevices } = useNetworkStore();
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -50,13 +51,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
return (
|
return (
|
||||||
<ModalContainer style={{ height: '250px' }}>
|
<ModalContainer style={{ height: '250px' }}>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
|
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholderText='Введите заметку...'
|
placeholderText={t('note-enter')}
|
||||||
value={note}
|
value={note}
|
||||||
id='deviceNote'
|
id='deviceNote'
|
||||||
onChange={(value) => setNote(value)}
|
onChange={(value) => setNote(value)}
|
||||||
|
|
@ -67,13 +68,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
<MainButton
|
<MainButton
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
title="Отмена"
|
title={t('cancel')}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
<MainButton
|
<MainButton
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
title='Сохранить'
|
title={t('save')}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
/>
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|
@ -6,6 +6,7 @@ import { useState } from "react";
|
||||||
import {NetworkService} from "../../../api/request";
|
import {NetworkService} from "../../../api/request";
|
||||||
import useNetworkStore from "../../../store/useDeviceStore";
|
import useNetworkStore from "../../../store/useDeviceStore";
|
||||||
import {useIpStore} from "../../../store";
|
import {useIpStore} from "../../../store";
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
address: string;
|
address: string;
|
||||||
|
|
@ -20,6 +21,7 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
const [note, setNote] = useState(currentNote || '');
|
const [note, setNote] = useState(currentNote || '');
|
||||||
const { ipAddress } = useIpStore();
|
const { ipAddress } = useIpStore();
|
||||||
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -50,13 +52,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
return (
|
return (
|
||||||
<ModalContainer style={{ height: '220px' }}>
|
<ModalContainer style={{ height: '220px' }}>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
|
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholderText='Введите заметку...'
|
placeholderText={t('note-enter')}
|
||||||
value={note}
|
value={note}
|
||||||
id='deviceNote'
|
id='deviceNote'
|
||||||
onChange={(value) => setNote(value)}
|
onChange={(value) => setNote(value)}
|
||||||
|
|
@ -67,13 +69,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
<MainButton
|
<MainButton
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
title="Отмена"
|
title={t('cancel')}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
<MainButton
|
<MainButton
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
title='Сохранить'
|
title={t('save')}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
/>
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|
@ -3,6 +3,7 @@ import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
||||||
import { validateIPAddress } from "../../../utils/nodeController/validator";
|
import { validateIPAddress } from "../../../utils/nodeController/validator";
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSave?: (data: string) => void;
|
onSave?: (data: string) => void;
|
||||||
|
|
@ -12,6 +13,7 @@ interface Props {
|
||||||
export const AddDevice = ({ onSave, onClose }: Props) => {
|
export const AddDevice = ({ onSave, onClose }: Props) => {
|
||||||
const [ipAddress, setIpAddress] = useState('')
|
const [ipAddress, setIpAddress] = useState('')
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!validateIPAddress(ipAddress)) {
|
if (!validateIPAddress(ipAddress)) {
|
||||||
|
|
@ -28,31 +30,39 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContainer style={{ height: '230px' }}>
|
<ModalContainer style={{ height: '230px' }}>
|
||||||
{/* <form onSubmit={handleSubmit}> */}
|
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Добавление узла</h3>
|
<h3>{t('addNode')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholderText='Введите адрес узла...'
|
placeholderText={t('enterNodeAddress')}
|
||||||
id="newIp"
|
id="newIp"
|
||||||
value={ipAddress}
|
value={ipAddress}
|
||||||
onChange={(value) => setIpAddress(value)}
|
onChange={(value) => setIpAddress(value)}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p style={{ color: 'red', marginTop: '5px' }}>
|
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||||
Некорректный адрес узла
|
{t('invalidAddress')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton type="button" size="small" title="Отмена" onClick={onClose} />
|
<MainButton
|
||||||
<MainButton type="submit" size="small" title="Сохранить" onClick={handleSubmit} />
|
type="button"
|
||||||
|
size="small"
|
||||||
|
title={t('cancel')}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
type="submit"
|
||||||
|
size="small"
|
||||||
|
title={t('save')}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
{/* </form> */}
|
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer }
|
||||||
import { useModalContext } from "../../../store"
|
import { useModalContext } from "../../../store"
|
||||||
import { useIpStore } from "../../../store"
|
import { useIpStore } from "../../../store"
|
||||||
import { MainButton } from "../../UI/buttons/mainButton"
|
import { MainButton } from "../../UI/buttons/mainButton"
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
type Props={
|
type Props={
|
||||||
message: string
|
message: string
|
||||||
|
|
@ -10,28 +11,37 @@ type Props={
|
||||||
export const ConnectionError = ({message}: Props)=>{
|
export const ConnectionError = ({message}: Props)=>{
|
||||||
const {closeModal} = useModalContext()
|
const {closeModal} = useModalContext()
|
||||||
const { setIpAddress } = useIpStore();
|
const { setIpAddress } = useIpStore();
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
const close = ()=>{
|
const close = ()=>{
|
||||||
setIpAddress('localhost')
|
setIpAddress('localhost')
|
||||||
closeModal()
|
closeModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
return(
|
return (
|
||||||
<ModalContainer>
|
<ModalContainer>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Соединение прервано</h3>
|
<h3>{t('connectionLost')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalText>
|
<ModalText>
|
||||||
<h3>Произошёл разрыв с сервером</h3>
|
<h3>{t('serverDisconnected')}</h3>
|
||||||
<p>Пожалуйста, проверьте доступ к адресу и попробуйте снова.</p>
|
<p>{t('checkAddressAndRetry')}</p>
|
||||||
</ModalText>
|
</ModalText>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton title="Закрыть" onClick={close} size="small"/>
|
<MainButton
|
||||||
<MainButton title="Повторить" onClick={closeModal} size="small"/>
|
title={t('close')}
|
||||||
|
onClick={close}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title={t('retry')}
|
||||||
|
onClick={closeModal}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
)
|
)
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -6,6 +6,7 @@ import {useState} from "react";
|
||||||
import {NetworkService} from "../../../api/request";
|
import {NetworkService} from "../../../api/request";
|
||||||
import RouterLoader from "../../UI/routerLoader";
|
import RouterLoader from "../../UI/routerLoader";
|
||||||
import {useTheme} from "styled-components";
|
import {useTheme} from "styled-components";
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
type Props={
|
type Props={
|
||||||
address: string,
|
address: string,
|
||||||
|
|
@ -16,6 +17,7 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
|
||||||
const { ipAddress } = useIpStore();
|
const { ipAddress } = useIpStore();
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
const handleDisconnect = async () => {
|
const handleDisconnect = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,21 +36,32 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
|
||||||
return(
|
return(
|
||||||
<ModalContainer>
|
<ModalContainer>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Отключение от Wi-Fi сети</h3>
|
<h3>{t('wifiDisconnect-title')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalText>
|
<ModalText>
|
||||||
<h3>Подтверждение отключения</h3>
|
<h3>{t('wifiDisconnect-confirm')}</h3>
|
||||||
|
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
|
||||||
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
|
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
|
||||||
</ModalText>
|
</ModalText>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton title="Отменить" onClick={onClose} size="small"/>
|
<MainButton
|
||||||
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
|
title={t('cancel')}
|
||||||
|
onClick={onClose}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title={t('wifiDisconnect-disconnect')}
|
||||||
|
color={theme.errorColor}
|
||||||
|
hoverColor={theme.errorColor}
|
||||||
|
onClick={handleDisconnect}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
|
{isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import { useIpStore } from "../../../store";
|
||||||
import { MainButton } from "../../UI/buttons/mainButton";
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
import { useAdminStore } from "../../../store/adminStore";
|
import { useAdminStore } from "../../../store/adminStore";
|
||||||
import { useDevicesStore } from "../../../store";
|
import { useDevicesStore } from "../../../store";
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
ipData: changeIp
|
ipData: changeIp
|
||||||
|
|
@ -52,6 +53,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
const { ipAddress, setIpAddress } = useIpStore();
|
const { ipAddress, setIpAddress } = useIpStore();
|
||||||
const {isAdmin} = useAdminStore()
|
const {isAdmin} = useAdminStore()
|
||||||
const { devices, addDevice, deleteDevice } = useDevicesStore();
|
const { devices, addDevice, deleteDevice } = useDevicesStore();
|
||||||
|
const {t} = useTranslation('common');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ipData && isAdmin) {
|
if (ipData && isAdmin) {
|
||||||
|
|
@ -120,7 +122,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
);
|
);
|
||||||
useDevicesStore.setState({ devices: updatedDevices });
|
useDevicesStore.setState({ devices: updatedDevices });
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -146,40 +148,48 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loader && isAdmin ? (
|
{loader && isAdmin ? (
|
||||||
<Loader loaderText="Идет проверка корректности IP адреса" />
|
<Loader loaderText={t('ipChange-checkingIp')} />
|
||||||
) : (
|
) : (
|
||||||
<ModalContainer {...(checkStatus ? { height: "unset" } : {})}>
|
<ModalContainer {...(checkStatus ? { height: "unset" } : {})}>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Изменение IP-Адреса</h3>
|
<h3>{t('ipChange-title')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
{checkStatus ? (
|
{checkStatus ? (
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Information>
|
<Information>
|
||||||
<h3>Общие сведения</h3>
|
<h3>{t('ipChange-generalInfo')}</h3>
|
||||||
<Info>
|
<Info>
|
||||||
<p>Интерфейс: {ipData.device}</p>
|
<p>{t('ipChange-interface')}: {ipData.device}</p>
|
||||||
<p>--{'>'}</p>
|
<p>--{'>'}</p>
|
||||||
<p>{ipData.connection}</p>
|
<p>{ipData.connection}</p>
|
||||||
</Info>
|
</Info>
|
||||||
<h3>Сведения об IP</h3>
|
<h3>{t('ipChange-ipInfo')}</h3>
|
||||||
<DataContainer>
|
<DataContainer>
|
||||||
<CurrentData>
|
<CurrentData>
|
||||||
<p>Текущий ip: {ipData.currentIp}</p>
|
<p>{t('ipChange-currentIp')}: {ipData.currentIp}</p>
|
||||||
<p>Текущий шлюз: {ipData.currentGate}</p>
|
<p>{t('ipChange-currentGateway')}: {ipData.currentGate}</p>
|
||||||
<p>Текущий DNS: {ipData.currentDns}</p>
|
<p>{t('ipChange-currentDns')}: {ipData.currentDns}</p>
|
||||||
</CurrentData>
|
</CurrentData>
|
||||||
|
|
||||||
<NewData>
|
<NewData>
|
||||||
<p>Новый ip: {ipData.newIp}</p>
|
<p>{t('ipChange-newIp')}: {ipData.newIp}</p>
|
||||||
<p>Новый шлюз: {ipData.newGate}</p>
|
<p>{t('ipChange-newGateway')}: {ipData.newGate}</p>
|
||||||
<p>Новый DNS: {ipData.newDns}</p>
|
<p>{t('ipChange-newDns')}: {ipData.newDns}</p>
|
||||||
</NewData>
|
</NewData>
|
||||||
</DataContainer>
|
</DataContainer>
|
||||||
</Information>
|
</Information>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton title="Отмена" size='small' onClick={() => cancelIp(currentIp)}/>
|
<MainButton
|
||||||
<MainButton title="Сохранить" size='small' onClick={() => saveIp(newIP)}/>
|
title={t('cancel')}
|
||||||
|
size='small'
|
||||||
|
onClick={() => cancelIp(currentIp)}
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title={t('save')}
|
||||||
|
size='small'
|
||||||
|
onClick={() => saveIp(newIP)}
|
||||||
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -188,19 +198,21 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
<ModalText>
|
<ModalText>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<>
|
<>
|
||||||
<h3>Произошла ошибка проверки корректности</h3>
|
<h3>{t('ipChange-checkError')}</h3>
|
||||||
<ErrorMessage>{message}</ErrorMessage>
|
<ErrorMessage>{message}</ErrorMessage>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
):(
|
):(
|
||||||
<h3>Для изменения сетевых настроек необходимо авторизироваться под администратором</h3>
|
<h3>{t('ipChange-adminRequired')}</h3>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</ModalText>
|
</ModalText>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton title="Закрыть" size='small' onClick={smoothClose}/>
|
<MainButton
|
||||||
|
title={t('close')}
|
||||||
|
size='small'
|
||||||
|
onClick={smoothClose}
|
||||||
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -9,6 +9,7 @@ import { useCheckAdmin } from "../../../utils/checkAdmin"
|
||||||
|
|
||||||
import { buttonNames } from "../../../const/buttons/names"
|
import { buttonNames } from "../../../const/buttons/names"
|
||||||
import { IconComponent } from "../../../const/buttons/icons"
|
import { IconComponent } from "../../../const/buttons/icons"
|
||||||
|
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -17,9 +18,10 @@ interface Props {
|
||||||
export const RightsConfirm = ({ onClose }: Props) => {
|
export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
|
|
||||||
const { ipAddress } = useIpStore();
|
const { ipAddress } = useIpStore();
|
||||||
const { isAdmin, setAdmin } = useAdminStore()
|
const { isAdmin } = useAdminStore()
|
||||||
const checkAdmin = useCheckAdmin();
|
const checkAdmin = useCheckAdmin();
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
const {t} = useTranslation('common');
|
||||||
const [success, setSuccess] = useState({
|
const [success, setSuccess] = useState({
|
||||||
message: '',
|
message: '',
|
||||||
status: false
|
status: false
|
||||||
|
|
@ -45,7 +47,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
setError(false)
|
setError(false)
|
||||||
setSuccess(prevData => ({
|
setSuccess(prevData => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
message: 'Успешный вход',
|
message: t('auth-successLogin'),
|
||||||
status: true
|
status: true
|
||||||
}));
|
}));
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
@ -54,20 +56,20 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
setError(true)
|
setError(true)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error:', err);
|
console.error(t('auth-error'), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await NetworkService.logout(ipAddress);
|
await NetworkService.logout(ipAddress);
|
||||||
await checkAdmin();
|
await checkAdmin();
|
||||||
setSuccess(prevData => ({
|
setSuccess(prevData => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
message: 'Успешный выход',
|
message: t('auth-successLogout'),
|
||||||
status: true
|
status: true
|
||||||
}));
|
}));
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -76,60 +78,55 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
!isAdmin ? (
|
!isAdmin ? (
|
||||||
<>
|
<>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Получение прав доступа</h3>
|
<h3>{t('auth-getAccess')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
|
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
|
||||||
|
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>{t('auth-enableAdmin')}</h3>
|
||||||
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>Включить административный доступ</h3>
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholderText='Введите пароль администратора...'
|
placeholderText={t('auth-enterAdminPass')}
|
||||||
id="password"
|
id="password"
|
||||||
value={user.password}
|
value={user.password}
|
||||||
onChange={(value) => handleChange('password', value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
||||||
Неверный пароль оператора
|
{t('auth-wrongPass')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton size="small" title="Отмена" onClick={onClose} />
|
<MainButton size="small" title={t('cancel')} onClick={onClose} />
|
||||||
<MainButton size="small" title="Отправить" onClick={() => login(user)} />
|
<MainButton size="small" title={t('submit')} onClick={() => login(user)} />
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ModalTitle>
|
<ModalTitle>
|
||||||
<h3>Переключиться на ограниченный доступ</h3>
|
<h3>{t('auth-switchToLimited')}</h3>
|
||||||
</ModalTitle>
|
</ModalTitle>
|
||||||
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<p>В данном режиме доступа ограничиваются полномочия администратора.</p>
|
<p>{t('auth-limitedModeDesc')}</p>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton size="small" title="Отмена" onClick={onClose} />
|
<MainButton size="small" title={t('cancel')} onClick={onClose} />
|
||||||
<MainButton size="small" title="Ограничить доступ" onClick={logout} />
|
<MainButton size="small" title={t('auth-restrictAccess')} onClick={logout} />
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
):(
|
):(
|
||||||
<ModalContainer style={{display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '15px'}}>
|
<ModalContainer style={{display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '15px'}}>
|
||||||
<IconComponent name={buttonNames[8]} width="50" height="50"/>
|
<IconComponent name={buttonNames[8]} width="50" height="50"/>
|
||||||
<h3>{success.message}</h3>
|
<h3>{success.message}</h3>
|
||||||
<p style={{fontSize: '12px'}}>Это окно можно закрыть, либо оно пропадёт само</p>
|
<p style={{fontSize: '12px'}}>{t('auth-closeHint')}</p>
|
||||||
<MainButton size="small" title="Закрыть" onClick={onClose} style={{width: '30%'}}/>
|
<MainButton size="small" title={t('close')} onClick={onClose} style={{width: '30%'}}/>
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ import {NoteDevice} from "./modalContainers/NoteDevice";
|
||||||
import { NoteWifi } from "./modalContainers/NoteWifi";
|
import { NoteWifi } from "./modalContainers/NoteWifi";
|
||||||
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
||||||
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
||||||
|
import {Settings} from "../settings/Settings";
|
||||||
|
import CustomizeNodes from "./modalContainers/customizeNodes";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
|
@ -74,6 +76,14 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
|
||||||
{type === ModalMap['5'] && (
|
{type === ModalMap['5'] && (
|
||||||
<RightsConfirm onClose={closeLoader} />
|
<RightsConfirm onClose={closeLoader} />
|
||||||
)}
|
)}
|
||||||
|
{type === ModalMap['10'] && (
|
||||||
|
<Settings
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['11'] && (
|
||||||
|
<CustomizeNodes />
|
||||||
|
)}
|
||||||
</ModalWindow >
|
</ModalWindow >
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
|
@ -13,7 +13,7 @@ export const ModalWindow = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ModalContainer = styled.div<{height?: string}>`
|
export const ModalContainer = styled.div<{height?: string, width?:string}>`
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: ${(props)=>props.theme.secondary};
|
background: ${(props)=>props.theme.secondary};
|
||||||
color: ${(props=>props.theme.textColor)};
|
color: ${(props=>props.theme.textColor)};
|
||||||
|
|
@ -25,13 +25,14 @@ export const ModalContainer = styled.div<{height?: string}>`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
height: ${(props) => props.height ? props.height : '230px'};
|
height: ${(props) => props.height ? props.height : '230px'};
|
||||||
width: 520px;
|
width: ${(props) => props.width ? props.width : '520px'};
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ModalTitle = styled.div`
|
export const ModalTitle = styled.div`
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-bottom: 1px solid #9D9D9D;
|
border-bottom: 1px solid #9D9D9D;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
h3{
|
h3{
|
||||||
|
|
@ -10,4 +10,6 @@ export const ModalMap: ModalType = {
|
||||||
'7': 'noteWifi',
|
'7': 'noteWifi',
|
||||||
'8': 'connectNetwork',
|
'8': 'connectNetwork',
|
||||||
'9': 'disconnectConfirm',
|
'9': 'disconnectConfirm',
|
||||||
|
"10": 'settings',
|
||||||
|
'11': 'customNodes',
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import { IconComponent } from "../const/buttons/icons"
|
||||||
import { buttonNames } from "../const/buttons/names"
|
import { buttonNames } from "../const/buttons/names"
|
||||||
import ThemeButton from "./UI/buttons/toggletheme"
|
import ThemeButton from "./UI/buttons/toggletheme"
|
||||||
import About from "./workSpace/modules/appModules/about"
|
import About from "./workSpace/modules/appModules/about"
|
||||||
|
import {useTranslation} from "../utils/i18n/hooks";
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -73,6 +74,7 @@ export const CreateUser = () => {
|
||||||
const [repeatPassword, setRepeatPassword] = useState('');
|
const [repeatPassword, setRepeatPassword] = useState('');
|
||||||
const [passwordsMatch, setPasswordsMatch] = useState(true);
|
const [passwordsMatch, setPasswordsMatch] = useState(true);
|
||||||
const [about, setAbout] = useState(false)
|
const [about, setAbout] = useState(false)
|
||||||
|
const {t} = useTranslation('common')
|
||||||
|
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState({
|
||||||
username: 'operator',
|
username: 'operator',
|
||||||
|
|
@ -118,77 +120,81 @@ export const CreateUser = () => {
|
||||||
setAbout(true)
|
setAbout(true)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<>
|
<>
|
||||||
{!about ? (
|
{!about ? (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
<Container onSubmit={handleSubmit}>
|
||||||
|
<Title>
|
||||||
|
<IconComponent name={buttonNames[3]} width="52" height="52" />
|
||||||
|
<h3>{t('operatorConfig-title')}</h3>
|
||||||
|
{ipAddress != 'localhost' &&
|
||||||
|
<p>{ipAddress}</p>
|
||||||
|
}
|
||||||
|
</Title>
|
||||||
|
|
||||||
<Container onSubmit={handleSubmit}>
|
<Content>
|
||||||
<Title>
|
<div>
|
||||||
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent>
|
<p>{t('operatorConfig-instruction')}</p>
|
||||||
<h3>Настройка оператора узла</h3>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholderText={t('operatorConfig-passwordPlaceholder')}
|
||||||
|
id="password"
|
||||||
|
value={user.password}
|
||||||
|
onChange={(value) => handleChange('password', value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholderText={t('operatorConfig-repeatPasswordPlaceholder')}
|
||||||
|
id="repeatPassword"
|
||||||
|
value={repeatPassword}
|
||||||
|
onChange={(value) => handleChange('repeatPassword', value)}
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
/>
|
||||||
|
{!passwordsMatch && (
|
||||||
|
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||||
|
{t('operatorConfig-passwordMismatch')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
title={t('operatorConfig-submit')}
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Menu>
|
||||||
{ipAddress != 'localhost' &&
|
{ipAddress != 'localhost' &&
|
||||||
<p>{ipAddress}</p>
|
<MainButton
|
||||||
|
title={t('common-back')}
|
||||||
|
onClick={back}
|
||||||
|
style={{marginRight: '10px'}}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
|
<ThemeButton />
|
||||||
</Title>
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
<Content>
|
<IconComponent
|
||||||
|
name={buttonNames[4]}
|
||||||
<div>
|
width="30"
|
||||||
<p>Перед началом работы необходимо создать пароль оператора приложения</p>
|
height="30"
|
||||||
</div>
|
onClick={toAbout}
|
||||||
|
/>
|
||||||
<div>
|
</IconContainer>
|
||||||
<Input
|
</Button>
|
||||||
type="password"
|
|
||||||
placeholderText='Введите пароль...'
|
|
||||||
id="password"
|
|
||||||
value={user.password}
|
|
||||||
onChange={(value) => handleChange('password', value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholderText='Повторите пароль...'
|
|
||||||
id="repeatPassword"
|
|
||||||
value={repeatPassword}
|
|
||||||
onChange={(value) => handleChange('repeatPassword', value)}
|
|
||||||
style={{ marginTop: '10px' }}
|
|
||||||
/>
|
|
||||||
{!passwordsMatch && (
|
|
||||||
<p style={{ color: 'red', marginTop: '5px' }}>
|
|
||||||
Пароли не совпадают!
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Content>
|
|
||||||
|
|
||||||
<ButtonContainer>
|
|
||||||
<MainButton style={{ width: '100%' }} title="Отправить" type="submit" />
|
|
||||||
</ButtonContainer>
|
|
||||||
|
|
||||||
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
|
|
||||||
<Menu>
|
|
||||||
{ipAddress != 'localhost' &&
|
|
||||||
<MainButton title="Назад" onClick={back} style={{marginRight: '10px'}}/>
|
|
||||||
}
|
|
||||||
<ThemeButton />
|
|
||||||
<Button>
|
|
||||||
<IconContainer>
|
|
||||||
<IconComponent name={buttonNames[4]} width="30" height="30" onClick={toAbout}/>
|
|
||||||
</IconContainer>
|
|
||||||
</Button>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
): (
|
) : (
|
||||||
<About from="reg" onBack={() => setAbout(false)} />
|
<About from="reg" onBack={() => setAbout(false)} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
226
FRONT/DEV/src/components/settings/SettingItem.tsx
Normal 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 };
|
||||||
118
FRONT/DEV/src/components/settings/SettingSelect.tsx
Normal 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;
|
||||||
264
FRONT/DEV/src/components/settings/Settings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
252
FRONT/DEV/src/components/workSpace/modules/appModules/about.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useWorkSpaceStore } from "../../../../store"
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
import { workSpaceNames } from "../../../../const/workspace/names"
|
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||||
import { buttonNames } from "../../../../const/buttons/names"
|
import { buttonNames } from "../../../../const/buttons/names"
|
||||||
|
import {useTranslation} from "../../../../utils/i18n/hooks";
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -49,30 +50,33 @@
|
||||||
`
|
`
|
||||||
|
|
||||||
export const HelloWindow = () => {
|
export const HelloWindow = () => {
|
||||||
|
const {t} = useTranslation('common')
|
||||||
const { setWorkModule } = useWorkSpaceStore()
|
const { setWorkModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Container className="unselectable">
|
<Container className="unselectable">
|
||||||
<Title>
|
<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>
|
</Title>
|
||||||
|
|
||||||
|
|
||||||
<ButtonWrapper>
|
<ButtonWrapper>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<MainButton
|
<MainButton
|
||||||
title="Настройки сети"
|
title={t('main-networkSettings')}
|
||||||
icon={buttonNames[3]}
|
icon={buttonNames[3]}
|
||||||
iconSize={{ width: '32', height: '32' }}
|
iconSize={{ width: '32', height: '32' }}
|
||||||
onClick={() => setWorkModule(workSpaceNames[3])}
|
onClick={() => setWorkModule(workSpaceNames[3])}
|
||||||
/>
|
/>
|
||||||
<MainButton
|
<MainButton
|
||||||
title="О программе"
|
title={t('main-about')}
|
||||||
icon={buttonNames[4]}
|
icon={buttonNames[4]}
|
||||||
iconSize={{ width: '32', height: '32' }}
|
iconSize={{ width: '32', height: '32' }}
|
||||||
onClick={()=>setWorkModule(workSpaceNames[4])}
|
onClick={() => setWorkModule(workSpaceNames[4])}
|
||||||
/>
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
|
|
||||||
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
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`
|
const ScanBtn = styled.button`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -20,23 +20,24 @@ const ScanBtn = styled.button`
|
||||||
|
|
||||||
interface ListUsersProps {
|
interface ListUsersProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
handleScan: ()=>void;
|
handleScan: (replay?: boolean) => void;
|
||||||
devices: DeviceUser[];
|
devices: DeviceUser[];
|
||||||
scanInitiated: boolean;
|
scanInitiated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
||||||
|
const {t} = useTranslation('common')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScanBtn onClick={handleScan} disabled={loading}>
|
<ScanBtn onClick={() => handleScan()} disabled={loading}>
|
||||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
|
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
|
||||||
</ScanBtn>
|
</ScanBtn>
|
||||||
|
|
||||||
{devices.length > 0 ? (
|
{devices.length > 0 ? (
|
||||||
<TableScanner devices={devices} />
|
<TableScanner devices={devices} />
|
||||||
) : scanInitiated ? (
|
) : scanInitiated && !loading ? (
|
||||||
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
|
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { useIpStore } from "../../../../../store";
|
import { useIpStore } from "../../../../../store";
|
||||||
import styled, { keyframes } from "styled-components";
|
import styled, { keyframes } from "styled-components";
|
||||||
import { useState } from "react";
|
import {useEffect, useState, useTransition} from "react";
|
||||||
import { NetworkService } from "../../../../../api/request";
|
import { NetworkService } from "../../../../../api/request";
|
||||||
import ListUsers from "./modules/ListUsers";
|
import ListUsers from "./modules/ListUsers";
|
||||||
import TableUsers from "./modules/TableUsers";
|
import TableUsers from "./modules/TableUsers";
|
||||||
import DropdownType from "../../../../UI/Dropdown/DropdownType";
|
import DropdownType from "../../../../UI/Dropdown/DropdownType";
|
||||||
import useNetworkStore from "../../../../../store/useDeviceStore";
|
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||||
|
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
|
||||||
|
import IpScanning from "./modules/IpScanning";
|
||||||
|
import {useTranslation} from "../../../../../utils/i18n/hooks";
|
||||||
|
|
||||||
|
|
||||||
type Option ={
|
type Option ={
|
||||||
value: number,
|
value: number,
|
||||||
|
|
@ -51,7 +55,7 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: ${(props) => props.theme.background};
|
background: ${(props) => props.theme.background};
|
||||||
padding: 50px;
|
padding: 80px 50px 40px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
transition: justify-content .4s ease;
|
transition: justify-content .4s ease;
|
||||||
|
|
@ -59,13 +63,20 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
const TitleWrapper = styled.h2`
|
const TopWrapper = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
const TopTitle = styled.h2`
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
text-align: center;
|
||||||
`
|
`
|
||||||
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
|
@ -77,7 +88,7 @@ const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||||
span {
|
span {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: ${(props) => props.theme.accent};
|
background: ${(props) => props.theme.scannerColor};
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|
@ -130,31 +141,65 @@ const ScanningText = styled.span`
|
||||||
|
|
||||||
|
|
||||||
export default function UserScanner() {
|
export default function UserScanner() {
|
||||||
|
const {t} = useTranslation('common')
|
||||||
const { ipAddress } = useIpStore();
|
const { ipAddress } = useIpStore();
|
||||||
|
const [ipScanning, setIpScanning] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isValid, setIsValid] = useState(true);
|
||||||
const { devices, setDevices } = useNetworkStore();
|
const { devices, setDevices } = useNetworkStore();
|
||||||
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
|
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 {
|
try {
|
||||||
setLoading(true);
|
if (ipScanning && !validateIp(ipScanning)) {
|
||||||
|
setIsValid(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsValid(true);
|
||||||
|
|
||||||
|
if (!replay) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
setScanInitiated(true);
|
setScanInitiated(true);
|
||||||
const res = await NetworkService.scanUser(ipAddress);
|
const res = await NetworkService.scanUser(ipAddress, ipScanning);
|
||||||
console.log(res);
|
console.log(res)
|
||||||
setDevices(res.data.devices || []);
|
setDevices(res.data.devices || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка сканирования сети:", err);
|
console.error("Ошибка сканирования сети:", err);
|
||||||
setDevices([])
|
setDevices([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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[] = [
|
const viewOptions: Option[] = [
|
||||||
{ value: 1, label: 'Таблица' },
|
{ value: 1, label: t('userScanner-visible_table') },
|
||||||
{ value: 2, label: 'Список' },
|
{ value: 2, label: t('userScanner-visible_list') },
|
||||||
];
|
];
|
||||||
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
|
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
|
||||||
const handleViewChange = (option: Option) => {
|
const handleViewChange = (option: Option) => {
|
||||||
|
|
@ -163,7 +208,18 @@ export default function UserScanner() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
<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}>
|
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||||
<span className="center"></span>
|
<span className="center"></span>
|
||||||
<span className="ring1"></span>
|
<span className="ring1"></span>
|
||||||
|
|
@ -172,17 +228,17 @@ export default function UserScanner() {
|
||||||
</UserLocate>
|
</UserLocate>
|
||||||
|
|
||||||
{scanInitiated && !loading && (
|
{scanInitiated && !loading && (
|
||||||
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
|
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingText>
|
<LoadingText>
|
||||||
Сканирование узлов в подсети<ScanningText />
|
{t('userScanner-loading')} <ScanningText />
|
||||||
</LoadingText>
|
</LoadingText>
|
||||||
) : selectedView.value == 1 ? (
|
) : 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>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
@ -5,6 +5,7 @@ import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
|
||||||
import ModalWifiScaner from "./modules/ModalWifiScaner";
|
import ModalWifiScaner from "./modules/ModalWifiScaner";
|
||||||
import useNetworkStore from "../../../../../store/useDeviceStore";
|
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||||
import {useIpStore} from "../../../../../store";
|
import {useIpStore} from "../../../../../store";
|
||||||
|
import {useTranslation} from "../../../../../utils/i18n/hooks";
|
||||||
|
|
||||||
// Анимации
|
// Анимации
|
||||||
const ring1 = keyframes`
|
const ring1 = keyframes`
|
||||||
|
|
@ -57,6 +58,7 @@ const TitleWrapper = styled.h2`
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
text-align: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||||
|
|
@ -137,41 +139,45 @@ const ScanBtn = styled.button<{scanInitiated: boolean}>`
|
||||||
|
|
||||||
export function WifiScanner() {
|
export function WifiScanner() {
|
||||||
const { ipAddress } = useIpStore();
|
const { ipAddress } = useIpStore();
|
||||||
|
//const ipAddress = '192.168.1.148'
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [scanInitiated, setScanInitiated] = useState(false);
|
const [scanInitiated, setScanInitiated] = useState(false);
|
||||||
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
||||||
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
|
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
|
setWifiNetworks([]);
|
||||||
|
setCurrentNetwork(null);
|
||||||
|
setLoading(true);
|
||||||
|
setScanInitiated(true);
|
||||||
|
try {
|
||||||
|
const res = await NetworkService.scanWifi(ipAddress);
|
||||||
|
const networks = res.data.access_points || [];
|
||||||
|
setWifiNetworks(networks);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(t('wifiScanner-scanError'), err);
|
||||||
setWifiNetworks([]);
|
setWifiNetworks([]);
|
||||||
setCurrentNetwork(null)
|
} finally {
|
||||||
setLoading(true);
|
setLoading(false);
|
||||||
setScanInitiated(true);
|
}
|
||||||
try {
|
};
|
||||||
const res = await NetworkService.scanWifi(ipAddress);
|
|
||||||
const networks = res.data.access_points || [];
|
|
||||||
setWifiNetworks(networks);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Ошибка сканирования сети:", err);
|
|
||||||
setWifiNetworks([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||||
<TitleWrapper>Сканирование точек доступа Wi-Fi</TitleWrapper>
|
<TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
|
||||||
|
|
||||||
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||||
<span className="center circel"></span>
|
<span className="center circel"></span>
|
||||||
<span className="ring1 circel"></span>
|
<span className="ring1 circel"></span>
|
||||||
<span className="ring2 circel"></span>
|
<span className="ring2 circel"></span>
|
||||||
<span className="ring3 circel"></span>
|
<span className="ring3 circel"></span>
|
||||||
{wifiNetworks.map((network) => (
|
{wifiNetworks.map((network, index) => (
|
||||||
<WifiDot
|
<WifiDot
|
||||||
key={network.bssid}
|
key={network.bssid}
|
||||||
network={network}
|
network={network}
|
||||||
|
index={index}
|
||||||
|
total={wifiNetworks.length}
|
||||||
onSelect={setCurrentNetwork}
|
onSelect={setCurrentNetwork}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -186,11 +192,15 @@ export function WifiScanner() {
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingText>
|
<LoadingText>
|
||||||
Сканирование точек доступа <ScanningText/>
|
{t('wifiScanner-scanning')} <ScanningText/>
|
||||||
</LoadingText>
|
</LoadingText>
|
||||||
) : (
|
) : (
|
||||||
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
|
<ScanBtn
|
||||||
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'}
|
onClick={handleScan}
|
||||||
|
disabled={loading}
|
||||||
|
scanInitiated={wifiNetworks.length > 0}
|
||||||
|
>
|
||||||
|
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
|
||||||
</ScanBtn>
|
</ScanBtn>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||