ATC_SIMPLE/FRONTEND/src/components/BottomToolBar.tsx

837 lines
31 KiB
TypeScript

import {
ActionIcon,
Box,
Button,
CloseButton,
Flex,
Grid,
ScrollArea,
Tabs,
Text,
Card,
Badge,
} from "@mantine/core";
import { useEffect, useMemo, useRef, useState } from "react";
import classes from "./Component.module.css";
import type { IScenario, TLine, TStation, TUser } from "../untils/types";
import type { Socket } from "socket.io-client";
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
import DrawerLogs from "./Drawer/DrawerLogs";
import { DrawerAPCControl, DrawerSwitchControl } from "./Drawer/DrawerControl";
import DrawerScenario from "./Modal/ModalScenario";
import { isJsonString } from "../untils/helper";
import { motion } from "motion/react";
import {
IconCaretDown,
IconCaretUp,
IconPlayerPlay,
IconPlus,
} from "@tabler/icons-react";
import InputHistory from "./InputHistory";
interface TabsProps {
selectedLines: TLine[];
socket: Socket | null;
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
isDisable: boolean;
station: TStation;
stationId: number;
setIsDisable: (value: React.SetStateAction<boolean>) => void;
testLogContent: string;
isLogModalOpen: boolean;
setIsLogModalOpen: (value: React.SetStateAction<boolean>) => void;
setTestLogContent: (value: React.SetStateAction<string>) => void;
scenarios: IScenario[];
setScenarios: (value: React.SetStateAction<IScenario[]>) => void;
setExpanded: (value: React.SetStateAction<boolean>) => void;
activeTabBottom: string;
setActiveTabBottom: (value: React.SetStateAction<string>) => void;
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: TUser;
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<HTMLDivElement>(null);
const steps = JSON.parse(scenario.body || "[]");
const handleMouseEnter = () => {
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect();
const overlayWidth = 400;
const viewportWidth = window.innerWidth;
const gap = 10; // Khoảng cách giữa card và overlay
// Đặt overlay bên phải card
let left = rect.right + gap;
// Nếu overlay tràn ra ngoài màn hình bên phải → đặt bên trái card
if (left + overlayWidth > viewportWidth) {
left = rect.left - overlayWidth - gap;
// Nếu vẫn tràn ra ngoài bên trái → đặt ở giữa màn hình
if (left < 10) {
left = (viewportWidth - overlayWidth) / 2;
}
}
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;
const gap = 10; // Khoảng cách giữa card và overlay
// Đặt overlay bên phải card
let left = rect.right + gap;
// Nếu overlay tràn ra ngoài màn hình bên phải → đặt bên trái card
if (left + overlayWidth > viewportWidth) {
left = rect.left - overlayWidth - gap;
// Nếu vẫn tràn ra ngoài bên trái → đặt ở giữa màn hình
if (left < 10) {
left = (viewportWidth - overlayWidth) / 2;
}
}
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 (
<Grid.Col key={scenario.id} span={3}>
<div ref={cardRef} style={{ position: "relative" }}>
<Card
shadow="sm"
padding="md"
radius="md"
withBorder
style={{
transition: "all 0.2s ease",
height: "auto",
minHeight: "80px",
}}
className={classes.scenarioCard}
>
<Flex direction="column" gap="sm" align="center" justify="center">
<Text
fw={600}
size="lg"
ta="center"
style={{
cursor: "pointer",
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setIsHovered(false)}
>
{scenario.title}
</Text>
<Button
fullWidth
variant="light"
color="green"
size="sm"
leftSection={<IconPlayerPlay size={16} />}
disabled={
isDisable ||
selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
).length === 0
}
onClick={() => {
setOpenScenarioModal(false);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
selectedLines
.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
)
.forEach((el) => {
socket?.emit(
"run_scenario",
Object.assign(el, {
scenario: scenario,
})
);
});
}}
>
Run
</Button>
</Flex>
</Card>
{/* Hover overlay - Fixed position để không bị cắt bởi modal */}
{isHovered && (
<div
style={{
position: "fixed",
top: `${overlayPosition.top}px`,
left: `${overlayPosition.left}px`,
width: "400px",
maxWidth: "90vw",
background: "white",
border: "2px solid #4dabf7",
borderRadius: "8px",
padding: "16px",
boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
zIndex: 99999,
minHeight: "200px",
maxHeight: "400px",
overflowY: "auto",
overflowX: "hidden",
}}
className={classes.hideScrollBar}
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Flex direction="column" gap="xs">
<Flex justify="space-between" align="center">
<Text fw={700} size="md">
{scenario.title}
</Text>
<Badge color="blue" variant="light">
#{index + 1}
</Badge>
</Flex>
<Flex gap="xs" wrap="wrap" mt="xs">
<Badge size="sm" color="gray" variant="dot">
Timeout: {scenario.timeout}ms
</Badge>
{scenario.isReboot && (
<Badge size="sm" color="orange" variant="dot">
Reboot
</Badge>
)}
<Badge size="sm" color="green" variant="dot">
{steps.length} steps
</Badge>
</Flex>
<Text size="xs" fw={600} mt="xs" c="dimmed">
Commands Preview:
</Text>
<Box
style={{
background: "#f8f9fa",
padding: "8px",
borderRadius: "4px",
maxHeight: "100px",
overflow: "auto",
}}
>
{steps.slice(0, 5).map((step: { send: string }, i: number) => (
<Text
key={i}
size="xs"
c="dimmed"
style={{
fontFamily: "monospace",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{i + 1}. {step.send || "(empty)"}
</Text>
))}
{steps.length > 5 && (
<Text size="xs" c="dimmed" ta="center" mt="xs">
... and {steps.length - 5} more
</Text>
)}
</Box>
</Flex>
</div>
)}
</div>
</Grid.Col>
);
};
const BottomToolBar = ({
selectedLines,
socket,
setSelectedLines,
isDisable,
station,
setIsDisable,
testLogContent,
isLogModalOpen,
setIsLogModalOpen,
setTestLogContent,
scenarios,
setScenarios,
setExpanded,
setActiveTabBottom,
activeTabBottom,
isExpand,
stationId,
}: TabsProps) => {
const user = useMemo(() => {
return localStorage.getItem("user") &&
isJsonString(localStorage.getItem("user"))
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
const [openScenarioModal, setOpenScenarioModal] = useState<boolean>(false);
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
// const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
// const [isExpand, setIsExpand] = useState<boolean>(true);
return (
<>
{/* Modal chọn Scenario - Custom Simple Modal */}
<motion.div
initial={false}
animate={{
height: isExpand ? "150px" : 0,
y: 0,
}}
transition={{ type: "spring", stiffness: 180, damping: 20 }}
style={{
width: "100%",
position: "fixed",
bottom: 0,
left: 0,
zIndex: 1,
overflow: "hidden",
}}
>
<Box style={{ position: "relative", height: isExpand ? "150px" : "0px" }}>
<ActionIcon
style={{
position: "absolute",
top: isExpand ? -4 : -24,
left: "50%",
translate: "-19px 0",
backgroundColor: "#e3e0e0",
width: "55px",
zIndex: 10,
}}
variant="light"
onClick={() => {
setExpanded((prev) => !prev);
}}
>
{isExpand ? (
<IconCaretDown color="green" />
) : (
<IconCaretUp color="green" />
)}
</ActionIcon>
<Grid style={{ height: "100%" }}>
<Grid.Col span={isExpand ? 1 : 3.5}></Grid.Col>
<Grid.Col span={isExpand ? 10 : 5}>
{isExpand ? (
<Tabs
defaultValue="command"
orientation="vertical"
value={activeTabBottom}
onChange={(val) => {
setActiveTabBottom(val || "command");
}}
className={classes.containerBottom}
style={{ height: "100%" }}
>
<Tabs.List>
<Tabs.Tab
style={{
backgroundColor:
activeTabBottom === "command" ? "#c8d9fd" : "",
fontSize: "13px",
paddingTop: "8px",
paddingBottom: "8px",
}}
value="command"
>
Command Line
</Tabs.Tab>
<Tabs.Tab
style={{
backgroundColor:
activeTabBottom === "apc" ? "#c8d9fd" : "",
fontSize: "13px",
paddingTop: "8px",
paddingBottom: "8px",
}}
value="apc"
>
APC
</Tabs.Tab>
<Tabs.Tab
style={{
backgroundColor:
activeTabBottom === "switch" ? "#c8d9fd" : "",
fontSize: "13px",
paddingTop: "8px",
paddingBottom: "8px",
}}
value="switch"
>
Switch
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel
value="command"
p={4}
style={{ height: "100%", overflow: "auto" }}
>
<Flex justify={"space-between"} align="flex-start">
<Box>
<ScrollArea h={"8vh"}>
<Flex wrap={"wrap"} gap={"8px"} w={"420px"}>
{selectedLines.map((el) => (
<Box
key={el.id}
style={{
position: "relative",
padding: "4px 6px",
height: "26px",
width: "60px",
backgroundColor: "#d4e3ff",
borderRadius: "8px",
}}
>
{/* Close button góc trên phải */}
<CloseButton
size="xs"
style={{
position: "absolute",
top: "-4px",
right: "-6px",
minWidth: "18px",
width: "18px",
height: "18px",
zIndex: 10,
}}
onClick={() => {
setSelectedLines(
selectedLines.filter(
(line) => line.id !== el.id
)
);
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el.stationId || el.station_id,
});
}}
/>
<Flex
align={"center"}
justify={"center"}
h="100%"
>
<Text fz={"11px"}>Line {el.lineNumber}</Text>
</Flex>
</Box>
))}
{selectedLines.length > 0 && (
<Box
style={{
padding: "4px 10px",
height: "26px",
backgroundColor: "#ffe3e3",
borderRadius: "999px",
cursor: "pointer",
display: "flex",
alignItems: "center",
}}
onClick={() => {
selectedLines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
stationId:
line.stationId || line.station_id,
});
});
setSelectedLines([]);
}}
>
<Text fz={"11px"} c="red">
Clear
</Text>
</Box>
)}
</Flex>
</ScrollArea>
<Flex justify={"space-between"} align={"center"} mt={4}>
<Text fz={"11px"} c="dimmed">
Selected: {selectedLines.length} /{" "}
{station.lines.length}
</Text>
<ButtonSelect
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 {
selectedLines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
stationId: line.stationId || line.station_id,
});
});
setSelectedLines([]);
}
}}
/>
</Flex>
</Box>
<Box pl={"md"} pr={"md"}>
<Flex justify={"space-between"} mb={"xs"}>
<Flex></Flex>
<Button
fw={400}
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="orange"
size="xs"
radius="md"
onClick={() => {
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: "spam_break",
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}
}}
>
Send Break
</Button>
</Flex>
<Box>
<InputHistory
selectedLines={selectedLines}
socket={socket}
station={station}
/>
</Box>
</Box>
<Box style={{ width: "260px" }}>
<Flex
align={"center"}
justify={"flex-end"}
gap={"xs"}
wrap={"wrap"}
>
<ButtonDPELP
socket={socket}
selectedLines={selectedLines}
isDisable={isDisable || selectedLines.length === 0}
onClick={() => {
if (
selectedLines.length > 0
// &&
// selectedLines.length === station?.lines?.length
) {
socket?.emit("run_all_dpelp", {
lineIds: selectedLines.map((line) => line.id),
stationName: station.name,
stationId: station.id,
});
}
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
/>
<Button
fw={400}
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="yellow"
style={{ height: "30px", width: "100px" }}
onClick={() => setOpenScenarioModal(true)}
>
Scenario
</Button>
<DrawerLogs
socket={socket}
isLogModalOpen={isLogModalOpen}
setIsLogModalOpen={setIsLogModalOpen}
testLogContent={testLogContent}
setTestLogContent={setTestLogContent}
/>
</Flex>
</Box>
</Flex>
</Tabs.Panel>
<Tabs.Panel
value="apc"
p={0}
ps={"xs"}
style={{
height: "100%",
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
<Box style={{ overflow: "auto",height: "200px", padding: "4px 8px 0 0" ,}}>
<DrawerAPCControl
socket={socket}
stationAPI={station}
stationId={stationId}
/>
</Box>
</Tabs.Panel>
<Tabs.Panel
value="switch"
ps={"xs"}
style={{
height: "100%",
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
<Box style={{ height: "200px", overflow: "auto" }}>
<DrawerSwitchControl
socket={socket}
stationAPI={station}
stationId={stationId}
/>
</Box>
</Tabs.Panel>
</Tabs>
) : (
<Box p={3}>
<Flex direction="column" gap="xs">
<Flex justify={"space-between"} align={"center"} wrap="wrap" gap="xs">
<Flex wrap={"wrap"} gap={"6px"} style={{ flex: 1, minWidth: 0 }}>
{selectedLines.map((el) => (
<Box
key={el.id}
style={{
position: "relative",
padding: "3px 5px",
height: "22px",
width: "55px",
backgroundColor: "#d4e3ff",
borderRadius: "6px",
}}
>
<CloseButton
size="xs"
style={{
position: "absolute",
top: "-4px",
right: "-6px",
minWidth: "16px",
width: "16px",
height: "16px",
zIndex: 10,
}}
onClick={() => {
setSelectedLines(
selectedLines.filter(
(line) => line.id !== el.id
)
);
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el.stationId || el.station_id,
});
}}
/>
<Flex
align={"center"}
justify={"center"}
h="100%"
>
<Text fz={"10px"}>Line {el.lineNumber}</Text>
</Flex>
</Box>
))}
{selectedLines.length > 0 && (
<Box
style={{
padding: "3px 8px",
height: "22px",
backgroundColor: "#ffe3e3",
borderRadius: "999px",
cursor: "pointer",
display: "flex",
alignItems: "center",
}}
onClick={() => {
selectedLines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
stationId:
line.stationId || line.station_id,
});
});
setSelectedLines([]);
}}
>
<Text fz={"10px"} c="red">
Clear
</Text>
</Box>
)}
</Flex>
<Flex align={"center"} gap="xs">
<Text fz={"10px"} c="dark" fw={600}>
Selected: {selectedLines.length} /{" "}
{station.lines.length}
</Text>
<ButtonSelect
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 {
selectedLines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
stationId: line.stationId || line.station_id,
});
});
setSelectedLines([]);
}
}}
/>
</Flex>
</Flex>
<Box style={{ width: "100%" }}>
<InputHistory
selectedLines={selectedLines}
socket={socket}
station={station}
/>
</Box>
</Flex>
</Box>
)}
</Grid.Col>
<Grid.Col span={isExpand ? 1 : 3.5}></Grid.Col>
</Grid>
</Box>
</motion.div>
{/* Drawer Scenario để Add/Edit */}
<DrawerScenario
scenarios={scenarios}
setScenarios={setScenarios}
externalOpened={openDrawerScenario}
onExternalClose={() => setOpenDrawerScenario(false)}
/>
</>
);
};
export default BottomToolBar;