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.
This commit is contained in:
nguyentrungthat 2025-11-14 13:20:08 +07:00
parent 8a06650eab
commit 654fbe0468
8 changed files with 188 additions and 59 deletions

View File

@ -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 {

View File

@ -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() {
/>
<ModalTerminal
selectedLines={selectedLines}
opened={openModalTerminal}
onClose={() => {
setOpenModalTerminal(false);

View File

@ -111,11 +111,15 @@ const BottomToolBar = ({
<CloseButton
style={{ minWidth: "24px" }}
aria-label="Clear input"
onClick={() =>
onClick={() => {
setSelectedLines(
selectedLines.filter((line) => line.id !== el.id)
)
}
);
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el.stationId || el.station_id,
});
}}
/>
</Flex>
</Box>
@ -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([]);
}
}}
/>
<ButtonDPELP
socket={socket}
selectedLines={selectedLines}
isDisable={isDisable || selectedLines.length === 0}
onClick={() => {
setSelectedLines([]);
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
@ -257,7 +287,7 @@ const BottomToolBar = ({
).length === 0
}
onClick={() => {
setSelectedLines([]);
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);

View File

@ -223,24 +223,28 @@ export const ButtonCopy = ({
export const ButtonSelect = ({
selectedLines,
setSelectedLines,
station,
onClick,
userName,
}: {
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
selectedLines: TLine[];
station: TStation;
onClick: () => void;
userName: string;
}) => {
return (
<Button
variant="filled"
style={{ height: "30px", width: "100px" }}
onClick={() => {
if (selectedLines.length !== station.lines.length)
setSelectedLines(station.lines);
else setSelectedLines([]);
onClick();
}}
>
{selectedLines.length !== station.lines.length
{selectedLines.length !==
station.lines.filter(
(line) => !line?.userOpenCLI || line?.userOpenCLI === userName
).length
? "Select All"
: "Deselect"}
</Button>

View File

@ -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<boolean>(false);
const [valueBaud, setValueBaud] = useState<string>("");
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 (
<Card
key={line.id}
@ -99,9 +138,7 @@ const CardLine = ({
onClick={(e) => {
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 = ({
<Flex direction={"column"} justify={"center"} align={"center"}>
<Box>
<Text
onClick={(e) => {
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 = ({
<Flex justify={"space-between"} w={"100%"}>
<div className={classes.info_line}>
PID:{" "}
<Text className={classes.info_line} fs={"italic"}>
<Text
onClick={(e) => {
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 || ""}
</Text>
{line?.inventory?.vid ? (
@ -176,19 +235,38 @@ const CardLine = ({
style={{ width: "120px" }}
>
SN:{" "}
<Text className={classes.info_line} fs={"italic"}>
<Text
onClick={(e) => {
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 || ""}
</Text>
</div>
</Flex>
<Flex justify={"space-between"} w={"100%"}>
<Box></Box>
<div
style={{
fontSize: "12px",
fontSize: "11px",
color: "red",
}}
>
{line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""}
{line?.userOpenCLI
? (line?.userOpenCLI === user?.userName
? "You are"
: line?.userOpenCLI + " is") + " using"
: ""}
</div>
</Flex>
</Box>
</Flex>
</Flex>
@ -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,14 +500,13 @@ 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={() => {
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,

View File

@ -124,3 +124,7 @@
.topBarLine:hover {
background-color: #f3f3f3;
}
.buttonCopy:hover {
background-color: #ccc !important;
}

View File

@ -211,9 +211,9 @@ export const DrawerAPCControl: React.FC<DrawerProps> = ({
default:
return (
<>
{/* <Text fw={800} c="red" fz={"12px"}>
WRONG CONFIG
</Text> */}
<Text fw={800} c="red" fz={"12px"}>
DISCONNECTED
</Text>
</>
);
}
@ -799,8 +799,6 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
case "DISCONNECTED":
return (
<>
<div></div>
<Text size="sm" fw={800} c="red">
{apc.status}
</Text>
@ -879,7 +877,13 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
}}
>
<Box ps={"8px"} pt={"4px"}>
{dataStation?.switch ? RenderAPCStatus(dataStation?.switch) : ""}
{dataStation?.switch ? (
RenderAPCStatus(dataStation?.switch)
) : (
<Text size="sm" fw={800} c="red">
DISCONNECTED
</Text>
)}
</Box>
{dataStation?.switch?.status !== "CONNECTED" ? (
<Button

View File

@ -35,6 +35,7 @@ const ModalTerminal = ({
setDisableRequestTakeOver,
setCountDownRequest,
setDataRequestTakeOver,
selectedLines,
}: {
opened: boolean;
onClose: () => void;
@ -45,6 +46,7 @@ const ModalTerminal = ({
dataRequestTakeOver: IDataTakeOver | undefined;
countDownRequest: number;
disableRequestTakeOver: boolean;
selectedLines: TLine[];
setDisableRequestTakeOver: (value: React.SetStateAction<boolean>) => void;
setCountDownRequest: (value: React.SetStateAction<number>) => void;
setDataRequestTakeOver: (
@ -96,7 +98,10 @@ const ModalTerminal = ({
opened={opened}
onClose={() => {
onClose();
if (line?.userOpenCLI === user?.userName)
if (
line?.userOpenCLI === user?.userName &&
!selectedLines.find((value) => value.id === line?.id)
)
socket?.emit("close_cli", {
lineId: line?.id,
stationId: line?.station_id,