Update physical test and load ios

This commit is contained in:
nguyentrungthat 2025-12-31 14:06:49 +07:00
parent ef1ba4ac99
commit e4687f27f9
9 changed files with 904 additions and 460 deletions

View File

@ -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 = ''

View File

@ -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

View File

@ -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)

View File

@ -103,6 +103,7 @@ function App() {
const flushScheduledRef = useRef(false);
const [listBrands, setListBrands] = useState<TBrands[]>([]);
const [listCategories, setListCategories] = useState<TCategories[]>([]);
const [listIos, setListIos] = useState<string[]>([]);
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}
/>
{/* <ModalConfirmRunScenario

View File

@ -394,6 +394,23 @@ const BottomToolBar = ({
>
Scenario
</Button>
<Button
fw={400}
disabled={isDisable || selectedLines.length === 0}
variant="filled"
mr={"5px"}
style={{ height: "30px", width: "100px" }}
onClick={() => {
selectedLines.forEach((line) => {
socket?.emit("run_physical_test", {
lineId: line?.id,
stationId: Number(station.id),
});
});
}}
>
Physical
</Button>
<DrawerLogs
socket={socket}
isLogModalOpen={isLogModalOpen}

View File

@ -308,7 +308,7 @@ const CardLine = ({
/>
)}
</div>
{line?.connecting && (
{line?.connecting && line?.status !== "connected" && (
<motion.div
style={{ fontSize: "11px", color: "red" }}
animate={{ opacity: [0.2, 1, 0.2] }}
@ -686,14 +686,28 @@ const CardLine = ({
/>
</Box>
<Box>
<div className={classes.info_line}>
Latest: {line?.latestScenario?.name || ""}
<Text style={{ fontStyle: "italic", fontSize: "11px" }}>
{line?.latestScenario?.time
? "(" + convertTimestampToDate(line?.latestScenario?.time) + ")"
: ""}
</Text>
</div>
<Flex justify={"space-between"}>
<div className={classes.info_line}>
Latest: {line?.latestScenario?.name || ""}
<Text style={{ fontStyle: "italic", fontSize: "11px" }}>
{line?.latestScenario?.time
? "(" +
convertTimestampToDate(line?.latestScenario?.time) +
")"
: ""}
</Text>
</div>
<div style={{ display: line?.runningPhysical ? "" : "none" }}>
<Text className={classes.info_line}>
Ports Tested{" "}
{line?.ports?.length
? `(${line?.listPortsPhysical?.length || 0}/${
line?.ports?.length || 0
})`
: ""}
</Text>
</div>
</Flex>
</Box>
</Flex>
</Card>

View File

@ -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<boolean>(false);
const [inputSearch, setInputSearch] = useState<string>("");
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 (
<Modal
style={{ position: "absolute", left: 0 }}
opened={opened}
onClose={() => {
close();
}}
title={
<Text fz={"lg"} fw={"bolder"}>
Select IOS
</Text>
}
size="xl"
>
<Tabs defaultValue="router" h={"100%"}>
<Tabs.List>
<Tabs.Tab w={"50%"} value="router">
Router
</Tabs.Tab>
<Tabs.Tab w={"50%"} value="switch">
Switch
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="router" w={"100%"}>
<Flex justify={"space-between"} align={"center"} mt={"xs"}>
<TextInput
style={{ width: "350px" }}
placeholder="Search file name"
value={inputSearch}
onChange={(event) => setInputSearch(event.currentTarget.value)}
rightSection={
inputSearch ? (
<IconX
size={14}
style={{ cursor: "pointer" }}
onClick={() => setInputSearch("")}
/>
) : null
}
rightSectionPointerEvents="auto"
size="xs"
/>
<Checkbox
label="Reboot and send Break"
style={{ color: "red" }}
checked={isReboot}
onChange={(event) => setIsReboot(event.currentTarget.checked)}
/>
</Flex>
<ScrollArea h={"70vh"} style={{ marginTop: "15px" }}>
<Table
stickyHeader
striped
highlightOnHover
withRowBorders={true}
withTableBorder={true}
withColumnBorders={true}
>
<Table.Thead
style={{
top: 1,
}}
>
<Table.Tr>
<Table.Th
style={{
backgroundColor: "#94c6ff",
}}
>
Name
</Table.Th>
<Table.Th
style={{
width: "70px",
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Action
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filterIos("router")?.map((ios, i) => (
<Table.Tr key={i}>
<Table.Td>{ios || ""}</Table.Td>
<Table.Td
style={{
textAlign: "center",
}}
>
<Button
style={{ width: "100px" }}
variant="light"
color="green"
size="sm"
leftSection={<IconPlayerPlay size={16} />}
onClick={() => {
if (isReboot)
socket?.emit("control_apc", {
outletNumbers: [line?.outlet],
station: { ...station, lines: [] },
action: "restart",
apcName: line?.apc_name || line?.apcName,
});
socket?.emit("load_ios_router", {
stationId: Number(station?.id),
lineId: Number(line?.id),
iosName: ios,
});
close();
}}
>
Run
</Button>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Tabs.Panel>
<Tabs.Panel value="switch" w={"100%"}>
<Flex justify={"space-between"} align={"center"} mt={"xs"}>
<TextInput
style={{ width: "350px" }}
placeholder="Search file name"
value={inputSearch}
onChange={(event) => setInputSearch(event.currentTarget.value)}
rightSection={
inputSearch ? (
<IconX
size={14}
style={{ cursor: "pointer" }}
onClick={() => setInputSearch("")}
/>
) : null
}
rightSectionPointerEvents="auto"
size="xs"
/>
</Flex>
<ScrollArea h={"70vh"} style={{ marginTop: "15px" }}>
<Table
stickyHeader
striped
highlightOnHover
withRowBorders={true}
withTableBorder={true}
withColumnBorders={true}
>
<Table.Thead
style={{
top: 1,
}}
>
<Table.Tr>
<Table.Th
style={{
backgroundColor: "#94c6ff",
}}
>
Name
</Table.Th>
<Table.Th
style={{
width: "70px",
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Action
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filterIos("switch")?.map((ios, i) => (
<Table.Tr key={i}>
<Table.Td>{ios || ""}</Table.Td>
<Table.Td
style={{
textAlign: "center",
}}
>
<Button
style={{ width: "100px" }}
variant="light"
color="green"
size="sm"
leftSection={<IconPlayerPlay size={16} />}
onClick={() => {
socket?.emit("load_ios_switch", {
stationId: Number(station?.id),
lineId: Number(line?.id),
iosName: ios,
});
close();
}}
>
Run
</Button>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Tabs.Panel>
</Tabs>
</Modal>
);
};
export default ModalSelectIOS;

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,7 @@ export type TLine = {
scenario?: IScenario;
loadingClearTerminal?: boolean;
runningPhysical?: boolean;
listPortsPhysical?: string[];
};
export type TUser = {