Compare commits
52 commits
release/0.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aa31aa8d2 | |||
| 8b936603cc | |||
| b77725b9f9 | |||
| 660fb66fc0 | |||
| a69c5ac5fb | |||
| 5fc8e1ce7e | |||
| 11a1c6e392 | |||
| 84adc5d941 | |||
|
|
98581132dd | ||
| 40c98e68d7 | |||
| a13636ca49 | |||
| 9f0ab4ae5a | |||
| 752604b20b | |||
| ab38a0db50 | |||
| e1b027b301 | |||
| 6a4ad48577 | |||
| e264750187 | |||
|
|
2b8eff58ac | ||
|
|
81ff7959f7 | ||
|
|
bc4e9caacb | ||
|
|
af1420dbde | ||
|
|
5bc23e7eb8 | ||
|
|
8f48df2cf5 | ||
| 4e92aba2a2 | |||
|
|
4286f28d1e | ||
| 3315123029 | |||
| ab78185f08 | |||
| 0c8533257d | |||
| b1306ff66f | |||
| c326edfb9a | |||
| ec054bbaad | |||
| f0d5481dfe | |||
| 990a7594fc | |||
| 35225b90a1 | |||
| 4215a62b6c | |||
| 6efbb4285f | |||
| 200d47754c | |||
| 44dab7402d | |||
| ba8cea01e7 | |||
| 7491ce455d | |||
| 9ac52c8a8b | |||
|
|
48e8551b0e | ||
| 486d46c467 | |||
|
|
e896a13f3c | ||
|
|
b997f554bc | ||
|
|
ca33a55375 | ||
| 26a364aad6 | |||
|
|
c423cf7172 | ||
| 1672f54e02 | |||
| 61c526d3ee | |||
|
|
ee350c1852 | ||
| 4257864b3d |
387 changed files with 49665 additions and 3405 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -6,5 +6,6 @@ __pycache__
|
|||
.wheels
|
||||
.vendor
|
||||
.python3-deb
|
||||
SRV/storage/devices/devices.json
|
||||
SRV/storage/wifi/wifi.json
|
||||
SRV/storage/subnets/*
|
||||
SRV/storage/wifi/wifi.json
|
||||
build/
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ steps:
|
|||
- python3 --version
|
||||
- node -v
|
||||
- npm -v
|
||||
raspberry
|
||||
|
||||
# ПОДГОТОВКА СПИСКА PYTHON ЗАВИСИМОСТЕЙ GONETS
|
||||
- rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
|||
setHasInited(false)
|
||||
initializationAttempt.current = 0;
|
||||
|
||||
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
||||
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
|
||||
|
||||
let connectionTimeout;
|
||||
setSocket(newSocket);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export const DevicesBar = () => {
|
|||
|
||||
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
||||
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
|
||||
const timeout = setTimeout(() => {
|
||||
testSocket.close();
|
||||
resolve(false);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
|||
import { HashRouter } from 'react-router-dom';
|
||||
import { useIpStore } from './src/store';
|
||||
import { useEffect } from 'react';
|
||||
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
|
||||
import { useCheckAdmin } from './src/utils/checkAdmin';
|
||||
|
||||
import {
|
||||
ModalProvider,
|
||||
|
|
@ -11,28 +11,44 @@ import {
|
|||
ThemeProvider
|
||||
} from './src/store';
|
||||
|
||||
import WindowComponent from './src/components/modalwindows/modalWindow.tsx';
|
||||
import Router from './src/router.tsx';
|
||||
import WindowComponent from './src/components/modalwindows/modalWindow';
|
||||
import Router from './src/router';
|
||||
import { I18nProvider } from "./src/utils/i18n/provider";
|
||||
import { useLanguage } from "./src/hooks/useSettings";
|
||||
|
||||
const AppWithProviders = () => {
|
||||
const currentLanguage = useLanguage();
|
||||
|
||||
return (
|
||||
<I18nProvider initialLanguage={currentLanguage}>
|
||||
<ThemeProvider>
|
||||
<HashRouter>
|
||||
<ModalProvider>
|
||||
<MainComponent />
|
||||
</ModalProvider>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const MainComponent = () => {
|
||||
|
||||
const checkAdmin = useCheckAdmin()
|
||||
const {ipAddress} = useIpStore()
|
||||
const checkAdmin = useCheckAdmin();
|
||||
const { ipAddress } = useIpStore();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAdminStatus = async () => {
|
||||
await checkAdmin()
|
||||
}
|
||||
checkAdminStatus()
|
||||
}, [ipAddress])
|
||||
|
||||
await checkAdmin();
|
||||
};
|
||||
checkAdminStatus();
|
||||
}, [ipAddress]);
|
||||
|
||||
const { isOpen, modalType, modalData } = useModalContext();
|
||||
return (
|
||||
<>
|
||||
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
|
||||
<Router />
|
||||
</>
|
||||
<>
|
||||
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
|
||||
<Router />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -40,13 +56,6 @@ const container = document.getElementById('root');
|
|||
if (!container) {
|
||||
throw new Error('Root element with id "root" not found');
|
||||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<ThemeProvider>
|
||||
<HashRouter>
|
||||
<ModalProvider>
|
||||
<MainComponent />
|
||||
</ModalProvider>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
root.render(<AppWithProviders />);
|
||||
187
FRONT/DEV/package-lock.json
generated
187
FRONT/DEV/package-lock.json
generated
|
|
@ -28,7 +28,7 @@
|
|||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"electron": "^34.0.2",
|
||||
"electron": "^39.2.3",
|
||||
"electron-packager": "^17.1.2",
|
||||
"esbuild-wasm": "^0.17.12",
|
||||
"typescript": "^4.9.5",
|
||||
|
|
@ -1787,13 +1787,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz",
|
||||
"integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==",
|
||||
"version": "22.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
|
|
@ -1991,13 +1991,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -2046,9 +2046,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2138,6 +2138,19 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/camelize": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||
|
|
@ -2536,16 +2549,30 @@
|
|||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "34.0.2",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-34.0.2.tgz",
|
||||
"integrity": "sha512-u3F+DSUlg9NaGS+9qnYmSRN8VjAnc3LJDDk1ye1uISJnh4gjG76y3681qLowsPMx4obvCP2eBINnmbLo0yT5WA==",
|
||||
"version": "39.2.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz",
|
||||
"integrity": "sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -2685,9 +2712,7 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
|
|
@ -2696,9 +2721,34 @@
|
|||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
|
|
@ -2949,13 +2999,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -3003,7 +3055,6 @@
|
|||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
|
@ -3072,6 +3123,30 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-package-info": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz",
|
||||
|
|
@ -3105,6 +3180,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
|
|
@ -3208,9 +3296,7 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
|
|
@ -3265,11 +3351,37 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
|
|
@ -3568,6 +3680,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
|
|
@ -4580,9 +4701,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -4648,9 +4769,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"electron": "^34.0.2",
|
||||
"electron": "^39.2.3",
|
||||
"electron-packager": "^17.1.2",
|
||||
"esbuild-wasm": "^0.17.12",
|
||||
"typescript": "^4.9.5",
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
send: (channel, data) => {
|
||||
ipcRenderer.send(channel, data);
|
||||
},
|
||||
on: (channel, func) => {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
},
|
||||
quitApp: () => {
|
||||
ipcRenderer.send('quit-app');
|
||||
},
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
send: (channel, data) => ipcRenderer.send(channel, data),
|
||||
on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
|
||||
quitApp: () => ipcRenderer.send('quit-app'),
|
||||
|
||||
onThemeChange: (callback) => {
|
||||
ipcRenderer.on('theme-update', (event, theme) => {
|
||||
callback(theme);
|
||||
});
|
||||
}
|
||||
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
||||
writeFile: (path, data) => ipcRenderer.invoke('write-file', { path, data }),
|
||||
onThemeChange: (callback) => ipcRenderer.on('theme-update', (event, theme) => callback(theme))
|
||||
});
|
||||
186
FRONT/DEV/public/locales/en/common.json
Normal file
186
FRONT/DEV/public/locales/en/common.json
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
{
|
||||
"userScanner-title": "Subnet Node Scanning",
|
||||
"userScanner-visible": "Display Method",
|
||||
"userScanner-loading": "Scanning nodes",
|
||||
"userScanner-refresh": "Refresh List",
|
||||
"userScanner-scan": "Scan Nodes",
|
||||
"userScanner-notFound": "Nodes not found",
|
||||
"tableScanner-online": "Online",
|
||||
"tableScanner-name": "Name",
|
||||
"tableScanner-vendor": "Vendor",
|
||||
"tableScanner-ip": "IP Address",
|
||||
"tableScanner-ports": "Open Ports",
|
||||
"tableScanner-activity": "Last Activity",
|
||||
"tableScanner-note": "Note",
|
||||
"tableScanner-action": "Action",
|
||||
"tableScanner-unknown": "Unknown",
|
||||
"tableScanner-noData": "No data to display",
|
||||
"listUsers-activeNodes": "Active Nodes",
|
||||
"listUsers-inactiveNodes": "Inactive Nodes",
|
||||
"ipInput-placeholder": "Enter IP",
|
||||
"ipInput-error": "Enter a valid IP address",
|
||||
"userScanner-visible_table": "Table",
|
||||
"userScanner-visible_list": "List",
|
||||
|
||||
"wifiScanner-title": "Wi-Fi Access Point Scanning",
|
||||
"wifiScanner-scanning": "Scanning access points",
|
||||
"wifiScanner-scanError": "Network scanning error",
|
||||
"wifiScanner-refresh": "Refresh List",
|
||||
"wifiScanner-scan": "Scan Access Points",
|
||||
"wifiModal-bssid": "BSSID",
|
||||
"wifiModal-vendor": "Vendor",
|
||||
"wifiModal-signal": "Signal",
|
||||
"wifiModal-channel": "Channel",
|
||||
"wifiModal-encryption": "Encryption",
|
||||
"wifiModal-frequency": "Frequency",
|
||||
"wifiModal-hidden": "Hidden Network",
|
||||
"wifiModal-note": "Note",
|
||||
"wifiModal-connect": "Connect",
|
||||
"wifiModal-disconnect": "Disconnect",
|
||||
"wifiModal-unknown": "Unknown",
|
||||
"wifiModal-none": "None",
|
||||
"common-yes": "Yes",
|
||||
"common-no": "No",
|
||||
|
||||
"addNode": "Add Node",
|
||||
"enterNodeAddress": "Enter node address...",
|
||||
"connectionLost": "Connection Lost",
|
||||
"serverDisconnected": "Server connection interrupted",
|
||||
"checkAddressAndRetry": "Please check the address availability and try again",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"retry": "Retry",
|
||||
"connect": "Connect",
|
||||
"submit": "Submit",
|
||||
"common-back": "Back",
|
||||
"error": "Error",
|
||||
"networkConnection": "Network Connection",
|
||||
"enterPassword": "Enter password...",
|
||||
"connectingAttempt": "Attempting to connect to the network",
|
||||
"connectionError": "Network connection error",
|
||||
"connectionErrorLog": "Connection error: ",
|
||||
"customizeTable-title": "Subnet Node Table Customization",
|
||||
|
||||
"wifiDisconnect-title": "Wi-Fi Network Disconnection",
|
||||
"wifiDisconnect-confirm": "Confirm Disconnection",
|
||||
"wifiDisconnect-message1": "You are about to disconnect from the network - ",
|
||||
"wifiDisconnect-message2": " All active connections will be interrupted.",
|
||||
"wifiDisconnect-disconnect": "Disconnect",
|
||||
"wifiDisconnect-progress": "Disconnecting from the network",
|
||||
|
||||
"ipChange-title": "IP Address Change",
|
||||
"ipChange-checkingIp": "Checking IP address validity",
|
||||
"ipChange-generalInfo": "General Information",
|
||||
"ipChange-interface": "Interface",
|
||||
"ipChange-ipInfo": "IP Information",
|
||||
"ipChange-currentIp": "Current IP",
|
||||
"ipChange-currentGateway": "Current Gateway",
|
||||
"ipChange-currentDns": "Current DNS",
|
||||
"ipChange-newIp": "New IP",
|
||||
"ipChange-newGateway": "New Gateway",
|
||||
"ipChange-newDns": "New DNS",
|
||||
"ipChange-checkError": "An error occurred while checking validity",
|
||||
"ipChange-adminRequired": "To change network settings, you must log in as an administrator",
|
||||
|
||||
"note-edit": "Edit Note",
|
||||
"note-add": "Add Note",
|
||||
"note-enter": "Enter note...",
|
||||
|
||||
"auth-successLogin": "Successful Login",
|
||||
"auth-successLogout": "Successful Logout",
|
||||
"auth-getAccess": "Obtaining Access Rights",
|
||||
"auth-enableAdmin": "Enable Administrative Access",
|
||||
"auth-enterAdminPass": "Enter administrator password...",
|
||||
"auth-wrongPass": "Incorrect operator password",
|
||||
"auth-switchToLimited": "Switch to Limited Access",
|
||||
"auth-limitedModeDesc": "In this mode, administrator privileges are restricted.",
|
||||
"auth-restrictAccess": "Restrict Access",
|
||||
"auth-closeHint": "This window can be closed, or it will disappear on its own",
|
||||
|
||||
"operatorConfig-title": "Node Operator Configuration",
|
||||
"operatorConfig-instruction": "Before starting, you need to create an application operator password",
|
||||
"operatorConfig-passwordPlaceholder": "Enter password...",
|
||||
"operatorConfig-repeatPasswordPlaceholder": "Repeat password...",
|
||||
"operatorConfig-passwordMismatch": "Passwords do not match!",
|
||||
"operatorConfig-submit": "Submit",
|
||||
|
||||
"about-logoAlt": "Gossys Logo",
|
||||
"about-title": "About the Application",
|
||||
"about-description": "GoNets is a minimally viable cross-platform product (MVP) for demonstrating our approach to managing PC settings in a corporate environment.",
|
||||
"about-featuresTitle": "Features",
|
||||
"about-feature1": "Visualization of active network resources",
|
||||
"about-feature2": "Configuration of local and remote network resource parameters",
|
||||
"about-feature3": "Management of resource interaction with the GoNets Client",
|
||||
"about-disclaimer": "The software is for demonstration and home use only.",
|
||||
"about-goToPro": "Go to GoNets-PRO",
|
||||
"about-homepage": "Homepage",
|
||||
"about-documentation": "Documentation",
|
||||
"about-license": "License",
|
||||
"about-copyright": "Developed and maintained by Hybrid Office Systems LLC, 2024-2025. All rights reserved.",
|
||||
|
||||
"main-logoAlt": "Application Logo",
|
||||
"main-networkSettings": "Network Settings",
|
||||
"main-about": "About the Program",
|
||||
|
||||
"nodes-availableNodes": "Available Nodes",
|
||||
"nodes-rootAddress": "Root Address",
|
||||
"nodes-name": "Name",
|
||||
"nodes-loading": "Loading",
|
||||
"nodes-additionalNodes": "Additional Nodes",
|
||||
"nodes-node": "Node",
|
||||
"nodes-address": "Address",
|
||||
"nodes-connectionError": "Failed to connect to the server at the address ",
|
||||
|
||||
"settings-nodesSection": "Subnet Nodes",
|
||||
"settings-intervalScanning": "Interval Scanning",
|
||||
"settings-scanInterval": "Node Scan Interval (min)",
|
||||
"settings-displayNames": "Display Names When Missing",
|
||||
"settings-ipAddress": "IP Address",
|
||||
"settings-unknown": "Unknown",
|
||||
"settings-rowsPerPage": "Rows Per Table",
|
||||
"settings-displaySettings": "Nodes Display",
|
||||
"settings-configure": "Configure",
|
||||
"settings-clearSessionData": "Clear Session Data",
|
||||
"settings-clear": "Clear",
|
||||
"settings-clearFailed": "Data was not deleted: ",
|
||||
"settings-exportData": "Export Nodes Data",
|
||||
"settings-export": "Export",
|
||||
"settings-saveNodesTitle": "Save Nodes Data",
|
||||
"settings-exportSuccess": "Data exported successfully!",
|
||||
"settings-exportError": "Export error: ",
|
||||
"settings-status": "Online",
|
||||
"settings-name": "Name",
|
||||
"settings-vendor": "Manufacturer",
|
||||
"settings-openPorts": "Open Ports",
|
||||
"settings-lastActivity": "Last Activity",
|
||||
"settings-note": "Note",
|
||||
"settings-action": "Action",
|
||||
|
||||
"settings-title": "Settings",
|
||||
"settings-resetSettings": "Reset Settings",
|
||||
"settings-resetConfirm": "Are you sure you want to reset all settings to default values?",
|
||||
"settings-cancel": "Cancel",
|
||||
"settings-save": "Save",
|
||||
"settings-fixErrors": "Fix errors before saving",
|
||||
"settings-fillRequired": "Fill in all required fields",
|
||||
"settings-dataNotDeleted": "Data was not deleted: ",
|
||||
"settings-saveNetworksTitle": "Save Networks Data",
|
||||
"settings-fieldRequired": "Field cannot be empty",
|
||||
"settings-enterNumber": "Please enter a number",
|
||||
"settings-valueTooSmall": "Value cannot be less than ",
|
||||
"settings-valueTooLarge": "Value cannot be greater than ",
|
||||
|
||||
"settings-sections-subnetNodes": "Subnet Nodes",
|
||||
"settings-sections-accessPoints": "Access Points",
|
||||
"settings-sections-appearance": "Appearance",
|
||||
|
||||
"settings-exportNetworkData": "Export Access Points Data",
|
||||
"settings-colorTheme": "Color Theme",
|
||||
"settings-light": "Light",
|
||||
"settings-dark": "Dark",
|
||||
"settings-system": "As in System",
|
||||
"settings-interfaceLanguage": "Interface Language",
|
||||
"settings-russian": "Russian",
|
||||
"settings-english": "English"
|
||||
}
|
||||
192
FRONT/DEV/public/locales/ru/common.json
Normal file
192
FRONT/DEV/public/locales/ru/common.json
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
{
|
||||
"userScanner-title": "Сканирование узлов подсети",
|
||||
"userScanner-visible": "Способ отображения",
|
||||
"userScanner-loading": "Cканирование узлов",
|
||||
"userScanner-refresh": "Обновить список",
|
||||
"userScanner-scan": "Сканировать узлы",
|
||||
"userScanner-notFound": "Узлы не найдены",
|
||||
"tableScanner-online": "Онлайн",
|
||||
"tableScanner-name": "Имя",
|
||||
"tableScanner-vendor": "Производитель",
|
||||
"tableScanner-ip": "IP адрес",
|
||||
"tableScanner-ports": "Открытые порты",
|
||||
"tableScanner-activity": "Последняя активность",
|
||||
"tableScanner-note": "Заметка",
|
||||
"tableScanner-action": "Действие",
|
||||
"tableScanner-unknown": "Неизвестно",
|
||||
"tableScanner-noData": "Нет данных для отображения",
|
||||
"listUsers-activeNodes": "Активные узлы",
|
||||
"listUsers-inactiveNodes": "Неактивные узлы",
|
||||
"ipInput-placeholder": "Введите IP",
|
||||
"ipInput-error": "Введите корректный IP-адрес",
|
||||
"userScanner-visible_table": "Таблица",
|
||||
"userScanner-visible_list": "Список",
|
||||
|
||||
"wifiScanner-title": "Сканирование точек доступа Wi-Fi",
|
||||
"wifiScanner-scanning": "Сканирование точек доступа",
|
||||
"wifiScanner-scanError": "Ошибка сканирования сети",
|
||||
"wifiScanner-refresh": "Обновить список",
|
||||
"wifiScanner-scan": "Сканировать точки доступа",
|
||||
"wifiModal-bssid": "BSSID",
|
||||
"wifiModal-vendor": "Производитель",
|
||||
"wifiModal-signal": "Сигнал",
|
||||
"wifiModal-channel": "Канал",
|
||||
"wifiModal-encryption": "Шифрование",
|
||||
"wifiModal-frequency": "Частота",
|
||||
"wifiModal-hidden": "Скрытая сеть",
|
||||
"wifiModal-note": "Заметка",
|
||||
"wifiModal-connect": "Подключиться",
|
||||
"wifiModal-disconnect": "Отключиться",
|
||||
"wifiModal-unknown": "Неизвестно",
|
||||
"wifiModal-none": "Отсутствует",
|
||||
"common-yes": "Да",
|
||||
"common-no": "Нет",
|
||||
|
||||
"addNode": "Добавление узла",
|
||||
"enterNodeAddress": "Введите адрес узла...",
|
||||
"connectionLost": "Соединение прервано",
|
||||
"serverDisconnected": "Произошёл разрыв с сервером",
|
||||
"checkAddressAndRetry": "Пожалуйста, проверьте доступ к адресу и попробуйте снова",
|
||||
"cancel": "Отменить",
|
||||
"close": "Закрыть",
|
||||
"save": "Сохранить",
|
||||
"retry": "Повторить",
|
||||
"connect": "Подключиться",
|
||||
"common-back": "Назад",
|
||||
"submit": "Подтвердить",
|
||||
"error": "Ошибка",
|
||||
"networkConnection": "Подключение к сети",
|
||||
"enterPassword": "Введите пароль...",
|
||||
"connectingAttempt": "Попытка подключения к сети",
|
||||
"connectionError": "Ошибка подключения к сети",
|
||||
"connectionErrorLog": "Ошибка подключения: ",
|
||||
"customizeTable-title": "Кастомизация таблицы узлов подсети",
|
||||
|
||||
"wifiDisconnect-title": "Отключение от Wi-Fi сети",
|
||||
"wifiDisconnect-confirm": "Подтверждение отключения",
|
||||
"wifiDisconnect-message1": "Вы собираетесь отключиться от сети - ",
|
||||
"wifiDisconnect-message2": " Все активные подключения будут прерваны.",
|
||||
"wifiDisconnect-disconnect": "Отключиться",
|
||||
"wifiDisconnect-progress": "Отключение от сети",
|
||||
|
||||
"ipChange-title": "Изменение IP-Адреса",
|
||||
"ipChange-checkingIp": "Идет проверка корректности IP адреса",
|
||||
"ipChange-generalInfo": "Общие сведения",
|
||||
"ipChange-interface": "Интерфейс",
|
||||
"ipChange-ipInfo": "Сведения об IP",
|
||||
"ipChange-currentIp": "Текущий ip",
|
||||
"ipChange-currentGateway": "Текущий шлюз",
|
||||
"ipChange-currentDns": "Текущий DNS",
|
||||
"ipChange-newIp": "Новый ip",
|
||||
"ipChange-newGateway": "Новый шлюз",
|
||||
"ipChange-newDns": "Новый DNS",
|
||||
"ipChange-checkError": "Произошла ошибка проверки корректности",
|
||||
"ipChange-adminRequired": "Для изменения сетевых настроек необходимо авторизироваться под администратором",
|
||||
|
||||
"note-edit": "Редактирование заметки",
|
||||
"note-add": "Добавление заметки",
|
||||
"note-enter": "Введите заметку...",
|
||||
|
||||
"auth-successLogin": "Успешный вход",
|
||||
"auth-successLogout": "Успешный выход",
|
||||
"auth-getAccess": "Получение прав доступа",
|
||||
"auth-enableAdmin": "Включить административный доступ",
|
||||
"auth-enterAdminPass": "Введите пароль администратора...",
|
||||
"auth-wrongPass": "Неверный пароль оператора",
|
||||
"auth-switchToLimited": "Переключиться на ограниченный доступ",
|
||||
"auth-limitedModeDesc": "В данном режиме доступа ограничиваются полномочия администратора.",
|
||||
"auth-restrictAccess": "Ограничить доступ",
|
||||
"auth-closeHint": "Это окно можно закрыть, либо оно пропадёт само",
|
||||
|
||||
"operatorConfig-title": "Настройка оператора узла",
|
||||
"operatorConfig-instruction": "Перед началом работы необходимо создать пароль оператора приложения",
|
||||
"operatorConfig-passwordPlaceholder": "Введите пароль...",
|
||||
"operatorConfig-repeatPasswordPlaceholder": "Повторите пароль...",
|
||||
"operatorConfig-passwordMismatch": "Пароли не совпадают!",
|
||||
"operatorConfig-submit": "Отправить",
|
||||
|
||||
"about-logoAlt": "Логотип Gossys",
|
||||
"about-title": "О приложении",
|
||||
"about-description": "ГоNets - минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.",
|
||||
"about-featuresTitle": "Возможности",
|
||||
"about-feature1": "Визуализация активных сетевых ресурсов",
|
||||
"about-feature2": "Настройка параметров локальных и удалённых сетевых ресурсов",
|
||||
"about-feature3": "Управление взаимодействием ресурсов с Клиентом ГoNets",
|
||||
"about-disclaimer": "Программное обеспечение исключительно для ознакомления и домашнего использования.",
|
||||
"about-goToPro": "Перейти на ГоNets-PRO",
|
||||
"about-homepage": "Домашняя страница",
|
||||
"about-documentation": "Документация",
|
||||
"about-license": "Лицензия",
|
||||
"about-copyright": "Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.",
|
||||
|
||||
"main-logoAlt": "Логотип приложения",
|
||||
"main-networkSettings": "Настройки сети",
|
||||
"main-about": "О программе",
|
||||
|
||||
"nodes-availableNodes": "Доступные узлы",
|
||||
"nodes-rootAddress": "Корневой адрес",
|
||||
"nodes-name": "Имя",
|
||||
"nodes-loading": "Загрузка",
|
||||
"nodes-additionalNodes": "Дополнительные узлы",
|
||||
"nodes-node": "Узел",
|
||||
"nodes-address": "Адрес",
|
||||
"nodes-connectionError": "Не удалось подключиться к серверу по адресу ",
|
||||
|
||||
"settings-nodesSection": "Узлы подсети",
|
||||
"settings-intervalScanning": "Интервальное сканирование",
|
||||
"settings-scanInterval": "Интервал сканирования узлов (мин)",
|
||||
"settings-displayNames": "Отображаемые имена при отсутствии",
|
||||
"settings-ipAddress": "IP адрес",
|
||||
"settings-unknown": "Неизвестно",
|
||||
"settings-rowsPerPage": "Количество строк в таблице",
|
||||
"settings-displaySettings": "Отображение узлов",
|
||||
"settings-configure": "Настроить",
|
||||
"settings-clearSessionData": "Очистить данные сессии",
|
||||
"settings-clear": "Очистить",
|
||||
"settings-clearFailed": "Данные не были удалены: ",
|
||||
"settings-exportData": "Экспорт данных узлов",
|
||||
"settings-export": "Экспортировать",
|
||||
"settings-saveNodesTitle": "Сохранить данные узлов",
|
||||
"settings-exportSuccess": "Данные успешно экспортированы!",
|
||||
"settings-exportError": "Ошибка экспорта: ",
|
||||
"settings-status": "Онлайн",
|
||||
"settings-name": "Имя",
|
||||
"settings-vendor": "Производитель",
|
||||
"settings-openPorts": "Открытые порты",
|
||||
"settings-lastActivity": "Последняя активность",
|
||||
"settings-note": "Заметка",
|
||||
"settings-action": "Действие",
|
||||
|
||||
"settings-title": "Настройки",
|
||||
"settings-resetSettings": "Сбросить настройки",
|
||||
"settings-resetConfirm": "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?",
|
||||
"settings-cancel": "Отменить",
|
||||
"settings-save": "Сохранить",
|
||||
"settings-fixErrors": "Исправьте ошибки перед сохранением",
|
||||
"settings-fillRequired": "Заполните все обязательные поля",
|
||||
"settings-dataNotDeleted": "Данные не были удалены: ",
|
||||
"settings-saveNetworksTitle": "Сохранить данные сетей",
|
||||
"settings-fieldRequired": "Поле не может быть пустым",
|
||||
"settings-enterNumber": "Пожалуйста, введите число",
|
||||
"settings-valueTooSmall": "Значение не может быть меньше ",
|
||||
"settings-valueTooLarge": "Значение не может быть больше ",
|
||||
|
||||
"settings-sections-subnetNodes": "Узлы подсети",
|
||||
"settings-sections-accessPoints": "Точки доступа",
|
||||
"settings-sections-appearance": "Внешний вид",
|
||||
|
||||
"settings-exportNetworkData": "Экспорт данных точек доступа",
|
||||
"settings-colorTheme": "Цветовая тема",
|
||||
"settings-light": "Светлая",
|
||||
"settings-dark": "Тёмная",
|
||||
"settings-system": "Как в системе",
|
||||
"settings-interfaceLanguage": "Язык интерфейса",
|
||||
"settings-russian": "Русский",
|
||||
"settings-english": "Английский"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import axios, { AxiosResponse } from "axios";
|
||||
|
||||
const axiosWithoutCredentials = axios.create({
|
||||
baseURL: `https://localhost:8001`,
|
||||
baseURL: `http://localhost:8001`,
|
||||
})
|
||||
|
||||
export class NetworkService {
|
||||
static createAxiosInstance(ipAddress: string) {
|
||||
return axios.create({
|
||||
baseURL: `https://${ipAddress}:8001`,
|
||||
baseURL: `http://${ipAddress}:8001`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -83,10 +83,12 @@ export class NetworkService {
|
|||
}
|
||||
}
|
||||
|
||||
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
|
||||
static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
return await axiosInstance.get('/subnet/devices');
|
||||
return await axiosInstance.post('/subnet/devices',{
|
||||
subnet: ipScanning,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return error.response;
|
||||
}
|
||||
|
|
@ -157,6 +159,8 @@ export class NetworkService {
|
|||
return error.response;
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
|
|
@ -173,4 +177,20 @@ export class NetworkService {
|
|||
return error.response;
|
||||
}
|
||||
}
|
||||
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
return await axiosInstance.post(`/settings/export_devices`);
|
||||
} catch (error: any) {
|
||||
return error.response;
|
||||
}
|
||||
}
|
||||
static async exportNetwork(ipAddress: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||
return await axiosInstance.post(`/settings/export_wifi`);
|
||||
} catch (error: any) {
|
||||
return error.response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import useNetworkStore from "../../../store/useDeviceStore";
|
|||
import RouterLoader from "../../UI/routerLoader";
|
||||
import {useTheme} from "styled-components";
|
||||
import {useIpStore} from "../../../store";
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
|
|
@ -19,53 +20,55 @@ interface Props {
|
|||
export const ConnectNetwork = ({address, onClose}: Props) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const theme = useTheme();
|
||||
const { ipAddress } = useIpStore();
|
||||
const {ipAddress} = useIpStore();
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('')
|
||||
setError('');
|
||||
const res = await NetworkService.connectNetwork(ipAddress, password, address);
|
||||
if(res.status === 200) {
|
||||
onClose()
|
||||
}else{
|
||||
setError(res.data.detail)
|
||||
if (res.status === 200) {
|
||||
onClose();
|
||||
} else {
|
||||
setError(res.data.detail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error connection:', error);
|
||||
setError('Ошибка подключения к сети')
|
||||
}finally {
|
||||
console.error(t('connectionErrorLog'), error);
|
||||
setError(t('connectionError'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<ModalContainer style={{ height: '230px' }}>
|
||||
<ModalContainer style={{height: '230px'}}>
|
||||
<ModalTitle>
|
||||
<h3>Подключение к сети</h3>
|
||||
<h3>{t('networkConnection')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholderText='Введите пароль...'
|
||||
placeholderText={t('enterPassword')}
|
||||
value={password}
|
||||
id='deviceNote'
|
||||
onChange={(value) => setPassword(value)}
|
||||
/>
|
||||
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
||||
{error}
|
||||
</p>
|
||||
{error && (
|
||||
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title="Отмена"
|
||||
title={t('cancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<MainButton
|
||||
|
|
@ -73,12 +76,12 @@ export const ConnectNetwork = ({address, onClose}: Props) => {
|
|||
size="small"
|
||||
color={theme.successColor}
|
||||
hoverColor={theme.successColor}
|
||||
title='Подключиться'
|
||||
title={t('connect')}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
||||
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
|
||||
{isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
|
||||
</ModalContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
// components/modalContainers/NoteDevice.tsx
|
||||
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
|
||||
import { MainButton } from "../../UI/buttons/mainButton";
|
||||
import Input from "../../UI/inputs/input";
|
||||
|
|
@ -6,6 +5,7 @@ import { useState } from "react";
|
|||
import {useIpStore} from "../../../store";
|
||||
import {NetworkService} from "../../../api/request";
|
||||
import useNetworkStore from "../../../store/useDeviceStore";
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
|
|
@ -20,6 +20,7 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
|||
const [note, setNote] = useState(currentNote || '');
|
||||
const { ipAddress } = useIpStore();
|
||||
const { devices, setDevices } = useNetworkStore();
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
|
|
@ -50,13 +51,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
|||
return (
|
||||
<ModalContainer style={{ height: '250px' }}>
|
||||
<ModalTitle>
|
||||
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
|
||||
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholderText='Введите заметку...'
|
||||
placeholderText={t('note-enter')}
|
||||
value={note}
|
||||
id='deviceNote'
|
||||
onChange={(value) => setNote(value)}
|
||||
|
|
@ -67,13 +68,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
|
|||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title="Отмена"
|
||||
title={t('cancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title='Сохранить'
|
||||
title={t('save')}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useState } from "react";
|
|||
import {NetworkService} from "../../../api/request";
|
||||
import useNetworkStore from "../../../store/useDeviceStore";
|
||||
import {useIpStore} from "../../../store";
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
|
|
@ -20,6 +21,7 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
|||
const [note, setNote] = useState(currentNote || '');
|
||||
const { ipAddress } = useIpStore();
|
||||
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
|
|
@ -50,13 +52,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
|||
return (
|
||||
<ModalContainer style={{ height: '220px' }}>
|
||||
<ModalTitle>
|
||||
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
|
||||
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholderText='Введите заметку...'
|
||||
placeholderText={t('note-enter')}
|
||||
value={note}
|
||||
id='deviceNote'
|
||||
onChange={(value) => setNote(value)}
|
||||
|
|
@ -67,13 +69,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
|
|||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title="Отмена"
|
||||
title={t('cancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title='Сохранить'
|
||||
title={t('save')}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { MainButton } from "../../UI/buttons/mainButton";
|
|||
import { useState } from "react";
|
||||
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
|
||||
import { validateIPAddress } from "../../../utils/nodeController/validator";
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
interface Props {
|
||||
onSave?: (data: string) => void;
|
||||
|
|
@ -12,6 +13,7 @@ interface Props {
|
|||
export const AddDevice = ({ onSave, onClose }: Props) => {
|
||||
const [ipAddress, setIpAddress] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validateIPAddress(ipAddress)) {
|
||||
|
|
@ -28,31 +30,39 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
|
|||
|
||||
return (
|
||||
<ModalContainer style={{ height: '230px' }}>
|
||||
{/* <form onSubmit={handleSubmit}> */}
|
||||
<ModalTitle>
|
||||
<h3>Добавление узла</h3>
|
||||
<h3>{t('addNode')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholderText='Введите адрес узла...'
|
||||
placeholderText={t('enterNodeAddress')}
|
||||
id="newIp"
|
||||
value={ipAddress}
|
||||
onChange={(value) => setIpAddress(value)}
|
||||
/>
|
||||
{error && (
|
||||
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||
Некорректный адрес узла
|
||||
{t('invalidAddress')}
|
||||
</p>
|
||||
)}
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton type="button" size="small" title="Отмена" onClick={onClose} />
|
||||
<MainButton type="submit" size="small" title="Сохранить" onClick={handleSubmit} />
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title={t('cancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<MainButton
|
||||
type="submit"
|
||||
size="small"
|
||||
title={t('save')}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
{/* </form> */}
|
||||
</ModalContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer }
|
|||
import { useModalContext } from "../../../store"
|
||||
import { useIpStore } from "../../../store"
|
||||
import { MainButton } from "../../UI/buttons/mainButton"
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
type Props={
|
||||
message: string
|
||||
|
|
@ -10,28 +11,37 @@ type Props={
|
|||
export const ConnectionError = ({message}: Props)=>{
|
||||
const {closeModal} = useModalContext()
|
||||
const { setIpAddress } = useIpStore();
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const close = ()=>{
|
||||
setIpAddress('localhost')
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return(
|
||||
return (
|
||||
<ModalContainer>
|
||||
<ModalTitle>
|
||||
<h3>Соединение прервано</h3>
|
||||
<h3>{t('connectionLost')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
|
||||
<ModalContent>
|
||||
<ModalText>
|
||||
<h3>Произошёл разрыв с сервером</h3>
|
||||
<p>Пожалуйста, проверьте доступ к адресу и попробуйте снова.</p>
|
||||
<h3>{t('serverDisconnected')}</h3>
|
||||
<p>{t('checkAddressAndRetry')}</p>
|
||||
</ModalText>
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton title="Закрыть" onClick={close} size="small"/>
|
||||
<MainButton title="Повторить" onClick={closeModal} size="small"/>
|
||||
<MainButton
|
||||
title={t('close')}
|
||||
onClick={close}
|
||||
size="small"
|
||||
/>
|
||||
<MainButton
|
||||
title={t('retry')}
|
||||
onClick={closeModal}
|
||||
size="small"
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</ModalContainer>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import {ButtonContainer, ModalContainer, ModalContent, ModalTitle} from "../style";
|
||||
import {MainButton} from "../../UI/buttons/mainButton";
|
||||
import {ModalMap} from "../windowsType";
|
||||
import {useModalContext} from "../../../store";
|
||||
import styled from "styled-components";
|
||||
import { useSettingsStore } from '../../../store/settings/settingsStore';
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
const StyledTableList = styled.ul`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 40px;
|
||||
list-style-type: none;
|
||||
`
|
||||
const StyledTableItem = styled.li`
|
||||
width: 160px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: ${props => props.theme.accent};
|
||||
color: ${props => props.theme.primary};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
|
||||
&:first-child {
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
&.off{
|
||||
background-color: ${props => props.theme.offlineColor};
|
||||
}
|
||||
`
|
||||
const CustomizeNodes = () => {
|
||||
const { openModal } = useModalContext();
|
||||
const pendingChanges = useSettingsStore(state => state.pendingChanges);
|
||||
const updateSetting = useSettingsStore(state => state.updateSetting);
|
||||
const getSetting = useSettingsStore(state => state.getSetting);
|
||||
const initializeSettings = useSettingsStore(state => state.initializeSettings);
|
||||
const {t} = useTranslation('common');
|
||||
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||
|
||||
// Получаем текущие значения из хранилища
|
||||
const initialColumns = getSetting('table-columns-order').currentValue as string[];
|
||||
const disabledColumns = getSetting('table-columns-disabled').currentValue as number[];
|
||||
const items = pendingChanges['table-columns-order'] ?? initialColumns;
|
||||
const disabledItems = pendingChanges['table-columns-disabled'] ?? disabledColumns;
|
||||
|
||||
// Drag & Drop
|
||||
const handleDragStart = (index: number) => {
|
||||
setDragIndex(index);
|
||||
};
|
||||
const handleDragOver = (index: number) => {
|
||||
if (dragIndex === null || index === dragIndex) return;
|
||||
|
||||
const newItems = [...items];
|
||||
const [movedItem] = newItems.splice(dragIndex, 1);
|
||||
newItems.splice(index, 0, movedItem);
|
||||
|
||||
updateSetting('table-columns-order', newItems);
|
||||
setDragIndex(index);
|
||||
};
|
||||
const handleDrop = () => {
|
||||
setDragIndex(null);
|
||||
};
|
||||
|
||||
// Переключение состояния
|
||||
const handleSwitch = (index: number) => {
|
||||
const newDisabledItems = disabledItems.includes(index)
|
||||
? disabledItems.filter((i: number) => i !== index)
|
||||
: [...disabledItems, index];
|
||||
|
||||
updateSetting('table-columns-disabled', newDisabledItems);
|
||||
};
|
||||
|
||||
// Закрытие с сохранением
|
||||
const handleSave = () => {
|
||||
openModal(ModalMap['10']);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
initializeSettings();
|
||||
}, [initializeSettings]);
|
||||
|
||||
const getItemText = (item: string) => {
|
||||
if (item === 'Онлайн') return t('settings-status');
|
||||
if (item === 'Имя') return t('settings-name');
|
||||
if (item === 'Производитель') return t('settings-vendor');
|
||||
if (item === 'IP адрес') return t('settings-ipAddress');
|
||||
if (item === 'Открытые порты') return t('settings-openPorts');
|
||||
if (item === 'Последняя активность') return t('settings-lastActivity');
|
||||
if (item === 'Заметка') return t('settings-note');
|
||||
if (item === 'Действие') return t('settings-action');
|
||||
return item;
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalContainer style={{ height: '35vh', width: '80vw'}}>
|
||||
<ModalTitle>
|
||||
<h3>{t('customizeTable-title')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
<ModalContent style={{ display: 'flex', height: "100%", gap: '24px', padding: 0 }}>
|
||||
<StyledTableList>
|
||||
{items.map((item:string, index:number) => (
|
||||
<StyledTableItem
|
||||
key={index}
|
||||
className={disabledItems.includes(index) ? 'off' : ''}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(index)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
handleDragOver(index);
|
||||
}}
|
||||
onDragEnd={handleDrop}
|
||||
onClick={() => handleSwitch(index)}
|
||||
>
|
||||
{getItemText(item)}
|
||||
</StyledTableItem>
|
||||
))}
|
||||
</StyledTableList>
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title={t('common-back')}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</ModalContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomizeNodes;
|
||||
|
|
@ -6,6 +6,7 @@ import {useState} from "react";
|
|||
import {NetworkService} from "../../../api/request";
|
||||
import RouterLoader from "../../UI/routerLoader";
|
||||
import {useTheme} from "styled-components";
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
type Props={
|
||||
address: string,
|
||||
|
|
@ -16,6 +17,7 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
|
|||
const { ipAddress } = useIpStore();
|
||||
const theme = useTheme()
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
|
|
@ -34,21 +36,32 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
|
|||
return(
|
||||
<ModalContainer>
|
||||
<ModalTitle>
|
||||
<h3>Отключение от Wi-Fi сети</h3>
|
||||
<h3>{t('wifiDisconnect-title')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
|
||||
<ModalContent>
|
||||
<ModalText>
|
||||
<h3>Подтверждение отключения</h3>
|
||||
<h3>{t('wifiDisconnect-confirm')}</h3>
|
||||
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
|
||||
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
|
||||
</ModalText>
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton title="Отменить" onClick={onClose} size="small"/>
|
||||
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
|
||||
<MainButton
|
||||
title={t('cancel')}
|
||||
onClick={onClose}
|
||||
size="small"
|
||||
/>
|
||||
<MainButton
|
||||
title={t('wifiDisconnect-disconnect')}
|
||||
color={theme.errorColor}
|
||||
hoverColor={theme.errorColor}
|
||||
onClick={handleDisconnect}
|
||||
size="small"
|
||||
/>
|
||||
</ButtonContainer>
|
||||
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
|
||||
{isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
|
||||
</ModalContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useIpStore } from "../../../store";
|
|||
import { MainButton } from "../../UI/buttons/mainButton";
|
||||
import { useAdminStore } from "../../../store/adminStore";
|
||||
import { useDevicesStore } from "../../../store";
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
type Props = {
|
||||
ipData: changeIp
|
||||
|
|
@ -52,6 +53,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
|||
const { ipAddress, setIpAddress } = useIpStore();
|
||||
const {isAdmin} = useAdminStore()
|
||||
const { devices, addDevice, deleteDevice } = useDevicesStore();
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
useEffect(() => {
|
||||
if (ipData && isAdmin) {
|
||||
|
|
@ -146,40 +148,48 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
|||
return (
|
||||
<>
|
||||
{loader && isAdmin ? (
|
||||
<Loader loaderText="Идет проверка корректности IP адреса" />
|
||||
<Loader loaderText={t('ipChange-checkingIp')} />
|
||||
) : (
|
||||
<ModalContainer {...(checkStatus ? { height: "unset" } : {})}>
|
||||
<ModalTitle>
|
||||
<h3>Изменение IP-Адреса</h3>
|
||||
<h3>{t('ipChange-title')}</h3>
|
||||
</ModalTitle>
|
||||
{checkStatus ? (
|
||||
<ModalContent>
|
||||
<Information>
|
||||
<h3>Общие сведения</h3>
|
||||
<h3>{t('ipChange-generalInfo')}</h3>
|
||||
<Info>
|
||||
<p>Интерфейс: {ipData.device}</p>
|
||||
<p>{t('ipChange-interface')}: {ipData.device}</p>
|
||||
<p>--{'>'}</p>
|
||||
<p>{ipData.connection}</p>
|
||||
</Info>
|
||||
<h3>Сведения об IP</h3>
|
||||
<h3>{t('ipChange-ipInfo')}</h3>
|
||||
<DataContainer>
|
||||
<CurrentData>
|
||||
<p>Текущий ip: {ipData.currentIp}</p>
|
||||
<p>Текущий шлюз: {ipData.currentGate}</p>
|
||||
<p>Текущий DNS: {ipData.currentDns}</p>
|
||||
<p>{t('ipChange-currentIp')}: {ipData.currentIp}</p>
|
||||
<p>{t('ipChange-currentGateway')}: {ipData.currentGate}</p>
|
||||
<p>{t('ipChange-currentDns')}: {ipData.currentDns}</p>
|
||||
</CurrentData>
|
||||
|
||||
<NewData>
|
||||
<p>Новый ip: {ipData.newIp}</p>
|
||||
<p>Новый шлюз: {ipData.newGate}</p>
|
||||
<p>Новый DNS: {ipData.newDns}</p>
|
||||
<p>{t('ipChange-newIp')}: {ipData.newIp}</p>
|
||||
<p>{t('ipChange-newGateway')}: {ipData.newGate}</p>
|
||||
<p>{t('ipChange-newDns')}: {ipData.newDns}</p>
|
||||
</NewData>
|
||||
</DataContainer>
|
||||
</Information>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton title="Отмена" size='small' onClick={() => cancelIp(currentIp)}/>
|
||||
<MainButton title="Сохранить" size='small' onClick={() => saveIp(newIP)}/>
|
||||
<MainButton
|
||||
title={t('cancel')}
|
||||
size='small'
|
||||
onClick={() => cancelIp(currentIp)}
|
||||
/>
|
||||
<MainButton
|
||||
title={t('save')}
|
||||
size='small'
|
||||
onClick={() => saveIp(newIP)}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</ModalContent>
|
||||
) : (
|
||||
|
|
@ -188,19 +198,21 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
|||
<ModalText>
|
||||
{isAdmin ? (
|
||||
<>
|
||||
<h3>Произошла ошибка проверки корректности</h3>
|
||||
<ErrorMessage>{message}</ErrorMessage>
|
||||
<h3>{t('ipChange-checkError')}</h3>
|
||||
<ErrorMessage>{message}</ErrorMessage>
|
||||
</>
|
||||
|
||||
):(
|
||||
<h3>Для изменения сетевых настроек необходимо авторизироваться под администратором</h3>
|
||||
<h3>{t('ipChange-adminRequired')}</h3>
|
||||
)}
|
||||
|
||||
</ModalText>
|
||||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton title="Закрыть" size='small' onClick={smoothClose}/>
|
||||
<MainButton
|
||||
title={t('close')}
|
||||
size='small'
|
||||
onClick={smoothClose}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useCheckAdmin } from "../../../utils/checkAdmin"
|
|||
|
||||
import { buttonNames } from "../../../const/buttons/names"
|
||||
import { IconComponent } from "../../../const/buttons/icons"
|
||||
import {useTranslation} from "../../../utils/i18n/hooks";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
|
@ -17,9 +18,10 @@ interface Props {
|
|||
export const RightsConfirm = ({ onClose }: Props) => {
|
||||
|
||||
const { ipAddress } = useIpStore();
|
||||
const { isAdmin, setAdmin } = useAdminStore()
|
||||
const { isAdmin } = useAdminStore()
|
||||
const checkAdmin = useCheckAdmin();
|
||||
const [error, setError] = useState(false)
|
||||
const {t} = useTranslation('common');
|
||||
const [success, setSuccess] = useState({
|
||||
message: '',
|
||||
status: false
|
||||
|
|
@ -45,7 +47,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
|||
setError(false)
|
||||
setSuccess(prevData => ({
|
||||
...prevData,
|
||||
message: 'Успешный вход',
|
||||
message: t('auth-successLogin'),
|
||||
status: true
|
||||
}));
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
|
@ -54,20 +56,20 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
|||
setError(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
console.error(t('auth-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();
|
||||
await NetworkService.logout(ipAddress);
|
||||
await checkAdmin();
|
||||
setSuccess(prevData => ({
|
||||
...prevData,
|
||||
message: t('auth-successLogout'),
|
||||
status: true
|
||||
}));
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -76,60 +78,55 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
|||
!isAdmin ? (
|
||||
<>
|
||||
<ModalTitle>
|
||||
<h3>Получение прав доступа</h3>
|
||||
<h3>{t('auth-getAccess')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
|
||||
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
|
||||
|
||||
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>Включить административный доступ</h3>
|
||||
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>{t('auth-enableAdmin')}</h3>
|
||||
<Input
|
||||
type="password"
|
||||
placeholderText='Введите пароль администратора...'
|
||||
placeholderText={t('auth-enterAdminPass')}
|
||||
id="password"
|
||||
value={user.password}
|
||||
onChange={(value) => handleChange('password', value)}
|
||||
/>
|
||||
|
||||
|
||||
{error && (
|
||||
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
|
||||
Неверный пароль оператора
|
||||
{t('auth-wrongPass')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
</ModalContent>
|
||||
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton size="small" title="Отмена" onClick={onClose} />
|
||||
<MainButton size="small" title="Отправить" onClick={() => login(user)} />
|
||||
<MainButton size="small" title={t('cancel')} onClick={onClose} />
|
||||
<MainButton size="small" title={t('submit')} onClick={() => login(user)} />
|
||||
</ButtonContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalTitle>
|
||||
<h3>Переключиться на ограниченный доступ</h3>
|
||||
<h3>{t('auth-switchToLimited')}</h3>
|
||||
</ModalTitle>
|
||||
|
||||
|
||||
<ModalContent>
|
||||
<p>В данном режиме доступа ограничиваются полномочия администратора.</p>
|
||||
<p>{t('auth-limitedModeDesc')}</p>
|
||||
</ModalContent>
|
||||
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton size="small" title="Отмена" onClick={onClose} />
|
||||
<MainButton size="small" title="Ограничить доступ" onClick={logout} />
|
||||
<MainButton size="small" title={t('cancel')} onClick={onClose} />
|
||||
<MainButton size="small" title={t('auth-restrictAccess')} 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%'}}/>
|
||||
<p style={{fontSize: '12px'}}>{t('auth-closeHint')}</p>
|
||||
<MainButton size="small" title={t('close')} onClick={onClose} style={{width: '30%'}}/>
|
||||
</ModalContainer>
|
||||
)}
|
||||
|
||||
|
||||
</ModalContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { NoteWifi } from "./modalContainers/NoteWifi";
|
|||
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
||||
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
||||
import {Settings} from "../settings/Settings";
|
||||
import CustomizeNodes from "./modalContainers/customizeNodes";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean,
|
||||
|
|
@ -80,6 +81,9 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
|
|||
onClose={closeLoader}
|
||||
/>
|
||||
)}
|
||||
{type === ModalMap['11'] && (
|
||||
<CustomizeNodes />
|
||||
)}
|
||||
</ModalWindow >
|
||||
}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const ModalTitle = styled.div`
|
|||
padding: 20px;
|
||||
border-bottom: 1px solid #9D9D9D;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3{
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ export const ModalMap: ModalType = {
|
|||
'7': 'noteWifi',
|
||||
'8': 'connectNetwork',
|
||||
'9': 'disconnectConfirm',
|
||||
"10": 'settings',
|
||||
'11': 'customNodes',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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"
|
||||
import {useTranslation} from "../utils/i18n/hooks";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -73,6 +74,7 @@ export const CreateUser = () => {
|
|||
const [repeatPassword, setRepeatPassword] = useState('');
|
||||
const [passwordsMatch, setPasswordsMatch] = useState(true);
|
||||
const [about, setAbout] = useState(false)
|
||||
const {t} = useTranslation('common')
|
||||
|
||||
const [user, setUser] = useState({
|
||||
username: 'operator',
|
||||
|
|
@ -118,77 +120,81 @@ export const CreateUser = () => {
|
|||
setAbout(true)
|
||||
}
|
||||
return (
|
||||
|
||||
<>
|
||||
{!about ? (
|
||||
<Wrapper>
|
||||
<Container onSubmit={handleSubmit}>
|
||||
<Title>
|
||||
<IconComponent name={buttonNames[3]} width="52" height="52" />
|
||||
<h3>{t('operatorConfig-title')}</h3>
|
||||
{ipAddress != 'localhost' &&
|
||||
<p>{ipAddress}</p>
|
||||
}
|
||||
</Title>
|
||||
|
||||
<Container onSubmit={handleSubmit}>
|
||||
<Title>
|
||||
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent>
|
||||
<h3>Настройка оператора узла</h3>
|
||||
<Content>
|
||||
<div>
|
||||
<p>{t('operatorConfig-instruction')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholderText={t('operatorConfig-passwordPlaceholder')}
|
||||
id="password"
|
||||
value={user.password}
|
||||
onChange={(value) => handleChange('password', value)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
placeholderText={t('operatorConfig-repeatPasswordPlaceholder')}
|
||||
id="repeatPassword"
|
||||
value={repeatPassword}
|
||||
onChange={(value) => handleChange('repeatPassword', value)}
|
||||
style={{ marginTop: '10px' }}
|
||||
/>
|
||||
{!passwordsMatch && (
|
||||
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||
{t('operatorConfig-passwordMismatch')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton
|
||||
style={{ width: '100%' }}
|
||||
title={t('operatorConfig-submit')}
|
||||
type="submit"
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</Container>
|
||||
|
||||
<Menu>
|
||||
{ipAddress != 'localhost' &&
|
||||
<p>{ipAddress}</p>
|
||||
<MainButton
|
||||
title={t('common-back')}
|
||||
onClick={back}
|
||||
style={{marginRight: '10px'}}
|
||||
/>
|
||||
}
|
||||
|
||||
</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>
|
||||
<ThemeButton />
|
||||
<Button>
|
||||
<IconContainer>
|
||||
<IconComponent
|
||||
name={buttonNames[4]}
|
||||
width="30"
|
||||
height="30"
|
||||
onClick={toAbout}
|
||||
/>
|
||||
</IconContainer>
|
||||
</Button>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
): (
|
||||
</Wrapper>
|
||||
) : (
|
||||
<About from="reg" onBack={() => setAbout(false)} />
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@ import {buttonNames} from "../../const/buttons/names";
|
|||
import SettingSelect from "./SettingSelect";
|
||||
import {MainButton} from "../UI/buttons/mainButton";
|
||||
import React from 'react';
|
||||
import {useModalContext} from "../../store";
|
||||
import {ModalMap} from "../modalwindows/windowsType";
|
||||
import {useTranslation} from "../../utils/i18n/hooks";
|
||||
|
||||
const SettingBlock = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -19,6 +22,7 @@ const SettingBlock = styled.div`
|
|||
font-size: 16px;
|
||||
outline: none;
|
||||
background-color: ${props => props.theme.primary};
|
||||
color: ${props => props.theme.textColor};
|
||||
border: 1px solid ${props => props.theme.borderColor};
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
|
|
@ -73,7 +77,7 @@ const InputWrapper = styled.div`
|
|||
gap: 6px;
|
||||
`
|
||||
const ErrorText = styled.div`
|
||||
color: red;
|
||||
color: ${props => props.theme.errorColor};
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
|
|
@ -86,26 +90,65 @@ interface SettingItemProps {
|
|||
|
||||
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('common');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { openModal } = useModalContext();
|
||||
|
||||
const handleButtonAction = () => {
|
||||
if (setting.id === 'customize-nodes') {
|
||||
openModal(ModalMap['11']);
|
||||
} else if (setting.action) {
|
||||
// Создаем обертку для действия с переводами
|
||||
const actionWithTranslations = async () => {
|
||||
try {
|
||||
// Временно заменяем alert на версию с переводами
|
||||
const originalAlert = window.alert;
|
||||
window.alert = (message: string) => {
|
||||
// Применяем переводы к сообщениям
|
||||
let translatedMessage = message;
|
||||
if (message.startsWith('settings-dataNotDeleted')) {
|
||||
translatedMessage = t('settings-dataNotDeleted') + message.replace('settings-dataNotDeleted', '');
|
||||
} else if (message === 'settings-exportSuccess') {
|
||||
translatedMessage = t('settings-exportSuccess');
|
||||
} else if (message.startsWith('settings-exportError')) {
|
||||
translatedMessage = t('settings-exportError') + message.replace('settings-exportError', '');
|
||||
}
|
||||
originalAlert(translatedMessage);
|
||||
};
|
||||
|
||||
if (setting.action) {
|
||||
await setting.action();
|
||||
}
|
||||
|
||||
// Восстанавливаем оригинальный alert
|
||||
window.alert = originalAlert;
|
||||
} catch (error) {
|
||||
console.error('Action error:', error);
|
||||
}
|
||||
};
|
||||
actionWithTranslations();
|
||||
}
|
||||
};
|
||||
|
||||
// Изменение числа
|
||||
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value;
|
||||
let newError: string | null = null;
|
||||
|
||||
if (inputValue.trim() === '') {
|
||||
newError = 'Поле не может быть пустым';
|
||||
newError = t('settings-fieldRequired');
|
||||
onChange(null);
|
||||
}
|
||||
else if (isNaN(Number(inputValue))) {
|
||||
newError = 'Пожалуйста, введите число';
|
||||
newError = t('settings-enterNumber');
|
||||
}
|
||||
else {
|
||||
const numValue = Number(inputValue);
|
||||
if (setting.min !== undefined && numValue < setting.min) {
|
||||
newError = `Значение не может быть меньше ${setting.min}`;
|
||||
newError = t('settings-valueTooSmall') + setting.min;
|
||||
}
|
||||
else if (setting.max !== undefined && numValue > setting.max) {
|
||||
newError = `Значение не может быть больше ${setting.max}`;
|
||||
newError = t('settings-valueTooLarge') + setting.max;
|
||||
}
|
||||
|
||||
if (!newError) {
|
||||
|
|
@ -172,9 +215,10 @@ const SettingItem = React.memo(({ setting, value, onChange, onValidationError }:
|
|||
type="button"
|
||||
size="small"
|
||||
title={setting.defaultValue || ''}
|
||||
onClick={setting.action}
|
||||
onClick={handleButtonAction}
|
||||
/>
|
||||
)}
|
||||
{setting.type === 'hidden' && null}
|
||||
</SettingBlock>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
|
||||
import { MainButton } from "../UI/buttons/mainButton";
|
||||
import {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import styled from "styled-components";
|
||||
import styled, {useTheme} from "styled-components";
|
||||
import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
|
||||
import {SettingItem} from "./SettingItem";
|
||||
import {IconComponent} from "../../const/buttons/icons";
|
||||
import {buttonNames} from "../../const/buttons/names";
|
||||
import {ModalMap} from "../modalwindows/windowsType";
|
||||
import {workSpaceNames} from "../../const/workspace/names";
|
||||
import {useTranslation} from "../../utils/i18n/hooks";
|
||||
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
|
@ -49,6 +49,16 @@ const SectionLine = styled.span`
|
|||
const CloseBtn = styled.span`
|
||||
cursor: pointer;
|
||||
`
|
||||
const SettingsButtons = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
span{
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
`
|
||||
export const Settings = ({ onClose }: Props) => {
|
||||
const settingsConfig = useSettingsStore(state => state.settingsConfig);
|
||||
const settingsValues = useSettingsStore(state => state.settingsValues);
|
||||
|
|
@ -59,10 +69,16 @@ export const Settings = ({ onClose }: Props) => {
|
|||
const applyChanges = useSettingsStore(state => state.applyChanges);
|
||||
const discardChanges = useSettingsStore(state => state.discardChanges);
|
||||
const getSetting = useSettingsStore(state => state.getSetting);
|
||||
const resetToDefaults = useSettingsStore(state => state.resetToDefaults);
|
||||
const initializeSettings = useSettingsStore(state => state.initializeSettings);
|
||||
const updateSettingsConfig = useSettingsStore(state => state.updateSettingsConfig);
|
||||
|
||||
const theme = useTheme()
|
||||
const { t, language } = useTranslation('common');
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
const [currentLanguage, setCurrentLanguage] = useState(language);
|
||||
|
||||
// Проверка действий
|
||||
const handleValidationError = useCallback((fieldId: string, error: string | null) => {
|
||||
setFieldErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
|
|
@ -77,22 +93,33 @@ export const Settings = ({ onClose }: Props) => {
|
|||
|
||||
const settingsBySection = useMemo(() => {
|
||||
return settingsConfig.reduce((acc, setting) => {
|
||||
if (!acc[setting.sectionId]) {
|
||||
acc[setting.sectionId] = [];
|
||||
}
|
||||
acc[setting.sectionId].push({
|
||||
// Применяем переводы к настройкам
|
||||
const translatedSetting = {
|
||||
...setting,
|
||||
sectionId: t(setting.sectionId),
|
||||
label: t(setting.label),
|
||||
defaultValue: typeof setting.defaultValue === 'string' && setting.defaultValue.startsWith('settings-') ? t(setting.defaultValue) : setting.defaultValue,
|
||||
options: setting.options ? setting.options.map((option: any) => ({
|
||||
...option,
|
||||
label: option.label.startsWith('settings-') ? t(option.label) : option.label
|
||||
})) : setting.options,
|
||||
value: settingsValues[setting.id] ?? setting.defaultValue
|
||||
});
|
||||
};
|
||||
|
||||
if (!acc[translatedSetting.sectionId]) {
|
||||
acc[translatedSetting.sectionId] = [];
|
||||
}
|
||||
acc[translatedSetting.sectionId].push(translatedSetting);
|
||||
return acc;
|
||||
}, {} as Record<string, Setting[]>);
|
||||
}, [settingsConfig, settingsValues]);
|
||||
}, [settingsConfig, settingsValues, t]);
|
||||
|
||||
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSettingsConfig();
|
||||
initializeSettings();
|
||||
}, [initializeSettings]);
|
||||
}, [updateSettingsConfig, initializeSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sections.length > 0 && !activeSection) {
|
||||
|
|
@ -100,10 +127,15 @@ export const Settings = ({ onClose }: Props) => {
|
|||
}
|
||||
}, [sections, activeSection, setActiveSection]);
|
||||
|
||||
// Обновляем текущий язык при изменении
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
|
||||
if (hasErrors) {
|
||||
alert('Исправьте ошибки перед сохранением');
|
||||
alert(t('settings-fixErrors'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -114,13 +146,24 @@ export const Settings = ({ onClose }: Props) => {
|
|||
});
|
||||
|
||||
if (emptyRequiredFields.length > 0) {
|
||||
alert('Заполните все обязательные поля');
|
||||
alert(t('settings-fillRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, изменился ли язык
|
||||
const newLanguage = pendingChanges['language'] ?? settingsValues['language'];
|
||||
const languageChanged = newLanguage && newLanguage !== currentLanguage;
|
||||
|
||||
applyChanges();
|
||||
onClose();
|
||||
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose]);
|
||||
|
||||
// Если язык изменился, перезагружаем приложение
|
||||
if (languageChanged) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 100);
|
||||
}
|
||||
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose, t, currentLanguage]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
discardChanges();
|
||||
|
|
@ -128,10 +171,31 @@ export const Settings = ({ onClose }: Props) => {
|
|||
onClose();
|
||||
}, [discardChanges, onClose]);
|
||||
|
||||
const handleReset = () => {
|
||||
if (window.confirm(t('settings-resetConfirm'))) {
|
||||
resetToDefaults();
|
||||
}
|
||||
};
|
||||
|
||||
// Обработка нажатий клавиш
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter')
|
||||
handleSave();
|
||||
if (e.key === 'Escape')
|
||||
handleCancel()
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleSave, handleCancel]);
|
||||
|
||||
|
||||
return (
|
||||
<ModalContainer style={{ height: '60vh', width: '50vw'}}>
|
||||
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
|
||||
<ModalTitle>
|
||||
<h3>Настройки</h3>
|
||||
<h3>{t('settings-title')}</h3>
|
||||
<CloseBtn>
|
||||
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
|
||||
</CloseBtn>
|
||||
|
|
@ -168,13 +232,32 @@ export const Settings = ({ onClose }: Props) => {
|
|||
</ModalContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title='Сохранить'
|
||||
onClick={handleSave}
|
||||
|
||||
/>
|
||||
<SettingsButtons>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title={t('settings-resetSettings')}
|
||||
onClick={handleReset}
|
||||
color={theme.offlineColor}
|
||||
hoverColor={theme.borderColor}
|
||||
/>
|
||||
<span>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title={t('settings-cancel')}
|
||||
onClick={handleCancel}
|
||||
color={theme.offlineColor}
|
||||
hoverColor={theme.borderColor}
|
||||
/>
|
||||
<MainButton
|
||||
type="button"
|
||||
size="small"
|
||||
title={t('settings-save')}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</span>
|
||||
</SettingsButtons>
|
||||
</ButtonContainer>
|
||||
</ModalContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import styled, {useTheme} from "styled-components"
|
|||
import ThemeButton from "../../../UI/buttons/toggletheme"
|
||||
import {buttonNames} from "../../../../const/buttons/names";
|
||||
import {IconComponent} from "../../../../const/buttons/icons";
|
||||
import {useTranslation} from "../../../../utils/i18n/hooks";
|
||||
|
||||
type Props = {
|
||||
from: 'reg' | 'helloWindow' | 'workSpace'
|
||||
|
|
@ -112,7 +113,7 @@ const LinkBlock = styled.a`
|
|||
|
||||
const Copyright = styled.div`
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
text-align: center;
|
||||
color: ${(props => props.theme.offlineColor)};
|
||||
`
|
||||
|
||||
|
|
@ -158,6 +159,7 @@ const MainTitle = styled.div`
|
|||
|
||||
export default function About({ from, onBack }: Props) {
|
||||
const { setWorkModule } = useWorkSpaceStore()
|
||||
const {t} = useTranslation('common')
|
||||
const getBackHandler = () => {
|
||||
switch (from) {
|
||||
case 'reg':
|
||||
|
|
@ -177,11 +179,12 @@ export default function About({ from, onBack }: Props) {
|
|||
return (
|
||||
<Wrapper>
|
||||
<Content>
|
||||
|
||||
<MainTitle>
|
||||
<div className={'title'}>
|
||||
<img src="./assets/images/gossys.png" alt="" />
|
||||
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}><h1>О приложении</h1></div>
|
||||
<img src="./assets/images/gossys.png" alt={t('about-logoAlt')} />
|
||||
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}>
|
||||
<h1>{t('about-title')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className={'version'}>v 0.0.3</p>
|
||||
</MainTitle>
|
||||
|
|
@ -189,15 +192,15 @@ export default function About({ from, onBack }: Props) {
|
|||
<AboutLine/>
|
||||
|
||||
<Title>
|
||||
<p>ГоNets — минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.</p>
|
||||
<p>{t('about-description')}</p>
|
||||
</Title>
|
||||
|
||||
<Listing>
|
||||
<p>Возможности:</p>
|
||||
<p>{t('about-featuresTitle')}:</p>
|
||||
<List>
|
||||
<li>Визуализация активных сетевых ресурсов</li>
|
||||
<li>Настройка параметров локальных и удалённых сетевых ресурсов</li>
|
||||
<li>Управление взаимодействием ресурсов с Клиентом ГoNets</li>
|
||||
<li>{t('about-feature1')}</li>
|
||||
<li>{t('about-feature2')}</li>
|
||||
<li>{t('about-feature3')}</li>
|
||||
</List>
|
||||
</Listing>
|
||||
|
||||
|
|
@ -205,26 +208,28 @@ export default function About({ from, onBack }: Props) {
|
|||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2"/>
|
||||
</svg>
|
||||
<p>Программное обеспечение исключительно для ознакомления и домашнего использования.</p>
|
||||
<a href={'https://gossys.dev/pages/projects/gonets'}>Перейти на ГоNets-PRO</a>
|
||||
<p>{t('about-disclaimer')}</p>
|
||||
<a href={'https://gossys.dev/pages/projects/gonets'}>
|
||||
{t('about-goToPro')}
|
||||
</a>
|
||||
</SubTitle>
|
||||
|
||||
<Links>
|
||||
<LinkBlock href={'https://gossys.dev/pages/projects/gonets'}>
|
||||
Домашняя страницa
|
||||
{t('about-homepage')}
|
||||
</LinkBlock>
|
||||
|
||||
<LinkBlock href={'https://wiki.gossys.dev/ru/gonets'}>
|
||||
Документация
|
||||
{t('about-documentation')}
|
||||
</LinkBlock>
|
||||
|
||||
<LinkBlock href={'https://wiki.gossys.dev/ru/lic/GosysPUB'}>
|
||||
Лицензия
|
||||
{t('about-license')}
|
||||
</LinkBlock>
|
||||
</Links>
|
||||
|
||||
<Copyright>
|
||||
Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.
|
||||
{t('about-copyright')}
|
||||
</Copyright>
|
||||
|
||||
<ButtonContainer>
|
||||
|
|
@ -235,7 +240,6 @@ export default function About({ from, onBack }: Props) {
|
|||
name={buttonNames[12]}
|
||||
onClick={backHandler}
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
{from == 'reg' &&
|
||||
|
|
@ -243,7 +247,6 @@ export default function About({ from, onBack }: Props) {
|
|||
}
|
||||
</ButtonContainer>
|
||||
</Content>
|
||||
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { useWorkSpaceStore } from "../../../../store"
|
||||
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||
import { buttonNames } from "../../../../const/buttons/names"
|
||||
import {useTranslation} from "../../../../utils/i18n/hooks";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
|
|
@ -49,30 +50,33 @@
|
|||
`
|
||||
|
||||
export const HelloWindow = () => {
|
||||
|
||||
const {t} = useTranslation('common')
|
||||
const { setWorkModule } = useWorkSpaceStore()
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Container className="unselectable">
|
||||
<Title>
|
||||
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
|
||||
<img
|
||||
src="./assets/images/logo.svg"
|
||||
alt={t('main-logoAlt')}
|
||||
style={{width: '152px'}}
|
||||
/>
|
||||
</Title>
|
||||
|
||||
|
||||
<ButtonWrapper>
|
||||
<ButtonContainer>
|
||||
<MainButton
|
||||
title="Настройки сети"
|
||||
title={t('main-networkSettings')}
|
||||
icon={buttonNames[3]}
|
||||
iconSize={{ width: '32', height: '32' }}
|
||||
onClick={() => setWorkModule(workSpaceNames[3])}
|
||||
/>
|
||||
<MainButton
|
||||
title="О программе"
|
||||
title={t('main-about')}
|
||||
icon={buttonNames[4]}
|
||||
iconSize={{ width: '32', height: '32' }}
|
||||
onClick={()=>setWorkModule(workSpaceNames[4])}
|
||||
onClick={() => setWorkModule(workSpaceNames[4])}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</ButtonWrapper>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import React, {useState} from 'react';
|
||||
import styled, {keyframes} from 'styled-components';
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
setIpScanning: (value: string) => void;
|
||||
loading: boolean;
|
||||
isValid: boolean;
|
||||
setIsValid: (value: boolean) => void;
|
||||
}
|
||||
const Input = styled.input`
|
||||
height: 40px;
|
||||
width: 210px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-bottom: 2px solid ${props => props.theme.offlineColor};
|
||||
color: ${props => props.theme.textColor};
|
||||
transition: all 0.3s ease;
|
||||
&:focus {
|
||||
border-bottom: 2px solid ${props => props.theme.accent};
|
||||
width: 270px;
|
||||
}
|
||||
&::placeholder {
|
||||
color: ${props => props.theme.offlineColor};
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
`
|
||||
const errorVisible = keyframes`
|
||||
0%{opacity: 0}
|
||||
100%{opacity: 1}
|
||||
`;
|
||||
const ErrorMessage = styled.span`
|
||||
margin-top: 6px;
|
||||
color: ${props=>props.theme.errorColor};
|
||||
animation: ${errorVisible} .4s linear;
|
||||
`
|
||||
|
||||
|
||||
const IpScanning = ({value, setIpScanning, loading, isValid, setIsValid}: Props) => {
|
||||
const {t} = useTranslation('common')
|
||||
const validateInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const allowedKeys = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'.', '/',
|
||||
'Backspace', 'Delete',
|
||||
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'
|
||||
];
|
||||
|
||||
if (!allowedKeys.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIpScanning(e.target.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={t('ipInput-placeholder')}
|
||||
onKeyDown={validateInput}
|
||||
disabled={loading}
|
||||
/>
|
||||
{!isValid && !loading && (
|
||||
<ErrorMessage>{t('ipInput-error')}</ErrorMessage>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IpScanning;
|
||||
|
|
@ -4,6 +4,7 @@ import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
|
|||
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||
import {usePaginateSetting} from "../../../../../../hooks/useSettings";
|
||||
import Pagination from "../../../../../UI/Pagination";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
const ScanBtn = styled.button`
|
||||
position: relative;
|
||||
|
|
@ -27,6 +28,7 @@ interface ListUsersProps {
|
|||
}
|
||||
|
||||
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
||||
const {t} = useTranslation('common')
|
||||
const activeDevices = devices.filter(device => device.status === 'online');
|
||||
const inactiveDevices = devices.filter(device => device.status !== 'online');
|
||||
|
||||
|
|
@ -55,11 +57,10 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
setCurrentPage(page);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScanBtn onClick={handleScan} disabled={loading}>
|
||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'}
|
||||
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
|
||||
</ScanBtn>
|
||||
|
||||
{devices.length > 0 ? (
|
||||
|
|
@ -70,7 +71,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
}}>
|
||||
{/* Колонка активных устройств */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные узлы ({activeDevices.length})</h3>
|
||||
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>
|
||||
{t('listUsers-activeNodes')} ({activeDevices.length})
|
||||
</h3>
|
||||
{currentActiveDevices.map((device, index) => (
|
||||
<DeviceCard
|
||||
key={index}
|
||||
|
|
@ -81,7 +84,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
</div>
|
||||
{/* Колонка неактивных устройств */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные узлы ({inactiveDevices.length})</h3>
|
||||
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>
|
||||
{t('listUsers-inactiveNodes')} ({inactiveDevices.length})
|
||||
</h3>
|
||||
{currentInactiveDevices.map((device, index) => (
|
||||
<DeviceCard
|
||||
key={index}
|
||||
|
|
@ -91,8 +96,8 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : scanInitiated ? (
|
||||
<p style={{ marginTop: '20px' }}>Узлы не найдены</p>
|
||||
) : scanInitiated && !loading ? (
|
||||
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
|
||||
) : null}
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useMemo, useState} 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";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
import Pagination from "../../../../../UI/Pagination";
|
||||
import {useSettingsStore} from "../../../../../../store/settings/settingsStore";
|
||||
import {useNameSetting, usePaginateSetting} from "../../../../../../hooks/useSettings";
|
||||
import {
|
||||
useDisabledColumns,
|
||||
useNameSetting,
|
||||
useOrdersColumns,
|
||||
usePaginateSetting
|
||||
} from "../../../../../../hooks/useSettings";
|
||||
|
||||
interface TableScannerProps {
|
||||
devices: DeviceUser[];
|
||||
}
|
||||
|
||||
const TableContainer = styled.div`
|
||||
width: 100%;
|
||||
|
|
@ -65,87 +70,199 @@ const StatusBadge = styled.span<{ status:string }>`
|
|||
border-radius:4px;
|
||||
`
|
||||
|
||||
const TableScanner = ({devices}:TableScannerProps) => {
|
||||
export const TableScanner = ({ devices }: TableScannerProps) => {
|
||||
const { openModal } = useModalContext();
|
||||
const nameSetting = useNameSetting();
|
||||
const ordersColumns = useOrdersColumns();
|
||||
const disabledColumns = useDisabledColumns();
|
||||
const paginateSetting = usePaginateSetting();
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
|
||||
|
||||
console.log(ordersColumns);
|
||||
|
||||
useEffect(() => {
|
||||
setDevicesPerPage(paginateSetting);
|
||||
}, [paginateSetting]);
|
||||
|
||||
const openModalNote = (e: React.MouseEvent, device: DeviceUser) => {
|
||||
e.stopPropagation();
|
||||
openModal(ModalMap['6'], {
|
||||
address: device.mac,
|
||||
currentNote: device.note || ''
|
||||
});
|
||||
};
|
||||
const nameSetting = useNameSetting();
|
||||
|
||||
const defaultOrder = useMemo(() => ([
|
||||
t('settings-status'),
|
||||
t('settings-name'),
|
||||
t('settings-vendor'),
|
||||
t('settings-ipAddress'),
|
||||
t('settings-openPorts'),
|
||||
t('settings-lastActivity'),
|
||||
t('settings-note'),
|
||||
t('settings-action'),
|
||||
]), [t]);
|
||||
|
||||
const normalizedOrders = useMemo(
|
||||
() => (Array.isArray(ordersColumns) && ordersColumns.length ? ordersColumns : defaultOrder),
|
||||
[ordersColumns, defaultOrder]
|
||||
);
|
||||
const normalizedDisabled = useMemo(
|
||||
() => (Array.isArray(disabledColumns) ? disabledColumns : []),
|
||||
[disabledColumns]
|
||||
);
|
||||
|
||||
const visibleColumns = useMemo(() => {
|
||||
const normalizeColumnName = (column: string) => column?.toLowerCase();
|
||||
|
||||
const columnToKey = (column: string) => {
|
||||
const col = normalizeColumnName(column);
|
||||
switch (col) {
|
||||
case 'settings-status':
|
||||
case normalizeColumnName(t('settings-status')):
|
||||
case 'онлайн':
|
||||
case 'status':
|
||||
return 'status';
|
||||
case 'settings-name':
|
||||
case normalizeColumnName(t('settings-name')):
|
||||
case 'имя':
|
||||
case 'name':
|
||||
return 'name';
|
||||
case 'settings-vendor':
|
||||
case normalizeColumnName(t('settings-vendor')):
|
||||
case 'производитель':
|
||||
case 'vendor':
|
||||
return 'vendor';
|
||||
case 'settings-ipaddress':
|
||||
case normalizeColumnName(t('settings-ipAddress')):
|
||||
case 'ip адрес':
|
||||
case 'ip address':
|
||||
case 'ip':
|
||||
return 'ip';
|
||||
case 'settings-openports':
|
||||
case normalizeColumnName(t('settings-openPorts')):
|
||||
case 'открытые порты':
|
||||
case 'open ports':
|
||||
case 'ports':
|
||||
return 'ports';
|
||||
case 'settings-lastactivity':
|
||||
case normalizeColumnName(t('settings-lastActivity')):
|
||||
case 'последняя активность':
|
||||
case 'last activity':
|
||||
case 'activity':
|
||||
return 'activity';
|
||||
case 'settings-note':
|
||||
case normalizeColumnName(t('settings-note')):
|
||||
case 'заметка':
|
||||
case 'note':
|
||||
return 'note';
|
||||
case 'settings-action':
|
||||
case normalizeColumnName(t('settings-action')):
|
||||
case 'действие':
|
||||
case 'action':
|
||||
return 'action';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return normalizedOrders
|
||||
.map((column, index) => ({ column, index }))
|
||||
.filter(({ index }) => !normalizedDisabled.includes(index))
|
||||
.map(({ column }) => columnToKey(column))
|
||||
.filter(Boolean);
|
||||
}, [normalizedOrders, normalizedDisabled, t]);
|
||||
|
||||
// Пагинация
|
||||
const paginateSetting = usePaginateSetting();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
|
||||
|
||||
useEffect(() => {
|
||||
setDevicesPerPage(paginateSetting);
|
||||
}, [paginateSetting]);
|
||||
|
||||
const indexOfLastDevice = currentPage * devicesPerPage;
|
||||
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
|
||||
const currentDevices = devices.slice(indexOfFirstDevice, indexOfLastDevice);
|
||||
const totalPages = Math.ceil(devices.length / devicesPerPage);
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if(currentPage > 1) setCurrentPage(currentPage - 1);
|
||||
}
|
||||
if (currentPage > 1) setCurrentPage(currentPage - 1);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if(currentPage < totalPages) setCurrentPage(currentPage + 1);
|
||||
}
|
||||
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
|
||||
};
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
// Рендер ячейки таблицы
|
||||
const renderTableCell = (column: string, item: DeviceUser) => {
|
||||
switch (column) {
|
||||
case 'status':
|
||||
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
|
||||
case 'name':
|
||||
return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>;
|
||||
case 'vendor':
|
||||
return <TableCell key="vendor">{item.vendor}</TableCell>;
|
||||
case 'ip':
|
||||
return <TableCell key="ip">{item.ip}</TableCell>;
|
||||
case 'ports':
|
||||
return <TableCell key="ports">{item.open_ports.join(', ') || '-'}</TableCell>;
|
||||
case 'activity':
|
||||
return <TableCell key="activity">{item.last_seen}</TableCell>;
|
||||
case 'note':
|
||||
return <TableCell key="note">{item.note || '-'}</TableCell>;
|
||||
case 'action':
|
||||
return (
|
||||
<TableCell key="action">
|
||||
<button onClick={(e) => openModalNote(e, item)}>
|
||||
<IconComponent name={buttonNames[16]} width="26" height="26" />
|
||||
</button>
|
||||
</TableCell>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell></TableHeaderCell>
|
||||
<TableHeaderCell>Имя</TableHeaderCell>
|
||||
<TableHeaderCell>Производитель</TableHeaderCell>
|
||||
<TableHeaderCell>IP адрес</TableHeaderCell>
|
||||
<TableHeaderCell>Открытые порты</TableHeaderCell>
|
||||
<TableHeaderCell>Последняя активность</TableHeaderCell>
|
||||
<TableHeaderCell>Заметка</TableHeaderCell>
|
||||
<TableHeaderCell></TableHeaderCell>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentDevices.map((item, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
|
||||
<TableCell>{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</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>
|
||||
<button onClick={(e)=>openModalNote(e, item)}>
|
||||
<IconComponent name={buttonNames[16]} width="26" height="26" />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
onPageChange={onPageChange}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPrevPage={handlePrevPage}
|
||||
onNextPage={handleNextPage}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
<TableContainer>
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
{visibleColumns.map(column => {
|
||||
switch (column) {
|
||||
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
|
||||
case 'name': return <TableHeaderCell key="name">{t('settings-name')}</TableHeaderCell>;
|
||||
case 'vendor': return <TableHeaderCell key="vendor">{t('settings-vendor')}</TableHeaderCell>;
|
||||
case 'ip': return <TableHeaderCell key="ip">{t('settings-ipAddress')}</TableHeaderCell>;
|
||||
case 'ports': return <TableHeaderCell key="ports">{t('settings-openPorts')}</TableHeaderCell>;
|
||||
case 'activity': return <TableHeaderCell key="activity">{t('settings-lastActivity')}</TableHeaderCell>;
|
||||
case 'note': return <TableHeaderCell key="note">{t('settings-note')}</TableHeaderCell>;
|
||||
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
|
||||
default: return null;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentDevices.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
{visibleColumns.map(column => renderTableCell(column, item))}
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
|
||||
export default TableScanner;
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
onPageChange={onPageChange}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPrevPage={handlePrevPage}
|
||||
onNextPage={handleNextPage}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
|
||||
import {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||
import TableScanner from "./TableScanner";
|
||||
import {TableScanner} from "./TableScanner";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
const ScanBtn = styled.button`
|
||||
position: relative;
|
||||
|
|
@ -26,17 +26,18 @@ interface ListUsersProps {
|
|||
}
|
||||
|
||||
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
|
||||
const {t} = useTranslation('common')
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScanBtn onClick={()=>handleScan()} disabled={loading}>
|
||||
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'}
|
||||
<ScanBtn onClick={() => handleScan()} disabled={loading}>
|
||||
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
|
||||
</ScanBtn>
|
||||
|
||||
{devices.length > 0 ? (
|
||||
<TableScanner devices={devices} />
|
||||
) : scanInitiated ? (
|
||||
<p style={{ marginTop: '20px' }}>Узлы не найдены</p>
|
||||
) : scanInitiated && !loading ? (
|
||||
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { useIpStore } from "../../../../../store";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useEffect, useState, useTransition} from "react";
|
||||
import { NetworkService } from "../../../../../api/request";
|
||||
import ListUsers from "./modules/ListUsers";
|
||||
import TableUsers from "./modules/TableUsers";
|
||||
import DropdownType from "../../../../UI/Dropdown/DropdownType";
|
||||
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
|
||||
import IpScanning from "./modules/IpScanning";
|
||||
import {useTranslation} from "../../../../../utils/i18n/hooks";
|
||||
|
||||
|
||||
type Option ={
|
||||
value: number,
|
||||
|
|
@ -52,7 +55,7 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${(props) => props.theme.background};
|
||||
padding: 50px;
|
||||
padding: 80px 50px 40px;
|
||||
box-sizing: border-box;
|
||||
gap: 24px;
|
||||
transition: justify-content .4s ease;
|
||||
|
|
@ -60,13 +63,20 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
|
|||
overflow-x: hidden;
|
||||
position: relative;
|
||||
`;
|
||||
const TitleWrapper = styled.h2`
|
||||
const TopWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`
|
||||
const TopTitle = styled.h2`
|
||||
font-size: 32px;
|
||||
width: max-content;
|
||||
text-align: center;
|
||||
`
|
||||
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||
width: 60px;
|
||||
|
|
@ -131,24 +141,39 @@ const ScanningText = styled.span`
|
|||
|
||||
|
||||
export default function UserScanner() {
|
||||
const {t} = useTranslation('common')
|
||||
const { ipAddress } = useIpStore();
|
||||
const [ipScanning, setIpScanning] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
const { devices, setDevices } = useNetworkStore();
|
||||
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
|
||||
|
||||
const validateIp = (ip: string) => {
|
||||
const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
|
||||
return regex.test(ip);
|
||||
};
|
||||
|
||||
// Сканирование
|
||||
const handleScan = async (replay:boolean) => {
|
||||
const handleScan = async (replay: boolean) => {
|
||||
try {
|
||||
if(!replay){
|
||||
if (ipScanning && !validateIp(ipScanning)) {
|
||||
setIsValid(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValid(true);
|
||||
|
||||
if (!replay) {
|
||||
setLoading(true);
|
||||
}
|
||||
setScanInitiated(true);
|
||||
const res = await NetworkService.scanUser(ipAddress);
|
||||
console.log(res);
|
||||
const res = await NetworkService.scanUser(ipAddress, ipScanning);
|
||||
console.log(res)
|
||||
setDevices(res.data.devices || []);
|
||||
} catch (err) {
|
||||
console.error("Ошибка сканирования сети:", err);
|
||||
setDevices([])
|
||||
setDevices([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -173,8 +198,8 @@ export default function UserScanner() {
|
|||
|
||||
// Способ отображения
|
||||
const viewOptions: Option[] = [
|
||||
{ value: 1, label: 'Таблица' },
|
||||
{ value: 2, label: 'Список' },
|
||||
{ value: 1, label: t('userScanner-visible_table') },
|
||||
{ value: 2, label: t('userScanner-visible_list') },
|
||||
];
|
||||
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
|
||||
const handleViewChange = (option: Option) => {
|
||||
|
|
@ -183,7 +208,18 @@ export default function UserScanner() {
|
|||
|
||||
return (
|
||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов подсети</TitleWrapper>)}
|
||||
|
||||
<TopWrapper>
|
||||
{(!scanInitiated || loading) && (<TopTitle>{t('userScanner-title')}</TopTitle>)}
|
||||
<IpScanning
|
||||
value={ipScanning}
|
||||
setIpScanning={setIpScanning}
|
||||
loading={loading}
|
||||
isValid={isValid}
|
||||
setIsValid={setIsValid}
|
||||
/>
|
||||
</TopWrapper>
|
||||
|
||||
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||
<span className="center"></span>
|
||||
<span className="ring1"></span>
|
||||
|
|
@ -192,12 +228,12 @@ export default function UserScanner() {
|
|||
</UserLocate>
|
||||
|
||||
{scanInitiated && !loading && (
|
||||
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
|
||||
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<LoadingText>
|
||||
Сканирование узлов <ScanningText />
|
||||
{t('userScanner-loading')} <ScanningText />
|
||||
</LoadingText>
|
||||
) : selectedView.value == 1 ? (
|
||||
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
|
|||
import ModalWifiScaner from "./modules/ModalWifiScaner";
|
||||
import useNetworkStore from "../../../../../store/useDeviceStore";
|
||||
import {useIpStore} from "../../../../../store";
|
||||
import {useTranslation} from "../../../../../utils/i18n/hooks";
|
||||
|
||||
// Анимации
|
||||
const ring1 = keyframes`
|
||||
|
|
@ -57,6 +58,7 @@ const TitleWrapper = styled.h2`
|
|||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
|
||||
|
|
@ -136,33 +138,34 @@ const ScanBtn = styled.button<{scanInitiated: boolean}>`
|
|||
`
|
||||
|
||||
export function WifiScanner() {
|
||||
//const { ipAddress } = useIpStore();
|
||||
const ipAddress = '192.168.1.147'
|
||||
const { ipAddress } = useIpStore();
|
||||
//const ipAddress = '192.168.1.148'
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scanInitiated, setScanInitiated] = useState(false);
|
||||
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
|
||||
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const handleScan = async () => {
|
||||
const handleScan = async () => {
|
||||
setWifiNetworks([]);
|
||||
setCurrentNetwork(null);
|
||||
setLoading(true);
|
||||
setScanInitiated(true);
|
||||
try {
|
||||
const res = await NetworkService.scanWifi(ipAddress);
|
||||
const networks = res.data.access_points || [];
|
||||
setWifiNetworks(networks);
|
||||
} catch (err) {
|
||||
console.error(t('wifiScanner-scanError'), err);
|
||||
setWifiNetworks([]);
|
||||
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);
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper scanInitiated={scanInitiated} loading={loading}>
|
||||
<TitleWrapper>Сканирование точек доступа Wi-Fi сети</TitleWrapper>
|
||||
<TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
|
||||
|
||||
<UserLocate loading={loading} scanInitiated={scanInitiated}>
|
||||
<span className="center circel"></span>
|
||||
|
|
@ -189,11 +192,15 @@ export function WifiScanner() {
|
|||
|
||||
{loading ? (
|
||||
<LoadingText>
|
||||
Сканирование точек доступа <ScanningText/>
|
||||
{t('wifiScanner-scanning')} <ScanningText/>
|
||||
</LoadingText>
|
||||
) : (
|
||||
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
|
||||
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'}
|
||||
<ScanBtn
|
||||
onClick={handleScan}
|
||||
disabled={loading}
|
||||
scanInitiated={wifiNetworks.length > 0}
|
||||
>
|
||||
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
|
||||
</ScanBtn>
|
||||
)}
|
||||
</Wrapper>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import SignalSVG from "../../../../../UI/signalSvg";
|
|||
import { useTheme } from 'styled-components';
|
||||
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
|
||||
import {NetworkService} from "../../../../../../api/request";
|
||||
import {useTranslation} from "../../../../../../utils/i18n/hooks";
|
||||
|
||||
const Modal = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -114,6 +115,7 @@ const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onC
|
|||
const [signal, setSignal] = useState(1);
|
||||
const { wifiNetworks } = useNetworkStore();
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
useEffect(() => {
|
||||
const getStatus = async () => {
|
||||
|
|
@ -174,10 +176,9 @@ const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onC
|
|||
<ModalTitle>
|
||||
<h4>{network.ssid}</h4>
|
||||
<div className={'functionalIcons'}>
|
||||
|
||||
<span onClick={openModalNote} style={{cursor: "pointer"}}>
|
||||
<IconComponent name={buttonNames[16]} width="30" height="30" />
|
||||
</span>
|
||||
<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>
|
||||
|
|
@ -185,40 +186,42 @@ const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onC
|
|||
</ModalTitle>
|
||||
<ModalContent>
|
||||
<ModalRow>
|
||||
<h6>BSSID:</h6>
|
||||
<h6>{t('wifiModal-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>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-vendor')}:</h6>
|
||||
<p>{network.vendor || t('wifiModal-unknown')}</p>
|
||||
</ModalRow>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-signal')}:</h6>
|
||||
<p>{network.signal} dBm</p>
|
||||
</ModalRow>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-channel')}:</h6>
|
||||
<p>{network.channel}</p>
|
||||
</ModalRow>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-encryption')}:</h6>
|
||||
<p>{network.encryption || t('wifiModal-none')}</p>
|
||||
</ModalRow>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-frequency')}:</h6>
|
||||
<p>{network.frequency} MHz</p>
|
||||
</ModalRow>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-hidden')}:</h6>
|
||||
<p>{network.hidden ? t('common-yes') : t('common-no')}</p>
|
||||
</ModalRow>
|
||||
<ModalRow>
|
||||
<h6>{t('wifiModal-note')}:</h6>
|
||||
<p>{currentNetwork.note || '-'}</p>
|
||||
</ModalRow>
|
||||
<ModalConnect onClick={openModalNetwork}>
|
||||
{isConnected ? t('wifiModal-disconnect') : t('wifiModal-connect')}
|
||||
</ModalConnect>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
|
|||
setHasInited(false)
|
||||
initializationAttempt.current = 0;
|
||||
|
||||
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
|
||||
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
|
||||
|
||||
let connectionTimeout;
|
||||
setSocket(newSocket);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const ToolBar = () => {
|
|||
</IconContainer>
|
||||
</Button>
|
||||
|
||||
<ThemeButton />
|
||||
{/*<ThemeButton />*/}
|
||||
|
||||
<Button>
|
||||
<IconContainer>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { useWorkSpaceStore } from "../../../../store"
|
|||
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||
import { IconComponent } from "../../../../const/buttons/icons"
|
||||
import { buttonNames } from "../../../../const/buttons/names"
|
||||
import {useTranslation} from "../../../../utils/i18n/hooks";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -102,11 +103,12 @@ interface CurrentDevice {
|
|||
}
|
||||
|
||||
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 {openModal} = useModalContext();
|
||||
const {setIpAddress, ipAddress} = useIpStore();
|
||||
const {devices, addDevice, deleteDevice} = useDevicesStore()
|
||||
const {isOpen, collapseMenu} = useMenu(menusNames[1]);
|
||||
const {setWorkModule} = useWorkSpaceStore()
|
||||
const {t} = useTranslation('common');
|
||||
|
||||
const [currentDevice, setCurrentDevice] = useState<CurrentDevice>({
|
||||
address: 'localhost',
|
||||
|
|
@ -115,7 +117,7 @@ export const DevicesBar = () => {
|
|||
|
||||
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
|
||||
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
|
||||
const timeout = setTimeout(() => {
|
||||
testSocket.close();
|
||||
resolve(false);
|
||||
|
|
@ -163,8 +165,8 @@ export const DevicesBar = () => {
|
|||
const handleDelete = (number: number, adress: string) => {
|
||||
deleteDevice(number)
|
||||
|
||||
|
||||
if(adress == ipAddress){
|
||||
|
||||
if (adress == ipAddress) {
|
||||
setIpAddress('localhost')
|
||||
}
|
||||
};
|
||||
|
|
@ -173,7 +175,7 @@ export const DevicesBar = () => {
|
|||
const isAvailable = await checkWebSocketAvailability(ip);
|
||||
if (!isAvailable) {
|
||||
openModal(ModalMap['3'], {
|
||||
message: `Не удалось подключиться к серверу по адресу ${ip}`,
|
||||
message: t('nodes-connectionError') + ip,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -191,44 +193,46 @@ export const DevicesBar = () => {
|
|||
return (
|
||||
<Container>
|
||||
<Title>
|
||||
<p className="unselectable">Доступные узлы</p>
|
||||
<p className="unselectable">{t('nodes-availableNodes')}</p>
|
||||
</Title>
|
||||
|
||||
<DeviceBlock>
|
||||
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
|
||||
<p>Корневой адрес</p>
|
||||
<p>{t('nodes-rootAddress')}</p>
|
||||
</DeviceControl>
|
||||
{currentDevice.hostname == '' ? (
|
||||
<Text className="paddingP">Название: Загрузка<AnimDot /></Text>
|
||||
<Text className="paddingP">
|
||||
{t('nodes-name')}: {t('nodes-loading')}<AnimDot/>
|
||||
</Text>
|
||||
) : (
|
||||
<p className="paddingP">Название: {currentDevice.hostname}</p>
|
||||
<p className="paddingP">{t('nodes-name')}: {currentDevice.hostname}</p>
|
||||
)}
|
||||
</DeviceBlock>
|
||||
|
||||
<DevicesContainer>
|
||||
<DevicesHeader className="unselectable" onClick={collapseMenu}>
|
||||
<p>Дополнительные узлы</p>
|
||||
<IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]} />
|
||||
<p>{t('nodes-additionalNodes')}</p>
|
||||
<IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]}/>
|
||||
</DevicesHeader>
|
||||
|
||||
<DevicesContent $isactive={isOpen.toString()} >
|
||||
<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>
|
||||
<p>{t('nodes-node')} {device.number}</p>
|
||||
<IconComponent
|
||||
width="20"
|
||||
height="20"
|
||||
name={buttonNames[2]}
|
||||
setColor="white"
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation(); // Предотвращаем всплытие события
|
||||
e.stopPropagation();
|
||||
handleDelete(device.number, device.address);
|
||||
}}
|
||||
/>
|
||||
</DeviceControl>
|
||||
<p className="paddingP">Адрес: {device.address}</p>
|
||||
<p className="paddingP">{t('nodes-address')}: {device.address}</p>
|
||||
</DeviceBlock>
|
||||
))
|
||||
}
|
||||
|
|
@ -237,7 +241,6 @@ export const DevicesBar = () => {
|
|||
+
|
||||
</PlusBlock>
|
||||
</DevicesContent>
|
||||
|
||||
</DevicesContainer>
|
||||
</Container>
|
||||
)
|
||||
|
|
|
|||
4
FRONT/DEV/src/electron.d.ts
vendored
4
FRONT/DEV/src/electron.d.ts
vendored
|
|
@ -2,4 +2,8 @@ interface Window {
|
|||
electron: {
|
||||
quitApp: () => void;
|
||||
};
|
||||
electronAPI: {
|
||||
showSaveDialog: (options: Electron.SaveDialogOptions) => Promise<Electron.SaveDialogReturnValue>;
|
||||
writeFile: (path: string, data: string) => Promise<{ success: boolean }>;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import {useSettingsStore} from "../store/settings/settingsStore";
|
||||
import {defaultLanguage, Language} from "../utils/i18n/config";
|
||||
|
||||
export const useNameSetting = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('netnames-nodes')) as string;
|
||||
|
|
@ -8,6 +9,11 @@ export const usePaginateSetting = () => {
|
|||
return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20));
|
||||
};
|
||||
|
||||
export const useThemeSettings = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('theme'));
|
||||
};
|
||||
|
||||
//Интервальное сканирование под узлов
|
||||
export const useIntervalBoolean = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean;
|
||||
};
|
||||
|
|
@ -15,6 +21,17 @@ export const useIntervalTime = () => {
|
|||
return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60));
|
||||
};
|
||||
|
||||
export const useThemeSetting = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system';
|
||||
};
|
||||
// Кастомизация таблиц
|
||||
export const useOrdersColumns = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('table-columns-order')) as string[];
|
||||
};
|
||||
export const useDisabledColumns = () => {
|
||||
return useSettingsStore(state => state.getSettingValue('table-columns-disabled')) as number[];
|
||||
};
|
||||
|
||||
|
||||
export const useLanguage = () => {
|
||||
return useSettingsStore(
|
||||
state => state.getSettingValue<Language>('language')
|
||||
) ?? defaultLanguage;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface IpAddressState {
|
|||
export const useIpStore = create<IpAddressState>()(
|
||||
persist(
|
||||
(set)=>({
|
||||
ipAddress: '192:168.1.147',
|
||||
ipAddress: '192:168.1.0',
|
||||
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,25 +1,30 @@
|
|||
export const appearanceSettings = [
|
||||
{
|
||||
id: 'theme',
|
||||
sectionId: 'Внешний вид',
|
||||
label: 'Цветовая тема',
|
||||
type: 'select',
|
||||
defaultValue: 'light',
|
||||
options: [
|
||||
{ value: 'light', label: 'Светлая' },
|
||||
{ value: 'dark', label: 'Тёмная' },
|
||||
{ value: 'system', label: 'Как в системе' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
sectionId: 'Внешний вид',
|
||||
label: 'Язык интерфейса',
|
||||
type: 'select',
|
||||
defaultValue: 'ru',
|
||||
options: [
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'en', label: 'Английский' },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const createAppearanceSettings = () => {
|
||||
return [
|
||||
{
|
||||
id: 'theme',
|
||||
sectionId: 'settings-sections-appearance',
|
||||
label: 'settings-colorTheme',
|
||||
type: 'select',
|
||||
defaultValue: 'light',
|
||||
options: [
|
||||
{ value: 'light', label: 'settings-light' },
|
||||
{ value: 'dark', label: 'settings-dark' },
|
||||
{ value: 'system', label: 'settings-system' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
sectionId: 'settings-sections-appearance',
|
||||
label: 'settings-interfaceLanguage',
|
||||
type: 'select',
|
||||
defaultValue: 'ru',
|
||||
options: [
|
||||
{ value: 'ru', label: 'settings-russian' },
|
||||
{ value: 'en', label: 'settings-english' },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
|
||||
export { createAppearanceSettings };
|
||||
|
|
@ -1,16 +1,55 @@
|
|||
export const networkSettings = [
|
||||
import {useIpStore} from "../../ipStore";
|
||||
import {NetworkService} from "../../../api/request";
|
||||
|
||||
const createNetworkSettings = () => {
|
||||
const { ipAddress } = useIpStore.getState();
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'clean-network',
|
||||
type: 'button',
|
||||
label: 'Очистить данные сессии',
|
||||
sectionId: 'Точки доступа',
|
||||
defaultValue: 'Очистить',
|
||||
label: 'settings-clearSessionData',
|
||||
sectionId: 'settings-sections-accessPoints',
|
||||
defaultValue: 'settings-clear',
|
||||
action: async ()=>{
|
||||
try {
|
||||
const res = await NetworkService.clearNetwork(ipAddress);
|
||||
console.log(res);
|
||||
}catch (e){
|
||||
console.error(e);
|
||||
alert('settings-dataNotDeleted' + e)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'export-network',
|
||||
type: 'button',
|
||||
label: 'Экспорт данных точек доступа',
|
||||
sectionId: 'Точки доступа',
|
||||
defaultValue: 'Экспортировать',
|
||||
label: 'settings-exportNetworkData',
|
||||
sectionId: 'settings-sections-accessPoints',
|
||||
defaultValue: 'settings-export',
|
||||
action: async () => {
|
||||
try {
|
||||
const response = await NetworkService.exportNetwork(ipAddress);
|
||||
const { filePath } = await window.electronAPI.showSaveDialog({
|
||||
title: 'settings-saveNetworksTitle',
|
||||
defaultPath: "network_export.json",
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
await window.electronAPI.writeFile(
|
||||
filePath,
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
alert('settings-exportSuccess');
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert('settings-exportError' + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
] as const;
|
||||
] as const;
|
||||
};
|
||||
|
||||
export { createNetworkSettings };
|
||||
|
|
@ -1,59 +1,119 @@
|
|||
export const nodesSettings = [
|
||||
{
|
||||
id: 'rescan-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Интервальное сканирование',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'interval-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Интервал сканирования узлов (мин)',
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
min: 1,
|
||||
max: 2880
|
||||
},
|
||||
{
|
||||
id: 'netnames-nodes',
|
||||
sectionId: 'Узлы подсети',
|
||||
label: 'Отображаемые имена при отсутствии',
|
||||
type: 'select',
|
||||
defaultValue: 'ip',
|
||||
options: [
|
||||
{ value: 'ip', label: 'IP адрес' },
|
||||
{ value: 'unknown', label: 'Неизвестно' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'paginate-nodes',
|
||||
type: 'number',
|
||||
label: 'Количество строк в таблице',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 20,
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
id: 'customize-nodes',
|
||||
type: 'button',
|
||||
label: 'Отображение узлов',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Настроить',
|
||||
},
|
||||
{
|
||||
id: 'clean-nodes',
|
||||
type: 'button',
|
||||
label: 'Очистить данные сессии',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Очистить',
|
||||
},
|
||||
{
|
||||
id: 'export-nodes',
|
||||
type: 'button',
|
||||
label: 'Экспорт данных узлов',
|
||||
sectionId: 'Узлы подсети',
|
||||
defaultValue: 'Экспортировать',
|
||||
},
|
||||
] as const;
|
||||
import {useIpStore} from "../../ipStore";
|
||||
import {NetworkService} from "../../../api/request";
|
||||
|
||||
const createNodesSettings = () => {
|
||||
const { ipAddress } = useIpStore.getState();
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'rescan-nodes',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
label: 'settings-intervalScanning',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'interval-nodes',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
label: 'settings-scanInterval',
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
min: 1,
|
||||
max: 2880
|
||||
},
|
||||
{
|
||||
id: 'netnames-nodes',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
label: 'settings-displayNames',
|
||||
type: 'select',
|
||||
defaultValue: 'ip',
|
||||
options: [
|
||||
{ value: 'ip', label: 'settings-ipAddress' },
|
||||
{ value: 'unknown', label: 'settings-unknown' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'paginate-nodes',
|
||||
type: 'number',
|
||||
label: 'settings-rowsPerPage',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
defaultValue: 20,
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
id: 'customize-nodes',
|
||||
type: 'button',
|
||||
label: 'settings-displaySettings',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
defaultValue: 'settings-configure',
|
||||
},
|
||||
{
|
||||
id: 'clean-nodes',
|
||||
type: 'button',
|
||||
label: 'settings-clearSessionData',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
defaultValue: 'settings-clear',
|
||||
action: async ()=>{
|
||||
try {
|
||||
const res = await NetworkService.clearSubnet(ipAddress);
|
||||
console.log(res)
|
||||
}catch (e){
|
||||
console.error(e);
|
||||
alert('settings-dataNotDeleted' + e)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "export-nodes",
|
||||
type: "button",
|
||||
label: "settings-exportData",
|
||||
sectionId: "settings-sections-subnetNodes",
|
||||
defaultValue: "settings-export",
|
||||
action: async () => {
|
||||
try {
|
||||
const response = await NetworkService.exportDevices(ipAddress);
|
||||
const { filePath } = await window.electronAPI.showSaveDialog({
|
||||
title: "settings-saveNodesTitle",
|
||||
defaultPath: "nodes_export.json",
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
await window.electronAPI.writeFile(
|
||||
filePath,
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
alert("settings-exportSuccess");
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert("settings-exportError" + e.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'table-columns-order',
|
||||
type: 'hidden',
|
||||
label: '',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
defaultValue: [
|
||||
'settings-status',
|
||||
'settings-name',
|
||||
'settings-vendor',
|
||||
'settings-ipAddress',
|
||||
'settings-openPorts',
|
||||
'settings-lastActivity',
|
||||
'settings-note',
|
||||
'settings-action'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'table-columns-disabled',
|
||||
label: '',
|
||||
sectionId: 'settings-sections-subnetNodes',
|
||||
type: 'hidden',
|
||||
defaultValue: []
|
||||
}
|
||||
] as const;
|
||||
};
|
||||
|
||||
export { createNodesSettings };
|
||||
|
|
@ -1,20 +1,15 @@
|
|||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import {nodesSettings} from "./config/nodesSettings";
|
||||
import { networkSettings } from './config/networkSettings';
|
||||
import {appearanceSettings} from "./config/appearanceSettings";
|
||||
import {createNodesSettings} from "./config/nodesSettings";
|
||||
import { createNetworkSettings } from './config/networkSettings';
|
||||
import {createAppearanceSettings} from "./config/appearanceSettings";
|
||||
|
||||
type SettingValue = any;
|
||||
type SettingsDictionary = Record<string, SettingValue>;
|
||||
|
||||
interface SettingOption {
|
||||
readonly value: any;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export type Setting = {
|
||||
readonly id: string;
|
||||
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button';
|
||||
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button' | 'hidden';
|
||||
readonly label: string;
|
||||
readonly defaultValue: any;
|
||||
value?: string;
|
||||
|
|
@ -38,20 +33,16 @@ type SettingsState = {
|
|||
resetSettings: () => void;
|
||||
applyChanges: () => void;
|
||||
discardChanges: () => void;
|
||||
resetToDefaults: () => void;
|
||||
getSettingValue: <T = any>(id: string) => T;
|
||||
getNumberSetting: (id: string, fallback?: number) => number;
|
||||
updateSettingsConfig: () => void;
|
||||
};
|
||||
|
||||
const defaultSettings: Setting[] = [
|
||||
...nodesSettings,
|
||||
...networkSettings,
|
||||
...appearanceSettings,
|
||||
];
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
settingsConfig: defaultSettings,
|
||||
settingsConfig: [],
|
||||
settingsValues: {},
|
||||
activeSection: null,
|
||||
pendingChanges: {},
|
||||
|
|
@ -63,6 +54,16 @@ export const useSettingsStore = create<SettingsState>()(
|
|||
pendingChanges: { ...state.pendingChanges, [id]: value }
|
||||
})),
|
||||
|
||||
resetToDefaults: () => set({
|
||||
settingsValues: Object.fromEntries(
|
||||
get().settingsConfig.map(setting => [
|
||||
setting.id,
|
||||
setting.defaultValue
|
||||
])
|
||||
),
|
||||
pendingChanges: {}
|
||||
}),
|
||||
|
||||
applyChanges: () =>
|
||||
set((state) => ({
|
||||
settingsValues: { ...state.settingsValues, ...state.pendingChanges },
|
||||
|
|
@ -84,6 +85,15 @@ export const useSettingsStore = create<SettingsState>()(
|
|||
set({ settingsValues: newValues });
|
||||
},
|
||||
|
||||
updateSettingsConfig: () => {
|
||||
const defaultSettings: Setting[] = [
|
||||
...createNodesSettings(),
|
||||
...createNetworkSettings(),
|
||||
...createAppearanceSettings(),
|
||||
];
|
||||
set({ settingsConfig: defaultSettings });
|
||||
},
|
||||
|
||||
getSetting: (id) => {
|
||||
const state = get();
|
||||
const setting = state.settingsConfig.find(s => s.id === id);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { ThemeProvider as StyledTheme } from 'styled-components';
|
|||
import { light, dark} from '../const/theme/theme';
|
||||
import { GlobalStyle } from '../const/theme/globalStyles';
|
||||
import { themeNames, ThemName } from '../const/theme/names';
|
||||
import {useThemeSettings} from "../hooks/useSettings";
|
||||
|
||||
type ThemeContextType = {
|
||||
theme: ThemName;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
|
@ -23,32 +23,13 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
|||
const getSystemTheme = () =>
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches ? themeNames[2] : themeNames[1];
|
||||
|
||||
const getInitialTheme = () => {
|
||||
let savedTheme = localStorage.getItem('theme') as ThemName;
|
||||
if (!savedTheme) {
|
||||
savedTheme = themeNames[3]; // Устанавливаем значение по умолчанию
|
||||
localStorage.setItem('theme', savedTheme); // Сохраняем без JSON.stringify
|
||||
}
|
||||
return savedTheme;
|
||||
};
|
||||
const getInitialTheme = useThemeSettings();
|
||||
|
||||
const [theme, setTheme] = useState<ThemName>(getInitialTheme);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => {
|
||||
let newTheme: ThemName;
|
||||
if (prev === themeNames[3]) {
|
||||
newTheme = themeNames[1];
|
||||
} else if (prev === themeNames[1]) {
|
||||
newTheme = themeNames[2];
|
||||
} else {
|
||||
newTheme = themeNames[3];
|
||||
}
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
return newTheme;
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
setTheme(getInitialTheme)
|
||||
}, [getInitialTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
|
@ -68,8 +49,8 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
|||
const currentTheme = theme === themeNames[3] ? getSystemTheme() : theme;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
<StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark, toggleTheme }}>
|
||||
<ThemeContext.Provider value={{ theme }}>
|
||||
<StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark }}>
|
||||
<GlobalStyle />
|
||||
{children}
|
||||
</StyledTheme>
|
||||
|
|
|
|||
11
FRONT/DEV/src/utils/i18n/config.ts
Normal file
11
FRONT/DEV/src/utils/i18n/config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export const supportedLanguages = {
|
||||
en: 'English',
|
||||
ru: 'Русский',
|
||||
} as const;
|
||||
|
||||
export type Language = keyof typeof supportedLanguages;
|
||||
|
||||
export const defaultLanguage = 'ru';
|
||||
|
||||
export const namespaces = ['common', 'components'] as const;
|
||||
export type Namespace = (typeof namespaces)[number];
|
||||
11
FRONT/DEV/src/utils/i18n/hooks.ts
Normal file
11
FRONT/DEV/src/utils/i18n/hooks.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Namespace } from './config';
|
||||
import { useI18n } from './provider';
|
||||
|
||||
export const useTranslation = (ns?: Namespace) => {
|
||||
const { t, language } = useI18n();
|
||||
|
||||
return {
|
||||
t: (key: string) => t(key, ns),
|
||||
language,
|
||||
};
|
||||
};
|
||||
92
FRONT/DEV/src/utils/i18n/provider.tsx
Normal file
92
FRONT/DEV/src/utils/i18n/provider.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
'use client'
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { defaultLanguage, Language, Namespace, namespaces, supportedLanguages } from './config';
|
||||
|
||||
type Translation = Record<string, string>;
|
||||
type NamespaceTranslations = Record<Namespace, Translation>;
|
||||
type LanguageTranslations = Record<Language, NamespaceTranslations>;
|
||||
|
||||
const I18nContext = createContext<{
|
||||
language: Language;
|
||||
t: (key: string, namespace?: Namespace) => string;
|
||||
}>({
|
||||
language: defaultLanguage,
|
||||
t: (key: string) => key,
|
||||
});
|
||||
|
||||
interface I18nProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialLanguage: Language;
|
||||
}
|
||||
|
||||
export const I18nProvider = ({ children, initialLanguage }: I18nProviderProps) => {
|
||||
const [language, setLanguage] = useState<Language>(initialLanguage);
|
||||
const [translations, setTranslations] = useState<LanguageTranslations>({} as LanguageTranslations);
|
||||
|
||||
|
||||
// Загрузка переводов
|
||||
useEffect(() => {
|
||||
const loadTranslations = async () => {
|
||||
const newTranslations: LanguageTranslations = { ...translations };
|
||||
|
||||
await Promise.all(
|
||||
(Object.keys(supportedLanguages) as Language[]).map(async (lang) => {
|
||||
if (!newTranslations[lang]) {
|
||||
newTranslations[lang] = {} as NamespaceTranslations;
|
||||
|
||||
await Promise.all(
|
||||
namespaces.map(async (ns) => {
|
||||
try {
|
||||
const res = await fetch(`/locales/${lang}/${ns}.json`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
newTranslations[lang][ns] = data;
|
||||
} catch (e) {
|
||||
console.error(`Failed to load ${lang}/${ns} translations`, e);
|
||||
newTranslations[lang][ns] = {};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setTranslations(newTranslations);
|
||||
};
|
||||
|
||||
loadTranslations();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setLanguage(initialLanguage);
|
||||
}, [initialLanguage]);
|
||||
|
||||
const t = useMemo(() => {
|
||||
return (key: string, namespace: Namespace = 'common') => {
|
||||
return translations[language]?.[namespace]?.[key] || key;
|
||||
};
|
||||
}, [language, translations]);
|
||||
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
language,
|
||||
t,
|
||||
}), [language, t]);
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useI18n = () => {
|
||||
const context = useContext(I18nContext);
|
||||
if (!context) {
|
||||
throw new Error('useI18n must be used within I18nProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -628,12 +628,12 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^20.9.0":
|
||||
version "20.17.17"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz"
|
||||
integrity sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==
|
||||
"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.7.7":
|
||||
version "22.19.1"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz"
|
||||
integrity sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==
|
||||
dependencies:
|
||||
undici-types "~6.19.2"
|
||||
undici-types "~6.21.0"
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.13"
|
||||
|
|
@ -743,12 +743,12 @@ author-regex@^1.0.0:
|
|||
integrity sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==
|
||||
|
||||
axios@^1.7.9:
|
||||
version "1.8.4"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz"
|
||||
integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==
|
||||
version "1.13.2"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz"
|
||||
integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
form-data "^4.0.4"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
|
|
@ -772,9 +772,9 @@ boolean@^3.0.1:
|
|||
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
version "1.1.12"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
|
||||
integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
|
@ -817,6 +817,14 @@ cacheable-request@^7.0.2:
|
|||
normalize-url "^6.0.1"
|
||||
responselike "^2.0.0"
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
camelize@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz"
|
||||
|
|
@ -1032,6 +1040,15 @@ dir-compare@^3.0.0:
|
|||
buffer-equal "^1.0.0"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
electron-packager@^17.1.2:
|
||||
version "17.1.2"
|
||||
resolved "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz"
|
||||
|
|
@ -1062,13 +1079,13 @@ electron-to-chromium@^1.5.73:
|
|||
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.145.tgz"
|
||||
integrity sha512-pZ5EcTWRq/055MvSBgoFEyKf2i4apwfoqJbK/ak2jnFq8oHjZ+vzc3AhRcz37Xn+ZJfL58R666FLJx0YOK9yTw==
|
||||
|
||||
electron@^34.0.2:
|
||||
version "34.0.2"
|
||||
resolved "https://registry.npmjs.org/electron/-/electron-34.0.2.tgz"
|
||||
integrity sha512-u3F+DSUlg9NaGS+9qnYmSRN8VjAnc3LJDDk1ye1uISJnh4gjG76y3681qLowsPMx4obvCP2eBINnmbLo0yT5WA==
|
||||
electron@^39.2.3:
|
||||
version "39.2.3"
|
||||
resolved "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz"
|
||||
integrity sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
"@types/node" "^22.7.7"
|
||||
extract-zip "^2.0.1"
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
|
|
@ -1090,7 +1107,7 @@ error-ex@^1.2.0:
|
|||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-define-property@^1.0.0:
|
||||
es-define-property@^1.0.0, es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
|
@ -1100,6 +1117,23 @@ es-errors@^1.3.0:
|
|||
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
es6-error@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz"
|
||||
|
|
@ -1213,13 +1247,15 @@ follow-redirects@^1.15.6:
|
|||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz"
|
||||
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||
form-data@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz"
|
||||
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-extra@^10.0.0:
|
||||
|
|
@ -1292,6 +1328,22 @@ gensync@^1.0.0-beta.2:
|
|||
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
get-intrinsic@^1.2.6:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-package-info@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz"
|
||||
|
|
@ -1302,6 +1354,14 @@ get-package-info@^1.0.0:
|
|||
lodash.get "^4.0.0"
|
||||
read-pkg-up "^2.0.0"
|
||||
|
||||
get-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
get-stream@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
|
||||
|
|
@ -1346,7 +1406,7 @@ globalthis@^1.0.1:
|
|||
define-properties "^1.2.1"
|
||||
gopd "^1.0.1"
|
||||
|
||||
gopd@^1.0.1:
|
||||
gopd@^1.0.1, gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
|
@ -1380,6 +1440,18 @@ has-property-descriptors@^1.0.0:
|
|||
dependencies:
|
||||
es-define-property "^1.0.0"
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
|
||||
|
|
@ -1559,6 +1631,11 @@ matcher@^3.0.0:
|
|||
dependencies:
|
||||
escape-string-regexp "^4.0.0"
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
||||
|
|
@ -2120,10 +2197,10 @@ typescript@^4.9.5:
|
|||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
undici-types@~6.19.2:
|
||||
version "6.19.8"
|
||||
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz"
|
||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
|
|
@ -2162,9 +2239,9 @@ validate-npm-package-license@^3.0.1:
|
|||
spdx-expression-parse "^3.0.0"
|
||||
|
||||
"vite@^4.2.0 || ^5.0.0 || ^6.0.0", vite@^6.0.0, vite@^6.3.4:
|
||||
version "6.3.4"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz"
|
||||
integrity sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==
|
||||
version "6.4.1"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
|
||||
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.4.4"
|
||||
|
|
|
|||
3
SRV/.gitignore
vendored
3
SRV/.gitignore
vendored
|
|
@ -6,4 +6,5 @@ __pycache__
|
|||
.wheels
|
||||
.vendor
|
||||
.python3-deb
|
||||
storage/devices/
|
||||
storage/subnets/
|
||||
storage/wifi/
|
||||
|
|
@ -1,3 +1,27 @@
|
|||
"""
|
||||
Маршрутизация API-версии v1 для GONETS.
|
||||
|
||||
Модуль агрегирует и монтирует роутеры отдельных доменных областей под общим
|
||||
корневым роутером. Каждый подключенный модуль предоставляет набор эндпоинтов
|
||||
со своим префиксом и тегами для группировки в документации FastAPI.
|
||||
|
||||
Структура маршрутов:
|
||||
- `/ufw` (UFW): управление правилами и состоянием Uncomplicated Firewall.
|
||||
- `/ip` (IP): проверки и применение IP-конфигураций для сетевых профилей.
|
||||
- `/network` (Network): получение и визуализация состояния интерфейсов,
|
||||
управление позициями UI-контейнеров.
|
||||
- `/auth` (Auth): регистрация, аутентификация (JWT), сессии.
|
||||
- `/subnet` (Subnet): сканирование подсетей, заметки к устройствам,
|
||||
взаимодействие с Wi-Fi (скан/подключение/отключение/статус).
|
||||
- `WebSocket`: обработчики взаимодействия по WS (без префикса, путь внутри
|
||||
модуля), периодическая трансляция сетевых данных.
|
||||
- `/settings` (Settings): сервисные операции над локальными JSON-хранилищами
|
||||
(очистка данных подсетей/wi-fi и пр.).
|
||||
|
||||
Назначение:
|
||||
- Централизованная точка подключения доменных роутеров к приложению.
|
||||
- Единообразие тегов и префиксов для автогенерации схемы и документации.
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from .v1 import ufw, websocket, ip, network, auth, subnet, settings
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ from fastapi.security import OAuth2PasswordBearer
|
|||
from services.auth_service import AuthService
|
||||
from models.auth import UserCreate, UserLogin, UserResponse, Token
|
||||
from storage.json_storage import JsonStorage
|
||||
from utils.i18n import get_translations, _
|
||||
from babel.support import Translations
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
|
@ -13,9 +15,8 @@ router = APIRouter()
|
|||
auth_service = AuthService()
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(user: UserCreate):
|
||||
async def register(user: UserCreate, translations: Translations = Depends(get_translations)):
|
||||
"""Регистрирует нового пользователя.
|
||||
|
||||
Создаёт нового пользователя на основе предоставленных данных и возвращает
|
||||
|
|
@ -23,6 +24,7 @@ async def register(user: UserCreate):
|
|||
|
||||
Args:
|
||||
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin).
|
||||
|
|
@ -38,12 +40,11 @@ async def register(user: UserCreate):
|
|||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя")
|
||||
|
||||
logger.error(f"Error in register: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось зарегистрировать пользователя"))
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(user: UserLogin, request: Request):
|
||||
async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations)):
|
||||
"""Авторизует пользователя и создаёт сессию.
|
||||
|
||||
Проверяет учетные данные пользователя и возвращает токен и session_id для
|
||||
|
|
@ -52,6 +53,7 @@ async def login(user: UserLogin, request: Request):
|
|||
Args:
|
||||
user (UserLogin): Объект с данными для входа (username, password).
|
||||
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
Token: Объект с access_token и session_id для авторизации.
|
||||
|
|
@ -62,21 +64,18 @@ async def login(user: UserLogin, request: Request):
|
|||
"""
|
||||
try:
|
||||
client_id = request.client.host
|
||||
logger.debug(f"Попытка авторизации для client_id {client_id}")
|
||||
logger.debug(f"Попытка входа в систему клиента {client_id}")
|
||||
result = auth_service.login(user, client_id)
|
||||
logger.info(
|
||||
f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}"
|
||||
)
|
||||
logger.info(f"Успешная авторизация для клиента {client_id}, Идентификатор сессии: {result['session_id']}")
|
||||
return result
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
|
||||
|
||||
logger.error(f"Ошибка при авторизации: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось авторизовать пользователя"))
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user(request: Request):
|
||||
async def get_current_user(request: Request, translations: Translations = Depends(get_translations)):
|
||||
"""Возвращает данные текущего пользователя и токен.
|
||||
|
||||
Проверяет активную сессию по client_id и возвращает информацию о пользователе
|
||||
|
|
@ -84,6 +83,7 @@ async def get_current_user(request: Request):
|
|||
|
||||
Args:
|
||||
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и
|
||||
|
|
@ -95,29 +95,28 @@ async def get_current_user(request: Request):
|
|||
"""
|
||||
try:
|
||||
client_id = request.client.host
|
||||
logger.debug(f"Запрос /me для client_id {client_id}")
|
||||
logger.debug(f"Запрос /me для клиента {client_id}")
|
||||
result = auth_service.get_user_by_session(client_id)
|
||||
return {
|
||||
"user": result["user"],
|
||||
"access_token": result["access_token"],
|
||||
"token_type": "bearer",
|
||||
"token_type": translations.gettext("bearer"),
|
||||
}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
|
||||
|
||||
logger.error(f"Ошибка при get_current_user: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить данные пользователя"))
|
||||
|
||||
@router.get("/logout")
|
||||
async def logout(request: Request):
|
||||
async def logout(request: Request, translations: Translations = Depends(get_translations)):
|
||||
"""Выход из системы, удаление сессии.
|
||||
|
||||
Удаляет активную сессию пользователя по client_id.
|
||||
|
||||
Args:
|
||||
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Пустой словарь или сообщение об успешном выходе.
|
||||
|
|
@ -128,17 +127,16 @@ async def logout(request: Request):
|
|||
"""
|
||||
try:
|
||||
client_id = request.client.host
|
||||
logger.debug(f"Запрос logout для client_id {client_id}")
|
||||
logger.debug(f"Запрос на выход из системы для клиента {client_id}")
|
||||
return auth_service.logout(client_id)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Не удалось выйти из системы")
|
||||
|
||||
logger.error(f"Не удалось выйти из системы: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось выйти из системы"))
|
||||
|
||||
@router.get("/check_json")
|
||||
async def check_users_file():
|
||||
async def check_users_file(translations: Translations = Depends(get_translations)):
|
||||
"""Проверяет, существует ли файл users.json и содержит ли он пользователей.
|
||||
|
||||
Возвращает информацию о состоянии файла и наличии пользователей.
|
||||
|
|
@ -161,31 +159,25 @@ async def check_users_file():
|
|||
return {
|
||||
"exists": False,
|
||||
"has_users": False,
|
||||
"message": "Файл users.json не существует",
|
||||
"message": translations.gettext("Файл users.json не существует"),
|
||||
}
|
||||
|
||||
data = storage._read_data()
|
||||
has_users = len(data.get("users", [])) > 0
|
||||
if has_users:
|
||||
logger.info(
|
||||
f"Файл {file_path} существует и содержит пользователей"
|
||||
)
|
||||
logger.info(f"Файл {file_path} существует и содержит пользователей")
|
||||
return {
|
||||
"exists": True,
|
||||
"has_users": True,
|
||||
"message": "Файл users.json существует и содержит пользователей",
|
||||
"message": translations.gettext("Файл users.json существует и содержит пользователей"),
|
||||
}
|
||||
else:
|
||||
logger.info(
|
||||
f"Файл {file_path} существует, но не содержит пользователей"
|
||||
)
|
||||
logger.info(f"Файл {file_path} существует, но не содержит пользователей")
|
||||
return {
|
||||
"exists": True,
|
||||
"has_users": False,
|
||||
"message": "Файл users.json существует, но не содержит пользователей",
|
||||
"message": translations.gettext("Файл users.json существует, но не содержит пользователей"),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Ошибка при проверке файла")
|
||||
logger.error(f"Ошибка при проверке файла: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при проверке файла"))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from fastapi import APIRouter, Body, HTTPException, Depends
|
||||
from services.ip_service import IPService
|
||||
from utils.i18n import get_translations
|
||||
from babel.support import Translations
|
||||
|
||||
router = APIRouter()
|
||||
ip_service = IPService()
|
||||
|
||||
|
||||
@router.post("/check_ip")
|
||||
async def check_ip_config(data: Dict[str, Any] = Body(...)):
|
||||
async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
|
||||
"""Проверяет доступность IP-адреса перед его применением.
|
||||
|
||||
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
|
||||
|
|
@ -16,6 +17,7 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
|
|||
Args:
|
||||
data (Dict[str, Any]): Словарь с данными конфигурации, включая
|
||||
"ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Словарь с ключами:
|
||||
|
|
@ -26,16 +28,16 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
|
|||
HTTPException: Если произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
result = await ip_service.check_ip_config(data)
|
||||
result = await ip_service.check_ip_config(data, translations)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
||||
|
||||
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
|
||||
)
|
||||
|
||||
@router.post("/apply_ip")
|
||||
async def apply_ip_config(data: Dict[str, Any] = Body(...)):
|
||||
async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
|
||||
"""Применяет проверенные IP-настройки к указанному сетевому профилю.
|
||||
|
||||
Обновляет настройки соединения через NetworkManager, отключает и
|
||||
|
|
@ -44,6 +46,7 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...)):
|
|||
Args:
|
||||
data (Dict[str, Any]): Словарь с данными конфигурации, включая
|
||||
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
|
|
@ -55,19 +58,23 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...)):
|
|||
произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
await ip_service.apply_ip_config(data)
|
||||
await ip_service.apply_ip_config(data, translations)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Настройки сетевого профиля для {data['connection_id']} применены",
|
||||
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
|
||||
connection_id=data["connection_id"]
|
||||
),
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=404, detail=translations.gettext(str(e)))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
|
||||
)
|
||||
|
||||
@router.post("/save_ip")
|
||||
async def save_ip_config(data: Dict[str, Any] = Body(...)):
|
||||
async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
|
||||
"""Применяет проверенные IP-настройки для отката сетевого профиля.
|
||||
|
||||
Используется для сохранения и отката настроек сетевого профиля к предыдущему
|
||||
|
|
@ -76,6 +83,7 @@ async def save_ip_config(data: Dict[str, Any] = Body(...)):
|
|||
Args:
|
||||
data (Dict[str, Any]): Словарь с данными конфигурации, включая
|
||||
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
|
|
@ -87,12 +95,17 @@ async def save_ip_config(data: Dict[str, Any] = Body(...)):
|
|||
произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
await ip_service.apply_ip_config(data)
|
||||
await ip_service.apply_ip_config(data, translations)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Настройки сетевого профиля для {data['connection_id']} применены",
|
||||
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
|
||||
connection_id=data["connection_id"]
|
||||
),
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=404, detail=translations.gettext(str(e)))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from fastapi import APIRouter, Body, HTTPException
|
||||
from fastapi import APIRouter, Body, HTTPException, Depends
|
||||
import logging
|
||||
from typing import Dict, Tuple, List, Union
|
||||
|
||||
from services.network_service import NetworkService
|
||||
from models.network import (
|
||||
NetworkInterfacesContainer,
|
||||
|
|
@ -10,6 +9,8 @@ from models.network import (
|
|||
NetworkInterface,
|
||||
WiFiDevice,
|
||||
)
|
||||
from utils.i18n import get_translations, _, _load_translations
|
||||
from babel.support import Translations
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -17,7 +18,6 @@ logger = logging.getLogger(__name__)
|
|||
router = APIRouter()
|
||||
network_service = NetworkService()
|
||||
|
||||
|
||||
class ContainerPositionManager:
|
||||
"""Класс для управления позициями и состоянием контейнеров.
|
||||
|
||||
|
|
@ -44,11 +44,12 @@ class ContainerPositionManager:
|
|||
},
|
||||
}
|
||||
|
||||
def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
|
||||
def get_position_and_state(self, container_name: str, translations: Translations) -> Tuple[int, int, bool]:
|
||||
"""Получает текущие координаты и состояние указанного контейнера.
|
||||
|
||||
Args:
|
||||
container_name (str): Имя контейнера ("activation" или "deactivation").
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen).
|
||||
|
|
@ -58,7 +59,7 @@ class ContainerPositionManager:
|
|||
"""
|
||||
container = self.positions.get(container_name)
|
||||
if not container:
|
||||
raise ValueError(f"Неверное имя контейнера: {container_name}")
|
||||
raise ValueError(translations.gettext("Неверное имя контейнера: {container_name}").format(container_name=container_name))
|
||||
return container["x"], container["y"], container["isOpen"]
|
||||
|
||||
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
|
||||
|
|
@ -76,17 +77,18 @@ class ContainerPositionManager:
|
|||
container_name = "activation" if container_id == 1 else "deactivation"
|
||||
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
|
||||
|
||||
|
||||
container_manager = ContainerPositionManager()
|
||||
|
||||
|
||||
async def get_network_data() -> Dict[str, Dict]:
|
||||
async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]:
|
||||
"""Возвращает текущие сетевые данные в виде словаря.
|
||||
|
||||
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные,
|
||||
и возвращает их в формате Activation и Deactivation с учётом позиций и состояний
|
||||
контейнеров.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими
|
||||
данные о соответствующих контейнерах.
|
||||
|
|
@ -94,14 +96,14 @@ async def get_network_data() -> Dict[str, Dict]:
|
|||
Raises:
|
||||
HTTPException: Если произошла ошибка при получении данных (status_code=500).
|
||||
"""
|
||||
logger.debug("Вызов get_network_data()")
|
||||
logger.debug("Calling get_network_data()")
|
||||
try:
|
||||
network_data = await network_service.get_network_interfaces_container()
|
||||
|
||||
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
|
||||
|
||||
activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation"))
|
||||
deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation"))
|
||||
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state("activation", translations)
|
||||
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state("deactivation", translations)
|
||||
|
||||
activation = Activation(
|
||||
id=1,
|
||||
|
|
@ -123,9 +125,8 @@ async def get_network_data() -> Dict[str, Dict]:
|
|||
"deactivation": deactivation.model_dump(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в get_network_data: {e}")
|
||||
raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
|
||||
|
||||
logger.error(f"Error in get_network_data: {e}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить сетевые данные"))
|
||||
|
||||
def _split_interfaces(
|
||||
interfaces: List[Union[NetworkInterface, WiFiDevice]],
|
||||
|
|
@ -148,9 +149,8 @@ def _split_interfaces(
|
|||
deactivation_interfaces.append(interface)
|
||||
return activation_interfaces, deactivation_interfaces
|
||||
|
||||
|
||||
@router.post("/update_container_position")
|
||||
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)):
|
||||
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = Depends(get_translations)):
|
||||
"""Обновляет позицию и состояние контейнера.
|
||||
|
||||
Принимает данные о новом положении и состоянии контейнера и обновляет их
|
||||
|
|
@ -162,6 +162,7 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
|
|||
- "container_x" (int): Новая координата X.
|
||||
- "container_y" (int): Новая координата Y.
|
||||
- "isOpen" (bool): Новое состояние контейнера (открыт/закрыт).
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
|
|
@ -178,7 +179,7 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
|
|||
]
|
||||
if not all(key in data for key in required_keys):
|
||||
raise ValueError(
|
||||
"Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen"
|
||||
translations.gettext("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
|
||||
)
|
||||
|
||||
container_id = data["container_id"]
|
||||
|
|
@ -190,24 +191,26 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
|
|||
or not isinstance(container_x, int)
|
||||
or not isinstance(container_y, int)
|
||||
or not isinstance(is_open, bool)):
|
||||
raise ValueError("Неверный тип данных в запросе")
|
||||
raise ValueError(translations.gettext("Неверный тип данных в запросе"))
|
||||
|
||||
container_manager.update_position(container_id, container_x, container_y, is_open)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Позиция и состояние контейнера обновлены"
|
||||
"message": translations.gettext("Позиция и состояние контейнера обновлены")
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления позиции контейнера: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
|
||||
|
||||
logger.error(f"Error updating container position: {e}")
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e)))
|
||||
|
||||
@router.get("/get_hostname")
|
||||
async def get_hostname():
|
||||
async def get_hostname(translations: Translations = Depends(get_translations)):
|
||||
"""Возвращает имя хоста системы.
|
||||
|
||||
Выполняет запрос к сервису для получения имени хоста.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключом "hostname" и значением имени хоста.
|
||||
|
||||
|
|
@ -218,5 +221,5 @@ async def get_hostname():
|
|||
hostname_info = await network_service.get_hostname()
|
||||
return hostname_info
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в get_hostname: {e}")
|
||||
raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")
|
||||
logger.error(f"Error in get_hostname: {e}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить имя хоста"))
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, Depends, Response, Request, Body
|
||||
from starlette.responses import FileResponse
|
||||
from services.settings_service import SettingsService
|
||||
from utils.i18n import get_translations
|
||||
from babel.support import Translations
|
||||
from models.subnet import SubnetScanRequest # Предполагается, что модель определена
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
settings_service = SettingsService()
|
||||
|
||||
@router.post("/clear_subnet")
|
||||
def clear_subnet():
|
||||
def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)):
|
||||
"""Очищает данные узлов подсети, сохранённые в devices.json.
|
||||
|
||||
Удаляет все записи об узлах подсети, оставляя файл пустым.
|
||||
|
||||
Args:
|
||||
subnet (Optional[str]): Подсеть в формате CIDR (например, "10.1.1.0/24").
|
||||
Если None, очищает все подсети.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешной очистке.
|
||||
|
||||
|
|
@ -17,18 +33,27 @@ def clear_subnet():
|
|||
HTTPException: Если произошла ошибка очистки (status_code=500).
|
||||
"""
|
||||
try:
|
||||
success = settings_service.clear_subnet()
|
||||
success = settings_service.clear_subnet(subnet)
|
||||
if success:
|
||||
return {"message": "Данные узлов подсети успешно очищены"}
|
||||
if subnet:
|
||||
return {"message": translations.gettext("Данные узлов подсети {subnet} успешно очищены").format(subnet=subnet)}
|
||||
else:
|
||||
return {"message": translations.gettext("Данные всех подсетей успешно очищены")}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные узлов подсети"))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}")
|
||||
logger.error(f"Error clearing subnet data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
|
||||
|
||||
@router.post("/clear_wifi")
|
||||
def clear_wifi():
|
||||
def clear_wifi(translations: Translations = Depends(get_translations)):
|
||||
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
|
||||
|
||||
Удаляет все записи о точках доступа, оставляя файл пустым.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешной очистке.
|
||||
|
||||
|
|
@ -38,16 +63,22 @@ def clear_wifi():
|
|||
try:
|
||||
success = settings_service.clear_wifi()
|
||||
if success:
|
||||
return {"message": "Данные точек доступа Wi-Fi успешно очищены"}
|
||||
return {"message": translations.gettext("Данные точек доступа Wi-Fi успешно очищены")}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные точек доступа Wi-Fi"))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}")
|
||||
logger.error(f"Error clearing wifi data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
|
||||
|
||||
@router.post("/clear_all_data")
|
||||
def clear_all_data():
|
||||
def clear_all_data(translations: Translations = Depends(get_translations)):
|
||||
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
|
||||
|
||||
Удаляет все записи из devices.json и wifi.json.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешной очистке.
|
||||
|
||||
|
|
@ -57,6 +88,122 @@ def clear_all_data():
|
|||
try:
|
||||
success = settings_service.clear_all_data()
|
||||
if success:
|
||||
return {"message": "Все сетевые данные успешно очищены"}
|
||||
return {"message": translations.gettext("Все сетевые данные успешно очищены")}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить все сетевые данные"))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}")
|
||||
logger.error(f"Error clearing all data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
|
||||
|
||||
WIFI_PATH = "storage/wifi/wifi.json"
|
||||
|
||||
@router.post("/export_devices", response_class=FileResponse)
|
||||
async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations)):
|
||||
"""
|
||||
Экспортирует файл с данными устройств в формате JSON для указанной подсети.
|
||||
|
||||
Args:
|
||||
request (SubnetScanRequest): Объект с полем subnet в формате CIDR.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
FileResponse: Файл <subnet>.json для скачивания.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если файл не найден (status_code=404).
|
||||
"""
|
||||
subnet = request.subnet
|
||||
file_path = f"storage/subnets/{subnet.replace('/', '_')}.json"
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Export file not found: {file_path}")
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Файл для подсети {subnet} не найден").format(subnet=subnet))
|
||||
logger.info(f"Exporting file: {file_path}")
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
filename=f"{subnet.replace('/', '_')}.json",
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
@router.get("/export_wifi", response_class=FileResponse)
|
||||
async def export_wifi(translations: Translations = Depends(get_translations)):
|
||||
"""
|
||||
Экспортирует файл с данными устройств в формате JSON.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
FileResponse: Файл wifi.json для скачивания.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если файл не найден (status_code=404).
|
||||
"""
|
||||
if not os.path.exists(WIFI_PATH):
|
||||
logger.warning(f"Export file not found: {WIFI_PATH}")
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Файл не найден"))
|
||||
logger.info(f"Exporting file: {WIFI_PATH}")
|
||||
return FileResponse(
|
||||
path=WIFI_PATH,
|
||||
filename="wifi.json",
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
@router.post("/set_language")
|
||||
def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations)):
|
||||
"""
|
||||
Устанавливает куку locale для смены языка пользователя.
|
||||
|
||||
Args:
|
||||
locale (str): Используемый язык.
|
||||
response (Response): Запрос.
|
||||
translations: Объект переводов текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Сообщение об успешной установке языка.
|
||||
"""
|
||||
lc_messages_path = os.path.join(os.path.dirname(__file__), '..', '..', 'locales', locale, 'LC_MESSAGES')
|
||||
if not os.path.isdir(lc_messages_path):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=translations.gettext("Язык '{locale}' не поддерживается.").format(locale=locale)
|
||||
)
|
||||
has_mo_or_po = any(f.endswith('.mo') or f.endswith('.po') for f in os.listdir(lc_messages_path))
|
||||
if not has_mo_or_po:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=translations.gettext("Язык '{locale}' не поддерживается (нет файлов переводов).").format(locale=locale)
|
||||
)
|
||||
response.set_cookie(key="locale", value=locale, path="/", httponly=True)
|
||||
return {"message": translations.gettext("Язык успешно установлен: {locale}").format(locale=locale)}
|
||||
|
||||
@router.get("/get_system_language")
|
||||
async def get_system_language(request: Request, translations: Translations = Depends(get_translations)):
|
||||
"""Выводит текущий язык, установленный в системе на основе locale.
|
||||
|
||||
Выполняет команду 'locale' в системе и извлекает параметры LANG
|
||||
|
||||
Args:
|
||||
request (Request): Объект запроса для доступа к параметрам.
|
||||
translations: Объект переводов текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с текущим языком системы.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если не удалось извлечь системный язык (status_code=500).
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(['locale'], capture_output=True, text=True, check=True)
|
||||
output = result.stdout
|
||||
lang = next((line.split('=')[1].strip() for line in output.splitlines() if line.startswith('LANG=')), None)
|
||||
if lang:
|
||||
return {"language": lang}
|
||||
else:
|
||||
logger.warning(f"No locale found: {output}")
|
||||
return {"language": 'unknown'}
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Error executing locale command: {e}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось определить язык в системе"))
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error getting system language: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при определении языка в системе {error}").format(error=str(e)))
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from typing import List, Dict
|
||||
from models.subnet import SubnetScanResult
|
||||
from fastapi import APIRouter, HTTPException, Body, Depends
|
||||
from models.subnet import SubnetScanResult, SubnetScanRequest
|
||||
from models.wifi import WifiScanResult
|
||||
from models.request import NoteRequest, WifiConnectRequest, WifiDisconnectRequest
|
||||
from services.subnet_service import SubnetService
|
||||
from services.subnet_service import SubnetService, get_subnet_service
|
||||
from services.wifi_service import WifiService
|
||||
from services.connect_service import WifiConnectionService
|
||||
from utils.i18n import get_translations, _
|
||||
from babel.support import Translations
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/devices", response_model=SubnetScanResult)
|
||||
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@router.post("/devices", response_model=SubnetScanResult)
|
||||
def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
|
||||
"""Сканирует локальную подсеть и возвращает данные об устройствах.
|
||||
|
||||
Выполняет сканирование указанной подсети для обнаружения активных устройств
|
||||
с возможностью проверки указанных портов.
|
||||
Выполняет сканирование указанной подсети для обнаружения активных устройств.
|
||||
|
||||
Args:
|
||||
subnet (str, optional): Адрес подсети в формате CIDR (например, "192.168.1.0/24").
|
||||
По умолчанию "192.168.1.0/24".
|
||||
ports (List[int], optional): Список портов для сканирования. По умолчанию None.
|
||||
request (SubnetScanRequest): Объект с полем subnet.
|
||||
subnet_service: Сервис сканирования подсети.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
SubnetScanResult: Объект с результатами сканирования (устройства и их данные).
|
||||
|
|
@ -28,20 +32,26 @@ def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
|
|||
HTTPException: Если произошла ошибка сканирования (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
result = subnet_service.scan_subnet(subnet, ports)
|
||||
# Если subnet пустая строка, определяем автоматически
|
||||
subnet = request.subnet.strip() if request.subnet else None
|
||||
logger.info(f"Requested subnet scan for: {subnet or 'auto-detected'}")
|
||||
result = subnet_service.scan_subnet(subnet)
|
||||
if not result.devices and result.error:
|
||||
logger.warning(f"No devices found in subnet {subnet or 'auto-detected'}")
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка сканирования: {str(e)}")
|
||||
logger.error(f"Error in scan_subnet: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования подсети: {error}").format(error=str(e)))
|
||||
|
||||
@router.get("/wifi", response_model=WifiScanResult)
|
||||
def scan_wifi(timeout: int = 30):
|
||||
def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)):
|
||||
"""Сканирует Wi-Fi точки доступа и возвращает данные о них.
|
||||
|
||||
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
|
||||
|
||||
Args:
|
||||
timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей).
|
||||
|
|
@ -54,10 +64,10 @@ def scan_wifi(timeout: int = 30):
|
|||
result = wifi_service.scan_wifi(timeout=timeout)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка сканирования Wi-Fi: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования Wi-Fi: {error}").format(error=str(e)))
|
||||
|
||||
@router.post("/devices/{mac}/note")
|
||||
def add_note(mac: str, request: NoteRequest):
|
||||
def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
|
||||
"""Добавляет заметку для устройства по MAC-адресу.
|
||||
|
||||
Привязывает заметку к устройству, идентифицируемому по MAC-адресу.
|
||||
|
|
@ -65,6 +75,8 @@ def add_note(mac: str, request: NoteRequest):
|
|||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
request (NoteRequest): Объект с полем "note" для текста заметки.
|
||||
subnet_service: Сервис сканирования подсети.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном добавлении заметки.
|
||||
|
|
@ -74,21 +86,22 @@ def add_note(mac: str, request: NoteRequest):
|
|||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
success, error_message = subnet_service.add_note(mac, request.note)
|
||||
success, result = subnet_service.add_note(mac, request.note, translations)
|
||||
if not success:
|
||||
if "не найдено" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "уже существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' добавлена для устройства {mac}"}
|
||||
if result[0] == "not_found":
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
|
||||
elif result[0] == "note_exists":
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} уже существует").format(mac=mac))
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
|
||||
return {"message": translations.gettext("Заметка '{note}' добавлена для устройства {mac}").format(note=request.note, mac=mac)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
|
||||
|
||||
@router.put("/devices/{mac}/note")
|
||||
def update_note(mac: str, request: NoteRequest):
|
||||
def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
|
||||
"""Обновляет заметку для устройства по MAC-адресу.
|
||||
|
||||
Изменяет существующую заметку, привязанную к устройству.
|
||||
|
|
@ -96,6 +109,8 @@ def update_note(mac: str, request: NoteRequest):
|
|||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
request (NoteRequest): Объект с полем "note" для нового текста заметки.
|
||||
subnet_service: Сервис сканирования подсети.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном обновлении заметки.
|
||||
|
|
@ -105,27 +120,30 @@ def update_note(mac: str, request: NoteRequest):
|
|||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
success, error_message = subnet_service.update_note(mac, request.note)
|
||||
success, result = subnet_service.update_note(mac, request.note, translations)
|
||||
if not success:
|
||||
if "не найдено" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' обновлена для устройства {mac}"}
|
||||
if result[0] == "not_found":
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
|
||||
elif result[0] == "note_not_exists":
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
|
||||
return {"message": translations.gettext("Заметка '{note}' обновлена для устройства {mac}").format(note=request.note, mac=mac)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
|
||||
|
||||
@router.delete("/devices/{mac}/note")
|
||||
def delete_note(mac: str):
|
||||
def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
|
||||
"""Удаляет заметку для устройства по MAC-адресу.
|
||||
|
||||
Удаляет привязанную заметку для указанного устройства.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
subnet_service: Сервис сканирования подсети.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном удалении заметки.
|
||||
|
|
@ -135,21 +153,22 @@ def delete_note(mac: str):
|
|||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
success, error_message = subnet_service.delete_note(mac)
|
||||
success, result = subnet_service.delete_note(mac, translations)
|
||||
if not success:
|
||||
if "не найдено" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка удалена для устройства {mac}"}
|
||||
if result[0] == "not_found":
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
|
||||
elif result[0] == "note_not_exists":
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
|
||||
return {"message": translations.gettext("Заметка удалена для устройства {mac}").format(mac=mac)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
|
||||
|
||||
@router.post("/wifi/{ssid}/note")
|
||||
def add_wifi_note(ssid: str, request: NoteRequest):
|
||||
def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
|
||||
"""Добавляет заметку для сети Wi-Fi по SSID.
|
||||
|
||||
Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID.
|
||||
|
|
@ -157,6 +176,7 @@ def add_wifi_note(ssid: str, request: NoteRequest):
|
|||
Args:
|
||||
ssid (str): Идентификатор Wi-Fi сети.
|
||||
request (NoteRequest): Объект с полем "note" для текста заметки.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном добавлении заметки.
|
||||
|
|
@ -167,20 +187,22 @@ def add_wifi_note(ssid: str, request: NoteRequest):
|
|||
"""
|
||||
try:
|
||||
wifi_service = WifiService()
|
||||
success, error_message = wifi_service.add_note(ssid, request.note)
|
||||
success, result = wifi_service.add_note(ssid, request.note)
|
||||
if not success:
|
||||
if "не найдена" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "уже существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' добавлена для сети с SSID {ssid}"}
|
||||
if result[0] == "not_found":
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
|
||||
elif result[0] == "note_exists":
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} уже существует").format(ssid=ssid))
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
|
||||
return {"message": translations.gettext("Заметка '{note}' добавлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
|
||||
|
||||
@router.put("/wifi/{ssid}/note")
|
||||
def update_wifi_note(ssid: str, request: NoteRequest):
|
||||
def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
|
||||
"""Обновляет заметку для сети Wi-Fi по SSID.
|
||||
|
||||
Изменяет существующую заметку, привязанную к Wi-Fi сети.
|
||||
|
|
@ -188,6 +210,7 @@ def update_wifi_note(ssid: str, request: NoteRequest):
|
|||
Args:
|
||||
ssid (str): Идентификатор Wi-Fi сети.
|
||||
request (NoteRequest): Объект с полем "note" для нового текста заметки.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном обновлении заметки.
|
||||
|
|
@ -198,26 +221,29 @@ def update_wifi_note(ssid: str, request: NoteRequest):
|
|||
"""
|
||||
try:
|
||||
wifi_service = WifiService()
|
||||
success, error_message = wifi_service.update_note(ssid, request.note)
|
||||
success, result = wifi_service.update_note(ssid, request.note)
|
||||
if not success:
|
||||
if "не найдена" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' обновлена для сети с SSID {ssid}"}
|
||||
if result[0] == "not_found":
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
|
||||
elif result[0] == "note_not_exists":
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
|
||||
return {"message": translations.gettext("Заметка '{note}' обновлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
|
||||
|
||||
@router.delete("/wifi/{ssid}/note")
|
||||
def delete_wifi_note(ssid: str):
|
||||
def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)):
|
||||
"""Удаляет заметку для сети Wi-Fi по SSID.
|
||||
|
||||
Удаляет привязанную заметку для указанной Wi-Fi сети.
|
||||
|
||||
Args:
|
||||
ssid (str): Идентификатор Wi-Fi сети.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном удалении заметки.
|
||||
|
|
@ -228,20 +254,22 @@ def delete_wifi_note(ssid: str):
|
|||
"""
|
||||
try:
|
||||
wifi_service = WifiService()
|
||||
success, error_message = wifi_service.delete_note(ssid)
|
||||
success, result = wifi_service.delete_note(ssid)
|
||||
if not success:
|
||||
if "не найдена" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка удалена для сети с SSID {ssid}"}
|
||||
if result[0] == "not_found":
|
||||
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
|
||||
elif result[0] == "note_not_exists":
|
||||
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
|
||||
return {"message": translations.gettext("Заметка удалена для сети с SSID {ssid}").format(ssid=ssid)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
|
||||
|
||||
@router.post("/wifi/connect")
|
||||
def connect_to_wifi(request: WifiConnectRequest):
|
||||
def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)):
|
||||
"""Подключается к Wi-Fi сети по SSID и паролю.
|
||||
|
||||
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
|
||||
|
|
@ -249,6 +277,7 @@ def connect_to_wifi(request: WifiConnectRequest):
|
|||
|
||||
Args:
|
||||
request (WifiConnectRequest): Объект с полями "ssid" и "password".
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном подключении.
|
||||
|
|
@ -256,20 +285,26 @@ def connect_to_wifi(request: WifiConnectRequest):
|
|||
Raises:
|
||||
HTTPException: Если подключение не удалось (status_code=400).
|
||||
"""
|
||||
wifi_service = WifiConnectionService()
|
||||
success, message = wifi_service.connect(request.ssid, request.password)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"message": f"Успешно подключено к {request.ssid}"}
|
||||
try:
|
||||
wifi_service = WifiConnectionService(translations)
|
||||
success, message = wifi_service.connect(request.ssid, request.password)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=translations.gettext(message))
|
||||
return {"message": translations.gettext("Успешно подключено к {ssid}").format(ssid=request.ssid)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка подключения к Wi-Fi: {error}").format(error=str(e)))
|
||||
|
||||
@router.post("/wifi/disconnect")
|
||||
def disconnect_from_wifi(request: WifiDisconnectRequest):
|
||||
def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)):
|
||||
"""Отключается от Wi-Fi сети по указанному SSID.
|
||||
|
||||
Выполняет отключение от указанной Wi-Fi сети.
|
||||
|
||||
Args:
|
||||
request (WifiDisconnectRequest): Объект с полем "ssid".
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном отключении.
|
||||
|
|
@ -277,18 +312,26 @@ def disconnect_from_wifi(request: WifiDisconnectRequest):
|
|||
Raises:
|
||||
HTTPException: Если отключение не удалось (status_code=400).
|
||||
"""
|
||||
wifi_service = WifiConnectionService()
|
||||
success, message = wifi_service.disconnect(request.ssid)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"message": message or f"Успешно отключено от {request.ssid}"}
|
||||
try:
|
||||
wifi_service = WifiConnectionService(translations)
|
||||
success, message = wifi_service.disconnect(request.ssid)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=translations.gettext(message))
|
||||
return {"message": translations.gettext(message or "Успешно отключено от {ssid}").format(ssid=request.ssid)}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка отключения от Wi-Fi: {error}").format(error=str(e)))
|
||||
|
||||
@router.get("/wifi/status")
|
||||
def get_wifi_status():
|
||||
def get_wifi_status(translations: Translations = Depends(get_translations)):
|
||||
"""Проверяет статус текущего подключения к Wi-Fi.
|
||||
|
||||
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
- "status" (str): Состояние ("connected" или другое).
|
||||
|
|
@ -297,8 +340,13 @@ def get_wifi_status():
|
|||
Raises:
|
||||
HTTPException: Если проверка статуса не удалась (status_code=400).
|
||||
"""
|
||||
wifi_service = WifiConnectionService()
|
||||
success, message = wifi_service.get_status()
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"status": "connected", "ssid": message}
|
||||
try:
|
||||
wifi_service = WifiConnectionService(translations)
|
||||
success, message = wifi_service.get_status()
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=translations.gettext(message))
|
||||
return {"status": translations.gettext("connected"), "ssid": message}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка проверки статуса Wi-Fi: {error}").format(error=str(e)))
|
||||
|
|
|
|||
|
|
@ -9,14 +9,29 @@ router = APIRouter()
|
|||
|
||||
|
||||
class RuleNumbers(BaseModel):
|
||||
"""Модель запроса на удаление UFW правил.
|
||||
|
||||
Атрибуты:
|
||||
|
||||
- ``numbers`` (List[int]): Список номеров правил для удаления.
|
||||
"""
|
||||
numbers: List[int] = Field(
|
||||
..., description="Список номеров правил для удаления")
|
||||
|
||||
|
||||
@router.get("/rules", response_model=UFWRulesResponse)
|
||||
async def get_ufw_rules():
|
||||
"""
|
||||
Получает все правила UFW с помощью сервиса UFWService и возвращает их в формате JSON.
|
||||
"""Возвращает все правила UFW.
|
||||
|
||||
Осуществляет обращение к сервису `UFWService` для чтения текущего списка
|
||||
правил брандмауэра и возвращает результат в сериализованном виде для клиента.
|
||||
|
||||
Returns:
|
||||
UFWRulesResponse: Структура с коллекцией правил и сопутствующей
|
||||
информацией о состоянии.
|
||||
|
||||
Raises:
|
||||
HTTPException: В случае ошибок взаимодействия с UFW (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
@ -27,8 +42,13 @@ async def get_ufw_rules():
|
|||
|
||||
@router.get("/status", response_model=UFWStatus)
|
||||
async def get_ufw_status():
|
||||
"""
|
||||
Получает текущий статус UFW.
|
||||
"""Возвращает текущий статус UFW.
|
||||
|
||||
Returns:
|
||||
UFWStatus: Объект со сведениями о включенности UFW и деталями статуса.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если не удалось получить статус (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
@ -39,8 +59,13 @@ async def get_ufw_status():
|
|||
|
||||
@router.post("/enable")
|
||||
async def enable_ufw():
|
||||
"""
|
||||
Включает UFW.
|
||||
"""Включает UFW.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами `status` и `message` при успешном выполнении.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если включение завершилось неуспешно (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
@ -56,8 +81,13 @@ async def enable_ufw():
|
|||
|
||||
@router.post("/disable")
|
||||
async def disable_ufw():
|
||||
"""
|
||||
Выключает UFW.
|
||||
"""Выключает UFW.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами `status` и `message` при успешном выполнении.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если выключение завершилось неуспешно (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
@ -73,8 +103,19 @@ async def disable_ufw():
|
|||
|
||||
@router.post("/rules/in")
|
||||
async def add_ufw_in_rules(rules: List[UFWRule]):
|
||||
"""
|
||||
Добавляет входящие (in) правила UFW.
|
||||
"""Добавляет входящие (in) правила UFW.
|
||||
|
||||
Args:
|
||||
rules (List[UFWRule]): Список правил, определяющих направления,
|
||||
порты/диапазоны/протоколы и действия (allow/deny/reject).
|
||||
|
||||
Returns:
|
||||
dict: Итог выполнения с количеством успешно добавленных правил и деталями
|
||||
по каждому правилу.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если UFW отключен (status_code=400) или при внутренних
|
||||
ошибках (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
@ -108,8 +149,17 @@ async def add_ufw_in_rules(rules: List[UFWRule]):
|
|||
|
||||
@router.post("/rules/out")
|
||||
async def add_ufw_out_rules(rules: List[UFWRule]):
|
||||
"""
|
||||
Добавляет исходящие (out) правила UFW.
|
||||
"""Добавляет исходящие (out) правила UFW.
|
||||
|
||||
Args:
|
||||
rules (List[UFWRule]): Список правил исходящего трафика для добавления.
|
||||
|
||||
Returns:
|
||||
dict: Объект статуса с сообщением об успешном добавлении.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если UFW отключен (status_code=400) или при внутренних
|
||||
ошибках (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
@ -137,8 +187,18 @@ async def add_ufw_out_rules(rules: List[UFWRule]):
|
|||
|
||||
@router.delete("/rules")
|
||||
async def delete_ufw_rules(rule_numbers: RuleNumbers):
|
||||
"""
|
||||
Удаляет правила UFW по их номерам.
|
||||
"""Удаляет правила UFW по их номерам.
|
||||
|
||||
Args:
|
||||
rule_numbers (RuleNumbers): Номера правил, подлежащих удалению (как в
|
||||
выводе `ufw status numbered`).
|
||||
|
||||
Returns:
|
||||
dict: Информация об общем количестве удаляемых правил, числе успешных и
|
||||
возможной ошибке.
|
||||
|
||||
Raises:
|
||||
HTTPException: При ошибках удаления (status_code=500).
|
||||
"""
|
||||
ufw_service = UFWService()
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import asyncio
|
|||
import logging
|
||||
from typing import Set
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
|
||||
from starlette.websockets import WebSocketState
|
||||
from utils.i18n import get_translations_ws, _, _load_translations
|
||||
from babel.support import Translations
|
||||
|
||||
from .network import update_container_position, get_network_data
|
||||
|
||||
|
|
@ -70,15 +72,13 @@ class WebSocketManager:
|
|||
except WebSocketDisconnect:
|
||||
self.disconnect(connection)
|
||||
except Exception as e:
|
||||
logger.error(f"Сообщение об ошибке трансляции: {e}")
|
||||
logger.error(f"Broadcast error: {e}")
|
||||
self.disconnect(connection)
|
||||
|
||||
|
||||
manager = WebSocketManager()
|
||||
|
||||
|
||||
@router.websocket("/api/v1")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
async def websocket_endpoint(websocket: WebSocket, translations: Translations = Depends(get_translations_ws)):
|
||||
"""Обработчик WebSocket-соединения.
|
||||
|
||||
Управляет соединением клиента, принимает данные для обновления позиций
|
||||
|
|
@ -86,6 +86,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||
|
||||
Args:
|
||||
websocket (WebSocket): Объект WebSocket для обработки соединения.
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Raises:
|
||||
WebSocketDisconnect: Если клиент отключился.
|
||||
|
|
@ -95,26 +96,26 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
logger.info(f"Полученные данные: {data}")
|
||||
logger.info(f"Received data: {data}")
|
||||
|
||||
if all(key in data for key in ["container_id", "container_x", "container_y"]):
|
||||
await update_container_position(data)
|
||||
await update_container_position(data, translations=translations)
|
||||
|
||||
network_data = await get_network_data()
|
||||
logger.info(f"Текущие сетевые данные: {network_data}")
|
||||
# Для внутреннего вызова используем переводы из WebSocket зависимостии
|
||||
network_data = await get_network_data(translations=translations)
|
||||
logger.info(f"Current network data: {network_data}")
|
||||
|
||||
await manager.broadcast(network_data)
|
||||
await asyncio.sleep(2)
|
||||
except WebSocketDisconnect:
|
||||
logger.info("Клиент отключен")
|
||||
logger.info("Client disconnected")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в websocket_endpoint:{e}")
|
||||
logger.error(f"Error in websocket_endpoint: {e}")
|
||||
finally:
|
||||
manager.disconnect(websocket)
|
||||
if websocket.client_state != WebSocketState.DISCONNECTED:
|
||||
await websocket.close()
|
||||
|
||||
|
||||
@router.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Запуск таска для трансляции сетевых данных при старте приложения.
|
||||
|
|
@ -126,27 +127,28 @@ async def startup_event():
|
|||
"""
|
||||
asyncio.create_task(broadcast_data())
|
||||
|
||||
|
||||
async def broadcast_data():
|
||||
async def broadcast_data(translations: Translations = None):
|
||||
"""Таск для периодической трансляции сетевых данных.
|
||||
|
||||
Выполняет сканирование сети и отправляет данные всем подключённым клиентам
|
||||
с интервалом в 2 секунды.
|
||||
|
||||
Args:
|
||||
translations: Объект переводов для текущего языка.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при получении данных или трансляции.
|
||||
"""
|
||||
translations = translations or _load_translations("ru")
|
||||
while True:
|
||||
try:
|
||||
network_data = await get_network_data()
|
||||
network_data = await get_network_data(translations=translations)
|
||||
if "activation" not in network_data:
|
||||
logger.error(
|
||||
"Ошибка: Ключ 'activation' отсутствует в полученных данных сети"
|
||||
)
|
||||
logger.error(translations.gettext("Ошибка: Ключ 'activation' отсутствует в полученных данных сети"))
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
|
||||
await manager.broadcast(network_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в broadcast_data:{e}")
|
||||
await asyncio.sleep(2)
|
||||
logger.error(f"Error in broadcast_data: {e}")
|
||||
await asyncio.sleep(2)
|
||||
|
|
|
|||
3
SRV/babel.cfg
Normal file
3
SRV/babel.cfg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[python: **.py]
|
||||
encoding = utf-8
|
||||
extract_messages = _
|
||||
0
SRV/config/__init__.py
Normal file
0
SRV/config/__init__.py
Normal file
|
|
@ -1,3 +1,20 @@
|
|||
"""
|
||||
Конфигурация CORS для FastAPI-приложения GONETS.
|
||||
|
||||
Модуль инкапсулирует добавление middleware `CORSMiddleware` со списком
|
||||
разрешённых источников (origins), характерных для локальной разработки и
|
||||
типовых портов фронтенда (например, Vite/React на 5173/5174, dev-серверы).
|
||||
|
||||
Использование:
|
||||
cors_config = CORSMiddlewareConfig(app)
|
||||
cors_config.add_cors_middleware()
|
||||
|
||||
Поведение:
|
||||
- Разрешены все методы (`allow_methods=["*"]`) и заголовки (`allow_headers=["*"]`).
|
||||
- Разрешены креденшлы (`allow_credentials=True`) для работы с куками (локаль и др.).
|
||||
- При необходимости список `origins` можно расширить или параметризовать
|
||||
на основе конфигурационного файла окружения.
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# init_config.py
|
||||
import configparser
|
||||
import os
|
||||
import secrets
|
||||
|
|
@ -9,7 +8,18 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def init_config():
|
||||
"""Создаёт конфигурационный файл с SECRET_KEY"""
|
||||
"""
|
||||
Инициализация конфигурации приложения (JWT SECRET_KEY).
|
||||
|
||||
Назначение:
|
||||
- Создаёт файл `/etc/gonets/config.ini` с секцией `[JWT]` и случайным
|
||||
`secret_key`, если он отсутствует. Устанавливает права `0o600`.
|
||||
|
||||
Замечания:
|
||||
- Функция предназначена для однократного запуска (в инсталляторе/скрипте
|
||||
подготовки окружения) и безопасно пропускает работу, если файл уже существует.
|
||||
"""
|
||||
"""Создаёт конфигурационный файл с SECRET_KEY."""
|
||||
config_path = "/etc/gonets/config.ini"
|
||||
config_dir = os.path.dirname(config_path)
|
||||
|
||||
|
|
@ -34,6 +44,6 @@ def init_config():
|
|||
)
|
||||
raise
|
||||
|
||||
|
||||
# Нативный запуск для тестов
|
||||
if __name__ == "__main__":
|
||||
init_config()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ from typing import Dict, List
|
|||
|
||||
|
||||
class PortConfig:
|
||||
"""Конфигурация портов для сетевого сканирования."""
|
||||
"""Конфигурация известных портов для сетевого анализа.
|
||||
|
||||
Назначение:
|
||||
Предоставляет сопоставление распространённых портов с человекочитаемыми
|
||||
названиями сервисов для отображения в UI и логах. Поддерживает расширение
|
||||
пользовательским словарём.
|
||||
"""
|
||||
|
||||
_DEFAULT_PORTS: Dict[int, str] = {
|
||||
20: "FTP (Data)",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ from typing import Tuple
|
|||
|
||||
|
||||
class CLIExecutor:
|
||||
"""
|
||||
Класс для выполнения CLI-команд
|
||||
"""Утилита для безопасного выполнения CLI-команд.
|
||||
|
||||
Назначение:
|
||||
Инкапсулирует вызов `subprocess.run` и приводит результат к удобному
|
||||
кортежу (код возврата, stdout) без генерации исключений при ошибках.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
2
SRV/docs/_build/.sphinx_tmp/config.ini
vendored
Normal file
2
SRV/docs/_build/.sphinx_tmp/config.ini
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[JWT]
|
||||
secret_key = sphinx-docs-secret
|
||||
1
SRV/docs/_build/.sphinx_tmp/sessions.json
vendored
Normal file
1
SRV/docs/_build/.sphinx_tmp/sessions.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"sessions": {}}
|
||||
1
SRV/docs/_build/.sphinx_tmp/users.json
vendored
Normal file
1
SRV/docs/_build/.sphinx_tmp/users.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"users": [], "next_id": 1}
|
||||
4
SRV/docs/_build/html/.buildinfo
vendored
Normal file
4
SRV/docs/_build/html/.buildinfo
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Sphinx build info version 1
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 0de2fdeef2a39cc42113efadfd5a855d
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
BIN
SRV/docs/_build/html/.doctrees/api/config.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/config.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/config/cors.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/config/cors.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/config/init_config.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/config/init_config.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/config/ports.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/config/ports.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/core.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/core.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/core/cli_executor.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/core/cli_executor.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/auth.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/auth.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/ip.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/ip.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/network.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/network.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/settings.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/settings.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/subnet.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/subnet.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/ufw.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/ufw.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/websocket.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/endpoints/websocket.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models/auth.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models/auth.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models/network.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models/network.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models/request.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models/request.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models/subnet.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models/subnet.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models/ufw.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models/ufw.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/models/wifi.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/models/wifi.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/arp_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/arp_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/auth_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/auth_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/connect_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/connect_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/firewalld_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/firewalld_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/ip_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/ip_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/network_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/network_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/settings_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/settings_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/subnet_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/subnet_service.doctree
vendored
Normal file
Binary file not shown.
BIN
SRV/docs/_build/html/.doctrees/api/services/ufw_service.doctree
vendored
Normal file
BIN
SRV/docs/_build/html/.doctrees/api/services/ufw_service.doctree
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue