Update
This commit is contained in:
parent
cbc8397ea8
commit
240dfdff2c
|
|
@ -85,7 +85,12 @@ export default class LineConnection {
|
||||||
lineId: id,
|
lineId: id,
|
||||||
data: message,
|
data: message,
|
||||||
})
|
})
|
||||||
appendLog(cleanData(message), this.config.stationId, this.config.id)
|
appendLog(
|
||||||
|
cleanData(message),
|
||||||
|
this.config.stationId,
|
||||||
|
this.config.lineNumber,
|
||||||
|
this.config.port
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.client.on('error', (err) => {
|
this.client.on('error', (err) => {
|
||||||
|
|
@ -161,7 +166,8 @@ export default class LineConnection {
|
||||||
appendLog(
|
appendLog(
|
||||||
`\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`,
|
`\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`,
|
||||||
this.config.stationId,
|
this.config.stationId,
|
||||||
this.config.id
|
this.config.lineNumber,
|
||||||
|
this.config.port
|
||||||
)
|
)
|
||||||
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
|
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
|
||||||
let stepIndex = 0
|
let stepIndex = 0
|
||||||
|
|
@ -176,7 +182,12 @@ export default class LineConnection {
|
||||||
lineId: this.config.id,
|
lineId: this.config.id,
|
||||||
data: 'Timeout run scenario',
|
data: 'Timeout run scenario',
|
||||||
})
|
})
|
||||||
appendLog(`\n---end-scenarios---${Date.now()}---\n`, this.config.stationId, this.config.id)
|
appendLog(
|
||||||
|
`\n---end-scenarios---${Date.now()}---\n`,
|
||||||
|
this.config.stationId,
|
||||||
|
this.config.lineNumber,
|
||||||
|
this.config.port
|
||||||
|
)
|
||||||
// reject(new Error('Script timeout'))
|
// reject(new Error('Script timeout'))
|
||||||
}, script.timeout || 300000)
|
}, script.timeout || 300000)
|
||||||
|
|
||||||
|
|
@ -188,7 +199,8 @@ export default class LineConnection {
|
||||||
appendLog(
|
appendLog(
|
||||||
`\n---end-scenarios---${Date.now()}---\n`,
|
`\n---end-scenarios---${Date.now()}---\n`,
|
||||||
this.config.stationId,
|
this.config.stationId,
|
||||||
this.config.id
|
this.config.lineNumber,
|
||||||
|
this.config.port
|
||||||
)
|
)
|
||||||
resolve(true)
|
resolve(true)
|
||||||
return
|
return
|
||||||
|
|
@ -198,7 +210,8 @@ export default class LineConnection {
|
||||||
appendLog(
|
appendLog(
|
||||||
`\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`,
|
`\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`,
|
||||||
this.config.stationId,
|
this.config.stationId,
|
||||||
this.config.id
|
this.config.lineNumber,
|
||||||
|
this.config.port
|
||||||
)
|
)
|
||||||
let repeatCount = Number(step.repeat) || 1
|
let repeatCount = Number(step.repeat) || 1
|
||||||
const sendCommand = () => {
|
const sendCommand = () => {
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ export function sleep(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendLog(output: string, stationId: number, lineId: number) {
|
export function appendLog(output: string, stationId: number, lineNumber: number, port: number) {
|
||||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
|
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
|
||||||
const logDir = path.join('storage', 'system_logs')
|
const logDir = path.join('storage', 'system_logs')
|
||||||
const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineId}.log`)
|
const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineNumber}_${port}.log`)
|
||||||
|
|
||||||
// Ensure folder exists
|
// Ensure folder exists
|
||||||
if (!fs.existsSync(logDir)) {
|
if (!fs.existsSync(logDir)) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import fs from 'node:fs'
|
||||||
import { Server as SocketIOServer } from 'socket.io'
|
import { Server as SocketIOServer } from 'socket.io'
|
||||||
import http from 'node:http'
|
import http from 'node:http'
|
||||||
import LineConnection from '../app/services/line_connection.js'
|
import LineConnection from '../app/services/line_connection.js'
|
||||||
|
|
@ -142,7 +143,7 @@ export class WebSocketIo {
|
||||||
this.setTimeoutConnect(
|
this.setTimeoutConnect(
|
||||||
lineId,
|
lineId,
|
||||||
line,
|
line,
|
||||||
scenario?.timeout ? Number(scenario?.timeout) + 120000 : 300000
|
scenario?.timeout ? Number(scenario?.timeout) + 180000 : 300000
|
||||||
)
|
)
|
||||||
line.runScript(scenario)
|
line.runScript(scenario)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -205,6 +206,57 @@ export class WebSocketIo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on('request_take_over', async (data) => {
|
||||||
|
io.emit('confirm_take_over', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('get_list_logs', async () => {
|
||||||
|
let getListSystemLogs = fs
|
||||||
|
.readdirSync('storage/system_logs')
|
||||||
|
.map((f) => 'storage/system_logs/' + f)
|
||||||
|
io.to(socket.id).emit('list_logs', getListSystemLogs)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('get_content_log', async (data) => {
|
||||||
|
try {
|
||||||
|
const { line, socketId } = data
|
||||||
|
const filePath = line.systemLogUrl
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
// Get file stats
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const fileSizeInBytes = stats.size
|
||||||
|
if (fileSizeInBytes / 1024 / 1024 > 0.5) {
|
||||||
|
// File is larger than 0.5 MB
|
||||||
|
const fileId = Date.now() // Mã định danh file
|
||||||
|
const chunkSize = 64 * 1024 // 64KB
|
||||||
|
const fileBuffer = fs.readFileSync(filePath)
|
||||||
|
const totalChunks = Math.ceil(fileBuffer.length / chunkSize)
|
||||||
|
|
||||||
|
for (let i = 0; i < totalChunks; i++) {
|
||||||
|
const chunk = fileBuffer.slice(i * chunkSize, (i + 1) * chunkSize)
|
||||||
|
io.to(socketId).emit('response_content_log', {
|
||||||
|
type: 'chunk',
|
||||||
|
chunk: {
|
||||||
|
fileId,
|
||||||
|
chunkIndex: i,
|
||||||
|
totalChunks,
|
||||||
|
chunk,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(filePath)
|
||||||
|
const content = fs.readFileSync(filePath)
|
||||||
|
socket.emit('response_content_log', content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
io.to(socketId).emit('response_content_log', Buffer.from('File not found', 'utf-8'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
socketServer.listen(SOCKET_IO_PORT, () => {
|
socketServer.listen(SOCKET_IO_PORT, () => {
|
||||||
|
|
@ -244,7 +296,7 @@ export class WebSocketIo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => {
|
private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 180000) => {
|
||||||
if (this.intervalMap[`${lineId}`]) {
|
if (this.intervalMap[`${lineId}`]) {
|
||||||
clearInterval(this.intervalMap[`${lineId}`])
|
clearInterval(this.intervalMap[`${lineId}`])
|
||||||
delete this.intervalMap[`${lineId}`]
|
delete this.intervalMap[`${lineId}`]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
"name": "ATC",
|
"name": "ATC",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mantine/core": "^8.3.5",
|
"@mantine/core": "^8.3.5",
|
||||||
"@mantine/dates": "^8.3.5",
|
"@mantine/dates": "^8.3.5",
|
||||||
"@mantine/form": "^8.3.5",
|
"@mantine/form": "^8.3.5",
|
||||||
|
|
@ -16,6 +19,7 @@
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
|
|
@ -24,7 +28,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
"@types/node": "^24.9.1",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "^19.1.16",
|
"@types/react": "^19.1.16",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
|
@ -328,6 +332,59 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.11",
|
"version": "0.25.11",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
||||||
|
|
@ -1609,9 +1666,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.9.1",
|
"version": "24.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
|
||||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -3273,6 +3330,15 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mantine/core": "^8.3.5",
|
"@mantine/core": "^8.3.5",
|
||||||
"@mantine/dates": "^8.3.5",
|
"@mantine/dates": "^8.3.5",
|
||||||
"@mantine/form": "^8.3.5",
|
"@mantine/form": "^8.3.5",
|
||||||
|
|
@ -18,6 +21,7 @@
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
|
|
@ -26,7 +30,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
"@types/node": "^24.9.1",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "^19.1.16",
|
"@types/react": "^19.1.16",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,30 @@ import "@mantine/notifications/styles.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import classes from "./App.module.css";
|
import classes from "./App.module.css";
|
||||||
|
|
||||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
Container,
|
Container,
|
||||||
Flex,
|
Flex,
|
||||||
MantineProvider,
|
MantineProvider,
|
||||||
FloatingIndicator,
|
|
||||||
Grid,
|
Grid,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Button,
|
Button,
|
||||||
ActionIcon,
|
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
Avatar,
|
|
||||||
Tooltip,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import type {
|
import type {
|
||||||
|
IDataTakeOver,
|
||||||
IScenario,
|
IScenario,
|
||||||
LineConfig,
|
LineConfig,
|
||||||
|
ReceivedFile,
|
||||||
|
ResponseData,
|
||||||
TLine,
|
TLine,
|
||||||
TStation,
|
TStation,
|
||||||
TUser,
|
TUser,
|
||||||
} from "./untils/types";
|
} from "./untils/types";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import CardLine from "./components/CardLine";
|
import CardLine from "./components/CardLine";
|
||||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
|
||||||
import { SocketProvider, useSocket } from "./context/SocketContext";
|
import { SocketProvider, useSocket } from "./context/SocketContext";
|
||||||
import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
|
import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
|
||||||
import StationSetting from "./components/FormAddEdit";
|
import StationSetting from "./components/FormAddEdit";
|
||||||
|
|
@ -37,6 +35,8 @@ import DrawerScenario from "./components/DrawerScenario";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
import ModalTerminal from "./components/ModalTerminal";
|
import ModalTerminal from "./components/ModalTerminal";
|
||||||
import PageLogin from "./components/Authentication/LoginPage";
|
import PageLogin from "./components/Authentication/LoginPage";
|
||||||
|
import DrawerLogs from "./components/DrawerLogs";
|
||||||
|
import DraggableTabs from "./components/DragTabs";
|
||||||
|
|
||||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
|
|
@ -57,14 +57,6 @@ function App() {
|
||||||
const [stations, setStations] = useState<TStation[]>([]);
|
const [stations, setStations] = useState<TStation[]>([]);
|
||||||
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState("0");
|
const [activeTab, setActiveTab] = useState("0");
|
||||||
const [controlsRefs, setControlsRefs] = useState<
|
|
||||||
Record<string, HTMLButtonElement | null>
|
|
||||||
>({});
|
|
||||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
|
||||||
const setControlRef = (val: string) => (node: HTMLButtonElement) => {
|
|
||||||
controlsRefs[val] = node;
|
|
||||||
setControlsRefs(controlsRefs);
|
|
||||||
};
|
|
||||||
const [showBottomShadow, setShowBottomShadow] = useState(false);
|
const [showBottomShadow, setShowBottomShadow] = useState(false);
|
||||||
const [isDisable, setIsDisable] = useState(false);
|
const [isDisable, setIsDisable] = useState(false);
|
||||||
const [isOpenAddStation, setIsOpenAddStation] = useState(false);
|
const [isOpenAddStation, setIsOpenAddStation] = useState(false);
|
||||||
|
|
@ -75,6 +67,12 @@ function App() {
|
||||||
const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
|
const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
|
||||||
const [loadingTerminal, setLoadingTerminal] = useState(true);
|
const [loadingTerminal, setLoadingTerminal] = useState(true);
|
||||||
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
|
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
|
||||||
|
const [disableRequestTakeOver, setDisableRequestTakeOver] = useState(false);
|
||||||
|
const [countDownRequest, setCountDownRequest] = useState(0);
|
||||||
|
const [dataRequestTakeOver, setDataRequestTakeOver] =
|
||||||
|
useState<IDataTakeOver>();
|
||||||
|
const [testLogContent, setTestLogContent] = useState("");
|
||||||
|
const [isLogModalOpen, setIsLogModalOpen] = useState(false);
|
||||||
|
|
||||||
// function get list station
|
// function get list station
|
||||||
const getStation = async () => {
|
const getStation = async () => {
|
||||||
|
|
@ -119,17 +117,17 @@ function App() {
|
||||||
socket.on("line_disconnected", updateStatus);
|
socket.on("line_disconnected", updateStatus);
|
||||||
|
|
||||||
socket?.on("line_output", (data) => {
|
socket?.on("line_output", (data) => {
|
||||||
updateValueLineStation(data?.lineId, "netOutput", data.data);
|
updateValueLineStation(data?.lineId, { netOutput: data.data });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket?.on("line_error", (data) => {
|
socket?.on("line_error", (data) => {
|
||||||
updateValueLineStation(data?.lineId, "netOutput", data.error);
|
updateValueLineStation(data?.lineId, { netOutput: data.error });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket?.on("init", (data) => {
|
socket?.on("init", (data) => {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
data.forEach((value) => {
|
data.forEach((value) => {
|
||||||
updateValueLineStation(value?.id, "netOutput", value.output);
|
updateValueLineStation(value?.id, { netOutput: value.output });
|
||||||
updateStatus({ ...value, lineId: value.id });
|
updateStatus({ ...value, lineId: value.id });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -143,24 +141,85 @@ function App() {
|
||||||
|
|
||||||
socket?.on("user_open_cli", (data) => {
|
socket?.on("user_open_cli", (data) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateValueLineStation(data?.lineId, "cliOpened", true);
|
updateValueLineStation(data.lineId, {
|
||||||
updateValueLineStation(
|
cliOpened: true,
|
||||||
data?.lineId,
|
userEmailOpenCLI: data.userEmailOpenCLI,
|
||||||
"userEmailOpenCLI",
|
userOpenCLI: data.userOpenCLI,
|
||||||
data?.userEmailOpenCLI
|
});
|
||||||
);
|
|
||||||
updateValueLineStation(data?.lineId, "userOpenCLI", data?.userOpenCLI);
|
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket?.on("user_close_cli", (data) => {
|
socket?.on("user_close_cli", (data) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateValueLineStation(data?.lineId, "cliOpened", false);
|
updateValueLineStation(data.lineId, {
|
||||||
updateValueLineStation(data?.lineId, "userEmailOpenCLI", "");
|
cliOpened: false,
|
||||||
updateValueLineStation(data?.lineId, "userOpenCLI", "");
|
userEmailOpenCLI: "",
|
||||||
|
userOpenCLI: "",
|
||||||
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket?.on("confirm_take_over", (data) => {
|
||||||
|
setDataRequestTakeOver(data);
|
||||||
|
if (data?.userEmail !== user?.email) {
|
||||||
|
setCountDownRequest(20);
|
||||||
|
const intervalCount = setInterval(() => {
|
||||||
|
setCountDownRequest((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
setDataRequestTakeOver(undefined);
|
||||||
|
clearInterval(intervalCount);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.userEmail) setCountDownRequest(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const receivedFiles: Record<string, ReceivedFile> = {};
|
||||||
|
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 ✅
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ✅ cleanup on unmount or when socket changes
|
// ✅ cleanup on unmount or when socket changes
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("init");
|
socket.off("init");
|
||||||
|
|
@ -171,68 +230,66 @@ function App() {
|
||||||
socket.off("user_connecting");
|
socket.off("user_connecting");
|
||||||
socket.off("user_open_cli");
|
socket.off("user_open_cli");
|
||||||
socket.off("user_close_cli");
|
socket.off("user_close_cli");
|
||||||
|
socket.off("confirm_take_over");
|
||||||
|
socket.off("response_content_log");
|
||||||
};
|
};
|
||||||
}, [socket, stations]);
|
}, [socket, stations, selectedLine]);
|
||||||
|
|
||||||
const updateStatus = (data: LineConfig) => {
|
const updateStatus = (data: LineConfig) => {
|
||||||
const line = getLine(data.lineId, data.stationId);
|
const line = getLine(data.lineId, data.stationId);
|
||||||
if (line?.id) {
|
if (line?.id) {
|
||||||
updateValueLineStation(line.id, "status", data.status);
|
updateValueLineStation(line.id, { status: data.status });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateValueLineStation = <K extends keyof TLine>(
|
const updateValueLineStation = useCallback(
|
||||||
lineId: number,
|
(lineId: number, updates: Partial<TLine>) => {
|
||||||
field: K,
|
setStations((prevStations) =>
|
||||||
value: TLine[K]
|
prevStations?.map((station: TStation) =>
|
||||||
) => {
|
|
||||||
setStations((el) =>
|
|
||||||
el?.map((station: TStation) =>
|
|
||||||
station.id.toString() === activeTab
|
station.id.toString() === activeTab
|
||||||
? {
|
? {
|
||||||
...station,
|
...station,
|
||||||
lines: (station?.lines || [])?.map((lineItem: TLine) => {
|
lines: station.lines?.map((lineItem: TLine) => {
|
||||||
if (lineItem.id === lineId) {
|
if (lineItem.id !== lineId) return lineItem;
|
||||||
|
|
||||||
|
const isNetOutput = typeof updates?.netOutput !== "undefined";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...lineItem,
|
...lineItem,
|
||||||
[field]:
|
...updates,
|
||||||
field === "netOutput"
|
...(isNetOutput && {
|
||||||
? (lineItem.netOutput || "") + value
|
netOutput:
|
||||||
: value,
|
(lineItem.netOutput || "") + (updates.netOutput || ""),
|
||||||
output: field === "netOutput" ? value : lineItem.output,
|
output: updates.netOutput, // Nếu netOutput thì update luôn output
|
||||||
loadingOutput:
|
loadingOutput: lineItem.loadingOutput ? false : true,
|
||||||
field === "netOutput"
|
}),
|
||||||
? lineItem.loadingOutput
|
|
||||||
? false
|
|
||||||
: true
|
|
||||||
: false,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
return lineItem;
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: station
|
: station
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedLine) {
|
// Update selectedLine nếu nó đang được chọn
|
||||||
const line = {
|
setSelectedLine((prevSelected) => {
|
||||||
...selectedLine,
|
if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
|
||||||
[field]:
|
|
||||||
field === "netOutput"
|
const isNetOutput = typeof updates?.netOutput !== "undefined";
|
||||||
? (selectedLine.netOutput || "") + value
|
|
||||||
: value,
|
return {
|
||||||
output: field === "netOutput" ? value : selectedLine.output,
|
...prevSelected,
|
||||||
loadingOutput:
|
...updates,
|
||||||
field === "netOutput"
|
...(isNetOutput && {
|
||||||
? selectedLine.loadingOutput
|
netOutput:
|
||||||
? false
|
(prevSelected.netOutput || "") + (updates.netOutput || ""),
|
||||||
: true
|
output: updates.netOutput,
|
||||||
: false,
|
loadingOutput: prevSelected.loadingOutput ? false : true,
|
||||||
};
|
}),
|
||||||
setSelectedLine(line);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[activeTab]
|
||||||
|
);
|
||||||
|
|
||||||
const getLine = (lineId: number, stationId: number) => {
|
const getLine = (lineId: number, stationId: number) => {
|
||||||
const station = stations?.find((sta) => sta.id === stationId);
|
const station = stations?.find((sta) => sta.id === stationId);
|
||||||
|
|
@ -244,106 +301,31 @@ function App() {
|
||||||
|
|
||||||
const openTerminal = (line: TLine) => {
|
const openTerminal = (line: TLine) => {
|
||||||
setOpenModalTerminal(true);
|
setOpenModalTerminal(true);
|
||||||
setSelectedLine(line);
|
const data = { ...line };
|
||||||
|
if (!line.userEmailOpenCLI) {
|
||||||
|
data.cliOpened = true;
|
||||||
|
data.userEmailOpenCLI = user?.email;
|
||||||
|
data.userOpenCLI = user?.fullName;
|
||||||
socket?.emit("open_cli", {
|
socket?.emit("open_cli", {
|
||||||
lineId: line.id,
|
lineId: line.id,
|
||||||
stationId: line.station_id,
|
stationId: line.station_id,
|
||||||
userEmail: user?.email,
|
userEmail: user?.email,
|
||||||
userName: user?.fullName,
|
userName: user?.fullName,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
setSelectedLine(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
|
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||||
<Tabs
|
<DraggableTabs
|
||||||
value={activeTab}
|
socket={socket}
|
||||||
onChange={(id) => {
|
usersConnecting={usersConnecting}
|
||||||
setActiveTab(id?.toString() || "0");
|
setIsEditStation={setIsEditStation}
|
||||||
setLoadingTerminal(false);
|
setIsOpenAddStation={setIsOpenAddStation}
|
||||||
setTimeout(() => {
|
setStationEdit={setStationEdit}
|
||||||
setLoadingTerminal(true);
|
tabsData={stations}
|
||||||
}, 100);
|
panels={stations.map((station) => (
|
||||||
}}
|
|
||||||
variant="none"
|
|
||||||
keepMounted={false}
|
|
||||||
>
|
|
||||||
<Flex justify={"space-between"}>
|
|
||||||
<Flex wrap={"wrap"} style={{ maxWidth: "250px" }} gap={"4px"}>
|
|
||||||
{usersConnecting.map((el) => (
|
|
||||||
<Tooltip label={el.userName} key={el.userId}>
|
|
||||||
<Avatar color="cyan" radius="xl" size={"md"}>
|
|
||||||
{el.userName.slice(0, 2)}
|
|
||||||
</Avatar>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
<Tabs.List ref={setRootRef} className={classes.list}>
|
|
||||||
{stations.map((station) => (
|
|
||||||
<Tabs.Tab
|
|
||||||
ref={setControlRef(station.id.toString())}
|
|
||||||
className={classes.tab}
|
|
||||||
key={station.id}
|
|
||||||
value={station.id.toString()}
|
|
||||||
>
|
|
||||||
{station.name}
|
|
||||||
</Tabs.Tab>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<FloatingIndicator
|
|
||||||
target={activeTab ? controlsRefs[activeTab] : null}
|
|
||||||
parent={rootRef}
|
|
||||||
className={classes.indicator}
|
|
||||||
/>
|
|
||||||
<Flex gap={"sm"}>
|
|
||||||
{Number(activeTab) ? (
|
|
||||||
<ActionIcon
|
|
||||||
title="Edit Station"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setStationEdit(
|
|
||||||
stations.find((el) => el.id === Number(activeTab))
|
|
||||||
);
|
|
||||||
setIsOpenAddStation(true);
|
|
||||||
setIsEditStation(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconEdit />
|
|
||||||
</ActionIcon>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<ActionIcon
|
|
||||||
title="Add Station"
|
|
||||||
variant="outline"
|
|
||||||
color="green"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpenAddStation(true);
|
|
||||||
setIsEditStation(false);
|
|
||||||
setStationEdit(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconSettingsPlus />
|
|
||||||
</ActionIcon>
|
|
||||||
</Flex>
|
|
||||||
</Tabs.List>
|
|
||||||
<Flex gap={"sm"} align={"baseline"}>
|
|
||||||
<Text className={classes.userName}>{user?.fullName}</Text>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
color="red"
|
|
||||||
style={{ height: "30px", width: "100px" }}
|
|
||||||
onClick={() => {
|
|
||||||
localStorage.removeItem("user");
|
|
||||||
window.location.href = "/";
|
|
||||||
socket?.disconnect();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{stations.map((station) => (
|
|
||||||
<Tabs.Panel
|
<Tabs.Panel
|
||||||
className={classes.content}
|
className={classes.content}
|
||||||
key={station.id}
|
key={station.id}
|
||||||
|
|
@ -399,6 +381,12 @@ function App() {
|
||||||
<Grid.Col
|
<Grid.Col
|
||||||
span={1}
|
span={1}
|
||||||
style={{ backgroundColor: "#f1f1f1", borderRadius: 8 }}
|
style={{ backgroundColor: "#f1f1f1", borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
justify={"space-between"}
|
||||||
|
align={"center"}
|
||||||
|
h={"100%"}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction={"column"}
|
direction={"column"}
|
||||||
|
|
@ -460,7 +448,11 @@ function App() {
|
||||||
<ButtonScenario
|
<ButtonScenario
|
||||||
key={i}
|
key={i}
|
||||||
socket={socket}
|
socket={socket}
|
||||||
selectedLines={selectedLines}
|
selectedLines={selectedLines.filter(
|
||||||
|
(el) =>
|
||||||
|
typeof el?.userEmailOpenCLI === "undefined" ||
|
||||||
|
el?.userEmailOpenCLI === user?.email
|
||||||
|
)}
|
||||||
isDisable={isDisable || selectedLines.length === 0}
|
isDisable={isDisable || selectedLines.length === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedLines([]);
|
setSelectedLines([]);
|
||||||
|
|
@ -473,11 +465,26 @@ function App() {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<DrawerLogs
|
||||||
|
socket={socket}
|
||||||
|
isLogModalOpen={isLogModalOpen}
|
||||||
|
setIsLogModalOpen={setIsLogModalOpen}
|
||||||
|
testLogContent={testLogContent}
|
||||||
|
setTestLogContent={setTestLogContent}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
onChange={(id) => {
|
||||||
|
setActiveTab(id?.toString() || "0");
|
||||||
|
setLoadingTerminal(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoadingTerminal(true);
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<StationSetting
|
<StationSetting
|
||||||
dataStation={stationEdit}
|
dataStation={stationEdit}
|
||||||
|
|
@ -504,6 +511,12 @@ function App() {
|
||||||
socket={socket}
|
socket={socket}
|
||||||
stationItem={stations.find((el) => el.id === Number(activeTab))}
|
stationItem={stations.find((el) => el.id === Number(activeTab))}
|
||||||
scenarios={scenarios}
|
scenarios={scenarios}
|
||||||
|
dataRequestTakeOver={dataRequestTakeOver}
|
||||||
|
countDownRequest={countDownRequest}
|
||||||
|
setDisableRequestTakeOver={setDisableRequestTakeOver}
|
||||||
|
disableRequestTakeOver={disableRequestTakeOver}
|
||||||
|
setCountDownRequest={setCountDownRequest}
|
||||||
|
setDataRequestTakeOver={setDataRequestTakeOver}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import classes from "./Component.module.css";
|
||||||
import TerminalCLI from "./TerminalXTerm";
|
import TerminalCLI from "./TerminalXTerm";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||||
import { memo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
|
|
||||||
const CardLine = ({
|
const CardLine = ({
|
||||||
line,
|
line,
|
||||||
|
|
@ -23,6 +23,13 @@ const CardLine = ({
|
||||||
openTerminal: (value: TLine) => void;
|
openTerminal: (value: TLine) => void;
|
||||||
loadTerminal: boolean;
|
loadTerminal: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const user = useMemo(() => {
|
||||||
|
return localStorage.getItem("user") &&
|
||||||
|
typeof localStorage.getItem("user") === "string"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "")
|
||||||
|
: null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={line.id}
|
key={line.id}
|
||||||
|
|
@ -91,7 +98,10 @@ const CardLine = ({
|
||||||
loadingContent={line?.loadingOutput}
|
loadingContent={line?.loadingOutput}
|
||||||
line_id={Number(line?.id)}
|
line_id={Number(line?.id)}
|
||||||
station_id={Number(stationItem.id)}
|
station_id={Number(stationItem.id)}
|
||||||
isDisabled={false}
|
isDisabled={
|
||||||
|
typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||||
|
line?.userEmailOpenCLI !== user?.email
|
||||||
|
}
|
||||||
line_status={line?.status || ""}
|
line_status={line?.status || ""}
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
miniSize={true}
|
miniSize={true}
|
||||||
|
|
@ -108,11 +118,6 @@ const CardLine = ({
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* <Flex justify={"flex-end"}>
|
|
||||||
<Button variant="filled" style={{ height: "30px", width: "70px" }}>
|
|
||||||
Take
|
|
||||||
</Button>
|
|
||||||
</Flex> */}
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,60 @@
|
||||||
.buttonScenario :global(.mantine-Button-label) {
|
.buttonScenario :global(.mantine-Button-label) {
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewLog {
|
||||||
|
border: solid 1px gray;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 72vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logLight {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isDisabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
-moz-user-focus: none;
|
||||||
|
-webkit-user-focus: none;
|
||||||
|
-ms-user-focus: none;
|
||||||
|
-moz-user-modify: read-only;
|
||||||
|
-webkit-user-modify: read-only;
|
||||||
|
-ms-user-modify: read-only;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon {
|
||||||
|
color: rgb(107, 217, 60);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon:hover {
|
||||||
|
background-color: rgba(203, 203, 203, 0.809);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewIcon {
|
||||||
|
color: rgb(60, 112, 217);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewIcon:hover {
|
||||||
|
background-color: rgba(203, 203, 203, 0.809);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionIcon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
PointerSensor,
|
||||||
|
closestCenter,
|
||||||
|
type DragEndEvent,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
useSortable,
|
||||||
|
horizontalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { useEffect, useMemo, useState, type JSX } from "react";
|
||||||
|
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
||||||
|
import classes from "./Component.module.css";
|
||||||
|
import type { TStation, TUser } from "../untils/types";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
|
interface DraggableTabsProps {
|
||||||
|
tabsData: TStation[];
|
||||||
|
panels: JSX.Element[];
|
||||||
|
storageKey?: string;
|
||||||
|
onChange: (activeTabId: string | null) => void;
|
||||||
|
w?: string | number;
|
||||||
|
isStationSettings?: boolean;
|
||||||
|
socket: Socket | null;
|
||||||
|
usersConnecting: TUser[];
|
||||||
|
setIsEditStation: (value: React.SetStateAction<boolean>) => void;
|
||||||
|
setIsOpenAddStation: (value: React.SetStateAction<boolean>) => void;
|
||||||
|
setStationEdit: (value: React.SetStateAction<TStation | undefined>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortableTab({
|
||||||
|
tab,
|
||||||
|
active,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
tab: TStation;
|
||||||
|
active: string | null;
|
||||||
|
onChange: (id: string) => void;
|
||||||
|
isStationSettings?: boolean;
|
||||||
|
}) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({ id: tab.id.toString() });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.Tab
|
||||||
|
className={classes.tab}
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
listeners?.onPointerDown?.(e);
|
||||||
|
onChange(tab.id.toString());
|
||||||
|
}}
|
||||||
|
value={tab.id.toString()}
|
||||||
|
style={{
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
color={active === tab.id.toString() ? "green" : ""}
|
||||||
|
fw={600}
|
||||||
|
fz="md"
|
||||||
|
c="#747474"
|
||||||
|
>
|
||||||
|
<Box className={classes.stationName}>
|
||||||
|
<Text fw={600} fz="md" className={classes.stationText}>
|
||||||
|
{tab.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Tabs.Tab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DraggableTabs({
|
||||||
|
tabsData,
|
||||||
|
panels,
|
||||||
|
storageKey = "draggable-tabs-order",
|
||||||
|
onChange,
|
||||||
|
w,
|
||||||
|
isStationSettings = false,
|
||||||
|
socket,
|
||||||
|
usersConnecting,
|
||||||
|
setIsEditStation,
|
||||||
|
setIsOpenAddStation,
|
||||||
|
setStationEdit,
|
||||||
|
}: DraggableTabsProps) {
|
||||||
|
const user = useMemo(() => {
|
||||||
|
return localStorage.getItem("user") &&
|
||||||
|
typeof localStorage.getItem("user") === "string"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "")
|
||||||
|
: null;
|
||||||
|
}, []);
|
||||||
|
const [tabs, setTabs] = useState<TStation[]>(tabsData);
|
||||||
|
const [isChangeTab, setIsChangeTab] = useState<boolean>(false);
|
||||||
|
const [isSetActive, setIsSetActive] = useState<boolean>(false);
|
||||||
|
const [active, setActive] = useState<string | null>(
|
||||||
|
tabsData?.length > 0 ? tabsData[0]?.id.toString() : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
|
|
||||||
|
// Load saved order from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (isChangeTab) {
|
||||||
|
setTabs((pre) =>
|
||||||
|
pre.map((t) => {
|
||||||
|
const updatedTab = tabsData.find((td) => td.id === t.id);
|
||||||
|
return updatedTab ? updatedTab : t;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
let tabSelected =
|
||||||
|
tabsData?.length > 0 ? tabsData[0]?.id.toString() : null;
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const order = JSON.parse(saved) as { id: string; index: number }[];
|
||||||
|
|
||||||
|
// Find the max index in saved order
|
||||||
|
const maxIndex = Math.max(...order.map((o) => o.index), 0);
|
||||||
|
|
||||||
|
const sorted = [...tabsData].sort((a, b) => {
|
||||||
|
const aOrder = order.find(
|
||||||
|
(o) => o.id.toString() === a.id.toString()
|
||||||
|
)?.index;
|
||||||
|
const bOrder = order.find(
|
||||||
|
(o) => o.id.toString() === b.id.toString()
|
||||||
|
)?.index;
|
||||||
|
|
||||||
|
// If not found, assign index after all existing ones
|
||||||
|
const aIndex = aOrder !== undefined ? aOrder : maxIndex + 1;
|
||||||
|
const bIndex = bOrder !== undefined ? bOrder : maxIndex + 1;
|
||||||
|
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
tabSelected = sorted?.length > 0 ? sorted[0]?.id.toString() : null;
|
||||||
|
setTabs(sorted);
|
||||||
|
} catch {
|
||||||
|
setTabs(tabsData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTabs(tabsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSetActive && tabSelected) {
|
||||||
|
setActive(tabSelected);
|
||||||
|
setTimeout(() => {
|
||||||
|
onChange(tabSelected);
|
||||||
|
}, 100);
|
||||||
|
setIsSetActive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tabsData, storageKey]);
|
||||||
|
|
||||||
|
// Handle reorder
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active: dragActive, over } = event;
|
||||||
|
if (dragActive.id !== over?.id && over?.id) {
|
||||||
|
const oldIndex = tabs.findIndex(
|
||||||
|
(t) => t.id.toString() === dragActive.id.toString()
|
||||||
|
);
|
||||||
|
const newIndex = tabs.findIndex(
|
||||||
|
(t) => t.id.toString() === over?.id.toString()
|
||||||
|
);
|
||||||
|
const newTabs = arrayMove(tabs, oldIndex, newIndex);
|
||||||
|
setTabs(newTabs);
|
||||||
|
|
||||||
|
const order = newTabs.map((t, i) => ({ id: t.id, index: i }));
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(order));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsChangeTab(false);
|
||||||
|
setIsSetActive(false);
|
||||||
|
setTabs([]);
|
||||||
|
setActive(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
value={active}
|
||||||
|
onChange={(val) => {
|
||||||
|
setIsChangeTab(true);
|
||||||
|
onChange(val);
|
||||||
|
setActive(val);
|
||||||
|
}}
|
||||||
|
w={w}
|
||||||
|
>
|
||||||
|
<Flex justify={"space-between"}>
|
||||||
|
<Flex wrap={"wrap"} style={{ maxWidth: "250px" }} gap={"4px"}>
|
||||||
|
{usersConnecting.map((el) => (
|
||||||
|
<Tooltip label={el.userName} key={el.userId}>
|
||||||
|
<Avatar color="cyan" radius="xl" size={"md"}>
|
||||||
|
{el.userName.slice(0, 2)}
|
||||||
|
</Avatar>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Tabs.List className={classes.list}>
|
||||||
|
<SortableContext
|
||||||
|
items={tabs}
|
||||||
|
strategy={horizontalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<SortableTab
|
||||||
|
key={tab.id}
|
||||||
|
tab={tab}
|
||||||
|
active={active}
|
||||||
|
onChange={(id) => {
|
||||||
|
setIsChangeTab(true);
|
||||||
|
onChange(id);
|
||||||
|
setActive(id);
|
||||||
|
}}
|
||||||
|
isStationSettings={isStationSettings}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
|
||||||
|
<Flex gap={"sm"}>
|
||||||
|
{Number(active) ? (
|
||||||
|
<ActionIcon
|
||||||
|
title="Edit Station"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setStationEdit(
|
||||||
|
tabsData.find((el) => el.id === Number(active))
|
||||||
|
);
|
||||||
|
setIsOpenAddStation(true);
|
||||||
|
setIsEditStation(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
<ActionIcon
|
||||||
|
title="Add Station"
|
||||||
|
variant="outline"
|
||||||
|
color="green"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenAddStation(true);
|
||||||
|
setIsEditStation(false);
|
||||||
|
setStationEdit(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconSettingsPlus />
|
||||||
|
</ActionIcon>
|
||||||
|
</Flex>
|
||||||
|
</Tabs.List>
|
||||||
|
<Flex gap={"sm"} align={"baseline"}>
|
||||||
|
<Text className={classes.userName}>{user?.fullName}</Text>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
style={{ height: "30px", width: "100px" }}
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
window.location.href = "/";
|
||||||
|
socket?.disconnect();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{panels}
|
||||||
|
</Tabs>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Drawer,
|
||||||
|
Grid,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
ScrollArea,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { ISystemLog } from "../untils/types";
|
||||||
|
import { IconDownload, IconEye } from "@tabler/icons-react";
|
||||||
|
import classes from "./Component.module.css";
|
||||||
|
import moment from "moment";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
import ModalLog from "./ModalLog";
|
||||||
|
|
||||||
|
function DrawerLogs({
|
||||||
|
socket,
|
||||||
|
isLogModalOpen,
|
||||||
|
setIsLogModalOpen,
|
||||||
|
testLogContent,
|
||||||
|
setTestLogContent,
|
||||||
|
}: {
|
||||||
|
socket: Socket | null;
|
||||||
|
isLogModalOpen: boolean;
|
||||||
|
setIsLogModalOpen: (value: React.SetStateAction<boolean>) => void;
|
||||||
|
testLogContent: string;
|
||||||
|
setTestLogContent: (value: React.SetStateAction<string>) => void;
|
||||||
|
}) {
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const [systemLogs, setSystemLogs] = useState<ISystemLog[]>([]);
|
||||||
|
const [isDownloadLog, setIsDownloadLog] = useState(false);
|
||||||
|
// const [testLogContent, setTestLogContent] = useState("");
|
||||||
|
// const [isLogModalOpen, setIsLogModalOpen] = useState(false);
|
||||||
|
const [downloadName, setDownloadName] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened) {
|
||||||
|
socket?.emit("get_list_logs");
|
||||||
|
}
|
||||||
|
}, [opened]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket?.on("list_logs", (files: string[]) => {
|
||||||
|
const list: ISystemLog[] = files.map((file) => {
|
||||||
|
const filename = file.replace(/^.*[\\/]/, "");
|
||||||
|
const createAt = filename.match(/\d{8}/);
|
||||||
|
return {
|
||||||
|
fileName:
|
||||||
|
file.split("/")[3] || file.split("/")[2] || file.split("/")[1],
|
||||||
|
createdAt: createAt ? createAt[0] : "N/A",
|
||||||
|
path: file,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setSystemLogs(
|
||||||
|
list.sort(
|
||||||
|
(a: ISystemLog, b: ISystemLog) =>
|
||||||
|
parseInt(b.createdAt) - parseInt(a.createdAt)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDownloadLog && testLogContent && downloadName) {
|
||||||
|
const blob = new Blob([testLogContent], { type: "text/plain" });
|
||||||
|
// Create a temporary link element
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = downloadName;
|
||||||
|
// Trigger the download by clicking the link
|
||||||
|
link.click();
|
||||||
|
// Clean up
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
setIsDownloadLog(false);
|
||||||
|
setTestLogContent("");
|
||||||
|
setDownloadName("");
|
||||||
|
}
|
||||||
|
}, [testLogContent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Drawer
|
||||||
|
size={"50%"}
|
||||||
|
position="right"
|
||||||
|
style={{ position: "absolute", left: 0 }}
|
||||||
|
offset={8}
|
||||||
|
radius="md"
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={"List Logs"}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<ScrollArea h={"85vh"} style={{ marginBottom: "20px" }}>
|
||||||
|
<Table
|
||||||
|
stickyHeader
|
||||||
|
stickyHeaderOffset={-1}
|
||||||
|
striped
|
||||||
|
highlightOnHover
|
||||||
|
withRowBorders={true}
|
||||||
|
withTableBorder={true}
|
||||||
|
withColumnBorders={true}
|
||||||
|
>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th style={{ width: "50%" }}>File name</Table.Th>
|
||||||
|
<Table.Th style={{ width: "30%" }}>Created at</Table.Th>
|
||||||
|
<Table.Th style={{ width: "10%" }}></Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{systemLogs.map((element) => (
|
||||||
|
<Table.Tr key={element.path}>
|
||||||
|
<Table.Td>{element.fileName}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text>
|
||||||
|
{moment(element.createdAt).format("DD/MM/YYYY")}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Box
|
||||||
|
key={"action-" + element.fileName}
|
||||||
|
className={classes.optionIcon}
|
||||||
|
>
|
||||||
|
<IconEye
|
||||||
|
className={classes.viewIcon}
|
||||||
|
onClick={() => {
|
||||||
|
setTestLogContent("");
|
||||||
|
socket?.emit("get_content_log", {
|
||||||
|
line: { systemLogUrl: element.path },
|
||||||
|
});
|
||||||
|
setIsLogModalOpen(true);
|
||||||
|
}}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
<IconDownload
|
||||||
|
className={[
|
||||||
|
classes.downloadIcon,
|
||||||
|
isDownloadLog ? classes.isDisabled : "",
|
||||||
|
].join(" ")}
|
||||||
|
onClick={() => {
|
||||||
|
socket?.emit("get_content_log", {
|
||||||
|
line: { systemLogUrl: element.path },
|
||||||
|
});
|
||||||
|
setIsDownloadLog(true);
|
||||||
|
setTestLogContent("");
|
||||||
|
setDownloadName(
|
||||||
|
element.path.split("/")[3] ||
|
||||||
|
element.path.split("/")[2] ||
|
||||||
|
element.path.split("/")[1]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{isLogModalOpen && (
|
||||||
|
<ModalLog
|
||||||
|
opened={isLogModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsLogModalOpen(false);
|
||||||
|
}}
|
||||||
|
testLogContent={testLogContent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title="Add Scenario"
|
||||||
|
variant="outline"
|
||||||
|
// color="green"
|
||||||
|
onClick={() => {
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
List logs
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DrawerLogs;
|
||||||
|
|
@ -318,7 +318,7 @@ function DrawerScenario({
|
||||||
</Box>
|
</Box>
|
||||||
<hr style={{ width: "100%" }} />
|
<hr style={{ width: "100%" }} />
|
||||||
<Box>
|
<Box>
|
||||||
<ScrollArea h={500} style={{ marginBottom: "20px" }}>
|
<ScrollArea h={"70vh"} style={{ marginBottom: "20px" }}>
|
||||||
<Table
|
<Table
|
||||||
stickyHeader
|
stickyHeader
|
||||||
stickyHeaderOffset={-1}
|
stickyHeaderOffset={-1}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Modal, Text } from "@mantine/core";
|
||||||
|
import classes from "./Component.module.css";
|
||||||
|
|
||||||
|
const ModalLog = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
testLogContent,
|
||||||
|
}: {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
testLogContent: string;
|
||||||
|
}) => {
|
||||||
|
const addTooltipsToHighlights = () => {
|
||||||
|
const highlights = document.querySelectorAll(".highlight");
|
||||||
|
highlights.forEach((highlight) => {
|
||||||
|
const keyword = highlight.getAttribute("data-keyword");
|
||||||
|
highlight.setAttribute("title", keyword || "");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to highlight system log
|
||||||
|
const highlightSystemLog = (logText: string): string => {
|
||||||
|
const colorStart = "#7fffd4";
|
||||||
|
const colorEnd = "#ffa589";
|
||||||
|
const colorPhysicalStart = "#7fffd4";
|
||||||
|
const colorPhysicalEnd = "#ffa589";
|
||||||
|
return logText
|
||||||
|
.replace(/^---split-point-scenario---.*$/gm, "") // Remove split-point lines
|
||||||
|
.replace(/^---split-point---.*$/gm, "") // Remove split-point lines
|
||||||
|
.replace(
|
||||||
|
/^(---start-testing---|---end-testing---|---start-scenarios---|---end-scenarios---)(\d+)(---.*)?$/gm,
|
||||||
|
(_, prefix, timestamp, suffix = "") => {
|
||||||
|
const date = convertTimestampToDate(timestamp);
|
||||||
|
return `<span style="background-color: ${
|
||||||
|
prefix.includes("start") ? colorStart : colorEnd
|
||||||
|
}" title="${date}">${prefix}${timestamp}${suffix}</span>`;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/^(---start_physical_test_|---end_physical_test_)([A-Z0-9]+)_(\d+)---$/gm,
|
||||||
|
(_, prefix, sn, timestamp) => {
|
||||||
|
const date = convertTimestampToDate(timestamp);
|
||||||
|
const backgroundColor = prefix.includes("start")
|
||||||
|
? colorPhysicalStart
|
||||||
|
: colorPhysicalEnd;
|
||||||
|
return `<span style="background-color: ${backgroundColor}" title="${date}">${prefix}${sn}_${timestamp}---</span>`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to convert timestamp to date
|
||||||
|
const convertTimestampToDate = (timestamp: number) => {
|
||||||
|
const date = new Date(Number(timestamp));
|
||||||
|
return date.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
style={{ position: "absolute", left: 0 }}
|
||||||
|
opened={opened}
|
||||||
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Text fz={"lg"} fw={"bolder"}>
|
||||||
|
Log Content
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
size="90%"
|
||||||
|
styles={{
|
||||||
|
content: {
|
||||||
|
height: "85vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
flex: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: highlightSystemLog(testLogContent),
|
||||||
|
}}
|
||||||
|
className={`${classes.viewLog} ${classes.logLight}`}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) addTooltipsToHighlights();
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalLog;
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
import { Box, Button, Grid, Modal, Text } from "@mantine/core";
|
import {
|
||||||
import type { IScenario, TLine, TStation } from "../untils/types";
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Modal,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import type {
|
||||||
|
IDataTakeOver,
|
||||||
|
IScenario,
|
||||||
|
TLine,
|
||||||
|
TStation,
|
||||||
|
} from "../untils/types";
|
||||||
import TerminalCLI from "./TerminalXTerm";
|
import TerminalCLI from "./TerminalXTerm";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import classes from "./Component.module.css";
|
import classes from "./Component.module.css";
|
||||||
import { useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||||
|
|
||||||
const ModalTerminal = ({
|
const ModalTerminal = ({
|
||||||
|
|
@ -13,6 +27,12 @@ const ModalTerminal = ({
|
||||||
socket,
|
socket,
|
||||||
stationItem,
|
stationItem,
|
||||||
scenarios,
|
scenarios,
|
||||||
|
dataRequestTakeOver,
|
||||||
|
countDownRequest,
|
||||||
|
disableRequestTakeOver,
|
||||||
|
setDisableRequestTakeOver,
|
||||||
|
setCountDownRequest,
|
||||||
|
setDataRequestTakeOver,
|
||||||
}: {
|
}: {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -20,15 +40,61 @@ const ModalTerminal = ({
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
stationItem: TStation | undefined;
|
stationItem: TStation | undefined;
|
||||||
scenarios: IScenario[];
|
scenarios: IScenario[];
|
||||||
|
dataRequestTakeOver: IDataTakeOver | undefined;
|
||||||
|
countDownRequest: number;
|
||||||
|
disableRequestTakeOver: boolean;
|
||||||
|
setDisableRequestTakeOver: (value: React.SetStateAction<boolean>) => void;
|
||||||
|
setCountDownRequest: (value: React.SetStateAction<number>) => void;
|
||||||
|
setDataRequestTakeOver: (
|
||||||
|
value: React.SetStateAction<IDataTakeOver | undefined>
|
||||||
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const user = useMemo(() => {
|
||||||
|
return localStorage.getItem("user") &&
|
||||||
|
typeof localStorage.getItem("user") === "string"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "")
|
||||||
|
: null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isDisable, setIsDisable] = useState<boolean>(false);
|
const [isDisable, setIsDisable] = useState<boolean>(false);
|
||||||
// console.log(line);
|
const intervalTakeOverRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof dataRequestTakeOver?.userName !== "undefined" &&
|
||||||
|
line?.userEmailOpenCLI === user?.email &&
|
||||||
|
dataRequestTakeOver?.userName !== user?.email
|
||||||
|
) {
|
||||||
|
if (dataRequestTakeOver?.userName) {
|
||||||
|
intervalTakeOverRef.current = setInterval(() => {
|
||||||
|
socket?.emit("open_cli", {
|
||||||
|
lineId: line?.id,
|
||||||
|
stationId: line?.station_id,
|
||||||
|
userEmail: user?.email,
|
||||||
|
userName: user?.fullName,
|
||||||
|
});
|
||||||
|
socket?.emit("request_take_over", {
|
||||||
|
station_id: line?.station_id,
|
||||||
|
});
|
||||||
|
setDisableRequestTakeOver(false);
|
||||||
|
setCountDownRequest(0);
|
||||||
|
if (intervalTakeOverRef.current)
|
||||||
|
clearInterval(intervalTakeOverRef.current);
|
||||||
|
}, 20000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (intervalTakeOverRef.current)
|
||||||
|
clearInterval(intervalTakeOverRef.current);
|
||||||
|
}
|
||||||
|
}, [dataRequestTakeOver?.userName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onClose();
|
onClose();
|
||||||
|
if (line?.userEmailOpenCLI === user?.email)
|
||||||
socket?.emit("close_cli", {
|
socket?.emit("close_cli", {
|
||||||
lineId: line?.id,
|
lineId: line?.id,
|
||||||
stationId: line?.station_id,
|
stationId: line?.station_id,
|
||||||
|
|
@ -78,14 +144,21 @@ const ModalTerminal = ({
|
||||||
loadingContent={line?.loadingOutput}
|
loadingContent={line?.loadingOutput}
|
||||||
line_id={Number(line?.id)}
|
line_id={Number(line?.id)}
|
||||||
station_id={Number(stationItem?.id)}
|
station_id={Number(stationItem?.id)}
|
||||||
isDisabled={false}
|
isDisabled={
|
||||||
|
typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||||
|
line?.userEmailOpenCLI !== user?.email
|
||||||
|
}
|
||||||
line_status={line?.status || ""}
|
line_status={line?.status || ""}
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={2}>
|
<Grid.Col span={2}>
|
||||||
{scenarios.map((scenario) => (
|
{scenarios.map((scenario) => (
|
||||||
<Button
|
<Button
|
||||||
disabled={isDisable}
|
disabled={
|
||||||
|
isDisable ||
|
||||||
|
(typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||||
|
line?.userEmailOpenCLI !== user?.email)
|
||||||
|
}
|
||||||
className={classes.buttonScenario}
|
className={classes.buttonScenario}
|
||||||
key={scenario.id}
|
key={scenario.id}
|
||||||
miw={"100px"}
|
miw={"100px"}
|
||||||
|
|
@ -112,7 +185,116 @@ const ModalTerminal = ({
|
||||||
))}
|
))}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Flex justify={"space-between"}>
|
||||||
|
<Box></Box>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
disableRequestTakeOver ||
|
||||||
|
!line?.userEmailOpenCLI ||
|
||||||
|
line?.userEmailOpenCLI === user?.email
|
||||||
|
}
|
||||||
|
variant="filled"
|
||||||
|
size="xs"
|
||||||
|
radius="xs"
|
||||||
|
mt={"md"}
|
||||||
|
ml={"20px"}
|
||||||
|
onClick={() => {
|
||||||
|
socket?.emit("request_take_over", {
|
||||||
|
line_id: line?.id,
|
||||||
|
station_id: Number(line?.station_id),
|
||||||
|
userName: user?.fullName?.trim() || "",
|
||||||
|
userEmail: user?.email || "",
|
||||||
|
});
|
||||||
|
setDisableRequestTakeOver(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisableRequestTakeOver(false);
|
||||||
|
}, 20000);
|
||||||
|
setCountDownRequest(20);
|
||||||
|
const intervalCount = setInterval(() => {
|
||||||
|
setCountDownRequest((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(intervalCount);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Take over{" "}
|
||||||
|
{countDownRequest > 0 &&
|
||||||
|
(typeof dataRequestTakeOver?.userName === "undefined" ||
|
||||||
|
dataRequestTakeOver?.userEmail === user?.email)
|
||||||
|
? `(${countDownRequest}s)`
|
||||||
|
: ""}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
opened={
|
||||||
|
typeof dataRequestTakeOver?.userName !== "undefined" &&
|
||||||
|
line?.userEmailOpenCLI === user?.email &&
|
||||||
|
dataRequestTakeOver?.userName !== user?.email
|
||||||
|
}
|
||||||
|
position={{ bottom: 20, right: 20 }}
|
||||||
|
withCloseButton
|
||||||
|
style={{ border: "solid 2px #ff6c6b", left: 0 }}
|
||||||
|
shadow="md"
|
||||||
|
onClose={close}
|
||||||
|
size="lg"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Text size="sm" mb="xs" fw={700} c={"#ff6c6b"}>
|
||||||
|
{`${
|
||||||
|
dataRequestTakeOver?.userName
|
||||||
|
? `${dataRequestTakeOver?.userName} (${dataRequestTakeOver?.userEmail})`
|
||||||
|
: ""
|
||||||
|
} want to take over this line? ${
|
||||||
|
countDownRequest > 0 &&
|
||||||
|
typeof dataRequestTakeOver?.userName !== "undefined" &&
|
||||||
|
line?.userEmailOpenCLI === user?.email
|
||||||
|
? `(${countDownRequest}s)`
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: "pink", to: "red", deg: 90 }}
|
||||||
|
size="xs"
|
||||||
|
onClick={async () => {
|
||||||
|
socket?.emit("open_cli", {
|
||||||
|
lineId: line?.id,
|
||||||
|
stationId: line?.station_id,
|
||||||
|
userEmail: dataRequestTakeOver?.userEmail,
|
||||||
|
userName: dataRequestTakeOver?.userName,
|
||||||
|
});
|
||||||
|
socket?.emit("request_take_over", {
|
||||||
|
station_id: Number(line?.station_id),
|
||||||
|
});
|
||||||
|
setDisableRequestTakeOver(false);
|
||||||
|
setDataRequestTakeOver(undefined);
|
||||||
|
setCountDownRequest(0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
size="xs"
|
||||||
|
onClick={async () => {
|
||||||
|
setDisableRequestTakeOver(false);
|
||||||
|
setDataRequestTakeOver(undefined);
|
||||||
|
setCountDownRequest(0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -137,14 +137,6 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cliOpened && terminal?.current) {
|
|
||||||
// console.log('Dispose terminal CLI')
|
|
||||||
terminal?.current.clear();
|
|
||||||
terminal?.current.dispose();
|
|
||||||
terminal.current = null;
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
}, [cliOpened]);
|
}, [cliOpened]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -168,6 +160,11 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
// if (terminal.current) {
|
||||||
|
// terminal?.current.clear();
|
||||||
|
// terminal?.current.dispose();
|
||||||
|
// terminal.current = null;
|
||||||
|
// }
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -179,7 +176,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||||
minHeight: customStyle.maxHeight ?? "75vh",
|
minHeight: customStyle.maxHeight ?? "73vh",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -189,8 +186,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
paddingLeft: customStyle.paddingLeft ?? "10px",
|
paddingLeft: customStyle.paddingLeft ?? "10px",
|
||||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||||
fontSize: customStyle.fontSize ?? "9px",
|
fontSize: customStyle.fontSize ?? "9px",
|
||||||
maxHeight: customStyle.maxHeight ?? "75vh",
|
maxHeight: customStyle.maxHeight ?? "73vh",
|
||||||
height: customStyle.height ?? "75vh",
|
height: customStyle.height ?? "73vh",
|
||||||
padding: customStyle.padding ?? "4px",
|
padding: customStyle.padding ?? "4px",
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
|
|
|
||||||
|
|
@ -150,3 +150,34 @@ export type IBodyScenario = {
|
||||||
delay: string;
|
delay: string;
|
||||||
repeat: string;
|
repeat: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IDataTakeOver = {
|
||||||
|
lineId: number;
|
||||||
|
stationId: number;
|
||||||
|
userName: string;
|
||||||
|
userEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ISystemLog = {
|
||||||
|
fileName: string;
|
||||||
|
createdAt: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChunkData = {
|
||||||
|
fileId: string;
|
||||||
|
chunkIndex: number;
|
||||||
|
totalChunks: number;
|
||||||
|
chunk: ArrayBuffer | Uint8Array | Buffer; // socket may send different binary types
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResponseData = {
|
||||||
|
chunk?: ChunkData;
|
||||||
|
// Any other fields from socket data can go here if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReceivedFile = {
|
||||||
|
chunks: Buffer[];
|
||||||
|
receivedChunks: number;
|
||||||
|
totalChunks: number;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client", "node"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue