feat(manager/devices): Add device management features including creation and editing

This commit is contained in:
2026-01-22 17:07:02 +07:00
parent 0bac8d0f25
commit 8b95a620c2
8 changed files with 840 additions and 3 deletions

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{
"name": "smatec-frontend",
"name": "SMATEC-FRONTEND",
"lockfileVersion": 3,
"requires": true,
"packages": {

123
pnpm-lock.yaml generated
View File

@@ -26,6 +26,15 @@ importers:
dayjs:
specifier: ^1.11.19
version: 1.11.19
moment:
specifier: ^2.30.1
version: 2.30.1
ol:
specifier: ^10.6.1
version: 10.7.0
reconnecting-websocket:
specifier: ^4.4.0
version: 4.4.0
devDependencies:
'@types/react':
specifier: ^18.0.33
@@ -1091,6 +1100,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, tarball: https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz}
engines: {node: '>= 8'}
'@petamoriken/float16@3.9.3':
resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==, tarball: https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, tarball: https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz}
engines: {node: '>=14'}
@@ -1356,6 +1368,9 @@ packages:
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==, tarball: https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz}
'@types/rbush@4.0.0':
resolution: {integrity: sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==, tarball: https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz}
'@types/react-dom@18.3.7':
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==, tarball: https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz}
peerDependencies:
@@ -2746,6 +2761,9 @@ packages:
react: 15.x || ^16.0.0-0
react-dom: 15.x || ^16.0.0-0
earcut@3.0.2:
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==, tarball: https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, tarball: https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz}
@@ -3179,6 +3197,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz}
engines: {node: '>=6.9.0'}
geotiff@2.1.3:
resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==, tarball: https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz}
engines: {node: '>=10.19'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, tarball: https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -3840,6 +3862,9 @@ packages:
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==, tarball: https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz}
lerc@3.0.0:
resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==, tarball: https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz}
less-loader@12.3.0:
resolution: {integrity: sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==, tarball: https://registry.npmjs.org/less-loader/-/less-loader-12.3.0.tgz}
engines: {node: '>= 18.12.0'}
@@ -4281,6 +4306,9 @@ packages:
obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==, tarball: https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz}
ol@10.7.0:
resolution: {integrity: sha512-122U5gamPqNgLpLOkogFJhgpywvd/5en2kETIDW+Ubfi9lPnZ0G9HWRdG+CX0oP8od2d6u6ky3eewIYYlrVczw==, tarball: https://registry.npmjs.org/ol/-/ol-10.7.0.tgz}
on-exit-leak-free@0.2.0:
resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==, tarball: https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz}
@@ -4352,6 +4380,9 @@ packages:
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, tarball: https://registry.npmjs.org/pako/-/pako-1.0.11.tgz}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==, tarball: https://registry.npmjs.org/pako/-/pako-2.1.0.tgz}
param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==, tarball: https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz}
@@ -4363,6 +4394,9 @@ packages:
resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==, tarball: https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz}
engines: {node: '>= 0.10'}
parse-headers@2.0.6:
resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==, tarball: https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, tarball: https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz}
engines: {node: '>=8'}
@@ -4421,6 +4455,10 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, tarball: https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz}
engines: {node: '>=8'}
pbf@4.0.1:
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==, tarball: https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz}
hasBin: true
pbkdf2@3.1.5:
resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==, tarball: https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz}
engines: {node: '>= 0.10'}
@@ -4835,6 +4873,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, tarball: https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz}
protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==, tarball: https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, tarball: https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz}
engines: {node: '>= 0.10'}
@@ -4883,6 +4924,13 @@ packages:
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==, tarball: https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz}
engines: {node: '>=8'}
quick-lru@6.1.2:
resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==, tarball: https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz}
engines: {node: '>=12'}
quickselect@3.0.0:
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==, tarball: https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==, tarball: https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz}
@@ -4897,6 +4945,9 @@ packages:
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==, tarball: https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz}
engines: {node: '>= 0.8'}
rbush@4.0.1:
resolution: {integrity: sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==, tarball: https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz}
rc-align@4.0.15:
resolution: {integrity: sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==, tarball: https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz}
peerDependencies:
@@ -5462,6 +5513,9 @@ packages:
resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==, tarball: https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz}
engines: {node: '>= 12.13.0'}
reconnecting-websocket@4.4.0:
resolution: {integrity: sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==, tarball: https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz}
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==, tarball: https://registry.npmjs.org/redent/-/redent-3.0.0.tgz}
engines: {node: '>=8'}
@@ -5538,6 +5592,9 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, tarball: https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz}
resolve-protobuf-schema@2.1.0:
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==, tarball: https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz}
resolve-url-loader@5.0.0:
resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==, tarball: https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz}
engines: {node: '>=12'}
@@ -6377,6 +6434,9 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==, tarball: https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz}
engines: {node: '>= 8'}
web-worker@1.5.0:
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==, tarball: https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz}
webpack-5-chain@8.0.1:
resolution: {integrity: sha512-Tu1w80WA2Z+X6e7KzGy+cc0A0z+npVJA/fh55q2azMJ030gqz343Kx+yNAstDCeugsepmtDWY2J2IBRW/O+DEA==, tarball: https://registry.npmjs.org/webpack-5-chain/-/webpack-5-chain-8.0.1.tgz}
engines: {node: '>=10'}
@@ -6454,6 +6514,9 @@ packages:
utf-8-validate:
optional: true
xml-utils@1.10.2:
resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==, tarball: https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, tarball: https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz}
engines: {node: '>=0.4'}
@@ -6501,6 +6564,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==, tarball: https://registry.npmjs.org/zod/-/zod-3.25.76.tgz}
zstddec@0.1.0:
resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==, tarball: https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz}
snapshots:
'@ahooksjs/use-request@2.8.15(react@18.3.1)':
@@ -7810,6 +7876,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@petamoriken/float16@3.9.3': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -8095,6 +8163,8 @@ snapshots:
'@types/prop-types@15.7.15': {}
'@types/rbush@4.0.0': {}
'@types/react-dom@18.3.7(@types/react@18.3.27)':
dependencies:
'@types/react': 18.3.27
@@ -10061,6 +10131,8 @@ snapshots:
react-router-redux: 5.0.0-alpha.9(react@18.3.1)
redux: 3.7.2
earcut@3.0.2: {}
eastasianwidth@0.2.0: {}
ee-first@1.1.1: {}
@@ -10704,6 +10776,17 @@ snapshots:
gensync@1.0.0-beta.2: {}
geotiff@2.1.3:
dependencies:
'@petamoriken/float16': 3.9.3
lerc: 3.0.0
pako: 2.1.0
parse-headers: 2.0.6
quick-lru: 6.1.2
web-worker: 1.5.0
xml-utils: 1.10.2
zstddec: 0.1.0
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0:
@@ -11398,6 +11481,8 @@ snapshots:
kolorist@1.8.0: {}
lerc@3.0.0: {}
less-loader@12.3.0(less@4.5.1)(webpack@5.104.1):
dependencies:
less: 4.5.1
@@ -11877,6 +11962,14 @@ snapshots:
obuf@1.1.2: {}
ol@10.7.0:
dependencies:
'@types/rbush': 4.0.0
earcut: 3.0.2
geotiff: 2.1.3
pbf: 4.0.1
rbush: 4.0.1
on-exit-leak-free@0.2.0: {}
on-finished@2.3.0:
@@ -11953,6 +12046,8 @@ snapshots:
pako@1.0.11: {}
pako@2.1.0: {}
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
@@ -11970,6 +12065,8 @@ snapshots:
pbkdf2: 3.1.5
safe-buffer: 5.2.1
parse-headers@2.0.6: {}
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.28.6
@@ -12017,6 +12114,10 @@ snapshots:
path-type@4.0.0: {}
pbf@4.0.1:
dependencies:
resolve-protobuf-schema: 2.1.0
pbkdf2@3.1.5:
dependencies:
create-hash: 1.2.0
@@ -12396,6 +12497,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
protocol-buffers-schema@3.6.0: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -12447,6 +12550,10 @@ snapshots:
quick-lru@4.0.1: {}
quick-lru@6.1.2: {}
quickselect@3.0.0: {}
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
@@ -12465,6 +12572,10 @@ snapshots:
iconv-lite: 0.4.24
unpipe: 1.0.0
rbush@4.0.1:
dependencies:
quickselect: 3.0.0
rc-align@4.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.28.6
@@ -13262,6 +13373,8 @@ snapshots:
real-require@0.1.0: {}
reconnecting-websocket@4.4.0: {}
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@@ -13340,6 +13453,10 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
resolve-protobuf-schema@2.1.0:
dependencies:
protocol-buffers-schema: 3.6.0
resolve-url-loader@5.0.0:
dependencies:
adjust-sourcemap-loader: 4.0.0
@@ -14334,6 +14451,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
web-worker@1.5.0: {}
webpack-5-chain@8.0.1:
dependencies:
deepmerge: 1.5.2
@@ -14447,6 +14566,8 @@ snapshots:
ws@8.19.0: {}
xml-utils@1.10.2: {}
xtend@4.0.2: {}
y18n@5.0.8: {}
@@ -14480,3 +14601,5 @@ snapshots:
zod: 3.25.76
zod@3.25.76: {}
zstddec@0.1.0: {}

View File

@@ -86,7 +86,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
contentWidth: 'Fluid',
navTheme: isDark ? 'realDark' : 'light',
splitMenus: true,
iconfontUrl: '//at.alicdn.com/t/c/font_5096559_fvnh1x2eqer.js',
iconfontUrl: '//at.alicdn.com/t/c/font_5096559_pw36kpy0e7h.js',
contentStyle: {
padding: 0,
margin: 0,

View File

@@ -3,4 +3,33 @@ export default {
'master.thing.external_id': 'External ID',
'master.thing.group': 'Group',
'master.thing.address': 'Address',
// Device translations
'master.devices.title': 'Devices',
'master.devices.name': 'Name',
'master.devices.name.tip': 'The device name',
'master.devices.external_id': 'External ID',
'master.devices.external_id.tip': 'The external identifier',
'master.devices.type': 'Type',
'master.devices.type.tip': 'The device type',
'master.devices.online': 'Online',
'master.devices.offline': 'Offline',
'master.devices.table.pagination': 'devices',
'master.devices.register': 'Add Device',
'master.devices.register.title': 'Add new device',
'master.devices.create.success': 'Device created successfully',
'master.devices.create.error': 'Device creation failed',
'master.devices.groups': 'Groups',
'master.devices.groups.required': 'Please select groups',
// Edit device modal
'master.devices.update.title': 'Update device',
'master.devices.ok': 'OK',
'master.devices.cancel': 'Cancel',
'master.devices.name.placeholder': 'Enter device name',
'master.devices.name.required': 'Please enter device name',
'master.devices.external_id.placeholder': 'Enter external ID',
'master.devices.external_id.required': 'Please enter external ID',
'master.devices.address': 'Address',
'master.devices.address.placeholder': 'Enter address',
'master.devices.address.required': 'Please enter address',
};

View File

@@ -3,4 +3,32 @@ export default {
'master.thing.external_id': 'External ID',
'master.thing.group': 'Nhóm',
'master.thing.address': 'Địa chỉ',
// Device translations
'master.devices.title': 'Quản lý thiết bị',
'master.devices.name': 'Tên',
'master.devices.name.tip': 'Tên thiết bị',
'master.devices.external_id': 'External ID',
'master.devices.external_id.tip': 'Mã định danh bên ngoài',
'master.devices.type': 'Loại',
'master.devices.type.tip': 'Loại thiết bị',
'master.devices.online': 'Trực tuyến',
'master.devices.offline': 'Ngoại tuyến',
'master.devices.table.pagination': 'thiết bị',
'master.devices.register': 'Thêm thiết bị',
'master.devices.register.title': 'Thêm thiết bị mới',
'master.devices.create.success': 'Tạo thiết bị thành công',
'master.devices.create.error': 'Tạo thiết bị lỗi',
'master.devices.groups': 'Đơn vị',
'master.devices.groups.required': 'Vui lòng chọn đơn vị',
// Edit device modal
'master.devices.update.title': 'Cập nhật thiết bị',
'master.devices.ok': 'Đồng ý',
'master.devices.cancel': 'Hủy',
'master.devices.name.placeholder': 'Nhập tên thiết bị',
'master.devices.name.required': 'Vui lòng nhập tên thiết bị',
'master.devices.external_id.placeholder': 'Nhập mã định danh bên ngoài',
'master.devices.external_id.required': 'Vui lòng nhập mã định danh bên ngoài',
'master.devices.address': 'Địa chỉ',
'master.devices.address.placeholder': 'Nhập địa chỉ',
'master.devices.address.required': 'Vui lòng nhập địa chỉ',
};

View File

@@ -0,0 +1,210 @@
import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup';
import { PlusOutlined } from '@ant-design/icons';
import {
ModalForm,
ProForm,
ProFormInstance,
ProFormSelect,
ProFormText,
} from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Button } from 'antd';
import { MessageInstance } from 'antd/es/message/interface';
import { useRef, useState } from 'react';
type CreateDeviceProps = {
message: MessageInstance;
onSuccess?: (isSuccess: boolean) => void;
};
type CreateDeviceFormValues = {
name: string;
external_id: string;
type: string;
address?: string;
group_id?: string;
};
const CreateDevice = ({ message, onSuccess }: CreateDeviceProps) => {
const formRef = useRef<ProFormInstance<CreateDeviceFormValues>>();
const intl = useIntl();
const [group_id, setGroupId] = useState<string | string[] | null>(null);
const handleGroupSelect = (group: string | string[] | null) => {
setGroupId(group);
formRef.current?.setFieldsValue({
group_id: Array.isArray(group) ? group.join(',') : group || undefined,
});
};
return (
<ModalForm<CreateDeviceFormValues>
title={intl.formatMessage({
id: 'master.devices.register.title',
defaultMessage: 'Register New Device',
})}
formRef={formRef}
trigger={
<Button type="primary" key="primary" icon={<PlusOutlined />}>
<FormattedMessage
id="master.devices.register"
defaultMessage="Register"
/>
</Button>
}
autoFocusFirstInput
onFinish={async (values: CreateDeviceFormValues) => {
// TODO: Implement API call to create device
console.log('Create device with values:', values);
try {
// Placeholder for API call
// const body = {
// name: values.name,
// metadata: {
// external_id: values.external_id,
// type: values.type,
// address: values.address,
// group_id: values.group_id,
// },
// };
// const resp = await apiCreateDevice(body);
message.success(
intl.formatMessage({
id: 'master.devices.create.success',
defaultMessage: 'Create device successfully',
}),
);
formRef.current?.resetFields();
onSuccess?.(true);
return true;
} catch (error) {
message.error(
intl.formatMessage({
id: 'master.devices.create.error',
defaultMessage: 'Failed to create device',
}),
);
onSuccess?.(false);
return false;
}
}}
>
<ProFormText
name={'name'}
label={intl.formatMessage({
id: 'master.devices.name',
defaultMessage: 'Name',
})}
placeholder={intl.formatMessage({
id: 'master.devices.name.placeholder',
defaultMessage: 'Enter device name',
})}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.name.required',
defaultMessage: 'The device name is required',
}),
},
]}
/>
<ProFormText
name={'external_id'}
label={intl.formatMessage({
id: 'master.devices.external_id',
defaultMessage: 'External ID',
})}
placeholder={intl.formatMessage({
id: 'master.devices.external_id.placeholder',
defaultMessage: 'Enter external ID',
})}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.external_id.required',
defaultMessage: 'The external ID is required',
}),
},
]}
/>
<ProFormSelect
name="type"
label={intl.formatMessage({
id: 'master.devices.type',
defaultMessage: 'Type',
})}
options={[
{
label: intl.formatMessage({
id: 'master.devices.type.gms',
defaultMessage: 'GMS',
}),
value: 'gms',
},
{
label: intl.formatMessage({
id: 'master.devices.type.sgw',
defaultMessage: 'SGW',
}),
value: 'sgw',
},
{
label: intl.formatMessage({
id: 'master.devices.type.spole',
defaultMessage: 'SPOLE',
}),
value: 'spole',
},
]}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.type.required',
defaultMessage: 'Please select a device type',
}),
},
]}
/>
<ProFormText
name={'address'}
label={intl.formatMessage({
id: 'master.devices.address',
defaultMessage: 'Address',
})}
placeholder={intl.formatMessage({
id: 'master.devices.address.placeholder',
defaultMessage: 'Enter device address',
})}
/>
<ProForm.Item
name="group_id"
label={intl.formatMessage({
id: 'master.devices.groups',
defaultMessage: 'Groups',
})}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.groups.required',
defaultMessage: 'Please select groups!',
}),
},
]}
>
<TreeSelectedGroup groupIds={group_id} onSelected={handleGroupSelect} />
</ProForm.Item>
</ModalForm>
);
};
export default CreateDevice;

