This commit is contained in:
nguyentrungthat 2025-10-30 16:23:08 +07:00
parent 00621e2cbe
commit fb1554d857
15 changed files with 684 additions and 9 deletions

View File

@ -1,5 +1,14 @@
import { textfsmResults } from './../ultils/templates/index.js'
import fs from 'node:fs'
import net from 'node:net'
import { appendLog, cleanData, sleep } from '../ultils/helper.js'
import {
appendLog,
cleanData,
getLogWithTimeScenario,
getPathLog,
isValidJson,
sleep,
} from '../ultils/helper.js'
import Scenario from '#models/scenario'
interface LineConfig {
@ -14,6 +23,11 @@ interface LineConfig {
openCLI: boolean
userEmailOpenCLI: string
userOpenCLI: string
data: {
command: string
output: string
textfsm: string
}[]
}
interface User {
@ -163,8 +177,9 @@ export default class LineConnection {
}
this.isRunningScript = true
const now = Date.now()
appendLog(
`\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`,
`\n\n---start-scenarios---${now}---\n---scenario---${script?.title}---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
@ -183,7 +198,7 @@ export default class LineConnection {
data: 'Timeout run scenario',
})
appendLog(
`\n---end-scenarios---${Date.now()}---\n`,
`\n---end-scenarios---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
@ -197,18 +212,47 @@ export default class LineConnection {
this.isRunningScript = false
this.outputBuffer = ''
appendLog(
`\n---end-scenarios---${Date.now()}---\n`,
`\n---end-scenarios---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
)
const pathLog = getPathLog(
this.config.stationId,
this.config.lineNumber,
this.config.port
)
if (pathLog)
fs.readFile(pathLog, 'utf8', async (err, content) => {
if (err) return
const logScenarios = getLogWithTimeScenario(content, now) || ''
const data = await textfsmResults(logScenarios, '')
try {
data.forEach((item) => {
if (item?.textfsm && isValidJson(item?.textfsm)) {
item.textfsm = JSON.parse(item.textfsm)
}
})
this.config.data = data
this.socketIO.emit('data_textfsm', {
stationId: this.config.stationId,
lineId: this.config.id,
data,
})
} catch (error) {
console.log(error)
}
})
resolve(true)
return
}
const step = steps[index]
appendLog(
`\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`,
`\n---send-command---"${step?.send ?? ''}"---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port

View File

@ -36,3 +36,68 @@ export function appendLog(output: string, stationId: number, lineNumber: number,
}
})
}
export const getPathLog = (stationId: number, lineNumber: number, port: number) => {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
const logDir = path.join('storage', 'system_logs')
const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineNumber}_${port}.log`)
// Ensure folder exists
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
return null
} else return logFile
}
/**
* Utility function get scope log with timestamp.
* @param {string} text - content log.
* @param {number} time - Timestamp.
*/
export const getLogWithTimeScenario = (text: string, time: number) => {
try {
// Match all start and end blocks
const regex = /---(start|end)-scenarios---(\d+)---/g
let match
const blocks = []
while ((match = regex.exec(text)) !== null) {
blocks.push({
type: match[1],
timestamp: match[2],
index: match.index,
})
}
// Find the matching block for the end timestamp
let result = null
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i]
if (block.type === 'end' && block.timestamp === time.toString()) {
// Find nearest preceding "start"
for (let j = i - 1; j >= 0; j--) {
if (blocks[j].type === 'start') {
const startIndex = blocks[j].index
const endIndex = block.index + text.slice(block.index).indexOf('\n') // or manually offset length of the line
result = text.slice(startIndex, endIndex).trim()
break
}
}
break
}
}
return result
} catch (err) {
console.error('Error get log:', err)
return ''
}
}
export function isValidJson(string: string) {
try {
JSON.parse(string)
return true // Chuỗi là định dạng JSON hợp lệ
} catch (e) {
return false // Chuỗi không phải là định dạng JSON hợp lệ
}
}

View File

@ -0,0 +1,78 @@
import showInventory from './show_inventory.js'
import showVersion from './show_version.js'
import showLicense from './show_license.js'
import showLogging from './show_logging.js'
// const showPower = require("./show_power.js");
// Function to parse logs
function getStructuredDataTextfsm(output: string, command: string) {
switch (command) {
case 'show inventory':
case 'show inv':
case 'sh inventory':
case 'sh inv':
return showInventory(output)
case 'show version':
case 'show ver':
case 'sh version':
case 'sh ver':
return showVersion(output)
case 'show license':
case 'sh license':
return showLicense(output)
case 'show logging':
case 'sh logging':
return showLogging(output)
default:
return ''
}
// Call the parseLog method with log data and patterns
}
export const textfsmResults = (logContent: string, cmd: string) => {
let results = []
if (cmd) {
let structuredOutput = getStructuredDataTextfsm(logContent, cmd)
if (typeof structuredOutput === 'string') {
structuredOutput = {} // Convert to an empty object if it's a string
}
results = [
{
command: cmd,
output: logContent,
textfsm: JSON.stringify(structuredOutput).replace(/[\x00-\x1f\x7f-\x9f]/g, ''),
},
]
} else {
// Regular expression to parse commands and outputs inside the scoped content
const regexPattern =
/---send-command---"(?<command>.+?)"---\d+---(?<output>[\s\S]*?)(?=(---send-command---|---split-point---|---end-testing---|---end-scenarios---))/gms
// Parse commands and outputs
const matches = [...logContent.matchAll(regexPattern)]
// Process matches
results = matches
.map((match) => {
const command = match.groups?.command.trim() || ''
const output = match.groups?.output.trim() || ''
// Get structured output using the parser
let structuredOutput = getStructuredDataTextfsm(output, command)
if (typeof structuredOutput === 'string') {
structuredOutput = {} // Convert to an empty object if it's a string
}
return {
command,
output,
textfsm: JSON.stringify(structuredOutput).replace(/[\x00-\x1f\x7f-\x9f]/g, ''), // Clean special characters
}
})
.filter((el) => el.command)
}
return results
}

View File

@ -0,0 +1,130 @@
import XRegExp from 'xregexp'
// Parse the log data
const parseLog = (data: string) => {
const patterns = [
XRegExp('^NAME:\\s+"(?<name>.*)",\\s+DESCR:\\s+"(?<descr>.*)"'),
XRegExp('^PID:\\s+(?<pid>[\\S+]+|.*),.*VID:\\s+(?<vid>.*),.*SN:\\s+(?<sn>[\\w+\\d+]*)'),
XRegExp('^PID:\\s+,.*VID:\\s+(?<vid>.*),.*SN:\\s+(?<sn>[\\w+\\d+]*)'),
XRegExp('^PID:\\s+(?<pid>[\\S+]+|.*),.*VID:\\s+(?<vid>.*),.*SN:'),
XRegExp('^PID:\\s+(?<pid>\\S+)(?:,|\\s+)VID:\\s+(?<vid>\\S+)(?:,|\\s+)SN:\\s+(?<sn>\\w+)'),
// License info
XRegExp('^License Level:\\s+(?<licenseLevel>.*)'),
XRegExp('^License Type:\\s+(?<licenseType>.*)'),
XRegExp('^Next reload license Level:\\s+(?<nextLicenseLevel>.*)'),
]
const lines = data.split('\n')
const licenseLog = data.split('show version | include License')
let records: any = []
let currentRecord: any = {
name: '',
descr: '',
pid: '',
vid: '',
sn: '',
licenseLevel: '',
licenseType: '',
nextLicenseLevel: '',
}
for (const line of lines) {
for (const pattern of patterns) {
const match = XRegExp.exec(line, pattern)
if (match) {
const item = match?.groups
// Update current record with matched fields
Object.keys(currentRecord).forEach((key) => {
if (item && item[key] !== undefined) {
currentRecord[key] = item[key].trim()
}
})
// If "pid", "vid", or "sn" are matched, push a completed record
if (currentRecord.pid || currentRecord.vid || currentRecord.sn) {
records.push({ ...currentRecord })
currentRecord = {
name: '',
descr: '',
pid: '',
vid: '',
sn: '',
licenseLevel: '',
licenseType: '',
nextLicenseLevel: '',
} // Reset for the next record
}
if ((currentRecord.licenseLevel || currentRecord.licenseType) && records.length > 0) {
const value = records[0]
value.licenseLevel = currentRecord.licenseLevel
? currentRecord.licenseLevel
: value.licenseLevel
value.licenseType = currentRecord.licenseType
? currentRecord.licenseType
: value.licenseType
value.nextLicenseLevel = currentRecord.nextLicenseLevel
? currentRecord.nextLicenseLevel
: value.nextLicenseLevel
records = [value]
currentRecord = {
name: '',
descr: '',
pid: '',
vid: '',
sn: '',
licenseLevel: '',
licenseType: '',
nextLicenseLevel: '',
} // Reset for the next record
}
break // Stop checking other patterns for this line
}
}
}
if (records.length > 0) {
let extend = null
const firstRecord = records[0]
// check license and last 2 chars of pid
if (
firstRecord.licenseLevel &&
typeof firstRecord.licenseLevel === 'string' &&
firstRecord.pid.length >= 2
) {
switch (firstRecord.licenseLevel.toLowerCase()) {
case 'network essentials':
case 'network-essentials':
case 'dna essentials':
case 'dna-essentials':
extend = '-E'
break
case 'network advantage':
case 'network-advantage':
case 'dna advantage':
case 'dna-advantage':
extend = '-A'
break
default:
break
}
}
if (licenseLog[1] && !firstRecord.licenseLevel) {
if (licenseLog[1]?.includes('essentials')) {
extend = '-E'
firstRecord.licenseLevel = 'network-essentials'
firstRecord.licenseType = 'Smart License'
} else if (licenseLog[1]?.includes('advantage')) {
extend = '-A'
firstRecord.licenseLevel = 'network-advantage'
firstRecord.licenseType = 'Smart License'
}
}
if (extend) {
const key = firstRecord.pid.slice(-2)
if (key !== extend) firstRecord.pid = firstRecord.pid + extend
records = [firstRecord]
}
}
return records
}
export default parseLog

View File

@ -0,0 +1,62 @@
import XRegExp from 'xregexp'
// Patterns for each field
// Parser function
const parseLog = (data: string) => {
const patterns = [
XRegExp('^Index\\s+\\d+\\s+Feature:\\s+(?<FEATURE>\\S+)'),
XRegExp('^Period\\s+left:\\s+(?<PERIOD_LEFT>.+)'),
XRegExp('^Period\\s+Used:\\s+(?<PERIOD_USED>.+)'),
XRegExp('^License\\s+Type:\\s+(?<LICENSE_TYPE>.+)'),
XRegExp('^License\\s+State:\\s+(?<LICENSE_STATE>.+)'),
XRegExp('^License\\s+Count:\\s+(?<LICENSE_COUNT>.+)'),
XRegExp('^License\\s+Priority:\\s+(?<LICENSE_PRIORITY>.+)'),
]
const lines = data.split('\n')
const records = []
let currentRecord: any = null
for (const line of lines) {
if (XRegExp.test(line, XRegExp('^Index\\s+\\d+\\s+Feature:'))) {
// Start a new record
if (currentRecord) {
records.push(currentRecord)
}
currentRecord = {
FEATURE: '',
PERIOD_LEFT: '',
PERIOD_USED: '',
LICENSE_TYPE: '',
LICENSE_STATE: '',
LICENSE_COUNT: '',
LICENSE_PRIORITY: '',
}
}
if (currentRecord) {
for (const pattern of patterns) {
const match = XRegExp.exec(line, pattern)
if (match) {
const item = match?.groups || {}
Object.keys(item).forEach((key) => {
if (item && item[key] !== undefined) {
currentRecord[key] = item[key]
}
})
break // Stop processing this line once a pattern matches
}
}
}
}
// Push the last record if it exists
if (currentRecord) {
records.push(currentRecord)
}
return records
}
export default parseLog

View File

@ -0,0 +1,65 @@
// Import XRegExp
import XRegExp from 'xregexp'
// Example matching function
const parseLog = (data: string) => {
// Define the regex components
const logPattern = XRegExp(
`
^\\*(?<month>\\w{3})\\s+
(?<day>\\d{1,2})\\s+
(?<time>\\d{2}:\\d{2}:\\d{2}\\.\\d{3}):\\s+
%(?<facility>\\w+)-(?<severity>\\d)-(?<mnemonic>\\w+):\\s+
(?<message>.+)
`,
'x'
)
const logPattern2 = XRegExp(
`
^(?<day>\\d{2})-(?<month>\\w{3})-(?<year>\\d{4})\\s+
(?<time>\\d{2}:\\d{2}:\\d{2})\\s+
:%(?<facility>\\w+)-(?<severity>[A-Z0-9])-(?<mnemonic>\\w+):\\s+
(?<message>.+)
`,
'x'
)
// Split log content into individual lines
// const logLines = data.split("\n").filter((line) => line.startsWith("*"));
const logLines = data.split('\n')
// Parse each log line
const logs = logLines
.map((line) => {
const match = XRegExp.exec(line, logPattern)
const match2 = XRegExp.exec(line, logPattern2)
if (match && match.groups) {
return {
month: match.groups.month,
day: match.groups.day,
time: match.groups.time,
facility: match.groups.facility,
severity: match.groups.severity,
mnemonic: match.groups.mnemonic,
message: match.groups.message?.trim() ?? '',
}
} else if (match2 && match2.groups) {
return {
month: match2.groups.month,
day: match2.groups.day,
time: match2.groups.time,
facility: match2.groups.facility,
severity: match2.groups.severity,
mnemonic: match2.groups.mnemonic,
message: match2.groups.message?.trim() ?? '',
}
}
return null // Ignore lines that do not match the pattern
})
.filter(Boolean) // Remove null entries
return logs
}
export default parseLog

View File

@ -0,0 +1,39 @@
import XRegExp from 'xregexp'
// Parse the log data
const parseLog = (data: string) => {
const patterns = [
XRegExp('^NAME:\\s+"(?<name>.*)",\\s+DESCR:\\s+"(?<descr>.*)"'),
XRegExp('^PID:\\s+(?<pid>[\\S+]+|.*),.*VID:\\s+(?<vid>.*),.*SN:\\s+(?<sn>[\\w+\\d+]*)'),
XRegExp('^PID:\\s+,.*VID:\\s+(?<vid>.*),.*SN:\\s+(?<sn>[\\w+\\d+]*)'),
XRegExp('^PID:\\s+(?<pid>[\\S+]+|.*),.*VID:\\s+(?<vid>.*),.*SN:'),
]
const lines = data.split('\n')
const records = []
let currentRecord: any = { name: '', descr: '', pid: '', vid: '', sn: '' }
for (const line of lines) {
for (const pattern of patterns) {
const match = XRegExp.exec(line, pattern)
if (match) {
const item = match?.groups
// Update current record with matched fields
Object.keys(currentRecord).forEach((key) => {
if (item && item[key] !== undefined) {
currentRecord[key] = item[key]
}
})
// If "pid", "vid", or "sn" are matched, push a completed record
if (currentRecord.pid || currentRecord.vid || currentRecord.sn) {
records.push({ ...currentRecord })
currentRecord = { name: '', descr: '', pid: '', vid: '', sn: '' } // Reset for the next record
}
break // Stop checking other patterns for this line
}
}
}
return records
}
export default parseLog

View File

@ -0,0 +1,80 @@
import XRegExp from 'xregexp'
// Parser function
const parseLog = (data: string) => {
const patterns = [
XRegExp(
'^.*Software.*\\((?<SOFTWARE_IMAGE>\\S+)\\),\\s+Version\\s+(?<VERSION>.+?),\\s+RELEASE.*\\((?<RELEASE>\\S+)\\)'
),
XRegExp('Active-image:\\s+(?<SOFTWARE_IMAGE>\\S+)'),
XRegExp('Version:\\s+(?<VERSION>\\S+)'),
XRegExp('^ROM:\\s+(?<ROMMON>\\S+)'),
XRegExp('^(?<HOSTNAME>\\S+)\\s+uptime\\s+is\\s+(?<UPTIME>.+)'),
XRegExp('Date:\\s+(?<UPTIME>\\S+)'),
XRegExp('uptime\\s+is.*\\s+(?<UPTIME_YEARS>\\d+)\\syear'),
XRegExp('uptime\\s+is.*\\s+(?<UPTIME_WEEKS>\\d+)\\sweek'),
XRegExp('uptime\\s+is.*\\s+(?<UPTIME_DAYS>\\d+)\\sday'),
XRegExp('uptime\\s+is.*\\s+(?<UPTIME_HOURS>\\d+)\\shour'),
XRegExp('uptime\\s+is.*\\s+(?<UPTIME_MINUTES>\\d+)\\sminute'),
XRegExp('System\\s+image\\s+file\\s+is\\s+"(?:.*?):(?<RUNNING_IMAGE>\\S+)"'),
XRegExp('(?:Last reload reason:|System returned to ROM by)\\s+(?<RELOAD_REASON>.+)'),
XRegExp('[Pp]rocessor\\s+board\\s+ID\\s+(?<SERIAL>\\w+)'),
XRegExp(
'[Cc]isco\\s+(?<HARDWARE>\\S+|\\S+\\d\\S+)\\s+\\(.+\\)\\s+with\\s+(?<MEMORY>.+)\\s+bytes'
),
// XRegExp("^(?<USB_FLASH>.+)\\s+bytes\\s+of\\s+[Uu][Ss][Bb]+\\s+[Ff]lash"),
XRegExp(
'^(?<USB_FLASH>.+?)\\s+bytes\\s+of\\s+(?:' +
'[Uu][Ss][Bb]+\\s+[Ff]lash' + // USB Flash
'|ATA\\s+System\\s+CompactFlash.*' + // ATA System CompactFlash
'|flash\\s+memory\\s+at\\s+bootflash:' + // flash memory at bootflash
')'
),
XRegExp(
'Base\\s+[Ee]thernet\\s+MAC\\s+[Aa]ddress\\s+:\\s+(?<MAC_ADDRESS>[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})'
),
XRegExp('Configuration\\s+register\\s+is\\s+(?<CONFIG_REGISTER>\\S+)'),
]
const lines = data.split('\n')
const records: any = {
SOFTWARE_IMAGE: '',
VERSION: '',
RELEASE: '',
ROMMON: '',
HOSTNAME: '',
UPTIME: '',
UPTIME_YEARS: '',
UPTIME_WEEKS: '',
UPTIME_DAYS: '',
UPTIME_HOURS: '',
UPTIME_MINUTES: '',
RELOAD_REASON: '',
RUNNING_IMAGE: '',
HARDWARE: '',
SERIAL: '',
CONFIG_REGISTER: '',
MAC_ADDRESS: '',
MEMORY: '',
USB_FLASH: '',
}
for (const line of lines) {
for (const pattern of patterns) {
const match = XRegExp.exec(line, pattern)
if (match) {
const item = match?.groups || {}
Object.keys(item).forEach((key) => {
if (item[key] !== undefined) {
records[key] = item[key]
}
})
break
}
}
}
return [records]
}
export default parseLog

View File

@ -18,7 +18,8 @@
"mysql2": "^3.15.3",
"net": "^1.0.2",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1"
"socket.io": "^4.8.1",
"xregexp": "^5.1.2"
},
"devDependencies": {
"@adonisjs/assembler": "^7.8.2",
@ -648,6 +649,18 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz",
"integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==",
"license": "MIT",
"dependencies": {
"core-js-pure": "^3.43.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@borewit/text-codec": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz",
@ -3107,6 +3120,17 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-js-pure": {
"version": "3.46.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz",
"integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@ -7535,6 +7559,15 @@
}
}
},
"node_modules/xregexp": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.2.tgz",
"integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==",
"license": "MIT",
"dependencies": {
"@babel/runtime-corejs3": "^7.26.9"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",

View File

@ -60,7 +60,8 @@
"mysql2": "^3.15.3",
"net": "^1.0.2",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1"
"socket.io": "^4.8.1",
"xregexp": "^5.1.2"
},
"hotHook": {
"boundaries": [

View File

@ -283,6 +283,7 @@ export class WebSocketIo {
openCLI: false,
userEmailOpenCLI: '',
userOpenCLI: '',
data: [],
},
socket
)

View File

@ -22,6 +22,7 @@ import type {
LineConfig,
ReceivedFile,
ResponseData,
TextFSM,
TLine,
TStation,
TUser,
@ -158,8 +159,8 @@ function App() {
setTimeout(() => {
updateValueLineStation(data.lineId, {
cliOpened: false,
userEmailOpenCLI: "",
userOpenCLI: "",
userEmailOpenCLI: undefined,
userOpenCLI: undefined,
});
}, 100);
});
@ -225,6 +226,14 @@ function App() {
}
});
socket?.on("data_textfsm", (data) => {
setTimeout(() => {
updateValueLineStation(data.lineId, {
data: data.data,
});
}, 100);
});
// ✅ cleanup on unmount or when socket changes
return () => {
socket.off("init");
@ -432,6 +441,59 @@ function App() {
>
Connect
</Button>
<Button
disabled={selectedLines.length === 0}
variant="outline"
style={{ height: "30px", width: "100px" }}
onClick={() => {
if (selectedLines?.length > 0) {
const value = selectedLines
?.map((el) => {
// Get data platform
const dataPlatform = el.data?.find(
(comm: TextFSM) =>
comm.command?.trim() === "show platform"
);
const DPELP =
dataPlatform &&
!dataPlatform?.output?.includes("Incomplete")
? true
: false;
// Get data license
const dataLicense = el.data?.find(
(comm: TextFSM) =>
comm.command?.trim() === "show license" ||
comm.command?.trim() === "sh license"
);
const listLicense =
dataLicense?.textfsm &&
Array.isArray(dataLicense?.textfsm)
? dataLicense?.textfsm
?.map(
(val: { FEATURE: string }) =>
val.FEATURE
)
.join(", ")
: "";
return `Line ${el.line_number ?? ""}: PID: ${
el.inventory?.pid ?? ""
}, SN: ${el.inventory?.sn ?? ""}, VID: ${
el.inventory?.vid ?? ""
}, Tested mode: ${
DPELP ? "DPELP" : "DPEL"
}, License: ${listLicense}`;
})
.join("\n");
navigator.clipboard.writeText(value);
setSelectedLines([]);
}
}}
>
Copy
</Button>
<hr style={{ width: "100%" }} />
<DrawerScenario
scenarios={scenarios}
@ -484,6 +546,7 @@ function App() {
))}
onChange={(id) => {
setActiveTab(id?.toString() || "0");
setSelectedLines([]);
setLoadingTerminal(false);
setTimeout(() => {
setLoadingTerminal(true);

View File

@ -100,6 +100,7 @@ const CardLine = ({
station_id={Number(stationItem.id)}
isDisabled={
typeof line?.userEmailOpenCLI !== "undefined" &&
typeof line?.userEmailOpenCLI !== "string" &&
line?.userEmailOpenCLI !== user?.email
}
line_status={line?.status || ""}

View File

@ -19,3 +19,10 @@ export function isJsonString(str: string | null) {
return false;
}
}
export function mergeArray(array: any[], key: string) {
return array
.map((el) => el[key])
.flat()
.filter((el) => Object.keys(el).length > 0);
}

View File

@ -181,3 +181,9 @@ export type ReceivedFile = {
receivedChunks: number;
totalChunks: number;
};
export type TextFSM = {
command: string;
output: string;
textfsm: any;
};