From 654fbe04680136737f0290fec5b22bf003773a76 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:20:08 +0700 Subject: [PATCH] Improve CLI line selection and user access control Enhanced frontend and backend logic to better manage CLI line selection, ensuring only one user can access a line at a time. Added clipboard copy functionality for PID/SN, improved UI feedback for line usage, and updated event handling for opening and closing CLI sessions. Also improved status messaging for disconnected devices and refactored related components for clarity and maintainability. --- BACKEND/providers/socket_io_provider.ts | 6 +- FRONTEND/src/App.tsx | 2 + FRONTEND/src/components/BottomToolBar.tsx | 54 +++++-- FRONTEND/src/components/ButtonAction.tsx | 14 +- FRONTEND/src/components/CardLine.tsx | 144 ++++++++++++++----- FRONTEND/src/components/Component.module.css | 4 + FRONTEND/src/components/DrawerControl.tsx | 16 ++- FRONTEND/src/components/ModalTerminal.tsx | 7 +- 8 files changed, 188 insertions(+), 59 deletions(-) diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index be1da6e..a36d635 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -112,8 +112,8 @@ export class WebSocketIo { socket.on('disconnect', () => { console.log(`FE disconnected: ${socket.id}`) this.userConnecting.delete(userId) - const listLineS = Array.from(this.lineMap.values()).map((el) => el?.config || {}) - listLineS.forEach((el) => { + const listLine = Array.from(this.lineMap.values()).map((el) => el?.config || {}) + listLine.forEach((el) => { if (el?.userOpenCLI === userName) { const line = this.lineMap.get(el.id) if (line) { @@ -546,6 +546,7 @@ export class WebSocketIo { if (line && line.config.status === 'connected') { this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, line, options.timeout) + await sleep(500) await action(line, options) } else { if (this.lineConnecting.includes(lineId)) continue @@ -567,6 +568,7 @@ export class WebSocketIo { const lineReconnect = this.lineMap.get(lineId) if (lineReconnect) { this.setTimeoutConnect(lineId, lineReconnect, options.timeout) + await sleep(500) await action(lineReconnect, options) } } else { diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index 5271e23..002e1e9 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -185,6 +185,7 @@ function App() { socket?.on("init", (data) => { if (Array.isArray(data)) { + // console.log(data); data.forEach((value) => { updateValueLineStation( value?.id, @@ -570,6 +571,7 @@ function App() { /> { setOpenModalTerminal(false); diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index db67f21..9a384fe 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -111,11 +111,15 @@ const BottomToolBar = ({ + onClick={() => { setSelectedLines( selectedLines.filter((line) => line.id !== el.id) - ) - } + ); + socket?.emit("close_cli", { + lineId: el?.id, + stationId: el.stationId || el.station_id, + }); + }} /> @@ -174,13 +178,13 @@ const BottomToolBar = ({ stationId: station.id, command: valueInput + "\n", }); - setTimeout(() => { - socket?.emit("write_command_line_from_web", { - lineIds: listLine.map((line) => line.id), - stationId: station.id, - command: " \n", - }); - }, 1000); + // setTimeout(() => { + // socket?.emit("write_command_line_from_web", { + // lineIds: listLine.map((line) => line.id), + // stationId: station.id, + // command: " \n", + // }); + // }, 1000); } setValueInput(""); } @@ -204,13 +208,39 @@ const BottomToolBar = ({ selectedLines={selectedLines} setSelectedLines={setSelectedLines} station={station} + userName={user?.userName} + onClick={() => { + const lines = station.lines.filter( + (line) => + !line?.userOpenCLI || line?.userOpenCLI === user?.userName + ); + if (selectedLines.length !== lines.length) { + setSelectedLines(lines); + lines.forEach((line) => { + socket?.emit("open_cli", { + lineId: line.id, + stationId: line.stationId || line.station_id, + userEmail: user?.email, + userName: user?.userName, + }); + }); + } else { + lines.forEach((line) => { + socket?.emit("close_cli", { + lineId: line?.id, + stationId: line.stationId || line.station_id, + }); + }); + setSelectedLines([]); + } + }} /> { - setSelectedLines([]); + // setSelectedLines([]); setIsDisable(true); setTimeout(() => { setIsDisable(false); @@ -257,7 +287,7 @@ const BottomToolBar = ({ ).length === 0 } onClick={() => { - setSelectedLines([]); + // setSelectedLines([]); setIsDisable(true); setTimeout(() => { setIsDisable(false); diff --git a/FRONTEND/src/components/ButtonAction.tsx b/FRONTEND/src/components/ButtonAction.tsx index a389063..282268d 100644 --- a/FRONTEND/src/components/ButtonAction.tsx +++ b/FRONTEND/src/components/ButtonAction.tsx @@ -223,24 +223,28 @@ export const ButtonCopy = ({ export const ButtonSelect = ({ selectedLines, - setSelectedLines, station, + onClick, + userName, }: { setSelectedLines: (value: React.SetStateAction) => void; selectedLines: TLine[]; station: TStation; + onClick: () => void; + userName: string; }) => { return ( diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index 6b5fa3a..caecf8a 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -3,7 +3,7 @@ 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 { memo, useMemo, useState } from "react"; +import { memo, useEffect, useMemo, useState } from "react"; import { convertTimestampToDate } from "../untils/helper"; import { ButtonDPELP, ButtonScenario } from "./ButtonAction"; import { notifications } from "@mantine/notifications"; @@ -38,6 +38,17 @@ const CardLine = ({ const [isDisabled, setIsDisabled] = useState(false); const [valueBaud, setValueBaud] = useState(""); + useEffect(() => { + if ( + typeof line?.userOpenCLI !== "undefined" && + typeof line?.userOpenCLI === "string" && + line?.userOpenCLI.length > 0 && + line?.userOpenCLI !== user?.userName + ) + setIsDisabled(true); + else setIsDisabled(false); + }, [line?.userOpenCLI]); + const controlApc = (action: string) => { if (!line.outlet) { notifications.show({ @@ -78,7 +89,35 @@ const CardLine = ({ setIsDisabled(false); }, 5000); }; - console.log("RERENDER", line.lineNumber); + + const handleClick = (isSelect = false) => { + if ( + typeof line?.userOpenCLI !== "undefined" && + typeof line?.userOpenCLI === "string" && + line?.userOpenCLI.length > 0 && + line?.userOpenCLI !== user?.userName + ) + return; + + if (selectedLines.find((val) => val.id === line.id)) { + if (isSelect) { + setSelectedLines(selectedLines.filter((val) => val.id !== line.id)); + socket?.emit("close_cli", { + lineId: line?.id, + stationId: line.stationId || line.station_id, + }); + } + } else { + setSelectedLines((pre) => [...pre, line]); + socket?.emit("open_cli", { + lineId: line.id, + stationId: line.stationId || line.station_id, + userEmail: user?.email, + userName: user?.userName, + }); + } + }; + return ( { e.preventDefault(); e.stopPropagation(); - if (selectedLines.find((val) => val.id === line.id)) - setSelectedLines(selectedLines.filter((val) => val.id !== line.id)); - else setSelectedLines((pre) => [...pre, line]); + handleClick(true); }} onMouseLeave={() => setTimeout(() => setShowMenu(false), 150)} > @@ -131,6 +168,16 @@ const CardLine = ({ { + e.preventDefault(); + e.stopPropagation(); + navigator.clipboard.writeText( + `PID: ${line?.inventory?.pid || ""} | SN: ${ + line?.inventory?.sn || "" + }` + ); + }} + className={classes.buttonCopy} fw={600} style={{ fontSize: "24px", @@ -160,7 +207,19 @@ const CardLine = ({
PID:{" "} - + { + e.preventDefault(); + e.stopPropagation(); + navigator.clipboard.writeText( + `PID: ${line?.inventory?.pid || ""} | SN: ${ + line?.inventory?.sn || "" + }` + ); + }} + className={`${classes.info_line} ${classes.buttonCopy}`} + fs={"italic"} + > {line?.inventory?.pid || ""} {line?.inventory?.vid ? ( @@ -176,19 +235,38 @@ const CardLine = ({ style={{ width: "120px" }} > SN:{" "} - + { + e.preventDefault(); + e.stopPropagation(); + navigator.clipboard.writeText( + `PID: ${line?.inventory?.pid || ""} | SN: ${ + line?.inventory?.sn || "" + }` + ); + }} + className={`${classes.info_line} ${classes.buttonCopy}`} + fs={"italic"} + > {line?.inventory?.sn || ""}
-
- {line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""} -
+ + +
+ {line?.userOpenCLI + ? (line?.userOpenCLI === user?.userName + ? "You are" + : line?.userOpenCLI + " is") + " using" + : ""} +
+
@@ -360,7 +438,7 @@ const CardLine = ({ mt={"8px"} disabled={isDisabled} variant="outline" - color="orange" + color="red" size="xs" onClick={() => { controlApc("off"); @@ -372,7 +450,7 @@ const CardLine = ({ mt={"8px"} disabled={isDisabled} variant="outline" - color="red" + color="orange" size="xs" onClick={() => { controlApc("restart"); @@ -401,12 +479,13 @@ const CardLine = ({ line_id={Number(line?.id)} line={line} station_id={Number(stationItem.id)} - isDisabled={ - typeof line?.userOpenCLI !== "undefined" && - typeof line?.userOpenCLI === "string" && - line?.userOpenCLI.length > 0 && - line?.userOpenCLI !== user?.userName - } + // isDisabled={ + // typeof line?.userOpenCLI !== "undefined" && + // typeof line?.userOpenCLI === "string" && + // line?.userOpenCLI.length > 0 && + // line?.userOpenCLI !== user?.userName + // } + isDisabled={false} line_status={line?.status || ""} fontSize={11} miniSize={true} @@ -421,18 +500,17 @@ const CardLine = ({ openTerminal(line); }} onFocus={() => { - socket?.emit("open_cli", { - lineId: line.id, - stationId: line.stationId || line.station_id, - userEmail: user?.email, - userName: user?.userName, - }); + handleClick(); }} onBlur={() => { - socket?.emit("close_cli", { - lineId: line?.id, - stationId: line.stationId || line.station_id, - }); + if ( + !selectedLines.find((value) => value.id === line?.id) && + line?.userOpenCLI === user?.userName + ) + socket?.emit("close_cli", { + lineId: line?.id, + stationId: line.stationId || line.station_id, + }); }} /> diff --git a/FRONTEND/src/components/Component.module.css b/FRONTEND/src/components/Component.module.css index d87e8cc..4b215df 100644 --- a/FRONTEND/src/components/Component.module.css +++ b/FRONTEND/src/components/Component.module.css @@ -124,3 +124,7 @@ .topBarLine:hover { background-color: #f3f3f3; } + +.buttonCopy:hover { + background-color: #ccc !important; +} diff --git a/FRONTEND/src/components/DrawerControl.tsx b/FRONTEND/src/components/DrawerControl.tsx index b8ba0a7..a845602 100644 --- a/FRONTEND/src/components/DrawerControl.tsx +++ b/FRONTEND/src/components/DrawerControl.tsx @@ -211,9 +211,9 @@ export const DrawerAPCControl: React.FC = ({ default: return ( <> - {/* - WRONG CONFIG - */} + + DISCONNECTED + ); } @@ -799,8 +799,6 @@ export const DrawerSwitchControl: React.FC = ({ case "DISCONNECTED": return ( <> -
- {apc.status} @@ -879,7 +877,13 @@ export const DrawerSwitchControl: React.FC = ({ }} > - {dataStation?.switch ? RenderAPCStatus(dataStation?.switch) : ""} + {dataStation?.switch ? ( + RenderAPCStatus(dataStation?.switch) + ) : ( + + DISCONNECTED + + )} {dataStation?.switch?.status !== "CONNECTED" ? (