import "@mantine/core/styles.css"; import "@mantine/dates/styles.css"; import "@mantine/notifications/styles.css"; import "./App.css"; import classes from "./App.module.css"; import componentClasses from "./components/Component.module.css"; import { Suspense, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Tabs, Text, Container, Flex, MantineProvider, Grid, ScrollArea, LoadingOverlay, Loader, Box, } from "@mantine/core"; import type { DataSummaryTested, FileInfo, IScenario, ReceivedFile, ResponseData, TBrands, TCategories, TLine, TStation, TUser, } from "./untils/types"; import axios from "axios"; import CardLine from "./components/CardLine"; import { SocketProvider, useSocket } from "./context/SocketContext"; import // ButtonConnect, // ButtonControlApc, // ButtonCopy, // ButtonDPELP, // ButtonScenario, // ButtonSelect, "./components/ButtonAction"; import StationSetting from "./components/FormAddEdit"; // import DrawerScenario from "./components/DrawerScenario"; import { Notifications } from "@mantine/notifications"; import ModalTerminal from "./components/Modal/ModalTerminal"; import PageLogin from "./components/Authentication/LoginPage"; // import DrawerLogs from "./components/DrawerLogs"; import DraggableTabs from "./components/DragTabs"; import { isJsonString } from "./untils/helper"; import BottomToolBar from "./components/BottomToolBar"; import ModalConfirmSkipTestPort from "./components/Modal/ModalConfirmSkipTestPort"; import ModalConfirmRunPhysical from "./components/Modal/ModalConfirmRunPhysicalTest"; import ModalSummaryTested from "./components/Modal/ModalSummaryTested"; // import ModalConfirmRunScenario from "./components/Modal/ModalConfirmRunScenario"; const apiUrl = import.meta.env.VITE_BACKEND_URL; // Helper: chia mảng thành các chunk theo size const chunkArray = (array: T[], size: number): T[][] => { const result: T[][] = []; for (let i = 0; i < array.length; i += size) { result.push(array.slice(i, i + size)); } return result; }; /** * Main Component */ function App() { const user = useMemo(() => { return localStorage.getItem("user") && isJsonString(localStorage.getItem("user")) ? JSON.parse(localStorage.getItem("user") || "") : null; }, []); if (!user) { localStorage.removeItem("user"); window.location.href = "/"; } document.title = "Automation Test"; const { socket } = useSocket(); const [stations, setStations] = useState([]); const [selectedLines, setSelectedLines] = useState([]); const [activeTab, setActiveTab] = useState("0"); const [isDisable, setIsDisable] = useState(false); const [isOpenAddStation, setIsOpenAddStation] = useState(false); const [isEditStation, setIsEditStation] = useState(false); const [stationEdit, setStationEdit] = useState(); const [scenarios, setScenarios] = useState([]); const [openModalTerminal, setOpenModalTerminal] = useState(false); const [selectedLine, setSelectedLine] = useState(); const [loadingTerminal, setLoadingTerminal] = useState(false); const [usersConnecting, setUsersConnecting] = useState([]); const [testLogContent, setTestLogContent] = useState(""); const [isLogModalOpen, setIsLogModalOpen] = useState(false); const [expandedBottomBar, setExpandedBottomBar] = useState(true); const [activeTabBottom, setActiveTabBottom] = useState("command"); const lineBuffersRef = useRef(new Map()); const flushScheduledRef = useRef(false); const [listBrands, setListBrands] = useState([]); const [listCategories, setListCategories] = useState([]); const [listIos, setListIos] = useState([]); const [listLicense, setListLicense] = useState([]); const [isLoading, setIsLoading] = useState(true); const [linesConfirmSkipPort, setLinesConfirmSkipPort] = useState([]); const [linesConfirmRunPhysical, setLinesConfirmRunPhysical] = useState< TLine[] >([]); const [dataSummaryTested, setDataSummaryTested] = useState(null); const connectApcSwitch = (station: TStation) => { if (!station?.is_active) return; if (station?.apc_1_ip && station?.apc_1_port) { socket?.emit("connect_apc", { station: station, apcIp: station?.apc_1_ip, apcName: "apc_1", }); } if (station?.apc_2_ip && station?.apc_2_port) { socket?.emit("connect_apc", { station: station, apcIp: station?.apc_2_ip, apcName: "apc_2", }); } if (station?.switch_control_ip && station?.switch_control_port) { socket?.emit("connect_switch", { station: station, ip: station?.switch_control_ip, }); } }; // function get list station const getStation = async () => { try { const response = await axios.get(apiUrl + "api/stations"); if (response.status) { if (Array.isArray(response.data)) { setStations( response.data.map((station) => { connectApcSwitch(station); const lines = (station?.lines || []).sort( (a: TLine, b: TLine) => a?.lineNumber - b?.lineNumber, ); return { ...station, lines }; }), ); } } } catch (error) { console.log("Error get station", error); } }; // function get list station const getScenarios = async () => { try { const response = await axios.get(apiUrl + "api/scenarios"); if (response.data.status) { if (Array.isArray(response.data.data.data)) { setScenarios(response.data.data.data); } } } catch (error) { console.log("Error get station", error); } }; // function get list brand const getBrands = async () => { try { const response = await axios.get(apiUrl + "api/brands"); if (response.data) { if (response.data && Array.isArray(response.data)) { setListBrands(response.data); } } } catch (error) { console.log("Error get brand", error); } }; // function get list brand const getCategories = async () => { try { const response = await axios.get(apiUrl + "api/categories"); if (response.data && Array.isArray(response.data)) { setListCategories(response.data); } } catch (error) { console.log("Error get brand", error); } }; // 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); } }; // function get list license const getListLicense = async () => { try { const response = await axios.get(apiUrl + "api/license"); if (response.data && Array.isArray(response.data)) { setListLicense(response.data); } } catch (error) { console.log("Error get ios", error); } }; useEffect(() => { if (!socket) return; getStation(); getScenarios(); getBrands(); getCategories(); getListIos(); getListLicense(); }, [socket]); useEffect(() => { setTimeout(() => { setIsLoading(false); }, 2000); }, []); useEffect(() => { if (!socket || !stations?.length) return; socket.on("line_connected", (data) => updateValueLineStation( data?.lineId, { status: data.status, connecting: false }, data?.stationId, ), ); socket.on("line_disconnected", (data) => updateValueLineStation( data?.lineId, { status: data.status, connecting: false, netOutput: "[CLEAR_TERMINAL_SCROLL_BACK]", output: "[CLEAR_TERMINAL_SCROLL_BACK]", listFeatureTested: [], latestScenario: undefined, isReady: false, }, data?.stationId, ), ); socket?.on("line_output", (data) => { const { lineId, data: text } = data; // updateValueLineStation( // data?.lineId, // { isReady: data.isReady }, // data?.stationId // ); const buf = lineBuffersRef.current.get(lineId) || ""; lineBuffersRef.current.set(lineId, buf + text); if (!flushScheduledRef.current) { flushScheduledRef.current = true; setTimeout(() => flushBuffers(), 50); } }); socket?.on("update_status_ready", (data) => { const { isReady } = data; updateValueLineStation( data?.lineId, { isReady: isReady }, data?.stationId, ); }); socket?.on("line_error", (data) => { updateValueLineStation( data?.lineId, { netOutput: data.error, connecting: false }, data?.stationId, ); }); socket?.on("init", (data) => { if (Array.isArray(data)) { // console.log(data); setLoadingTerminal(true); data.forEach((value) => { updateValueLineStation( value?.id, { ...value, netOutput: value.output }, value?.stationId, ); }); } }); socket?.on("user_connecting", (data) => { if (Array.isArray(data)) { setUsersConnecting(data); } }); socket?.on("user_open_cli", (data) => { setTimeout(() => { updateValueLineStation( data.lineId, { cliOpened: true, userEmailOpenCLI: data.userEmailOpenCLI, userOpenCLI: data.userOpenCLI, }, data?.stationId, ); }, 100); }); socket?.on("user_close_cli", (data) => { setTimeout(() => { updateValueLineStation( data.lineId, { cliOpened: false, userEmailOpenCLI: undefined, userOpenCLI: undefined, }, data?.stationId, ); }, 100); }); const receivedFiles: Record = {}; socket?.on("response_content_log", (data: ResponseData) => { if (!data.chunk) { const decoder = new TextDecoder("utf-8"); const str = decoder.decode(data as unknown as ArrayBuffer); setTestLogContent(str); return; } const { fileId, chunkIndex, totalChunks, chunk } = data.chunk; if (!receivedFiles[fileId]) { receivedFiles[fileId] = { chunks: [], receivedChunks: 0, totalChunks, }; } let bufferChunk: Buffer; if (chunk instanceof ArrayBuffer) { bufferChunk = Buffer.from(new Uint8Array(chunk)); // ✅ convert properly } else if (chunk instanceof Uint8Array) { bufferChunk = Buffer.from(chunk); // ✅ direct support } else { bufferChunk = chunk as Buffer; // fallback if server sends Buffer } receivedFiles[fileId].chunks[chunkIndex] = bufferChunk; receivedFiles[fileId].receivedChunks++; if (receivedFiles[fileId].receivedChunks === totalChunks) { const fileBuffer = Buffer.concat(receivedFiles[fileId].chunks); const decoder = new TextDecoder("utf-8"); const str = decoder.decode(fileBuffer); setTestLogContent(str); delete receivedFiles[fileId]; // cleanup ✅ } }); socket?.on("data_textfsm", (data) => { setTimeout(() => { updateValueLineStation( data.lineId, { data: data.data, inventory: data.inventory, latestScenario: data.latestScenario, }, data?.stationId, ); }, 100); }); socket?.on("update_ticket", (data) => { setTimeout(() => { updateValueLineStation( data.lineId, { tickets: data.data, }, data?.stationId, ); }, 100); }); socket?.on("update_baud", (data) => { setTimeout(() => { updateValueLineStation( data.lineId, { baud: data.data, }, data?.stationId, ); }, 100); }); socket?.on("line_connecting", (data) => { setTimeout(() => { updateValueLineStation( data?.lineId, { connecting: true }, data?.stationId, ); }, 100); }); socket?.on("running_scenario", (data) => { setTimeout(() => { updateValueLineStation( data?.lineId, { runningScenario: data?.title || "", runningPhysical: data?.physical || false, ports: data?.ports || [], listPortsPhysical: [], }, data?.stationId, ); }, 100); }); socket?.on("user_clear_terminal", (data) => { updateValueLineStation( data?.lineId, { netOutput: "", output: "", loadingClearTerminal: true }, data?.stationId, ); }); socket?.on("test_port_physical", (data) => { if (data?.data && data?.data.length > 0) updateValueLineStation( data?.lineId, { listPortsPhysical: data?.data }, data?.stationId, ); }); socket?.on("feature_tested", (data) => { if (data?.listFeatureTested) { updateValueLineStation( data?.lineId, { listFeatureTested: data?.listFeatureTested, isSkipPhysical: data?.isSkipPhysical, reasonSkipPhysical: data?.reasonSkipPhysical, }, data?.stationId, ); if (data?.isSkipPhysical && !data?.reasonSkipPhysical) { const valueLine = findLineByLineId(data?.lineId, data?.stationId); if ( valueLine && openModalTerminal && selectedLine?.id === valueLine?.id && (!valueLine?.listPortsPhysical || valueLine?.listPortsPhysical?.length === 0) ) setLinesConfirmSkipPort((pre) => [ ...pre, { ...valueLine, inventory: { ...valueLine.inventory, pid: data?.pid, sn: data?.sn, vid: data?.vid, }, }, ]); } if ( !data?.listFeatureTested?.includes("PHYSICAL") && data?.listFeatureTested?.includes("DPELP") ) { const valueLine = findLineByLineId(data?.lineId, data?.stationId); if ( valueLine && openModalTerminal && selectedLine?.id === valueLine?.id ) setLinesConfirmRunPhysical((pre) => [ ...pre, { ...valueLine, inventory: { ...valueLine.inventory, pid: data?.pid, sn: data?.sn, vid: data?.vid, }, }, ]); } } }); socket?.on("summary_tested", (data) => { if ( data?.body && openModalTerminal && selectedLine?.id === data?.lineId ) { setDataSummaryTested({ body: data?.body || "", title: data?.title || "", }); } }); // ✅ cleanup on unmount or when socket changes return () => { socket.off("init"); socket.off("line_output"); socket.off("line_error"); socket.off("line_connected"); socket.off("line_disconnected"); socket.off("user_connecting"); socket.off("user_open_cli"); socket.off("user_close_cli"); socket.off("response_content_log"); socket.off("data_textfsm"); socket.off("update_ticket"); socket.off("update_baud"); socket.off("line_connecting"); socket.off("running_scenario"); socket.off("user_clear_terminal"); socket.off("test_port_physical"); socket.off("feature_tested"); socket.off("summary_tested"); }; }, [socket, stations, selectedLine]); const flushBuffers = useCallback(() => { setStations((prev) => prev.map((station) => ({ ...station, lines: station.lines.map((line) => { const buffered = lineBuffersRef.current.get(line.id || 0); if (!buffered) return line; // không có update const data = { ...line, netOutput: (line.netOutput || "") + buffered, output: buffered, loadingOutput: line.loadingOutput ? false : true, loadingClearTerminal: false, }; updateValueSelectedLine(line?.id || 0, data); return data; }), })), ); // clear lineBuffersRef.current.clear(); flushScheduledRef.current = false; }, []); const updateValueLineStation = useCallback( (lineId: number, updates: Partial, stationId?: number) => { setStations((prevStations) => prevStations?.map((station: TStation) => station.id === stationId ? { ...station, lines: station.lines?.map((lineItem: TLine) => { if (lineItem.id !== lineId) return lineItem; const isNetOutput = typeof updates?.netOutput !== "undefined"; return { ...lineItem, ...updates, lineNumber: lineItem.lineNumber, line_number: lineItem.line_number, ports: updates?.ports && updates?.ports?.length > 0 ? updates?.ports : lineItem.ports || [], ...(isNetOutput && { netOutput: updates?.loadingClearTerminal ? "" : (lineItem.netOutput || "") + (updates.netOutput || ""), output: updates.netOutput, // Nếu netOutput thì update luôn output loadingOutput: lineItem.loadingOutput ? false : true, loadingClearTerminal: updates?.loadingClearTerminal ? updates?.loadingClearTerminal : false, }), }; }), } : station, ), ); // Update selectedLine nếu nó đang được chọn setSelectedLine((prevSelected) => { if (!prevSelected || prevSelected.id !== lineId) return prevSelected; const isNetOutput = typeof updates?.netOutput !== "undefined"; return { ...prevSelected, ...updates, ports: updates?.ports && updates?.ports?.length > 0 ? updates?.ports : prevSelected.ports || [], ...(isNetOutput && { netOutput: updates?.loadingClearTerminal ? "" : (prevSelected.netOutput || "") + (updates.netOutput || ""), output: updates.netOutput, loadingOutput: prevSelected.loadingOutput ? false : true, loadingClearTerminal: updates?.loadingClearTerminal ? updates?.loadingClearTerminal : false, }), }; }); updateValueSelectedLines(lineId, updates); }, [], ); const updateValueSelectedLines = ( lineId: number, updates: Partial, ) => { // Update selectedLine nếu nó đang được chọn setSelectedLines((prevSelected) => prevSelected?.map((line) => { if (line.id === lineId) { return { ...line, ...updates }; } return line; }), ); }; const updateValueSelectedLine = (lineId: number, updates: Partial) => { // Update selectedLine nếu nó đang được chọn setSelectedLine((prevSelected) => { if (!prevSelected || prevSelected.id !== lineId) return prevSelected; return { ...prevSelected, ...updates, }; }); }; // const getLine = (lineId: number, stationId: number) => { // const station = stations?.find((sta) => sta.id === stationId); // if (station) { // const line = station.lines?.find((li) => li.id === lineId); // return line; // } else return null; // }; const openTerminal = (line: TLine) => { setOpenModalTerminal(true); const data = { ...line }; if (!line.userOpenCLI) { data.cliOpened = true; data.userEmailOpenCLI = user?.email; data.userOpenCLI = user?.userName; socket?.emit("open_cli", { lineId: line.id, stationId: line.stationId || line.station_id, userEmail: user?.email, userName: user?.userName, }); } setSelectedLine(data); }; useEffect(() => { if (!expandedBottomBar) { setActiveTabBottom("command"); } }, [expandedBottomBar]); const findLineByLineId = (lineId: number, stationId?: number) => { const valueStation = stations.find((el) => el.id === stationId); if (!valueStation || !stationId) return null; const valueLine = valueStation?.lines?.find((el) => el.id === lineId); return valueLine; }; return ( ( {isLoading ? ( ) : station.lines.length > 0 ? ( station.lines.length < 9 ? ( {station.lines.map((line, i) => ( ))} ) : ( // >= 9 lines: chia làm 2 cột, mỗi cột chứa 1/2 số line, // mỗi cột hiển thị 2 item trên một "hàng" như ví dụ yêu cầu (() => { // const total = station.lines.length; const half = 8; const leftLines = station.lines.slice(0, half); const rightLines = station.lines.slice(half); const leftChunks = chunkArray(leftLines, 2); const rightChunks = chunkArray(rightLines, 2); const numRows = Math.max( leftChunks.length, rightChunks.length, ); return ( <> {Array.from({ length: numRows }).map( (_, rowIndex) => { const leftRow = leftChunks[rowIndex] || []; const rightRow = rightChunks[rowIndex] || []; return ( {/* Cột A */} {leftRow.map((line, i) => ( ))} {/* Cột B */} {rightRow.map((line, i) => ( ))} ); }, )} ); })() ) ) : ( No lines configured )} ))} onChange={(id) => { const station = stations.find((el) => el.id === Number(activeTab)); if (station) { (station?.lines || [])?.forEach((el) => { if (el?.userOpenCLI === user?.userName) socket?.emit("close_cli", { lineId: el?.id, stationId: Number(activeTab), }); }); } setActiveTab(id?.toString() || "0"); setSelectedLines([]); setLoadingTerminal(false); setTimeout(() => { setLoadingTerminal(true); }, 500); }} setActive={setActiveTab} active={activeTab} onSendCommand={(value) => { const listLine = selectedLines.length ? selectedLines : stations.find((el) => el.id === Number(activeTab))?.lines; if (listLine?.length) { socket?.emit("write_command_line_from_web", { lineIds: listLine.map((line) => line.id), stationId: Number(activeTab), command: value + "\n", }); setTimeout(() => { socket?.emit("write_command_line_from_web", { lineIds: listLine.map((line) => line.id), stationId: Number(activeTab), command: " \n", }); }, 1000); } }} listBrands={listBrands} listCategories={listCategories} /> { setIsOpenAddStation(false); setIsEditStation(false); setStationEdit(undefined); }} isEdit={isEditStation} setStations={setStations} setActiveTab={(id: string) => { setActiveTab(id); setLoadingTerminal(false); setTimeout(() => { setLoadingTerminal(true); }, 100); }} stations={stations} socket={socket} /> { setOpenModalTerminal(false); setSelectedLine(undefined); }} line={selectedLine} socket={socket} stationItem={stations.find((el) => el.id === Number(activeTab))} scenarios={scenarios} listIos={listIos} listLicense={listLicense} getListIos={getListIos} getListLicense={getListLicense} setScenarios={setScenarios} listBrands={listBrands} listCategories={listCategories} setLinesConfirmSkipPort={setLinesConfirmSkipPort} linesConfirmSkipPort={linesConfirmSkipPort} dataSummaryTested={dataSummaryTested} /> {/* el.id === Number(activeTab))} scenarios={scenarios} /> */} el.id === Number(activeTab))} /> el.id === Number(activeTab))} /> ); } export default function Main() { const user = useMemo(() => { return localStorage.getItem("user") && isJsonString(localStorage.getItem("user")) ? JSON.parse(localStorage.getItem("user") || "") : null; }, []); return ( } > {user ? ( ) : ( )} ); }