Сборка фронт части
13
FRONT/BUILD/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GoNets</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/assets/icon-4fEsvohP.png" />
|
||||||
|
<script type="module" crossorigin src="/assets/index-QSWEVNb0.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-DWFkiel1.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
FRONT/BUILD/resources/app/.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/dist
|
||||||
|
Before Width: | Height: | Size: 126 KiB |
13
FRONT/BUILD/resources/app/dist/index.html
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>GoNets</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="./assets/icon-4fEsvohP.png" />
|
|
||||||
<script type="module" crossorigin src="./assets/index-CAnb47c8.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DWFkiel1.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
28
FRONT/BUILD/resources/app/eslint.config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
12
FRONT/BUILD/resources/app/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GoNets</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="assets/icon.png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
FRONT/BUILD/resources/app/index.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
import { useIpStore } from './src/store';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModalProvider,
|
||||||
|
useModalContext,
|
||||||
|
ThemeProvider
|
||||||
|
} from './src/store';
|
||||||
|
|
||||||
|
import WindowComponent from './src/components/modalwindows/modalWindow.tsx';
|
||||||
|
import Router from './src/router.tsx';
|
||||||
|
|
||||||
|
const MainComponent = () => {
|
||||||
|
|
||||||
|
const checkAdmin = useCheckAdmin()
|
||||||
|
const {ipAddress} = useIpStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAdminStatus = async () => {
|
||||||
|
await checkAdmin()
|
||||||
|
}
|
||||||
|
checkAdminStatus()
|
||||||
|
}, [ipAddress])
|
||||||
|
|
||||||
|
|
||||||
|
const { isOpen, modalType, modalData } = useModalContext();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
|
||||||
|
<Router />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
if (!container) {
|
||||||
|
throw new Error('Root element with id "root" not found');
|
||||||
|
}
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<HashRouter>
|
||||||
|
<ModalProvider>
|
||||||
|
<MainComponent />
|
||||||
|
</ModalProvider>
|
||||||
|
</HashRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
@ -21,7 +21,7 @@ function createWindow() {
|
||||||
win.show();
|
win.show();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
win.loadURL('http://localhost:3000');
|
win.loadURL('https://localhost:8001');
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/icons-ic": "^1.2.13",
|
"@iconify/icons-ic": "^1.2.13",
|
||||||
|
"@iconify/icons-ph": "^1.2.5",
|
||||||
"@iconify/react": "^5.2.0",
|
"@iconify/react": "^5.2.0",
|
||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"@xyflow/react": "^12.3.5",
|
"@xyflow/react": "^12.3.5",
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 459 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 966 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
160
FRONT/BUILD/resources/app/src/api/request.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
const axiosWithoutCredentials = axios.create({
|
||||||
|
baseURL: `https://localhost:8001`,
|
||||||
|
})
|
||||||
|
|
||||||
|
export class NetworkService {
|
||||||
|
static createAxiosInstance(ipAddress: string) {
|
||||||
|
return axios.create({
|
||||||
|
baseURL: `https://${ipAddress}:8001`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkIp(ipAddress: string, data: any): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post('/ip/check_ip', data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveIp(ipAddress: string, data: any): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post('/ip/apply_ip', data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getHostname(): Promise<AxiosResponse>{
|
||||||
|
try{
|
||||||
|
const response = await axiosWithoutCredentials.get('/network/get_hostname')
|
||||||
|
return response
|
||||||
|
} catch (error: any){
|
||||||
|
return error.response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createUser(ipAddress: string, data: any): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post('/auth/register', data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async login(ipAddress: string, data: any): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post('/auth/login', data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async logout(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/auth/logout');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkServerConf(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/auth/check_json');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async whoami(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/auth/me');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/subnet/devices');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/subnet/wifi');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async addNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post(`/subnet/${type}/${address}/note`, {
|
||||||
|
note: note
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async editNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.put(`/subnet/${type}/${address}/note`, {
|
||||||
|
note: note
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteNote(ipAddress: string, address:string, type:string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.delete(`/subnet/${type}/${address}/note`);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async statusNetwork(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get(`/subnet/wifi/status`);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async connectNetwork(ipAddress: string, password:string, address: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post(`/subnet/wifi/connect`, {
|
||||||
|
ssid: address,
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async disconnectNetwork(ipAddress: string, ssid: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
|
||||||
|
ssid: ssid,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
FRONT/BUILD/resources/app/src/assets/css/flow.css
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
.react-flow {
|
||||||
|
/* Custom Variables */
|
||||||
|
--xy-theme-selected: #F57DBD;
|
||||||
|
--xy-theme-hover: #C5C5C5;
|
||||||
|
--xy-theme-edge-hover: black;
|
||||||
|
--xy-theme-color-focus: #E8E8E8;
|
||||||
|
|
||||||
|
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
|
||||||
|
--xy-node-border-default: 1px solid #EDEDED;
|
||||||
|
|
||||||
|
/* --xy-node-boxshadow-default:
|
||||||
|
0px 3.54px 4.55px 0px #00000005,
|
||||||
|
0px 3.54px 4.55px 0px #0000000D,
|
||||||
|
0px 0.51px 1.01px 0px #0000001A; */
|
||||||
|
|
||||||
|
--xy-node-border-radius-default: 8px;
|
||||||
|
|
||||||
|
--xy-handle-background-color-default: #ffffff;
|
||||||
|
--xy-handle-border-color-default: #AAAAAA;
|
||||||
|
|
||||||
|
--xy-edge-label-color-default: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow.dark {
|
||||||
|
--xy-node-boxshadow-default:
|
||||||
|
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05), /* light shadow */
|
||||||
|
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13), /* medium shadow */
|
||||||
|
0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
|
||||||
|
--xy-theme-color-focus: #535353;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customizing Default Theming */
|
||||||
|
|
||||||
|
.react-flow__node {
|
||||||
|
/* box-shadow: var(--xy-node-boxshadow-default);
|
||||||
|
border-radius: var(--xy-node-border-radius-default);
|
||||||
|
background-color: var(--xy-node-background-color-default); */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
/* border: var(--xy-node-border-default);
|
||||||
|
color: var(--xy-node-color, var(--xy-node-color-default)); */
|
||||||
|
pointer-events: all;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node.selectable:focus {
|
||||||
|
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node.selectable:focus:active {
|
||||||
|
box-shadow: var(--xy-node-boxshadow-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.react-flow__node.selectable:hover,
|
||||||
|
.react-flow__node.draggable:hover {
|
||||||
|
border-color: var(--xy-theme-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node.selectable.selected {
|
||||||
|
border-color: var(--xy-theme-selected);
|
||||||
|
box-shadow: var(--xy-node-boxshadow-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node-group {
|
||||||
|
background-color: rgba(207, 182, 255, 0.4);
|
||||||
|
border-color: #9E86ED;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge.selectable:hover .react-flow__edge-path,
|
||||||
|
.react-flow__edge.selectable.selected .react-flow__edge-path {
|
||||||
|
stroke: var(--xy-theme-edge-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle.connectionindicator:hover {
|
||||||
|
pointer-events: all;
|
||||||
|
border-color: var(--xy-theme-edge-hover);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle.connectionindicator:focus ,
|
||||||
|
.react-flow__handle.connectingfrom,
|
||||||
|
.react-flow__handle.connectingto {
|
||||||
|
border-color: var(--xy-theme-edge-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node-resizer {
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__resize-control.handle {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #9E86ED;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
36
FRONT/BUILD/resources/app/src/assets/css/index.css
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button{
|
||||||
|
background-color: #006666;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover{
|
||||||
|
background-color: #255252;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
import { DeviceUser } from "../../../store/useDeviceStore";
|
||||||
|
import {buttonNames} from "../../../const/buttons/names";
|
||||||
|
import {ModalMap} from "../../modalwindows/windowsType";
|
||||||
|
import {IconComponent} from "../../../const/buttons/icons";
|
||||||
|
import {useModalContext} from "../../../store";
|
||||||
|
|
||||||
|
interface DeviceCardProps {
|
||||||
|
device: DeviceUser;
|
||||||
|
status: 'online' | 'offline';
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardWrapper = styled.div<{ status: 'online' | 'offline' }>`
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardHeader = styled.div<{ status: 'online' | 'offline' }>`
|
||||||
|
background: ${({ status, theme }) =>
|
||||||
|
status === 'online' ? theme.successColor : theme.offlineColor};
|
||||||
|
color: white;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const CardContent = styled.div<{ isExpanded: boolean }>`
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
${({ isExpanded }) => isExpanded && css`
|
||||||
|
max-height: 500px;
|
||||||
|
opacity: 1;
|
||||||
|
padding: 16px;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InfoRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled.span`
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Value = styled.span`
|
||||||
|
color: #333;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const toggleExpanded = () => setIsExpanded(!isExpanded);
|
||||||
|
const { openModal } = useModalContext();
|
||||||
|
|
||||||
|
const openModalNote = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openModal(ModalMap['6'], {
|
||||||
|
address: device.mac,
|
||||||
|
currentNote: device.note || ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardWrapper status={status} onClick={toggleExpanded}>
|
||||||
|
<CardHeader status={status}>
|
||||||
|
<p>
|
||||||
|
MAC: {device.mac}
|
||||||
|
</p>
|
||||||
|
<span onClick={openModalNote}>
|
||||||
|
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
|
||||||
|
</span>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent isExpanded={isExpanded}>
|
||||||
|
<InfoRow>
|
||||||
|
<Label>Имя:</Label>
|
||||||
|
<Value>{device.hostname || 'Неизвестно'}</Value>
|
||||||
|
</InfoRow>
|
||||||
|
|
||||||
|
<InfoRow>
|
||||||
|
<Label>Производитель:</Label>
|
||||||
|
<Value>{device.vendor || 'Неизвестно'}</Value>
|
||||||
|
</InfoRow>
|
||||||
|
|
||||||
|
<InfoRow>
|
||||||
|
<Label>IP адрес:</Label>
|
||||||
|
<Value>{device.ip}</Value>
|
||||||
|
</InfoRow>
|
||||||
|
|
||||||
|
{device.open_ports?.length > 0 && (
|
||||||
|
<InfoRow>
|
||||||
|
<Label>Открытые порты:</Label>
|
||||||
|
<Value>{device.open_ports.join(', ')}</Value>
|
||||||
|
</InfoRow>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<InfoRow>
|
||||||
|
<Label>Последняя активность:</Label>
|
||||||
|
<Value>{device.last_seen}</Value>
|
||||||
|
</InfoRow>
|
||||||
|
|
||||||
|
<InfoRow>
|
||||||
|
<Label>Заметка:</Label>
|
||||||
|
<Value>{device.note || '-'}</Value>
|
||||||
|
</InfoRow>
|
||||||
|
</CardContent>
|
||||||
|
</CardWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceCard;
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
|
import styled from "styled-components";
|
||||||
|
import {buttonNames} from "../../../const/buttons/names";
|
||||||
|
import {IconComponent} from "../../../const/buttons/icons";
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
value: number,
|
||||||
|
label: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownTypeProps {
|
||||||
|
options: Option[];
|
||||||
|
label?: string;
|
||||||
|
onChange?: (selectedItem: Option) => void;
|
||||||
|
value: Option | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownWrapper = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right:20px;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: ${({theme}) => theme.background};
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 10;
|
||||||
|
`
|
||||||
|
const Label = styled.p`
|
||||||
|
color: ${({theme}) => theme.textColor};
|
||||||
|
font-size: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Dropdown = styled.div`
|
||||||
|
background-color: ${({theme}) => theme.primary};
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all .3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: 6px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DropdownHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DropdownContent = styled.div<{$isOpen: boolean}>`
|
||||||
|
max-height: ${({$isOpen}) => $isOpen ? '500px' : '0'};
|
||||||
|
opacity: ${({$isOpen}) => $isOpen ? 1 : 0};
|
||||||
|
transition: all .3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DropdownRow = styled.button`
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: transparent;
|
||||||
|
text-align: start;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
color: ${({theme}) => theme.textColor};
|
||||||
|
transition: all .3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({theme}) => theme.background};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DropdownArrow = styled.div<{ $isOpen: boolean }>`
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
transform: rotate(${({ $isOpen }) => ($isOpen ? "90deg" : "0deg")});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropdownType = ({options, value, onChange, label}: DropdownTypeProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function toggleOpen() {
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(option: Option) {
|
||||||
|
onChange?.(option);
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownWrapper ref={dropdownRef}>
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownHeader onClick={toggleOpen}>
|
||||||
|
<p>{value?.label || 'Select an option'}</p>
|
||||||
|
<DropdownArrow $isOpen={isOpen}>
|
||||||
|
<IconComponent name={buttonNames[6]} />
|
||||||
|
</DropdownArrow>
|
||||||
|
</DropdownHeader>
|
||||||
|
<DropdownContent $isOpen={isOpen}>
|
||||||
|
{options.map((option, i) => (
|
||||||
|
<DropdownRow key={i} onClick={() => handleSelect(option)}>
|
||||||
|
{option.label}
|
||||||
|
</DropdownRow>
|
||||||
|
))}
|
||||||
|
</DropdownContent>
|
||||||
|
</Dropdown>
|
||||||
|
</DropdownWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownType;
|
||||||
29
FRONT/BUILD/resources/app/src/components/UI/animDot.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import styled, { keyframes } from "styled-components";
|
||||||
|
|
||||||
|
const blink = keyframes`
|
||||||
|
0%, 100% { opacity: 0.3; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface DotProps {
|
||||||
|
$delay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dot = styled.span<DotProps>`
|
||||||
|
animation: ${blink} 1s infinite;
|
||||||
|
animation-delay: ${({ $delay }) => $delay}s;
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AnimDot = () => (
|
||||||
|
<Wrapper>
|
||||||
|
<Dot $delay={0}>.</Dot>
|
||||||
|
<Dot $delay={0.2}>.</Dot>
|
||||||
|
<Dot $delay={0.4}>.</Dot>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useTheme } from "styled-components";
|
||||||
|
import { IconComponent } from "../../../const/buttons/icons";
|
||||||
|
|
||||||
|
const Button = styled.button.withConfig({
|
||||||
|
shouldForwardProp: (prop) => !['color', '$hoverсolor', 'size', '$iconpos'].includes(prop),
|
||||||
|
})<{ color: string; size?: string, $hoverсolor: string, $iconpos: string }>`
|
||||||
|
background-color: ${(props) => props.color};
|
||||||
|
border: none;
|
||||||
|
padding: ${(props) =>
|
||||||
|
props.size === "small"
|
||||||
|
? "10px 28px"
|
||||||
|
: props.size === "big"
|
||||||
|
? "10px 20px"
|
||||||
|
: "12px 20px"}; // По умолчанию
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: ${(props) =>
|
||||||
|
props.size === "small"
|
||||||
|
? "14px"
|
||||||
|
: props.size === "big"
|
||||||
|
? "16px"
|
||||||
|
: "14px"}; // По умолчанию
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: ${(props) =>
|
||||||
|
props.$iconpos === 'right'
|
||||||
|
? 'row-reverse'
|
||||||
|
: 'row'};
|
||||||
|
gap: 10px;
|
||||||
|
transition: all .3s ease;
|
||||||
|
&:hover{
|
||||||
|
background-color: ${(props) => props.$hoverсolor};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
color?: string;
|
||||||
|
hoverColor?: string
|
||||||
|
size?: "small" | "big";
|
||||||
|
title: string
|
||||||
|
onClick?: () => void;
|
||||||
|
type?: "button" | "submit" | "reset" | undefined
|
||||||
|
icon?: string
|
||||||
|
iconPosition?: 'left' | 'right'
|
||||||
|
iconSize?: {
|
||||||
|
width: string,
|
||||||
|
height: string
|
||||||
|
}
|
||||||
|
style?: any
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MainButton = ({ color, size, title, hoverColor, type, icon, iconPosition, iconSize, onClick, style }: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const defaultColor: string = theme.accent;
|
||||||
|
const defaultHoverColor: string = theme.accentHover
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="unselectable"
|
||||||
|
color={color || defaultColor} size={size}
|
||||||
|
$hoverсolor={hoverColor || defaultHoverColor}
|
||||||
|
onClick={onClick}
|
||||||
|
type={type}
|
||||||
|
$iconpos={iconPosition || 'left'}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{icon && <IconComponent name={icon} width={iconSize?.width} height={iconSize?.height} setColor="white"/> }
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { MainButton } from "./mainButton";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { buttonNames } from "../../../const/buttons/names";
|
||||||
|
import { IconComponent as Icon } from '../../../const/buttons/icons';
|
||||||
|
import { useWorkSpaceStore } from "../../../store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
size: "small" | "big";
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = styled.div<{ size: "small" | "big" }>`
|
||||||
|
${(props) => props.size === "small" && "width: 100%;"}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Button = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${(props) => props.theme.textColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const QuitButton = ({ size }: Props) => {
|
||||||
|
const handleExit = () => {
|
||||||
|
window.electron.quitApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setWorkModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={size} onClick={()=>setWorkModule('helloWindow')}>
|
||||||
|
{size === "big" ? (
|
||||||
|
<MainButton title="Выйти" size="big" />
|
||||||
|
) : (
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name={buttonNames[12]} width="30" height="30"/>
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import styled, { useTheme } from "styled-components";
|
||||||
|
import { IconComponent as Icon } from '../../../const/theme/icons';
|
||||||
|
import { ThemName } from "../../../const/theme/names";
|
||||||
|
|
||||||
|
const Button = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
color: ${props => props.theme.text};
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function ThemeButton() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const name = localStorage.getItem('theme') as ThemName
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={theme.toggleTheme}>
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name={name} width="30" height="30"/>
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
FRONT/BUILD/resources/app/src/components/UI/inputs/input.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
placeholderText: string;
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
type?: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
search?: boolean;
|
||||||
|
onFocus?: any
|
||||||
|
onBlur?: any
|
||||||
|
$size?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex: 3;
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInput = styled.input`
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: black;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #A1A1A1;
|
||||||
|
background-color: transparent;
|
||||||
|
&:focus{
|
||||||
|
border: 1px solid #adadad;
|
||||||
|
}
|
||||||
|
width: 100%;
|
||||||
|
font-size: 18px;
|
||||||
|
color: ${props=>props.theme.textColor}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id, value, onChange, onFileChange, type = "text", multiple, style, onFocus, onBlur }, ref) => {
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
if (onChange) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputContainer>
|
||||||
|
<StyledInput
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholderText}
|
||||||
|
value={value}
|
||||||
|
multiple={multiple}
|
||||||
|
onChange={type === "file" && onFileChange ? onFileChange : handleInputChange}
|
||||||
|
ref={ref}
|
||||||
|
style={style}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
</InputContainer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Input;
|
||||||
101
FRONT/BUILD/resources/app/src/components/UI/loader.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
type Props = {
|
||||||
|
loaderText: string; //Тип передаваемых данных loaderText - string
|
||||||
|
}
|
||||||
|
|
||||||
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
import { AnimDot } from './animDot';
|
||||||
|
|
||||||
|
|
||||||
|
//Константа которая устанавливает кейфвреймы для вращения
|
||||||
|
const spinAnimation = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
//Стиль экрана загрузки
|
||||||
|
export const LoaderContainer = styled.div`
|
||||||
|
top: 0px;
|
||||||
|
z-index:998;
|
||||||
|
background-color: rgba(0,0,0, 0.7);
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
//Стиль блока с элементами загрузки
|
||||||
|
export const Loaderblock = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
//Стиль для блока с кольцом загрузки
|
||||||
|
export const LoadingSpinner = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
`
|
||||||
|
|
||||||
|
//Стиль для кольца загрузки
|
||||||
|
export const LoadingRing = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 12px solid white;
|
||||||
|
`
|
||||||
|
//Стиль для части кольца загрузки, которая будет вращаться
|
||||||
|
export const LoadingPart = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 12px solid transparent;
|
||||||
|
border-top: 12px solid ${props => props.theme.accent};
|
||||||
|
|
||||||
|
// Добавление аниманции и использование кейфремов
|
||||||
|
animation: ${spinAnimation} 1s linear infinite;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Text = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-left: 25px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Loader({ loaderText }: Props) {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Loaderblock>
|
||||||
|
|
||||||
|
<LoadingSpinner>
|
||||||
|
<LoadingRing />
|
||||||
|
<LoadingPart />
|
||||||
|
</LoadingSpinner>
|
||||||
|
|
||||||
|
{/* Текст экрана загрузки устанавливается с помощью передаваемых данных loaderText */}
|
||||||
|
<Text>{loaderText}<AnimDot/></Text>
|
||||||
|
</Loaderblock>
|
||||||
|
)
|
||||||
|
}
|
||||||
103
FRONT/BUILD/resources/app/src/components/UI/routerLoader.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
type Props = {
|
||||||
|
loaderText: string; //Тип передаваемых данных loaderText - string
|
||||||
|
}
|
||||||
|
|
||||||
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
import { AnimDot } from './animDot';
|
||||||
|
import { ModalWindow } from '../modalwindows/style';
|
||||||
|
|
||||||
|
|
||||||
|
//Константа которая устанавливает кейфвреймы для вращения
|
||||||
|
const spinAnimation = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
//Стиль экрана загрузки
|
||||||
|
export const LoaderContainer = styled.div`
|
||||||
|
top: 0px;
|
||||||
|
z-index:998;
|
||||||
|
background-color: rgba(0,0,0, 0.7);
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
//Стиль блока с элементами загрузки
|
||||||
|
export const Loaderblock = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
//Стиль для блока с кольцом загрузки
|
||||||
|
export const LoadingSpinner = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
`
|
||||||
|
|
||||||
|
//Стиль для кольца загрузки
|
||||||
|
export const LoadingRing = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 12px solid white;
|
||||||
|
`
|
||||||
|
//Стиль для части кольца загрузки, которая будет вращаться
|
||||||
|
export const LoadingPart = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 12px solid transparent;
|
||||||
|
border-top: 12px solid ${props => props.theme.accent};
|
||||||
|
|
||||||
|
// Добавление аниманции и использование кейфремов
|
||||||
|
animation: ${spinAnimation} 1s linear infinite;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Text = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-left: 25px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function RouterLoader({ loaderText }: Props) {
|
||||||
|
return (
|
||||||
|
<ModalWindow>
|
||||||
|
<Loaderblock>
|
||||||
|
<LoadingSpinner>
|
||||||
|
<LoadingRing />
|
||||||
|
<LoadingPart />
|
||||||
|
</LoadingSpinner>
|
||||||
|
|
||||||
|
{/* Текст экрана загрузки устанавливается с помощью передаваемых данных loaderText */}
|
||||||
|
<Text>{loaderText}<AnimDot/></Text>
|
||||||
|
</Loaderblock>
|
||||||
|
</ModalWindow>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
63
FRONT/BUILD/resources/app/src/components/UI/signalSvg.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface SignalSVGProps {
|
||||||
|
signal: number; // Принимаем signal как пропс
|
||||||
|
color: string;
|
||||||
|
size: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
|
||||||
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Устанавливаем цвета в зависимости от signal
|
||||||
|
const colors = {
|
||||||
|
'--st1-color': signal >= 1 ? color : 'black',
|
||||||
|
'--st2-color': signal >= 2 ? color : 'black',
|
||||||
|
'--st3-color': signal >= 3 ? color : 'black',
|
||||||
|
'--st4-color': signal >= 4 ? color : 'black',
|
||||||
|
'--st5-color': signal === 5 ? color : 'black',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Применяем стили к SVG
|
||||||
|
if (svgRef.current) {
|
||||||
|
Object.entries(colors).forEach(([key, value]) => {
|
||||||
|
svgRef.current?.style.setProperty(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [signal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 296 235"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
width={size} // Фиксированная ширина
|
||||||
|
height={size}
|
||||||
|
>
|
||||||
|
<style type="text/css">
|
||||||
|
{`
|
||||||
|
.st1 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st1-color, white); }
|
||||||
|
.st2 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st2-color, white); }
|
||||||
|
.st3 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st3-color, white); }
|
||||||
|
.st4 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st4-color, white); }
|
||||||
|
.st5 { fill-rule: evenodd; clip-rule: evenodd; fill: var(--st5-color, white); }
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<polygon className="st1" points="47,138 47,188 0,188 0,164" />
|
||||||
|
<polygon className="st2" points="109,104 109,188 62,188 62,130" />
|
||||||
|
<polygon className="st3" points="171,69 171,188 124,188 124,95" />
|
||||||
|
<polygon className="st4" points="234,35 234,188 187,188 187,61" />
|
||||||
|
<polygon className="st5" points="296,0 296,188 249,188 249,26" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignalSVG;
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { DeviceUser } from '../../../store/useDeviceStore';
|
||||||
|
|
||||||
|
interface TableScannerProps {
|
||||||
|
devices: DeviceUser[];
|
||||||
|
}
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {IconComponent} from "../../../const/buttons/icons";
|
||||||
|
import {buttonNames} from "../../../const/buttons/names";
|
||||||
|
import {ModalMap} from "../../modalwindows/windowsType";
|
||||||
|
import {useModalContext} from "../../../store";
|
||||||
|
|
||||||
|
const TableContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
background: ${({ theme }) => theme.primary};
|
||||||
|
margin: 24px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTable = styled.table`
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
|
||||||
|
padding: 16px 24px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
|
background: ${({ theme }) => theme.primary};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TableRow = styled.tr<{ clickable?: boolean }>`
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ theme }) => theme.background};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TableCell = styled.td`
|
||||||
|
padding: 16px 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${({ theme }) => theme.text};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatusBadge = styled.span<{ status:string }>`
|
||||||
|
width: 10px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
|
||||||
|
display: inline-block;
|
||||||
|
border-radius:4px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TableScanner = ({devices}:TableScannerProps) => {
|
||||||
|
const { openModal } = useModalContext();
|
||||||
|
|
||||||
|
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openModal(ModalMap['6'], {
|
||||||
|
address: device.mac,
|
||||||
|
currentNote: device.note || ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<StyledTable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<TableHeaderCell></TableHeaderCell>
|
||||||
|
<TableHeaderCell>Имя</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Производитель</TableHeaderCell>
|
||||||
|
<TableHeaderCell>IP адрес</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Открытые порты</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Последняя активность</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Заметка</TableHeaderCell>
|
||||||
|
<TableHeaderCell></TableHeaderCell>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{devices.map((item, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
|
||||||
|
<TableCell>{item.hostname || 'Неизвестен'}</TableCell>
|
||||||
|
<TableCell>{item.vendor}</TableCell>
|
||||||
|
<TableCell>{item.ip}</TableCell>
|
||||||
|
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
|
||||||
|
<TableCell>{item.last_seen}</TableCell>
|
||||||
|
<TableCell>{item.note || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span onClick={(e)=>openModalNote(e, item)}>
|
||||||
|
<IconComponent name={buttonNames[16]} width="26" height="26" />
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</StyledTable>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableScanner;
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Circle } from './style';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
style:{
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CircleNode = ({ data }: CustomNodeProps) => {
|
||||||
|
return (
|
||||||
|
<Circle width={data.style.width} height={data.style.height} bordertype={data.style.borderType} bordercolor={data.style.borderColor}>{/* */}
|
||||||
|
{/* <div style={{transform: 'rotate(-45deg)', width: '100%', height: '100%'}}> */}
|
||||||
|
{data.label}
|
||||||
|
|
||||||
|
{/* Узлы для подключения */}
|
||||||
|
<Handle type="source" position={Position.Top} id="source-top" style={{ top: 0, }} />
|
||||||
|
<Handle type="source" position={Position.Right} id="source-right" style={{ right: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Bottom} id="source-bottom" style={{ bottom: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Left} id="source-left" style={{ left: 0 }} />
|
||||||
|
|
||||||
|
{/* Для входящих подключений */}
|
||||||
|
<Handle type="target" position={Position.Top} id="target-top" style={{ top: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Right} id="target-right" style={{ right: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Bottom} id="target-bottom" style={{ bottom: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Left} id="target-left" style={{ left: 0 }} />
|
||||||
|
{/* </div> */}
|
||||||
|
|
||||||
|
</Circle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
width: string,
|
||||||
|
height: string,
|
||||||
|
bordertype: string,
|
||||||
|
bordercolor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Circle = styled.div<Props>`
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px ${(props) => props.bordertype} ${(props) => props.bordercolor};
|
||||||
|
width: ${(props) => props.width}px;
|
||||||
|
height: ${(props) => props.height}px;
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Default } from './style';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
style:{
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultNode = ({ data }: CustomNodeProps) => {
|
||||||
|
return (
|
||||||
|
<Default width={data.style.width} height={data.style.height} bordertype={data.style.borderType} bordercolor={data.style.borderColor}>
|
||||||
|
{data.label}
|
||||||
|
|
||||||
|
{/* Узлы для подключения */}
|
||||||
|
{/* <Handle type="source" position={Position.Top} id="source-top" style={{ top: 0, }} />
|
||||||
|
<Handle type="source" position={Position.Right} id="source-right" style={{ right: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Bottom} id="source-bottom" style={{ bottom: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Left} id="source-left" style={{ left: 0 }} /> */}
|
||||||
|
|
||||||
|
{/* Для входящих подключений */}
|
||||||
|
{/* <Handle type="target" position={Position.Top} id="target-top" style={{ top: 0 }} /> */}
|
||||||
|
<Handle type="target" position={Position.Right} id="target-right" style={{ right: 0 }} />
|
||||||
|
{/* <Handle type="target" position={Position.Bottom} id="target-bottom" style={{ bottom: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Left} id="target-left" style={{ left: 0 }} /> */}
|
||||||
|
{/* </div> */}
|
||||||
|
|
||||||
|
</Default>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
width: string,
|
||||||
|
height: string,
|
||||||
|
bordertype: string,
|
||||||
|
bordercolor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default = styled.div<Props>`
|
||||||
|
background-color: white;
|
||||||
|
border: 2px ${(props) => props.bordertype} ${(props) => props.bordercolor};
|
||||||
|
width: ${(props) => props.width}px;
|
||||||
|
height: ${(props) => props.height}px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: visible;
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Diamond, InnerDiamond, Border } from './style';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
style:{
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DiamondNode = ({ data }: CustomNodeProps) => {
|
||||||
|
|
||||||
|
const baseSize = Math.max(data.style.width, data.style.height);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Diamond width={data.style.width} height={data.style.height}>{/* */}
|
||||||
|
<Border width={data.style.width} height={data.style.height} bordercolor={data.style.borderColor}>
|
||||||
|
<InnerDiamond width={data.style.width} height={data.style.height}>
|
||||||
|
{data.label}
|
||||||
|
</InnerDiamond>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
|
{/* Узлы для подключения */}
|
||||||
|
<Handle type="source" position={Position.Top} id="source-top" />
|
||||||
|
<Handle type="source" position={Position.Right} id="source-right" />
|
||||||
|
<Handle type="source" position={Position.Bottom} id="source-bottom" />
|
||||||
|
<Handle type="source" position={Position.Left} id="source-left" />
|
||||||
|
|
||||||
|
{/* Для входящих подключений */}
|
||||||
|
<Handle type="target" position={Position.Top} id="target-top" />
|
||||||
|
<Handle type="target" position={Position.Right} id="target-right" />
|
||||||
|
<Handle type="target" position={Position.Bottom} id="target-bottom" />
|
||||||
|
<Handle type="target" position={Position.Left} id="target-left" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Diamond >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
interface StyledDefaultProps {
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Diamond = styled.div<StyledDefaultProps>`
|
||||||
|
position: relative;
|
||||||
|
width: ${(props) => props.width}px;
|
||||||
|
height: ${(props) => props.height}px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InnerDiamond = styled.div<StyledDefaultProps>`
|
||||||
|
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
||||||
|
width: ${(props) => props.width - 6}px;
|
||||||
|
height: ${(props) => props.height - 4}px;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Border = styled.div<{ width: number; height: number, bordercolor: any }>`
|
||||||
|
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
||||||
|
width: ${(props) => props.width}px;
|
||||||
|
height: ${(props) => props.height}px;
|
||||||
|
background-color: ${(props) => props.bordercolor};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export {DefaultNode} from './default/defaultNode'
|
||||||
|
export {PillNode} from './pill/pillNode'
|
||||||
|
export {ParalelorgamNode} from './paralelogram/paralelogramNode'
|
||||||
|
export {CircleNode} from './circle/circleNode'
|
||||||
|
export {DiamondNode} from './diamond/diamondNode'
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Paralelorgam } from './style';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
style:{
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ParalelorgamNode = ({ data }: CustomNodeProps) => {
|
||||||
|
return (
|
||||||
|
<Paralelorgam width={data.style.width} height={data.style.height} bordertype={data.style.borderType} bordercolor={data.style.borderColor}>
|
||||||
|
{data.label}
|
||||||
|
|
||||||
|
{/* Узлы для подключения */}
|
||||||
|
<Handle type="source" position={Position.Top} id="source-top"/>
|
||||||
|
<Handle type="source" position={Position.Right} id="source-right"/>
|
||||||
|
<Handle type="source" position={Position.Bottom} id="source-bottom"/>
|
||||||
|
<Handle type="source" position={Position.Left} id="source-left"/>
|
||||||
|
|
||||||
|
{/* Для входящих подключений */}
|
||||||
|
<Handle type="target" position={Position.Top} id="target-top"/>
|
||||||
|
<Handle type="target" position={Position.Right} id="target-right"/>
|
||||||
|
<Handle type="target" position={Position.Bottom} id="target-bottom"/>
|
||||||
|
<Handle type="target" position={Position.Left} id="target-left"/>
|
||||||
|
</Paralelorgam>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
width: string,
|
||||||
|
height: string,
|
||||||
|
bordertype: string,
|
||||||
|
bordercolor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Paralelorgam = styled.div<Props>`
|
||||||
|
transform: skewX(-20deg);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px ${(props) => props.bordertype} ${(props) => props.bordercolor};
|
||||||
|
width: ${(props) => props.width}px;
|
||||||
|
height: ${(props) => props.height}px;
|
||||||
|
|
||||||
|
.react-flow__handle-top{
|
||||||
|
transform: skewX(20deg);
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle-bottom{
|
||||||
|
transform: skewX(20deg);
|
||||||
|
bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle-right{
|
||||||
|
transform: skewX(20deg);
|
||||||
|
right: -5px;
|
||||||
|
top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle-left{
|
||||||
|
transform: skewX(20deg);
|
||||||
|
left: -5px;
|
||||||
|
top: 25px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Pill } from './style';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
style:{
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PillNode = ({ data }: CustomNodeProps) => {
|
||||||
|
return (
|
||||||
|
<Pill width={data.style.width} height={data.style.height} bordertype={data.style.borderType} bordercolor={data.style.borderColor}>{/* */}
|
||||||
|
{/* <div style={{transform: 'rotate(-45deg)', width: '100%', height: '100%'}}> */}
|
||||||
|
{data.label}
|
||||||
|
|
||||||
|
{/* Узлы для подключения */}
|
||||||
|
<Handle type="source" position={Position.Top} id="source-top" style={{ top: 0, }} />
|
||||||
|
<Handle type="source" position={Position.Right} id="source-right" style={{ right: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Bottom} id="source-bottom" style={{ bottom: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Left} id="source-left" style={{ left: 0 }} />
|
||||||
|
|
||||||
|
{/* Для входящих подключений */}
|
||||||
|
<Handle type="target" position={Position.Top} id="target-top" style={{ top: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Right} id="target-right" style={{ right: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Bottom} id="target-bottom" style={{ bottom: 0 }} />
|
||||||
|
<Handle type="target" position={Position.Left} id="target-left" style={{ left: 0 }} />
|
||||||
|
{/* </div> */}
|
||||||
|
|
||||||
|
</Pill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PillNode;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
width: string,
|
||||||
|
height: string,
|
||||||
|
bordertype: string,
|
||||||
|
bordercolor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pill = styled.div<Props>`
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px ${(props) => props.bordertype} ${(props) => props.bordercolor};
|
||||||
|
width: ${(props) => props.width}px;
|
||||||
|
height: ${(props) => props.height}px;
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export {NodeGroupNode} from './networkGroup/nodeGroup'
|
||||||
|
export {NetWorkNode} from './networkNode/networkNode'
|
||||||
|
export {IpGroupeNode} from './ipGroup/ipGroup'
|
||||||
|
export {IpNode} from './ipNode/ipNode'
|
||||||
|
export {PortNode} from './portNode/portNode'
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
import { IpGroup } from "./style";
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
style: {
|
||||||
|
width: any,
|
||||||
|
height: any,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IpGroupeNode = ({ data }: CustomNodeProps) => {
|
||||||
|
return (
|
||||||
|
<IpGroup $width={data.style.width} $height={Number(data.style.height)} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
$width: string,
|
||||||
|
$height?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const IpGroup = styled.div<Props>`
|
||||||
|
width: ${(props) => props.$width}px;
|
||||||
|
height: ${(props) => typeof props.$height === 'number' ? `${props.$height}px` : props.$height};
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Ip } from './style';
|
||||||
|
import { Row, IpTitle, IpContent } from './style';
|
||||||
|
import Input from '../../../../UI/inputs/input';
|
||||||
|
import { useModalContext, usePannableStore } from '../../../../../store';
|
||||||
|
import { ModalMap } from '../../../../modalwindows/windowsType';
|
||||||
|
import { validateIPAddress, validateSameSubnet } from '../../../../../utils/nodeController/validator';
|
||||||
|
import {changeIp} from '../../../../../const/types/types'
|
||||||
|
import { IconComponent } from "../../../../../const/buttons/icons";
|
||||||
|
import { buttonNames } from "../../../../../const/buttons/names";
|
||||||
|
|
||||||
|
interface IIp {
|
||||||
|
ipType: string
|
||||||
|
ipAdress: string,
|
||||||
|
ipGate: string,
|
||||||
|
ipDns: string[]
|
||||||
|
ipVersion: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateType = Record<string, string>;
|
||||||
|
const typeMap: StateType = {
|
||||||
|
'auto': "DHCP",
|
||||||
|
'manual': "Static"
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
ipData: IIp,
|
||||||
|
interfaceData: {
|
||||||
|
device: string,
|
||||||
|
connection: string
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: any,
|
||||||
|
height: any,
|
||||||
|
borderType: string,
|
||||||
|
borderColor: string,
|
||||||
|
}
|
||||||
|
route: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
|
const { toggleEditingMode } = usePannableStore();
|
||||||
|
const [isInput, setIsInput] = useState(false)
|
||||||
|
const { openModal } = useModalContext();
|
||||||
|
|
||||||
|
const [ipData, setIpData] = useState<changeIp>(
|
||||||
|
{
|
||||||
|
currentIp: data.ipData.ipAdress,
|
||||||
|
newIp: data.ipData.ipAdress,
|
||||||
|
currentGate: data.ipData.ipGate,
|
||||||
|
newGate: data.ipData.ipGate,
|
||||||
|
currentDns: data.ipData.ipDns,
|
||||||
|
newDns: data.ipData.ipDns,
|
||||||
|
device: data.interfaceData.device,
|
||||||
|
connection: data.interfaceData.connection,
|
||||||
|
version: data.ipData.ipVersion,
|
||||||
|
prefix: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
toggleEditingMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
toggleEditingMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isInput) {
|
||||||
|
setIsInput(false)
|
||||||
|
setIpData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
newIp: data.ipData.ipAdress,
|
||||||
|
newGate: data.ipData.ipGate,
|
||||||
|
newDns: data.ipData.ipDns
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setIsInput(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (id: string, value: string) => {
|
||||||
|
if (id === 'newDns') {
|
||||||
|
const dnsArray = value.split(',').map(dns => dns.trim());
|
||||||
|
setIpData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
[id]: dnsArray
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setIpData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
[id]: value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSubnetMask(ipAddress: string, ipVersion: string): number {
|
||||||
|
if (ipVersion === "6") {
|
||||||
|
return 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstOctet = parseInt(ipAddress.split(".")[0], 10);
|
||||||
|
if (firstOctet >= 0 && firstOctet <= 127) {
|
||||||
|
return 8;
|
||||||
|
} else if (firstOctet >= 128 && firstOctet <= 191) {
|
||||||
|
return 16;
|
||||||
|
} else if (firstOctet >= 192 && firstOctet <= 223) {
|
||||||
|
return 24;
|
||||||
|
} else {
|
||||||
|
return 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (
|
||||||
|
!validateIPAddress(ipData.newIp) ||
|
||||||
|
!validateIPAddress(ipData.newGate) ||
|
||||||
|
!ipData.newDns.every(dns => validateIPAddress(dns))
|
||||||
|
) {
|
||||||
|
alert('Один или несколько IP-адресов некорректны.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateSameSubnet(ipData.newIp, ipData.newGate, ipData.newDns)) {
|
||||||
|
alert('Адрес, шлюз и хотя бы один DNS должны находиться в одной подсети.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatedPrefix = getSubnetMask(ipData.newIp, data.ipData.ipVersion);
|
||||||
|
|
||||||
|
setIpData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
prefix: calculatedPrefix,
|
||||||
|
}));
|
||||||
|
|
||||||
|
openModal(ModalMap['1'], {
|
||||||
|
...ipData,
|
||||||
|
prefix: calculatedPrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsInput(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Ip $width={data.style.width} $height={Number(data.style.height)} $bordertype={data.style.borderType} $bordercolor={data.style.borderColor}>
|
||||||
|
<IpTitle>
|
||||||
|
{/* <Row>
|
||||||
|
<p>Версия IP:</p>
|
||||||
|
<p>v{data.ipData.ipVersion}</p>
|
||||||
|
</Row> */}
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<p>Метод:</p>
|
||||||
|
<p>{typeMap[data.ipData.ipType]}</p>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{isInput ? (
|
||||||
|
<Row>
|
||||||
|
<IconComponent name={buttonNames[8]} onClick={handleConfirm} width='18' height='18' setColor='white'/>
|
||||||
|
<IconComponent name={buttonNames[9]} onClick={handleClick} width='18' height='18' setColor='white'/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
) : (
|
||||||
|
<IconComponent name={buttonNames[7]} onClick={handleClick} width='18' height='18' setColor='white'/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</IpTitle>
|
||||||
|
|
||||||
|
<IpContent>
|
||||||
|
<Row className='row'>
|
||||||
|
<p className='row-label'>IPv{data.ipData.ipVersion}-Addr:</p>
|
||||||
|
{isInput ? (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholderText={data.ipData.ipAdress}
|
||||||
|
id="newIp"
|
||||||
|
value={ipData.newIp}
|
||||||
|
onChange={(value) => handleChange('newIp', value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className='row-text'>{data.ipData.ipAdress}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className='row'>
|
||||||
|
<p className='row-label'>IPv{data.ipData.ipVersion}-GW:</p>
|
||||||
|
{isInput ? (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholderText={data.ipData.ipGate}
|
||||||
|
id="newGate"
|
||||||
|
value={ipData.newGate}
|
||||||
|
onChange={(value) => handleChange('newGate', value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className='row-text'>{data.ipData.ipGate}</p>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className='row'>
|
||||||
|
<p className='row-label'>IPv{data.ipData.ipVersion}-DNS:</p>
|
||||||
|
{isInput ? (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholderText={data.ipData.ipDns.join(', ')}
|
||||||
|
id="newDns"
|
||||||
|
value={ipData.newDns.join(', ')}
|
||||||
|
onChange={( value) => handleChange('newDns', value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className='row-text'>{data.ipData.ipDns.join(', ')}</p>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{data.route &&
|
||||||
|
<>
|
||||||
|
<Handle type="source" position={Position.Right} id={`source-right`} style={{ right: 0, top: 80 }} />
|
||||||
|
<Handle type="target" position={Position.Right} id={`target-right`} style={{ right: 0, top: 80 }} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</IpContent>
|
||||||
|
</Ip>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
$width: string,
|
||||||
|
$height?: any
|
||||||
|
$bordertype: string,
|
||||||
|
$bordercolor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ip = styled.div<Props>`
|
||||||
|
background-color: ${(props)=>props.theme.secondary};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: ${(props) => props.$width}px;
|
||||||
|
height: ${(props) => typeof props.$height === 'number' ? `${props.$height}px` : props.$height};
|
||||||
|
// box-shadow: 0px 0px 15px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img{
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const IpTitle = styled.div`
|
||||||
|
background: ${(props)=>props.theme.accent};
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
export const IpContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.row{
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
.row-label{
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-text{
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
p{
|
||||||
|
|
||||||
|
text-wrap: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Row = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { NodeGroup, Row, CollapseButton } from "./style";
|
||||||
|
import { useGroupStore } from "../../../../../store";
|
||||||
|
import { MainButton } from "../../../../UI/buttons/mainButton";
|
||||||
|
import { IconComponent } from "../../../../../const/buttons/icons";
|
||||||
|
import { buttonNames } from "../../../../../const/buttons/names";
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
height: any
|
||||||
|
style: {
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeGroupNode = ({ data }: CustomNodeProps) => {
|
||||||
|
const [click, setClick] = useState(false);
|
||||||
|
const { groups, setGroups } = useGroupStore();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const newClickState = !click;
|
||||||
|
setClick(newClickState);
|
||||||
|
|
||||||
|
const updatedNodes = groups.map((groups) =>
|
||||||
|
groups.id === data.id ? { ...groups, click: newClickState } : groups
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!groups.some((groups) => groups.id === data.id)) {
|
||||||
|
updatedNodes.push({ id: data.id, click: newClickState });
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroups(updatedNodes);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeGroup
|
||||||
|
$width={data.style.width}
|
||||||
|
$height={Number(data.style.height)}
|
||||||
|
$bordertype={data.style.borderType}
|
||||||
|
$bordercolor={data.style.borderColor}
|
||||||
|
>
|
||||||
|
<Row $bordertype={data.style.borderType} $bordercolor={data.style.borderColor} $width={data.style.width}>
|
||||||
|
<p>{data.label}</p>
|
||||||
|
|
||||||
|
{/* <MainButton title={click ? 'Показать' : 'Скрыть'} size="small" onClick={handleClick}/> */}
|
||||||
|
<CollapseButton onClick={handleClick}>
|
||||||
|
{click ?(
|
||||||
|
'Развернуть'
|
||||||
|
):(
|
||||||
|
'Свернуть'
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <IconComponent name={click ? buttonNames[5] : buttonNames[6]}/> */}
|
||||||
|
</CollapseButton>
|
||||||
|
</Row>
|
||||||
|
</NodeGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
$width?: string,
|
||||||
|
$height?: any
|
||||||
|
$bordertype?: string,
|
||||||
|
$bordercolor?: string
|
||||||
|
$backgroundcolor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const NodeGroup = styled.div<Props>`
|
||||||
|
border: 2px ${(props) => props.$bordertype} ${(props) => props.$bordercolor};
|
||||||
|
width: ${(props) => props.$width}px;
|
||||||
|
height: ${(props) => typeof props.$height === 'number' ? `${props.$height}px` : props.$height};
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: visible;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Row = styled.div<Props>`
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
left: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: ${props=>props.$width}px;
|
||||||
|
gap: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const CollapseButton = styled.p`
|
||||||
|
padding: unset;
|
||||||
|
background: none;
|
||||||
|
color: ${(props)=>props.theme.textColor};
|
||||||
|
margin-right: 15px !important;
|
||||||
|
&:hover{
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Network, TitleContainer, Device, Status, Content, Row, MAC, LabelColumn } from './style';
|
||||||
|
import SignalSVG from '../../../../UI/signalSvg';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
networkData: {
|
||||||
|
device: string
|
||||||
|
type: string
|
||||||
|
mac: string
|
||||||
|
mtu: number
|
||||||
|
state: string
|
||||||
|
connection: string
|
||||||
|
id: string,
|
||||||
|
strength: string
|
||||||
|
}
|
||||||
|
style: {
|
||||||
|
width: any,
|
||||||
|
height: any,
|
||||||
|
borderType: string,
|
||||||
|
borderColor: string,
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
height: string
|
||||||
|
visible: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetWorkNode = ({ data }: CustomNodeProps) => {
|
||||||
|
const [signal, setSignal] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const perc = Number(data.networkData.strength);
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case perc < 20:
|
||||||
|
setSignal(1);
|
||||||
|
break;
|
||||||
|
case perc < 40:
|
||||||
|
setSignal(2);
|
||||||
|
break;
|
||||||
|
case perc < 60:
|
||||||
|
setSignal(3);
|
||||||
|
break;
|
||||||
|
case perc < 80:
|
||||||
|
setSignal(4);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setSignal(5);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [data.networkData.strength])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Network $width={data.style.width} $height={Number(data.style.height)} $bordertype={data.style.borderType} $bordercolor={data.style.borderColor}>
|
||||||
|
<TitleContainer $backgroundcolor={data.style.backgroundColor} {...(!data.visible ? { $height: "100%" } : {})}>
|
||||||
|
|
||||||
|
{data.networkData.type == "WIFI" &&
|
||||||
|
<img src="./assets/images/wifi.svg" alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
{data.networkData.type == "ETHERNET" &&
|
||||||
|
<img src="./assets/images/ethernet.svg" alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
{data.networkData.type == "TUN" &&
|
||||||
|
<img src="./assets/images/vpn.svg" alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<Device>{data.networkData.device}</Device>
|
||||||
|
|
||||||
|
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
|
||||||
|
<Status>
|
||||||
|
<SignalSVG signal={signal} color={'white'} size={'20px'} />
|
||||||
|
</Status>
|
||||||
|
) : (
|
||||||
|
<div></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
</TitleContainer>
|
||||||
|
|
||||||
|
{data.visible &&
|
||||||
|
<Content>
|
||||||
|
<LabelColumn>
|
||||||
|
<p className='networkNode-label'>Конфигурация</p>
|
||||||
|
<MAC>
|
||||||
|
<Row className='networkNode-row'>
|
||||||
|
<p>MAC:</p>
|
||||||
|
<p>{data.networkData.mac}</p>
|
||||||
|
</Row>
|
||||||
|
<Row className='networkNode-row'>
|
||||||
|
<p>MTU:</p>
|
||||||
|
<p>{data.networkData.mtu}</p>
|
||||||
|
</Row>
|
||||||
|
</MAC>
|
||||||
|
</LabelColumn>
|
||||||
|
</Content>
|
||||||
|
}
|
||||||
|
|
||||||
|
{data.networkData.connection !== 'unknown' &&
|
||||||
|
<Handle type="source" position={Position.Left} id={`g0_${data.networkData.id}-source-left`} style={{ left: 0 }} />
|
||||||
|
}
|
||||||
|
</Network>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetWorkNode
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
$width?: string,
|
||||||
|
$height?: any
|
||||||
|
$bordertype?: string,
|
||||||
|
$bordercolor?: string
|
||||||
|
$backgroundcolor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Network = styled.div<Props>`
|
||||||
|
background-color: ${(props) => props.theme.primary};
|
||||||
|
border: 2px ${(props) => props.$bordertype} ${(props) => props.$bordercolor};
|
||||||
|
min-width: ${(props) => props.$width}px;
|
||||||
|
height: ${(props) => typeof props.$height === 'number' ? `${props.$height}px` : props.$height};
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
visibility: visible;
|
||||||
|
overflow: hidden;
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TitleContainer = styled.div<Props>`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: ${(props) => props.$backgroundcolor};
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
height: ${(props) => props.$height ? props.$height : 'unset'};
|
||||||
|
|
||||||
|
img{
|
||||||
|
width: 20px
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Device = styled.p`
|
||||||
|
font-size: 18px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Status = styled.p`
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
|
||||||
|
img{
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Content = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Row = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MAC = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: ${(props) => props.theme.secondary};
|
||||||
|
border-radius: 8px;
|
||||||
|
// box-shadow: 0px 0px 15px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.networkNode-row{
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const LabelColumn = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.networkNode-label{
|
||||||
|
font-size: 10px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
import { Port, Column } from './style';
|
||||||
|
|
||||||
|
interface CustomNodeProps {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
port: string
|
||||||
|
id: string;
|
||||||
|
style: {
|
||||||
|
width: any;
|
||||||
|
height: any;
|
||||||
|
borderType: any;
|
||||||
|
borderColor: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PortNode = ({ data }: CustomNodeProps) => {
|
||||||
|
return (
|
||||||
|
<Port $width={data.style.width} $height={Number(data.style.height)} $bordertype={data.style.borderType} $bordercolor={data.style.borderColor}>
|
||||||
|
|
||||||
|
<Column>
|
||||||
|
<p>{data.label}</p>
|
||||||
|
|
||||||
|
|
||||||
|
{data.port &&
|
||||||
|
<p>{data.port}</p>
|
||||||
|
}
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
|
||||||
|
{data.port ? (
|
||||||
|
<>
|
||||||
|
<Handle type="target" position={Position.Left} id={`${data.id}_target-left`} style={{ left: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Left} id={`${data.id}_target-left`} style={{ left: 0 }} />
|
||||||
|
</>
|
||||||
|
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Handle type="target" position={Position.Right} id={`${data.id}_target-right`} style={{ right: 0 }} />
|
||||||
|
<Handle type="source" position={Position.Right} id={`${data.id}source-right`} style={{ right: 0 }} />
|
||||||
|
</>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Port>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
$width: string,
|
||||||
|
$height?: any
|
||||||
|
$bordertype: string,
|
||||||
|
$bordercolor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Port = styled.div<Props>`
|
||||||
|
background-color: ${(props)=>props.theme.primary};
|
||||||
|
border: 2px ${(props) => props.$bordertype} ${(props) => props.$bordercolor};
|
||||||
|
width: ${(props) => props.$width}px;
|
||||||
|
height: ${(props) => typeof props.$height === 'number' ? `${props.$height}px` : props.$height};
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: visible;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Column = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
type ColorType = Record<string, string>;
|
||||||
|
|
||||||
|
export const NodeColors: ColorType = {
|
||||||
|
'0': '#c0c0c0',
|
||||||
|
'1': '#ef6e7d',
|
||||||
|
'2': '#f19e4c',
|
||||||
|
'3': '#e9c44c',
|
||||||
|
'4': '#51ce82',
|
||||||
|
'5': '#4cd2d0',
|
||||||
|
'6': '#a085f3',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
import { DefaultNode, PillNode, ParalelorgamNode, DiamondNode, CircleNode } from '../nodes/baseForm'
|
||||||
|
import { NodeGroupNode, NetWorkNode, IpGroupeNode, IpNode, PortNode } from '../nodes/networkInterface';
|
||||||
|
|
||||||
|
export const nodeTypes = {
|
||||||
|
custom: DefaultNode,
|
||||||
|
pill: PillNode,
|
||||||
|
parallelogram: ParalelorgamNode,
|
||||||
|
diamond: DiamondNode,
|
||||||
|
circle: CircleNode,
|
||||||
|
nodegroup: NodeGroupNode,
|
||||||
|
portNode: PortNode,
|
||||||
|
network: NetWorkNode,
|
||||||
|
ipgroup: IpGroupeNode,
|
||||||
|
ip: IpNode,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ModalWindow, ModalContainer } from "./style"
|
||||||
|
import { modalNames } from "../../const/modals/names"
|
||||||
|
|
||||||
|
export const WindowComponent = ()=>{
|
||||||
|
return(
|
||||||
|
<ModalWindow>
|
||||||
|
|
||||||
|
</ModalWindow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// components/modalContainers/NoteDevice.tsx
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
|
import Input from "../../UI/inputs/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {NetworkService} from "../../../api/request";
|
||||||
|
import useNetworkStore from "../../../store/useDeviceStore";
|
||||||
|
import RouterLoader from "../../UI/routerLoader";
|
||||||
|
import {useTheme} from "styled-components";
|
||||||
|
import {useIpStore} from "../../../store";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
address: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const ConnectNetwork = ({address, onClose}: Props) => {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const theme = useTheme();
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError('')
|
||||||
|
const res = await NetworkService.connectNetwork(ipAddress, password, address);
|
||||||
|
if(res.status === 200) {
|
||||||
|
onClose()
|
||||||
|
}else{
|
||||||
|
setError(res.data.detail)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connection:', error);
|
||||||
|
setError('Ошибка подключения к сети')
|
||||||
|
}finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContainer style={{ height: '230px' }}>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Подключение к сети</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholderText='Введите пароль...'
|
||||||
|
value={password}
|
||||||
|
id='deviceNote'
|
||||||
|
onChange={(value) => setPassword(value)}
|
||||||
|
/>
|
||||||
|
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
title="Отмена"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
color={theme.successColor}
|
||||||
|
hoverColor={theme.successColor}
|
||||||
|
title='Подключиться'
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
|
||||||
|
</ModalContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// components/modalContainers/NoteDevice.tsx
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
|
import Input from "../../UI/inputs/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {useIpStore} from "../../../store";
|
||||||
|
import {NetworkService} from "../../../api/request";
|
||||||
|
import useNetworkStore from "../../../store/useDeviceStore";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
address: string;
|
||||||
|
onClose: () => void;
|
||||||
|
currentNote?: string;
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
|
const [note, setNote] = useState(currentNote || '');
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const { devices, setDevices } = useNetworkStore();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
if (!currentNote && note.trim() !== '') {
|
||||||
|
await NetworkService.addNote(ipAddress, address, note, type);
|
||||||
|
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
|
||||||
|
await NetworkService.editNote(ipAddress, address, note, type);
|
||||||
|
}else if (currentNote && note.trim() === '') {
|
||||||
|
await NetworkService.deleteNote(ipAddress, address, type);
|
||||||
|
}else {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDevices = devices.map(device =>
|
||||||
|
device.mac === address
|
||||||
|
? { ...device, note: note.trim() }
|
||||||
|
: device
|
||||||
|
);
|
||||||
|
setDevices(updatedDevices);
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving note:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContainer style={{ height: '250px' }}>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholderText='Введите заметку...'
|
||||||
|
value={note}
|
||||||
|
id='deviceNote'
|
||||||
|
onChange={(value) => setNote(value)}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
title="Отмена"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
title='Сохранить'
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</ModalContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// components/modalContainers/NoteDevice.tsx
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
|
import Input from "../../UI/inputs/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {NetworkService} from "../../../api/request";
|
||||||
|
import useNetworkStore from "../../../store/useDeviceStore";
|
||||||
|
import {useIpStore} from "../../../store";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
address: string;
|
||||||
|
onClose: () => void;
|
||||||
|
currentNote?: string;
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
||||||
|
const [note, setNote] = useState(currentNote || '');
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
if (!currentNote && note.trim() !== '') {
|
||||||
|
await NetworkService.addNote(ipAddress, address, note, type);
|
||||||
|
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
|
||||||
|
await NetworkService.editNote(ipAddress, address, note, type);
|
||||||
|
}else if (currentNote && note.trim() === '') {
|
||||||
|
await NetworkService.deleteNote(ipAddress, address, type);
|
||||||
|
}else {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updatedNetworks = wifiNetworks.map(network =>
|
||||||
|
network.ssid === address
|
||||||
|
? { ...network, note: note.trim() }
|
||||||
|
: { ...network }
|
||||||
|
);
|
||||||
|
setWifiNetworks(updatedNetworks);
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving note:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContainer style={{ height: '220px' }}>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholderText='Введите заметку...'
|
||||||
|
value={note}
|
||||||
|
id='deviceNote'
|
||||||
|
onChange={(value) => setNote(value)}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
title="Отмена"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
title='Сохранить'
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</ModalContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import Input from "../../UI/inputs/input"
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
||||||
|
import { validateIPAddress } from "../../../utils/nodeController/validator";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSave?: (data: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddDevice = ({ onSave, onClose }: Props) => {
|
||||||
|
const [ipAddress, setIpAddress] = useState('')
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!validateIPAddress(ipAddress)) {
|
||||||
|
setError(true)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// e.preventDefault(); // Предотвращаем перезагрузку страницы
|
||||||
|
if (onSave && ipAddress) {
|
||||||
|
setError(false)
|
||||||
|
onSave(ipAddress);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContainer style={{ height: '230px' }}>
|
||||||
|
{/* <form onSubmit={handleSubmit}> */}
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Добавление узла</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholderText='Введите адрес узла...'
|
||||||
|
id="newIp"
|
||||||
|
value={ipAddress}
|
||||||
|
onChange={(value) => setIpAddress(value)}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||||
|
Некорректный адрес узла
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton type="button" size="small" title="Отмена" onClick={onClose} />
|
||||||
|
<MainButton type="submit" size="small" title="Сохранить" onClick={handleSubmit} />
|
||||||
|
</ButtonContainer>
|
||||||
|
{/* </form> */}
|
||||||
|
</ModalContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
||||||
|
import { useModalContext } from "../../../store"
|
||||||
|
import { useIpStore } from "../../../store"
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton"
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConnectionError = ({message}: Props)=>{
|
||||||
|
const {closeModal} = useModalContext()
|
||||||
|
const { setIpAddress } = useIpStore();
|
||||||
|
|
||||||
|
const close = ()=>{
|
||||||
|
setIpAddress('localhost')
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<ModalContainer>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Соединение прервано</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent>
|
||||||
|
<ModalText>
|
||||||
|
<h3>Произошёл разрыв с сервером</h3>
|
||||||
|
<p>Пожалуйста, проверьте доступ к адресу и попробуйте снова.</p>
|
||||||
|
</ModalText>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton title="Закрыть" onClick={close} size="small"/>
|
||||||
|
<MainButton title="Повторить" onClick={closeModal} size="small"/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</ModalContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
||||||
|
import { useModalContext } from "../../../store"
|
||||||
|
import { useIpStore } from "../../../store"
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton"
|
||||||
|
import {useState} from "react";
|
||||||
|
import {NetworkService} from "../../../api/request";
|
||||||
|
import RouterLoader from "../../UI/routerLoader";
|
||||||
|
import {useTheme} from "styled-components";
|
||||||
|
|
||||||
|
type Props={
|
||||||
|
address: string,
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisconnectConfirm = ({address, onClose}: Props)=>{
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const theme = useTheme()
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleDisconnect = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const res = await NetworkService.disconnectNetwork(ipAddress, address);
|
||||||
|
if(res.status === 200) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connection:', error);
|
||||||
|
}finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return(
|
||||||
|
<ModalContainer>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Отключение от Wi-Fi сети</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent>
|
||||||
|
<ModalText>
|
||||||
|
<h3>Подтверждение отключения</h3>
|
||||||
|
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
|
||||||
|
</ModalText>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton title="Отменить" onClick={onClose} size="small"/>
|
||||||
|
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
|
||||||
|
</ButtonContainer>
|
||||||
|
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
|
||||||
|
</ModalContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export {IpConfirm} from './ipConfirm'
|
||||||
|
export {ConnectionError} from './connectionError'
|
||||||
|
export {AddDevice} from './addDevice'
|
||||||
|
export {RightsConfirm} from './rightsConfirm'
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Loader from "../../UI/loader";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { changeIp } from '../../../const/types/types'
|
||||||
|
import { NetworkService } from '../../../api/request'
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer, ErrorMessage } from "../style"
|
||||||
|
import { useIpStore } from "../../../store";
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton";
|
||||||
|
import { useAdminStore } from "../../../store/adminStore";
|
||||||
|
import { useDevicesStore } from "../../../store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
ipData: changeIp
|
||||||
|
onClose: () => void;
|
||||||
|
smoothClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Information = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Info = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DataContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 35px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CurrentData = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const NewData = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
|
const [loader, setLoader] = useState(true)
|
||||||
|
const [newIP, setNewIp] = useState(null)
|
||||||
|
const [currentIp, setCurrentIP] = useState({})
|
||||||
|
const [checkStatus, setcheckStatus] = useState(false)
|
||||||
|
const [message, setMessgae] = useState('')
|
||||||
|
const { ipAddress, setIpAddress } = useIpStore();
|
||||||
|
const {isAdmin} = useAdminStore()
|
||||||
|
const { devices, addDevice, deleteDevice } = useDevicesStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ipData && isAdmin) {
|
||||||
|
const data = {
|
||||||
|
"connection_id": ipData.connection,
|
||||||
|
"ipv4_updates": {
|
||||||
|
"ipv4.method": "manual",
|
||||||
|
"ipv4.address-data": [
|
||||||
|
{
|
||||||
|
"address": ipData.newIp,
|
||||||
|
"prefix": ipData.prefix
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ipv4.gateway": ipData.newGate,
|
||||||
|
"ipv4.dns-data": ipData.newDns
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldData = {
|
||||||
|
"connection_id": ipData.connection,
|
||||||
|
"ipv4_updates": {
|
||||||
|
"ipv4.method": "manual",
|
||||||
|
"ipv4.address-data": [
|
||||||
|
{
|
||||||
|
"address": ipData.currentIp,
|
||||||
|
"prefix": ipData.prefix
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ipv4.gateway": ipData.currentGate,
|
||||||
|
"ipv4.dns-data": ipData.currentDns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentIP(oldData)
|
||||||
|
|
||||||
|
checkIp(data);
|
||||||
|
}
|
||||||
|
}, [ipData, isAdmin]);
|
||||||
|
|
||||||
|
const checkIp = async (data: any) => {
|
||||||
|
try {
|
||||||
|
const result = await NetworkService.checkIp(ipAddress, data);
|
||||||
|
setcheckStatus(result.data.available)
|
||||||
|
setMessgae(result.data.message)
|
||||||
|
if (result.data.available) {
|
||||||
|
setNewIp(data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveIp = async (data: any) => {
|
||||||
|
try {
|
||||||
|
const result = await NetworkService.saveIp(ipAddress, data);
|
||||||
|
if (result.status === 200) {
|
||||||
|
if(ipAddress!=='localhost'){
|
||||||
|
setIpAddress(ipData.newIp)
|
||||||
|
|
||||||
|
const updatedDevices = devices.map(device =>
|
||||||
|
device.address === ipData.currentIp
|
||||||
|
? { ...device, address: ipData.newIp }
|
||||||
|
: device
|
||||||
|
);
|
||||||
|
useDevicesStore.setState({ devices: updatedDevices });
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelIp = async (data: any) => {
|
||||||
|
try {
|
||||||
|
const result = await NetworkService.saveIp(ipAddress, data);
|
||||||
|
if (result.status === 200) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loader && isAdmin ? (
|
||||||
|
<Loader loaderText="Идет проверка корректности IP адреса" />
|
||||||
|
) : (
|
||||||
|
<ModalContainer {...(checkStatus ? { height: "unset" } : {})}>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Изменение IP-Адреса</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
{checkStatus ? (
|
||||||
|
<ModalContent>
|
||||||
|
<Information>
|
||||||
|
<h3>Общие сведения</h3>
|
||||||
|
<Info>
|
||||||
|
<p>Интерфейс: {ipData.device}</p>
|
||||||
|
<p>--{'>'}</p>
|
||||||
|
<p>{ipData.connection}</p>
|
||||||
|
</Info>
|
||||||
|
<h3>Сведения об IP</h3>
|
||||||
|
<DataContainer>
|
||||||
|
<CurrentData>
|
||||||
|
<p>Текущий ip: {ipData.currentIp}</p>
|
||||||
|
<p>Текущий шлюз: {ipData.currentGate}</p>
|
||||||
|
<p>Текущий DNS: {ipData.currentDns}</p>
|
||||||
|
</CurrentData>
|
||||||
|
|
||||||
|
<NewData>
|
||||||
|
<p>Новый ip: {ipData.newIp}</p>
|
||||||
|
<p>Новый шлюз: {ipData.newGate}</p>
|
||||||
|
<p>Новый DNS: {ipData.newDns}</p>
|
||||||
|
</NewData>
|
||||||
|
</DataContainer>
|
||||||
|
</Information>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton title="Отмена" size='small' onClick={() => cancelIp(currentIp)}/>
|
||||||
|
<MainButton title="Сохранить" size='small' onClick={() => saveIp(newIP)}/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</ModalContent>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalText>
|
||||||
|
{isAdmin ? (
|
||||||
|
<>
|
||||||
|
<h3>Произошла ошибка проверки корректности</h3>
|
||||||
|
<ErrorMessage>{message}</ErrorMessage>
|
||||||
|
</>
|
||||||
|
|
||||||
|
):(
|
||||||
|
<h3>Для изменения сетевых настроек необходимо авторизироваться под администратором</h3>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</ModalText>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton title="Закрыть" size='small' onClick={smoothClose}/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
||||||
|
import Input from "../../UI/inputs/input"
|
||||||
|
import { MainButton } from "../../UI/buttons/mainButton"
|
||||||
|
import { NetworkService } from "../../../api/request"
|
||||||
|
import { useIpStore } from "../../../store"
|
||||||
|
import { useAdminStore } from "../../../store/adminStore"
|
||||||
|
import { useCheckAdmin } from "../../../utils/checkAdmin"
|
||||||
|
|
||||||
|
import { buttonNames } from "../../../const/buttons/names"
|
||||||
|
import { IconComponent } from "../../../const/buttons/icons"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
|
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const { isAdmin, setAdmin } = useAdminStore()
|
||||||
|
const checkAdmin = useCheckAdmin();
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [success, setSuccess] = useState({
|
||||||
|
message: '',
|
||||||
|
status: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const [user, setUser] = useState({
|
||||||
|
username: 'operator',
|
||||||
|
password: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (id: string, value: string) => {
|
||||||
|
setUser(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
[id]: value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (user: object) => {
|
||||||
|
try {
|
||||||
|
const response = await NetworkService.login(ipAddress, user);
|
||||||
|
if(response.status === 200){
|
||||||
|
await checkAdmin();
|
||||||
|
setError(false)
|
||||||
|
setSuccess(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
message: 'Успешный вход',
|
||||||
|
status: true
|
||||||
|
}));
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
onClose();
|
||||||
|
}else{
|
||||||
|
setError(true)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await NetworkService.logout(ipAddress);
|
||||||
|
await checkAdmin();
|
||||||
|
setSuccess(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
message: 'Успешный выход',
|
||||||
|
status: true
|
||||||
|
}));
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContainer style={!isAdmin ? {height: '250px'} : {height: 'unset'}}>
|
||||||
|
{!success.status ? (
|
||||||
|
!isAdmin ? (
|
||||||
|
<>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Получение прав доступа</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
|
||||||
|
|
||||||
|
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>Включить административный доступ</h3>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholderText='Введите пароль администратора...'
|
||||||
|
id="password"
|
||||||
|
value={user.password}
|
||||||
|
onChange={(value) => handleChange('password', value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
||||||
|
Неверный пароль оператора
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton size="small" title="Отмена" onClick={onClose} />
|
||||||
|
<MainButton size="small" title="Отправить" onClick={() => login(user)} />
|
||||||
|
</ButtonContainer>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ModalTitle>
|
||||||
|
<h3>Переключиться на ограниченный доступ</h3>
|
||||||
|
</ModalTitle>
|
||||||
|
|
||||||
|
<ModalContent>
|
||||||
|
<p>В данном режиме доступа ограничиваются полномочия администратора.</p>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton size="small" title="Отмена" onClick={onClose} />
|
||||||
|
<MainButton size="small" title="Ограничить доступ" onClick={logout} />
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
):(
|
||||||
|
<ModalContainer style={{display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '15px'}}>
|
||||||
|
<IconComponent name={buttonNames[8]} width="50" height="50"/>
|
||||||
|
<h3>{success.message}</h3>
|
||||||
|
<p style={{fontSize: '12px'}}>Это окно можно закрыть, либо оно пропадёт само</p>
|
||||||
|
<MainButton size="small" title="Закрыть" onClick={onClose} style={{width: '30%'}}/>
|
||||||
|
</ModalContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</ModalContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useModalContext } from "../../store";
|
||||||
|
import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalContainers";
|
||||||
|
import { ModalMap } from "./windowsType";
|
||||||
|
import Loader from "../UI/loader";
|
||||||
|
import { ModalWindow } from "./style";
|
||||||
|
import {NoteDevice} from "./modalContainers/NoteDevice";
|
||||||
|
import { NoteWifi } from "./modalContainers/NoteWifi";
|
||||||
|
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
||||||
|
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean,
|
||||||
|
type: string
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WindowComponent({ isOpen, type, data }: Props) {
|
||||||
|
|
||||||
|
const { closeModal, closeLoader } = useModalContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isOpen &&
|
||||||
|
<ModalWindow>
|
||||||
|
{type == ModalMap['1'] && data &&
|
||||||
|
<IpConfirm
|
||||||
|
ipData={data}
|
||||||
|
onClose={closeModal}
|
||||||
|
smoothClose={closeLoader}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{type == ModalMap['2'] &&
|
||||||
|
<Loader loaderText={data} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{type === ModalMap['3'] &&
|
||||||
|
<ConnectionError
|
||||||
|
message={data.message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{type === ModalMap['6'] && (
|
||||||
|
<NoteDevice
|
||||||
|
type={'devices'}
|
||||||
|
address={data.address}
|
||||||
|
currentNote={data.currentNote}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['7'] && (
|
||||||
|
<NoteWifi
|
||||||
|
type={'wifi'}
|
||||||
|
address={data.address}
|
||||||
|
currentNote={data.currentNote}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['8'] && (
|
||||||
|
<ConnectNetwork
|
||||||
|
address={data.address}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['9'] && (
|
||||||
|
<DisconnectConfirm
|
||||||
|
address={data.address}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['4'] && (
|
||||||
|
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === ModalMap['5'] && (
|
||||||
|
<RightsConfirm onClose={closeLoader} />
|
||||||
|
)}
|
||||||
|
</ModalWindow >
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const ModalWindow = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ModalContainer = styled.div<{height?: string}>`
|
||||||
|
border-radius: 12px;
|
||||||
|
background: ${(props)=>props.theme.secondary};
|
||||||
|
color: ${(props=>props.theme.textColor)};
|
||||||
|
position: relative;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 15px;
|
||||||
|
height: ${(props) => props.height ? props.height : '230px'};
|
||||||
|
width: 520px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ModalTitle = styled.div`
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #9D9D9D;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h3{
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ModalContent = styled.div`
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
h3{
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p{
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ModalText = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ErrorMessage = styled.p`
|
||||||
|
color: #d95454;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ButtonContainer = styled.div`
|
||||||
|
justify-content: flex-end;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 20px 20px;
|
||||||
|
`
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export type ModalType = Record<string, string>;
|
||||||
|
|
||||||
|
export const ModalMap: ModalType = {
|
||||||
|
'1': 'ipConfirm',
|
||||||
|
'2': 'loader',
|
||||||
|
'3': 'connectionError',
|
||||||
|
'4': 'addDevice',
|
||||||
|
'5': 'sudo',
|
||||||
|
'6': 'noteDevice',
|
||||||
|
'7': 'noteWifi',
|
||||||
|
'8': 'connectNetwork',
|
||||||
|
'9': 'disconnectConfirm',
|
||||||
|
}
|
||||||
194
FRONT/BUILD/resources/app/src/components/register.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { ModalContainer, ModalTitle, ModalContent, ModalText } from "./modalwindows/style"
|
||||||
|
import Input from "./UI/inputs/input"
|
||||||
|
import { MainButton } from "./UI/buttons/mainButton"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useIpStore } from "../store"
|
||||||
|
import { NetworkService } from "../api/request"
|
||||||
|
import { IconComponent } from "../const/buttons/icons"
|
||||||
|
import { buttonNames } from "../const/buttons/names"
|
||||||
|
import ThemeButton from "./UI/buttons/toggletheme"
|
||||||
|
import About from "./workSpace/modules/appModules/about"
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
background: ${(props) => props.theme.background};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.form`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
align-items: center;
|
||||||
|
background: ${(props) => props.theme.primary};
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 18px;
|
||||||
|
width: 350px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Menu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Button = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
color: ${props => props.theme.text};
|
||||||
|
`
|
||||||
|
|
||||||
|
export const CreateUser = () => {
|
||||||
|
|
||||||
|
const { ipAddress, setIpAddress } = useIpStore();
|
||||||
|
const [repeatPassword, setRepeatPassword] = useState('');
|
||||||
|
const [passwordsMatch, setPasswordsMatch] = useState(true);
|
||||||
|
const [about, setAbout] = useState(false)
|
||||||
|
|
||||||
|
const [user, setUser] = useState({
|
||||||
|
username: 'operator',
|
||||||
|
password: '',
|
||||||
|
is_admin: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (id: string, value: string) => {
|
||||||
|
if (id === 'repeatPassword') {
|
||||||
|
setRepeatPassword(value);
|
||||||
|
setPasswordsMatch(user.password === value);
|
||||||
|
} else {
|
||||||
|
setUser(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
[id]: value
|
||||||
|
}));
|
||||||
|
if (id === 'password') {
|
||||||
|
setPasswordsMatch(value === repeatPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async (user: object) => {
|
||||||
|
try {
|
||||||
|
const result = await NetworkService.createUser(ipAddress, user);
|
||||||
|
if (result.status === 200) { window.location.reload() }
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (passwordsMatch) {
|
||||||
|
handleSave(user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const back = ()=>{
|
||||||
|
setIpAddress('localhost')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toAbout = ()=>{
|
||||||
|
setAbout(true)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
|
||||||
|
<>
|
||||||
|
{!about ? (
|
||||||
|
<Wrapper>
|
||||||
|
|
||||||
|
<Container onSubmit={handleSubmit}>
|
||||||
|
<Title>
|
||||||
|
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent>
|
||||||
|
<h3>Настройка оператора узла</h3>
|
||||||
|
{ipAddress != 'localhost' &&
|
||||||
|
<p>{ipAddress}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Content>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Перед началом работы необходимо создать пароль оператора приложения</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholderText='Введите пароль...'
|
||||||
|
id="password"
|
||||||
|
value={user.password}
|
||||||
|
onChange={(value) => handleChange('password', value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholderText='Повторите пароль...'
|
||||||
|
id="repeatPassword"
|
||||||
|
value={repeatPassword}
|
||||||
|
onChange={(value) => handleChange('repeatPassword', value)}
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
/>
|
||||||
|
{!passwordsMatch && (
|
||||||
|
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||||
|
Пароли не совпадают!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton style={{ width: '100%' }} title="Отправить" type="submit" />
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
{ipAddress != 'localhost' &&
|
||||||
|
<MainButton title="Назад" onClick={back} style={{marginRight: '10px'}}/>
|
||||||
|
}
|
||||||
|
<ThemeButton />
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[4]} width="30" height="30" onClick={toAbout}/>
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
</Wrapper>
|
||||||
|
): (
|
||||||
|
<About from="reg" onBack={() => setAbout(false)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
|
||||||
|
import { MainButton } from "../../../UI/buttons/mainButton"
|
||||||
|
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||||
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import ThemeButton from "../../../UI/buttons/toggletheme"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
from: 'reg' | 'helloWindow' | 'workSpace'
|
||||||
|
onBack?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: ${(props => props.theme.background)};
|
||||||
|
padding: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
max-width: 1200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 25px 50px;
|
||||||
|
background: ${(props => props.theme.secondary)};
|
||||||
|
border-radius: 18px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Listing = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const List = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
margin-left: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
font-size: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SubTitle = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const Links = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LinkColumn = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
const LinkBlock = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Copyright = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justifi-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MainTitle = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
img{
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const Text = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function About({ from, onBack }: Props) {
|
||||||
|
const { setWorkModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
|
const getBackHandler = () => {
|
||||||
|
switch (from) {
|
||||||
|
case 'reg':
|
||||||
|
return onBack
|
||||||
|
case 'helloWindow':
|
||||||
|
return () => setWorkModule(workSpaceNames[1])
|
||||||
|
case 'workSpace':
|
||||||
|
return () => setWorkModule(workSpaceNames[3])
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backHandler = getBackHandler()
|
||||||
|
const showBackButton = from !== 'reg' || (from === 'reg' && onBack)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Content>
|
||||||
|
|
||||||
|
<MainTitle>
|
||||||
|
<img src="./assets/images/gossys.png" alt="" />
|
||||||
|
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}><h1>О приложении</h1></div>
|
||||||
|
|
||||||
|
</MainTitle>
|
||||||
|
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
<p>Приложение ГоNets является минимально работоспособным кроссплатформенным продуктом (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.</p>
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
|
||||||
|
<Listing>
|
||||||
|
<p>Данное приложение умеет:</p>
|
||||||
|
<List>
|
||||||
|
<p>- Отображать в графическом виде активные сетевые ресурсы</p>
|
||||||
|
<p>- Настраивать сетевые параметры локального и удаленных сетевых ресурсов</p>
|
||||||
|
<p>- Отображать и изменять взаимодействие сетевых ресурсов с установленным Клиентом ГоNets</p>
|
||||||
|
</List>
|
||||||
|
</Listing>
|
||||||
|
|
||||||
|
<SubTitle>
|
||||||
|
<p>Программное обеспечение исключительно для ознакомления и домашнего использования.</p>
|
||||||
|
<p>Для коммерческого использования приобретите версию ГоNets-PRO (Подробная информация на странице продукта)</p>
|
||||||
|
</SubTitle>
|
||||||
|
|
||||||
|
<Links>
|
||||||
|
<LinkBlock>
|
||||||
|
<p>Ссылка на домашнюю страницу:</p>
|
||||||
|
<a href="https://gossys.dev/pages/projects/gonets">https://gossys.dev/pages/projects/gonets</a>
|
||||||
|
</LinkBlock>
|
||||||
|
|
||||||
|
<LinkBlock>
|
||||||
|
<p>Инструкции и информация:</p>
|
||||||
|
<a href="https://wiki.gossys.dev/ru/gonets">https://wiki.gossys.dev/ru/gonets</a>
|
||||||
|
</LinkBlock>
|
||||||
|
|
||||||
|
<LinkColumn>
|
||||||
|
<p>Распространяется под лицензией GosysPUB текущей версии.</p>
|
||||||
|
<LinkBlock>
|
||||||
|
<p>Полный текст актуальной лицензии:</p>
|
||||||
|
<a href="https://wiki.gossys.dev/ru/lic/GosysPUB">https://wiki.gossys.dev/ru/lic/GosysPUB</a>
|
||||||
|
</LinkBlock>
|
||||||
|
</LinkColumn>
|
||||||
|
|
||||||
|
</Links>
|
||||||
|
|
||||||
|
<Copyright>
|
||||||
|
<p>Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.</p>
|
||||||
|
</Copyright>
|
||||||
|
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
{showBackButton && backHandler && (
|
||||||
|
<MainButton
|
||||||
|
title="Назад"
|
||||||
|
onClick={backHandler}
|
||||||
|
style={{ marginRight: '10px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{from == 'reg' &&
|
||||||
|
<ThemeButton />
|
||||||
|
}
|
||||||
|
</ButtonContainer>
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { IconComponent } from "../../../../const/buttons/icons"
|
||||||
|
import { buttonNames } from "../../../../const/buttons/names"
|
||||||
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
|
import { useModalContext } from "../../../../store"
|
||||||
|
import { useAdminStore } from "../../../../store/adminStore"
|
||||||
|
import { ModalMap } from "../../../modalwindows/windowsType"
|
||||||
|
import { QuitButton } from "../../../UI/buttons/quitButton"
|
||||||
|
import { usePosStore } from "../../../../store/posStore"
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background-color: ${(props) => props.theme.primary};
|
||||||
|
padding: 0 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
svg{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Header = () => {
|
||||||
|
const {setNullPos} = usePosStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<ButtonContainer>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[11]} width="30" height="30" onClick={()=>setNullPos(true)}/>
|
||||||
|
</IconContainer>
|
||||||
|
|
||||||
|
<QuitButton size="small"/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { MainButton } from "../../../UI/buttons/mainButton"
|
||||||
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
|
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||||
|
import { buttonNames } from "../../../../const/buttons/names"
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${(props => props.theme.background)};
|
||||||
|
background-image: url('./assets/images/back1.svg');
|
||||||
|
background-size: 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: bottom;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 52px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 75px;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
h1{
|
||||||
|
font-size: 52px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const HelloWindow = () => {
|
||||||
|
|
||||||
|
const { setWorkModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Container className="unselectable">
|
||||||
|
<Title>
|
||||||
|
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
|
||||||
|
<ButtonWrapper>
|
||||||
|
<ButtonContainer>
|
||||||
|
<MainButton
|
||||||
|
title="Настройки сети"
|
||||||
|
icon={buttonNames[3]}
|
||||||
|
iconSize={{ width: '32', height: '32' }}
|
||||||
|
onClick={() => setWorkModule(workSpaceNames[3])}
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title="О программе"
|
||||||
|
icon={buttonNames[4]}
|
||||||
|
iconSize={{ width: '32', height: '32' }}
|
||||||
|
onClick={()=>setWorkModule(workSpaceNames[4])}
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
</ButtonWrapper>
|
||||||
|
</Container>
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
|
||||||
|
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||||
|
|
||||||
|
const ScanBtn = styled.button`
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 22px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: .4s ease;
|
||||||
|
&:hover{
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
interface ListUsersProps {
|
||||||
|
loading: boolean;
|
||||||
|
handleScan: ()=>void;
|
||||||
|
devices: DeviceUser[];
|
||||||
|
scanInitiated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
||||||
|
const activeDevices = devices.filter(device => device.status === 'online');
|
||||||
|
const inactiveDevices = devices.filter(device => device.status !== 'online');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||||
|
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
|
||||||
|
</ScanBtn>
|
||||||
|
|
||||||
|
{devices.length > 0 ? (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '70%',
|
||||||
|
gap: '20px',
|
||||||
|
}}>
|
||||||
|
{/* Колонка активных устройств */}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные устройства ({activeDevices.length})</h3>
|
||||||
|
{activeDevices.map((device, index) => (
|
||||||
|
<DeviceCard
|
||||||
|
key={index}
|
||||||
|
status="online"
|
||||||
|
device={device}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Колонка неактивных устройств */}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные устройства ({inactiveDevices.length})</h3>
|
||||||
|
{inactiveDevices.map((device, index) => (
|
||||||
|
<DeviceCard
|
||||||
|
key={index}
|
||||||
|
status="offline"
|
||||||
|
device={device}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : scanInitiated ? (
|
||||||
|
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListUsers;
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
|
||||||
|
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||||
|
import TableScanner from "../../../../../UI/tables/TableScanner";
|
||||||
|
|
||||||
|
const ScanBtn = styled.button`
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 22px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: .4s ease;
|
||||||
|
&:hover{
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
interface ListUsersProps {
|
||||||
|
loading: boolean;
|
||||||
|
handleScan: ()=>void;
|
||||||
|
devices: DeviceUser[];
|
||||||
|
scanInitiated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||||
|
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
|
||||||
|
</ScanBtn>
|
||||||
|
|
||||||
|
{devices.length > 0 ? (
|
||||||
|
<TableScanner devices={devices} />
|
||||||
|
) : scanInitiated ? (
|
||||||
|
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListUsers;
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { useIpStore } from "../../../../../store";
|
||||||
|
import styled, { keyframes } from "styled-components";
|
||||||
|
import { useState } 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";
|
||||||
|
|
||||||
|
type Option ={
|
||||||
|
value: number,
|
||||||
|
label: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Анимации
|
||||||
|
const ring1 = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ring2 = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ring3 = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
|
||||||
|
`;
|
||||||
|
const dotAnimation = keyframes`
|
||||||
|
0% { content: '.'; }
|
||||||
|
33% { content: '..'; }
|
||||||
|
66% { content: '...'; }
|
||||||
|
100% { content: '.'; }
|
||||||
|
`;
|
||||||
|
const pulse = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Стили
|
||||||
|
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: ${(props) => props.theme.background};
|
||||||
|
padding: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 24px;
|
||||||
|
transition: justify-content .4s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
const TitleWrapper = styled.h2`
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
width: max-content;
|
||||||
|
`
|
||||||
|
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
transition: .4s ease;
|
||||||
|
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
background: ${(props) => props.theme.accent};
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
&.center {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
|
||||||
|
}
|
||||||
|
&.ring1 {
|
||||||
|
opacity: 0.12;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ring2 {
|
||||||
|
opacity: 0.08;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
|
||||||
|
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ring3 {
|
||||||
|
opacity: 0.04;
|
||||||
|
width: 550px;
|
||||||
|
height: 550px;
|
||||||
|
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
|
||||||
|
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LoadingText = styled.div`
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 22px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
`;
|
||||||
|
const ScanningText = styled.span`
|
||||||
|
&::after {
|
||||||
|
content: '.';
|
||||||
|
animation: ${dotAnimation} 1.5s infinite;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export default function UserScanner() {
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { devices, setDevices } = useNetworkStore();
|
||||||
|
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
|
||||||
|
|
||||||
|
// Сканирование
|
||||||
|
const handleScan = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setScanInitiated(true);
|
||||||
|
const res = await NetworkService.scanUser(ipAddress);
|
||||||
|
console.log(res);
|
||||||
|
setDevices(res.data.devices || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка сканирования сети:", err);
|
||||||
|
setDevices([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Способ отображения
|
||||||
|
const viewOptions: Option[] = [
|
||||||
|
{ value: 1, label: 'Таблица' },
|
||||||
|
{ value: 2, label: 'Список' },
|
||||||
|
];
|
||||||
|
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
|
||||||
|
const handleViewChange = (option: Option) => {
|
||||||
|
setSelectedView(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||||
|
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование пользователей в сети</TitleWrapper>)}
|
||||||
|
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||||
|
<span className="center"></span>
|
||||||
|
<span className="ring1"></span>
|
||||||
|
<span className="ring2"></span>
|
||||||
|
<span className="ring3"></span>
|
||||||
|
</UserLocate>
|
||||||
|
|
||||||
|
{scanInitiated && !loading && (
|
||||||
|
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<LoadingText>
|
||||||
|
Сканирование сети<ScanningText />
|
||||||
|
</LoadingText>
|
||||||
|
) : selectedView.value == 1 ? (
|
||||||
|
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
|
||||||
|
):(
|
||||||
|
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import styled, { keyframes } from "styled-components";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { NetworkService } from "../../../../../api/request";
|
||||||
|
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
|
||||||
|
import ModalWifiScaner from "./modules/ModalWifiScaner";
|
||||||
|
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||||
|
import {useIpStore} from "../../../../../store";
|
||||||
|
|
||||||
|
// Анимации
|
||||||
|
const ring1 = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
|
||||||
|
`;
|
||||||
|
const ring2 = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
|
||||||
|
`;
|
||||||
|
const ring3 = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
|
||||||
|
`;
|
||||||
|
const dotAnimation = keyframes`
|
||||||
|
0% { content: '.'; }
|
||||||
|
33% { content: '..'; }
|
||||||
|
66% { content: '...'; }
|
||||||
|
100% { content: '.'; }
|
||||||
|
`;
|
||||||
|
const pulse = keyframes`
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.1); }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Стили
|
||||||
|
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content:center ;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: ${(props) => props.theme.background};
|
||||||
|
padding: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 24px;
|
||||||
|
transition: justify-content .4s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
`;
|
||||||
|
const TitleWrapper = styled.h2`
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
transition: .4s ease;
|
||||||
|
transform: scale(1);
|
||||||
|
.circel {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
background: ${(props) => props.theme.scannerColor};
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
&.center {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
|
||||||
|
}
|
||||||
|
&.ring1 {
|
||||||
|
opacity: 0.12;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ring2 {
|
||||||
|
opacity: 0.08;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
|
||||||
|
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ring3 {
|
||||||
|
opacity: 0.04;
|
||||||
|
width: 550px;
|
||||||
|
height: 550px;
|
||||||
|
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
|
||||||
|
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const LoadingText = styled.div`
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 22px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
`;
|
||||||
|
const ScanningText = styled.span`
|
||||||
|
&::after {
|
||||||
|
content: '.';
|
||||||
|
animation: ${dotAnimation} 1.5s infinite;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const ScanBtn = styled.button<{scanInitiated: boolean}>`
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 22px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: transform .4s ease;
|
||||||
|
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")};
|
||||||
|
&:hover{
|
||||||
|
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")} scale(1.03);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export function WifiScanner() {
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [scanInitiated, setScanInitiated] = useState(false);
|
||||||
|
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
||||||
|
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
|
||||||
|
|
||||||
|
const handleScan = async () => {
|
||||||
|
setWifiNetworks([]);
|
||||||
|
setCurrentNetwork(null)
|
||||||
|
setLoading(true);
|
||||||
|
setScanInitiated(true);
|
||||||
|
try {
|
||||||
|
const res = await NetworkService.scanWifi(ipAddress);
|
||||||
|
const networks = res.data.access_points || [];
|
||||||
|
setWifiNetworks(networks);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка сканирования сети:", err);
|
||||||
|
setWifiNetworks([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||||
|
<TitleWrapper>Сканирование Wi-Fi сети</TitleWrapper>
|
||||||
|
|
||||||
|
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||||
|
<span className="center circel"></span>
|
||||||
|
<span className="ring1 circel"></span>
|
||||||
|
<span className="ring2 circel"></span>
|
||||||
|
<span className="ring3 circel"></span>
|
||||||
|
{wifiNetworks.map((network) => (
|
||||||
|
<WifiDot
|
||||||
|
key={network.bssid}
|
||||||
|
network={network}
|
||||||
|
onSelect={setCurrentNetwork}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentNetwork && (
|
||||||
|
<ModalWifiScaner
|
||||||
|
onClose={() => setCurrentNetwork(null)}
|
||||||
|
network={currentNetwork}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</UserLocate>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<LoadingText>
|
||||||
|
Сканирование сети <ScanningText/>
|
||||||
|
</LoadingText>
|
||||||
|
) : (
|
||||||
|
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
|
||||||
|
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
|
||||||
|
</ScanBtn>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {WifiNetworkScanner} from "./WifiDot";
|
||||||
|
import {IconComponent} from "../../../../../../const/buttons/icons";
|
||||||
|
import {buttonNames} from "../../../../../../const/buttons/names";
|
||||||
|
import {ModalMap} from "../../../../../modalwindows/windowsType";
|
||||||
|
import {useIpStore, useModalContext} from "../../../../../../store";
|
||||||
|
import {Status} from "../../../../../customeNodes/nodes/networkInterface/networkNode/style";
|
||||||
|
import SignalSVG from "../../../../../UI/signalSvg";
|
||||||
|
import { useTheme } from 'styled-components';
|
||||||
|
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||||
|
import {NetworkService} from "../../../../../../api/request";
|
||||||
|
|
||||||
|
const Modal = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: ${(props) => props.theme.secondary};
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 32px 28px;
|
||||||
|
bottom: -40%;
|
||||||
|
left: 220px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
min-width: 300px;
|
||||||
|
&:hover .close-btn{
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const CloseBtn = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
right: -20px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: ${(props) => props.theme.primary};
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
transition: all .4s ease;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) => props.theme.hoverBtn};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const ModalTitle = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
h4{
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.functionalIcons{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const ModalContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
const ModalRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
h6{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
max-width:180px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const ModalConnect = styled.button`
|
||||||
|
color: ${(props) => props.theme.primary};
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
background-color: ${(props) => props.theme.accent};
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .4s ease;
|
||||||
|
&:hover{
|
||||||
|
background-color: ${(props) => props.theme.accentHover};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
|
||||||
|
const { openModal } = useModalContext();
|
||||||
|
const { ipAddress } = useIpStore();
|
||||||
|
const theme = useTheme()
|
||||||
|
const [signal, setSignal] = useState(1);
|
||||||
|
const { wifiNetworks } = useNetworkStore();
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await NetworkService.statusNetwork(ipAddress);
|
||||||
|
const { status, ssid } = response.data;
|
||||||
|
setIsConnected(
|
||||||
|
!!network?.ssid &&
|
||||||
|
ssid === network.ssid &&
|
||||||
|
status === 'connected'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setIsConnected(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getStatus();
|
||||||
|
}, [network?.ssid, ipAddress]);
|
||||||
|
|
||||||
|
const currentNetwork = wifiNetworks.find(n => n.bssid === network.bssid) || network;
|
||||||
|
const openModalNote = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openModal(ModalMap['7'], {
|
||||||
|
address: currentNetwork.ssid,
|
||||||
|
currentNote: currentNetwork.note || ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModalNetwork= (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if(isConnected){
|
||||||
|
openModal(ModalMap['9'], {
|
||||||
|
address: currentNetwork.ssid,
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
openModal(ModalMap['8'], {
|
||||||
|
address: currentNetwork.ssid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const perc = Number(network.signal);
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case perc >= -50: setSignal(5); break;
|
||||||
|
case perc >= -60: setSignal(4); break;
|
||||||
|
case perc >= -70: setSignal(3); break;
|
||||||
|
case perc >= -80: setSignal(2); break;
|
||||||
|
default: setSignal(1); break;
|
||||||
|
}
|
||||||
|
}, [network.signal])
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<CloseBtn className={'close-btn'} onClick={onClose}>
|
||||||
|
<IconComponent name={buttonNames[12]} width="30" height="30" />
|
||||||
|
</CloseBtn>
|
||||||
|
<ModalTitle>
|
||||||
|
<h4>{network.ssid}</h4>
|
||||||
|
<div className={'functionalIcons'}>
|
||||||
|
|
||||||
|
<span onClick={openModalNote} style={{cursor: "pointer"}}>
|
||||||
|
<IconComponent name={buttonNames[16]} width="30" height="30" />
|
||||||
|
</span>
|
||||||
|
<Status>
|
||||||
|
<SignalSVG signal={signal} size={'36px'} color={theme.accent} />
|
||||||
|
</Status>
|
||||||
|
</div>
|
||||||
|
</ModalTitle>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>BSSID:</h6>
|
||||||
|
<p>{network.bssid}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Производитель:</h6>
|
||||||
|
<p>{network.vendor}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Сигнал:</h6>
|
||||||
|
<p>{network.signal} dBm</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Канал:</h6>
|
||||||
|
<p>{network.channel}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Шифрование:</h6>
|
||||||
|
<p>{network.encryption}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Частота:</h6>
|
||||||
|
<p>{network.frequency}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Скрытая сеть:</h6>
|
||||||
|
<p>{network.hidden ? 'Да' : 'Нет'}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalRow>
|
||||||
|
<h6>Заметка:</h6>
|
||||||
|
<p>{currentNetwork.note ? currentNetwork.note : '-'}</p>
|
||||||
|
</ModalRow>
|
||||||
|
<ModalConnect onClick={openModalNetwork}>{isConnected ? 'Отключиться' : 'Подключиться'}</ModalConnect>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalWifiScaner;
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export interface WifiNetworkScanner {
|
||||||
|
ssid: string;
|
||||||
|
bssid: string;
|
||||||
|
channel: number;
|
||||||
|
signal: number;
|
||||||
|
encryption: string;
|
||||||
|
frequency: string;
|
||||||
|
vendor: string;
|
||||||
|
hidden: boolean;
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Точка вайфай
|
||||||
|
const Dot = styled.div<{ top: string; left: string }>`
|
||||||
|
position: absolute;
|
||||||
|
top: ${props => props.top};
|
||||||
|
left: ${props => props.left};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
user-select: none;
|
||||||
|
transition: .4s;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WifiDot = ({ network, onSelect }: { network: WifiNetworkScanner, onSelect: (n: WifiNetworkScanner) => void }) => {
|
||||||
|
const getPositionInRing = (bssid: string, ringSize: number) => {
|
||||||
|
const hash = bssid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
const randomAngle = (hash % 360) * (Math.PI / 180);
|
||||||
|
const innerRadius = ringSize * 0.4;
|
||||||
|
const outerRadius = ringSize * 0.5;
|
||||||
|
const randomValue = (hash % 1000) / 1000;
|
||||||
|
const randomRadius = Math.sqrt(innerRadius * innerRadius + randomValue * (outerRadius * outerRadius - innerRadius * innerRadius));
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.cos(randomAngle) * randomRadius,
|
||||||
|
y: Math.sin(randomAngle) * randomRadius
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPosition = (signal: number, bssid: string) => {
|
||||||
|
let strength: 1 | 2 | 3;
|
||||||
|
if (signal >= -50) {
|
||||||
|
strength = 3;
|
||||||
|
} else if (signal >= -70) {
|
||||||
|
strength = 2;
|
||||||
|
} else if (signal >= -90) {
|
||||||
|
strength = 1;
|
||||||
|
} else {
|
||||||
|
strength = 1;
|
||||||
|
}
|
||||||
|
const ringSizes = { 1: 550, 2: 400, 3: 250 };
|
||||||
|
const ringSize = ringSizes[strength];
|
||||||
|
const offset = getPositionInRing(bssid, ringSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: `calc(50% - ${offset.y}px)`,
|
||||||
|
left: `calc(50% - ${offset.x}px)`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const position = getPosition(network.signal, network.bssid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
|
||||||
|
{network.ssid}
|
||||||
|
</Dot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WifiDot;
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
|
import { useTheme } from 'styled-components';
|
||||||
|
import { usePosStore } from '../../../../store/posStore';
|
||||||
|
|
||||||
|
import { ReactFlow, useNodesState, useEdgesState, Background, addEdge, applyEdgeChanges, Controls } from '@xyflow/react';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import '../../../../assets/css/flow.css'
|
||||||
|
|
||||||
|
import { nodeTypes } from '../../../customeNodes/types/types';
|
||||||
|
import { ModalMap } from '../../../modalwindows/windowsType';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useGroupStore,
|
||||||
|
useModalContext,
|
||||||
|
usePannableStore
|
||||||
|
} from '../../../../store'
|
||||||
|
|
||||||
|
import {
|
||||||
|
autosavePosition,
|
||||||
|
resetPosition,
|
||||||
|
processNodeChanges,
|
||||||
|
initNetworkIntData,
|
||||||
|
groupSort,
|
||||||
|
collapseGroup,
|
||||||
|
updateWifiData,
|
||||||
|
useInitCanvas
|
||||||
|
} from '../../../../utils/nodeController';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ResponseData,
|
||||||
|
ResponseNetwork,
|
||||||
|
IEdge,
|
||||||
|
Interfaces,
|
||||||
|
} from '../../../../const/types/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
startConnection: boolean
|
||||||
|
ws: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const proOptions = { hideAttribution: true, };
|
||||||
|
|
||||||
|
export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
||||||
|
|
||||||
|
const initializationAttempt = useRef(0);
|
||||||
|
const [requestError, setRequestError] = useState({ status: false, message: '' })
|
||||||
|
|
||||||
|
const { openModal, closeLoader } = useModalContext();
|
||||||
|
const [loader, setLoader] = useState(true)
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [socket, setSocket] = useState<WebSocket | null>(null);
|
||||||
|
const [data, setData] = useState<ResponseData[]>([]);
|
||||||
|
const [wifiData, setWifi] = useState<ResponseNetwork>()
|
||||||
|
const [hasInited, setHasInited] = useState(false);
|
||||||
|
const { initedNodes, initedEdges, handleInit } = useInitCanvas();
|
||||||
|
|
||||||
|
const [interfaces, setInterfaces] = useState<Interfaces[]>([]);
|
||||||
|
const [edgeData, setEdgeData] = useState<IEdge[]>([]);
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([] as any[]);
|
||||||
|
const [edges, setEdges] = useEdgesState([] as any[]);
|
||||||
|
const { isPanDisabled } = usePannableStore()
|
||||||
|
|
||||||
|
const [changeArray, setChangeArray] = useState<any[]>([]);
|
||||||
|
const { groups } = useGroupStore();
|
||||||
|
const {isNullPos, setNullPos} = usePosStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startConnection) {
|
||||||
|
|
||||||
|
openModal(ModalMap['2'], 'Идет загрузка данных');
|
||||||
|
|
||||||
|
setNodes([])
|
||||||
|
setEdges([])
|
||||||
|
setHasInited(false)
|
||||||
|
initializationAttempt.current = 0;
|
||||||
|
|
||||||
|
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
||||||
|
|
||||||
|
let connectionTimeout;
|
||||||
|
setSocket(newSocket);
|
||||||
|
|
||||||
|
connectionTimeout = setTimeout(() => {
|
||||||
|
if (newSocket.readyState !== WebSocket.OPEN) {
|
||||||
|
newSocket.close();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
newSocket.onopen = () => {
|
||||||
|
console.log('WebSocket connection established');
|
||||||
|
};
|
||||||
|
|
||||||
|
newSocket.onmessage = (event) => {
|
||||||
|
const newData = JSON.parse(event.data);
|
||||||
|
|
||||||
|
setData(() => [
|
||||||
|
newData.activation,
|
||||||
|
newData.deactivation,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
newSocket.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
newSocket.onclose = () => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
setRequestError({
|
||||||
|
status: true,
|
||||||
|
message: 'Соединение с сервером разорвано'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
newSocket.close();
|
||||||
|
closeLoader()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [startConnection, ws]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.length > 0) {
|
||||||
|
const { interfaces: newInterfaces, edgeData: newEdgeData } = initNetworkIntData(data);
|
||||||
|
const activationData = data[0].interfaces;
|
||||||
|
|
||||||
|
const wifi = activationData.find((el: any) => el.type === 'WIFI');
|
||||||
|
|
||||||
|
setInterfaces(newInterfaces);
|
||||||
|
setEdgeData(newEdgeData);
|
||||||
|
if (wifi) { setWifi(wifi); }
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (interfaces.length > 0 && !hasInited) {
|
||||||
|
handleInit(edgeData, interfaces)
|
||||||
|
setHasInited(true)
|
||||||
|
}
|
||||||
|
}, [interfaces, edgeData, hasInited])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializationAttempt.current === 0 && !requestError.status) {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
|
||||||
|
if(initedNodes.length> 0 && initedNodes.length<=4){
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initedNodes.length > 0 && initedEdges.length > 0) {
|
||||||
|
setNodes(initedNodes);
|
||||||
|
setEdges(initedEdges);
|
||||||
|
console.log("Ноды и эджи успешно инициализированы");
|
||||||
|
initializationAttempt.current = 1;
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
} else {
|
||||||
|
console.log("Попытка инициализации нод и эджей...");
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
console.error("Ошибка: слишком долгая загрузка данных");
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
setRequestError({
|
||||||
|
status: true,
|
||||||
|
message: 'Превышено время ожидания'
|
||||||
|
})
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [initedNodes, initedEdges, requestError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
initializationAttempt.current = 0;
|
||||||
|
} else {
|
||||||
|
initializationAttempt.current = 1;
|
||||||
|
}
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((nodes.length > 0 && loader == true)) {
|
||||||
|
setLoader(false)
|
||||||
|
closeLoader()
|
||||||
|
}
|
||||||
|
}, [nodes, loader])
|
||||||
|
|
||||||
|
const onEdgesChange = useCallback(
|
||||||
|
(changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connection: any) => setEdges((eds) => addEdge(connection, eds)),
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodesChange = (changes: any) => {
|
||||||
|
onNodesChange(changes);
|
||||||
|
|
||||||
|
const newData = processNodeChanges(changes, data);
|
||||||
|
|
||||||
|
if (newData) {
|
||||||
|
setChangeArray((prevArray) => [...prevArray, newData]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
|
autosavePosition(changeArray, socket);
|
||||||
|
}
|
||||||
|
setChangeArray([]);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [changeArray, socket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sortedGroups = groupSort(groups, nodes)
|
||||||
|
setNodes(collapseGroup(sortedGroups, nodes));
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wifiData) {
|
||||||
|
setNodes(updateWifiData(wifiData, nodes));
|
||||||
|
}
|
||||||
|
}, [wifiData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestError.status) {
|
||||||
|
setTimeout(() => {
|
||||||
|
openModal(ModalMap['3'], {
|
||||||
|
message: requestError.message,
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, [requestError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(isNullPos){
|
||||||
|
if(socket && socket.readyState === WebSocket.OPEN){
|
||||||
|
resetPosition(socket, setNullPos, nodes, setNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isNullPos]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={handleNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
style={{ background: '#F7F9FB' }}
|
||||||
|
minZoom={0}
|
||||||
|
maxZoom={4}
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{ padding: 0.1 }}
|
||||||
|
attributionPosition="bottom-left"
|
||||||
|
snapToGrid
|
||||||
|
snapGrid={[20, 20]}
|
||||||
|
panOnDrag={!isPanDisabled}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
proOptions={proOptions}
|
||||||
|
>
|
||||||
|
<Background gap={20} size={1} bgColor={theme.background} />
|
||||||
|
<Controls fitViewOptions={{ padding: 0.1 }} />
|
||||||
|
</ReactFlow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ToolBar } from "./toolBar/toolBar"
|
||||||
|
import { ToolSpace } from "./toolSpace/toolSpace"
|
||||||
|
import styled from "styled-components"
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SideBar = ()=>{
|
||||||
|
return(
|
||||||
|
<Container>
|
||||||
|
<ToolBar/>
|
||||||
|
<ToolSpace/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { IconComponent as Icon } from "../../../../const/menus/sideBar/icons"
|
||||||
|
import { useToolBar } from "../../../../hooks/useToolBar"
|
||||||
|
import { useTheme } from "styled-components"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
menuId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ToolBarButton = ({ menuId }: Props) => {
|
||||||
|
const { isActive, collapseMenu } = useToolBar(menuId);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const iconColor = isActive ? theme.textColor : theme.active;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={collapseMenu}>
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name={menuId} width="30" height="30" setColor={iconColor} />
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { useToolBarStore } from "../../../../store"
|
||||||
|
import { ToolBarButton } from "./toolBar-button"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import ThemeButton from "../../../UI/buttons/toggletheme"
|
||||||
|
import { IconComponent } from "../../../../const/buttons/icons"
|
||||||
|
import { buttonNames } from "../../../../const/buttons/names"
|
||||||
|
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||||
|
|
||||||
|
import { useModalContext } from "../../../../store"
|
||||||
|
import { useAdminStore } from "../../../../store/adminStore"
|
||||||
|
import { ModalMap } from "../../../modalwindows/windowsType"
|
||||||
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: ${(props) => props.theme.primary};
|
||||||
|
padding: 5px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Button = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
color: ${props => props.theme.text};
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ToolBar = () => {
|
||||||
|
const { toolBar } = useToolBarStore()
|
||||||
|
const toolBarMenu = toolBar?.toolSpace
|
||||||
|
|
||||||
|
const { openModal } = useModalContext()
|
||||||
|
const { isAdmin } = useAdminStore()
|
||||||
|
const { setWorkModule, workModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<ButtonContainer>
|
||||||
|
{toolBarMenu && toolBarMenu.map((menu) => (
|
||||||
|
<ToolBarButton menuId={menu.id} key={menu.id} />
|
||||||
|
))}
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[5])} />
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[15]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[6])} />
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ThemeButton />
|
||||||
|
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[4]} width="30" height="30"
|
||||||
|
onClick={() => workModule === workSpaceNames[3]
|
||||||
|
? setWorkModule(workSpaceNames[2])
|
||||||
|
: setWorkModule(workSpaceNames[4])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useModalContext } from "../../../../store"
|
||||||
|
import { ModalMap } from "../../../modalwindows/windowsType"
|
||||||
|
import { NetworkService } from "../../../../api/request"
|
||||||
|
import { AnimDot } from "../../../UI/animDot"
|
||||||
|
import { useMenu } from "../../../../hooks/useMenu"
|
||||||
|
import { useIpStore, useDevicesStore } from "../../../../store"
|
||||||
|
import { menusNames } from "../../../../const/menus/othersMenus/names"
|
||||||
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
|
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||||
|
import { IconComponent } from "../../../../const/buttons/icons"
|
||||||
|
import { buttonNames } from "../../../../const/buttons/names"
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
width: 210px;
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
padding: 5px 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid ${props => props.theme.text};
|
||||||
|
`
|
||||||
|
|
||||||
|
const DeviceBlock = styled.div`
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid ${props => props.theme.accent};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.paddingP{
|
||||||
|
padding: 7px 5px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DeviceControl = styled.div`
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: ${props => props.theme.accent};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PlusBlock = styled.div`
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px dotted ${props => props.theme.accent};
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Text = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DevicesContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DevicesHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.hControl{
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DevicesContent = styled.div<{ $isactive: string }>`
|
||||||
|
${props => props.$isactive === 'true' && `display: flex;`}
|
||||||
|
${props => props.$isactive === 'false' && `display: none;`}
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
interface CurrentDevice {
|
||||||
|
hostname: string
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DevicesBar = () => {
|
||||||
|
const { openModal } = useModalContext();
|
||||||
|
const { setIpAddress, ipAddress } = useIpStore();
|
||||||
|
const { devices, addDevice, deleteDevice } = useDevicesStore()
|
||||||
|
const { isOpen, collapseMenu } = useMenu(menusNames[1]);
|
||||||
|
const { setWorkModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
|
const [currentDevice, setCurrentDevice] = useState<CurrentDevice>({
|
||||||
|
address: 'localhost',
|
||||||
|
hostname: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
testSocket.close();
|
||||||
|
resolve(false);
|
||||||
|
}, 500
|
||||||
|
);
|
||||||
|
|
||||||
|
testSocket.onopen = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
testSocket.close();
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
testSocket.onerror = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
testSocket.close();
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchHostame = async () => {
|
||||||
|
let response = await NetworkService.getHostname();
|
||||||
|
if (response.status === 200) {
|
||||||
|
setCurrentDevice(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
hostname: response.data.hostname
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchHostame();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
openModal(ModalMap['4'], {
|
||||||
|
onSave: handleAddDeviceComplete,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddDeviceComplete = (data: string) => {
|
||||||
|
addDevice(data);
|
||||||
|
switchWs(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (number: number, adress: string) => {
|
||||||
|
deleteDevice(number)
|
||||||
|
|
||||||
|
|
||||||
|
if(adress == ipAddress){
|
||||||
|
setIpAddress('localhost')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchWs = async (ip: string) => {
|
||||||
|
const isAvailable = await checkWebSocketAvailability(ip);
|
||||||
|
if (!isAvailable) {
|
||||||
|
openModal(ModalMap['3'], {
|
||||||
|
message: `Не удалось подключиться к серверу по адресу ${ip}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIpAddress(ip);
|
||||||
|
setWorkModule(workSpaceNames[3]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (devices.length === 0) {
|
||||||
|
setIpAddress('localhost');
|
||||||
|
}
|
||||||
|
}, [devices]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>
|
||||||
|
<p className="unselectable">Доступные узлы</p>
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<DeviceBlock>
|
||||||
|
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
|
||||||
|
<p>Корневой адрес</p>
|
||||||
|
</DeviceControl>
|
||||||
|
{currentDevice.hostname == '' ? (
|
||||||
|
<Text className="paddingP">Название: Загрузка<AnimDot /></Text>
|
||||||
|
) : (
|
||||||
|
<p className="paddingP">Название: {currentDevice.hostname}</p>
|
||||||
|
)}
|
||||||
|
</DeviceBlock>
|
||||||
|
|
||||||
|
<DevicesContainer>
|
||||||
|
<DevicesHeader className="unselectable" onClick={collapseMenu}>
|
||||||
|
<p>Дополнительные узлы</p>
|
||||||
|
<IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]} />
|
||||||
|
</DevicesHeader>
|
||||||
|
|
||||||
|
<DevicesContent $isactive={isOpen.toString()} >
|
||||||
|
{devices.length > 0 &&
|
||||||
|
devices.map((device, index) => (
|
||||||
|
<DeviceBlock key={index}>
|
||||||
|
<DeviceControl className="unselectable" onClick={() => switchWs(device.address)}>
|
||||||
|
<p>Узел {device.number}</p>
|
||||||
|
<IconComponent
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
name={buttonNames[2]}
|
||||||
|
setColor="white"
|
||||||
|
onClick={(e: any) => {
|
||||||
|
e.stopPropagation(); // Предотвращаем всплытие события
|
||||||
|
handleDelete(device.number, device.address);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DeviceControl>
|
||||||
|
<p className="paddingP">Адрес: {device.address}</p>
|
||||||
|
</DeviceBlock>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
<PlusBlock className="unselectable" onClick={handleAdd}>
|
||||||
|
+
|
||||||
|
</PlusBlock>
|
||||||
|
</DevicesContent>
|
||||||
|
|
||||||
|
</DevicesContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useToolBarStore } from "../../../../store"
|
||||||
|
import { useToolBar } from "../../../../hooks/useToolBar"
|
||||||
|
import { Space } from "../../../../const/menus/sideBar/components"
|
||||||
|
import styled from "styled-components"
|
||||||
|
|
||||||
|
const Container = styled.div<{isOpen: boolean}>`
|
||||||
|
background-color: ${(props) => props.theme.secondary};
|
||||||
|
width: 250px;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 3;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
|
||||||
|
transition: max-width .4s ease;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ToolSpace = () => {
|
||||||
|
const { toolBar } = useToolBarStore()
|
||||||
|
const name = toolBar?.current || ''
|
||||||
|
const { isActive, isOpen } = useToolBar(name)
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container isOpen={isOpen}>
|
||||||
|
<Space name={name} />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
101
FRONT/BUILD/resources/app/src/components/workSpace/workSpace.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { HelloWindow } from "./modules/appModules/helloWindow";
|
||||||
|
import { NetworkInterfaces } from "./modules/workModules/networkInterfaces";
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { useIpStore, useToolBarStore } from "../../store";
|
||||||
|
import { SideBar } from "./sideBar/sideBar";
|
||||||
|
import { useWorkSpaceStore } from "../../store";
|
||||||
|
import { workSpaceNames } from "../../const/workspace/names";
|
||||||
|
import { Header } from "./modules/appModules/header";
|
||||||
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
|
import About from "./modules/appModules/about";
|
||||||
|
import UserScanner from "./modules/appModules/usersScanner/userScanner";
|
||||||
|
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
|
||||||
|
|
||||||
|
const Section = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FlowContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const WorkSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function WorkSpace() {
|
||||||
|
const { ipAddress, setIpAddress } = useIpStore();
|
||||||
|
const { workModule } = useWorkSpaceStore()
|
||||||
|
const [currentWs, setCurrentWs] = useState(ipAddress);
|
||||||
|
const initMenus = useToolBarStore((state) => state.initialize);
|
||||||
|
const initModule = useWorkSpaceStore((state) => state.initialize)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem('toolBar-storage')) {
|
||||||
|
initMenus();
|
||||||
|
}
|
||||||
|
}, [initMenus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem('workSpace-storage')) {
|
||||||
|
initModule();
|
||||||
|
}
|
||||||
|
}, [initModule]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentWs(ipAddress);
|
||||||
|
}, [ipAddress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIpAddress(currentWs);
|
||||||
|
}, [currentWs]);
|
||||||
|
|
||||||
|
localStorage.getItem('workSpace-storage')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<FlowContainer>
|
||||||
|
<SideBar />
|
||||||
|
|
||||||
|
{workModule === workSpaceNames[1] && <HelloWindow />}
|
||||||
|
|
||||||
|
{workModule === workSpaceNames[2] && (
|
||||||
|
<About
|
||||||
|
from={"workSpace"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{workModule === workSpaceNames[4] && (
|
||||||
|
<About
|
||||||
|
from={"helloWindow"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{workModule === workSpaceNames[5] && (
|
||||||
|
<UserScanner />
|
||||||
|
)}
|
||||||
|
{workModule === workSpaceNames[6] && (
|
||||||
|
<WifiScanner />
|
||||||
|
)}
|
||||||
|
{workModule === workSpaceNames[3] &&
|
||||||
|
<WorkSection>
|
||||||
|
<Header />
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<NetworkInterfaces
|
||||||
|
key={currentWs}
|
||||||
|
startConnection={true}
|
||||||
|
ws={currentWs}
|
||||||
|
/>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
</WorkSection>
|
||||||
|
}
|
||||||
|
</FlowContainer>
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
63
FRONT/BUILD/resources/app/src/const/buttons/icons.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React from "react";
|
||||||
|
import { buttonNames } from "./names";
|
||||||
|
import { useTheme } from "styled-components";
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
import exitToApp from '@iconify/icons-ic/twotone-exit-to-app';
|
||||||
|
import deleteIcon from '@iconify/icons-ic/baseline-delete';
|
||||||
|
import settings from '@iconify/icons-ic/baseline-settings';
|
||||||
|
import info from '@iconify/icons-ic/baseline-info';
|
||||||
|
import arrowDown from '@iconify/icons-ic/baseline-keyboard-arrow-down';
|
||||||
|
import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
|
||||||
|
import edit from '@iconify/icons-ic/baseline-edit';
|
||||||
|
import checkCircle from '@iconify/icons-ic/round-check-circle';
|
||||||
|
import cancel from '@iconify/icons-ic/baseline-cancel';
|
||||||
|
import userSearch from '@iconify/icons-ic/baseline-person-search';
|
||||||
|
import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
|
||||||
|
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
|
||||||
|
import refresh from '@iconify/icons-ic/baseline-refresh';
|
||||||
|
import close from '@iconify/icons-ic/round-close';
|
||||||
|
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
|
||||||
|
import noteEdit from '@iconify/icons-ph/note-pencil';
|
||||||
|
|
||||||
|
export const ICONS = {
|
||||||
|
[buttonNames[1]]: exitToApp,
|
||||||
|
[buttonNames[2]]: deleteIcon,
|
||||||
|
[buttonNames[3]]: settings,
|
||||||
|
[buttonNames[4]]: info,
|
||||||
|
[buttonNames[5]]: arrowDown,
|
||||||
|
[buttonNames[6]]: arrowRight,
|
||||||
|
[buttonNames[7]]: edit,
|
||||||
|
[buttonNames[8]]: checkCircle,
|
||||||
|
[buttonNames[9]]: cancel,
|
||||||
|
[buttonNames[10]]: admin,
|
||||||
|
[buttonNames[11]]: refresh,
|
||||||
|
[buttonNames[12]]: close,
|
||||||
|
[buttonNames[13]]: noAdmin,
|
||||||
|
[buttonNames[14]]: userSearch,
|
||||||
|
[buttonNames[15]]: wifiFind,
|
||||||
|
[buttonNames[16]]: noteEdit,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type IconName = keyof typeof ICONS;
|
||||||
|
|
||||||
|
interface IconsProps {
|
||||||
|
name: IconName,
|
||||||
|
width?: string
|
||||||
|
height?: string
|
||||||
|
setColor?: string
|
||||||
|
onClick?: (event?: React.MouseEvent<SVGSVGElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconComponent = ({ name, width = "24", height = "24", setColor, onClick}: IconsProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
icon={ICONS[name]}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={setColor || theme.textColor}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
FRONT/BUILD/resources/app/src/const/buttons/names.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { names } from "../types/types"
|
||||||
|
|
||||||
|
export const buttonNames: names ={
|
||||||
|
1: 'powerOff',
|
||||||
|
2: 'delete',
|
||||||
|
3: 'settings',
|
||||||
|
4: 'information',
|
||||||
|
5: 'arrow-down',
|
||||||
|
6: 'arrow-right',
|
||||||
|
7: 'edit',
|
||||||
|
8: 'accept',
|
||||||
|
9: 'cancel',
|
||||||
|
10: 'admin',
|
||||||
|
11: 'reset',
|
||||||
|
12: 'close',
|
||||||
|
13: 'noAdmin',
|
||||||
|
14: 'userSearch',
|
||||||
|
15: 'wifiFind',
|
||||||
|
16: 'noteEdit'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Menu } from "../../types/types";
|
||||||
|
import { menusNames } from "./names";
|
||||||
|
|
||||||
|
export const menusData: Menu[] = [
|
||||||
|
{
|
||||||
|
id: menusNames[1],
|
||||||
|
isOpen: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { names } from "../../types/types"
|
||||||
|
|
||||||
|
export const menusNames: names ={
|
||||||
|
1: 'collapse-devices',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from "react"
|
||||||
|
import { toolBarNames } from "./names"
|
||||||
|
import { DevicesBar } from "../../../components/workSpace/sideBar/toolSpace/devicesBar"
|
||||||
|
|
||||||
|
export const COMPONENTS = {
|
||||||
|
[toolBarNames[1]]:(
|
||||||
|
<DevicesBar/>
|
||||||
|
),
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type ComponentName = keyof typeof COMPONENTS
|
||||||
|
|
||||||
|
interface SpaceProps {
|
||||||
|
name: ComponentName,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Space = ({name}: SpaceProps) => {
|
||||||
|
const Component = COMPONENTS[name]
|
||||||
|
return React.cloneElement(Component);
|
||||||
|
}
|
||||||
13
FRONT/BUILD/resources/app/src/const/menus/sideBar/data.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ToolBar } from "../../types/types";
|
||||||
|
import { toolBarNames } from "./names";
|
||||||
|
|
||||||
|
export const toolBarData: ToolBar = {
|
||||||
|
current: toolBarNames[1],
|
||||||
|
toolSpace: [
|
||||||
|
{
|
||||||
|
id: toolBarNames[1],
|
||||||
|
isOpen: true,
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
32
FRONT/BUILD/resources/app/src/const/menus/sideBar/icons.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from "react"
|
||||||
|
import { toolBarNames } from "./names"
|
||||||
|
import { useTheme } from "styled-components"
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
import desktop from '@iconify/icons-ic/round-desktop-windows';
|
||||||
|
|
||||||
|
export const ICONS = {
|
||||||
|
[toolBarNames[1]]: desktop,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
|
||||||
|
type IconName = keyof typeof ICONS
|
||||||
|
|
||||||
|
interface IconsProps {
|
||||||
|
name: IconName,
|
||||||
|
width?: string
|
||||||
|
height?: string
|
||||||
|
setColor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconComponent = ({ name, width = "24", height = "24", setColor}: IconsProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
icon={ICONS[name]}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={setColor || theme.textColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { names } from "../../types/types"
|
||||||
|
|
||||||
|
export const toolBarNames: names ={
|
||||||
|
1: 'toolSpace-devices',
|
||||||
|
}
|
||||||