feat: add Manager Dashboard and Device Terminal features
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
||||
forgotPasswordRoute,
|
||||
loginRoute,
|
||||
managerCameraRoute,
|
||||
managerDashboardRoute,
|
||||
managerDeviceTerminalRoute,
|
||||
managerRouteBase,
|
||||
notFoundRoute,
|
||||
profileRoute,
|
||||
@@ -28,7 +30,12 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
...managerRouteBase,
|
||||
routes: [...commonManagerRoutes, managerCameraRoute],
|
||||
routes: [
|
||||
managerDashboardRoute,
|
||||
...commonManagerRoutes,
|
||||
managerCameraRoute,
|
||||
managerDeviceTerminalRoute,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
|
||||
@@ -97,11 +97,22 @@ export const managerCameraRoute = {
|
||||
component: './Manager/Device/Camera',
|
||||
};
|
||||
|
||||
export const managerDeviceTerminalRoute = {
|
||||
path: '/manager/devices/:thingId/terminal',
|
||||
component: './Manager/Device/Terminal',
|
||||
};
|
||||
|
||||
export const managerRouteBase = {
|
||||
name: 'manager',
|
||||
icon: 'icon-setting',
|
||||
path: '/manager',
|
||||
access: 'canAdmin_SysAdmin',
|
||||
hideChildrenInMenu: true,
|
||||
};
|
||||
|
||||
export const managerDashboardRoute = {
|
||||
path: '/manager',
|
||||
component: './Manager/Dashboard',
|
||||
};
|
||||
|
||||
export const notFoundRoute = {
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -18,7 +18,9 @@
|
||||
"ol": "^10.6.1",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
@@ -22725,6 +22727,23 @@
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xterm-addon-fit": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
|
||||
"integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
|
||||
"deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -28,8 +28,11 @@
|
||||
"mqtt": "^5.15.0",
|
||||
"ol": "^10.6.1",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-countup": "^6.5.3",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
|
||||
284
pnpm-lock.yaml
generated
284
pnpm-lock.yaml
generated
@@ -32,15 +32,30 @@ importers:
|
||||
moment:
|
||||
specifier: ^2.30.1
|
||||
version: 2.30.1
|
||||
mqtt:
|
||||
specifier: ^5.15.0
|
||||
version: 5.15.0
|
||||
ol:
|
||||
specifier: ^10.6.1
|
||||
version: 10.7.0
|
||||
react-chartjs-2:
|
||||
specifier: ^5.3.1
|
||||
version: 5.3.1(chart.js@4.5.1)(react@18.3.1)
|
||||
react-countup:
|
||||
specifier: ^6.5.3
|
||||
version: 6.5.3(react@18.3.1)
|
||||
reconnecting-websocket:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
uuid:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
xterm:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
xterm-addon-fit:
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0(xterm@5.3.0)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^18.0.33
|
||||
@@ -48,6 +63,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^18.0.11
|
||||
version: 18.3.7(@types/react@18.3.27)
|
||||
'@types/uuid':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
babel-plugin-transform-remove-console:
|
||||
specifier: ^6.9.4
|
||||
version: 6.9.4
|
||||
@@ -1397,6 +1415,9 @@ packages:
|
||||
'@types/react@18.3.27':
|
||||
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz}
|
||||
|
||||
'@types/readable-stream@4.0.23':
|
||||
resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==, tarball: https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz}
|
||||
|
||||
'@types/resolve@1.20.6':
|
||||
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz}
|
||||
|
||||
@@ -1409,6 +1430,12 @@ packages:
|
||||
'@types/use-sync-external-store@0.0.3':
|
||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==, tarball: https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz}
|
||||
|
||||
'@types/uuid@10.0.0':
|
||||
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==, tarball: https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==, tarball: https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz}
|
||||
|
||||
'@types/yargs-parser@21.0.3':
|
||||
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==, tarball: https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz}
|
||||
|
||||
@@ -1824,6 +1851,10 @@ packages:
|
||||
'@xtuc/long@4.2.2':
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==, tarball: https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, tarball: https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz}
|
||||
engines: {node: '>=6.5'}
|
||||
|
||||
accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, tarball: https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2090,6 +2121,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, tarball: https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
bl@6.1.6:
|
||||
resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==, tarball: https://registry.npmjs.org/bl/-/bl-6.1.6.tgz}
|
||||
|
||||
bn.js@4.12.2:
|
||||
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==, tarball: https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz}
|
||||
|
||||
@@ -2117,6 +2151,9 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, tarball: https://registry.npmjs.org/braces/-/braces-3.0.3.tgz}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
broker-factory@3.1.13:
|
||||
resolution: {integrity: sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==, tarball: https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz}
|
||||
|
||||
brorand@1.1.0:
|
||||
resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==, tarball: https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz}
|
||||
|
||||
@@ -2160,6 +2197,9 @@ packages:
|
||||
buffer@4.9.2:
|
||||
resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==, tarball: https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz}
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, tarball: https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz}
|
||||
|
||||
builtin-status-codes@3.0.0:
|
||||
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, tarball: https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz}
|
||||
|
||||
@@ -2318,6 +2358,9 @@ packages:
|
||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==, tarball: https://registry.npmjs.org/commander/-/commander-8.3.0.tgz}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
commist@3.2.0:
|
||||
resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==, tarball: https://registry.npmjs.org/commist/-/commist-3.2.0.tgz}
|
||||
|
||||
common-path-prefix@3.0.0:
|
||||
resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==, tarball: https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz}
|
||||
|
||||
@@ -2338,6 +2381,10 @@ packages:
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, tarball: https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==, tarball: https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz}
|
||||
engines: {'0': node >= 6.0}
|
||||
|
||||
connect-history-api-fallback@2.0.0:
|
||||
resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==, tarball: https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -2405,6 +2452,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
countup.js@2.9.0:
|
||||
resolution: {integrity: sha512-llqrvyXztRFPp6+i8jx25phHWcVWhrHO4Nlt0uAOSKHB8778zzQswa4MU3qKBvkXfJKftRYFJuVHez67lyKdHg==, tarball: https://registry.npmjs.org/countup.js/-/countup.js-2.9.0.tgz}
|
||||
|
||||
create-ecdh@4.0.4:
|
||||
resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==, tarball: https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz}
|
||||
|
||||
@@ -3015,6 +3065,10 @@ packages:
|
||||
event-emitter@0.3.5:
|
||||
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==, tarball: https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, tarball: https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==, tarball: https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz}
|
||||
|
||||
@@ -3073,6 +3127,10 @@ packages:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==, tarball: https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-unique-numbers@9.0.26:
|
||||
resolution: {integrity: sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==, tarball: https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz}
|
||||
engines: {node: '>=18.2.0'}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==, tarball: https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz}
|
||||
|
||||
@@ -3372,6 +3430,9 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==, tarball: https://registry.npmjs.org/he/-/he-1.2.0.tgz}
|
||||
hasBin: true
|
||||
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==, tarball: https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz}
|
||||
|
||||
history@4.10.1:
|
||||
resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==, tarball: https://registry.npmjs.org/history/-/history-4.10.1.tgz}
|
||||
|
||||
@@ -3539,6 +3600,10 @@ packages:
|
||||
invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==, tarball: https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==, tarball: https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, tarball: https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -3808,6 +3873,9 @@ packages:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==, tarball: https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz}
|
||||
hasBin: true
|
||||
|
||||
js-sdsl@4.3.0:
|
||||
resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==, tarball: https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz}
|
||||
|
||||
js-sdsl@4.4.2:
|
||||
resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==, tarball: https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz}
|
||||
|
||||
@@ -4169,6 +4237,9 @@ packages:
|
||||
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==, tarball: https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -4176,6 +4247,14 @@ packages:
|
||||
moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==, tarball: https://registry.npmjs.org/moment/-/moment-2.30.1.tgz}
|
||||
|
||||
mqtt-packet@9.0.2:
|
||||
resolution: {integrity: sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==, tarball: https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz}
|
||||
|
||||
mqtt@5.15.0:
|
||||
resolution: {integrity: sha512-KC+wAssYk83Qu5bT8YDzDYgUJxPhbLeVsDvpY2QvL28PnXYJzC2WkKruyMUgBAZaQ7h9lo9k2g4neRNUUxzgMw==, tarball: https://registry.npmjs.org/mqtt/-/mqtt-5.15.0.tgz}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==, tarball: https://registry.npmjs.org/ms/-/ms-2.0.0.tgz}
|
||||
|
||||
@@ -4276,6 +4355,9 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==, tarball: https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz}
|
||||
|
||||
number-allocator@1.0.14:
|
||||
resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==, tarball: https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, tarball: https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5401,6 +5483,11 @@ packages:
|
||||
chart.js: ^4.1.1
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-countup@6.5.3:
|
||||
resolution: {integrity: sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==, tarball: https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz}
|
||||
peerDependencies:
|
||||
react: '>= 16.3.0'
|
||||
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==, tarball: https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz}
|
||||
peerDependencies:
|
||||
@@ -5524,6 +5611,10 @@ packages:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, tarball: https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readable-stream@4.7.0:
|
||||
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==, tarball: https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@@ -5859,6 +5950,14 @@ packages:
|
||||
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==, tarball: https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
smart-buffer@4.2.0:
|
||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==, tarball: https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz}
|
||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
socks@2.8.7:
|
||||
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==, tarball: https://registry.npmjs.org/socks/-/socks-2.8.7.tgz}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
sonic-boom@2.8.0:
|
||||
resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==, tarball: https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz}
|
||||
|
||||
@@ -6288,6 +6387,9 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==, tarball: https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==, tarball: https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -6377,6 +6479,10 @@ packages:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==, tarball: https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
uuid@13.0.0:
|
||||
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==, tarball: https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz}
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache@2.4.0:
|
||||
resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==, tarball: https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz}
|
||||
|
||||
@@ -6506,6 +6612,18 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, tarball: https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
worker-factory@7.0.48:
|
||||
resolution: {integrity: sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==, tarball: https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz}
|
||||
|
||||
worker-timers-broker@8.0.15:
|
||||
resolution: {integrity: sha512-Te+EiVUMzG5TtHdmaBZvBrZSFNauym6ImDaCAnzQUxvjnw+oGjMT2idmAOgDy30vOZMLejd0bcsc90Axu6XPWA==, tarball: https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.15.tgz}
|
||||
|
||||
worker-timers-worker@9.0.13:
|
||||
resolution: {integrity: sha512-qjn18szGb1kjcmh2traAdki1eiIS5ikFo+L90nfMOvSRpuDw1hAcR1nzkP2+Hkdqz5thIRnfuWx7QSpsEUsA6Q==, tarball: https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.13.tgz}
|
||||
|
||||
worker-timers@8.0.30:
|
||||
resolution: {integrity: sha512-8P7YoMHWN0Tz7mg+9oEhuZdjBIn2z6gfjlJqFcHiDd9no/oLnMGCARCDkV1LR3ccQus62ZdtIp7t3aTKrMLHOg==, tarball: https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.30.tgz}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6540,6 +6658,16 @@ packages:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, tarball: https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
xterm-addon-fit@0.8.0:
|
||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==, tarball: https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz}
|
||||
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
|
||||
peerDependencies:
|
||||
xterm: ^5.0.0
|
||||
|
||||
xterm@5.3.0:
|
||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==, tarball: https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz}
|
||||
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, tarball: https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8213,6 +8341,10 @@ snapshots:
|
||||
'@types/prop-types': 15.7.15
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/readable-stream@4.0.23':
|
||||
dependencies:
|
||||
'@types/node': 25.0.9
|
||||
|
||||
'@types/resolve@1.20.6': {}
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
@@ -8221,6 +8353,12 @@ snapshots:
|
||||
|
||||
'@types/use-sync-external-store@0.0.3': {}
|
||||
|
||||
'@types/uuid@10.0.0': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.0.9
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
'@types/yargs@13.0.12':
|
||||
@@ -8518,7 +8656,7 @@ snapshots:
|
||||
|
||||
'@umijs/history@5.3.1':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@babel/runtime': 7.28.6
|
||||
query-string: 6.14.1
|
||||
|
||||
'@umijs/lint@4.6.23(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)':
|
||||
@@ -9012,6 +9150,10 @@ snapshots:
|
||||
|
||||
'@xtuc/long@4.2.2': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
@@ -9419,6 +9561,13 @@ snapshots:
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bl@6.1.6:
|
||||
dependencies:
|
||||
'@types/readable-stream': 4.0.23
|
||||
buffer: 6.0.3
|
||||
inherits: 2.0.4
|
||||
readable-stream: 4.7.0
|
||||
|
||||
bn.js@4.12.2: {}
|
||||
|
||||
bn.js@5.2.2: {}
|
||||
@@ -9459,6 +9608,13 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
broker-factory@3.1.13:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
fast-unique-numbers: 9.0.26
|
||||
tslib: 2.8.1
|
||||
worker-factory: 7.0.48
|
||||
|
||||
brorand@1.1.0: {}
|
||||
|
||||
browserify-aes@1.2.0:
|
||||
@@ -9533,6 +9689,11 @@ snapshots:
|
||||
ieee754: 1.2.1
|
||||
isarray: 1.0.0
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
builtin-status-codes@3.0.0: {}
|
||||
|
||||
bundle-name@3.0.0:
|
||||
@@ -9697,6 +9858,8 @@ snapshots:
|
||||
|
||||
commander@8.3.0: {}
|
||||
|
||||
commist@3.2.0: {}
|
||||
|
||||
common-path-prefix@3.0.0: {}
|
||||
|
||||
compressible@2.0.18:
|
||||
@@ -9721,6 +9884,13 @@ snapshots:
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
typedarray: 0.0.6
|
||||
|
||||
connect-history-api-fallback@2.0.0: {}
|
||||
|
||||
console-browserify@1.2.0: {}
|
||||
@@ -9781,6 +9951,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
countup.js@2.9.0: {}
|
||||
|
||||
create-ecdh@4.0.4:
|
||||
dependencies:
|
||||
bn.js: 4.12.2
|
||||
@@ -10551,6 +10723,8 @@ snapshots:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
events-okam@3.3.0: {}
|
||||
@@ -10662,6 +10836,11 @@ snapshots:
|
||||
|
||||
fast-redact@3.5.0: {}
|
||||
|
||||
fast-unique-numbers@9.0.26:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
tslib: 2.8.1
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fastest-levenshtein@1.0.16: {}
|
||||
@@ -10984,6 +11163,8 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
history@4.10.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
@@ -11158,6 +11339,8 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-arguments@1.2.0:
|
||||
@@ -11450,6 +11633,8 @@ snapshots:
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
js-sdsl@4.3.0: {}
|
||||
|
||||
js-sdsl@4.4.2: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -11789,10 +11974,43 @@ snapshots:
|
||||
is-plain-obj: 1.1.0
|
||||
kind-of: 6.0.3
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
mqtt-packet@9.0.2:
|
||||
dependencies:
|
||||
bl: 6.1.6
|
||||
debug: 4.4.3
|
||||
process-nextick-args: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mqtt@5.15.0:
|
||||
dependencies:
|
||||
'@types/readable-stream': 4.0.23
|
||||
'@types/ws': 8.18.1
|
||||
commist: 3.2.0
|
||||
concat-stream: 2.0.0
|
||||
debug: 4.4.3
|
||||
help-me: 5.0.0
|
||||
lru-cache: 10.4.3
|
||||
minimist: 1.2.8
|
||||
mqtt-packet: 9.0.2
|
||||
number-allocator: 1.0.14
|
||||
readable-stream: 4.7.0
|
||||
rfdc: 1.4.1
|
||||
socks: 2.8.7
|
||||
split2: 4.2.0
|
||||
worker-timers: 8.0.30
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.1: {}
|
||||
@@ -11933,6 +12151,13 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
number-allocator@1.0.14:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
js-sdsl: 4.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
@@ -13238,6 +13463,11 @@ snapshots:
|
||||
chart.js: 4.5.1
|
||||
react: 18.3.1
|
||||
|
||||
react-countup@6.5.3(react@18.3.1):
|
||||
dependencies:
|
||||
countup.js: 2.9.0
|
||||
react: 18.3.1
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -13397,6 +13627,14 @@ snapshots:
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readable-stream@4.7.0:
|
||||
dependencies:
|
||||
abort-controller: 3.0.0
|
||||
buffer: 6.0.3
|
||||
events: 3.3.0
|
||||
process: 0.11.10
|
||||
string_decoder: 1.3.0
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
@@ -13768,6 +14006,13 @@ snapshots:
|
||||
ansi-styles: 6.2.3
|
||||
is-fullwidth-code-point: 4.0.0
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
|
||||
socks@2.8.7:
|
||||
dependencies:
|
||||
ip-address: 10.1.0
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
sonic-boom@2.8.0:
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
@@ -14293,6 +14538,8 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
umi@4.6.23(@babel/core@7.28.6)(@types/node@25.0.9)(@types/react@18.3.27)(eslint@8.35.0)(less-loader@12.3.0(less@4.5.1)(webpack@5.104.1))(less@4.5.1)(lightningcss@1.22.1)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.29.5)(sass-loader@16.0.6(sass@1.54.0)(webpack@5.104.1))(sass@1.54.0)(stylelint@14.8.2)(terser@5.46.0)(type-fest@1.4.0)(typescript@5.9.3)(webpack@5.104.1):
|
||||
@@ -14424,6 +14671,8 @@ snapshots:
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@13.0.0: {}
|
||||
|
||||
v8-compile-cache@2.4.0: {}
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
@@ -14575,6 +14824,33 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
worker-factory@7.0.48:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
fast-unique-numbers: 9.0.26
|
||||
tslib: 2.8.1
|
||||
|
||||
worker-timers-broker@8.0.15:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
broker-factory: 3.1.13
|
||||
fast-unique-numbers: 9.0.26
|
||||
tslib: 2.8.1
|
||||
worker-timers-worker: 9.0.13
|
||||
|
||||
worker-timers-worker@9.0.13:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
tslib: 2.8.1
|
||||
worker-factory: 7.0.48
|
||||
|
||||
worker-timers@8.0.30:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
tslib: 2.8.1
|
||||
worker-timers-broker: 8.0.15
|
||||
worker-timers-worker: 9.0.13
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -14600,6 +14876,12 @@ snapshots:
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
xterm-addon-fit@0.8.0(xterm@5.3.0):
|
||||
dependencies:
|
||||
xterm: 5.3.0
|
||||
|
||||
xterm@5.3.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
10
src/app.tsx
10
src/app.tsx
@@ -159,6 +159,12 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
||||
},
|
||||
layout: 'top',
|
||||
menuHeaderRender: undefined,
|
||||
subMenuItemRender: (item, dom) => {
|
||||
if (item.path) {
|
||||
return <Link to={item.path}>{dom}</Link>;
|
||||
}
|
||||
return dom;
|
||||
},
|
||||
menuItemRender: (item, dom) => {
|
||||
if (item.path) {
|
||||
// Coerce values to string to satisfy TypeScript expectations
|
||||
@@ -176,11 +182,11 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
||||
},
|
||||
token: {
|
||||
header: {
|
||||
colorBgMenuItemSelected: '#EEF7FF', // background khi chọn
|
||||
colorBgMenuItemSelected: isDark ? '#111b26' : '#EEF7FF', // background khi chọn
|
||||
colorTextMenuSelected: isDark ? '#fff' : '#1A2130', // màu chữ khi chọn
|
||||
},
|
||||
pageContainer: {
|
||||
paddingInlinePageContainerContent: 8,
|
||||
// paddingInlinePageContainerContent: 0,
|
||||
paddingBlockPageContainerContent: 8,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import { useModel } from '@umijs/max';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getTheme } from './ThemeSwitcher';
|
||||
import React from 'react';
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const [isDark, setIsDark] = useState(getTheme() === 'dark');
|
||||
|
||||
useEffect(() => {
|
||||
const handleThemeChange = (e: CustomEvent) => {
|
||||
setIsDark(e.detail.theme === 'dark');
|
||||
};
|
||||
|
||||
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'theme-change',
|
||||
handleThemeChange as EventListener,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const isDark = (initialState?.theme as 'light' | 'dark') === 'dark';
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
|
||||
259
src/pages/Manager/Dashboard/index.tsx
Normal file
259
src/pages/Manager/Dashboard/index.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { apiQueryGroups } from '@/services/master/GroupController';
|
||||
import { apiQueryLogs } from '@/services/master/LogController';
|
||||
import { apiSearchThings } from '@/services/master/ThingController';
|
||||
import { apiQueryUsers } from '@/services/master/UserController';
|
||||
import { RightOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProCard } from '@ant-design/pro-components';
|
||||
import { history, useIntl } from '@umijs/max';
|
||||
import { Button, Col, Divider, Row, Statistic, Typography } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import CountUp from 'react-countup';
|
||||
const { Text } = Typography;
|
||||
|
||||
const ManagerDashboard = () => {
|
||||
const intl = useIntl();
|
||||
const [counts, setCounts] = useState({
|
||||
devices: 0,
|
||||
groups: 0,
|
||||
users: 0,
|
||||
logs: 0,
|
||||
});
|
||||
const [deviceMetadata, setDeviceMetadata] =
|
||||
useState<MasterModel.ThingsResponseMetadata | null>(null);
|
||||
|
||||
const formatter = (value: number | string) => (
|
||||
<CountUp end={Number(value)} separator="," duration={2} />
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [devicesRes, groupsRes, usersRes, logsRes] = await Promise.all([
|
||||
apiSearchThings({ limit: 1 }),
|
||||
apiQueryGroups({}),
|
||||
apiQueryUsers({ limit: 1 }),
|
||||
apiQueryLogs({ limit: 1 }, 'user_logs'),
|
||||
]);
|
||||
|
||||
const devicesTotal = devicesRes?.total || 0;
|
||||
const metadata = (devicesRes as any)?.metadata || null;
|
||||
|
||||
// Group response handling
|
||||
const groupsTotal =
|
||||
(Array.isArray(groupsRes)
|
||||
? groupsRes.length
|
||||
: (groupsRes as any)?.total) || 0;
|
||||
const usersTotal = usersRes?.total || 0;
|
||||
const logsTotal = logsRes?.total || 0;
|
||||
|
||||
setCounts({
|
||||
devices: devicesTotal,
|
||||
groups: groupsTotal,
|
||||
users: usersTotal,
|
||||
logs: logsTotal,
|
||||
});
|
||||
setDeviceMetadata(metadata);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard counts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProCard gutter={[16, 16]} ghost>
|
||||
<ProCard colSpan={{ xs: 24, md: 10 }} ghost gutter={[16, 16]} wrap>
|
||||
<ProCard
|
||||
colSpan={24}
|
||||
title={intl.formatMessage({
|
||||
id: 'menu.manager.devices',
|
||||
defaultMessage: 'Thiết bị',
|
||||
})}
|
||||
extra={
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => history.push('/manager/devices')}
|
||||
icon={<RightOutlined />}
|
||||
>
|
||||
Xem chi tiết
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
value={counts.devices}
|
||||
formatter={formatter as any}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={
|
||||
<IconFont
|
||||
type="icon-gateway"
|
||||
style={{ fontSize: 24, marginRight: 8 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ProCard>
|
||||
<ProCard
|
||||
colSpan={24}
|
||||
title={intl.formatMessage({
|
||||
id: 'menu.manager.groups',
|
||||
defaultMessage: 'Đơn vị',
|
||||
})}
|
||||
extra={
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => history.push('/manager/groups')}
|
||||
icon={<RightOutlined />}
|
||||
>
|
||||
Xem chi tiết
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
value={counts.groups}
|
||||
formatter={formatter as any}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
prefix={
|
||||
<IconFont
|
||||
type="icon-tree"
|
||||
style={{ fontSize: 24, marginRight: 8 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ProCard>
|
||||
<ProCard
|
||||
colSpan={24}
|
||||
title={intl.formatMessage({
|
||||
id: 'menu.manager.users',
|
||||
defaultMessage: 'Người dùng',
|
||||
})}
|
||||
extra={
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => history.push('/manager/users')}
|
||||
icon={<RightOutlined />}
|
||||
>
|
||||
Xem chi tiết
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
value={counts.users}
|
||||
formatter={formatter as any}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
prefix={
|
||||
<IconFont
|
||||
type="icon-users"
|
||||
style={{ fontSize: 24, marginRight: 8 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ProCard>
|
||||
<ProCard
|
||||
colSpan={24}
|
||||
title={intl.formatMessage({
|
||||
id: 'menu.manager.logs',
|
||||
defaultMessage: 'Hoạt động',
|
||||
})}
|
||||
extra={
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => history.push('/manager/logs')}
|
||||
icon={<RightOutlined />}
|
||||
>
|
||||
Xem chi tiết
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
value={counts.logs}
|
||||
formatter={formatter as any}
|
||||
valueStyle={{ color: '#f5222d' }}
|
||||
prefix={
|
||||
<IconFont
|
||||
type="icon-diary"
|
||||
style={{ fontSize: 24, marginRight: 8 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
<ProCard
|
||||
colSpan={{ xs: 24, md: 8 }}
|
||||
title="Trạng thái thiết bị"
|
||||
headerBordered
|
||||
>
|
||||
{deviceMetadata && (
|
||||
<Row gutter={[0, 16]}>
|
||||
<Col span={24}>
|
||||
<Text type="secondary">Kết nối</Text>
|
||||
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
|
||||
<CountUp end={deviceMetadata.total_connected || 0} /> /{' '}
|
||||
{deviceMetadata.total_thing}
|
||||
</div>
|
||||
</Col>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Col span={24}>
|
||||
<Text type="secondary">SOS</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
(deviceMetadata.total_sos || 0) > 0
|
||||
? '#ff4d4f'
|
||||
: 'inherit',
|
||||
}}
|
||||
>
|
||||
<CountUp end={deviceMetadata.total_sos || 0} />
|
||||
</div>
|
||||
</Col>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Col span={24}>
|
||||
<Text type="secondary">Bình thường</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#52c41a',
|
||||
}}
|
||||
>
|
||||
<CountUp end={deviceMetadata.total_state_level_0 || 0} />
|
||||
</div>
|
||||
</Col>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Col span={24}>
|
||||
<Text type="secondary">Cảnh báo</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#faad14',
|
||||
}}
|
||||
>
|
||||
<CountUp end={deviceMetadata.total_state_level_1 || 0} />
|
||||
</div>
|
||||
</Col>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Col span={24}>
|
||||
<Text type="secondary">Nghiêm trọng</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#f5222d',
|
||||
}}
|
||||
>
|
||||
<CountUp end={deviceMetadata.total_state_level_2 || 0} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagerDashboard;
|
||||
142
src/pages/Manager/Device/Terminal/components/XTerm.tsx
Normal file
142
src/pages/Manager/Device/Terminal/components/XTerm.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import 'xterm/css/xterm.css';
|
||||
|
||||
export interface XTermHandle {
|
||||
write: (text: string) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface XTermProps {
|
||||
onData?: (data: string) => void;
|
||||
cursorBlink?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
disabled?: boolean;
|
||||
welcomeMessage?: string;
|
||||
theme?: {
|
||||
background?: string;
|
||||
foreground?: string;
|
||||
cursor?: string;
|
||||
selectionBackground?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_WELCOME = 'Welcome to Smatec IoT Agent';
|
||||
|
||||
const XTerm = forwardRef<XTermHandle, XTermProps>(function XTerm(
|
||||
{
|
||||
onData,
|
||||
cursorBlink = false,
|
||||
className,
|
||||
style,
|
||||
disabled = false,
|
||||
welcomeMessage = DEFAULT_WELCOME,
|
||||
theme,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const terminalRef = useRef<Terminal | null>(null);
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const onDataRef = useRef(onData);
|
||||
const disabledRef = useRef(disabled);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
write: (text: string) => {
|
||||
if (terminalRef.current && text) {
|
||||
terminalRef.current.write(text);
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
terminalRef.current?.clear();
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onDataRef.current = onData;
|
||||
}, [onData]);
|
||||
|
||||
useEffect(() => {
|
||||
disabledRef.current = disabled;
|
||||
// Update terminal's disableStdin option when disabled prop changes
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.options.disableStdin = disabled;
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const terminal = new Terminal({
|
||||
cursorBlink,
|
||||
scrollback: 1000,
|
||||
convertEol: true,
|
||||
fontSize: 14,
|
||||
disableStdin: disabled,
|
||||
allowProposedApi: true,
|
||||
theme,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
|
||||
if (containerRef.current) {
|
||||
terminal.open(containerRef.current);
|
||||
fitAddon.fit();
|
||||
}
|
||||
|
||||
if (welcomeMessage) {
|
||||
terminal.writeln(welcomeMessage);
|
||||
}
|
||||
|
||||
const dataDisposable = terminal.onData((data) => {
|
||||
if (!disabledRef.current) {
|
||||
onDataRef.current?.(data);
|
||||
}
|
||||
});
|
||||
|
||||
terminalRef.current = terminal;
|
||||
fitAddonRef.current = fitAddon;
|
||||
|
||||
const observer =
|
||||
typeof ResizeObserver !== 'undefined' && containerRef.current
|
||||
? new ResizeObserver(() => {
|
||||
fitAddon.fit();
|
||||
})
|
||||
: null;
|
||||
if (observer && containerRef.current) {
|
||||
observer.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
dataDisposable.dispose();
|
||||
observer?.disconnect();
|
||||
terminal.dispose();
|
||||
fitAddon.dispose();
|
||||
terminalRef.current = null;
|
||||
fitAddonRef.current = null;
|
||||
};
|
||||
// We intentionally want to run this once on mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current && theme) {
|
||||
terminalRef.current.options.theme = theme;
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{ height: '100%', ...style }}
|
||||
ref={containerRef}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default XTerm;
|
||||
661
src/pages/Manager/Device/Terminal/index.tsx
Normal file
661
src/pages/Manager/Device/Terminal/index.tsx
Normal file
@@ -0,0 +1,661 @@
|
||||
// Component Terminal - Giao diện điều khiển thiết bị từ xa qua MQTT
|
||||
// Không hỗ trợ thiết bị loại GMSv5
|
||||
|
||||
import { getBadgeConnection } from '@/components/shared/ThingShared';
|
||||
import { ROUTE_MANAGER_DEVICES } from '@/constants/routes';
|
||||
import { apiGetThingDetail } from '@/services/master/ThingController';
|
||||
import { mqttClient } from '@/utils/mqttClient';
|
||||
import { BgColorsOutlined, ClearOutlined } from '@ant-design/icons';
|
||||
import { PageContainer, ProCard } from '@ant-design/pro-components';
|
||||
import { history, useIntl, useModel, useParams } from '@umijs/max';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
Result,
|
||||
Space,
|
||||
Spin,
|
||||
Typography,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import XTerm, { XTermHandle } from './components/XTerm';
|
||||
|
||||
// Format SenML record nhận từ MQTT
|
||||
|
||||
const TERMINAL_THEMES: MasterModel.TerminalThemes = {
|
||||
dark: {
|
||||
name: 'Dark',
|
||||
theme: {
|
||||
background: '#141414',
|
||||
foreground: '#ffffff',
|
||||
cursor: '#ffffff',
|
||||
selectionBackground: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
name: 'Light',
|
||||
theme: {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
cursor: '#000000',
|
||||
selectionBackground: 'rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
},
|
||||
green: {
|
||||
name: 'Green (Classic)',
|
||||
theme: {
|
||||
background: '#000000',
|
||||
foreground: '#00ff00',
|
||||
cursor: '#00ff00',
|
||||
selectionBackground: 'rgba(0, 255, 0, 0.3)',
|
||||
},
|
||||
},
|
||||
amber: {
|
||||
name: 'Amber',
|
||||
theme: {
|
||||
background: '#1a0f00',
|
||||
foreground: '#ffaa00',
|
||||
cursor: '#ffaa00',
|
||||
selectionBackground: 'rgba(255, 170, 0, 0.3)',
|
||||
},
|
||||
},
|
||||
solarized: {
|
||||
name: 'Solarized Dark',
|
||||
theme: {
|
||||
background: '#002b36',
|
||||
foreground: '#839496',
|
||||
cursor: '#93a1a1',
|
||||
selectionBackground: 'rgba(7, 54, 66, 0.99)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode chuỗi sang Base64 để gửi qua MQTT
|
||||
* Hỗ trợ Unicode characters qua UTF-8 normalization
|
||||
* Áp dụng cho tất cả thiết bị (ngoại trừ GMSv5)
|
||||
*/
|
||||
const encodeBase64 = (value: string) => {
|
||||
try {
|
||||
const normalized = encodeURIComponent(value).replace(
|
||||
/%([0-9A-F]{2})/g,
|
||||
(_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)),
|
||||
);
|
||||
if (typeof globalThis !== 'undefined' && globalThis.btoa) {
|
||||
return globalThis.btoa(normalized);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to encode terminal payload', error);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse payload nhận từ MQTT (format SenML)
|
||||
* Trích xuất các field 'vs' và join thành string hoàn chỉnh
|
||||
*/
|
||||
const parseTerminalPayload = (payload: string): string => {
|
||||
try {
|
||||
const records: MasterModel.SenmlRecord[] = JSON.parse(payload);
|
||||
if (!Array.isArray(records)) {
|
||||
return '';
|
||||
}
|
||||
return records
|
||||
.map((record) => record?.vs ?? '')
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
} catch (error) {
|
||||
console.error('Failed to parse terminal payload', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const DeviceTerminalPage = () => {
|
||||
// Lấy thingId từ URL params
|
||||
const { thingId } = useParams<{ thingId: string }>();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const { token } = theme.useToken();
|
||||
const intl = useIntl();
|
||||
|
||||
// States quản lý trạng thái thiết bị và terminal
|
||||
const [thing, setThing] = useState<MasterModel.Thing | null>(null); // Thông tin thiết bị
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false); // Đang load dữ liệu
|
||||
const [terminalError, setTerminalError] = useState<string | null>(null); // Lỗi terminal
|
||||
const [isSessionReady, setIsSessionReady] = useState<boolean>(false); // Session đã sẵn sàng
|
||||
const [connectionState, setConnectionState] =
|
||||
useState<MasterModel.TerminalConnection>({
|
||||
online: false,
|
||||
}); // Trạng thái online/offline
|
||||
const [sessionAttempt, setSessionAttempt] = useState<number>(0); // Số lần thử kết nối lại
|
||||
const [selectedThemeKey, setSelectedThemeKey] = useState<string>('dark'); // Theme được chọn
|
||||
|
||||
// Refs lưu trữ các giá trị không trigger re-render
|
||||
const sessionIdRef = useRef(uuidv4()); // ID phiên terminal duy nhất
|
||||
const terminalRef = useRef<XTermHandle | null>(null); // Reference đến XTerm component
|
||||
const responseTopicRef = useRef<string | null>(null); // Topic MQTT nhận phản hồi
|
||||
const requestTopicRef = useRef<string | null>(null); // Topic MQTT gửi yêu cầu
|
||||
const handshakeCompletedRef = useRef<boolean>(false); // Đã hoàn thành handshake chưa
|
||||
const mqttCleanupRef = useRef<Array<() => void>>([]); // Mảng cleanup functions
|
||||
|
||||
const credentialMetadata = initialState?.currentUserProfile?.metadata;
|
||||
|
||||
/**
|
||||
* Lấy MQTT credentials từ metadata user
|
||||
* Username: frontend_thing_id
|
||||
* Password: frontend_thing_key
|
||||
*/
|
||||
const mqttCredentials = useMemo(() => {
|
||||
const username = credentialMetadata?.frontend_thing_id;
|
||||
const password = credentialMetadata?.frontend_thing_key;
|
||||
if (username && password) {
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [credentialMetadata]);
|
||||
|
||||
// Kiểm tra thiết bị có hỗ trợ terminal không (không hỗ trợ GMSv5)
|
||||
const supportsTerminal = thing?.metadata?.type !== 'gmsv5';
|
||||
// Channel ID để gửi lệnh điều khiển
|
||||
const ctrlChannelId = thing?.metadata?.ctrl_channel_id;
|
||||
|
||||
/**
|
||||
* Dọn dẹp MQTT connection khi unmount hoặc reconnect
|
||||
- Hủy subscribe topic
|
||||
- Reset các ref
|
||||
*/
|
||||
const tearDownMqtt = useCallback(() => {
|
||||
mqttCleanupRef.current.forEach((dispose) => {
|
||||
try {
|
||||
dispose();
|
||||
} catch {
|
||||
// ignore individual cleanup errors
|
||||
}
|
||||
});
|
||||
mqttCleanupRef.current = [];
|
||||
if (responseTopicRef.current) {
|
||||
mqttClient.unsubscribe(responseTopicRef.current);
|
||||
responseTopicRef.current = null;
|
||||
}
|
||||
requestTopicRef.current = null;
|
||||
handshakeCompletedRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Fetch thông tin chi tiết thiết bị từ API
|
||||
- Cập nhật state thing
|
||||
- Cập nhật trạng thái online/offline
|
||||
- Xóa lỗi nếu fetch thành công
|
||||
*/
|
||||
const fetchThingDetail = useCallback(async () => {
|
||||
if (!thingId) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await apiGetThingDetail(thingId);
|
||||
setThing(resp);
|
||||
const isConnected = Boolean(resp?.metadata?.connected);
|
||||
setConnectionState({
|
||||
online: isConnected,
|
||||
lastSeen: isConnected ? undefined : Date.now(),
|
||||
});
|
||||
setTerminalError(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to load device details', error);
|
||||
setTerminalError(
|
||||
intl.formatMessage({
|
||||
id: 'terminal.loadDeviceError',
|
||||
defaultMessage: 'Không thể tải thông tin thiết bị.',
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [intl, thingId]);
|
||||
|
||||
// Fetch thông tin thiết bị khi component mount
|
||||
useEffect(() => {
|
||||
fetchThingDetail();
|
||||
}, [fetchThingDetail]);
|
||||
|
||||
// Tạo sessionId mới khi thingId thay đổi
|
||||
useEffect(() => {
|
||||
sessionIdRef.current = uuidv4();
|
||||
}, [thingId]);
|
||||
|
||||
// Khôi phục theme từ localStorage khi component mount
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('terminal_theme_key');
|
||||
if (savedTheme && TERMINAL_THEMES[savedTheme]) {
|
||||
setSelectedThemeKey(savedTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Gửi lệnh tới thiết bị qua MQTT
|
||||
* Format SenML: bn(base name), n(name), bt(base time), vs(value string)
|
||||
*/
|
||||
const publishTerminalCommand = useCallback((command: string) => {
|
||||
const topic = requestTopicRef.current;
|
||||
if (!topic || !command) {
|
||||
return;
|
||||
}
|
||||
const payload: MasterModel.MqttTerminalPayload = [
|
||||
{
|
||||
bn: `${sessionIdRef.current}:`,
|
||||
n: 'term',
|
||||
bt: Math.floor(Date.now() / 1000),
|
||||
vs: encodeBase64(command),
|
||||
},
|
||||
];
|
||||
mqttClient.publish(topic, JSON.stringify(payload));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Setup MQTT connection và handlers
|
||||
- Tạo topics để giao tiếp với thiết bị
|
||||
- Đăng ký handlers cho các sự kiện MQTT
|
||||
- Thực hiện handshake khi kết nối thành công
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!supportsTerminal || !mqttCredentials || !ctrlChannelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
tearDownMqtt();
|
||||
// Tạo topics: response topic duy nhất cho mỗi session, request topic chung
|
||||
const responseTopic = `channels/${ctrlChannelId}/messages/res/term/${sessionIdRef.current}`;
|
||||
const requestTopic = `channels/${ctrlChannelId}/messages/req`;
|
||||
responseTopicRef.current = responseTopic;
|
||||
requestTopicRef.current = requestTopic;
|
||||
handshakeCompletedRef.current = false;
|
||||
|
||||
/**
|
||||
* Handler khi MQTT kết nối thành công
|
||||
- Subscribe response topic
|
||||
- Gửi lệnh handshake: 'open' và 'cd /'
|
||||
*/
|
||||
const handleConnect = () => {
|
||||
setTerminalError(null);
|
||||
if (!handshakeCompletedRef.current) {
|
||||
mqttClient.subscribe(responseTopic);
|
||||
publishTerminalCommand('open');
|
||||
publishTerminalCommand('cd /');
|
||||
handshakeCompletedRef.current = true;
|
||||
}
|
||||
setIsSessionReady(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler nhận message từ thiết bị
|
||||
- Parse payload SenML
|
||||
- Ghi dữ liệu ra terminal
|
||||
*/
|
||||
const handleMessage = (topic: string, message: Buffer) => {
|
||||
if (topic !== responseTopic) {
|
||||
return;
|
||||
}
|
||||
const text = parseTerminalPayload(message.toString());
|
||||
if (text) {
|
||||
terminalRef.current?.write(text);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler khi MQTT bị ngắt kết nối
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setIsSessionReady(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler khi có lỗi MQTT
|
||||
- Dọn dẹp connection
|
||||
- Hiển thị thông báo lỗi
|
||||
*/
|
||||
const handleError = (error: Error) => {
|
||||
console.error('MQTT terminal error', error);
|
||||
setIsSessionReady(false);
|
||||
tearDownMqtt();
|
||||
mqttClient.disconnect();
|
||||
setTerminalError(
|
||||
intl.formatMessage({
|
||||
id: 'terminal.mqttError',
|
||||
defaultMessage: 'Không thể kết nối MQTT.',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Kết nối MQTT và đăng ký handlers
|
||||
mqttClient.connect(mqttCredentials);
|
||||
const offConnect = mqttClient.onConnect(handleConnect);
|
||||
const offMessage = mqttClient.onMessage(handleMessage);
|
||||
const offClose = mqttClient.onClose(handleClose);
|
||||
const offError = mqttClient.onError(handleError);
|
||||
mqttCleanupRef.current = [offConnect, offMessage, offClose, offError];
|
||||
|
||||
// Nếu đã kết nối thì xử lý luôn
|
||||
if (mqttClient.isConnected()) {
|
||||
handleConnect();
|
||||
}
|
||||
|
||||
// Cleanup khi unmount hoặc dependencies thay đổi
|
||||
return () => {
|
||||
tearDownMqtt();
|
||||
mqttClient.disconnect();
|
||||
setIsSessionReady(false);
|
||||
};
|
||||
}, [
|
||||
ctrlChannelId,
|
||||
intl,
|
||||
mqttCredentials,
|
||||
publishTerminalCommand,
|
||||
sessionAttempt,
|
||||
supportsTerminal,
|
||||
tearDownMqtt,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Xử lý khi người dùng nhập liệu vào terminal
|
||||
- Gửi từng ký tự với prefix 'c,'
|
||||
- Chỉ gửi khi session đã sẵn sàng
|
||||
*/
|
||||
const handleTyping = useCallback(
|
||||
(data: string) => {
|
||||
if (!isSessionReady) {
|
||||
return;
|
||||
}
|
||||
publishTerminalCommand(`c,${data}`);
|
||||
},
|
||||
[isSessionReady, publishTerminalCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
* Xử lý retry khi có lỗi
|
||||
- Tạo sessionId mới
|
||||
- Tăng sessionAttempt để trigger reconnect
|
||||
- Fetch lại thông tin thiết bị
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
setTerminalError(null);
|
||||
setSessionAttempt((prev) => prev + 1);
|
||||
sessionIdRef.current = uuidv4();
|
||||
fetchThingDetail();
|
||||
};
|
||||
|
||||
// Label hiển thị trạng thái kết nối
|
||||
const connectionLabel = connectionState.online
|
||||
? intl.formatMessage({ id: 'common.online', defaultMessage: 'Online' })
|
||||
: intl.formatMessage({ id: 'common.offline', defaultMessage: 'Offline' });
|
||||
|
||||
/**
|
||||
* Render kết quả blocking (lỗi, warning, info)
|
||||
- Hiển thị nút quay lại và thử lại
|
||||
*/
|
||||
const renderBlockingResult = (
|
||||
status: 'info' | 'warning' | 'error' | '404',
|
||||
title: string,
|
||||
subTitle?: string,
|
||||
) => (
|
||||
<Result
|
||||
status={status}
|
||||
title={title}
|
||||
subTitle={subTitle}
|
||||
extra={[
|
||||
<Button key="back" onClick={() => history.push(ROUTE_MANAGER_DEVICES)}>
|
||||
{intl.formatMessage({
|
||||
id: 'common.back',
|
||||
defaultMessage: 'Quay lại',
|
||||
})}
|
||||
</Button>,
|
||||
<Button key="retry" type="primary" onClick={handleRetry}>
|
||||
{intl.formatMessage({
|
||||
id: 'common.retry',
|
||||
defaultMessage: 'Thử lại',
|
||||
})}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
// Tiêu đề trang
|
||||
const pageTitle =
|
||||
thing?.name ||
|
||||
intl.formatMessage({
|
||||
id: 'terminal.pageTitle',
|
||||
defaultMessage: 'Terminal',
|
||||
});
|
||||
|
||||
/**
|
||||
* Điều kiện hiển thị terminal
|
||||
- Tất cả điều kiện phải true
|
||||
*/
|
||||
const showTerminal =
|
||||
!isLoading &&
|
||||
!terminalError &&
|
||||
thing &&
|
||||
supportsTerminal &&
|
||||
ctrlChannelId &&
|
||||
mqttCredentials;
|
||||
|
||||
/**
|
||||
* Theme của terminal theo dark/light mode
|
||||
*/
|
||||
const terminalTheme = useMemo(() => {
|
||||
return (
|
||||
TERMINAL_THEMES[selectedThemeKey]?.theme || TERMINAL_THEMES.dark.theme
|
||||
);
|
||||
}, [selectedThemeKey]);
|
||||
|
||||
/**
|
||||
* Xử lý thay đổi theme
|
||||
* - Cập nhật state
|
||||
* - Lưu vào localStorage
|
||||
*/
|
||||
const handleThemeChange: MenuProps['onClick'] = (e) => {
|
||||
const themeKey = e.key;
|
||||
setSelectedThemeKey(themeKey);
|
||||
localStorage.setItem('terminal_theme_key', themeKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Menu items cho theme selector
|
||||
*/
|
||||
const themeMenuItems: MenuProps['items'] = Object.entries(
|
||||
TERMINAL_THEMES,
|
||||
).map(([key, preset]) => ({
|
||||
key,
|
||||
label: preset.name,
|
||||
}));
|
||||
|
||||
// ===== RENDER =====
|
||||
return (
|
||||
<PageContainer
|
||||
header={{
|
||||
title: pageTitle,
|
||||
onBack: () => history.push(ROUTE_MANAGER_DEVICES),
|
||||
tags: (
|
||||
<Space size={8}>
|
||||
{getBadgeConnection(connectionState.online)}
|
||||
<Typography.Text>{connectionLabel}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{/* Loading state */}
|
||||
{isLoading && (
|
||||
<div
|
||||
style={{
|
||||
minHeight: 320,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generic error state */}
|
||||
{!isLoading && terminalError && (
|
||||
<Result
|
||||
status="error"
|
||||
title={intl.formatMessage({
|
||||
id: 'terminal.genericError',
|
||||
defaultMessage: 'Đã có lỗi xảy ra',
|
||||
})}
|
||||
subTitle={terminalError}
|
||||
extra={[
|
||||
<Button key="retry" type="primary" onClick={handleRetry}>
|
||||
{intl.formatMessage({
|
||||
id: 'common.retry',
|
||||
defaultMessage: 'Thử lại',
|
||||
})}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Thiết bị không hỗ trợ terminal (không phải GMSv6) */}
|
||||
{!isLoading &&
|
||||
!terminalError &&
|
||||
thing &&
|
||||
!supportsTerminal &&
|
||||
renderBlockingResult(
|
||||
'info',
|
||||
intl.formatMessage({
|
||||
id: 'terminal.unsupported.title',
|
||||
defaultMessage: 'Thiết bị không hỗ trợ terminal',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'terminal.unsupported.desc',
|
||||
defaultMessage:
|
||||
'Thiết bị GMSv5 không được hỗ trợ. Vui lòng sử dụng thiết bị khác.',
|
||||
}),
|
||||
)}
|
||||
|
||||
{/* Thiếu ctrl_channel_id */}
|
||||
{!isLoading &&
|
||||
!terminalError &&
|
||||
thing &&
|
||||
supportsTerminal &&
|
||||
!ctrlChannelId &&
|
||||
renderBlockingResult(
|
||||
'warning',
|
||||
intl.formatMessage({
|
||||
id: 'terminal.missingChannel.title',
|
||||
defaultMessage: 'Thiếu thông tin kênh điều khiển',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'terminal.missingChannel.desc',
|
||||
defaultMessage:
|
||||
'Thiết bị chưa được cấu hình ctrl_channel_id nên không thể mở terminal.',
|
||||
}),
|
||||
)}
|
||||
|
||||
{/* Thiếu MQTT credentials */}
|
||||
{!isLoading &&
|
||||
!terminalError &&
|
||||
thing &&
|
||||
supportsTerminal &&
|
||||
ctrlChannelId &&
|
||||
!mqttCredentials &&
|
||||
renderBlockingResult(
|
||||
'warning',
|
||||
intl.formatMessage({
|
||||
id: 'terminal.missingCredential.title',
|
||||
defaultMessage: 'Thiếu thông tin xác thực',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'terminal.missingCredential.desc',
|
||||
defaultMessage:
|
||||
'Tài khoản hiện tại chưa được cấp frontend_thing_id/frontend_thing_key.',
|
||||
}),
|
||||
)}
|
||||
|
||||
{/* Terminal UI - chỉ hiển thị khi tất cả điều kiện thỏa mãn */}
|
||||
{showTerminal && (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
{/* Cảnh báo khi thiết bị offline */}
|
||||
{!connectionState.online && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={intl.formatMessage({
|
||||
id: 'terminal.offline',
|
||||
defaultMessage:
|
||||
'Thiết bị đang ngoại tuyến. Terminal chuyển sang chế độ chỉ xem.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{/* Đang kết nối MQTT */}
|
||||
{connectionState.online && !isSessionReady && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={intl.formatMessage({
|
||||
id: 'terminal.connecting',
|
||||
defaultMessage: 'Đang chuẩn bị phiên terminal...',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{/* XTerm component */}
|
||||
<ProCard
|
||||
bordered
|
||||
bodyStyle={{ padding: 0, height: 560 }}
|
||||
style={{
|
||||
minHeight: 560,
|
||||
border: `2px solid ${token.colorBorder}`,
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
>
|
||||
<XTerm
|
||||
ref={terminalRef}
|
||||
cursorBlink
|
||||
disabled={!connectionState.online || !isSessionReady}
|
||||
onData={handleTyping}
|
||||
theme={terminalTheme}
|
||||
/>
|
||||
</ProCard>
|
||||
{/* Nút xóa màn hình */}
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
onClick={() => terminalRef.current?.clear()}
|
||||
disabled={!isSessionReady}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: 'terminal.action.clear',
|
||||
defaultMessage: 'Xóa màn hình',
|
||||
})}
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: themeMenuItems,
|
||||
onClick: handleThemeChange,
|
||||
selectedKeys: [selectedThemeKey],
|
||||
}}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button icon={<BgColorsOutlined />}>
|
||||
{intl.formatMessage({
|
||||
id: 'terminal.action.theme',
|
||||
defaultMessage: 'Theme',
|
||||
})}
|
||||
: {TERMINAL_THEMES[selectedThemeKey]?.name || 'Dark'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Space>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceTerminalPage;
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { FormattedMessage, history, useIntl } from '@umijs/max';
|
||||
import { Button, Divider, Grid, Space, Tag, theme } from 'antd';
|
||||
import { Button, Divider, Grid, Space, Tag, Tooltip, theme } from 'antd';
|
||||
import message from 'antd/es/message';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import { useRef, useState } from 'react';
|
||||
@@ -231,11 +231,21 @@ const ManagerDevicePage = () => {
|
||||
}}
|
||||
/>
|
||||
{device?.metadata?.type === 'gmsv6' && (
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<IconFont type="icon-terminal" />}
|
||||
/>
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id: 'master.devices.openTerminal',
|
||||
defaultMessage: 'Open terminal',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<IconFont type="icon-terminal" />}
|
||||
onClick={() =>
|
||||
history.push(`/manager/devices/${device.id}/terminal`)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
|
||||
59
src/services/master/typings/terminal.d.ts
vendored
Normal file
59
src/services/master/typings/terminal.d.ts
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
declare namespace MasterModel {
|
||||
/**
|
||||
* Trạng thái kết nối terminal
|
||||
*/
|
||||
interface TerminalConnection {
|
||||
online: boolean;
|
||||
lastSeen?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format SenML record nhận từ MQTT
|
||||
*/
|
||||
interface SenmlRecord {
|
||||
vs?: string; // value string - dữ liệu terminal
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset theme cho terminal
|
||||
*/
|
||||
interface TerminalThemePreset {
|
||||
name: string;
|
||||
theme: {
|
||||
background: string;
|
||||
foreground: string;
|
||||
cursor: string;
|
||||
selectionBackground: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Định nghĩa các preset themes cho terminal
|
||||
*/
|
||||
type TerminalThemes = Record<string, TerminalThemePreset>;
|
||||
|
||||
/**
|
||||
* Record SenML gửi tới MQTT (dùng cho terminal commands)
|
||||
* Format: SenML (Sensor Measurement List)
|
||||
*/
|
||||
interface MqttTerminalRecord {
|
||||
bn: string; // base name - session ID
|
||||
n: string; // name - tên field
|
||||
bt: number; // base time - timestamp (seconds)
|
||||
vs: string; // value string - command được encode Base64
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload gửi tới MQTT cho terminal
|
||||
* Là mảng các SenML records
|
||||
*/
|
||||
type MqttTerminalPayload = MqttTerminalRecord[];
|
||||
|
||||
/**
|
||||
* Options cho MQTT publish
|
||||
*/
|
||||
interface MqttPublishOptions {
|
||||
topic: string;
|
||||
payload: string;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user