Доработка мелочей и начало интернац.
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)
|
||||
initializationAttempt.current = 0;
|
||||
|
||||
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
||||
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
|
||||
|
||||
let connectionTimeout;
|
||||
setSocket(newSocket);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export const DevicesBar = () => {
|
|||
|
||||
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
||||
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
|
||||
const timeout = setTimeout(() => {
|
||||
testSocket.close();
|
||||
resolve(false);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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.ts'
|
||||
import { useCheckAdmin } from './src/utils/checkAdmin';
|
||||
|
||||
import {
|
||||
ModalProvider,
|
||||
|
|
@ -11,28 +11,44 @@ import {
|
|||
ThemeProvider
|
||||
} from './src/store';
|
||||
|
||||
import WindowComponent from './src/components/modalwindows/modalWindow.tsx';
|
||||
import Router from './src/router.tsx';
|
||||
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()
|
||||
const checkAdmin = useCheckAdmin();
|
||||
const { ipAddress } = useIpStore();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAdminStatus = async () => {
|
||||
await checkAdmin()
|
||||
}
|
||||
checkAdminStatus()
|
||||
}, [ipAddress])
|
||||
|
||||
await checkAdmin();
|
||||
};
|
||||
checkAdminStatus();
|
||||
}, [ipAddress]);
|
||||
|
||||
const { isOpen, modalType, modalData } = useModalContext();
|
||||
return (
|
||||
<>
|
||||
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
|
||||
<Router />
|
||||
</>
|
||||
<>
|
||||
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
|
||||
<Router />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -40,13 +56,6 @@ const container = document.getElementById('root');
|
|||
if (!container) {
|
||||
throw new Error('Root element with id "root" not found');
|
||||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<ThemeProvider>
|
||||
<HashRouter>
|
||||
<ModalProvider>
|
||||
<MainComponent />
|
||||
</ModalProvider>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
root.render(<AppWithProviders />);
|
||||
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";
|
||||
|
||||
const axiosWithoutCredentials = axios.create({
|
||||
baseURL: `https://localhost:8001`,
|
||||
baseURL: `http://localhost:8001`,
|
||||
})
|
||||
|
||||
export class NetworkService {
|
||||
static createAxiosInstance(ipAddress: string) {
|
||||
return axios.create({
|
||||
baseURL: `https://${ipAddress}:8001`,
|
||||
baseURL: `http://${ipAddress}:8001`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +83,11 @@ export class NetworkService {
|
|||
}
|
||||
}
|
||||
|
||||
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
|
||||
static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
return await axiosInstance.post('/subnet/devices',{
|
||||
subnet: ''
|
||||
subnet: ipScanning,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return error.response;
|
||||
|
|
@ -180,7 +180,15 @@ export class NetworkService {
|
|||
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
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) {
|
||||
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 (
|
||||
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
|
||||
<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 {usePaginateSetting} from "../../../../../../hooks/useSettings";
|
||||
import Pagination from "../../../../../UI/Pagination";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
const ScanBtn = styled.button`
|
||||
position: relative;
|
||||
|
|
@ -27,6 +28,7 @@ interface ListUsersProps {
|
|||
}
|
||||
|
||||
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');
|
||||
|
||||
|
|
@ -55,11 +57,10 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
setCurrentPage(page);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'}
|
||||
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
|
||||
</ScanBtn>
|
||||
|
||||
{devices.length > 0 ? (
|
||||
|
|
@ -70,7 +71,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
}}>
|
||||
{/* Колонка активных устройств */}
|
||||
<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) => (
|
||||
<DeviceCard
|
||||
key={index}
|
||||
|
|
@ -81,7 +84,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
</div>
|
||||
{/* Колонка неактивных устройств */}
|
||||
<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) => (
|
||||
<DeviceCard
|
||||
key={index}
|
||||
|
|
@ -91,8 +96,8 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : scanInitiated ? (
|
||||
<p style={{ marginTop: '20px' }}>Узлы не найдены</p>
|
||||
) : scanInitiated && !loading ? (
|
||||
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
|
||||
) : null}
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import {ModalMap} from "../../../../../modalwindows/windowsType";
|
|||
import {useModalContext} from "../../../../../../store";
|
||||
import Pagination from "../../../../../UI/Pagination";
|
||||
import {
|
||||
useDisabledColumns,
|
||||
useDisabledColumns, useLanguage,
|
||||
useNameSetting,
|
||||
useOrdersColumns,
|
||||
usePaginateSetting
|
||||
} from "../../../../../../hooks/useSettings";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
interface TableScannerProps {
|
||||
devices: DeviceUser[];
|
||||
|
|
@ -70,6 +71,7 @@ const StatusBadge = styled.span<{ status:string }>`
|
|||
`
|
||||
|
||||
export const TableScanner = ({ devices }: TableScannerProps) => {
|
||||
const {t} = useTranslation('common')
|
||||
const { openModal } = useModalContext();
|
||||
const nameSetting = useNameSetting();
|
||||
const ordersColumns = useOrdersColumns();
|
||||
|
|
@ -96,14 +98,14 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
|
|||
.filter(column => !disabledColumns.includes(ordersColumns.indexOf(column)))
|
||||
.map(column => {
|
||||
switch(column) {
|
||||
case 'Онлайн': return 'status';
|
||||
case 'Имя': return 'name';
|
||||
case 'Производитель': return 'vendor';
|
||||
case 'IP адрес': return 'ip';
|
||||
case 'Открытые порты': return 'ports';
|
||||
case 'Последняя активность': return 'activity';
|
||||
case 'Заметка': return 'note';
|
||||
case 'Действие': return 'action';
|
||||
case t('tableScanner-online'): return 'status';
|
||||
case t('tableScanner-name'): return 'name';
|
||||
case t('tableScanner-vendor'): return 'vendor';
|
||||
case t('tableScanner-ip'): return 'ip';
|
||||
case t('tableScanner-ports'): return 'ports';
|
||||
case t('tableScanner-activity'): return 'activity';
|
||||
case t('tableScanner-note'): return 'note';
|
||||
case t('tableScanner-action'): return 'action';
|
||||
default: return '';
|
||||
}
|
||||
})
|
||||
|
|
@ -134,7 +136,7 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
|
|||
case 'status':
|
||||
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
|
||||
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':
|
||||
return <TableCell key="vendor">{item.vendor}</TableCell>;
|
||||
case 'ip':
|
||||
|
|
@ -166,12 +168,12 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
|
|||
{visibleColumns.map(column => {
|
||||
switch (column) {
|
||||
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
|
||||
case 'name': return <TableHeaderCell key="name">Имя</TableHeaderCell>;
|
||||
case 'vendor': return <TableHeaderCell key="vendor">Производитель</TableHeaderCell>;
|
||||
case 'ip': return <TableHeaderCell key="ip">IP адрес</TableHeaderCell>;
|
||||
case 'ports': return <TableHeaderCell key="ports">Открытые порты</TableHeaderCell>;
|
||||
case 'activity': return <TableHeaderCell key="activity">Последняя активность</TableHeaderCell>;
|
||||
case 'note': return <TableHeaderCell key="note">Заметка</TableHeaderCell>;
|
||||
case 'name': return <TableHeaderCell key="name">{t('tableScanner-name')}</TableHeaderCell>;
|
||||
case 'vendor': return <TableHeaderCell key="vendor">{t('tableScanner-vendor')}</TableHeaderCell>;
|
||||
case 'ip': return <TableHeaderCell key="ip">{t('tableScanner-ip')}</TableHeaderCell>;
|
||||
case 'ports': return <TableHeaderCell key="ports">{t('tableScanner-ports')}</TableHeaderCell>;
|
||||
case 'activity': return <TableHeaderCell key="activity">{t('tableScanner-activity')}</TableHeaderCell>;
|
||||
case 'note': return <TableHeaderCell key="note">{t('tableScanner-note')}</TableHeaderCell>;
|
||||
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
|
||||
default: return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import styled from 'styled-components';
|
||||
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||
import {TableScanner} from "./TableScanner";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
const ScanBtn = styled.button`
|
||||
position: relative;
|
||||
|
|
@ -25,17 +26,18 @@ interface ListUsersProps {
|
|||
}
|
||||
|
||||
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
||||
const {t} = useTranslation('common')
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScanBtn onClick={()=>handleScan()} disabled={loading}>
|
||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'}
|
||||
<ScanBtn onClick={() => handleScan()} disabled={loading}>
|
||||
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
|
||||
</ScanBtn>
|
||||
|
||||
{devices.length > 0 ? (
|
||||
<TableScanner devices={devices} />
|
||||
) : scanInitiated ? (
|
||||
<p style={{ marginTop: '20px' }}>Узлы не найдены</p>
|
||||
) : scanInitiated && !loading ? (
|
||||
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { useIpStore } from "../../../../../store";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useEffect, useState, useTransition} from "react";
|
||||
import { NetworkService } from "../../../../../api/request";
|
||||
import ListUsers from "./modules/ListUsers";
|
||||
import TableUsers from "./modules/TableUsers";
|
||||
import DropdownType from "../../../../UI/Dropdown/DropdownType";
|
||||
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
|
||||
import IpScanning from "./modules/IpScanning";
|
||||
import {useTranslation} from "../../../../../utils/i18n/hooks";
|
||||
|
||||
|
||||
type Option ={
|
||||
value: number,
|
||||
|
|
@ -52,7 +55,7 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${(props) => props.theme.background};
|
||||
padding: 50px;
|
||||
padding: 80px 50px 40px;
|
||||
box-sizing: border-box;
|
||||
gap: 24px;
|
||||
transition: justify-content .4s ease;
|
||||
|
|
@ -60,13 +63,20 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
|||
overflow-x: hidden;
|
||||
position: relative;
|
||||
`;
|
||||
const TitleWrapper = styled.h2`
|
||||
const TopWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`
|
||||
const TopTitle = styled.h2`
|
||||
font-size: 32px;
|
||||
width: max-content;
|
||||
text-align: center;
|
||||
`
|
||||
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||
width: 60px;
|
||||
|
|
@ -131,24 +141,38 @@ const ScanningText = styled.span`
|
|||
|
||||
|
||||
export default function UserScanner() {
|
||||
const {t} = useTranslation('common')
|
||||
const { ipAddress } = useIpStore();
|
||||
const [ipScanning, setIpScanning] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
const { devices, setDevices } = useNetworkStore();
|
||||
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
|
||||
|
||||
const validateIp = (ip: string) => {
|
||||
const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
|
||||
return regex.test(ip);
|
||||
};
|
||||
|
||||
// Сканирование
|
||||
const handleScan = async (replay:boolean) => {
|
||||
const handleScan = async (replay: boolean) => {
|
||||
try {
|
||||
if(!replay){
|
||||
if (ipScanning && !validateIp(ipScanning)) {
|
||||
setIsValid(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValid(true);
|
||||
|
||||
if (!replay) {
|
||||
setLoading(true);
|
||||
}
|
||||
setScanInitiated(true);
|
||||
const res = await NetworkService.scanUser(ipAddress);
|
||||
console.log(res)
|
||||
const res = await NetworkService.scanUser(ipAddress, ipScanning);
|
||||
setDevices(res.data.devices || []);
|
||||
} catch (err) {
|
||||
console.error("Ошибка сканирования сети:", err);
|
||||
setDevices([])
|
||||
setDevices([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -173,8 +197,8 @@ export default function UserScanner() {
|
|||
|
||||
// Способ отображения
|
||||
const viewOptions: Option[] = [
|
||||
{ value: 1, label: 'Таблица' },
|
||||
{ value: 2, label: 'Список' },
|
||||
{ value: 1, label: t('userScanner-visible_table') },
|
||||
{ value: 2, label: t('userScanner-visible_list') },
|
||||
];
|
||||
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
|
||||
const handleViewChange = (option: Option) => {
|
||||
|
|
@ -183,7 +207,18 @@ export default function UserScanner() {
|
|||
|
||||
return (
|
||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов подсети</TitleWrapper>)}
|
||||
|
||||
<TopWrapper>
|
||||
{(!scanInitiated || loading) && (<TopTitle>{t('userScanner-title')}</TopTitle>)}
|
||||
<IpScanning
|
||||
value={ipScanning}
|
||||
setIpScanning={setIpScanning}
|
||||
loading={loading}
|
||||
isValid={isValid}
|
||||
setIsValid={setIsValid}
|
||||
/>
|
||||
</TopWrapper>
|
||||
|
||||
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||
<span className="center"></span>
|
||||
<span className="ring1"></span>
|
||||
|
|
@ -192,12 +227,12 @@ export default function UserScanner() {
|
|||
</UserLocate>
|
||||
|
||||
{scanInitiated && !loading && (
|
||||
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
|
||||
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<LoadingText>
|
||||
Сканирование узлов <ScanningText />
|
||||
{t('userScanner-loading')} <ScanningText />
|
||||
</LoadingText>
|
||||
) : selectedView.value == 1 ? (
|
||||
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const TitleWrapper = styled.h2`
|
|||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
|||
setHasInited(false)
|
||||
initializationAttempt.current = 0;
|
||||
|
||||
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
||||
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
|
||||
|
||||
let connectionTimeout;
|
||||
setSocket(newSocket);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export const DevicesBar = () => {
|
|||
|
||||
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
||||
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
|
||||
const timeout = setTimeout(() => {
|
||||
testSocket.close();
|
||||
resolve(false);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@ export const useDisabledColumns = () => {
|
|||
};
|
||||
|
||||
|
||||
export const useThemeSetting = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system';
|
||||
export const useLanguage = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('language'));
|
||||
};
|
||||
|
|
@ -27,6 +27,27 @@ const createNetworkSettings = () => {
|
|||
label: 'Экспорт данных точек доступа',
|
||||
sectionId: 'Точки доступа',
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
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()
|
||||
|
||||
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)
|
||||
#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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue