diff --git a/BACKEND/app/controllers/ios_license_controller.ts b/BACKEND/app/controllers/ios_license_controller.ts
index bb64a8b..5f0d6d9 100644
--- a/BACKEND/app/controllers/ios_license_controller.ts
+++ b/BACKEND/app/controllers/ios_license_controller.ts
@@ -1,18 +1,51 @@
import fs from 'node:fs'
+import path from 'node:path'
+
+interface FileInfo {
+ name: string
+ fileSize: number
+ dateModify: number
+}
export default class IosLicenseController {
/* ================= HELPER ================= */
- private getBinFiles(dir: string): string[] {
+ private getBinFiles(dir: string): FileInfo[] {
if (!fs.existsSync(dir)) return []
- return fs.readdirSync(dir).filter((file) => file.toLowerCase().endsWith('.bin'))
+ return fs
+ .readdirSync(dir)
+ .filter((file) => file.toLowerCase().endsWith('.bin'))
+ .map((file) => {
+ const fullPath = path.join(dir, file)
+ const stat = fs.statSync(fullPath)
+
+ return {
+ name: file,
+ fileSize: stat.size,
+ dateModify: stat.mtime.getTime(),
+ }
+ })
+ .sort((a, b) => b.dateModify - a.dateModify)
}
- private getLicFiles(dir: string): string[] {
+ private getLicFiles(dir: string): FileInfo[] {
if (!fs.existsSync(dir)) return []
- return fs.readdirSync(dir).filter((file) => file.toLowerCase().endsWith('.lic'))
+ return fs
+ .readdirSync(dir)
+ .filter((file) => file.toLowerCase().endsWith('.lic'))
+ .map((file) => {
+ const fullPath = path.join(dir, file)
+ const stat = fs.statSync(fullPath)
+
+ return {
+ name: file,
+ fileSize: stat.size,
+ dateModify: stat.mtime.getTime(),
+ }
+ })
+ .sort((a, b) => b.dateModify - a.dateModify)
}
/* ================= IOS ================= */
diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts
index bf0b1de..b8e326d 100644
--- a/BACKEND/app/services/line_connection.ts
+++ b/BACKEND/app/services/line_connection.ts
@@ -127,6 +127,7 @@ export default class LineConnection {
private session: TestSession
public physicalTest: PhysicalPortTest
private outputPhysicalTest: string
+ private outputLoadIosLicense: string | boolean
private listDeviceIos: string[]
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
@@ -155,6 +156,7 @@ export default class LineConnection {
this.handleClearLine = handleClearLine
this.physicalTest = new PhysicalPortTest([])
this.outputPhysicalTest = ''
+ this.outputLoadIosLicense = ''
this.listDeviceIos = []
}
/**
@@ -200,6 +202,10 @@ export default class LineConnection {
if (!this.config.inventory)
this.outputInventory = this.outputInventory.slice(-3000) + message
}
+ if (this.outputLoadIosLicense) {
+ if (this.outputLoadIosLicense === true) this.outputLoadIosLicense = ''
+ this.outputLoadIosLicense += message
+ }
if (this.config.runningPhysical) {
this.outputPhysicalTest += message
const ports = this.physicalTest.handleLog(message)
@@ -1130,6 +1136,7 @@ export default class LineConnection {
async loadIosRouter(nameIos: string, userName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
+ this.outputLoadIosLicense = true
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
const [a, b] = network.split('.').map(Number)
@@ -1165,6 +1172,7 @@ export default class LineConnection {
async loadIosSwitch(nameIos: string, userName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
+ this.outputLoadIosLicense = true
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
const [a, b] = network.split('.').map(Number)
@@ -1214,8 +1222,14 @@ export default class LineConnection {
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Load IOS Report`,
- body
+ body +
+ `${`
+
+ Logs:
+
+ ${this.outputLoadIosLicense}
`}`
)
+ this.outputLoadIosLicense = ''
}
/**
@@ -1237,8 +1251,14 @@ export default class LineConnection {
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Load License Report`,
- body
+ body +
+ `${`
+
+ Logs:
+
+ ${this.outputLoadIosLicense}
`}`
)
+ this.outputLoadIosLicense = ''
}
/**
@@ -1352,7 +1372,7 @@ export default class LineConnection {
// console.log(`SKIP active IOS: ${ios}`)
// continue
// }
- if (listIos?.includes(ios)) {
+ if (listIos?.map((value) => value.name)?.includes(ios)) {
console.log(`Already backed up: ${ios}`)
if (ios !== nameIos) await this.deleteFileOnFlash(ios)
} else {
@@ -1373,7 +1393,7 @@ export default class LineConnection {
async loadLicenseSwitch(licenseFileName: string, userName: string, portName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
-
+ this.outputLoadIosLicense = true
// Setup network variables (giống hệt logic load IOS để đảm bảo thông mạng)
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
@@ -1413,7 +1433,7 @@ export default class LineConnection {
async loadLicenseRouter(licenseFileName: string, userName: string, portName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
-
+ this.outputLoadIosLicense = true
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
const [a, b] = network.split('.').map(Number)
diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts
index fc56b93..0cc1250 100644
--- a/BACKEND/app/ultils/helper.ts
+++ b/BACKEND/app/ultils/helper.ts
@@ -768,6 +768,13 @@ export function buildBody(
repeat: '1',
note: '',
},
+ {
+ expect: '#',
+ send: `show inventory`,
+ delay: '1',
+ repeat: '1',
+ note: '',
+ },
{
expect: '#',
send: `show license`,
@@ -777,15 +784,8 @@ export function buildBody(
},
{
expect: '#',
- send: ` show inventory`,
- delay: '1',
- repeat: '1',
- note: '',
- },
- {
- expect: '#',
- send: `show version`,
- delay: '1',
+ send: ` show version`,
+ delay: '3',
repeat: '1',
note: '',
},
@@ -998,7 +998,7 @@ export function buildBody(
{
expect: '#',
send: ` show version`,
- delay: '1',
+ delay: '3',
repeat: '1',
note: 'Verify version info',
},
@@ -1073,7 +1073,7 @@ export function buildBody(
{
expect: '',
send: ``,
- delay: '1',
+ delay: '2',
repeat: '1',
note: '',
},
@@ -1159,7 +1159,7 @@ export function buildBody(
{
expect: '#',
send: ` show version`,
- delay: '1',
+ delay: '3',
repeat: '1',
note: 'Verify version info',
},
@@ -1234,7 +1234,7 @@ export function buildBody(
{
expect: '',
send: ``,
- delay: '1',
+ delay: '2',
repeat: '1',
note: '',
},
@@ -1273,6 +1273,13 @@ export function buildBody(
repeat: '1',
note: 'Reload router',
},
+ {
+ expect: '',
+ send: `yes`,
+ delay: '1',
+ repeat: '1',
+ note: 'Confirm reload',
+ },
{
expect: '',
send: ``,
@@ -1320,7 +1327,7 @@ export function buildBody(
{
expect: '#',
send: ` show version`,
- delay: '1',
+ delay: '3',
repeat: '1',
note: 'Verify version info',
},
diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx
index d105a61..c621ff2 100644
--- a/FRONTEND/src/App.tsx
+++ b/FRONTEND/src/App.tsx
@@ -24,6 +24,7 @@ import {
LoadingOverlay,
} from "@mantine/core";
import type {
+ FileInfo,
IScenario,
ReceivedFile,
ResponseData,
@@ -103,8 +104,8 @@ function App() {
const flushScheduledRef = useRef(false);
const [listBrands, setListBrands] = useState([]);
const [listCategories, setListCategories] = useState([]);
- const [listIos, setListIos] = useState([]);
- const [listLicense, setListLicense] = useState([]);
+ const [listIos, setListIos] = useState([]);
+ const [listLicense, setListLicense] = useState([]);
const connectApcSwitch = (station: TStation) => {
if (station?.apc_1_ip && station?.apc_1_port) {
@@ -886,6 +887,8 @@ function App() {
scenarios={scenarios}
listIos={listIos}
listLicense={listLicense}
+ getListIos={getListIos}
+ getListLicense={getListLicense}
/>
{/* void;
line: TLine | undefined;
+ getListIos: () => void;
}) => {
const [isReboot, setIsReboot] = useState(true);
const [inputSearch, setInputSearch] = useState("");
+ const [isDisable, setIsDisable] = useState(false);
const filterIos = (type: string = "") => {
// Switch: Ưu tiên các dòng 4 chữ số cụ thể
@@ -43,16 +50,18 @@ const ModalSelectIOS = ({
/^(c8\d{2}|c18|c19|c28|c29(?!60)|c38(?!50)|c39|isr|asr)/i;
return listIos
- .filter((name) => {
+ .filter((ios) => {
if (type === "switch") {
- return switchRegex.test(name);
+ return switchRegex.test(ios.name);
}
if (type === "router") {
- return routerRegex.test(name);
+ return routerRegex.test(ios.name);
}
return false;
})
- .filter((ios) => ios.toLowerCase().includes(inputSearch.toLowerCase()));
+ .filter((ios) =>
+ ios.name.toLowerCase().includes(inputSearch.toLowerCase())
+ );
};
return (
@@ -65,11 +74,28 @@ const ModalSelectIOS = ({
setIsReboot(true);
}}
title={
-
- Select IOS
-
+
+
+ Select IOS
+
+
+
}
- size="xl"
+ size="55%"
>
@@ -106,76 +132,114 @@ const ModalSelectIOS = ({
onChange={(event) => setIsReboot(event.currentTarget.checked)}
/>
-
-
-
+
+ ) : (
+
+
-
-
- Name
-
-
- Action
-
-
-
-
- {filterIos("router")?.map((ios, i) => (
-
- {ios || ""}
-
+
+
- }
- onClick={() => {
- const payload = {
- stationId: Number(station?.id),
- lineId: Number(line?.id),
- iosName: ios,
- station: station,
- outletNumber: line?.outlet || -1,
- apcName: line?.apcName || line?.apc_name,
- isReboot: isReboot,
- };
- socket?.emit("load_ios_router", payload);
- close();
+ Name
+
+
+ Date
+
+
+ Size
+
+
+ Action
+
+
+
+
+ {filterIos("router")?.map((ios, i) => (
+
+ {ios?.name || ""}
+
+ {moment(ios?.dateModify).format(
+ "YYYY/MM/DD HH:mm:ss"
+ ) || ""}
+
+ {bytesToMB(ios?.fileSize) || "0"} MB
+
- Run
-
-
-
- ))}
-
-
-
+ }
+ onClick={() => {
+ const payload = {
+ stationId: Number(station?.id),
+ lineId: Number(line?.id),
+ iosName: ios,
+ station: station,
+ outletNumber: line?.outlet || -1,
+ apcName: line?.apcName || line?.apc_name,
+ isReboot: isReboot,
+ };
+ socket?.emit("load_ios_router", payload);
+ close();
+ }}
+ >
+ Run
+
+
+
+ ))}
+
+
+
+ )}
@@ -197,71 +261,109 @@ const ModalSelectIOS = ({
size="xs"
/>
-
-
-
+
+ ) : (
+
+
-
-
- Name
-
-
- Action
-
-
-
-
- {filterIos("switch")?.map((ios, i) => (
-
- {ios || ""}
-
+
+
- }
- onClick={() => {
- socket?.emit("load_ios_switch", {
- stationId: Number(station?.id),
- lineId: Number(line?.id),
- iosName: ios,
- });
- close();
+ Name
+
+
+ Date
+
+
+ Size
+
+
+ Action
+
+
+
+
+ {filterIos("switch")?.map((ios, i) => (
+
+ {ios?.name || ""}
+
+ {moment(ios?.dateModify).format(
+ "YYYY/MM/DD HH:mm:ss"
+ ) || ""}
+
+ {bytesToMB(ios?.fileSize) || "0"} MB
+
- Run
-
-
-
- ))}
-
-
-
+ }
+ onClick={() => {
+ socket?.emit("load_ios_switch", {
+ stationId: Number(station?.id),
+ lineId: Number(line?.id),
+ iosName: ios,
+ });
+ close();
+ }}
+ >
+ Run
+
+
+
+ ))}
+
+
+
+ )}
diff --git a/FRONTEND/src/components/Modal/ModalSelectLicense.tsx b/FRONTEND/src/components/Modal/ModalSelectLicense.tsx
index 05d7866..a2f8e59 100644
--- a/FRONTEND/src/components/Modal/ModalSelectLicense.tsx
+++ b/FRONTEND/src/components/Modal/ModalSelectLicense.tsx
@@ -1,6 +1,8 @@
import {
+ Box,
Button,
Flex,
+ Loader,
Modal,
ScrollArea,
Table,
@@ -9,9 +11,11 @@ import {
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";
+import type { FileInfo, TLine, TStation } from "../../untils/types";
+import { IconPlayerPlay, IconRepeat, IconX } from "@tabler/icons-react";
+import { useEffect, useState } from "react";
+import moment from "moment";
+import { bytesToKB } from "../../untils/helper";
const ModalSelectLicense = ({
socket,
@@ -20,22 +24,33 @@ const ModalSelectLicense = ({
opened,
close,
line,
+ getListLicense,
}: {
socket: Socket | null;
station: TStation | undefined;
- listLicense: string[];
+ listLicense: FileInfo[];
opened: boolean;
close: () => void;
line: TLine | undefined;
+ getListLicense: () => void;
}) => {
const [inputSearch, setInputSearch] = useState("");
const [inputPort, setInputPort] = useState("GigabitEthernet0/0");
const [licenseName, setLicenseName] = useState("");
const [modalConfirm, setModalConfirm] = useState(false);
+ const [isDisable, setIsDisable] = useState(false);
+
+ useEffect(() => {
+ if (opened) {
+ if (line?.inventory?.sn) setInputSearch(line?.inventory?.sn);
+ } else {
+ setInputSearch("");
+ }
+ }, [opened]);
const filterLicense = () => {
- return listLicense.filter((ios) =>
- ios.toLowerCase().includes(inputSearch.toLowerCase())
+ return listLicense.filter((lic) =>
+ lic.name.toLowerCase().includes(inputSearch.toLowerCase())
);
};
@@ -48,11 +63,28 @@ const ModalSelectLicense = ({
setInputSearch("");
}}
title={
-
- Select License
-
+
+
+ Select License
+
+
+
}
- size="xl"
+ size="55%"
>
-
-
-
+
+ ) : (
+
+
-
-
- Name
-
-
- Action
-
-
-
-
- {filterLicense()?.map((lic, i) => (
-
- {lic || ""}
-
+
+
- }
- onClick={() => {
- // socket?.emit("load_ios_switch", {
- // stationId: Number(station?.id),
- // lineId: Number(line?.id),
- // iosName: ios,
- // });
- // close();
- setLicenseName(lic);
- setModalConfirm(true);
+ Name
+
+
+ Date
+
+
+ Size
+
+
+ Action
+
+
+
+
+ {filterLicense()?.map((lic, i) => (
+
+ {lic?.name || ""}
+
+ {moment(lic?.dateModify).format("YYYY/MM/DD HH:mm:ss") ||
+ ""}
+
+
- Run
-
-
-
- ))}
-
-
-
+ {bytesToKB(lic?.fileSize) || "0"} KB
+
+
+ }
+ onClick={() => {
+ // socket?.emit("load_ios_switch", {
+ // stationId: Number(station?.id),
+ // lineId: Number(line?.id),
+ // iosName: ios,
+ // });
+ // close();
+ setLicenseName(lic?.name);
+ setModalConfirm(true);
+ }}
+ >
+ Run
+
+
+
+ ))}
+
+
+
+ )}
+ Port Name:{" "}
+ Port Name:
void;
@@ -74,8 +77,10 @@ const ModalTerminal = ({
stationItem: TStation | undefined;
scenarios: IScenario[];
selectedLines: TLine[];
- listIos: string[];
- listLicense: string[];
+ listIos: FileInfo[];
+ listLicense: FileInfo[];
+ getListLicense: () => void;
+ getListIos: () => void;
}) => {
const user = useMemo(() => {
return localStorage.getItem("user") &&
@@ -1210,18 +1215,20 @@ const ModalTerminal = ({
size="xs"
onClick={() => {
setOpenSelectIos(true);
+ getListIos();
}}
>
Select IOS