Update modal run scenario
This commit is contained in:
parent
2729feed4e
commit
76659b3e68
|
|
@ -12,7 +12,7 @@ export default class ScenariosController {
|
|||
const perPage = request.input('per_page', 10)
|
||||
const page = request.input('page', 1)
|
||||
|
||||
const query = Scenario.query()
|
||||
const query = Scenario.query().preload('brand').preload('category')
|
||||
|
||||
const scenarios = await query.orderBy('scenarios.created_at', 'asc').paginate(page, perPage)
|
||||
return response.ok({
|
||||
|
|
@ -80,10 +80,16 @@ export default class ScenariosController {
|
|||
|
||||
await trx.commit()
|
||||
|
||||
// Lấy lại station kèm lines
|
||||
const resScenario = await Scenario.query()
|
||||
.where('id', scenario.id)
|
||||
.preload('brand')
|
||||
.preload('category')
|
||||
|
||||
return response.ok({
|
||||
status: true,
|
||||
message: 'Scenario created successfully',
|
||||
data: scenario,
|
||||
data: resScenario,
|
||||
})
|
||||
} catch (error) {
|
||||
await trx.rollback()
|
||||
|
|
@ -153,7 +159,17 @@ export default class ScenariosController {
|
|||
scenario.merge(payload)
|
||||
await scenario.save()
|
||||
|
||||
return response.ok({ status: true, message: 'Scenario updated successfully', data: scenario })
|
||||
// Lấy lại station kèm lines
|
||||
const resScenario = await Scenario.query()
|
||||
.where('id', scenario.id)
|
||||
.preload('brand')
|
||||
.preload('category')
|
||||
|
||||
return response.ok({
|
||||
status: true,
|
||||
message: 'Scenario updated successfully',
|
||||
data: resScenario,
|
||||
})
|
||||
} catch (error) {
|
||||
return response.internalServerError({
|
||||
status: false,
|
||||
|
|
|
|||
|
|
@ -58,11 +58,19 @@ const Login = () => {
|
|||
} catch (error) {
|
||||
setIsSubmit(false);
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Login fail, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
if (axios.isAxiosError(error)) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: error?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Login fail, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ import {
|
|||
ScrollArea,
|
||||
Tabs,
|
||||
Text,
|
||||
Card,
|
||||
Badge,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import classes from "./Component.module.css";
|
||||
import type {
|
||||
IScenario,
|
||||
|
|
@ -19,7 +17,6 @@ import type {
|
|||
TCategories,
|
||||
TLine,
|
||||
TStation,
|
||||
TUser,
|
||||
} from "../untils/types";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
|
||||
|
|
@ -28,13 +25,9 @@ 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 { IconCaretDown, IconCaretUp } from "@tabler/icons-react";
|
||||
import InputHistory from "./InputHistory";
|
||||
import ModalRunScenario from "./Modal/ModalRunScenario";
|
||||
|
||||
interface TabsProps {
|
||||
selectedLines: TLine[];
|
||||
|
|
@ -58,318 +51,6 @@ interface TabsProps {
|
|||
listCategories: TCategories[];
|
||||
}
|
||||
|
||||
// Component cho từng Scenario Card
|
||||
const ScenarioCard = ({
|
||||
scenario,
|
||||
index,
|
||||
isDisable,
|
||||
selectedLines,
|
||||
user,
|
||||
socket,
|
||||
setOpenScenarioModal,
|
||||
setIsDisable,
|
||||
station,
|
||||
}: {
|
||||
scenario: IScenario;
|
||||
index: number;
|
||||
isDisable: boolean;
|
||||
selectedLines: TLine[];
|
||||
user: TUser;
|
||||
socket: Socket | null;
|
||||
setOpenScenarioModal: (value: boolean) => void;
|
||||
setIsDisable: (value: boolean) => void;
|
||||
station: TStation;
|
||||
}) => {
|
||||
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);
|
||||
|
||||
if (scenario?.isReboot || scenario?.is_reboot) {
|
||||
const lineApc1 = selectedLines
|
||||
?.filter(
|
||||
(el) =>
|
||||
el.outlet &&
|
||||
Number(el.outlet) > 0 &&
|
||||
(el.apcName === "apc_1" || el.apc_name === "apc_1")
|
||||
)
|
||||
?.map((el) => el.outlet);
|
||||
const lineApc2 = selectedLines
|
||||
?.filter(
|
||||
(el) =>
|
||||
el.outlet &&
|
||||
Number(el.outlet) > 0 &&
|
||||
(el.apcName === "apc_2" || el.apc_name === "apc_2")
|
||||
)
|
||||
?.map((el) => el.outlet);
|
||||
if (lineApc1.length > 0)
|
||||
socket?.emit("control_apc", {
|
||||
outletNumbers: lineApc1,
|
||||
station: { ...station, lines: [] },
|
||||
action: "restart",
|
||||
apcName: "apc_1",
|
||||
});
|
||||
if (lineApc2.length > 0)
|
||||
socket?.emit("control_apc", {
|
||||
outletNumbers: lineApc2,
|
||||
station: { ...station, lines: [] },
|
||||
action: "restart",
|
||||
apcName: "apc_2",
|
||||
});
|
||||
}
|
||||
if (scenario?.send_result)
|
||||
socket?.emit("run_all_dpelp", {
|
||||
lineIds: selectedLines?.map((el) => el.id),
|
||||
stationName: station.name,
|
||||
stationId: Number(station.id),
|
||||
});
|
||||
|
||||
selectedLines
|
||||
.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)
|
||||
.forEach((el) => {
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(el, {
|
||||
scenario: {
|
||||
...scenario,
|
||||
isReboot:
|
||||
typeof scenario?.isReboot !== "undefined"
|
||||
? scenario?.isReboot
|
||||
: scenario?.is_reboot,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
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,
|
||||
|
|
@ -948,129 +629,20 @@ const BottomToolBar = ({
|
|||
</Box>
|
||||
</motion.div>
|
||||
|
||||
{openScenarioModal && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
zIndex: 99998,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backdropFilter: "blur(3px)",
|
||||
}}
|
||||
onClick={() => setOpenScenarioModal(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "12px",
|
||||
maxWidth: "90vw",
|
||||
width: "90%",
|
||||
height: "85vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
||||
overflowY: "auto",
|
||||
overflowX: "visible",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="lg"
|
||||
style={{
|
||||
borderBottom: "1px solid #e9ecef",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Text fw={700} size="xl">
|
||||
🎯 Select Scenario to Run
|
||||
</Text>
|
||||
<Flex gap="md" align="center">
|
||||
<Button
|
||||
leftSection={<IconPlus size={16} />}
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenDrawerScenario(true);
|
||||
}}
|
||||
>
|
||||
Add/Edit Scenario
|
||||
</Button>
|
||||
<CloseButton
|
||||
size="lg"
|
||||
onClick={() => setOpenScenarioModal(false)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
padding: "20px",
|
||||
overflowY: "auto",
|
||||
overflowX: "visible",
|
||||
flex: 1,
|
||||
position: "relative",
|
||||
}}
|
||||
className={classes.hideScrollBar}
|
||||
>
|
||||
{scenarios.length > 0 ? (
|
||||
<Grid
|
||||
gutter="md"
|
||||
style={{
|
||||
margin: 0,
|
||||
overflow: "visible",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{scenarios.map((scenario, index) => (
|
||||
<ScenarioCard
|
||||
key={scenario.id}
|
||||
scenario={scenario}
|
||||
index={index}
|
||||
isDisable={isDisable}
|
||||
selectedLines={selectedLines}
|
||||
user={user}
|
||||
socket={socket}
|
||||
setOpenScenarioModal={setOpenScenarioModal}
|
||||
setIsDisable={setIsDisable}
|
||||
station={station}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ minHeight: "300px" }}
|
||||
gap="md"
|
||||
>
|
||||
<Text size="xl" c="dimmed">
|
||||
📋
|
||||
</Text>
|
||||
<Text ta="center" c="dimmed" size="lg">
|
||||
No scenarios available
|
||||
</Text>
|
||||
<Text ta="center" c="dimmed" size="sm">
|
||||
Please create a new scenario to get started
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ModalRunScenario
|
||||
open={openScenarioModal}
|
||||
setOpen={setOpenScenarioModal}
|
||||
scenarios={scenarios}
|
||||
isDisable={isDisable}
|
||||
selectedLines={selectedLines}
|
||||
user={user}
|
||||
socket={socket}
|
||||
setIsDisable={setIsDisable}
|
||||
station={station}
|
||||
setOpenEdit={setOpenDrawerScenario}
|
||||
listBrands={listBrands}
|
||||
listCategories={listCategories}
|
||||
/>
|
||||
|
||||
{/* Drawer Scenario để Add/Edit */}
|
||||
<DrawerScenario
|
||||
|
|
|
|||
|
|
@ -0,0 +1,560 @@
|
|||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
CloseButton,
|
||||
Flex,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Table,
|
||||
TagsInput,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { IconPlayerPlay, IconPlus, IconX } from "@tabler/icons-react";
|
||||
import classes from "../Component.module.css";
|
||||
import type {
|
||||
IScenario,
|
||||
TBrands,
|
||||
TCategories,
|
||||
TLine,
|
||||
TStation,
|
||||
TUser,
|
||||
} from "../../untils/types";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { useState } from "react";
|
||||
|
||||
const ModalRunScenario = ({
|
||||
open,
|
||||
setOpen,
|
||||
scenarios,
|
||||
isDisable,
|
||||
selectedLines,
|
||||
user,
|
||||
socket,
|
||||
setIsDisable,
|
||||
station,
|
||||
setOpenEdit,
|
||||
listBrands,
|
||||
listCategories,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (value: React.SetStateAction<boolean>) => void;
|
||||
setOpenEdit: (value: React.SetStateAction<boolean>) => void;
|
||||
scenarios: IScenario[];
|
||||
isDisable: boolean;
|
||||
selectedLines: TLine[];
|
||||
user: TUser;
|
||||
socket: Socket | null;
|
||||
setIsDisable: (value: boolean) => void;
|
||||
station: TStation;
|
||||
listBrands: TBrands[];
|
||||
listCategories: TCategories[];
|
||||
}) => {
|
||||
const [optionBrand, setOptionBrand] = useState("");
|
||||
const [optionCategory, setOptionCategory] = useState("");
|
||||
const [inputSeries, setInputSeries] = useState<string[]>([]);
|
||||
const [inputTitle, setInputTitle] = useState("");
|
||||
|
||||
const filteredScenarios = () => {
|
||||
let listScenarios = [...scenarios];
|
||||
if (inputTitle.trim())
|
||||
listScenarios = listScenarios.filter((el) =>
|
||||
el.title?.toLowerCase()?.includes(inputTitle?.toLowerCase())
|
||||
);
|
||||
if (optionBrand)
|
||||
listScenarios = listScenarios.filter(
|
||||
(el) =>
|
||||
el.brandId === Number(optionBrand) ||
|
||||
el.brand_id === Number(optionBrand)
|
||||
);
|
||||
if (optionCategory)
|
||||
listScenarios = listScenarios.filter(
|
||||
(el) =>
|
||||
el.categoryId === Number(optionCategory) ||
|
||||
el.category_id === Number(optionCategory)
|
||||
);
|
||||
if (inputSeries.length)
|
||||
listScenarios = listScenarios.filter((el) => {
|
||||
const series: string[] = JSON.parse(el.series) || [];
|
||||
if (!series.length) return false;
|
||||
|
||||
return series.some((serial) =>
|
||||
inputSeries.some((input) =>
|
||||
serial.toLowerCase().includes(input.toLowerCase())
|
||||
)
|
||||
);
|
||||
});
|
||||
return listScenarios;
|
||||
};
|
||||
return (
|
||||
open && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
zIndex: 98,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backdropFilter: "blur(3px)",
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "12px",
|
||||
maxWidth: "90vw",
|
||||
width: "90%",
|
||||
height: "85vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
||||
overflowY: "auto",
|
||||
overflowX: "visible",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="lg"
|
||||
style={{
|
||||
borderBottom: "1px solid #e9ecef",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Text fw={700} size="xl">
|
||||
🎯 Select Scenario to Run
|
||||
</Text>
|
||||
<Flex gap="md" align="center">
|
||||
<Button
|
||||
leftSection={<IconPlus size={16} />}
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenEdit(true);
|
||||
}}
|
||||
>
|
||||
Add/Edit Scenario
|
||||
</Button>
|
||||
<CloseButton size="lg" onClick={() => setOpen(false)} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex style={{ paddingLeft: "20px" }} gap={"20px"}>
|
||||
<Box style={{ width: "155px" }}>
|
||||
<Select
|
||||
label="Brand"
|
||||
placeholder="Select Brand"
|
||||
data={listBrands?.map((el) => ({
|
||||
value: el.id.toString(),
|
||||
label: el.name,
|
||||
}))}
|
||||
value={optionBrand || null}
|
||||
onChange={(value) => setOptionBrand(value || "")}
|
||||
clearable
|
||||
/>
|
||||
</Box>
|
||||
<Box style={{ width: "150px" }}>
|
||||
<Select
|
||||
label="Category"
|
||||
placeholder="Select Category"
|
||||
data={listCategories?.map((el) => ({
|
||||
value: el.id.toString(),
|
||||
label: el.name,
|
||||
}))}
|
||||
value={optionCategory || null}
|
||||
onChange={(value) => setOptionCategory(value || "")}
|
||||
clearable
|
||||
/>
|
||||
</Box>
|
||||
<Box style={{ width: "31vw", maxWidth: "500px" }}>
|
||||
<TagsInput
|
||||
label="Series"
|
||||
placeholder="Enter Series"
|
||||
data={[]}
|
||||
value={inputSeries || []}
|
||||
onChange={(value: string[]) => setInputSeries(value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box style={{ marginLeft: "2vw" }}>
|
||||
<TextInput
|
||||
style={{ width: "250px" }}
|
||||
label="Title"
|
||||
placeholder="Search title"
|
||||
value={inputTitle}
|
||||
onChange={(event) => setInputTitle(event.currentTarget.value)}
|
||||
rightSection={
|
||||
inputTitle ? (
|
||||
<IconX
|
||||
size={14}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setInputTitle("")}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
padding: "20px",
|
||||
paddingTop: 0,
|
||||
overflowY: "auto",
|
||||
overflowX: "visible",
|
||||
flex: 1,
|
||||
position: "relative",
|
||||
}}
|
||||
className={classes.hideScrollBar}
|
||||
>
|
||||
<ScrollArea h={"60vh"} style={{ marginTop: "15px" }}>
|
||||
<Table
|
||||
stickyHeader
|
||||
striped
|
||||
highlightOnHover
|
||||
withRowBorders={true}
|
||||
withTableBorder={true}
|
||||
withColumnBorders={true}
|
||||
>
|
||||
<Table.Thead
|
||||
style={{
|
||||
top: 1,
|
||||
}}
|
||||
>
|
||||
<Table.Tr>
|
||||
<Table.Th
|
||||
style={{
|
||||
width: "170px",
|
||||
textAlign: "center",
|
||||
backgroundColor: "#94c6ff",
|
||||
}}
|
||||
>
|
||||
Brand
|
||||
</Table.Th>
|
||||
<Table.Th
|
||||
style={{
|
||||
width: "170px",
|
||||
textAlign: "center",
|
||||
backgroundColor: "#94c6ff",
|
||||
}}
|
||||
>
|
||||
Category
|
||||
</Table.Th>
|
||||
<Table.Th
|
||||
style={{
|
||||
width: "550px",
|
||||
textAlign: "center",
|
||||
backgroundColor: "#94c6ff",
|
||||
}}
|
||||
>
|
||||
Series
|
||||
</Table.Th>
|
||||
<Table.Th
|
||||
style={{
|
||||
width: "340px",
|
||||
textAlign: "center",
|
||||
backgroundColor: "#94c6ff",
|
||||
}}
|
||||
>
|
||||
Title
|
||||
</Table.Th>
|
||||
<Table.Th
|
||||
style={{
|
||||
textAlign: "center",
|
||||
backgroundColor: "#94c6ff",
|
||||
}}
|
||||
>
|
||||
Note
|
||||
</Table.Th>
|
||||
<Table.Th
|
||||
style={{
|
||||
width: "140px",
|
||||
textAlign: "center",
|
||||
backgroundColor: "#94c6ff",
|
||||
}}
|
||||
>
|
||||
Action
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{filteredScenarios().length > 0 ? (
|
||||
filteredScenarios().map((scenario, index) => (
|
||||
<Table.Tr key={scenario.id}>
|
||||
<Table.Td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Text>{scenario?.brand?.name || ""}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Text>{scenario?.category?.name || ""}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Flex wrap={"wrap"} gap={"4px"}>
|
||||
{scenario.series
|
||||
? JSON.parse(scenario.series)?.map(
|
||||
(el: string, i: number) => (
|
||||
<Badge
|
||||
key={i}
|
||||
size="md"
|
||||
color="orange"
|
||||
variant="dot"
|
||||
>
|
||||
{el}
|
||||
</Badge>
|
||||
)
|
||||
)
|
||||
: ""}
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text>{scenario.title}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text>{scenario.note || ""}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
openDelay={0}
|
||||
position="left-start"
|
||||
style={{
|
||||
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: 99,
|
||||
minHeight: "200px",
|
||||
maxHeight: "450px",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
className={classes.hideScrollBar}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
label={
|
||||
<Flex direction="column" gap="xs">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Box>
|
||||
<Text c={"dark"} fw={700} size="md">
|
||||
{scenario.title}
|
||||
</Text>
|
||||
</Box>
|
||||
<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}s
|
||||
</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" fw={600} mt="xs" c="dimmed">
|
||||
Commands Preview:
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
background: "#f8f9fa",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
maxHeight: "150px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{(JSON.parse(scenario?.body) || [])
|
||||
.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>
|
||||
)
|
||||
)}
|
||||
{(JSON.parse(scenario?.body) || []).length >
|
||||
5 && (
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
mt="xs"
|
||||
>
|
||||
... and{" "}
|
||||
{(JSON.parse(scenario?.body) || [])
|
||||
.length - 5}{" "}
|
||||
more
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
style={{ width: "100px" }}
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
leftSection={<IconPlayerPlay size={16} />}
|
||||
disabled={
|
||||
isDisable ||
|
||||
selectedLines.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
).length === 0
|
||||
}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 5000);
|
||||
|
||||
if (scenario?.isReboot || scenario?.is_reboot) {
|
||||
const lineApc1 = selectedLines
|
||||
?.filter(
|
||||
(el) =>
|
||||
el.outlet &&
|
||||
Number(el.outlet) > 0 &&
|
||||
(el.apcName === "apc_1" ||
|
||||
el.apc_name === "apc_1")
|
||||
)
|
||||
?.map((el) => el.outlet);
|
||||
const lineApc2 = selectedLines
|
||||
?.filter(
|
||||
(el) =>
|
||||
el.outlet &&
|
||||
Number(el.outlet) > 0 &&
|
||||
(el.apcName === "apc_2" ||
|
||||
el.apc_name === "apc_2")
|
||||
)
|
||||
?.map((el) => el.outlet);
|
||||
if (lineApc1.length > 0)
|
||||
socket?.emit("control_apc", {
|
||||
outletNumbers: lineApc1,
|
||||
station: { ...station, lines: [] },
|
||||
action: "restart",
|
||||
apcName: "apc_1",
|
||||
});
|
||||
if (lineApc2.length > 0)
|
||||
socket?.emit("control_apc", {
|
||||
outletNumbers: lineApc2,
|
||||
station: { ...station, lines: [] },
|
||||
action: "restart",
|
||||
apcName: "apc_2",
|
||||
});
|
||||
}
|
||||
if (scenario?.send_result)
|
||||
socket?.emit("run_all_dpelp", {
|
||||
lineIds: selectedLines?.map((el) => el.id),
|
||||
stationName: station.name,
|
||||
stationId: Number(station.id),
|
||||
});
|
||||
|
||||
selectedLines
|
||||
.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)
|
||||
.forEach((el) => {
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(el, {
|
||||
scenario: {
|
||||
...scenario,
|
||||
isReboot:
|
||||
typeof scenario?.isReboot !==
|
||||
"undefined"
|
||||
? scenario?.isReboot
|
||||
: scenario?.is_reboot,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={6}>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ minHeight: "300px" }}
|
||||
gap="md"
|
||||
>
|
||||
<Text size="xl" c="dimmed">
|
||||
📋
|
||||
</Text>
|
||||
<Text ta="center" c="dimmed" size="lg">
|
||||
No scenarios available
|
||||
</Text>
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalRunScenario;
|
||||
|
|
@ -156,7 +156,7 @@ function ModalScenario({
|
|||
isEdit ? { ...payload, id: dataScenario?.id } : payload
|
||||
);
|
||||
if (res.data.status === true) {
|
||||
const scenario = res.data.data;
|
||||
const scenario = res.data.data[0];
|
||||
setScenarios((pre) =>
|
||||
isEdit
|
||||
? pre.map((el) =>
|
||||
|
|
@ -181,11 +181,19 @@ function ModalScenario({
|
|||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Failed to create scenario, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
if (axios.isAxiosError(error)) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: error?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Failed to create scenario, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
|
|
@ -294,7 +302,7 @@ function ModalScenario({
|
|||
{/* Sidebar - List Scenarios */}
|
||||
<Box
|
||||
style={{
|
||||
width: "200px",
|
||||
width: "250px",
|
||||
borderRight: "1px solid #e9ecef",
|
||||
paddingRight: "10px",
|
||||
flexShrink: 0,
|
||||
|
|
@ -350,7 +358,7 @@ function ModalScenario({
|
|||
"categoryId",
|
||||
scenario.category_id
|
||||
? scenario.category_id.toString()
|
||||
: scenario.categoryId.toString()
|
||||
: scenario.categoryId?.toString()
|
||||
);
|
||||
form.setFieldValue(
|
||||
"series",
|
||||
|
|
@ -502,7 +510,6 @@ function ModalScenario({
|
|||
onChange={(e) =>
|
||||
form.setFieldValue("note", e.target.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,320 @@
|
|||
import type { Socket } from "socket.io-client";
|
||||
import type { IScenario, TLine, TStation, TUser } from "../../../untils/types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Badge, Box, Button, Card, Flex, Grid, Text } from "@mantine/core";
|
||||
import classes from "../../Component.module.css";
|
||||
import { IconPlayerPlay } from "@tabler/icons-react";
|
||||
|
||||
// Component cho từng Scenario Card
|
||||
const ScenarioCard = ({
|
||||
scenario,
|
||||
index,
|
||||
isDisable,
|
||||
selectedLines,
|
||||
user,
|
||||
socket,
|
||||
setOpenScenarioModal,
|
||||
setIsDisable,
|
||||
station,
|
||||
}: {
|
||||
scenario: IScenario;
|
||||
index: number;
|
||||
isDisable: boolean;
|
||||
selectedLines: TLine[];
|
||||
user: TUser;
|
||||
socket: Socket | null;
|
||||
setOpenScenarioModal: (value: boolean) => void;
|
||||
setIsDisable: (value: boolean) => void;
|
||||
station: TStation;
|
||||
}) => {
|
||||
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);
|
||||
|
||||
if (scenario?.isReboot || scenario?.is_reboot) {
|
||||
const lineApc1 = selectedLines
|
||||
?.filter(
|
||||
(el) =>
|
||||
el.outlet &&
|
||||
Number(el.outlet) > 0 &&
|
||||
(el.apcName === "apc_1" || el.apc_name === "apc_1")
|
||||
)
|
||||
?.map((el) => el.outlet);
|
||||
const lineApc2 = selectedLines
|
||||
?.filter(
|
||||
(el) =>
|
||||
el.outlet &&
|
||||
Number(el.outlet) > 0 &&
|
||||
(el.apcName === "apc_2" || el.apc_name === "apc_2")
|
||||
)
|
||||
?.map((el) => el.outlet);
|
||||
if (lineApc1.length > 0)
|
||||
socket?.emit("control_apc", {
|
||||
outletNumbers: lineApc1,
|
||||
station: { ...station, lines: [] },
|
||||
action: "restart",
|
||||
apcName: "apc_1",
|
||||
});
|
||||
if (lineApc2.length > 0)
|
||||
socket?.emit("control_apc", {
|
||||
outletNumbers: lineApc2,
|
||||
station: { ...station, lines: [] },
|
||||
action: "restart",
|
||||
apcName: "apc_2",
|
||||
});
|
||||
}
|
||||
if (scenario?.send_result)
|
||||
socket?.emit("run_all_dpelp", {
|
||||
lineIds: selectedLines?.map((el) => el.id),
|
||||
stationName: station.name,
|
||||
stationId: Number(station.id),
|
||||
});
|
||||
|
||||
selectedLines
|
||||
.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)
|
||||
.forEach((el) => {
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(el, {
|
||||
scenario: {
|
||||
...scenario,
|
||||
isReboot:
|
||||
typeof scenario?.isReboot !== "undefined"
|
||||
? scenario?.isReboot
|
||||
: scenario?.is_reboot,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioCard;
|
||||
|
|
@ -2,13 +2,48 @@ import { Table, TextInput } from "@mantine/core";
|
|||
import { IconRowInsertTop, IconX } from "@tabler/icons-react";
|
||||
import classes from "./Scenario.module.css";
|
||||
import { numberOnly } from "../../../untils/helper";
|
||||
import type { IBodyScenario } from "../../../untils/types";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
|
||||
interface IPayload {
|
||||
element: any;
|
||||
i: any;
|
||||
form: any;
|
||||
deleteRow: any;
|
||||
addRowUnder: any;
|
||||
element: IBodyScenario;
|
||||
i: number;
|
||||
form: UseFormReturnType<
|
||||
{
|
||||
title: string;
|
||||
body: IBodyScenario[];
|
||||
timeout: string;
|
||||
isReboot: boolean;
|
||||
send_result: boolean;
|
||||
note: string;
|
||||
series: string[];
|
||||
brandId: string;
|
||||
categoryId: string;
|
||||
},
|
||||
(values: {
|
||||
title: string;
|
||||
body: IBodyScenario[];
|
||||
timeout: string;
|
||||
isReboot: boolean;
|
||||
send_result: boolean;
|
||||
note: string;
|
||||
series: string[];
|
||||
brandId: string;
|
||||
categoryId: string;
|
||||
}) => {
|
||||
title: string;
|
||||
body: IBodyScenario[];
|
||||
timeout: string;
|
||||
isReboot: boolean;
|
||||
send_result: boolean;
|
||||
note: string;
|
||||
series: string[];
|
||||
brandId: string;
|
||||
categoryId: string;
|
||||
}
|
||||
>;
|
||||
deleteRow: (index: number) => void;
|
||||
addRowUnder: (index: number) => void;
|
||||
}
|
||||
|
||||
const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
|
||||
|
|
@ -28,7 +63,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
|
|||
<IconGripVertical />
|
||||
</Box> */}
|
||||
<TextInput
|
||||
style={{ width: "250px" }}
|
||||
style={{ width: "300px" }}
|
||||
value={element.expect}
|
||||
placeholder="Expect previous output"
|
||||
onChange={(e) => {
|
||||
|
|
@ -49,7 +84,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
|
|||
</Table.Td>
|
||||
<Table.Td>
|
||||
<TextInput
|
||||
style={{ width: "250px" }}
|
||||
style={{ width: "350px" }}
|
||||
value={element.send}
|
||||
placeholder="Command send"
|
||||
onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
const el = xtermRef.current;
|
||||
if (el) {
|
||||
if (el && miniSize) {
|
||||
if (!isSelected) {
|
||||
// tắt pointer events để terminal không bắt wheel/click nữa
|
||||
el.style.pointerEvents = "none";
|
||||
|
|
|
|||
|
|
@ -167,8 +167,10 @@ export type IScenario = {
|
|||
send_result: boolean;
|
||||
brandId: number;
|
||||
brand_id?: number;
|
||||
brand?: TBrands;
|
||||
categoryId: number;
|
||||
category_id?: number;
|
||||
category?: TCategories;
|
||||
note: string;
|
||||
series: string;
|
||||
updated_at: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue