Refactor BottomToolBar and update user types #15

Merged
andrew.ng merged 1 commits from that into main 2025-11-27 14:37:00 +11:00
3 changed files with 337 additions and 288 deletions
Showing only changes of commit 1b9b18ce3b - Show all commits

View File

@ -14,7 +14,7 @@ import {
} from "@mantine/core";
import { useEffect, useMemo, useRef, useState } from "react";
import classes from "./Component.module.css";
import type { IScenario, TLine, TStation } from "../untils/types";
import type { IScenario, TLine, TStation, TUser } from "../untils/types";
import type { Socket } from "socket.io-client";
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
import DrawerLogs from "./DrawerLogs";
@ -22,7 +22,12 @@ 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[];
@ -58,7 +63,7 @@ const ScenarioCard = ({
index: number;
isDisable: boolean;
selectedLines: TLine[];
user: any;
user: TUser;
socket: Socket | null;
setOpenScenarioModal: (value: boolean) => void;
setIsDisable: (value: boolean) => void;
@ -73,7 +78,7 @@ const ScenarioCard = ({
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) {
@ -82,7 +87,7 @@ const ScenarioCard = ({
if (left < 10) {
left = 10; // 10px margin từ bên trái
}
setOverlayPosition({
top: rect.top,
left: left,
@ -98,7 +103,7 @@ const ScenarioCard = ({
const rect = cardRef.current.getBoundingClientRect();
const overlayWidth = 400;
const viewportWidth = window.innerWidth;
let left = rect.left;
if (left + overlayWidth > viewportWidth) {
left = viewportWidth - overlayWidth - 10;
@ -106,7 +111,7 @@ const ScenarioCard = ({
if (left < 10) {
left = 10;
}
setOverlayPosition({
top: rect.top,
left: left,
@ -122,7 +127,7 @@ const ScenarioCard = ({
scrollContainers.forEach((container) => {
container.addEventListener("scroll", updatePosition, true);
});
window.addEventListener("scroll", updatePosition, true);
window.addEventListener("resize", updatePosition);
@ -138,10 +143,7 @@ const ScenarioCard = ({
return (
<Grid.Col key={scenario.id} span={3}>
<div
ref={cardRef}
style={{ position: "relative" }}
>
<div ref={cardRef} style={{ position: "relative" }}>
<Card
shadow="sm"
padding="md"
@ -178,7 +180,8 @@ const ScenarioCard = ({
isDisable ||
selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI || el?.userEmailOpenCLI === user?.email
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
).length === 0
}
onClick={() => {
@ -270,23 +273,21 @@ const ScenarioCard = ({
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.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
@ -420,7 +421,11 @@ const BottomToolBar = ({
{scenarios.length > 0 ? (
<Grid
gutter="md"
style={{ margin: 0, overflow: "visible", position: "relative" }}
style={{
margin: 0,
overflow: "visible",
position: "relative",
}}
>
{scenarios.map((scenario, index) => (
<ScenarioCard
@ -475,217 +480,143 @@ const BottomToolBar = ({
zIndex: 1,
}}
>
<Box style={{ position: "relative" }}>
<ActionIcon
style={{
position: "absolute",
top: isExpand ? -4 : -24,
left: "50%",
translate: "-19px 0",
backgroundColor: "#e3e0e0",
width: "55px",
}}
variant="light"
onClick={() => {
setExpanded((prev) => !prev);
}}
>
{isExpand ? (
<IconCaretDown color="green" />
) : (
<IconCaretUp color="green" />
)}
</ActionIcon>
<Grid>
<Grid.Col span={1}></Grid.Col>
<Grid.Col span={10}>
<Tabs
defaultValue="command"
orientation="vertical"
value={activeTabBottom}
onChange={(val) => {
setActiveTabBottom(val || "command");
}}
className={classes.containerBottom}
style={{ height: "20vh" }}
>
<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>
<Box style={{ position: "relative" }}>
<ActionIcon
style={{
position: "absolute",
top: isExpand ? -4 : -24,
left: "50%",
translate: "-19px 0",
backgroundColor: "#e3e0e0",
width: "55px",
}}
variant="light"
onClick={() => {
setExpanded((prev) => !prev);
}}
>
{isExpand ? (
<IconCaretDown color="green" />
) : (
<IconCaretUp color="green" />
)}
</ActionIcon>
<Grid>
<Grid.Col span={1}></Grid.Col>
<Grid.Col span={10}>
<Tabs
defaultValue="command"
orientation="vertical"
value={activeTabBottom}
onChange={(val) => {
setActiveTabBottom(val || "command");
}}
className={classes.containerBottom}
style={{ height: "20vh" }}
>
<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={"xs"}>
<Flex justify={"space-between"}>
<ScrollArea h={"15vh"}>
<Flex wrap={"wrap"} gap={"xs"} w={"400px"}>
{selectedLines.map((el) => (
<Box
key={el.id}
style={{
paddingLeft: "4px",
height: "30px",
width: "80px",
backgroundColor: "#d4e3ff",
borderRadius: "8px",
}}
>
<Flex align={"center"} justify={"center"} gap={"4px"}>
<Text fz={"12px"}>Line {el.lineNumber}</Text>
<CloseButton
style={{ minWidth: "24px" }}
aria-label="Clear input"
onClick={() => {
setSelectedLines(
selectedLines.filter(
(line) => line.id !== el.id
)
);
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el.stationId || el.station_id,
});
}}
/>
</Flex>
</Box>
))}
</Flex>
</ScrollArea>
<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>
<Tabs.Panel value="command" p={"xs"}>
<Flex justify={"space-between"}>
<Box>
<Input
ref={inputRef}
style={{
width: "30vw",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}}
placeholder={"Send command to port(s)"}
value={valueInput}
onChange={(event) => {
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={
<CloseButton
aria-label="Clear input"
onClick={() => setValueInput("")}
style={{
display: valueInput ? undefined : "none",
}}
/>
}
/>
</Box>
</Box>
<Box style={{ width: "220px" }}>
<Flex align={"center"} wrap={"wrap"} gap={"xs"}>
<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 {
<ScrollArea h={"12vh"}>
<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>
))}
</Flex>
</ScrollArea>
{selectedLines?.length > 0 ? (
<Button
fw={400}
className={classes.buttonControl}
variant="outline"
onClick={() => {
const lines = station.lines.filter(
(line) =>
!line?.userOpenCLI ||
line?.userOpenCLI === user?.userName
);
lines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
@ -693,53 +624,170 @@ const BottomToolBar = ({
});
});
setSelectedLines([]);
}}
>
Clear
</Button>
) : (
""
)}
</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>
<Input
ref={inputRef}
style={{
width: "30vw",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}}
placeholder={"Send command to port(s)"}
value={valueInput}
onChange={(event) => {
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={
<CloseButton
aria-label="Clear input"
onClick={() => setValueInput("")}
style={{
display: valueInput ? undefined : "none",
}}
/>
}
}}
/>
<ButtonDPELP
socket={socket}
selectedLines={selectedLines}
isDisable={isDisable || selectedLines.length === 0}
onClick={() => {
// setSelectedLines([]);
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" ps={"xs"}>
<DrawerAPCControl socket={socket} stationAPI={station} />
</Tabs.Panel>
<Tabs.Panel value="switch" ps={"xs"}>
<DrawerSwitchControl socket={socket} stationAPI={station} />
</Tabs.Panel>
</Tabs>
</Grid.Col>
<Grid.Col span={1}></Grid.Col>
</Grid>
</Box>
/>
</Box>
</Box>
<Box style={{ width: "220px" }}>
<Flex align={"center"} wrap={"wrap"} gap={"xs"}>
<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 {
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([]);
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" ps={"xs"}>
<DrawerAPCControl socket={socket} stationAPI={station} />
</Tabs.Panel>
<Tabs.Panel value="switch" ps={"xs"}>
<DrawerSwitchControl socket={socket} stationAPI={station} />
</Tabs.Panel>
</Tabs>
</Grid.Col>
<Grid.Col span={1}></Grid.Col>
</Grid>
</Box>
</motion.div>
{/* Drawer Scenario để Add/Edit */}

View File

@ -2,7 +2,6 @@ import {
ActionIcon,
Avatar,
Box,
Button,
Flex,
Group,
Menu,
@ -296,7 +295,7 @@ export default function DraggableTabs({
<Tooltip
withArrow
label={usersConnecting.map((el) => (
<Text key={el.userId}>{el.userName}</Text>
<Text key={el.userId || el.id}>{el.userName}</Text>
))}
>
<Avatar radius="xl" me={"sm"}>

View File

@ -105,6 +105,8 @@ export type TLine = {
export type TUser = {
userId: number;
userName: string;
id: number;
email: string;
};
export type APCProps = {