diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 0b49800..6842510 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -124,7 +124,7 @@ export default class LineConnection { private listScenarios: number[] public handleClearLine: () => void private session: TestSession - private physicalTest: PhysicalPortTest + public physicalTest: PhysicalPortTest private outputPhysicalTest: string constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { @@ -514,24 +514,30 @@ export default class LineConnection { resolve(true) return } - const detectLog = await this.detectLogWithAI(logScenarios) - const result = mapToLineFormat({ - lineNumber: this.config.lineNumber, - inventory: this.config.inventory, - latestScenario: { - detectAI: detectLog, - }, - data, - }) - // if (script?.send_result || script?.sendResult) { - this.dataDPELP = result - console.log( - `DPELP DATA line ${this.config.lineNumber} of ${this.config.stationName}:`, - this.dataDPELP - ) - // } - if (this.config.latestScenario) - this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog } + if (script?.send_result || script?.sendResult) { + const detectLog = await this.detectLogWithAI(logScenarios) + const result = mapToLineFormat({ + lineNumber: this.config.lineNumber, + inventory: this.config.inventory, + latestScenario: { + detectAI: detectLog, + }, + data, + }) + // if (script?.send_result || script?.sendResult) { + this.dataDPELP = result + console.log( + `DPELP DATA line ${this.config.lineNumber} of ${this.config.stationName}:`, + this.dataDPELP + ) + + // } + if (this.config.latestScenario) + this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog } + if (result.sn) { + this.updateNote(result.sn, result) + } + } this.config.data = data this.socketIO.emit('data_textfsm', { stationId: this.config.stationId, @@ -540,9 +546,6 @@ export default class LineConnection { inventory: this.config.inventory || null, latestScenario: this.config.latestScenario || null, }) - if (result.sn) { - this.updateNote(result.sn, result) - } } catch (error) { console.log(error) } @@ -1014,6 +1017,7 @@ export default class LineConnection { endTesting() { this.physicalTest.done = true + this.physicalTest.resetTestedPorts() this.config.runningPhysical = false this.config.runningScenario = '' this.outputBuffer = '' diff --git a/BACKEND/app/services/physical_test_service.ts b/BACKEND/app/services/physical_test_service.ts index e86ba76..e00a94a 100644 --- a/BACKEND/app/services/physical_test_service.ts +++ b/BACKEND/app/services/physical_test_service.ts @@ -76,6 +76,16 @@ export class PhysicalPortTest { .sort() } + resetTestedPorts() { + // this.ports.clear() + this.expectedPorts.forEach((p) => { + this.ports.set(normalizeInterface(p), { + name: normalizeInterface(p), + tested: false, + }) + }) + } + private checkDone() { const testedCount = [...this.ports.values()].filter((p) => p.tested).length diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 7406c22..45a4be1 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -635,8 +635,28 @@ export class WebSocketIo { stationId, [lineId], async (lineCon) => { - lineCon.endTesting() await lineCon.sendReportPhysicalTest() + lineCon.endTesting() + }, + {} + ) + }) + + socket.on('reset_physical_test', async (data) => { + const { stationId, lineId } = data + await this.handleLineOperation( + io, + stationId, + [lineId], + async (lineCon) => { + lineCon.physicalTest.resetTestedPorts() + io.emit('running_scenario', { + stationId: stationId, + lineId: lineId, + title: 'Physical Test', + physical: true, + ports: lineCon.config.ports, + }) }, {} ) @@ -684,7 +704,8 @@ export class WebSocketIo { lines: Line[], station: Station, output = '', - inventory: string = '' + inventory: string = '', + latestScenario?: any ) { try { for (const line of lines) { @@ -710,6 +731,7 @@ export class WebSocketIo { inventory: inventory, runningPhysical: false, runningScenario: '', + latestScenario: latestScenario, }, socket, async () => { @@ -793,7 +815,8 @@ export class WebSocketIo { [linesData], stationData, line?.config?.output || '', - line?.config?.inventory || '' + line?.config?.inventory || '', + line?.config?.latestScenario || undefined ) this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index e16ad89..2361f53 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -103,6 +103,7 @@ function App() { const flushScheduledRef = useRef(false); const [listBrands, setListBrands] = useState([]); const [listCategories, setListCategories] = useState([]); + const [listIos, setListIos] = useState([]); const connectApcSwitch = (station: TStation) => { if (station?.apc_1_ip && station?.apc_1_port) { @@ -189,12 +190,25 @@ function App() { } }; + // function get list ios + const getListIos = async () => { + try { + const response = await axios.get(apiUrl + "api/ios"); + if (response.data && Array.isArray(response.data)) { + setListIos(response.data); + } + } catch (error) { + console.log("Error get ios", error); + } + }; + useEffect(() => { if (!socket) return; getStation(); getScenarios(); getBrands(); getCategories(); + getListIos(); }, [socket]); useEffect(() => { @@ -387,6 +401,7 @@ function App() { runningScenario: data?.title || "", runningPhysical: data?.physical || false, ports: data?.ports || [], + listPortsPhysical: [], }, data?.stationId ); @@ -401,6 +416,15 @@ function App() { ); }); + socket?.on("test_port_physical", (data) => { + if (data?.data && data?.data.length > 0) + updateValueLineStation( + data?.lineId, + { listPortsPhysical: data?.data }, + data?.stationId + ); + }); + // ✅ cleanup on unmount or when socket changes return () => { socket.off("init"); @@ -418,6 +442,7 @@ function App() { socket.off("line_connecting"); socket.off("running_scenario"); socket.off("user_clear_terminal"); + socket.off("test_port_physical"); }; }, [socket, stations, selectedLine]); @@ -462,6 +487,10 @@ function App() { ...updates, lineNumber: lineItem.lineNumber, line_number: lineItem.line_number, + ports: + updates?.ports && updates?.ports?.length > 0 + ? updates?.ports + : lineItem.ports || [], ...(isNetOutput && { netOutput: updates?.loadingClearTerminal ? "" @@ -489,6 +518,10 @@ function App() { return { ...prevSelected, ...updates, + ports: + updates?.ports && updates?.ports?.length > 0 + ? updates?.ports + : prevSelected.ports || [], ...(isNetOutput && { netOutput: updates?.loadingClearTerminal ? "" @@ -837,6 +870,7 @@ function App() { socket={socket} stationItem={stations.find((el) => el.id === Number(activeTab))} scenarios={scenarios} + listIos={listIos} /> {/* Scenario + )} - {line?.connecting && ( + {line?.connecting && line?.status !== "connected" && ( -
- Latest: {line?.latestScenario?.name || ""} - - {line?.latestScenario?.time - ? "(" + convertTimestampToDate(line?.latestScenario?.time) + ")" - : ""} - -
+ +
+ Latest: {line?.latestScenario?.name || ""} + + {line?.latestScenario?.time + ? "(" + + convertTimestampToDate(line?.latestScenario?.time) + + ")" + : ""} + +
+
+ + Ports Tested{" "} + {line?.ports?.length + ? `(${line?.listPortsPhysical?.length || 0}/${ + line?.ports?.length || 0 + })` + : ""} + +
+
diff --git a/FRONTEND/src/components/Modal/ModalSelectIOS.tsx b/FRONTEND/src/components/Modal/ModalSelectIOS.tsx new file mode 100644 index 0000000..c1e96fd --- /dev/null +++ b/FRONTEND/src/components/Modal/ModalSelectIOS.tsx @@ -0,0 +1,271 @@ +import { + Button, + Checkbox, + Flex, + Modal, + ScrollArea, + Table, + Tabs, + Text, + TextInput, +} from "@mantine/core"; +import type { Socket } from "socket.io-client"; +import type { TLine, TStation } from "../../untils/types"; +import { IconPlayerPlay, IconX } from "@tabler/icons-react"; +import { useState } from "react"; + +const ModalSelectIOS = ({ + socket, + station, + listIos, + opened, + close, + line, +}: { + socket: Socket | null; + station: TStation | undefined; + listIos: string[]; + opened: boolean; + close: () => void; + line: TLine | undefined; +}) => { + const [isReboot, setIsReboot] = useState(false); + const [inputSearch, setInputSearch] = useState(""); + + const filterIos = (type: string = "") => { + // Switch: Ưu tiên các dòng 4 chữ số cụ thể + // c2960, c3560, c3750, c3850, c4500, c9xxx + const switchRegex = /^(c2960|c3560|c3750|c3850|c4500|c9\d{3})/i; + + // Router: Các dòng ISR đời cũ và mới + // c18xx, c19xx, c28xx, c29xx (nhưng không phải 2960), c38xx (nhưng không phải 3850), c39xx, isr, asr + const routerRegex = + /^(c8\d{2}|c18|c19|c28|c29(?!60)|c38(?!50)|c39|isr|asr)/i; + + return listIos + .filter((name) => { + if (type === "switch") { + return switchRegex.test(name); + } + if (type === "router") { + return routerRegex.test(name); + } + return false; + }) + .filter((ios) => ios.toLowerCase().includes(inputSearch.toLowerCase())); + }; + + return ( + { + close(); + }} + title={ + + Select IOS + + } + size="xl" + > + + + + Router + + + Switch + + + + + setInputSearch(event.currentTarget.value)} + rightSection={ + inputSearch ? ( + setInputSearch("")} + /> + ) : null + } + rightSectionPointerEvents="auto" + size="xs" + /> + setIsReboot(event.currentTarget.checked)} + /> + + + + + + + Name + + + Action + + + + + {filterIos("router")?.map((ios, i) => ( + + {ios || ""} + + + + + ))} + +
+
+
+ + + setInputSearch(event.currentTarget.value)} + rightSection={ + inputSearch ? ( + setInputSearch("")} + /> + ) : null + } + rightSectionPointerEvents="auto" + size="xs" + /> + + + + + + + Name + + + Action + + + + + {filterIos("switch")?.map((ios, i) => ( + + {ios || ""} + + + + + ))} + +
+
+
+
+
+ ); +}; + +export default ModalSelectIOS; diff --git a/FRONTEND/src/components/Modal/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx index 61dd240..2a118f9 100644 --- a/FRONTEND/src/components/Modal/ModalTerminal.tsx +++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx @@ -9,6 +9,7 @@ import { Menu, Modal, ScrollArea, + Tabs, Text, Textarea, Tooltip, @@ -31,6 +32,7 @@ import { IconCircleCheckFilled, IconCircleDot, IconInfoCircle, + IconPlug, IconSettings, } from "@tabler/icons-react"; import { ButtonDPELP, ButtonScenario } from "../ButtonAction"; @@ -41,6 +43,7 @@ import { notifications } from "@mantine/notifications"; import classes from "../Component.module.css"; import { listBaudDefault } from "../../untils/constanst"; import { motion } from "motion/react"; +import ModalSelectIOS from "./ModalSelectIOS"; const apiUrl = import.meta.env.VITE_BACKEND_URL; const INIT_TICKET = { @@ -60,6 +63,7 @@ const ModalTerminal = ({ stationItem, scenarios, selectedLines, + listIos, }: { opened: boolean; onClose: () => void; @@ -68,6 +72,7 @@ const ModalTerminal = ({ stationItem: TStation | undefined; scenarios: IScenario[]; selectedLines: TLine[]; + listIos: string[]; }) => { const user = useMemo(() => { return localStorage.getItem("user") && @@ -78,7 +83,7 @@ const ModalTerminal = ({ const [isDisable, setIsDisable] = useState(false); const [isDisableTicket, setIsDisableTicket] = useState(false); const [listPorts, setListPorts] = useState([]); - const [listPortsPhysical, setListPortsPhysical] = useState([]); + // const [listPortsPhysical, setListPortsPhysical] = useState([]); const [latestTicket, setLatestTicket] = useState(INIT_TICKET); const [dataTicket, setDataTicket] = useState(INIT_TICKET); const [valueBaud, setValueBaud] = useState(""); @@ -86,6 +91,7 @@ const ModalTerminal = ({ const [dataTextfsm, setDataTextfsm] = useState([]); const [isClearKeepScrollBack, setIsClearKeepScrollBack] = useState(false); + const [openSelectIos, setOpenSelectIos] = useState(false); useEffect(() => { if (opened && line?.tickets && line?.tickets?.length > 0) { @@ -131,14 +137,14 @@ const ModalTerminal = ({ if (data?.ports && data?.ports.length > 0) setListPorts(data?.ports || []); }); - socket?.on("test_port_physical", (data) => { - if (data.stationId !== stationItem?.id) return; - if (data?.data && data?.data.length > 0) - setListPortsPhysical(data?.data || []); - }); + // socket?.on("test_port_physical", (data) => { + // if (data.stationId !== stationItem?.id) return; + // if (data?.data && data?.data.length > 0) + // setListPortsPhysical(data?.data || []); + // }); return () => { socket?.off("switch_ports_status"); - socket?.off("test_port_physical"); + // socket?.off("test_port_physical"); }; }, [socket, stationItem]); @@ -466,12 +472,13 @@ const ModalTerminal = ({ size={"100%"} style={{ position: "absolute", left: 0 }} title={ - +
- - } - > - - - - {line?.connecting && ( + + {line?.connecting && line?.status !== "connected" && ( )} - - - - - - Line {line?.lineNumber || line?.line_number || ""} - - - - - {line?.port || ""} - - {line?.status === "connected" && ( - - )} - - - - - BAUD: - - - {line?.baud || ""} - - - - - {}} + + } + > + + + + + + + General + + + + Physical Test + + + + + + + + + + Line {line?.lineNumber || line?.line_number || ""} + + + + - {line?.port || ""} + + {line?.status === "connected" && ( + + )} + + + + + BAUD: + + + {line?.baud || ""} + + + - - - - - - {listBaudDefault.map((el, i) => ( - - ))} - 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); - } - }} - /> - - - - - - - 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} + {listBaudDefault.map((el, i) => ( + + ))} + 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); + } + }} + /> + + + + + + + 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: - ) : ( - "" - )} - - - - - SN: - - + {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 + + +