Update
This commit is contained in:
parent
cbc8397ea8
commit
240dfdff2c
|
|
@ -85,7 +85,12 @@ export default class LineConnection {
|
|||
lineId: id,
|
||||
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) => {
|
||||
|
|
@ -161,7 +166,8 @@ export default class LineConnection {
|
|||
appendLog(
|
||||
`\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`,
|
||||
this.config.stationId,
|
||||
this.config.id
|
||||
this.config.lineNumber,
|
||||
this.config.port
|
||||
)
|
||||
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
|
||||
let stepIndex = 0
|
||||
|
|
@ -176,7 +182,12 @@ export default class LineConnection {
|
|||
lineId: this.config.id,
|
||||
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'))
|
||||
}, script.timeout || 300000)
|
||||
|
||||
|
|
@ -188,7 +199,8 @@ export default class LineConnection {
|
|||
appendLog(
|
||||
`\n---end-scenarios---${Date.now()}---\n`,
|
||||
this.config.stationId,
|
||||
this.config.id
|
||||
this.config.lineNumber,
|
||||
this.config.port
|
||||
)
|
||||
resolve(true)
|
||||
return
|
||||
|
|
@ -198,7 +210,8 @@ export default class LineConnection {
|
|||
appendLog(
|
||||
`\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`,
|
||||
this.config.stationId,
|
||||
this.config.id
|
||||
this.config.lineNumber,
|
||||
this.config.port
|
||||
)
|
||||
let repeatCount = Number(step.repeat) || 1
|
||||
const sendCommand = () => {
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ export function sleep(ms: number) {
|
|||
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 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
|
||||
if (!fs.existsSync(logDir)) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import fs from 'node:fs'
|
||||
import { Server as SocketIOServer } from 'socket.io'
|
||||
import http from 'node:http'
|
||||
import LineConnection from '../app/services/line_connection.js'
|
||||
|
|
@ -142,7 +143,7 @@ export class WebSocketIo {
|
|||
this.setTimeoutConnect(
|
||||
lineId,
|
||||
line,
|
||||
scenario?.timeout ? Number(scenario?.timeout) + 120000 : 300000
|
||||
scenario?.timeout ? Number(scenario?.timeout) + 180000 : 300000
|
||||
)
|
||||
line.runScript(scenario)
|
||||
} 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, () => {
|
||||
|
|
@ -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}`]) {
|
||||
clearInterval(this.intervalMap[`${lineId}`])
|
||||
delete this.intervalMap[`${lineId}`]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
"name": "ATC",
|
||||
"version": "0.0.0",
|
||||
"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/dates": "^8.3.5",
|
||||
"@mantine/form": "^8.3.5",
|
||||
|
|
@ -16,6 +19,7 @@
|
|||
"@tabler/icons-react": "^3.35.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"axios": "^1.12.2",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
|
|
@ -24,7 +28,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
|
|
@ -328,6 +332,59 @@
|
|||
"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": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
||||
|
|
@ -1609,9 +1666,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"version": "24.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
|
||||
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -3273,6 +3330,15 @@
|
|||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"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/dates": "^8.3.5",
|
||||
"@mantine/form": "^8.3.5",
|
||||
|
|
@ -18,6 +21,7 @@
|
|||
"@tabler/icons-react": "^3.35.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"axios": "^1.12.2",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
|
|
@ -26,7 +30,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
|
|
|
|||
|
|
@ -4,32 +4,30 @@ import "@mantine/notifications/styles.css";
|
|||
import "./App.css";
|
||||
import classes from "./App.module.css";
|
||||
|
||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Tabs,
|
||||
Text,
|
||||
Container,
|
||||
Flex,
|
||||
MantineProvider,
|
||||
FloatingIndicator,
|
||||
Grid,
|
||||
ScrollArea,
|
||||
Button,
|
||||
ActionIcon,
|
||||
LoadingOverlay,
|
||||
Avatar,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import type {
|
||||
IDataTakeOver,
|
||||
IScenario,
|
||||
LineConfig,
|
||||
ReceivedFile,
|
||||
ResponseData,
|
||||
TLine,
|
||||
TStation,
|
||||
TUser,
|
||||
} from "./untils/types";
|
||||
import axios from "axios";
|
||||
import CardLine from "./components/CardLine";
|
||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
||||
import { SocketProvider, useSocket } from "./context/SocketContext";
|
||||
import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
|
||||
import StationSetting from "./components/FormAddEdit";
|
||||
|
|
@ -37,6 +35,8 @@ import DrawerScenario from "./components/DrawerScenario";
|
|||
import { Notifications } from "@mantine/notifications";
|
||||
import ModalTerminal from "./components/ModalTerminal";
|
||||
import PageLogin from "./components/Authentication/LoginPage";
|
||||
import DrawerLogs from "./components/DrawerLogs";
|
||||
import DraggableTabs from "./components/DragTabs";
|
||||
|
||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
|
|
@ -57,14 +57,6 @@ function App() {
|
|||
const [stations, setStations] = useState<TStation[]>([]);
|
||||
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
||||
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 [isDisable, setIsDisable] = useState(false);
|
||||
const [isOpenAddStation, setIsOpenAddStation] = useState(false);
|
||||
|
|
@ -75,6 +67,12 @@ function App() {
|
|||
const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
|
||||
const [loadingTerminal, setLoadingTerminal] = useState(true);
|
||||
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
|
||||
const getStation = async () => {
|
||||
|
|
@ -119,17 +117,17 @@ function App() {
|
|||
socket.on("line_disconnected", updateStatus);
|
||||
|
||||
socket?.on("line_output", (data) => {
|
||||
updateValueLineStation(data?.lineId, "netOutput", data.data);
|
||||
updateValueLineStation(data?.lineId, { netOutput: data.data });
|
||||
});
|
||||
|
||||
socket?.on("line_error", (data) => {
|
||||
updateValueLineStation(data?.lineId, "netOutput", data.error);
|
||||
updateValueLineStation(data?.lineId, { netOutput: data.error });
|
||||
});
|
||||
|
||||
socket?.on("init", (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((value) => {
|
||||
updateValueLineStation(value?.id, "netOutput", value.output);
|
||||
updateValueLineStation(value?.id, { netOutput: value.output });
|
||||
updateStatus({ ...value, lineId: value.id });
|
||||
});
|
||||
}
|
||||
|
|
@ -143,24 +141,85 @@ function App() {
|
|||
|
||||
socket?.on("user_open_cli", (data) => {
|
||||
setTimeout(() => {
|
||||
updateValueLineStation(data?.lineId, "cliOpened", true);
|
||||
updateValueLineStation(
|
||||
data?.lineId,
|
||||
"userEmailOpenCLI",
|
||||
data?.userEmailOpenCLI
|
||||
);
|
||||
updateValueLineStation(data?.lineId, "userOpenCLI", data?.userOpenCLI);
|
||||
updateValueLineStation(data.lineId, {
|
||||
cliOpened: true,
|
||||
userEmailOpenCLI: data.userEmailOpenCLI,
|
||||
userOpenCLI: data.userOpenCLI,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
socket?.on("user_close_cli", (data) => {
|
||||
setTimeout(() => {
|
||||
updateValueLineStation(data?.lineId, "cliOpened", false);
|
||||
updateValueLineStation(data?.lineId, "userEmailOpenCLI", "");
|
||||
updateValueLineStation(data?.lineId, "userOpenCLI", "");
|
||||
updateValueLineStation(data.lineId, {
|
||||
cliOpened: false,
|
||||
userEmailOpenCLI: "",
|
||||
userOpenCLI: "",
|
||||
});
|
||||
}, 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
|
||||
return () => {
|
||||
socket.off("init");
|
||||
|
|
@ -171,68 +230,66 @@ function App() {
|
|||
socket.off("user_connecting");
|
||||
socket.off("user_open_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 line = getLine(data.lineId, data.stationId);
|
||||
if (line?.id) {
|
||||
updateValueLineStation(line.id, "status", data.status);
|
||||
updateValueLineStation(line.id, { status: data.status });
|
||||
}
|
||||
};
|
||||
|
||||
const updateValueLineStation = <K extends keyof TLine>(
|
||||
lineId: number,
|
||||
field: K,
|
||||
value: TLine[K]
|
||||
) => {
|
||||
setStations((el) =>
|
||||
el?.map((station: TStation) =>
|
||||
station.id.toString() === activeTab
|
||||
? {
|
||||
...station,
|
||||
lines: (station?.lines || [])?.map((lineItem: TLine) => {
|
||||
if (lineItem.id === lineId) {
|
||||
const updateValueLineStation = useCallback(
|
||||
(lineId: number, updates: Partial<TLine>) => {
|
||||
setStations((prevStations) =>
|
||||
prevStations?.map((station: TStation) =>
|
||||
station.id.toString() === activeTab
|
||||
? {
|
||||
...station,
|
||||
lines: station.lines?.map((lineItem: TLine) => {
|
||||
if (lineItem.id !== lineId) return lineItem;
|
||||
|
||||
const isNetOutput = typeof updates?.netOutput !== "undefined";
|
||||
|
||||
return {
|
||||
...lineItem,
|
||||
[field]:
|
||||
field === "netOutput"
|
||||
? (lineItem.netOutput || "") + value
|
||||
: value,
|
||||
output: field === "netOutput" ? value : lineItem.output,
|
||||
loadingOutput:
|
||||
field === "netOutput"
|
||||
? lineItem.loadingOutput
|
||||
? false
|
||||
: true
|
||||
: false,
|
||||
...updates,
|
||||
...(isNetOutput && {
|
||||
netOutput:
|
||||
(lineItem.netOutput || "") + (updates.netOutput || ""),
|
||||
output: updates.netOutput, // Nếu netOutput thì update luôn output
|
||||
loadingOutput: lineItem.loadingOutput ? false : true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return lineItem;
|
||||
}),
|
||||
}
|
||||
: station
|
||||
)
|
||||
);
|
||||
}),
|
||||
}
|
||||
: station
|
||||
)
|
||||
);
|
||||
|
||||
if (selectedLine) {
|
||||
const line = {
|
||||
...selectedLine,
|
||||
[field]:
|
||||
field === "netOutput"
|
||||
? (selectedLine.netOutput || "") + value
|
||||
: value,
|
||||
output: field === "netOutput" ? value : selectedLine.output,
|
||||
loadingOutput:
|
||||
field === "netOutput"
|
||||
? selectedLine.loadingOutput
|
||||
? false
|
||||
: true
|
||||
: false,
|
||||
};
|
||||
setSelectedLine(line);
|
||||
}
|
||||
};
|
||||
// Update selectedLine nếu nó đang được chọn
|
||||
setSelectedLine((prevSelected) => {
|
||||
if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
|
||||
|
||||
const isNetOutput = typeof updates?.netOutput !== "undefined";
|
||||
|
||||
return {
|
||||
...prevSelected,
|
||||
...updates,
|
||||
...(isNetOutput && {
|
||||
netOutput:
|
||||
(prevSelected.netOutput || "") + (updates.netOutput || ""),
|
||||
output: updates.netOutput,
|
||||
loadingOutput: prevSelected.loadingOutput ? false : true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[activeTab]
|
||||
);
|
||||
|
||||
const getLine = (lineId: number, stationId: number) => {
|
||||
const station = stations?.find((sta) => sta.id === stationId);
|
||||
|
|
@ -244,106 +301,31 @@ function App() {
|
|||
|
||||
const openTerminal = (line: TLine) => {
|
||||
setOpenModalTerminal(true);
|
||||
setSelectedLine(line);
|
||||
socket?.emit("open_cli", {
|
||||
lineId: line.id,
|
||||
stationId: line.station_id,
|
||||
userEmail: user?.email,
|
||||
userName: user?.fullName,
|
||||
});
|
||||
const data = { ...line };
|
||||
if (!line.userEmailOpenCLI) {
|
||||
data.cliOpened = true;
|
||||
data.userEmailOpenCLI = user?.email;
|
||||
data.userOpenCLI = user?.fullName;
|
||||
socket?.emit("open_cli", {
|
||||
lineId: line.id,
|
||||
stationId: line.station_id,
|
||||
userEmail: user?.email,
|
||||
userName: user?.fullName,
|
||||
});
|
||||
}
|
||||
setSelectedLine(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(id) => {
|
||||
setActiveTab(id?.toString() || "0");
|
||||
setLoadingTerminal(false);
|
||||
setTimeout(() => {
|
||||
setLoadingTerminal(true);
|
||||
}, 100);
|
||||
}}
|
||||
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) => (
|
||||
<DraggableTabs
|
||||
socket={socket}
|
||||
usersConnecting={usersConnecting}
|
||||
setIsEditStation={setIsEditStation}
|
||||
setIsOpenAddStation={setIsOpenAddStation}
|
||||
setStationEdit={setStationEdit}
|
||||
tabsData={stations}
|
||||
panels={stations.map((station) => (
|
||||
<Tabs.Panel
|
||||
className={classes.content}
|
||||
key={station.id}
|
||||
|
|
@ -402,63 +384,55 @@ function App() {
|
|||
>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
justify={"space-between"}
|
||||
align={"center"}
|
||||
gap={"xs"}
|
||||
wrap={"wrap"}
|
||||
h={"100%"}
|
||||
>
|
||||
<Button
|
||||
variant="filled"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines.length !== station.lines.length)
|
||||
setSelectedLines(station.lines);
|
||||
else setSelectedLines([]);
|
||||
}}
|
||||
<Flex
|
||||
direction={"column"}
|
||||
align={"center"}
|
||||
gap={"xs"}
|
||||
wrap={"wrap"}
|
||||
>
|
||||
{selectedLines.length !== station.lines.length
|
||||
? "Select All"
|
||||
: "Deselect All"}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
selectedLines.filter((el) => el.status !== "connected")
|
||||
.length === 0
|
||||
}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
const lines = selectedLines.filter(
|
||||
(el) => el.status !== "connected"
|
||||
);
|
||||
socket?.emit("connect_lines", {
|
||||
stationData: station,
|
||||
linesData: lines,
|
||||
});
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<DrawerScenario
|
||||
scenarios={scenarios}
|
||||
setScenarios={setScenarios}
|
||||
/>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
{scenarios.map((el, i) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
<Button
|
||||
variant="filled"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines.length !== station.lines.length)
|
||||
setSelectedLines(station.lines);
|
||||
else setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
{selectedLines.length !== station.lines.length
|
||||
? "Select All"
|
||||
: "Deselect All"}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
selectedLines.filter((el) => el.status !== "connected")
|
||||
.length === 0
|
||||
}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
const lines = selectedLines.filter(
|
||||
(el) => el.status !== "connected"
|
||||
);
|
||||
socket?.emit("connect_lines", {
|
||||
stationData: station,
|
||||
linesData: lines,
|
||||
});
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<DrawerScenario
|
||||
scenarios={scenarios}
|
||||
setScenarios={setScenarios}
|
||||
/>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
|
|
@ -469,15 +443,48 @@ function App() {
|
|||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
scenario={el}
|
||||
/>
|
||||
))}
|
||||
{scenarios.map((el, i) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
socket={socket}
|
||||
selectedLines={selectedLines.filter(
|
||||
(el) =>
|
||||
typeof el?.userEmailOpenCLI === "undefined" ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
scenario={el}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<DrawerLogs
|
||||
socket={socket}
|
||||
isLogModalOpen={isLogModalOpen}
|
||||
setIsLogModalOpen={setIsLogModalOpen}
|
||||
testLogContent={testLogContent}
|
||||
setTestLogContent={setTestLogContent}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
onChange={(id) => {
|
||||
setActiveTab(id?.toString() || "0");
|
||||
setLoadingTerminal(false);
|
||||
setTimeout(() => {
|
||||
setLoadingTerminal(true);
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
|
||||
<StationSetting
|
||||
dataStation={stationEdit}
|
||||
|
|
@ -504,6 +511,12 @@ function App() {
|
|||
socket={socket}
|
||||
stationItem={stations.find((el) => el.id === Number(activeTab))}
|
||||
scenarios={scenarios}
|
||||
dataRequestTakeOver={dataRequestTakeOver}
|
||||
countDownRequest={countDownRequest}
|
||||
setDisableRequestTakeOver={setDisableRequestTakeOver}
|
||||
disableRequestTakeOver={disableRequestTakeOver}
|
||||
setCountDownRequest={setCountDownRequest}
|
||||
setDataRequestTakeOver={setDataRequestTakeOver}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import classes from "./Component.module.css";
|
|||
import TerminalCLI from "./TerminalXTerm";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||
import { memo } from "react";
|
||||
import { memo, useMemo } from "react";
|
||||
|
||||
const CardLine = ({
|
||||
line,
|
||||
|
|
@ -23,6 +23,13 @@ const CardLine = ({
|
|||
openTerminal: (value: TLine) => void;
|
||||
loadTerminal: boolean;
|
||||
}) => {
|
||||
const user = useMemo(() => {
|
||||
return localStorage.getItem("user") &&
|
||||
typeof localStorage.getItem("user") === "string"
|
||||
? JSON.parse(localStorage.getItem("user") || "")
|
||||
: null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={line.id}
|
||||
|
|
@ -91,7 +98,10 @@ const CardLine = ({
|
|||
loadingContent={line?.loadingOutput}
|
||||
line_id={Number(line?.id)}
|
||||
station_id={Number(stationItem.id)}
|
||||
isDisabled={false}
|
||||
isDisabled={
|
||||
typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||
line?.userEmailOpenCLI !== user?.email
|
||||
}
|
||||
line_status={line?.status || ""}
|
||||
fontSize={11}
|
||||
miniSize={true}
|
||||
|
|
@ -108,11 +118,6 @@ const CardLine = ({
|
|||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
{/* <Flex justify={"flex-end"}>
|
||||
<Button variant="filled" style={{ height: "30px", width: "70px" }}>
|
||||
Take
|
||||
</Button>
|
||||
</Flex> */}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,14 +7,71 @@
|
|||
}
|
||||
|
||||
.info_line {
|
||||
color: dimgrey;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
height: 20px;
|
||||
color: dimgrey;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.buttonScenario :global(.mantine-Button-label) {
|
||||
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>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<Box>
|
||||
<ScrollArea h={500} style={{ marginBottom: "20px" }}>
|
||||
<ScrollArea h={"70vh"} style={{ marginBottom: "20px" }}>
|
||||
<Table
|
||||
stickyHeader
|
||||
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 type { IScenario, TLine, TStation } from "../untils/types";
|
||||
import {
|
||||
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 type { Socket } from "socket.io-client";
|
||||
import classes from "./Component.module.css";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||
|
||||
const ModalTerminal = ({
|
||||
|
|
@ -13,6 +27,12 @@ const ModalTerminal = ({
|
|||
socket,
|
||||
stationItem,
|
||||
scenarios,
|
||||
dataRequestTakeOver,
|
||||
countDownRequest,
|
||||
disableRequestTakeOver,
|
||||
setDisableRequestTakeOver,
|
||||
setCountDownRequest,
|
||||
setDataRequestTakeOver,
|
||||
}: {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
|
|
@ -20,19 +40,65 @@ const ModalTerminal = ({
|
|||
socket: Socket | null;
|
||||
stationItem: TStation | undefined;
|
||||
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);
|
||||
// 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 (
|
||||
<Box>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
socket?.emit("close_cli", {
|
||||
lineId: line?.id,
|
||||
stationId: line?.station_id,
|
||||
});
|
||||
if (line?.userEmailOpenCLI === user?.email)
|
||||
socket?.emit("close_cli", {
|
||||
lineId: line?.id,
|
||||
stationId: line?.station_id,
|
||||
});
|
||||
}}
|
||||
size={"80%"}
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
|
|
@ -78,14 +144,21 @@ const ModalTerminal = ({
|
|||
loadingContent={line?.loadingOutput}
|
||||
line_id={Number(line?.id)}
|
||||
station_id={Number(stationItem?.id)}
|
||||
isDisabled={false}
|
||||
isDisabled={
|
||||
typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||
line?.userEmailOpenCLI !== user?.email
|
||||
}
|
||||
line_status={line?.status || ""}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={2}>
|
||||
{scenarios.map((scenario) => (
|
||||
<Button
|
||||
disabled={isDisable}
|
||||
disabled={
|
||||
isDisable ||
|
||||
(typeof line?.userEmailOpenCLI !== "undefined" &&
|
||||
line?.userEmailOpenCLI !== user?.email)
|
||||
}
|
||||
className={classes.buttonScenario}
|
||||
key={scenario.id}
|
||||
miw={"100px"}
|
||||
|
|
@ -112,7 +185,116 @@ const ModalTerminal = ({
|
|||
))}
|
||||
</Grid.Col>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,14 +137,6 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
setLoading(false);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (!cliOpened && terminal?.current) {
|
||||
// console.log('Dispose terminal CLI')
|
||||
terminal?.current.clear();
|
||||
terminal?.current.dispose();
|
||||
terminal.current = null;
|
||||
setLoading(true);
|
||||
}
|
||||
}, [cliOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -168,6 +160,11 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
useEffect(() => {
|
||||
return () => {
|
||||
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%",
|
||||
backgroundColor: "black",
|
||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||
minHeight: customStyle.maxHeight ?? "75vh",
|
||||
minHeight: customStyle.maxHeight ?? "73vh",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -189,8 +186,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
paddingLeft: customStyle.paddingLeft ?? "10px",
|
||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||
fontSize: customStyle.fontSize ?? "9px",
|
||||
maxHeight: customStyle.maxHeight ?? "75vh",
|
||||
height: customStyle.height ?? "75vh",
|
||||
maxHeight: customStyle.maxHeight ?? "73vh",
|
||||
height: customStyle.height ?? "73vh",
|
||||
padding: customStyle.padding ?? "4px",
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
|
|
|
|||
|
|
@ -150,3 +150,34 @@ export type IBodyScenario = {
|
|||
delay: 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,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"types": ["vite/client", "node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
|
|
|
|||
Loading…
Reference in New Issue