1242 lines
39 KiB
TypeScript
1242 lines
39 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
CloseButton,
|
|
Flex,
|
|
Grid,
|
|
Input,
|
|
Menu,
|
|
Modal,
|
|
ScrollArea,
|
|
Text,
|
|
Textarea,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import type {
|
|
IScenario,
|
|
SwitchPortsProps,
|
|
TDataTicket,
|
|
TextFSM,
|
|
TextTSMLicense,
|
|
THistoryTicket,
|
|
TLine,
|
|
TStation,
|
|
} from "../../untils/types";
|
|
import TerminalCLI from "../TerminalXTerm";
|
|
import type { Socket } from "socket.io-client";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
IconCircleCheckFilled,
|
|
IconCircleDot,
|
|
IconInfoCircle,
|
|
} from "@tabler/icons-react";
|
|
import { ButtonDPELP, ButtonScenario } from "../ButtonAction";
|
|
import CopyIcon from "../CopyIcon";
|
|
import moment from "moment";
|
|
import axios from "axios";
|
|
import { notifications } from "@mantine/notifications";
|
|
import classes from "../Component.module.css";
|
|
import { listBaudDefault } from "../../untils/constanst";
|
|
import { motion } from "motion/react";
|
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
|
|
|
const INIT_TICKET = {
|
|
description: "",
|
|
sn: "",
|
|
model: "",
|
|
station_id: 0,
|
|
history: "",
|
|
status: "open",
|
|
};
|
|
|
|
const ModalTerminal = ({
|
|
opened,
|
|
onClose,
|
|
line,
|
|
socket,
|
|
stationItem,
|
|
scenarios,
|
|
selectedLines,
|
|
}: {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
line: TLine | undefined;
|
|
socket: Socket | null;
|
|
stationItem: TStation | undefined;
|
|
scenarios: IScenario[];
|
|
selectedLines: TLine[];
|
|
}) => {
|
|
const user = useMemo(() => {
|
|
return localStorage.getItem("user") &&
|
|
typeof localStorage.getItem("user") === "string"
|
|
? JSON.parse(localStorage.getItem("user") || "")
|
|
: null;
|
|
}, []);
|
|
const [isDisable, setIsDisable] = useState<boolean>(false);
|
|
const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false);
|
|
const [listPorts, setListPorts] = useState<SwitchPortsProps[]>([]);
|
|
const [latestTicket, setLatestTicket] = useState<TDataTicket>(INIT_TICKET);
|
|
const [dataTicket, setDataTicket] = useState<TDataTicket>(INIT_TICKET);
|
|
const [valueBaud, setValueBaud] = useState<string>("");
|
|
const [valueIssue, setValueIssue] = useState<string>("");
|
|
const [dataTextfsm, setDataTextfsm] = useState<TextFSM[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (opened && line?.tickets && line?.tickets?.length > 0) {
|
|
const data = line?.tickets[0];
|
|
setLatestTicket(data);
|
|
setDataTicket({ ...data, description: "" });
|
|
} else {
|
|
setLatestTicket(INIT_TICKET);
|
|
setDataTicket(INIT_TICKET);
|
|
setValueBaud("");
|
|
}
|
|
}, [opened, line?.tickets]);
|
|
|
|
useEffect(() => {
|
|
if (opened && line?.latestScenario?.detectAI) {
|
|
const data =
|
|
line?.latestScenario?.detectAI?.issue &&
|
|
Array.isArray(line?.latestScenario?.detectAI?.issue)
|
|
? "- " + line?.latestScenario?.detectAI?.issue?.join("\n- ")
|
|
: "";
|
|
setValueIssue(data);
|
|
} else {
|
|
setValueIssue("");
|
|
}
|
|
}, [opened, line?.latestScenario?.detectAI]);
|
|
|
|
useEffect(() => {
|
|
if (opened && line?.data) {
|
|
const data = Array.isArray(line?.data) ? line?.data : [];
|
|
setDataTextfsm(data);
|
|
} else {
|
|
setDataTextfsm([]);
|
|
}
|
|
}, [opened, line?.data]);
|
|
|
|
useEffect(() => {
|
|
setListPorts([]);
|
|
}, [stationItem?.id]);
|
|
|
|
useEffect(() => {
|
|
socket?.on("switch_ports_status", (data) => {
|
|
if (data.stationId !== stationItem?.id) return;
|
|
if (data?.ports && data?.ports.length > 0)
|
|
setListPorts(data?.ports || []);
|
|
});
|
|
return () => {
|
|
socket?.off("switch_ports_status");
|
|
};
|
|
}, [socket, stationItem]);
|
|
|
|
const renderHistory = (data: TDataTicket) => {
|
|
const latest = JSON.parse(latestTicket?.history || "[]");
|
|
const list =
|
|
data?.history && data?.id
|
|
? JSON.parse(data?.history)
|
|
: latest?.length > 0
|
|
? [latest[0], latest[latest?.length - 1]]
|
|
: [];
|
|
return (
|
|
<div>
|
|
{list.reverse().map((item: THistoryTicket, index: number) => (
|
|
<div key={index}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
marginBottom: "4px",
|
|
}}
|
|
>
|
|
<Flex
|
|
style={{
|
|
gap: "4px",
|
|
color:
|
|
index === 0
|
|
? "#30d100"
|
|
: item?.status === "closed"
|
|
? "red"
|
|
: item?.status === "issue"
|
|
? "#fab005"
|
|
: "inherit",
|
|
}}
|
|
>
|
|
<Text style={{ fontSize: "14px", fontWeight: "bold" }} fw={600}>
|
|
{item?.status === "closed" ? "*" : ""}@{item?.userName}:
|
|
</Text>
|
|
|
|
<Text
|
|
style={{
|
|
fontSize: "14px",
|
|
}}
|
|
c={
|
|
index === 0
|
|
? "#30d100"
|
|
: item?.status === "closed"
|
|
? "red"
|
|
: item?.status === "issue"
|
|
? "#fab005"
|
|
: "dimmed"
|
|
}
|
|
fw={400}
|
|
>
|
|
{item?.description}
|
|
</Text>
|
|
</Flex>
|
|
<Text
|
|
style={{
|
|
fontSize: "14px",
|
|
}}
|
|
c="dimmed"
|
|
fw={500}
|
|
>
|
|
{moment(Number(item?.time)).format("HH:m DD/M")}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const handleCreate = async () => {
|
|
if (!dataTicket?.description.trim()) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Description is required",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
id: dataTicket.id || 0,
|
|
description: dataTicket.description.trim(),
|
|
model: line?.inventory?.pid.trim(),
|
|
sn: line?.inventory?.sn.trim(),
|
|
station_id: Number(stationItem?.id),
|
|
line_id: Number(line?.id),
|
|
status: "open",
|
|
userName: user?.userName,
|
|
userId: user?.id,
|
|
};
|
|
|
|
try {
|
|
const res = await axios.post(apiUrl + "api/ticket/create", payload);
|
|
if (res.status) {
|
|
// setLatestTicket(res.data);
|
|
// setDataTicket({ ...res.data, description: "" });
|
|
|
|
// notifications.show({
|
|
// title: 'Success',
|
|
// message: res.message,
|
|
// color: 'green',
|
|
// })
|
|
|
|
socket?.emit("update_ticket", {
|
|
lineId: Number(line?.id),
|
|
data: [res.data.data, ...(line?.tickets || [])],
|
|
stationId: Number(stationItem?.id),
|
|
});
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.log("Error create ticket", error);
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Failed to create ticket, please try again!",
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleUpdate = async (status: string) => {
|
|
if (!dataTicket?.description.trim()) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Description is required",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
id: dataTicket.id || 0,
|
|
description: dataTicket.description.trim()
|
|
? dataTicket.description.trim()
|
|
: status === "closed"
|
|
? "Closed"
|
|
: "",
|
|
model: dataTicket.model.trim(),
|
|
sn: dataTicket.sn.trim(),
|
|
station_id: Number(stationItem?.id),
|
|
line_id: Number(line?.id),
|
|
status: status,
|
|
userName: user?.userName,
|
|
userId: user?.id,
|
|
};
|
|
|
|
try {
|
|
const res = await axios.post(
|
|
`${apiUrl + "api/ticket/update" + "/" + dataTicket.id}`,
|
|
payload
|
|
);
|
|
if (res.status) {
|
|
// if (res?.data?.status !== "closed")
|
|
// setDataTicket({ ...res.data, description: "" });
|
|
// else
|
|
// setDataTicket({
|
|
// id: 0,
|
|
// description: "",
|
|
// model: latestTicket.model.trim(),
|
|
// sn: latestTicket.sn.trim(),
|
|
// station_id: latestTicket.station_id,
|
|
// history: "",
|
|
// status: "open",
|
|
// });
|
|
|
|
// notifications.show({
|
|
// title: 'Success',
|
|
// message: res.message,
|
|
// color: 'green',
|
|
// })
|
|
socket?.emit("update_ticket", {
|
|
lineId: Number(line?.id),
|
|
data: line?.tickets?.map((el) =>
|
|
el.id === dataTicket.id ? res.data.data : el
|
|
),
|
|
stationId: Number(stationItem?.id),
|
|
});
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.log("Error update ticket", error);
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Failed to update ticket, please try again!",
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
const controlApc = (action: string) => {
|
|
if (!line?.outlet) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Hasn't config outlet number",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
const apcName = line.apcName || line.apc_name;
|
|
if (!apcName) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Hasn't config apc",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
if (
|
|
(apcName === "apc_1" && !stationItem?.apc_1_ip) ||
|
|
(apcName === "apc_2" && !stationItem?.apc_2_ip)
|
|
) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Hasn't config apc ip",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
socket?.emit("control_apc", {
|
|
outletNumbers: [line.outlet],
|
|
station: stationItem,
|
|
action: action,
|
|
apcName: line.apcName || line.apc_name,
|
|
});
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 10000);
|
|
};
|
|
|
|
const controlSwitch = (action: string) => {
|
|
if (!line?.interface) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Hasn't config interface",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
socket?.emit("control_switch", {
|
|
ports: [line?.interface],
|
|
command: action,
|
|
station: { ...stationItem, lines: [] },
|
|
ip: stationItem?.switch_control_ip,
|
|
});
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 15000);
|
|
};
|
|
|
|
const findSwitchPort = (portName: string): SwitchPortsProps | null => {
|
|
if (listPorts?.length > 0) {
|
|
const port = listPorts.find(
|
|
(el) => normalizePortName(el.name) === normalizePortName(portName)
|
|
);
|
|
if (port) return port;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const normalizePortName = (port: string): string => {
|
|
if (!port) return "";
|
|
|
|
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
|
|
const match = port.match(/^([A-Za-z]+)([\d/]+)$/);
|
|
|
|
if (!match) return port;
|
|
|
|
const type = match[1]; // Fa, Gi, Te, etc.
|
|
const numbers = match[2]; // "0/1" / "0/0/1" / "0/0/2"
|
|
|
|
// Get the last part after slash
|
|
const parts = numbers.split("/");
|
|
const last = parts[parts.length - 1];
|
|
|
|
return `${type?.slice(0, 2)}${last}`;
|
|
};
|
|
|
|
const findDataShowVersion = () => {
|
|
const showVersion = dataTextfsm?.find(
|
|
(d) =>
|
|
d.command?.trim() === "show version" ||
|
|
d.command?.trim() === "sh version" ||
|
|
d.command?.trim() === "show ver" ||
|
|
d.command?.trim() === "sh ver"
|
|
);
|
|
return showVersion?.textfsm && showVersion?.textfsm?.[0]
|
|
? showVersion?.textfsm?.[0]
|
|
: null;
|
|
};
|
|
|
|
const findDataShowLicense = () => {
|
|
const showLicense = dataTextfsm?.find(
|
|
(d) =>
|
|
d.command?.trim() === "show license" ||
|
|
d.command?.trim() === "sh license" ||
|
|
d.command?.trim() === "show lic" ||
|
|
d.command?.trim() === "sh lic"
|
|
);
|
|
return showLicense?.textfsm && Array.isArray(showLicense?.textfsm)
|
|
? showLicense?.textfsm
|
|
: null;
|
|
};
|
|
|
|
return (
|
|
<Box>
|
|
<Modal
|
|
opened={opened}
|
|
onClose={() => {
|
|
onClose();
|
|
if (
|
|
line?.userOpenCLI === user?.userName &&
|
|
!selectedLines.find((value) => value.id === line?.id)
|
|
)
|
|
socket?.emit("close_cli", {
|
|
lineId: line?.id,
|
|
stationId: line?.station_id,
|
|
});
|
|
}}
|
|
size={"100%"}
|
|
style={{ position: "absolute", left: 0 }}
|
|
title={
|
|
<Flex>
|
|
<Box
|
|
style={{
|
|
display: "flex",
|
|
// justifyContent: "center",
|
|
width: "400px",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
alignItems: "center",
|
|
fontSize: "13px",
|
|
color: "red",
|
|
display: "flex",
|
|
}}
|
|
>
|
|
{line?.userOpenCLI
|
|
? (line?.userOpenCLI === user?.userName
|
|
? "You are"
|
|
: line?.userOpenCLI + " is") + " using"
|
|
: "Terminal is used"}
|
|
</div>
|
|
</Box>
|
|
</Flex>
|
|
}
|
|
>
|
|
<Grid>
|
|
<Grid.Col span={3}>
|
|
<Flex style={{ height: "20px" }}>
|
|
{line?.connecting && (
|
|
<motion.div
|
|
style={{ fontSize: "12px", color: "red" }}
|
|
animate={{ opacity: [0.2, 1, 0.2] }}
|
|
transition={{
|
|
duration: 1.2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
}}
|
|
>
|
|
connecting...
|
|
</motion.div>
|
|
)}
|
|
{line?.runningScenario && (
|
|
<motion.div
|
|
style={{ fontSize: "12px", color: "red" }}
|
|
animate={{ opacity: [0.2, 1, 0.2] }}
|
|
transition={{
|
|
duration: 1.2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
}}
|
|
>
|
|
Running {line?.runningScenario}
|
|
</motion.div>
|
|
)}
|
|
</Flex>
|
|
<Flex justify={"space-between"} direction={"column"} h={"95%"}>
|
|
<Box>
|
|
<Flex gap={"sm"} justify={"center"} align={"center"}>
|
|
<Text size="xl">
|
|
<strong>
|
|
Line {line?.lineNumber || line?.line_number || ""}
|
|
</strong>
|
|
</Text>
|
|
<Text size="xl">
|
|
- <strong>{line?.port || ""}</strong>
|
|
</Text>
|
|
{line?.status === "connected" && (
|
|
<IconCircleCheckFilled color="green" fontSize={"18px"} />
|
|
)}
|
|
</Flex>
|
|
<Flex mt="4px">
|
|
<Text size="md" mr="6px">
|
|
BAUD:
|
|
</Text>
|
|
<Text size="md">
|
|
<strong>{line?.baud || ""}</strong>
|
|
</Text>
|
|
</Flex>
|
|
<Flex mt="4px" align="center">
|
|
<Text size="md" mr="6px">
|
|
PID:
|
|
</Text>
|
|
<Text
|
|
size="md"
|
|
style={{
|
|
opacity: line?.inventory?.pid ? 1 : 0.5,
|
|
cursor: line?.inventory?.pid ? "pointer" : "default",
|
|
}}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!line?.inventory?.pid) return;
|
|
navigator.clipboard.writeText(line.inventory?.pid || "");
|
|
}}
|
|
>
|
|
{line?.inventory?.pid || ""}
|
|
</Text>
|
|
{line?.inventory?.vid ? (
|
|
<Text size="md" ml={"sm"}>
|
|
{line?.inventory?.vid}
|
|
</Text>
|
|
) : (
|
|
""
|
|
)}
|
|
<CopyIcon
|
|
value={line?.inventory?.pid}
|
|
label="Copy PID"
|
|
copiedLabel="Copied PID"
|
|
ml={8}
|
|
/>
|
|
</Flex>
|
|
<Flex mt="4px" align="center">
|
|
<Text size="md" mr="6px">
|
|
SN:
|
|
</Text>
|
|
<Text
|
|
size="md"
|
|
style={{
|
|
opacity: line?.inventory?.sn ? 1 : 0.5,
|
|
cursor: line?.inventory?.sn ? "pointer" : "default",
|
|
}}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!line?.inventory?.sn) return;
|
|
navigator.clipboard.writeText(line.inventory?.sn || "");
|
|
}}
|
|
>
|
|
{line?.inventory?.sn || ""}
|
|
</Text>
|
|
<CopyIcon
|
|
value={line?.inventory?.sn}
|
|
label="Copy SN"
|
|
copiedLabel="Copied SN"
|
|
ml={8}
|
|
mr={8}
|
|
/>
|
|
<CopyIcon
|
|
value={
|
|
line?.inventory?.pid && line?.inventory?.sn
|
|
? `Line ${
|
|
line?.lineNumber || line?.line_number || ""
|
|
}: ${line.inventory?.pid}${
|
|
line.inventory?.vid ? ` ${line.inventory.vid}` : ""
|
|
} - ${line.inventory?.sn}`
|
|
: ""
|
|
}
|
|
label="Copy PID + SN"
|
|
copiedLabel="Copied PID + SN"
|
|
color="grape"
|
|
copiedColor="violet"
|
|
/>
|
|
</Flex>
|
|
<Flex mt="4px">
|
|
<Text size="md" mr={"6px"} fw={"bold"}>
|
|
IOS:
|
|
</Text>
|
|
<Text size="md">
|
|
{findDataShowVersion()
|
|
? findDataShowVersion()?.SOFTWARE_IMAGE
|
|
? findDataShowVersion()?.SOFTWARE_IMAGE +
|
|
" " +
|
|
(findDataShowVersion()?.VERSION || "")
|
|
: ""
|
|
: ""}
|
|
</Text>
|
|
</Flex>
|
|
</Box>
|
|
<Flex>
|
|
<Text size="md" mr={"sm"} fw={"bold"}>
|
|
License:
|
|
</Text>
|
|
<Text size="md">
|
|
{findDataShowLicense()
|
|
? findDataShowLicense()
|
|
?.filter(
|
|
(el: TextTSMLicense) =>
|
|
el.LICENSE_TYPE === "Permanent"
|
|
)
|
|
?.map((v: TextTSMLicense) => v.FEATURE)
|
|
?.join(", ")
|
|
: ""}
|
|
</Text>
|
|
</Flex>
|
|
<Flex>
|
|
<Text size="md" mr={"sm"} fw={"bold"}>
|
|
Sh env/module:
|
|
</Text>
|
|
<Text size="md">{""}</Text>
|
|
</Flex>
|
|
<Flex>
|
|
<Text size="md" mr={"sm"} fw={"bold"}>
|
|
Mem/Flash:
|
|
</Text>
|
|
<Text size="md">
|
|
{findDataShowVersion()
|
|
? findDataShowVersion()?.MEMORY +
|
|
(findDataShowVersion()?.USB_FLASH
|
|
? " - " + findDataShowVersion()?.USB_FLASH
|
|
: "")
|
|
: ""}
|
|
</Text>
|
|
</Flex>
|
|
<Box>
|
|
<Text size="md" mr={"sm"} fw={"bold"}>
|
|
Warning from test report: AI
|
|
</Text>
|
|
<Box>
|
|
<Textarea
|
|
rows={5}
|
|
size="sm"
|
|
placeholder="Report from AI"
|
|
value={valueIssue}
|
|
onChange={(event) =>
|
|
setValueIssue(event.currentTarget.value)
|
|
}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
<Box
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<fieldset
|
|
style={{
|
|
width: "280px",
|
|
}}
|
|
>
|
|
<Flex justify={"center"}>
|
|
<IconCircleDot
|
|
color={
|
|
line?.interface &&
|
|
findSwitchPort(line?.interface)?.status === "ON"
|
|
? "green"
|
|
: "red"
|
|
}
|
|
/>
|
|
<Flex>
|
|
<Text size="sm" ml={"sm"}>
|
|
Internet
|
|
</Text>
|
|
{line?.interface ? (
|
|
findSwitchPort(line?.interface)?.status === "ON" ? (
|
|
<Text size="sm" ml={"4px"} c={"green"}>
|
|
Connected ({normalizePortName(line?.interface)})
|
|
</Text>
|
|
) : (
|
|
<Text size="sm" ml={"4px"} c={"red"}>
|
|
Not Connected ({normalizePortName(line?.interface)})
|
|
</Text>
|
|
)
|
|
) : (
|
|
<Text c={"red"} size="sm" ml={"4px"}>
|
|
Not config
|
|
</Text>
|
|
)}
|
|
</Flex>
|
|
</Flex>
|
|
<Flex justify={"space-around"} mt={"4px"}>
|
|
<Button
|
|
className={classes.buttonControl}
|
|
disabled={isDisable || !line?.interface}
|
|
fw={400}
|
|
variant="outline"
|
|
color="green"
|
|
size="xs"
|
|
onClick={() => {
|
|
controlSwitch("on");
|
|
}}
|
|
>
|
|
ON
|
|
</Button>
|
|
<Button
|
|
className={classes.buttonControl}
|
|
disabled={isDisable || !line?.interface}
|
|
fw={400}
|
|
variant="outline"
|
|
color="red"
|
|
size="xs"
|
|
onClick={() => {
|
|
controlSwitch("off");
|
|
}}
|
|
>
|
|
OFF
|
|
</Button>
|
|
<Button
|
|
className={classes.buttonControl}
|
|
disabled={isDisable || !line?.interface}
|
|
fw={400}
|
|
variant="outline"
|
|
color="orange"
|
|
size="xs"
|
|
onClick={() => {
|
|
controlSwitch("restart");
|
|
}}
|
|
>
|
|
Restart
|
|
</Button>
|
|
</Flex>
|
|
</fieldset>
|
|
</Box>
|
|
<Flex
|
|
justify={"center"}
|
|
style={{
|
|
borderTop: "1px solid #ccc",
|
|
borderBottom: "1px solid #ccc",
|
|
paddingTop: "12px",
|
|
paddingBottom: "12px",
|
|
}}
|
|
>
|
|
<Button
|
|
disabled={isDisable}
|
|
fw={400}
|
|
w={"120px"}
|
|
variant="outline"
|
|
color="green"
|
|
size="xs"
|
|
onClick={() => {
|
|
if (!line?.lineClear && !line?.line_clear) {
|
|
notifications.show({
|
|
title: "Error",
|
|
message: "Clear line has not been configured",
|
|
color: "red",
|
|
});
|
|
return;
|
|
}
|
|
socket?.emit("clear_line", {
|
|
lineClear: line?.lineClear || line?.line_clear,
|
|
stationId: stationItem?.id,
|
|
});
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}}
|
|
>
|
|
Clear line
|
|
</Button>
|
|
</Flex>
|
|
</Flex>
|
|
</Grid.Col>
|
|
<Grid.Col
|
|
span={9}
|
|
style={{
|
|
// borderRight: "1px solid #ccc",
|
|
borderLeft: "1px solid #ccc",
|
|
}}
|
|
>
|
|
<TerminalCLI
|
|
cliOpened={opened}
|
|
socket={socket}
|
|
content={line?.output ?? ""}
|
|
initContent={line?.netOutput ?? ""}
|
|
loadingContent={line?.loadingOutput}
|
|
line_id={Number(line?.id)}
|
|
line={line}
|
|
station_id={Number(stationItem?.id)}
|
|
isDisabled={
|
|
typeof line?.userOpenCLI !== "undefined" &&
|
|
line?.userOpenCLI !== user?.userName
|
|
}
|
|
line_status={line?.status || ""}
|
|
/>
|
|
<Flex justify={"space-around"} mt={"md"} pt={"md"} pb={"md"}>
|
|
<ButtonDPELP
|
|
socket={socket}
|
|
selectedLines={line ? [line] : []}
|
|
isDisable={isDisable}
|
|
onClick={() => {
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 10000);
|
|
}}
|
|
/>
|
|
<Menu trigger="hover" withArrow shadow="md" position="top">
|
|
<Menu.Target>
|
|
<Button
|
|
fw={400}
|
|
disabled={isDisable}
|
|
variant="filled"
|
|
color="yellow"
|
|
style={{ height: "30px", width: "100px" }}
|
|
onClick={() => {}}
|
|
>
|
|
Scenario
|
|
</Button>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
<Box
|
|
px="xs"
|
|
py="sm"
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr 1fr",
|
|
gap: "12px",
|
|
}}
|
|
>
|
|
{scenarios.map((el, i) => (
|
|
<ButtonScenario
|
|
key={i}
|
|
socket={socket}
|
|
selectedLines={line ? [line] : []}
|
|
isDisable={isDisable}
|
|
onClick={() => {
|
|
// setSelectedLines([]);
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}}
|
|
scenario={el}
|
|
/>
|
|
))}
|
|
</Box>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
<Menu
|
|
closeOnItemClick={false}
|
|
closeOnClickOutside={false}
|
|
trigger="hover"
|
|
shadow="md"
|
|
position="top"
|
|
>
|
|
<Menu.Target>
|
|
<Button
|
|
fw={400}
|
|
disabled={isDisable}
|
|
variant="filled"
|
|
size="xs"
|
|
style={{ height: "30px", width: "100px" }}
|
|
onClick={() => {}}
|
|
>
|
|
BAUD
|
|
</Button>
|
|
</Menu.Target>
|
|
<Menu.Dropdown style={{ width: "110px" }}>
|
|
<Flex
|
|
justify={"space-between"}
|
|
direction={"column"}
|
|
style={{
|
|
gap: "8px",
|
|
}}
|
|
>
|
|
{listBaudDefault.map((el, i) => (
|
|
<Button
|
|
key={i}
|
|
disabled={isDisable}
|
|
variant="outline"
|
|
size="xs"
|
|
fw={400}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
socket?.emit("set_baud", {
|
|
lineId: line?.id,
|
|
baud: el,
|
|
stationId: Number(stationItem?.id),
|
|
});
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}}
|
|
>
|
|
{el}
|
|
</Button>
|
|
))}
|
|
<Input
|
|
placeholder="Custom"
|
|
value={valueBaud}
|
|
onChange={(e) => setValueBaud(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
socket?.emit("set_baud", {
|
|
lineId: line?.id,
|
|
baud: Number(valueBaud),
|
|
stationId: Number(stationItem?.id),
|
|
});
|
|
setValueBaud("");
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}
|
|
}}
|
|
/>
|
|
</Flex>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
<Button
|
|
disabled={true}
|
|
fw={400}
|
|
variant="filled"
|
|
color="green"
|
|
size="xs"
|
|
onClick={() => {}}
|
|
>
|
|
Select license
|
|
</Button>
|
|
<Button
|
|
disabled={true}
|
|
fw={400}
|
|
variant="filled"
|
|
color="green"
|
|
size="xs"
|
|
onClick={() => {}}
|
|
>
|
|
Select IOS
|
|
</Button>
|
|
<Button
|
|
fw={400}
|
|
disabled={isDisable}
|
|
variant="filled"
|
|
color="orange"
|
|
size="xs"
|
|
onClick={() => {
|
|
socket?.emit("write_command_line_from_web", {
|
|
lineIds: [line?.id],
|
|
stationId: stationItem?.id,
|
|
command: "spam_break",
|
|
});
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}}
|
|
>
|
|
Send Break
|
|
</Button>
|
|
<Menu trigger="hover" withArrow shadow="md" position="top">
|
|
<Menu.Target>
|
|
<Button
|
|
fw={400}
|
|
disabled={isDisable}
|
|
variant="filled"
|
|
style={{ height: "30px", width: "100px" }}
|
|
onClick={() => {}}
|
|
>
|
|
Power
|
|
</Button>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
<Flex gap={"4px"} p={"4px"}>
|
|
<Button
|
|
disabled={isDisable}
|
|
mr={"8px"}
|
|
fw={400}
|
|
variant="outline"
|
|
color="green"
|
|
size="xs"
|
|
onClick={() => {
|
|
controlApc("on");
|
|
}}
|
|
>
|
|
ON
|
|
</Button>
|
|
<Button
|
|
disabled={isDisable}
|
|
mr={"8px"}
|
|
fw={400}
|
|
variant="outline"
|
|
color="red"
|
|
size="xs"
|
|
onClick={() => {
|
|
controlApc("off");
|
|
}}
|
|
>
|
|
OFF
|
|
</Button>
|
|
<Button
|
|
disabled={isDisable}
|
|
fw={400}
|
|
variant="outline"
|
|
color="orange"
|
|
size="xs"
|
|
onClick={() => {
|
|
controlApc("restart");
|
|
}}
|
|
>
|
|
Restart
|
|
</Button>
|
|
</Flex>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Flex>
|
|
</Grid.Col>
|
|
<Grid.Col span={3} display={"none"}>
|
|
<Box>
|
|
<Tooltip
|
|
label={
|
|
<div>
|
|
<Flex>
|
|
<Text
|
|
style={{
|
|
fontSize: "14px",
|
|
}}
|
|
c={"#30d100"}
|
|
fw={500}
|
|
>
|
|
Text
|
|
</Text>
|
|
: is opened
|
|
</Flex>
|
|
<Flex>
|
|
<Text
|
|
style={{
|
|
fontSize: "14px",
|
|
}}
|
|
c={"#fab005"}
|
|
fw={500}
|
|
>
|
|
Text
|
|
</Text>
|
|
: is issue
|
|
</Flex>
|
|
<Flex>
|
|
<Text
|
|
style={{
|
|
fontSize: "14px",
|
|
}}
|
|
c={"red"}
|
|
fw={500}
|
|
>
|
|
Text
|
|
</Text>
|
|
: is closed
|
|
</Flex>
|
|
</div>
|
|
}
|
|
position="right"
|
|
>
|
|
<Flex align={"center"} gap={"6px"} w={"85px"}>
|
|
<Text size="md">
|
|
<strong>Ticket:</strong>{" "}
|
|
</Text>
|
|
<IconInfoCircle
|
|
color="#3bb7e9"
|
|
width={"18px"}
|
|
height={"18px"}
|
|
/>
|
|
</Flex>
|
|
</Tooltip>
|
|
</Box>
|
|
<ScrollArea
|
|
h={"65vh"}
|
|
style={{ border: "1px solid #ccc" }}
|
|
p={"4px"}
|
|
>
|
|
{renderHistory(latestTicket)}
|
|
</ScrollArea>
|
|
<Box mt={"8px"}>
|
|
<Input
|
|
style={{
|
|
width: "100%",
|
|
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
|
|
}}
|
|
placeholder={"Input description ticket"}
|
|
value={dataTicket.description || ""}
|
|
onChange={(event) => {
|
|
setDataTicket((pre) => ({
|
|
...pre,
|
|
description: event.target.value,
|
|
}));
|
|
}}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter" && dataTicket.description) {
|
|
setDataTicket((pre) => ({ ...pre, description: "" }));
|
|
if (dataTicket?.status === "closed" || !dataTicket.id) {
|
|
handleCreate();
|
|
} else handleUpdate("open");
|
|
setIsDisableTicket(true);
|
|
setTimeout(() => {
|
|
setIsDisableTicket(false);
|
|
}, 2000);
|
|
}
|
|
}}
|
|
rightSectionPointerEvents="all"
|
|
rightSection={
|
|
<CloseButton
|
|
aria-label="Clear input"
|
|
onClick={() =>
|
|
setDataTicket((pre) => ({ ...pre, description: "" }))
|
|
}
|
|
style={{
|
|
display: dataTicket?.description ? undefined : "none",
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
</Box>
|
|
<Box mt={"8px"}>
|
|
<Flex justify={"end"} mt={"4px"}>
|
|
{dataTicket?.status === "closed" || !dataTicket.id ? (
|
|
<Button
|
|
disabled={isDisableTicket || !dataTicket.description}
|
|
mr={"8px"}
|
|
fw={400}
|
|
variant="outline"
|
|
size="xs"
|
|
onClick={() => {
|
|
setDataTicket((pre) => ({ ...pre, description: "" }));
|
|
handleCreate();
|
|
setIsDisableTicket(true);
|
|
setTimeout(() => {
|
|
setIsDisableTicket(false);
|
|
}, 2000);
|
|
}}
|
|
>
|
|
Open
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
disabled={isDisableTicket || !dataTicket.description}
|
|
mr={"8px"}
|
|
fw={400}
|
|
variant="outline"
|
|
size="xs"
|
|
onClick={() => {
|
|
handleUpdate("open");
|
|
setIsDisableTicket(true);
|
|
setTimeout(() => {
|
|
setIsDisableTicket(false);
|
|
}, 2000);
|
|
}}
|
|
>
|
|
Send
|
|
</Button>
|
|
)}
|
|
<Button
|
|
disabled={
|
|
isDisableTicket ||
|
|
dataTicket?.status === "closed" ||
|
|
!dataTicket.id ||
|
|
!dataTicket.description
|
|
}
|
|
mr={"8px"}
|
|
fw={400}
|
|
variant="outline"
|
|
color="orange"
|
|
size="xs"
|
|
onClick={() => {
|
|
setDataTicket((pre) => ({ ...pre, description: "" }));
|
|
handleUpdate("issue");
|
|
setIsDisableTicket(true);
|
|
setTimeout(() => {
|
|
setIsDisableTicket(false);
|
|
}, 2000);
|
|
}}
|
|
>
|
|
Issue
|
|
</Button>
|
|
<Button
|
|
disabled={
|
|
isDisableTicket ||
|
|
!dataTicket.id ||
|
|
dataTicket?.status === "closed"
|
|
}
|
|
fw={400}
|
|
variant="outline"
|
|
color="red"
|
|
size="xs"
|
|
onClick={() => {
|
|
handleUpdate("closed");
|
|
setIsDisableTicket(true);
|
|
setTimeout(() => {
|
|
setIsDisableTicket(false);
|
|
}, 2000);
|
|
}}
|
|
>
|
|
Close
|
|
</Button>
|
|
</Flex>
|
|
</Box>
|
|
</Grid.Col>
|
|
</Grid>
|
|
</Modal>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default ModalTerminal;
|