Доработка мелочей и начало интернац.
This commit is contained in:
parent
486d46c467
commit
48e8551b0e
21 changed files with 408 additions and 76 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
22
FRONT/DEV/public/locales/en/common.json
Normal file
22
FRONT/DEV/public/locales/en/common.json
Normal 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"
|
||||||
|
}
|
||||||
22
FRONT/DEV/public/locales/ru/common.json
Normal file
22
FRONT/DEV/public/locales/ru/common.json
Normal 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-адрес"
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(!replay){
|
if (ipScanning && !validateIp(ipScanning)) {
|
||||||
|
setIsValid(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsValid(true);
|
||||||
|
|
||||||
|
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} />
|
||||||
|
|
|
||||||
|
|
@ -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 }>`
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
11
FRONT/DEV/src/utils/i18n/config.ts
Normal file
11
FRONT/DEV/src/utils/i18n/config.ts
Normal 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];
|
||||||
11
FRONT/DEV/src/utils/i18n/hooks.ts
Normal file
11
FRONT/DEV/src/utils/i18n/hooks.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
84
FRONT/DEV/src/utils/i18n/provider.tsx
Normal file
84
FRONT/DEV/src/utils/i18n/provider.tsx
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue