Fix giao diện select scebario và modal add/edit
This commit is contained in:
parent
3fedaa33de
commit
b5bb90ca4e
|
|
@ -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<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;
|
||||
|
||||
// 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 (
|
||||
<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,
|
||||
|
|
@ -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(() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenDrawerScenario(true);
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
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 ? (
|
||||
<Grid gutter="md" style={{ margin: 0 }}>
|
||||
<Grid
|
||||
gutter="md"
|
||||
style={{ margin: 0, overflow: "visible", position: "relative" }}
|
||||
>
|
||||
{scenarios.map((scenario, index) => (
|
||||
<Grid.Col key={scenario.id} span={6}>
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
height: "100%",
|
||||
}}
|
||||
className={classes.scenarioCard}
|
||||
>
|
||||
<Flex direction="column" gap="xs" h="100%">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text fw={600} size="md" lineClamp={1}>
|
||||
{scenario.title}
|
||||
</Text>
|
||||
<Badge color="blue" variant="light">
|
||||
#{index + 1}
|
||||
</Badge>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="xs" wrap="wrap">
|
||||
<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">
|
||||
{JSON.parse(scenario.body || "[]").length} steps
|
||||
</Badge>
|
||||
</Flex>
|
||||
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
lineClamp={2}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{JSON.parse(scenario.body || "[]")
|
||||
.slice(0, 2)
|
||||
.map((step: { send: string }) => step.send)
|
||||
.join(" → ")}
|
||||
{JSON.parse(scenario.body || "[]").length > 2 &&
|
||||
" ..."}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="green"
|
||||
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);
|
||||
|
||||
// Run scenario cho các line được chọn
|
||||
selectedLines
|
||||
.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)
|
||||
.forEach((el) => {
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(el, {
|
||||
scenario: scenario,
|
||||
})
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Run Scenario
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<ScenarioCard
|
||||
key={scenario.id}
|
||||
scenario={scenario}
|
||||
index={index}
|
||||
isDisable={isDisable}
|
||||
selectedLines={selectedLines}
|
||||
user={user}
|
||||
socket={socket}
|
||||
setOpenScenarioModal={setOpenScenarioModal}
|
||||
setIsDisable={setIsDisable}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
|
|
@ -350,8 +524,7 @@ const BottomToolBar = ({
|
|||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor:
|
||||
activeTabBottom === "apc" ? "#c8d9fd" : "",
|
||||
backgroundColor: activeTabBottom === "apc" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
|
|
@ -376,33 +549,24 @@ const BottomToolBar = ({
|
|||
|
||||
<Tabs.Panel value="command" p={"xs"}>
|
||||
<Flex justify={"space-between"}>
|
||||
<Box>
|
||||
<ScrollArea h={"12vh"}>
|
||||
<Flex wrap={"wrap"} gap={"8px"} w={"420px"}>
|
||||
<ScrollArea h={"15vh"}>
|
||||
<Flex wrap={"wrap"} gap={"xs"} w={"400px"}>
|
||||
{selectedLines.map((el) => (
|
||||
<Box
|
||||
key={el.id}
|
||||
style={{
|
||||
position: "relative",
|
||||
padding: "4px 6px",
|
||||
height: "26px",
|
||||
width: "60px",
|
||||
paddingLeft: "4px",
|
||||
height: "30px",
|
||||
width: "80px",
|
||||
backgroundColor: "#d4e3ff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
{/* Close button góc trên phải */}
|
||||
<Flex align={"center"} justify={"center"} gap={"4px"}>
|
||||
<Text fz={"12px"}>Line {el.lineNumber}</Text>
|
||||
<CloseButton
|
||||
size="xs"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-4px",
|
||||
right: "-6px",
|
||||
minWidth: "18px",
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
zIndex: 10,
|
||||
}}
|
||||
style={{ minWidth: "24px" }}
|
||||
aria-label="Clear input"
|
||||
onClick={() => {
|
||||
setSelectedLines(
|
||||
selectedLines.filter(
|
||||
|
|
@ -415,45 +579,11 @@ const BottomToolBar = ({
|
|||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<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,
|
||||
stationId: line.stationId || line.station_id,
|
||||
});
|
||||
});
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box pl={"md"} pr={"md"}>
|
||||
<Flex justify={"space-between"} mb={"xs"}>
|
||||
<Flex></Flex>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -218,7 +218,10 @@ function DrawerScenario({
|
|||
justifyContent: "center",
|
||||
backdropFilter: "blur(3px)",
|
||||
}}
|
||||
onClick={handleClose}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
Loading…
Reference in New Issue