From 8af31a04351f7366d78daf80e02dfbfe2bc4cff1 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Thu, 5 Feb 2026 10:09:59 +0700 Subject: [PATCH] feat(device/detail): update BinarySensors component, add Chart component for sensor data visualization and add update-iconfont.sh --- config/request_dev.ts | 14 +- package-lock.json | 30 + package.json | 2 + pnpm-lock.yaml | 30 + .../Detail/components/BinarySensors.tsx | 11 +- .../Device/Detail/components/Chart.tsx | 610 ++++++++++++++++++ src/pages/Manager/Device/Detail/index.tsx | 19 + src/pages/Manager/Device/index.tsx | 1 - src/services/master/MessageController.ts | 16 + src/services/master/typings/log.d.ts | 22 +- src/utils/chart.ts | 22 + update-iconfont.sh | 118 ++++ 12 files changed, 871 insertions(+), 24 deletions(-) create mode 100644 src/pages/Manager/Device/Detail/components/Chart.tsx create mode 100644 src/utils/chart.ts create mode 100644 update-iconfont.sh diff --git a/config/request_dev.ts b/config/request_dev.ts index 8d64a26..d48e220 100644 --- a/config/request_dev.ts +++ b/config/request_dev.ts @@ -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); diff --git a/package-lock.json b/package-lock.json index 6533cf0..7aea81f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f625be4..6b54aab 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6155c17..0ac5e54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/pages/Manager/Device/Detail/components/BinarySensors.tsx b/src/pages/Manager/Device/Detail/components/BinarySensors.tsx index 72ea96f..1f7e967 100644 --- a/src/pages/Manager/Device/Detail/components/BinarySensors.tsx +++ b/src/pages/Manager/Device/Detail/components/BinarySensors.tsx @@ -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 ( - Cảm biến {binarySensors.map((entity) => (
{ + 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 ( + + ); +}; + +export default ChartComponent; diff --git a/src/pages/Manager/Device/Detail/index.tsx b/src/pages/Manager/Device/Detail/index.tsx index 1318fc2..e797840 100644 --- a/src/pages/Manager/Device/Detail/index.tsx +++ b/src/pages/Manager/Device/Detail/index.tsx @@ -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 = () => { + Cảm biến Trạng thái + , + , + , + ]} + > + + diff --git a/src/pages/Manager/Device/index.tsx b/src/pages/Manager/Device/index.tsx index a9508c8..d0c33b9 100644 --- a/src/pages/Manager/Device/index.tsx +++ b/src/pages/Manager/Device/index.tsx @@ -165,7 +165,6 @@ const ManagerDevicePage = () => { '-' ), }, - { key: 'type', hideInSearch: true, diff --git a/src/services/master/MessageController.ts b/src/services/master/MessageController.ts index 341e94b..215bd26 100644 --- a/src/services/master/MessageController.ts +++ b/src/services/master/MessageController.ts @@ -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 + >(`${API_READER}/${dataChanelId}/messages`, { + method: 'GET', + headers: { + Authorization: authorization, + }, + params: params, + }); +} diff --git a/src/services/master/typings/log.d.ts b/src/services/master/typings/log.d.ts index 6911714..77809af 100644 --- a/src/services/master/typings/log.d.ts +++ b/src/services/master/typings/log.d.ts @@ -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 { @@ -27,13 +36,7 @@ declare namespace MasterModel { type MessageDataType = NodeConfig[] | CameraV5 | CameraV6; - interface Message { - channel?: string; - subtopic?: string; - publisher?: string; - protocol?: string; - name?: string; - time?: number; + interface Message 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; + } } diff --git a/src/utils/chart.ts b/src/utils/chart.ts new file mode 100644 index 0000000..8be1ef8 --- /dev/null +++ b/src/utils/chart.ts @@ -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, +); diff --git a/update-iconfont.sh b/update-iconfont.sh new file mode 100644 index 0000000..17b53f2 --- /dev/null +++ b/update-iconfont.sh @@ -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."