From f695062ec40ebf50c96552efced2813de0d932d6 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:39:38 +0700 Subject: [PATCH] Enhance switch and APC controls, UI improvements Refactored switch port restart logic to use turnPortOff/on methods. Improved socket communication for switch port status and clear line actions. Updated DrawerControl and ModalTerminal to streamline APC and switch controls, added filtering and persistent view options for switch ports, and improved UI consistency. Fixed ticket creation logic and enhanced terminal and toolbar layouts for better usability. --- BACKEND/app/services/apc_connection.ts | 13 +- BACKEND/app/services/line_connection.ts | 2 +- BACKEND/app/services/switch_connection.ts | 35 +- BACKEND/providers/socket_io_provider.ts | 16 + FRONTEND/src/App.tsx | 12 +- FRONTEND/src/components/BottomToolBar.tsx | 13 +- FRONTEND/src/components/DrawerControl.tsx | 789 +++++++++++----------- FRONTEND/src/components/ModalTerminal.tsx | 320 ++++++--- FRONTEND/src/components/TerminalXTerm.tsx | 1 + 9 files changed, 659 insertions(+), 542 deletions(-) diff --git a/BACKEND/app/services/apc_connection.ts b/BACKEND/app/services/apc_connection.ts index c4375eb..c48d8a3 100644 --- a/BACKEND/app/services/apc_connection.ts +++ b/BACKEND/app/services/apc_connection.ts @@ -123,14 +123,17 @@ class APCController { } } - private _handleError(err: NodeJS.ErrnoException): void { + private async _handleError(err: NodeJS.ErrnoException): Promise { this.output += `\r\n\r\n[ERROR] ${err.message}` this.onData(this.output, this.status) if (err.code === 'ECONNRESET') { - setTimeout(() => { - console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip) - this.reconnect() - }, 15000) + console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip) + if (this.retryConnect <= 5) { + await this.sleep(15000) + console.log('Retry connect times', this.retryConnect) + this.retryConnect += 1 + await this.reconnect() + } } } diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 5041d30..40f45a3 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -147,7 +147,7 @@ export default class LineConnection { this.socketIO.emit('line_error', { stationId, lineId: id, - error: '\n' + err.message + '\n', + error: '\r\n' + err.message + '\r\n', }) resolve() }) diff --git a/BACKEND/app/services/switch_connection.ts b/BACKEND/app/services/switch_connection.ts index 02dc7f0..da70e27 100644 --- a/BACKEND/app/services/switch_connection.ts +++ b/BACKEND/app/services/switch_connection.ts @@ -214,21 +214,26 @@ export default class SwitchController { } public async restartPort(port: string) { - await this.enterEnableMode() - this._send(`configure terminal`) - // await this._waitFor('(config)#') - await this.sleep(500) - this._send(`interface ${port}`) - // await this._waitFor('(config-if)#') - await this.sleep(500) - this._send(`shutdown`) - // await this._waitFor('(config-if)#') - await this.sleep(500) - await this.sleep(2000) - this._send(`no shutdown`) - // await this._waitFor('(config-if)#') - await this.sleep(500) - this._send(`end`) + // await this.enterEnableMode() + // this._send(`configure terminal`) + // // await this._waitFor('(config)#') + // await this.sleep(500) + // this._send(`interface ${port}`) + // // await this._waitFor('(config-if)#') + // await this.sleep(500) + // this._send(`shutdown`) + // // await this._waitFor('(config-if)#') + // await this.sleep(500) + // await this.sleep(2000) + // this._send(`no shutdown`) + // // await this._waitFor('(config-if)#') + // await this.sleep(500) + // this._send(`end`) + await this.turnPortOff(port) + await this.sleep(300) + await this.getPorts() + await this.sleep(300) + await this.turnPortOn(port) } public async disablePoE(port: string) { diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index c708d8a..a6d2328 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -380,6 +380,13 @@ export class WebSocketIo { portGroups: element.portGroups, status: element.status, }) + socket.emit('switch_ports_status', { + stationId: station.id, + ports: + Array.isArray(element.portGroups) && element.portGroups?.length > 0 + ? element.portGroups?.flat() + : [], + }) } else if (element && element.status !== 'CONNECTED') { await element.reconnect() } else await this.connectSwitch(io, station) @@ -460,6 +467,11 @@ export class WebSocketIo { line.config = { ...line.config, ...update } } }) + + socket.on('clear_line', async (data) => { + const { stationId, lineClear } = data + await this.clearLineBeforeConnect(stationId, lineClear) + }) }) socketServer.listen(SOCKET_IO_PORT, () => { @@ -659,6 +671,10 @@ export class WebSocketIo { portGroups: ports, status, }) + socket.emit('switch_ports_status', { + stationId: station.id, + ports: Array.isArray(ports) && ports?.length > 0 ? ports?.flat() : [], + }) }, }) // Connect và login diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index 9850c78..c850703 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -411,7 +411,7 @@ function App() { borderRadius: 8, }} > - + {station.lines.length > 8 ? ( { if (selectedLines.length > 0) { selectedLines.forEach((el) => { - if ( - el?.userOpenCLI === user?.userName && - !selectedLines.find((value) => value.id === el?.id) - ) + console.log(el?.userOpenCLI, user?.userName); + if (el?.userOpenCLI === user?.userName) socket?.emit("close_cli", { lineId: el?.id, - stationId: el?.station_id, + stationId: Number(activeTab), }); }); } @@ -532,7 +530,7 @@ function App() { setLoadingTerminal(false); setTimeout(() => { setLoadingTerminal(true); - }, 100); + }, 500); }} setActive={setActiveTab} active={activeTab} diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index d44b370..6ad49ed 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -69,7 +69,7 @@ const BottomToolBar = ({ - + {selectedLines.map((el) => ( - + + + + - + {listOutlet .filter((el) => el.apc === 1) .map((outlet, i) => ( @@ -315,182 +451,6 @@ export const DrawerAPCControl: React.FC = ({ ))} -
- - - - -
@@ -512,7 +472,7 @@ export const DrawerAPCControl: React.FC = ({ onClick={() => { socket?.emit("control_apc", { outletNumbers: [], - station: stationAPI, + station: { ...stationAPI, lines: [] }, action: "reconnect", apcName: "apc_2", }); @@ -528,10 +488,145 @@ export const DrawerAPCControl: React.FC = ({ ) : (
)} + + + + - + {listOutlet .filter((el) => el.apc === 2) .map((outlet, i) => ( @@ -573,183 +668,6 @@ export const DrawerAPCControl: React.FC = ({ ))} -
- - - - -
@@ -768,6 +686,7 @@ export const DrawerSwitchControl: React.FC = ({ >([]); const [isSubmit, setIsSubmit] = useState(false); const [loading, setLoading] = useState(true); + const [checkedActive, setCheckedActive] = useState("all"); useEffect(() => { if (!open) { @@ -776,6 +695,13 @@ export const DrawerSwitchControl: React.FC = ({ } }, [open]); + useEffect(() => { + const value = localStorage.getItem("show-switch-port"); + if (value) { + setCheckedActive(value); + } + }, []); + useEffect(() => { if (loading) setTimeout(() => { @@ -881,6 +807,11 @@ export const DrawerSwitchControl: React.FC = ({ return `${type}${last}`; }; + const changeShowPort = (status: string) => { + localStorage.setItem("show-switch-port", status); + setCheckedActive(status); + }; + return loading ? ( = ({ socket?.emit("control_switch", { ports: [], command: "reconnect", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); setIsSubmit(true); @@ -1002,7 +933,7 @@ export const DrawerSwitchControl: React.FC = ({ .filter((el) => el.poe !== "ON") .map((el) => el.name), command: "restart", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); if (listPortsRestart.filter((el) => el.poe === "ON").length > 0) @@ -1011,7 +942,7 @@ export const DrawerSwitchControl: React.FC = ({ .filter((el) => el.poe === "ON") .map((el) => el.name), command: "restart-poe", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); setListPortsSelected([]); @@ -1058,7 +989,7 @@ export const DrawerSwitchControl: React.FC = ({ .filter((el) => el.poe !== "ON") .map((el) => el.name), command: "on", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); if (listPortsRestart.filter((el) => el.poe === "ON").length > 0) @@ -1067,7 +998,7 @@ export const DrawerSwitchControl: React.FC = ({ .filter((el) => el.poe === "ON") .map((el) => el.name), command: "on-poe", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); setListPortsSelected([]); @@ -1114,7 +1045,7 @@ export const DrawerSwitchControl: React.FC = ({ .filter((el) => el.poe !== "ON") .map((el) => el.name), command: "off", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); if (listPortsRestart.filter((el) => el.poe === "ON").length > 0) @@ -1123,7 +1054,7 @@ export const DrawerSwitchControl: React.FC = ({ .filter((el) => el.poe === "ON") .map((el) => el.name), command: "off-poe", - station: stationAPI, + station: { ...stationAPI, lines: [] }, ip: stationAPI?.switch_control_ip, }); setListPortsSelected([]); @@ -1138,6 +1069,26 @@ export const DrawerSwitchControl: React.FC = ({ ? "Turn Off All" : "Turn Off Selected"} + + changeShowPort("all")} + /> + changeShowPort("on")} + /> + changeShowPort("off")} + /> + @@ -1155,10 +1106,17 @@ export const DrawerSwitchControl: React.FC = ({ span={isLarge ? 11 : isMini ? 1 : 12} > {isLarge ? ( - + {sortedPorts(group) .slice(0, sortedPorts(group).length / 2) + .filter((el) => { + if (checkedActive === "all") return true; + if (checkedActive === "on") + return el.status === "ON"; + if (checkedActive === "off") + return el.status === "OFF"; + }) ?.map((port, i) => ( = ({ sortedPorts(group).length / 2, sortedPorts(group).length ) + .filter((el) => { + if (checkedActive === "all") return true; + if (checkedActive === "on") + return el.status === "ON"; + if (checkedActive === "off") + return el.status === "OFF"; + }) ?.map((port, i) => ( = ({ justifyContent: "center", gap: "10px", overflow: "auto", - maxHeight: "7vh", + maxHeight: "12vh", maxWidth: "70vw", borderLeft: "1px solid #dedede", }} > - {sortedPorts(group)?.map((port, i) => ( - el.name === port.name - )?.name - ? "1px solid #0018ff" - : "", - }} - className={`${isSubmit ? classes.isDisabled : ""}`} - onClick={() => { - toggleSelect(port); - }} - > - { + if (checkedActive === "all") return true; + if (checkedActive === "on") + return el.status === "ON"; + if (checkedActive === "off") + return el.status === "OFF"; + }) + ?.map((port, i) => ( + el.name === port.name + )?.name + ? "1px solid #0018ff" + : "", + }} + className={`${ + isSubmit ? classes.isDisabled : "" + }`} + onClick={() => { + toggleSelect(port); }} > - {/* + {/* = ({ : "#b8b8b8" } /> */} - - {normalizePortName(port.name)} - - - - ))} + + {normalizePortName(port.name)} + + + + ))}
)} diff --git a/FRONTEND/src/components/ModalTerminal.tsx b/FRONTEND/src/components/ModalTerminal.tsx index 59893c8..05d6143 100644 --- a/FRONTEND/src/components/ModalTerminal.tsx +++ b/FRONTEND/src/components/ModalTerminal.tsx @@ -13,6 +13,7 @@ import { } from "@mantine/core"; import type { IScenario, + SwitchPortsProps, TDataTicket, THistoryTicket, TLine, @@ -32,6 +33,15 @@ import axios from "axios"; import { notifications } from "@mantine/notifications"; const apiUrl = import.meta.env.VITE_BACKEND_URL; +const INIT_TICKET = { + description: "", + sn: "", + model: "", + station_id: 0, + history: "", + status: "open", +}; + const ModalTerminal = ({ opened, onClose, @@ -57,31 +67,32 @@ const ModalTerminal = ({ }, []); const [isDisable, setIsDisable] = useState(false); const [isDisableTicket, setIsDisableTicket] = useState(false); - const [latestTicket, setLatestTicket] = useState({ - description: "", - sn: "", - model: "", - station_id: 0, - history: "", - status: "open", - }); - const [dataTicket, setDataTicket] = useState({ - description: "", - sn: "", - model: "", - station_id: 0, - history: "", - status: "open", - }); + const [listPorts, setListPorts] = useState([]); + const [latestTicket, setLatestTicket] = useState(INIT_TICKET); + const [dataTicket, setDataTicket] = useState(INIT_TICKET); useEffect(() => { if (opened && line?.tickets && line?.tickets?.length > 0) { const data = line?.tickets[0]; setLatestTicket(data); setDataTicket({ ...data, description: "" }); + } else { + setLatestTicket(INIT_TICKET); + setDataTicket(INIT_TICKET); } }, [opened, line?.tickets]); + useEffect(() => { + socket?.on("switch_ports_status", (data) => { + if (data.stationId !== stationItem?.id) return; + if (data?.ports && data?.ports.length > 0) + setListPorts(data?.ports || []); + }); + return () => { + socket?.off("switch_ports_status"); + }; + }, [socket, stationItem]); + const renderHistory = (data: TDataTicket) => { const latest = JSON.parse(latestTicket?.history || "[]"); const list = @@ -165,8 +176,8 @@ const ModalTerminal = ({ const payload = { id: dataTicket.id || 0, description: dataTicket.description.trim(), - model: dataTicket.model.trim(), - sn: dataTicket.sn.trim(), + model: line?.inventory?.pid.trim(), + sn: line?.inventory?.sn.trim(), station_id: Number(stationItem?.id), line_id: Number(line?.id), status: "open", @@ -313,6 +324,35 @@ const ModalTerminal = ({ }, 5000); }; + const controlSwitch = (action: string) => { + if (!line?.interface) { + notifications.show({ + title: "Error", + message: "Hasn't config interface", + color: "red", + }); + return; + } + socket?.emit("control_switch", { + ports: [line?.interface], + command: action, + station: { ...stationItem, lines: [] }, + ip: stationItem?.switch_control_ip, + }); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 5000); + }; + + const findSwitchPort = (portName: string): SwitchPortsProps | null => { + if (listPorts?.length > 0) { + const port = listPorts.find((el) => el.name === portName); + if (port) return port; + } + return null; + }; + return ( - Warning from test report: + Warning from test report: AI {""} - - - - Internet Connected - - - - - - - +
+ + + + + Internet + + {line?.interface ? ( + findSwitchPort(line?.interface)?.status === "ON" ? ( + + Connected ({line?.interface}) + + ) : ( + + Not Connected ({line?.interface}) + + ) + ) : ( + + Not Connected + + )} + + + + + + + +
- + @@ -505,7 +599,7 @@ const ModalTerminal = ({ } line_status={line?.status || ""} /> - + - + - - - - - + + + + + + + + + + + + @@ -722,7 +831,7 @@ const ModalTerminal = ({ onKeyDown={(event) => { if (event.key === "Enter" && dataTicket.description) { setDataTicket((pre) => ({ ...pre, description: "" })); - if (dataTicket?.status === "closed") { + if (dataTicket?.status === "closed" || !dataTicket.id) { handleCreate(); } else handleUpdate("open"); setIsDisableTicket(true); @@ -747,7 +856,7 @@ const ModalTerminal = ({ - {dataTicket?.status === "closed" ? ( + {dataTicket?.status === "closed" || !dataTicket.id ? (