feat(device/detail): update BinarySensors component, add Chart component for sensor data visualization and add update-iconfont.sh

This commit is contained in:
Tran Anh Tuan
2026-02-05 10:09:59 +07:00
parent 256ce06ea2
commit 8af31a0435
12 changed files with 871 additions and 24 deletions

View File

@@ -52,7 +52,8 @@ export const handleRequestConfig: RequestConfig = {
return (
(status >= 200 && status < 300) ||
status === HTTPSTATUS.HTTP_NOTFOUND ||
status === HTTPSTATUS.HTTP_UNAUTHORIZED
status === HTTPSTATUS.HTTP_UNAUTHORIZED ||
status === HTTPSTATUS.HTTP_FORBIDDEN
);
},
headers: { 'X-Requested-With': 'XMLHttpRequest' },
@@ -121,15 +122,16 @@ export const handleRequestConfig: RequestConfig = {
// Unwrap data from backend response
responseInterceptors: [
async (response: any, options: any) => {
const isRefreshRequest = response.url?.includes(API_PATH_REFRESH_TOKEN);
const alreadyRetried = options?.skipAuthRefresh === true;
const isRefreshRequest = response.config.url?.includes(
API_PATH_REFRESH_TOKEN,
);
console.log('Is refresh request:', isRefreshRequest);
// Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại
if (
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
// Không tự refresh cho chính API refresh token để tránh vòng lặp vô hạn
!isRefreshRequest &&
!alreadyRetried
!isRefreshRequest
) {
const newToken = await getValidAccessToken();
console.log('Access Token hết hạn, đang refresh...');
@@ -172,7 +174,7 @@ export const handleRequestConfig: RequestConfig = {
if (
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
(isRefreshRequest || alreadyRetried)
isRefreshRequest
) {
clearAllData();
history.push(ROUTE_LOGIN);

30
package-lock.json generated
View File

@@ -10,10 +10,12 @@
"@ant-design/pro-components": "^2.8.10",
"@umijs/max": "^4.6.23",
"antd": "^5.4.0",
"chart.js": "^4.5.1",
"classnames": "^2.5.1",
"dayjs": "^1.11.19",
"moment": "^2.30.1",
"ol": "^10.6.1",
"react-chartjs-2": "^5.3.1",
"reconnecting-websocket": "^4.4.0"
},
"devDependencies": {
@@ -2842,6 +2844,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@loadable/component": {
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.2.tgz",
@@ -8422,6 +8430,18 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -17295,6 +17315,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
"integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",

View File

@@ -21,10 +21,12 @@
"@ant-design/pro-components": "^2.8.10",
"@umijs/max": "^4.6.23",
"antd": "^5.4.0",
"chart.js": "^4.5.1",
"classnames": "^2.5.1",
"dayjs": "^1.11.19",
"moment": "^2.30.1",
"ol": "^10.6.1",
"react-chartjs-2": "^5.3.1",
"reconnecting-websocket": "^4.4.0"
},
"devDependencies": {

30
pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
antd:
specifier: ^5.4.0
version: 5.29.3(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
chart.js:
specifier: ^4.5.1
version: 4.5.1
classnames:
specifier: ^2.5.1
version: 2.5.1
@@ -32,6 +35,9 @@ importers:
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)
reconnecting-websocket:
specifier: ^4.4.0
version: 4.4.0
@@ -958,6 +964,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==, tarball: https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz}
'@loadable/component@5.15.2':
resolution: {integrity: sha512-ryFAZOX5P2vFkUdzaAtTG88IGnr9qxSdvLRvJySXcUA4B4xVWurUNADu3AnKPksxOZajljqTrDEDcYjeL4lvLw==, tarball: https://registry.npmjs.org/@loadable/component/-/component-5.15.2.tgz}
engines: {node: '>=8'}
@@ -2215,6 +2224,10 @@ packages:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==, tarball: https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==, tarball: https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz}
engines: {pnpm: '>=8'}
chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz}
engines: {node: '>= 8.10.0'}
@@ -5382,6 +5395,12 @@ packages:
react: '>=16.9.0'
react-dom: '>=16.9.0'
react-chartjs-2@5.3.1:
resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==, tarball: https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz}
peerDependencies:
chart.js: ^4.1.1
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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:
@@ -7759,6 +7778,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@kurkle/color@0.3.4': {}
'@loadable/component@5.15.2(react@18.3.1)':
dependencies:
'@babel/runtime': 7.23.6
@@ -9573,6 +9594,10 @@ snapshots:
chalk@5.3.0: {}
chart.js@4.5.1:
dependencies:
'@kurkle/color': 0.3.4
chokidar@3.5.3:
dependencies:
anymatch: 3.1.3
@@ -13208,6 +13233,11 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-chartjs-2@5.3.1(chart.js@4.5.1)(react@18.3.1):
dependencies:
chart.js: 4.5.1
react: 18.3.1
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0

View File

@@ -1,14 +1,6 @@
import IconFont from '@/components/IconFont';
import { StatisticCard } from '@ant-design/pro-components';
import {
Divider,
Flex,
GlobalToken,
Grid,
theme,
Tooltip,
Typography,
} from 'antd';
import { Flex, GlobalToken, Grid, theme, Tooltip, Typography } from 'antd';
const { Text } = Typography;
type BinarySensorsProps = {
nodeConfigs: MasterModel.NodeConfig[];
@@ -148,7 +140,6 @@ const BinarySensors = ({ nodeConfigs }: BinarySensorsProps) => {
return (
<Flex wrap="wrap" gap="middle">
<Divider orientation="left">Cảm biến</Divider>
{binarySensors.map((entity) => (
<div
key={entity.entityId}

View File

@@ -0,0 +1,610 @@
import '@/utils/chart';
import type { ChartData, ChartOptions } from 'chart.js';
import { Line } from 'react-chartjs-2';
type ChartComponentProps = {
message: MasterModel.SensorLogMessage[];
};
const exampleSensorLogMessages: MasterModel.SensorLogMessage[] = [
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770260026,
value: 76.3,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770259725,
value: 77.1,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770259424,
value: 75.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770259242,
value: 72.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770259142,
value: 69.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770258926,
value: 66.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770258877,
value: 69.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770258839,
value: 73,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770258678,
value: 76.1,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770258377,
value: 77.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770258077,
value: 77.8,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770257776,
value: 75.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770257596,
value: 72.5,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770257487,
value: 69.5,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770257412,
value: 66.4,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770257349,
value: 63.3,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770257117,
value: 60.3,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770256816,
value: 62.3,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770256625,
value: 65.3,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770256563,
value: 68.4,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770256529,
value: 71.4,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770256363,
value: 74.7,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770256062,
value: 75.2,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255761,
value: 74.3,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255590,
value: 71.2,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255498,
value: 68.1,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255436,
value: 64.9,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255331,
value: 61.8,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255111,
value: 64.8,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255038,
value: 67.9,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770255002,
value: 71.1,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770254968,
value: 74.2,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770254688,
value: 77.4,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770254387,
value: 78.4,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770254086,
value: 77.9,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253786,
value: 75,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253631,
value: 72,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253549,
value: 68.9,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253481,
value: 65.8,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253434,
value: 62.7,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253364,
value: 59.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770253063,
value: 60.4,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770252763,
value: 62.5,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770252695,
value: 65.5,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770252657,
value: 68.6,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770252489,
value: 71.7,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770252188,
value: 71.2,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770251886,
value: 68.7,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770251585,
value: 65.9,
},
{
channel: 'c2552f8e-6e2f-4703-bdc2-681d2a0646d9',
subtopic: 'log.gmsv5.hum.0:20',
publisher: 'f4bd927c-f578-420d-9b63-ef7fb053d901',
protocol: 'mqtt',
name: 'urn:dev:mac:0038310555e4:0:20',
unit: '%',
time: 1770251469,
value: 68.9,
},
];
// Custom plugin để vẽ đường kẻ dọc khi hover
const verticalLinePlugin = {
id: 'verticalLine',
afterDraw: (chart: any) => {
if (chart.tooltip?._active?.length) {
const ctx = chart.ctx;
const x = chart.tooltip._active[0].element.x;
const topY = chart.scales.y.top;
const bottomY = chart.scales.y.bottom;
ctx.save();
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.stroke();
ctx.restore();
}
},
};
const ChartComponent = () => {
const data: ChartData<'line', number[], string> = {
labels: exampleSensorLogMessages.map((msg) =>
new Date(msg.time! * 1000).toLocaleTimeString(),
),
datasets: [
{
label: exampleSensorLogMessages[0]?.unit || '',
data: exampleSensorLogMessages.map((msg) => msg.value),
borderColor: 'rgba(75, 192, 192, 0.2)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1,
fill: 'start',
pointRadius: 0,
},
],
};
const options: ChartOptions<'line'> = {
interaction: {
mode: 'index',
intersect: true,
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false,
},
},
y: {
display: false,
min: 0,
max: 100,
},
},
plugins: {
tooltip: {
enabled: true,
mode: 'index',
intersect: false,
callbacks: {
label: (context) => {
return `${context.parsed.y} ${
exampleSensorLogMessages[0]?.unit || ''
}`;
},
title: (context) => {
const index = context[0]?.dataIndex;
if (index !== undefined) {
const msg = exampleSensorLogMessages[index];
return new Date(msg.time! * 1000).toLocaleString();
}
return '';
},
},
},
legend: {
display: false,
},
},
};
return (
<Line
title="Nooo"
data={data}
options={options}
plugins={[verticalLinePlugin]}
/>
);
};
export default ChartComponent;

View File

@@ -3,11 +3,17 @@ import TooltipIconFontButton from '@/components/shared/TooltipIconFontButton';
import { ROUTER_HOME } from '@/constants/routes';
import { apiQueryNodeConfigMessage } from '@/services/master/MessageController';
import { apiGetThingDetail } from '@/services/master/ThingController';
import {
EditOutlined,
EllipsisOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { PageContainer, ProCard } from '@ant-design/pro-components';
import { history, useIntl, useModel, useParams } from '@umijs/max';
import { Divider, Flex, Grid } from 'antd';
import { useEffect, useState } from 'react';
import BinarySensors from './components/BinarySensors';
import ChartComponent from './components/Chart';
import ThingTitle from './components/ThingTitle';
const DetailDevicePage = () => {
@@ -109,8 +115,21 @@ const DetailDevicePage = () => {
</ProCard>
<ProCard colSpan={{ xs: 24, sm: 24, md: 24, lg: 18, xl: 18 }}>
<Flex wrap gap="small">
<Divider orientation="left">Cảm biến</Divider>
<BinarySensors nodeConfigs={nodeConfigs} />
<Divider orientation="left">Trạng thái</Divider>
<ProCard
title="Mẫu 1"
bordered
style={{ maxWidth: 600 }}
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
]}
>
<ChartComponent />
</ProCard>
</Flex>
</ProCard>
</ProCard>

View File

@@ -165,7 +165,6 @@ const ManagerDevicePage = () => {
'-'
),
},
{
key: 'type',
hideInSearch: true,

View File

@@ -211,3 +211,19 @@ export async function apiQueryConfigAlarm(
return resp;
}
export async function apiQuerySensorLogMessage(
dataChanelId: string,
authorization: string,
params: MasterModel.SearchMessagePaginationBody,
) {
return request<
MasterModel.MesageReaderResponse<MasterModel.SensorLogMessage[]>
>(`${API_READER}/${dataChanelId}/messages`, {
method: 'GET',
headers: {
Authorization: authorization,
},
params: params,
});
}

View File

@@ -6,6 +6,15 @@ declare namespace MasterModel {
subtopic?: string;
}
interface MessageBasicInfo {
channel?: string;
subtopic?: string;
publisher?: string;
protocol?: string;
name?: string;
time?: number;
}
type LogTypeRequest = 'user_logs' | undefined;
interface MesageReaderResponse<T = MessageDataType> {
@@ -27,13 +36,7 @@ declare namespace MasterModel {
type MessageDataType = NodeConfig[] | CameraV5 | CameraV6;
interface Message<T = MessageDataType> {
channel?: string;
subtopic?: string;
publisher?: string;
protocol?: string;
name?: string;
time?: number;
interface Message<T = MessageDataType> extends MessageBasicInfo {
string_value?: string;
string_value_parsed?: T;
}
@@ -69,4 +72,9 @@ declare namespace MasterModel {
type: Type;
name: string;
}
interface SensorLogMessage extends MessageBasicInfo {
unit: string;
value: number;
}
}

22
src/utils/chart.ts Normal file
View File

@@ -0,0 +1,22 @@
import {
BarElement,
CategoryScale,
Chart as ChartJS,
Filler,
Legend,
LinearScale,
LineElement,
PointElement,
Tooltip,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Filler,
Tooltip,
Legend,
);

118
update-iconfont.sh Normal file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
set -e
echo "=== Update Iconfont - Git Push Script with Merge Main ==="
# Hàm kiểm tra lỗi và thoát
handle_error() {
echo "❌ Lỗi: $1"
# Nếu có stash, hiển thị thông báo để người dùng biết cách xử lý
if [ "$stashed" = true ]; then
echo "⚠️ Có stash được lưu, hãy kiểm tra bằng 'git stash list' và xử lý thủ công nếu cần."
fi
exit 1
}
# Kiểm tra xem có trong Git repository không
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
handle_error "Thư mục hiện tại không phải là Git repository!"
fi
# Các file cần commit
FILE1="src/components/IconFont/index.tsx"
FILE2="src/app.tsx"
# Kiểm tra file tồn tại
if [ ! -f "$FILE1" ]; then
handle_error "Không tìm thấy file $FILE1"
fi
if [ ! -f "$FILE2" ]; then
handle_error "Không tìm thấy file $FILE2"
fi
# Lấy nhánh hiện tại
current_branch=$(git branch --show-current)
if [ -z "$current_branch" ]; then
handle_error "Không thể xác định nhánh hiện tại!"
fi
echo "👉 Bạn đang ở nhánh: $current_branch"
# Hỏi nhánh chính, mặc định là 'master' nếu người dùng không nhập
read -p "Nhập tên nhánh chính (mặc định: master): " target_branch
target_branch=${target_branch:-master}
# Kiểm tra xem nhánh chính có tồn tại trên remote không
if ! git ls-remote --heads origin "$target_branch" >/dev/null 2>&1; then
handle_error "Nhánh $target_branch không tồn tại trên remote!"
fi
# Hiển thị thay đổi của 2 file
echo ""
echo "📋 Các thay đổi trong 2 file iconfont:"
echo "---"
git --no-pager diff "$FILE1" "$FILE2" 2>/dev/null || echo "(Không có thay đổi hoặc file chưa được track)"
echo "---"
# Kiểm tra có thay đổi không
if git diff --quiet "$FILE1" "$FILE2" 2>/dev/null && git diff --cached --quiet "$FILE1" "$FILE2" 2>/dev/null; then
echo "⚠️ Không có thay đổi nào trong 2 file iconfont."
exit 0
fi
# --- Bước 1: Stash nếu có thay đổi chưa commit ---
stashed=false
if [[ -n $(git status --porcelain) ]]; then
echo "💾 Có thay đổi chưa commit -> stash lại..."
git stash push -m "auto-stash-before-iconfont-$(date +%s)" || handle_error "Lỗi khi stash code!"
stashed=true
fi
# --- Bước 2: Đồng bộ code từ nhánh chính về nhánh hiện tại ---
echo "🔄 Đồng bộ code từ $target_branch về $current_branch..."
git fetch origin "$target_branch" || handle_error "Lỗi khi fetch $target_branch!"
git merge origin/"$target_branch" --no-edit || {
handle_error "Merge từ $target_branch về $current_branch bị conflict, hãy xử lý thủ công rồi chạy lại."
}
# --- Bước 3: Nếu có stash thì pop lại ---
if [ "$stashed" = true ]; then
echo "📥 Pop lại code đã stash..."
git stash pop || handle_error "Stash pop bị conflict, hãy xử lý thủ công bằng 'git stash list' và 'git stash apply'!"
fi
# --- Bước 4: Add và Commit 2 file iconfont ---
echo "📥 Đang add 2 file iconfont..."
git add "$FILE1" "$FILE2" || handle_error "Lỗi khi add files!"
read -p "Nhập commit message (mặc định: 'chore: update iconfont url'): " commit_message
commit_message=${commit_message:-"chore: update iconfont url"}
git commit -m "$commit_message" || handle_error "Lỗi khi commit!"
# --- Bước 5: Push nhánh hiện tại ---
echo "🚀 Đang push nhánh $current_branch lên remote..."
git push origin "$current_branch" || handle_error "Push nhánh $current_branch thất bại!"
# --- Bước 6: Checkout sang nhánh chính ---
echo "🔄 Chuyển sang nhánh $target_branch..."
git checkout "$target_branch" || handle_error "Checkout sang $target_branch thất bại!"
# --- Bước 7: Pull nhánh chính ---
echo "🔄 Pull code mới nhất từ remote $target_branch..."
git pull origin "$target_branch" --no-rebase || handle_error "Pull $target_branch thất bại!"
# --- Bước 8: Merge nhánh hiện tại vào nhánh chính ---
echo "🔀 Merge $current_branch vào $target_branch..."
git merge "$current_branch" --no-edit || {
handle_error "Merge từ $current_branch vào $target_branch bị conflict, hãy xử lý thủ công rồi chạy lại."
}
# --- Bước 9: Push nhánh chính ---
git push origin "$target_branch" || handle_error "Push $target_branch thất bại!"
# --- Quay lại nhánh hiện tại ---
echo "🔄 Quay lại nhánh $current_branch..."
git checkout "$current_branch" || handle_error "Checkout về $current_branch thất bại!"
echo ""
echo "✅ Hoàn tất! Đã commit 2 file iconfont và merge vào $target_branch."