From 8a06650eab770b4cdc86d572fde6dc463988ac1d Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:58:02 +0700 Subject: [PATCH] Add baud and interface to lines, enhance line controls Added 'baud' and 'interface' fields to the Line model and database schema. Implemented backend and frontend support for setting baud rate per line, including socket event handling and UI controls. Enhanced CardLine and BottomToolBar components to provide scenario and baud controls, and improved UI/UX for line management. Minor fixes and refactoring for consistency and usability. --- BACKEND/app/models/line.ts | 6 + BACKEND/app/services/apc_connection.ts | 2 +- BACKEND/app/services/line_connection.ts | 19 +- ...pdate_baud_and_interface_to_lines_table.ts | 19 + BACKEND/providers/socket_io_provider.ts | 21 + FRONTEND/src/App.tsx | 17 +- FRONTEND/src/components/BottomToolBar.tsx | 125 +++--- FRONTEND/src/components/ButtonAction.tsx | 6 +- FRONTEND/src/components/CardLine.tsx | 379 +++++++++++++++--- FRONTEND/src/components/Component.module.css | 5 + FRONTEND/src/components/DrawerControl.tsx | 30 +- FRONTEND/src/components/FormAddEdit.tsx | 30 +- FRONTEND/src/untils/constanst.ts | 2 + FRONTEND/src/untils/types.ts | 2 + 14 files changed, 540 insertions(+), 123 deletions(-) create mode 100644 BACKEND/database/migrations/1763006980759_update_baud_and_interface_to_lines_table.ts diff --git a/BACKEND/app/models/line.ts b/BACKEND/app/models/line.ts index d298ac2..2c42405 100644 --- a/BACKEND/app/models/line.ts +++ b/BACKEND/app/models/line.ts @@ -26,6 +26,12 @@ export default class Line extends BaseModel { @column() declare outlet: number + @column() + declare interface: string + + @column() + declare baud: number + @belongsTo(() => Station) declare station: BelongsTo diff --git a/BACKEND/app/services/apc_connection.ts b/BACKEND/app/services/apc_connection.ts index 96133cc..c4375eb 100644 --- a/BACKEND/app/services/apc_connection.ts +++ b/BACKEND/app/services/apc_connection.ts @@ -100,7 +100,7 @@ class APCController { this.buffer = '' } } - appendLog(data, 0, 0, this.apc_number || 0) + // appendLog(data, 0, 0, this.apc_number || 0) } private _handleClose(): void { diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 0ab2b3b..b306ac6 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -200,8 +200,10 @@ export default class LineConnection { if (char === '\x7F') this.bufferCommand = this.bufferCommand.slice(0, -1) else if (char === '\r' && cleanData(this.bufferCommand).length > 0) { this.config.commands = [ - cleanData(this.bufferCommand), - ...this.config.commands.filter((el) => el !== cleanData(this.bufferCommand)), + cleanData(this.bufferCommand.replace('\r', '')), + ...this.config.commands.filter( + (el) => el !== cleanData(this.bufferCommand.replace('\r', '')) + ), ].slice(0, 10) this.bufferCommand = '' } else this.bufferCommand += char @@ -516,6 +518,7 @@ export default class LineConnection { // Gửi nhiều ký tự ESC để vào ROMMON breakSpam() { + console.log('SPAM Break to line:', this.config.lineNumber) let count = 0 const escInterval = setInterval(() => { if (count >= 100) { @@ -526,4 +529,16 @@ export default class LineConnection { count++ }, 1) } + + async setBaud(baud: number) { + this.writeCommand('enable\r\n') + await sleep(500) + this.writeCommand('line console 0\r\n') + await sleep(500) + this.writeCommand(`speed ${baud}\r\n`) + await sleep(500) + this.writeCommand('end\r\n') + await sleep(500) + this.writeCommand('write memory\r\n') + } } diff --git a/BACKEND/database/migrations/1763006980759_update_baud_and_interface_to_lines_table.ts b/BACKEND/database/migrations/1763006980759_update_baud_and_interface_to_lines_table.ts new file mode 100644 index 0000000..830bfd9 --- /dev/null +++ b/BACKEND/database/migrations/1763006980759_update_baud_and_interface_to_lines_table.ts @@ -0,0 +1,19 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'lines' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.string('interface').defaultTo('').nullable() + table.integer('baud').nullable() + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('interface') + table.dropColumn('baud') + }) + } +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 0937e7e..be1da6e 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -18,6 +18,7 @@ interface HandleOptions { actionApc?: string scenario?: any timeout?: number + baud?: number } type LineAction = (line: LineConnection, options?: HandleOptions) => Promise | void @@ -166,6 +167,26 @@ export class WebSocketIo { ) }) + socket.on('set_baud', async (data) => { + const lineId = data.lineId + const baud = data.baud + const line = await Line.find(lineId) + if (line) { + Object.assign(line, { baud }) + line?.save() + } + await this.handleLineOperation( + io, + data.stationId, + [lineId], + async (value) => value.setBaud(baud), + { + baud, + timeout: 120000, + } + ) + }) + socket.on('open_cli', async (data) => { const { lineId, userEmail, userName: name, stationId } = data const line = this.lineMap.get(lineId) diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index 38c8d20..5271e23 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -112,10 +112,15 @@ function App() { const response = await axios.get(apiUrl + "api/stations"); if (response.status) { if (Array.isArray(response.data)) { - setStations(response.data); - response.data.forEach((station) => { - connectApcSwitch(station); - }); + setStations( + response.data.map((station) => { + connectApcSwitch(station); + const lines = (station?.lines || []).sort( + (a: TLine, b: TLine) => a?.lineNumber - b?.lineNumber + ); + return { ...station, lines }; + }) + ); } } } catch (error) { @@ -441,6 +446,7 @@ function App() { loadingTerminal && Number(station.id) === Number(activeTab) } + scenarios={scenarios} /> ))} @@ -462,6 +468,7 @@ function App() { loadingTerminal && Number(station.id) === Number(activeTab) } + scenarios={scenarios} /> ))} @@ -483,6 +490,7 @@ function App() { loadingTerminal && Number(station.id) === Number(activeTab) } + scenarios={scenarios} /> ))} @@ -505,6 +513,7 @@ function App() { isLogModalOpen={isLogModalOpen} setIsLogModalOpen={setIsLogModalOpen} setTestLogContent={setTestLogContent} + scenarios={scenarios} /> diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index e467e27..db67f21 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -4,29 +4,32 @@ import { CloseButton, Flex, Input, + Menu, ScrollArea, Tabs, Text, } from "@mantine/core"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import classes from "./Component.module.css"; -import type { TLine, TStation } from "../untils/types"; +import type { IScenario, TLine, TStation } from "../untils/types"; import type { Socket } from "socket.io-client"; -import { ButtonDPELP, ButtonSelect } from "./ButtonAction"; +import { ButtonDPELP, ButtonScenario, ButtonSelect } from "./ButtonAction"; import DrawerLogs from "./DrawerLogs"; import { DrawerAPCControl, DrawerSwitchControl } from "./DrawerControl"; +import { isJsonString } from "../untils/helper"; interface TabsProps { selectedLines: TLine[]; socket: Socket | null; - setSelectedLines: (lines: React.SetStateAction) => void; + setSelectedLines: (value: React.SetStateAction) => void; isDisable: boolean; station: TStation; - setIsDisable: (lines: React.SetStateAction) => void; + setIsDisable: (value: React.SetStateAction) => void; testLogContent: string; isLogModalOpen: boolean; - setIsLogModalOpen: (lines: React.SetStateAction) => void; - setTestLogContent: (lines: React.SetStateAction) => void; + setIsLogModalOpen: (value: React.SetStateAction) => void; + setTestLogContent: (value: React.SetStateAction) => void; + scenarios: IScenario[]; } const BottomToolBar = ({ @@ -40,7 +43,14 @@ const BottomToolBar = ({ isLogModalOpen, setIsLogModalOpen, setTestLogContent, + scenarios, }: TabsProps) => { + const user = useMemo(() => { + return localStorage.getItem("user") && + isJsonString(localStorage.getItem("user")) + ? JSON.parse(localStorage.getItem("user") || "") + : null; + }, []); const [valueInput, setValueInput] = useState(""); const [activeTabBottom, setActiveBottom] = useState("command"); @@ -129,7 +139,7 @@ const BottomToolBar = ({ socket?.emit("write_command_line_from_web", { lineIds: listLine.map((line) => line.id), stationId: station.id, - command: " \n", + command: "spam_break", }); setIsDisable(true); setTimeout(() => { @@ -207,53 +217,58 @@ const BottomToolBar = ({ }, 5000); }} /> - - {/* - {scenarios.map((el, i) => ( - - !el?.userEmailOpenCLI || - el?.userEmailOpenCLI === user?.email - )} - isDisable={ - isDisable || - selectedLines.filter( - (el) => - !el?.userEmailOpenCLI || - el?.userEmailOpenCLI === user?.email - ).length === 0 - } - onClick={() => { - setSelectedLines([]); - setIsDisable(true); - setTimeout(() => { - setIsDisable(false); - }, 5000); - }} - scenario={el} - /> - ))} - */} + + + + + + + {scenarios.map((el, i) => ( + + !el?.userEmailOpenCLI || + el?.userEmailOpenCLI === user?.email + )} + isDisable={ + isDisable || + selectedLines.filter( + (el) => + !el?.userEmailOpenCLI || + el?.userEmailOpenCLI === user?.email + ).length === 0 + } + onClick={() => { + setSelectedLines([]); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 5000); + }} + scenario={el} + /> + ))} + + + void; + onClick: (e: React.MouseEvent) => void; selectedLines: TLine[]; scenario: IScenario; }) => { @@ -146,8 +146,8 @@ export const ButtonScenario = ({ variant="outline" color="#00a164" className={classes.buttonScenario} - onClick={async () => { - onClick(); + onClick={async (e) => { + onClick(e); selectedLines?.forEach((el) => { socket?.emit( "run_scenario", diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index fae89d9..6b5fa3a 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -1,11 +1,13 @@ -import { Card, Text, Box, Flex } from "@mantine/core"; -import type { TLine, TStation } from "../untils/types"; +import { Card, Text, Box, Flex, Menu, Button, Input } from "@mantine/core"; +import type { IScenario, TLine, TStation } from "../untils/types"; import classes from "./Component.module.css"; import TerminalCLI from "./TerminalXTerm"; import type { Socket } from "socket.io-client"; -import { IconCircleCheckFilled } from "@tabler/icons-react"; -import { memo, useMemo } from "react"; +import { memo, useMemo, useState } from "react"; import { convertTimestampToDate } from "../untils/helper"; +import { ButtonDPELP, ButtonScenario } from "./ButtonAction"; +import { notifications } from "@mantine/notifications"; +import { listBaudDefault } from "../untils/constanst"; const CardLine = ({ line, @@ -15,6 +17,7 @@ const CardLine = ({ stationItem, openTerminal, loadTerminal, + scenarios, }: { line: TLine; selectedLines: TLine[]; @@ -23,6 +26,7 @@ const CardLine = ({ stationItem: TStation; openTerminal: (value: TLine) => void; loadTerminal: boolean; + scenarios: IScenario[]; }) => { const user = useMemo(() => { return localStorage.getItem("user") && @@ -30,7 +34,51 @@ const CardLine = ({ ? JSON.parse(localStorage.getItem("user") || "") : null; }, []); + const [showMenu, setShowMenu] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const [valueBaud, setValueBaud] = useState(""); + const controlApc = (action: string) => { + if (!line.outlet) { + notifications.show({ + title: "Error", + message: "Hasn't config outlet number", + color: "red", + }); + return; + } + const apcName = line.apcName || line.apc_name; + if (!apcName) { + notifications.show({ + title: "Error", + message: "Hasn't config apc", + color: "red", + }); + return; + } + if ( + (apcName === "apc_1" && !stationItem.apc_1_ip) || + (apcName === "apc_2" && !stationItem.apc_2_ip) + ) { + notifications.show({ + title: "Error", + message: "Hasn't config apc ip", + color: "red", + }); + return; + } + socket?.emit("control_apc", { + outletNumbers: [line.outlet], + station: stationItem, + action: action, + apcName: line.apcName || line.apc_name, + }); + setIsDisabled(true); + setTimeout(() => { + setIsDisabled(false); + }, 5000); + }; + console.log("RERENDER", line.lineNumber); return ( val.id !== line.id)); else setSelectedLines((pre) => [...pre, line]); }} + onMouseLeave={() => setTimeout(() => setShowMenu(false), 150)} > - - - Line: {line.lineNumber || line.line_number} - {line.port}{" "} - {line.status === "connected" && ( - - )} - -
- {line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""} -
-
- -
- PID:{" "} - - {line?.inventory?.pid || ""} - -
-
- SN:{" "} - - {line?.inventory?.sn || ""} - -
-
- VID:{" "} - - {line?.inventory?.vid || ""} - -
-
+ + + setShowMenu(true)} + > + + + + + {line.lineNumber || line.line_number} + + + + + {line.port} + + + + + +
+ PID:{" "} + + {line?.inventory?.pid || ""} + + {line?.inventory?.vid ? ( + + {" | " + line?.inventory?.vid} + + ) : ( + "" + )} +
+
+ SN:{" "} + + {line?.inventory?.sn || ""} + +
+
+
+ {line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""} +
+
+
+
+
+ + { + e.preventDefault(); + e.stopPropagation(); + }} + // onMouseEnter={() => setShowMenu(true)} + // onMouseLeave={() => setTimeout(() => setShowMenu(false), 300)} + > + + { + setIsDisabled(true); + setTimeout(() => { + setIsDisabled(false); + }, 5000); + }} + /> + + + + + + + {scenarios.map((el, i) => ( + { + setShowMenu(true); + setIsDisabled(true); + setTimeout(() => { + setIsDisabled(false); + }, 5000); + }} + scenario={el} + /> + ))} + + + + + + + + + + {listBaudDefault.map((el, i) => ( + + ))} + setValueBaud(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + socket?.emit("set_baud", { + lineId: line.id, + baud: Number(valueBaud), + }); + setValueBaud(""); + setIsDisabled(true); + setTimeout(() => { + setIsDisabled(false); + }, 5000); + } + }} + /> + + + + +
+ + + Power + + + + + +
+
+
+
+ { e.preventDefault(); e.stopPropagation(); }} - style={{ height: "175px", width: "332px" }} + style={{ height: "160px", width: "332px" }} > = ({ APC 1 {RenderAPCStatus(dataStation?.apc1)} - {dataStation?.apc1?.status === "DISCONNECTED" || - dataStation?.apc1?.status === "TIMEOUT" ? ( + {dataStation?.apc1?.status !== "CONNECTED" ? ( + ) : ( + "" + )}