View File

@@ -0,0 +1,133 @@
import { useIntl } from '@umijs/max';
import { Form, Input, Modal } from 'antd';
import React, { useEffect } from 'react';
interface Props {
visible: boolean;
device: MasterModel.Thing | null;
onCancel: () => void;
onSubmit: (values: {
name: string;
external_id: string;
address?: string;
}) => void;
}
const EditDeviceModal: React.FC<Props> = ({
visible,
device,
onCancel,
onSubmit,
}) => {
const [form] = Form.useForm();
const intl = useIntl();
useEffect(() => {
if (device) {
form.setFieldsValue({
name: device.name,
external_id: device?.metadata?.external_id,
address: device?.metadata?.address,
});
} else {
form.resetFields();
}
}, [device, form]);
return (
<Modal
title={intl.formatMessage({
id: 'master.devices.update.title',
defaultMessage: 'Update device',
})}
open={visible}
onCancel={onCancel}
onOk={() => form.submit()}
okText={intl.formatMessage({
id: 'master.devices.ok',
defaultMessage: 'OK',
})}
cancelText={intl.formatMessage({
id: 'master.devices.cancel',
defaultMessage: 'Cancel',
})}
destroyOnClose
>
<Form form={form} layout="vertical" onFinish={onSubmit} preserve={false}>
<Form.Item
name="name"
label={intl.formatMessage({
id: 'master.devices.name',
defaultMessage: 'Name',
})}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.name.required',
defaultMessage: 'Please enter device name',
}),
},
]}
>
<Input
placeholder={intl.formatMessage({
id: 'master.devices.name.placeholder',
defaultMessage: 'Enter device name',
})}
/>
</Form.Item>
<Form.Item
name="external_id"
label={intl.formatMessage({
id: 'master.devices.external_id',
defaultMessage: 'External ID',
})}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.external_id.required',
defaultMessage: 'Please enter external ID',
}),
},
]}
>
<Input
placeholder={intl.formatMessage({
id: 'master.devices.external_id.placeholder',
defaultMessage: 'Enter external ID',
})}
/>
</Form.Item>
<Form.Item
name="address"
label={intl.formatMessage({
id: 'master.devices.address',
defaultMessage: 'Address',
})}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'master.devices.address.required',
defaultMessage: 'Please enter address',
}),
},
]}
>
<Input
placeholder={intl.formatMessage({
id: 'master.devices.address.placeholder',
defaultMessage: 'Enter address',
})}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default EditDeviceModal;

