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" ? ( + ) : ( + "" + )}