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 perPage = request.input('per_page', 10)
|
||||||
const page = request.input('page', 1)
|
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)
|
const scenarios = await query.orderBy('scenarios.created_at', 'asc').paginate(page, perPage)
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
|
@ -80,10 +80,16 @@ export default class ScenariosController {
|
||||||
|
|
||||||
await trx.commit()
|
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({
|
return response.ok({
|
||||||
status: true,
|
status: true,
|
||||||
message: 'Scenario created successfully',
|
message: 'Scenario created successfully',
|
||||||
data: scenario,
|
data: resScenario,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await trx.rollback()
|
await trx.rollback()
|
||||||
|
|
@ -153,7 +159,17 @@ export default class ScenariosController {
|
||||||
scenario.merge(payload)
|
scenario.merge(payload)
|
||||||
await scenario.save()
|
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) {
|
} catch (error) {
|
||||||
return response.internalServerError({
|
return response.internalServerError({
|
||||||
status: false,
|
status: false,
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,20 @@ const Login = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsSubmit(false);
|
setIsSubmit(false);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
notifications.show({
|
||||||
|
title: "Error",
|
||||||
|
message: error?.response?.data?.message,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Login fail, please try again!",
|
message: "Login fail, please try again!",
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,8 @@ import {
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
Card,
|
|
||||||
Badge,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import classes from "./Component.module.css";
|
import classes from "./Component.module.css";
|
||||||
import type {
|
import type {
|
||||||
IScenario,
|
IScenario,
|
||||||
|
|
@ -19,7 +17,6 @@ import type {
|
||||||
TCategories,
|
TCategories,
|
||||||
TLine,
|
TLine,
|
||||||
TStation,
|
TStation,
|
||||||
TUser,
|
|
||||||
} from "../untils/types";
|
} from "../untils/types";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
|
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
|
||||||
|
|
@ -28,13 +25,9 @@ import { DrawerAPCControl, DrawerSwitchControl } from "./Drawer/DrawerControl";
|
||||||
import DrawerScenario from "./Modal/ModalScenario";
|
import DrawerScenario from "./Modal/ModalScenario";
|
||||||
import { isJsonString } from "../untils/helper";
|
import { isJsonString } from "../untils/helper";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import {
|
import { IconCaretDown, IconCaretUp } from "@tabler/icons-react";
|
||||||
IconCaretDown,
|
|
||||||
IconCaretUp,
|
|
||||||
IconPlayerPlay,
|
|
||||||
IconPlus,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import InputHistory from "./InputHistory";
|
import InputHistory from "./InputHistory";
|
||||||
|
import ModalRunScenario from "./Modal/ModalRunScenario";
|
||||||
|
|
||||||
interface TabsProps {
|
interface TabsProps {
|
||||||
selectedLines: TLine[];
|
selectedLines: TLine[];
|
||||||
|
|
@ -58,318 +51,6 @@ interface TabsProps {
|
||||||
listCategories: TCategories[];
|
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 = ({
|
const BottomToolBar = ({
|
||||||
selectedLines,
|
selectedLines,
|
||||||
socket,
|
socket,
|
||||||
|
|
@ -948,129 +629,20 @@ const BottomToolBar = ({
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{openScenarioModal && (
|
<ModalRunScenario
|
||||||
<div
|
open={openScenarioModal}
|
||||||
style={{
|
setOpen={setOpenScenarioModal}
|
||||||
position: "fixed",
|
scenarios={scenarios}
|
||||||
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}
|
isDisable={isDisable}
|
||||||
selectedLines={selectedLines}
|
selectedLines={selectedLines}
|
||||||
user={user}
|
user={user}
|
||||||
socket={socket}
|
socket={socket}
|
||||||
setOpenScenarioModal={setOpenScenarioModal}
|
|
||||||
setIsDisable={setIsDisable}
|
setIsDisable={setIsDisable}
|
||||||
station={station}
|
station={station}
|
||||||
|
setOpenEdit={setOpenDrawerScenario}
|
||||||
|
listBrands={listBrands}
|
||||||
|
listCategories={listCategories}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Drawer Scenario để Add/Edit */}
|
{/* Drawer Scenario để Add/Edit */}
|
||||||
<DrawerScenario
|
<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
|
isEdit ? { ...payload, id: dataScenario?.id } : payload
|
||||||
);
|
);
|
||||||
if (res.data.status === true) {
|
if (res.data.status === true) {
|
||||||
const scenario = res.data.data;
|
const scenario = res.data.data[0];
|
||||||
setScenarios((pre) =>
|
setScenarios((pre) =>
|
||||||
isEdit
|
isEdit
|
||||||
? pre.map((el) =>
|
? pre.map((el) =>
|
||||||
|
|
@ -181,11 +181,19 @@ function ModalScenario({
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
notifications.show({
|
||||||
|
title: "Error",
|
||||||
|
message: error?.response?.data?.message,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to create scenario, please try again!",
|
message: "Failed to create scenario, please try again!",
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmit(false);
|
setIsSubmit(false);
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +302,7 @@ function ModalScenario({
|
||||||
{/* Sidebar - List Scenarios */}
|
{/* Sidebar - List Scenarios */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
width: "200px",
|
width: "250px",
|
||||||
borderRight: "1px solid #e9ecef",
|
borderRight: "1px solid #e9ecef",
|
||||||
paddingRight: "10px",
|
paddingRight: "10px",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
|
@ -350,7 +358,7 @@ function ModalScenario({
|
||||||
"categoryId",
|
"categoryId",
|
||||||
scenario.category_id
|
scenario.category_id
|
||||||
? scenario.category_id.toString()
|
? scenario.category_id.toString()
|
||||||
: scenario.categoryId.toString()
|
: scenario.categoryId?.toString()
|
||||||
);
|
);
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
"series",
|
"series",
|
||||||
|
|
@ -502,7 +510,6 @@ function ModalScenario({
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
form.setFieldValue("note", e.target.value)
|
form.setFieldValue("note", e.target.value)
|
||||||
}
|
}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6}>
|
<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 { IconRowInsertTop, IconX } from "@tabler/icons-react";
|
||||||
import classes from "./Scenario.module.css";
|
import classes from "./Scenario.module.css";
|
||||||
import { numberOnly } from "../../../untils/helper";
|
import { numberOnly } from "../../../untils/helper";
|
||||||
|
import type { IBodyScenario } from "../../../untils/types";
|
||||||
|
import type { UseFormReturnType } from "@mantine/form";
|
||||||
|
|
||||||
interface IPayload {
|
interface IPayload {
|
||||||
element: any;
|
element: IBodyScenario;
|
||||||
i: any;
|
i: number;
|
||||||
form: any;
|
form: UseFormReturnType<
|
||||||
deleteRow: any;
|
{
|
||||||
addRowUnder: any;
|
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) => {
|
const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
|
||||||
|
|
@ -28,7 +63,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
|
||||||
<IconGripVertical />
|
<IconGripVertical />
|
||||||
</Box> */}
|
</Box> */}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{ width: "250px" }}
|
style={{ width: "300px" }}
|
||||||
value={element.expect}
|
value={element.expect}
|
||||||
placeholder="Expect previous output"
|
placeholder="Expect previous output"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -49,7 +84,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{ width: "250px" }}
|
style={{ width: "350px" }}
|
||||||
value={element.send}
|
value={element.send}
|
||||||
placeholder="Command send"
|
placeholder="Command send"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = xtermRef.current;
|
const el = xtermRef.current;
|
||||||
if (el) {
|
if (el && miniSize) {
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
// tắt pointer events để terminal không bắt wheel/click nữa
|
// tắt pointer events để terminal không bắt wheel/click nữa
|
||||||
el.style.pointerEvents = "none";
|
el.style.pointerEvents = "none";
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,10 @@ export type IScenario = {
|
||||||
send_result: boolean;
|
send_result: boolean;
|
||||||
brandId: number;
|
brandId: number;
|
||||||
brand_id?: number;
|
brand_id?: number;
|
||||||
|
brand?: TBrands;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
category_id?: number;
|
category_id?: number;
|
||||||
|
category?: TCategories;
|
||||||
note: string;
|
note: string;
|
||||||
series: string;
|
series: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue