diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index d39430a..52ff00e 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -22,12 +22,7 @@ import { DrawerAPCControl, DrawerSwitchControl } from "./DrawerControl"; import DrawerScenario from "./DrawerScenario"; import { isJsonString } from "../untils/helper"; import { motion } from "motion/react"; -import { - IconCaretDown, - IconCaretUp, - IconPlayerPlay, - IconPlus, -} from "@tabler/icons-react"; +import { IconCaretDown, IconCaretUp, IconPlayerPlay, IconPlus } from "@tabler/icons-react"; interface TabsProps { selectedLines: TLine[]; @@ -48,6 +43,264 @@ interface TabsProps { isExpand: boolean; } +// Component cho từng Scenario Card +const ScenarioCard = ({ + scenario, + index, + isDisable, + selectedLines, + user, + socket, + setOpenScenarioModal, + setIsDisable, +}: { + scenario: IScenario; + index: number; + isDisable: boolean; + selectedLines: TLine[]; + user: any; + socket: Socket | null; + setOpenScenarioModal: (value: boolean) => void; + setIsDisable: (value: boolean) => void; +}) => { + const [isHovered, setIsHovered] = useState(false); + const [overlayPosition, setOverlayPosition] = useState({ top: 0, left: 0 }); + const cardRef = useRef(null); + const steps = JSON.parse(scenario.body || "[]"); + + const handleMouseEnter = () => { + if (cardRef.current) { + const rect = cardRef.current.getBoundingClientRect(); + const overlayWidth = 400; + const viewportWidth = window.innerWidth; + + // Tính toán vị trí để overlay không tràn ra ngoài màn hình + let left = rect.left; + if (left + overlayWidth > viewportWidth) { + left = viewportWidth - overlayWidth - 10; // 10px margin + } + if (left < 10) { + left = 10; // 10px margin từ bên trái + } + + setOverlayPosition({ + top: rect.top, + left: left, + }); + } + setIsHovered(true); + }; + + useEffect(() => { + if (isHovered && cardRef.current) { + const updatePosition = () => { + if (cardRef.current) { + const rect = cardRef.current.getBoundingClientRect(); + const overlayWidth = 400; + const viewportWidth = window.innerWidth; + + let left = rect.left; + if (left + overlayWidth > viewportWidth) { + left = viewportWidth - overlayWidth - 10; + } + if (left < 10) { + left = 10; + } + + setOverlayPosition({ + top: rect.top, + left: left, + }); + } + }; + + // Update position immediately + updatePosition(); + + // Listen to scroll events (including inside modal) + const scrollContainers = document.querySelectorAll('[style*="overflow"]'); + scrollContainers.forEach((container) => { + container.addEventListener("scroll", updatePosition, true); + }); + + window.addEventListener("scroll", updatePosition, true); + window.addEventListener("resize", updatePosition); + + return () => { + scrollContainers.forEach((container) => { + container.removeEventListener("scroll", updatePosition, true); + }); + window.removeEventListener("scroll", updatePosition, true); + window.removeEventListener("resize", updatePosition); + }; + } + }, [isHovered]); + + return ( + +
+ + + setIsHovered(false)} + > + {scenario.title} + + + + + + + {/* Hover overlay - Fixed position để không bị cắt bởi modal */} + {isHovered && ( +
e.stopPropagation()} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + + {scenario.title} + + + #{index + 1} + + + + + + Timeout: {scenario.timeout}ms + + {scenario.isReboot && ( + + Reboot + + )} + + {steps.length} steps + + + + + Commands Preview: + + + {steps + .slice(0, 5) + .map((step: { send: string }, i: number) => ( + + {i + 1}. {step.send || "(empty)"} + + ))} + {steps.length > 5 && ( + + ... and {steps.length - 5} more + + )} + + +
+ )} +
+
+ ); +}; + const BottomToolBar = ({ selectedLines, socket, @@ -109,13 +362,14 @@ const BottomToolBar = ({ style={{ background: "white", borderRadius: "12px", - maxWidth: "1000px", - width: "auto", - maxHeight: "85vh", + maxWidth: "90vw", + width: "90%", + height: "85vh", display: "flex", flexDirection: "column", boxShadow: "0 20px 60px rgba(0,0,0,0.3)", - overflow: "hidden", + overflowY: "auto", + overflowX: "visible", }} onClick={(e) => e.stopPropagation()} > @@ -138,11 +392,9 @@ const BottomToolBar = ({ variant="light" color="green" size="sm" - onClick={() => { - setOpenScenarioModal(false); - setTimeout(() => { - setOpenDrawerScenario(true); - }, 100); + onClick={(e) => { + e.stopPropagation(); + setOpenDrawerScenario(true); }} > Add/Edit Scenario @@ -159,107 +411,29 @@ const BottomToolBar = ({ style={{ padding: "20px", overflowY: "auto", - overflowX: "hidden", + overflowX: "visible", flex: 1, + position: "relative", }} className={classes.hideScrollBar} > {scenarios.length > 0 ? ( - + {scenarios.map((scenario, index) => ( - - - - - - {scenario.title} - - - #{index + 1} - - - - - - Timeout: {scenario.timeout}ms - - {scenario.isReboot && ( - - Reboot - - )} - - {JSON.parse(scenario.body || "[]").length} steps - - - - - {JSON.parse(scenario.body || "[]") - .slice(0, 2) - .map((step: { send: string }) => step.send) - .join(" → ")} - {JSON.parse(scenario.body || "[]").length > 2 && - " ..."} - - - - - - + ))} ) : ( @@ -301,143 +475,217 @@ const BottomToolBar = ({ zIndex: 1, }} > - - { - setExpanded((prev) => !prev); - }} - > - {isExpand ? ( - - ) : ( - - )} - - - - - { - setActiveTabBottom(val || "command"); - }} - className={classes.containerBottom} - style={{ height: "20vh" }} - > - - - Command Line - - - APC - - - Switch - - + + { + setExpanded((prev) => !prev); + }} + > + {isExpand ? ( + + ) : ( + + )} + + + + + { + setActiveTabBottom(val || "command"); + }} + className={classes.containerBottom} + style={{ height: "20vh" }} + > + + + Command Line + + + APC + + + Switch + + - - - - - - {selectedLines.map((el) => ( - + + + + {selectedLines.map((el) => ( + + + Line {el.lineNumber} + { + setSelectedLines( + selectedLines.filter( + (line) => line.id !== el.id + ) + ); + socket?.emit("close_cli", { + lineId: el?.id, + stationId: el.stationId || el.station_id, + }); }} - > - {/* Close button góc trên phải */} - { - setSelectedLines( - selectedLines.filter( - (line) => line.id !== el.id - ) - ); - socket?.emit("close_cli", { - lineId: el?.id, - stationId: el.stationId || el.station_id, - }); - }} - /> - - - Line {el.lineNumber} - - - ))} - - - {selectedLines?.length > 0 ? ( - + + + { + const newValue = event.currentTarget.value; + setValueInput(newValue); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + const listLine = selectedLines.length + ? selectedLines + : station?.lines; + if (listLine?.length) { + socket?.emit("write_command_line_from_web", { + lineIds: listLine.map((line) => line.id), + stationId: station.id, + command: valueInput + "\r\n", + }); + // setTimeout(() => { + // socket?.emit("write_command_line_from_web", { + // lineIds: listLine.map((line) => line.id), + // stationId: station.id, + // command: " \n", + // }); + // }, 1000); + } + setValueInput(""); + } + }} + rightSectionPointerEvents="all" + rightSection={ + setValueInput("")} + style={{ + display: valueInput ? undefined : "none", + }} + /> + } + /> + + + + + { + 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, @@ -445,171 +693,53 @@ const BottomToolBar = ({ }); }); setSelectedLines([]); - }} - > - Clear - - ) : ( - "" - )} - - - - - - - - - { - const newValue = event.currentTarget.value; - setValueInput(newValue); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - const listLine = selectedLines.length - ? selectedLines - : station?.lines; - if (listLine?.length) { - socket?.emit("write_command_line_from_web", { - lineIds: listLine.map((line) => line.id), - stationId: station.id, - command: valueInput + "\r\n", - }); - // setTimeout(() => { - // socket?.emit("write_command_line_from_web", { - // lineIds: listLine.map((line) => line.id), - // stationId: station.id, - // command: " \n", - // }); - // }, 1000); - } - setValueInput(""); - } - }} - rightSectionPointerEvents="all" - rightSection={ - setValueInput("")} - style={{ - display: valueInput ? undefined : "none", - }} - /> } - /> - - - - - { - 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([]); - setIsDisable(true); - setTimeout(() => { - setIsDisable(false); - }, 5000); - }} - /> - - - - - - - - - - - - - - - - - + }} + /> + { + // setSelectedLines([]); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 5000); + }} + /> + + + + + + + + + + + + + + + + + {/* Drawer Scenario để Add/Edit */} diff --git a/FRONTEND/src/components/DialogConfirm.tsx b/FRONTEND/src/components/DialogConfirm.tsx index 3565434..0d3f2a4 100644 --- a/FRONTEND/src/components/DialogConfirm.tsx +++ b/FRONTEND/src/components/DialogConfirm.tsx @@ -26,6 +26,13 @@ const DialogConfirm = ({ onClose={close} size="xs" radius="md" + zIndex={100001} + withinPortal={true} + portalProps={{ target: document.body }} + overlayProps={{ + backgroundOpacity: 0.7, + blur: 2, + }} > { + e.stopPropagation(); + handleClose(); + }} >