Доработка мелочей и начало интернац.

This commit is contained in:
test 2025-07-07 17:35:46 +05:00
parent 486d46c467
commit 48e8551b0e
21 changed files with 408 additions and 76 deletions

View file

@ -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);

View file

@ -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);

View file

@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { useIpStore } from './src/store'; import { useIpStore } from './src/store';
import { useEffect } from 'react'; import { useEffect } from 'react';
import {useCheckAdmin} from './src/utils/checkAdmin.ts' import { useCheckAdmin } from './src/utils/checkAdmin';
import { import {
ModalProvider, ModalProvider,
@ -11,21 +11,37 @@ import {
ThemeProvider ThemeProvider
} from './src/store'; } from './src/store';
import WindowComponent from './src/components/modalwindows/modalWindow.tsx'; import WindowComponent from './src/components/modalwindows/modalWindow';
import Router from './src/router.tsx'; 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 MainComponent = () => {
const checkAdmin = useCheckAdmin();
const checkAdmin = useCheckAdmin() const { ipAddress } = useIpStore();
const {ipAddress} = useIpStore()
useEffect(() => { useEffect(() => {
const checkAdminStatus = async () => { const checkAdminStatus = async () => {
await checkAdmin() await checkAdmin();
} };
checkAdminStatus() checkAdminStatus();
}, [ipAddress]) }, [ipAddress]);
const { isOpen, modalType, modalData } = useModalContext(); const { isOpen, modalType, modalData } = useModalContext();
return ( return (
@ -40,13 +56,6 @@ const container = document.getElementById('root');
if (!container) { if (!container) {
throw new Error('Root element with id "root" not found'); throw new Error('Root element with id "root" not found');
} }
const root = createRoot(container); const root = createRoot(container);
root.render( root.render(<AppWithProviders />);
<ThemeProvider>
<HashRouter>
<ModalProvider>
<MainComponent />
</ModalProvider>
</HashRouter>
</ThemeProvider>
);

View file

@ -0,0 +1,22 @@
{
"userScanner-title": "Subnet nodes scanning",
"userScanner-visible": "Display mode",
"userScanner-loading": "Scanning nodes",
"userScanner-refresh": "Refresh list",
"userScanner-scan": "Scan nodes",
"userScanner-notFound": "No nodes found",
"tableScanner-online": "Online",
"tableScanner-name": "Name",
"tableScanner-vendor": "Manufacturer",
"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": "Please enter a valid IP address"
}

View file

@ -0,0 +1,22 @@
{
"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-адрес"
}

View file

@ -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,11 +83,11 @@ 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.post('/subnet/devices',{ return await axiosInstance.post('/subnet/devices',{
subnet: '' subnet: ipScanning,
}); });
} catch (error: any) { } catch (error: any) {
return error.response; return error.response;
@ -180,7 +180,15 @@ export class NetworkService {
static async exportDevices(ipAddress: string): Promise<AxiosResponse> { static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
try { try {
const axiosInstance = this.createAxiosInstance(ipAddress); const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export`); 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) { } catch (error: any) {
return error.response; return error.response;
} }

View file

@ -146,6 +146,20 @@ export const Settings = ({ onClose }: Props) => {
} }
}; };
// Обработка нажатий клавиш
useEffect(() => {
const handleKeyPress = (e: React.KeyboardEvent<HTMLElement>) => {
if (e.key === 'Enter')
handleSave();
if (e.key === 'Escape')
handleCancel()
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, []);
return ( return (
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}> <ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle> <ModalTitle>

View file

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

View file

@ -4,6 +4,7 @@ import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore"; import {DeviceUser} from "../../../../../../store/useDeviceStore";
import {usePaginateSetting} from "../../../../../../hooks/useSettings"; import {usePaginateSetting} from "../../../../../../hooks/useSettings";
import Pagination from "../../../../../UI/Pagination"; import Pagination from "../../../../../UI/Pagination";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button` const ScanBtn = styled.button`
position: relative; position: relative;
@ -27,6 +28,7 @@ interface ListUsersProps {
} }
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => { const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
const activeDevices = devices.filter(device => device.status === 'online'); const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online'); const inactiveDevices = devices.filter(device => device.status !== 'online');
@ -55,11 +57,10 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
setCurrentPage(page); setCurrentPage(page);
} }
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 ? (
@ -70,7 +71,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
}}> }}>
{/* Колонка активных устройств */} {/* Колонка активных устройств */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные узлы ({activeDevices.length})</h3> <h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>
{t('listUsers-activeNodes')} ({activeDevices.length})
</h3>
{currentActiveDevices.map((device, index) => ( {currentActiveDevices.map((device, index) => (
<DeviceCard <DeviceCard
key={index} key={index}
@ -81,7 +84,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
</div> </div>
{/* Колонка неактивных устройств */} {/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные узлы ({inactiveDevices.length})</h3> <h3 style={{ color: '#ababab', marginBottom:'12px' }}>
{t('listUsers-inactiveNodes')} ({inactiveDevices.length})
</h3>
{currentInactiveDevices.map((device, index) => ( {currentInactiveDevices.map((device, index) => (
<DeviceCard <DeviceCard
key={index} key={index}
@ -91,8 +96,8 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
))} ))}
</div> </div>
</div> </div>
) : scanInitiated ? ( ) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>Узлы не найдены</p> <p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null} ) : null}
{totalPages > 1 && ( {totalPages > 1 && (
<Pagination <Pagination

View file

@ -7,11 +7,12 @@ import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store"; import {useModalContext} from "../../../../../../store";
import Pagination from "../../../../../UI/Pagination"; import Pagination from "../../../../../UI/Pagination";
import { import {
useDisabledColumns, useDisabledColumns, useLanguage,
useNameSetting, useNameSetting,
useOrdersColumns, useOrdersColumns,
usePaginateSetting usePaginateSetting
} from "../../../../../../hooks/useSettings"; } from "../../../../../../hooks/useSettings";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
interface TableScannerProps { interface TableScannerProps {
devices: DeviceUser[]; devices: DeviceUser[];
@ -70,6 +71,7 @@ const StatusBadge = styled.span<{ status:string }>`
` `
export const TableScanner = ({ devices }: TableScannerProps) => { export const TableScanner = ({ devices }: TableScannerProps) => {
const {t} = useTranslation('common')
const { openModal } = useModalContext(); const { openModal } = useModalContext();
const nameSetting = useNameSetting(); const nameSetting = useNameSetting();
const ordersColumns = useOrdersColumns(); const ordersColumns = useOrdersColumns();
@ -96,14 +98,14 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
.filter(column => !disabledColumns.includes(ordersColumns.indexOf(column))) .filter(column => !disabledColumns.includes(ordersColumns.indexOf(column)))
.map(column => { .map(column => {
switch(column) { switch(column) {
case 'Онлайн': return 'status'; case t('tableScanner-online'): return 'status';
case 'Имя': return 'name'; case t('tableScanner-name'): return 'name';
case 'Производитель': return 'vendor'; case t('tableScanner-vendor'): return 'vendor';
case 'IP адрес': return 'ip'; case t('tableScanner-ip'): return 'ip';
case 'Открытые порты': return 'ports'; case t('tableScanner-ports'): return 'ports';
case 'Последняя активность': return 'activity'; case t('tableScanner-activity'): return 'activity';
case 'Заметка': return 'note'; case t('tableScanner-note'): return 'note';
case 'Действие': return 'action'; case t('tableScanner-action'): return 'action';
default: return ''; default: return '';
} }
}) })
@ -134,7 +136,7 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
case 'status': case 'status':
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>; return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
case 'name': case 'name':
return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>; return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : t('tableScanner-unknown'))}</TableCell>;
case 'vendor': case 'vendor':
return <TableCell key="vendor">{item.vendor}</TableCell>; return <TableCell key="vendor">{item.vendor}</TableCell>;
case 'ip': case 'ip':
@ -166,12 +168,12 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
{visibleColumns.map(column => { {visibleColumns.map(column => {
switch (column) { switch (column) {
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>; case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
case 'name': return <TableHeaderCell key="name">Имя</TableHeaderCell>; case 'name': return <TableHeaderCell key="name">{t('tableScanner-name')}</TableHeaderCell>;
case 'vendor': return <TableHeaderCell key="vendor">Производитель</TableHeaderCell>; case 'vendor': return <TableHeaderCell key="vendor">{t('tableScanner-vendor')}</TableHeaderCell>;
case 'ip': return <TableHeaderCell key="ip">IP адрес</TableHeaderCell>; case 'ip': return <TableHeaderCell key="ip">{t('tableScanner-ip')}</TableHeaderCell>;
case 'ports': return <TableHeaderCell key="ports">Открытые порты</TableHeaderCell>; case 'ports': return <TableHeaderCell key="ports">{t('tableScanner-ports')}</TableHeaderCell>;
case 'activity': return <TableHeaderCell key="activity">Последняя активность</TableHeaderCell>; case 'activity': return <TableHeaderCell key="activity">{t('tableScanner-activity')}</TableHeaderCell>;
case 'note': return <TableHeaderCell key="note">Заметка</TableHeaderCell>; case 'note': return <TableHeaderCell key="note">{t('tableScanner-note')}</TableHeaderCell>;
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>; case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
default: return null; default: return null;
} }

View file

@ -2,6 +2,7 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {DeviceUser} from "../../../../../../store/useDeviceStore"; import {DeviceUser} from "../../../../../../store/useDeviceStore";
import {TableScanner} from "./TableScanner"; import {TableScanner} from "./TableScanner";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button` const ScanBtn = styled.button`
position: relative; position: relative;
@ -25,17 +26,18 @@ interface ListUsersProps {
} }
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}
</> </>
); );

View file

@ -1,12 +1,15 @@
import { useIpStore } from "../../../../../store"; import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components"; import styled, { keyframes } from "styled-components";
import {useEffect, 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 {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
import IpScanning from "./modules/IpScanning";
import {useTranslation} from "../../../../../utils/i18n/hooks";
type Option ={ type Option ={
value: number, value: number,
@ -52,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;
@ -60,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;
@ -131,24 +141,38 @@ 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 (replay: boolean) => { const handleScan = async (replay: boolean) => {
try { try {
if (ipScanning && !validateIp(ipScanning)) {
setIsValid(false);
return;
}
setIsValid(true);
if (!replay) { if (!replay) {
setLoading(true); setLoading(true);
} }
setScanInitiated(true); setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress); const res = await NetworkService.scanUser(ipAddress, ipScanning);
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);
} }
@ -173,8 +197,8 @@ export default function UserScanner() {
// Способ отображения // Способ отображения
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) => {
@ -183,7 +207,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>
@ -192,12 +227,12 @@ 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(false)} devices={devices} scanInitiated={scanInitiated} /> <TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />

View file

@ -57,6 +57,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 }>`

View file

@ -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);

View file

@ -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);

View file

@ -29,6 +29,6 @@ export const useDisabledColumns = () => {
}; };
export const useThemeSetting = () => { export const useLanguage = () => {
return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system'; return useSettingsStore(state => state.getSettingValue('language'));
}; };

View file

@ -27,6 +27,27 @@ const createNetworkSettings = () => {
label: 'Экспорт данных точек доступа', label: 'Экспорт данных точек доступа',
sectionId: 'Точки доступа', sectionId: 'Точки доступа',
defaultValue: 'Экспортировать', defaultValue: 'Экспортировать',
action: async () => {
try {
const response = await NetworkService.exportNetwork(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: "Сохранить данные сетей",
defaultPath: "network_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert("Данные успешно экспортированы!");
}
} catch (e: any) {
alert("Ошибка экспорта: " + e.message);
}
},
}, },
] as const; ] as const;
}; };

View file

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

View file

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

View file

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

View file

@ -18,5 +18,5 @@ app.include_router(router)
network_service = NetworkService() network_service = NetworkService()
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt") #uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt")
#uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True)