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(false); const [isDisableTicket, setIsDisableTicket] = useState(false); const [listPorts, setListPorts] = useState([]); const [latestTicket, setLatestTicket] = useState(INIT_TICKET); const [dataTicket, setDataTicket] = useState(INIT_TICKET); const [valueBaud, setValueBaud] = useState(""); const [valueIssue, setValueIssue] = useState(""); const [dataTextfsm, setDataTextfsm] = useState([]); 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 (
{list.reverse().map((item: THistoryTicket, index: number) => (
{item?.status === "closed" ? "*" : ""}@{item?.userName}: {item?.description} {moment(Number(item?.time)).format("HH:m DD/M")}
))}
); }; 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 ( { 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={
{line?.userOpenCLI ? (line?.userOpenCLI === user?.userName ? "You are" : line?.userOpenCLI + " is") + " using" : "Terminal is used"}
} > {line?.connecting && ( connecting... )} {line?.runningScenario && ( Running {line?.runningScenario} )} Line {line?.lineNumber || line?.line_number || ""} - {line?.port || ""} {line?.status === "connected" && ( )} BAUD: {line?.baud || ""} PID: { e.preventDefault(); e.stopPropagation(); if (!line?.inventory?.pid) return; navigator.clipboard.writeText(line.inventory?.pid || ""); }} > {line?.inventory?.pid || ""} {line?.inventory?.vid ? ( {line?.inventory?.vid} ) : ( "" )} SN: { e.preventDefault(); e.stopPropagation(); if (!line?.inventory?.sn) return; navigator.clipboard.writeText(line.inventory?.sn || ""); }} > {line?.inventory?.sn || ""} IOS: {findDataShowVersion() ? findDataShowVersion()?.SOFTWARE_IMAGE ? findDataShowVersion()?.SOFTWARE_IMAGE + " " + (findDataShowVersion()?.VERSION || "") : "" : ""} License: {findDataShowLicense() ? findDataShowLicense() ?.filter( (el: TextTSMLicense) => el.LICENSE_TYPE === "Permanent" ) ?.map((v: TextTSMLicense) => v.FEATURE) ?.join(", ") : ""} Sh env/module: {""} Mem/Flash: {findDataShowVersion() ? findDataShowVersion()?.MEMORY + (findDataShowVersion()?.USB_FLASH ? " - " + findDataShowVersion()?.USB_FLASH : "") : ""} Warning from test report: AI