View File

@@ -1,5 +1,319 @@
import IconFont from '@/components/IconFont';
import TreeGroup from '@/components/shared/TreeGroup';
import { DEFAULT_PAGE_SIZE } from '@/constants';
import { apiSearchThings } from '@/services/master/ThingController';
import { EditOutlined, EnvironmentOutlined } from '@ant-design/icons';
import {
ActionType,
ProCard,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Button, Divider, Grid, Space, Tag, theme } from 'antd';
import message from 'antd/es/message';
import Paragraph from 'antd/lib/typography/Paragraph';
import { useRef, useState } from 'react';
import CreateDevice from './components/CreateDevice';
import EditDeviceModal from './components/EditDeviceModal';
const ManagerDevicePage = () => {
return <div>ManagerDevicePage</div>;
const { useBreakpoint } = Grid;
const intl = useIntl();
const screens = useBreakpoint();
const { token } = theme.useToken();
const actionRef = useRef<ActionType | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [messageApi, contextHolder] = message.useMessage();
const [selectedRowsState, setSelectedRowsState] = useState<
MasterModel.Thing[]
>([]);
const [groupCheckedKeys, setGroupCheckedKeys] = useState<
string | string[] | null
>(null);
const [isEditModalVisible, setIsEditModalVisible] = useState<boolean>(false);
const [editingDevice, setEditingDevice] = useState<MasterModel.Thing | null>(
null,
);
const handleClickAssign = (device: MasterModel.Thing) => {
console.log('Device ', device);
};
const handleEdit = (device: MasterModel.Thing) => {
setEditingDevice(device);
setIsEditModalVisible(true);
};
const handleEditCancel = () => {
setIsEditModalVisible(false);
setEditingDevice(null);
};
const handleEditSubmit = async (values: any) => {
// TODO: call update API here if available. For now just simulate success.
console.log('Update values for', editingDevice?.id, values);
messageApi.success('Cập nhật thành công');
setIsEditModalVisible(false);
setEditingDevice(null);
actionRef.current?.reload();
};
const columns: ProColumns<MasterModel.Thing>[] = [
{
key: 'name',
title: (
<FormattedMessage id="master.devices.name" defaultMessage="Name" />
),
tip: intl.formatMessage({
id: 'master.devices.name.tip',
defaultMessage: 'The device name',
}),
dataIndex: 'name',
render: (_, record) => (
<Paragraph
style={{
marginBottom: 0,
verticalAlign: 'middle',
display: 'inline-block',
color: token.colorText,
}}
copyable
>
{record?.name}
</Paragraph>
),
},
{
key: 'external_id',
title: (
<FormattedMessage
id="master.devices.external_id"
defaultMessage="External ID"
/>
),
tip: intl.formatMessage({
id: 'master.devices.external_id.tip',
defaultMessage: 'The external identifier',
}),
responsive: ['lg', 'md'],
dataIndex: ['metadata', 'external_id'],
render: (_, record) =>
record?.metadata?.external_id ? (
<Paragraph
style={{
marginBottom: 0,
verticalAlign: 'middle',
display: 'inline-block',
color: token.colorText,
}}
copyable
>
{record?.metadata?.external_id}
</Paragraph>
) : (
'-'
),
},
{
key: 'type',
hideInSearch: true,
title: (
<FormattedMessage id="master.devices.type" defaultMessage="Type" />
),
tip: intl.formatMessage({
id: 'master.devices.type.tip',
defaultMessage: 'The device type',
}),
dataIndex: ['metadata', 'type'],
render: (_, record) => record?.metadata?.type || '...',
},
{
key: 'connected',
hideInSearch: true,
title: <FormattedMessage id="common.status" defaultMessage="Status" />,
dataIndex: ['metadata', 'connected'],
render: (_, record) => (
<Tag color={record?.metadata?.connected ? 'green' : 'red'}>
{record?.metadata?.connected
? intl.formatMessage({
id: 'master.devices.online',
defaultMessage: 'Online',
})
: intl.formatMessage({
id: 'master.devices.offline',
defaultMessage: 'Offline',
})}
</Tag>
),
},
{
title: (
<FormattedMessage id="common.actions" defaultMessage="Operating" />
),
hideInSearch: true,
render: (_, device) => {
return (
<Space
size={5}
split={<Divider type="vertical" style={{ margin: '0 4px' }} />}
>
<Button
shape="default"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(device)}
/>
<Button
shape="default"
size="small"
icon={<EnvironmentOutlined />}
// onClick={() => handleClickAssign(device)}
/>
<Button
shape="default"
size="small"
icon={<IconFont type="icon-camera" />}
// onClick={() => handleClickAssign(device)}
/>
{device?.metadata?.type === 'gmsv6' && (
<Button
shape="default"
size="small"
icon={<IconFont type="icon-terminal" />}
// onClick={() => handleClickAssign(device)}
/>
)}
</Space>
);
},
},
];
return (
<>
<EditDeviceModal
visible={isEditModalVisible}
device={editingDevice}
onCancel={handleEditCancel}
onSubmit={handleEditSubmit}
/>
{contextHolder}
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
<ProCard colSpan={{ xs: 24, sm: 24, md: 6, lg: 6, xl: 6 }}>
<TreeGroup
disable={isLoading}
multiple={true}
groupIds={groupCheckedKeys}
onSelected={(value: string | string[] | null) => {
setGroupCheckedKeys(value);
if (actionRef.current) {
actionRef.current.reload();
}
}}
/>
</ProCard>
<ProCard colSpan={{ xs: 24, sm: 24, md: 18, lg: 18, xl: 18 }}>
<ProTable<MasterModel.Thing>
columns={columns}
tableLayout="auto"
actionRef={actionRef}
rowKey="id"
search={{
layout: 'vertical',
defaultCollapsed: false,
}}
dateFormatter="string"
rowSelection={{
selectedRowKeys: selectedRowsState.map((row) => row.id!),
onChange: (_: React.Key[], selectedRows: MasterModel.Thing[]) => {
setSelectedRowsState(selectedRows);
},
}}
pagination={{
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
showSizeChanger: true,
pageSizeOptions: ['10', '15', '20'],
showTotal: (total, range) =>
`${range[0]}-${range[1]}
${intl.formatMessage({
id: 'common.paginations.of',
defaultMessage: 'of',
})}
${total} ${intl.formatMessage({
id: 'master.devices.table.pagination',
defaultMessage: 'devices',
})}`,
}}
request={async (params = {}) => {
const { current = 1, pageSize, name, external_id } = params;
const size = pageSize || DEFAULT_PAGE_SIZE * 2;
const offset = current === 1 ? 0 : (current - 1) * size;
setIsLoading(true);
const metadata: Partial<MasterModel.ThingMetadata> = {};
if (external_id) metadata.external_id = external_id;
// Add group filter if groups are selected
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
const groupId = Array.isArray(groupCheckedKeys)
? groupCheckedKeys.join(',')
: groupCheckedKeys;
metadata.group_id = groupId;
}
const query: MasterModel.SearchThingPaginationBody = {
offset: offset,
limit: size,
order: 'name',
dir: 'asc',
};
if (name) query.name = name;
if (Object.keys(metadata).length > 0) query.metadata = metadata;
try {
const response = await apiSearchThings(query);
setIsLoading(false);
return {
data: response.things || [],
success: true,
total: response.total || 0,
};
} catch (error) {
setIsLoading(false);
return {
data: [],
success: false,
total: 0,
};
}
}}
options={{
search: false,
setting: false,
density: false,
reload: true,
}}
toolBarRender={() => [
<CreateDevice
message={messageApi}
onSuccess={(isSuccess) => {
if (isSuccess) {
actionRef.current?.reload();
}
}}
key="create-device"
/>,
]}
/>
</ProCard>
</ProCard>
</>
);
};
export default ManagerDevicePage;