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 || ""} - + + - - - - ))} - -
-
+ + + + ))} + + + + )} @@ -197,71 +261,109 @@ const ModalSelectIOS = ({ size="xs" /> - - - + + ) : ( + +
- - - Name - - - Action - - - - - {filterIos("switch")?.map((ios, i) => ( - - {ios || ""} - + + - - - - ))} - -
-
+ + + + ))} + + + + )}
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 || ""} - + + - - - - ))} - -
-
+ {bytesToKB(lic?.fileSize) || "0"} KB + + + + + + ))} + + + + )} + 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