Compare commits

..

No commits in common. "cbc8397ea8e6b8be595e02d42070392900fc9d26" and "dea4d2b8047f369322d09652ad9eea34a68b0d5b" have entirely different histories.

24 changed files with 403 additions and 911 deletions

2
BACKEND/.gitignore vendored
View File

@ -23,5 +23,3 @@ yarn-error.log
# Platform specific # Platform specific
.DS_Store .DS_Store
storage/system_logs

View File

@ -4,20 +4,9 @@ import User from '../models/user.js'
export default class AuthController { export default class AuthController {
// Đăng ký // Đăng ký
async register({ request, response }: HttpContext) { async register({ request, response }: HttpContext) {
try {
const data = request.only(['email', 'password', 'full_name']) const data = request.only(['email', 'password', 'full_name'])
const user = await User.create(data)
const user = await User.query().where('email', data.email).first() return response.json({ message: 'User created', user })
if (user) {
return response.status(401).json({ status: false, message: 'Email is exist' })
}
const newUser = await User.create(data)
return response.json({ status: true, message: 'User created', user: newUser })
} catch (error) {
return response.status(401).json({ status: false, message: 'Invalid credentials' })
}
} }
// Đăng nhập // Đăng nhập
@ -35,9 +24,11 @@ export default class AuthController {
return response.status(401).json({ message: 'Invalid email or password' }) return response.status(401).json({ message: 'Invalid email or password' })
} }
// ✅ Nếu dùng token thủ công:
const token = Math.random().toString(36).substring(2) // hoặc JWT nếu bạn cài auth
return response.json({ return response.json({
message: 'Login successful', message: 'Login successful',
user: { id: user.id, email: user.email, fullName: user.fullName }, user: { id: user.id, email: user.email, token },
}) })
} catch { } catch {
return response.status(401).json({ message: 'Invalid credentials' }) return response.status(401).json({ message: 'Invalid credentials' })

View File

@ -1,6 +1,8 @@
import Scenario from '#models/scenario' import Scenario from '#models/scenario'
import type { HttpContext } from '@adonisjs/core/http' import type { HttpContext } from '@adonisjs/core/http'
import { searchRequest } from '../utils/hasPaginationRequest.js'
import db from '@adonisjs/lucid/services/db' import db from '@adonisjs/lucid/services/db'
import UserScenarios from '#models/user_scenario'
export default class ScenariosController { export default class ScenariosController {
/** /**
@ -34,6 +36,7 @@ export default class ScenariosController {
async create({ request, response, auth }: HttpContext) { async create({ request, response, auth }: HttpContext) {
try { try {
const payload = await request.all() const payload = await request.all()
const trx = await db.transaction() const trx = await db.transaction()
try { try {
const scenario = await Scenario.create( const scenario = await Scenario.create(
@ -41,7 +44,7 @@ export default class ScenariosController {
title: payload.title.trim(), title: payload.title.trim(),
body: JSON.stringify(payload.body), body: JSON.stringify(payload.body),
timeout: payload.timeout, timeout: payload.timeout,
isReboot: payload.isReboot, isReboot: payload.is_reboot,
}, },
{ client: trx } { client: trx }
) )

View File

@ -1,5 +1,5 @@
import net from 'node:net' import net from 'node:net'
import { appendLog, cleanData, sleep } from '../ultils/helper.js' import { cleanData, sleep } from '../ultils/helper.js'
import Scenario from '#models/scenario' import Scenario from '#models/scenario'
interface LineConfig { interface LineConfig {
@ -11,14 +11,6 @@ interface LineConfig {
apcName?: string apcName?: string
output: string output: string
status: string status: string
openCLI: boolean
userEmailOpenCLI: string
userOpenCLI: string
}
interface User {
userEmail: string
userName: string
} }
export default class LineConnection { export default class LineConnection {
@ -85,7 +77,6 @@ export default class LineConnection {
lineId: id, lineId: id,
data: message, data: message,
}) })
appendLog(cleanData(message), this.config.stationId, this.config.id)
}) })
this.client.on('error', (err) => { this.client.on('error', (err) => {
@ -158,11 +149,6 @@ export default class LineConnection {
} }
this.isRunningScript = true this.isRunningScript = true
appendLog(
`\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`,
this.config.stationId,
this.config.id
)
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
@ -170,13 +156,6 @@ export default class LineConnection {
const timeoutTimer = setTimeout(() => { const timeoutTimer = setTimeout(() => {
this.isRunningScript = false this.isRunningScript = false
this.outputBuffer = '' this.outputBuffer = ''
this.config.output += 'Timeout run scenario'
this.socketIO.emit('line_output', {
stationId: this.config.stationId,
lineId: this.config.id,
data: 'Timeout run scenario',
})
appendLog(`\n---end-scenarios---${Date.now()}---\n`, this.config.stationId, this.config.id)
// reject(new Error('Script timeout')) // reject(new Error('Script timeout'))
}, script.timeout || 300000) }, script.timeout || 300000)
@ -185,21 +164,11 @@ export default class LineConnection {
clearTimeout(timeoutTimer) clearTimeout(timeoutTimer)
this.isRunningScript = false this.isRunningScript = false
this.outputBuffer = '' this.outputBuffer = ''
appendLog(
`\n---end-scenarios---${Date.now()}---\n`,
this.config.stationId,
this.config.id
)
resolve(true) resolve(true)
return return
} }
const step = steps[index] const step = steps[index]
appendLog(
`\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`,
this.config.stationId,
this.config.id
)
let repeatCount = Number(step.repeat) || 1 let repeatCount = Number(step.repeat) || 1
const sendCommand = () => { const sendCommand = () => {
if (repeatCount <= 0) { if (repeatCount <= 0) {
@ -234,27 +203,4 @@ export default class LineConnection {
runStep(stepIndex) runStep(stepIndex)
}) })
} }
userOpenCLI(user: User) {
this.config.openCLI = true
this.config.userEmailOpenCLI = user.userEmail
this.config.userOpenCLI = user.userName
this.socketIO.emit('user_open_cli', {
stationId: this.config.stationId,
lineId: this.config.id,
userEmailOpenCLI: user.userEmail,
userOpenCLI: user.userName,
})
}
userCloseCLI() {
this.config.openCLI = false
this.config.userEmailOpenCLI = ''
this.config.userOpenCLI = ''
this.socketIO.emit('user_close_cli', {
stationId: this.config.stationId,
lineId: this.config.id,
userEmailOpenCLI: '',
})
}
} }

View File

@ -1,6 +1,3 @@
import fs from 'node:fs'
import path from 'node:path'
/** /**
* Function to clean up unwanted characters from the output data. * Function to clean up unwanted characters from the output data.
* @param {string} data - The raw data to be cleaned. * @param {string} data - The raw data to be cleaned.
@ -19,20 +16,3 @@ export const cleanData = (data: string) => {
export function sleep(ms: number) { 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) {
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`)
// Ensure folder exists
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
fs.appendFile(logFile, output, (err) => {
if (err) {
console.error('❌ Failed to write log:', err.message)
}
})
}

View File

@ -9,7 +9,7 @@ export default class extends BaseSchema {
table.string('title').notNullable() table.string('title').notNullable()
table.text('body').notNullable() table.text('body').notNullable()
table.integer('timeout').notNullable() table.integer('timeout').notNullable()
table.boolean('is_reboot').defaultTo(false) table.boolean('isReboot').defaultTo(false)
table.timestamps() table.timestamps()
}) })
} }

View File

@ -51,7 +51,6 @@ export class WebSocketIo {
stationMap: Map<number, Station> = new Map() stationMap: Map<number, Station> = new Map()
lineMap: Map<number, LineConnection> = new Map() // key = lineId lineMap: Map<number, LineConnection> = new Map() // key = lineId
lineConnecting: number[] = [] // key = lineId lineConnecting: number[] = [] // key = lineId
userConnecting: Map<number, { userId: number; userName: string }> = new Map()
constructor(protected app: ApplicationService) {} constructor(protected app: ApplicationService) {}
@ -71,14 +70,8 @@ export class WebSocketIo {
}) })
io.on('connection', (socket: CustomSocket) => { io.on('connection', (socket: CustomSocket) => {
const { userId, userName } = socket.handshake.auth
console.log('Socket connected:', socket.id) console.log('Socket connected:', socket.id)
socket.connectionTime = new Date() socket.connectionTime = new Date()
this.userConnecting.set(userId, { userId, userName })
setTimeout(() => {
io.emit('user_connecting', Array.from(this.userConnecting.values()))
}, 200)
setTimeout(() => { setTimeout(() => {
io.to(socket.id).emit( io.to(socket.id).emit(
@ -89,10 +82,6 @@ export class WebSocketIo {
socket.on('disconnect', () => { socket.on('disconnect', () => {
console.log(`FE disconnected: ${socket.id}`) console.log(`FE disconnected: ${socket.id}`)
this.userConnecting.delete(userId)
setTimeout(() => {
io.emit('user_connecting', Array.from(this.userConnecting.values()))
}, 200)
}) })
// FE gửi yêu cầu connect lines // FE gửi yêu cầu connect lines
@ -105,8 +94,8 @@ export class WebSocketIo {
const { lineIds, stationId, command } = data const { lineIds, stationId, command } = data
for (const lineId of lineIds) { for (const lineId of lineIds) {
const line = this.lineMap.get(lineId) const line = this.lineMap.get(lineId)
if (line && line.config.status === 'connected') { if (line) {
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, line) this.setTimeoutConnect(lineId, line)
line.writeCommand(command) line.writeCommand(command)
} else { } else {
@ -118,7 +107,7 @@ export class WebSocketIo {
await this.connectLine(io, [linesData], stationData) await this.connectLine(io, [linesData], stationData)
const lineReconnect = this.lineMap.get(lineId) const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) { if (lineReconnect) {
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, lineReconnect) this.setTimeoutConnect(lineId, lineReconnect)
lineReconnect.writeCommand(command) lineReconnect.writeCommand(command)
} }
@ -138,7 +127,7 @@ export class WebSocketIo {
const lineId = data.id const lineId = data.id
const scenario = data.scenario const scenario = data.scenario
const line = this.lineMap.get(lineId) const line = this.lineMap.get(lineId)
if (line && line.config.status === 'connected') { if (line) {
this.setTimeoutConnect( this.setTimeoutConnect(
lineId, lineId,
line, line,
@ -166,44 +155,9 @@ export class WebSocketIo {
} }
}) })
socket.on('open_cli', async (data) => { // FE yêu cầu ngắt kết nối 1 station
const { lineId, userEmail, userName: name, stationId } = data socket.on('disconnect_station', (stationId) => {
const line = this.lineMap.get(lineId) this.disconnectStation(stationId)
if (line) {
line.userOpenCLI({ userEmail, userName: name })
} else {
if (this.lineConnecting.includes(lineId)) return
const linesData = await Line.findBy('id', lineId)
const stationData = await Station.findBy('id', stationId)
if (linesData && stationData) {
this.lineConnecting.push(lineId)
await this.connectLine(io, [linesData], stationData)
const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) {
lineReconnect.userOpenCLI({ userEmail, userName: name })
}
}
}
})
socket.on('close_cli', async (data) => {
const { lineId, stationId } = data
const line = this.lineMap.get(lineId)
if (line) {
line.userCloseCLI()
} else {
if (this.lineConnecting.includes(lineId)) return
const linesData = await Line.findBy('id', lineId)
const stationData = await Station.findBy('id', stationId)
if (linesData && stationData) {
this.lineConnecting.push(lineId)
await this.connectLine(io, [linesData], stationData)
const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) {
lineReconnect.userCloseCLI()
}
}
}
}) })
}) })
@ -228,9 +182,6 @@ export class WebSocketIo {
apcName: line.apcName, apcName: line.apcName,
output: '', output: '',
status: '', status: '',
openCLI: false,
userEmailOpenCLI: '',
userOpenCLI: '',
}, },
socket socket
) )
@ -244,6 +195,22 @@ export class WebSocketIo {
} }
} }
private disconnectStation(stationId: number) {
const station = this.stationMap.get(stationId)
if (!station) return
for (const line of station.lines) {
const conn = this.lineMap.get(line.id)
if (conn) {
conn.disconnect()
this.lineMap.delete(line.id)
}
}
this.stationMap.delete(stationId)
console.log(`🔻 Station ${station.name} disconnected`)
}
private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => { private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => {
if (this.intervalMap[`${lineId}`]) { if (this.intervalMap[`${lineId}`]) {
clearInterval(this.intervalMap[`${lineId}`]) clearInterval(this.intervalMap[`${lineId}`])
@ -251,7 +218,7 @@ export class WebSocketIo {
} }
const interval = setInterval(() => { const interval = setInterval(() => {
lineConn.disconnect() lineConn.disconnect()
// this.lineMap.delete(lineId) this.lineMap.delete(lineId)
if (this.intervalMap[`${lineId}`]) { if (this.intervalMap[`${lineId}`]) {
clearInterval(this.intervalMap[`${lineId}`]) clearInterval(this.intervalMap[`${lineId}`])
delete this.intervalMap[`${lineId}`] delete this.intervalMap[`${lineId}`]

View File

@ -67,6 +67,5 @@ router
router router
.group(() => { .group(() => {
router.post('/login', '#controllers/auth_controller.login') router.post('/login', '#controllers/auth_controller.login')
router.post('/register', '#controllers/auth_controller.register')
}) })
.prefix('api/auth') .prefix('api/auth')

View File

@ -18,7 +18,6 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.4",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"xterm": "^5.3.0" "xterm": "^5.3.0"
}, },
@ -2217,15 +2216,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -3605,44 +3595,6 @@
} }
} }
}, },
"node_modules/react-router": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
"license": "MIT",
"dependencies": {
"react-router": "7.9.4"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@ -3801,12 +3753,6 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@ -20,7 +20,6 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.4",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"xterm": "^5.3.0" "xterm": "^5.3.0"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -3,7 +3,7 @@
} }
body { body {
font-family: "Mulish", sans-serif; font-family: 'Mulish', sans-serif;
} }
.list { .list {
@ -48,25 +48,7 @@ body {
} }
} }
.content { .content{
width: 100%; width: 100%;
border-top: 1px #ccc solid; border-top: 1px #ccc solid;
} }
.userName {
position: relative;
font-size: 16px;
font-weight: 600;
color: #222;
text-align: center;
margin-right: 8px;
}
.userName::after {
content: "";
display: block;
height: 1px;
background-color: #007bff; /* blue accent */
margin: 0.1rem auto 0;
border-radius: 3px;
}

View File

@ -4,7 +4,7 @@ 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, useEffect, useState } from "react";
import { import {
Tabs, Tabs,
Text, Text,
@ -17,16 +17,8 @@ import {
Button, Button,
ActionIcon, ActionIcon,
LoadingOverlay, LoadingOverlay,
Avatar,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import type { import type { IScenario, LineConfig, TLine, TStation } from "./untils/types";
IScenario,
LineConfig,
TLine,
TStation,
TUser,
} 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 { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
@ -35,8 +27,6 @@ import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
import StationSetting from "./components/FormAddEdit"; import StationSetting from "./components/FormAddEdit";
import DrawerScenario from "./components/DrawerScenario"; import DrawerScenario from "./components/DrawerScenario";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import ModalTerminal from "./components/ModalTerminal";
import PageLogin from "./components/Authentication/LoginPage";
const apiUrl = import.meta.env.VITE_BACKEND_URL; const apiUrl = import.meta.env.VITE_BACKEND_URL;
@ -44,14 +34,6 @@ const apiUrl = import.meta.env.VITE_BACKEND_URL;
* Main Component * Main Component
*/ */
function App() { function App() {
const user = useMemo(() => {
return localStorage.getItem("user") &&
typeof localStorage.getItem("user") === "string"
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
if (!user) window.location.href = "/";
document.title = "Automation Test"; document.title = "Automation Test";
const { socket } = useSocket(); const { socket } = useSocket();
const [stations, setStations] = useState<TStation[]>([]); const [stations, setStations] = useState<TStation[]>([]);
@ -71,10 +53,6 @@ function App() {
const [isEditStation, setIsEditStation] = useState(false); const [isEditStation, setIsEditStation] = useState(false);
const [stationEdit, setStationEdit] = useState<TStation | undefined>(); const [stationEdit, setStationEdit] = useState<TStation | undefined>();
const [scenarios, setScenarios] = useState<IScenario[]>([]); const [scenarios, setScenarios] = useState<IScenario[]>([]);
const [openModalTerminal, setOpenModalTerminal] = useState(false);
const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
const [loadingTerminal, setLoadingTerminal] = useState(true);
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
// function get list station // function get list station
const getStation = async () => { const getStation = async () => {
@ -115,74 +93,24 @@ function App() {
if (!socket || !stations?.length) return; if (!socket || !stations?.length) return;
socket.on("line_connected", updateStatus); socket.on("line_connected", updateStatus);
socket.on("line_disconnected", updateStatus); socket.on("line_disconnected", updateStatus);
socket?.on("line_output", (data) => {
updateValueLineStation(data?.lineId, "netOutput", data.data);
});
socket?.on("line_error", (data) => {
updateValueLineStation(data?.lineId, "netOutput", data.error);
});
socket?.on("init", (data) => {
if (Array.isArray(data)) {
data.forEach((value) => {
updateValueLineStation(value?.id, "netOutput", value.output);
updateStatus({ ...value, lineId: value.id });
});
}
});
socket?.on("user_connecting", (data) => {
if (Array.isArray(data)) {
setUsersConnecting(data);
}
});
socket?.on("user_open_cli", (data) => {
setTimeout(() => {
updateValueLineStation(data?.lineId, "cliOpened", true);
updateValueLineStation(
data?.lineId,
"userEmailOpenCLI",
data?.userEmailOpenCLI
);
updateValueLineStation(data?.lineId, "userOpenCLI", data?.userOpenCLI);
}, 100);
});
socket?.on("user_close_cli", (data) => {
setTimeout(() => {
updateValueLineStation(data?.lineId, "cliOpened", false);
updateValueLineStation(data?.lineId, "userEmailOpenCLI", "");
updateValueLineStation(data?.lineId, "userOpenCLI", "");
}, 100);
});
// ✅ cleanup on unmount or when socket changes // ✅ cleanup on unmount or when socket changes
return () => { return () => {
socket.off("init");
socket.off("line_output");
socket.off("line_error");
socket.off("line_connected"); socket.off("line_connected");
socket.off("line_disconnected"); socket.off("line_disconnected");
socket.off("user_connecting");
socket.off("user_open_cli");
socket.off("user_close_cli");
}; };
}, [socket, stations]); }, [socket, stations]);
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) {
updateValueLineStation(line.id, "status", data.status); updateValueLineStation(line, "status", data.status);
} }
}; };
const updateValueLineStation = <K extends keyof TLine>( const updateValueLineStation = <K extends keyof TLine>(
lineId: number, currentLine: TLine,
field: K, field: K,
value: TLine[K] value: TLine[K]
) => { ) => {
@ -192,20 +120,10 @@ function App() {
? { ? {
...station, ...station,
lines: (station?.lines || [])?.map((lineItem: TLine) => { lines: (station?.lines || [])?.map((lineItem: TLine) => {
if (lineItem.id === lineId) { if (lineItem.id === currentLine.id) {
return { return {
...lineItem, ...lineItem,
[field]: [field]: value,
field === "netOutput"
? (lineItem.netOutput || "") + value
: value,
output: field === "netOutput" ? value : lineItem.output,
loadingOutput:
field === "netOutput"
? lineItem.loadingOutput
? false
: true
: false,
}; };
} }
return lineItem; return lineItem;
@ -214,24 +132,6 @@ function App() {
: 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);
}
}; };
const getLine = (lineId: number, stationId: number) => { const getLine = (lineId: number, stationId: number) => {
@ -242,41 +142,14 @@ function App() {
} else return null; } else return null;
}; };
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,
});
};
return ( return (
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}> <Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onChange={(id) => { onChange={(id) => setActiveTab(id?.toString() || "0")}
setActiveTab(id?.toString() || "0");
setLoadingTerminal(false);
setTimeout(() => {
setLoadingTerminal(true);
}, 100);
}}
variant="none" variant="none"
keepMounted={false} 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}> <Tabs.List ref={setRootRef} className={classes.list}>
{stations.map((station) => ( {stations.map((station) => (
<Tabs.Tab <Tabs.Tab
@ -326,22 +199,6 @@ function App() {
</ActionIcon> </ActionIcon>
</Flex> </Flex>
</Tabs.List> </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) => ( {stations.map((station) => (
<Tabs.Panel <Tabs.Panel
@ -381,11 +238,7 @@ function App() {
line={line} line={line}
selectedLines={selectedLines} selectedLines={selectedLines}
setSelectedLines={setSelectedLines} setSelectedLines={setSelectedLines}
openTerminal={openTerminal} updateStatus={updateStatus}
loadTerminal={
loadingTerminal &&
Number(station.id) === Number(activeTab)
}
/> />
))} ))}
</Flex> </Flex>
@ -456,9 +309,8 @@ function App() {
}, 10000); }, 10000);
}} }}
/> />
{scenarios.map((el, i) => ( {scenarios.map((el) => (
<ButtonScenario <ButtonScenario
key={i}
socket={socket} socket={socket}
selectedLines={selectedLines} selectedLines={selectedLines}
isDisable={isDisable || selectedLines.length === 0} isDisable={isDisable || selectedLines.length === 0}
@ -493,24 +345,11 @@ function App() {
setActiveTab(stations.length ? stations[0]?.id.toString() : "0") setActiveTab(stations.length ? stations[0]?.id.toString() : "0")
} }
/> />
<ModalTerminal
opened={openModalTerminal}
onClose={() => {
setOpenModalTerminal(false);
setSelectedLine(undefined);
}}
line={selectedLine}
socket={socket}
stationItem={stations.find((el) => el.id === Number(activeTab))}
scenarios={scenarios}
/>
</Container> </Container>
); );
} }
export default function Main() { export default function Main() {
const user = localStorage.getItem("user");
return ( return (
<MantineProvider> <MantineProvider>
<SocketProvider> <SocketProvider>
@ -524,13 +363,7 @@ export default function Main() {
} }
> >
<Notifications position="top-right" autoClose={5000} /> <Notifications position="top-right" autoClose={5000} />
{user ? (
<App /> <App />
) : (
<Container w={"100%"} style={{ maxWidth: "100%", padding: 0 }}>
<PageLogin />
</Container>
)}
</Suspense> </Suspense>
</SocketProvider> </SocketProvider>
</MantineProvider> </MantineProvider>

View File

@ -1,22 +1,70 @@
.wrapper { .wrapper {
height: 100vh; min-height: rem(100vh);
background-size: cover; background-size: cover;
background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80); background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80);
display: flex; }
justify-content: center;
align-items: center;
}
.form { .form {
border-right: rem(1px) solid border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7)); min-height: rem(100vh);
max-width: rem(450px); max-width: rem(500px);
padding-top: rem(80px); padding-top: rem(80px);
@media (max-width: var(--mantine-breakpoint-sm)) {
max-width: 100%;
};
font-weight: 600; font-weight: 600;
}
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
}
.google-btn {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #4285f4;
border-radius: 4px;
background-color: white;
margin: 15px auto;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease;
}
.google-btn:hover {
background-color: #f0f0f0;
}
.google-btn:active {
background-color: #e0e0e0;
}
.google-icon {
width: 20px;
height: 20px;
margin-right: 10px;
}
.title {
position: relative;
font-size: 1.5rem;
font-weight: 600;
color: #222;
text-align: center;
margin-bottom: 1.5rem;
} }
.title { .title::after {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); content: "";
font-family: Greycliff CF, var(--mantine-font-family); display: block;
width: 200px;
height: 2px;
background-color: #007bff; /* blue accent */
margin: 0.1rem auto 0;
border-radius: 3px;
} }

View File

@ -1,80 +1,105 @@
import { Button, PasswordInput, TextInput } from "@mantine/core"; import { useGoogleLogin } from '@react-oauth/google'
import { useForm } from "@mantine/form"; import { emailRegex } from '@/utils/formRegexs'
import { notifications } from "@mantine/notifications";
import axios from "axios";
const apiUrl = import.meta.env.VITE_BACKEND_URL; import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from '@/rtk/store'
import {
loginAsync,
loginERPAsync,
loginWithGoogleAsync,
} from '@/rtk/slices/authSlice'
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
import { useForm } from '@mantine/form'
import classes from './AuthenticationImage.module.css'
import { useNavigate } from 'react-router-dom'
import ImgERP from '../../lib/images/erp.jpg'
import { useState } from 'react'
type TLogin = { type TLogin = {
email: string; email: string
password: string; password: string
}; }
const Login = () => { const Login = () => {
const navigate = useNavigate()
const dispatch = useDispatch<AppDispatch>()
const { status } = useSelector((state: RootState) => state.auth)
const [isLoginERP, setIsLoginERP] = useState(false)
const formLogin = useForm<TLogin>({ const formLogin = useForm<TLogin>({
initialValues: { initialValues: {
email: "", email: '',
password: "", password: '',
}, },
validate: (values) => ({ validate: (values) => ({
email: values.email === "" ? "Email is required" : null, email:
values.email === ''
? 'Email is required'
: isLoginERP
? null
: emailRegex.test(values.email)
? null
: 'Invalid email',
password: values.password === "" ? "Password is required" : null, password: values.password === '' ? 'Password is required' : null,
}), }),
}); })
const handleLogin = async () => { const handleLogin = async (values: TLogin) => {
try { if (isLoginERP) {
if (!formLogin.values.email) {
notifications.show({
title: "Error",
message: "Email is required",
color: "red",
});
return;
}
if (!formLogin.values.password) {
notifications.show({
title: "Error",
message: "Password is required",
color: "red",
});
return;
}
const payload = { const payload = {
email: formLogin.values.email, userEmail: values.email,
password: formLogin.values.password, password: values.password,
};
const response = await axios.post(apiUrl + "api/auth/login", payload);
if (response.data.user) {
const user = response.data.user;
localStorage.setItem("user", JSON.stringify(user));
window.location.href = "/";
} }
} catch (error) { const resultAction = await dispatch(loginERPAsync(payload))
console.log(error);
notifications.show({ if (loginERPAsync.fulfilled.match(resultAction)) {
title: "Error", // set interval to wait for localStorage to be set
message: "Login fail, please try again!", window.location.href = '/dashboard'
color: "red",
});
} }
}; } else {
const resultAction = await dispatch(loginAsync(values))
if (loginAsync.fulfilled.match(resultAction)) {
navigate('/dashboard')
}
}
}
const handleLoginGG = useGoogleLogin({
onSuccess: async (codeResponse) => {
const accessToken = codeResponse.access_token
const resultAction = await dispatch(loginWithGoogleAsync(accessToken))
if (loginWithGoogleAsync.fulfilled.match(resultAction)) {
navigate('/dashboard')
}
},
onError: (error) => console.log('Login Failed:', error),
})
return ( return (
<form <form
style={{ style={{
padding: "10px 20px", padding: '10px 20px',
}} }}
onSubmit={formLogin.onSubmit(handleLogin)} onSubmit={formLogin.onSubmit(handleLogin)}
> >
<div style={{ textAlign: 'center' }}>
<h3 className={classes.title}>
{isLoginERP ? 'Login with ERP account' : 'Login with ATC account'}
</h3>
</div>
<TextInput <TextInput
label={"Email address"} label={isLoginERP ? 'Username/email:' : 'Email address'}
placeholder="hello@gmail.com" placeholder="hello@gmail.com"
value={formLogin.values.email} value={formLogin.values.email}
error={formLogin.errors.email} error={formLogin.errors.email}
onChange={(e) => { onChange={(e) => {
formLogin.setFieldValue("email", e.target.value!); formLogin.setFieldValue('email', e.target.value!)
}} }}
required required
size="md" size="md"
@ -85,7 +110,7 @@ const Login = () => {
value={formLogin.values.password} value={formLogin.values.password}
error={formLogin.errors.password} error={formLogin.errors.password}
onChange={(e) => { onChange={(e) => {
formLogin.setFieldValue("password", e.target.value!); formLogin.setFieldValue('password', e.target.value!)
}} }}
required required
mt="md" mt="md"
@ -97,12 +122,60 @@ const Login = () => {
mt="xl" mt="xl"
size="md" size="md"
type="submit" type="submit"
loading={status === "loading"} loading={status === 'loading'}
> >
Sign in Sign in
</Button> </Button>
</form>
);
};
export default Login; {!isLoginERP ? (
<Box ta={'center'}>
<Button
variant="outline"
color="#228be6"
radius={'5px'}
className={classes['google-btn']}
onClick={() => setIsLoginERP(true)}
>
<img
src={ImgERP}
alt="ERP logo"
className={classes['google-icon']}
/>
Sign in with ERP
</Button>
</Box>
) : (
<Box ta={'center'}>
<Button
variant="outline"
color="#228be6"
radius={'5px'}
className={classes['google-btn']}
onClick={() => setIsLoginERP(false)}
>
Sign in normally
</Button>
</Box>
)}
<Box ta={'center'}>
<Button
variant="outline"
color="#228be6"
radius={'5px'}
onClick={() => handleLoginGG()}
className={classes['google-btn']}
>
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/480px-Google_%22G%22_logo.svg.png"
alt="Google logo"
className={classes['google-icon']}
/>
Sign in with Google
</Button>
</Box>
</form>
)
}
export default Login

View File

@ -1,63 +0,0 @@
import { useState } from "react";
import { Anchor, Image, Paper, Text } from "@mantine/core";
import Login from "./Login";
import Register from "./Register";
import classes from "./AuthenticationImage.module.css";
export const PageLogin = () => {
const [isRegister, setIsRegister] = useState(false);
return (
<div
style={{
height: "100vh",
}}
>
<div className={classes.wrapper}>
<Paper className={classes.form} radius={0} p={30}>
<Image
w={"45%"}
mt={"sm"}
mb={"xs"}
m={"0 auto"}
src={import.meta.env.VITE_DOMAIN + "logo-ATC-removebg-preview.png"}
/>
{isRegister ? (
<>
<Register />
<Text ta="center" mt="md">
You have an account?{" "}
<Anchor<"a">
href="#"
fw={700}
onClick={() => setIsRegister(false)}
>
Sign in
</Anchor>
</Text>
</>
) : (
<>
<Login />
<Text ta="center" mt="md">
Don&apos;t have an account?{" "}
<Anchor<"a">
href="#"
fw={700}
onClick={() => setIsRegister(true)}
>
Register
</Anchor>
</Text>
</>
)}
</Paper>
</div>
</div>
);
};
export default PageLogin;

View File

@ -1,77 +1,48 @@
import { useState } from "react"; import { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from '@/rtk/store'
import { registerAsync } from '@/rtk/slices/authSlice'
import { Box, Button, PasswordInput, TextInput } from "@mantine/core"; import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
import { emailRegex } from "../../untils/helper"; import { emailRegex, passwordRegex } from '@/utils/formRegexs'
import { notifications } from "@mantine/notifications"; import { requirementsPassword } from '@/rtk/helpers/variables'
import axios from "axios";
const apiUrl = import.meta.env.VITE_BACKEND_URL; import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
type TRegister = { type TRegister = {
email: string; email: string
password: string; password: string
confirm_password: string; confirm_password: string
full_name: string; full_name: string
}; }
function Register() { function Register() {
const dispatch = useDispatch<AppDispatch>()
const { status } = useSelector((state: RootState) => state.auth)
const [formRegister, setFormRegister] = useState<TRegister>({ const [formRegister, setFormRegister] = useState<TRegister>({
email: "", email: '',
full_name: "", full_name: '',
password: "", password: '',
confirm_password: "", confirm_password: '',
}); })
const handleRegister = async () => { const handleRegister = async () => {
try { // Dispatch action registerAsync với dữ liệu form và đợi kết quả
if (!formRegister.email) { const resultAction = await dispatch(registerAsync(formRegister))
notifications.show({
title: "Error", // Kiểm tra nếu action thành công
message: "Email is required", if (registerAsync.fulfilled.match(resultAction)) {
color: "red", // Tải lại trang web
}); // window.location.reload()
return;
} }
if (!formRegister.password) { }
notifications.show({
title: "Error",
message: "Password is required",
color: "red",
});
return;
}
const payload = {
email: formRegister.email,
password: formRegister.password,
full_name: formRegister.full_name,
};
const response = await axios.post(apiUrl + "api/auth/register", payload);
if (response.data.user) {
const user = response.data.user;
user.fullName = user.full_name;
localStorage.setItem("user", JSON.stringify(user));
window.location.href = "/";
} else {
notifications.show({
title: "Error",
message: response.data.message,
color: "red",
});
}
} catch (error) {
console.log(error);
notifications.show({
title: "Error",
message: "Register fail, please try again!",
color: "red",
});
}
};
return ( return (
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault()
handleRegister(); handleRegister()
}} }}
> >
<TextInput <TextInput
@ -79,12 +50,12 @@ function Register() {
placeholder="hello@gmail.com" placeholder="hello@gmail.com"
value={formRegister.email} value={formRegister.email}
error={ error={
emailRegex.test(formRegister.email) || formRegister.email === "" emailRegex.test(formRegister.email) || formRegister.email === ''
? null ? null
: "Invalid email" : 'Invalid email'
} }
onChange={(e) => { onChange={(e) => {
setFormRegister({ ...formRegister, email: e.target.value }); setFormRegister({ ...formRegister, email: e.target.value })
}} }}
required required
size="md" size="md"
@ -97,22 +68,19 @@ function Register() {
placeholder="Bill Gates" placeholder="Bill Gates"
value={formRegister.full_name} value={formRegister.full_name}
onChange={(e) => { onChange={(e) => {
setFormRegister({ ...formRegister, full_name: e.target.value }); setFormRegister({ ...formRegister, full_name: e.target.value })
}} }}
required required
size="md" size="md"
/> />
<PasswordInput <PasswordRequirementInput
mt="md" requirements={requirementsPassword}
value={formRegister}
setValue={setFormRegister}
label="Password" label="Password"
placeholder="Your password" placeholder="Password"
value={formRegister.password} name="password"
onChange={(e) => {
setFormRegister({ ...formRegister, password: e.target.value });
}}
required
size="md"
/> />
<PasswordInput <PasswordInput
@ -122,29 +90,27 @@ function Register() {
value={formRegister.confirm_password} value={formRegister.confirm_password}
error={ error={
formRegister.confirm_password === formRegister.password || formRegister.confirm_password === formRegister.password ||
formRegister.confirm_password === "" formRegister.confirm_password === ''
? null ? null
: "Password do not match" : 'Password do not match'
} }
onChange={(e) => { onChange={(e) => {
setFormRegister({ setFormRegister({ ...formRegister, confirm_password: e.target.value })
...formRegister,
confirm_password: e.target.value,
});
}} }}
required required
size="md" size="md"
/> />
<Box ta={"center"}> <Box ta={'center'}>
<Button <Button
type="submit" type="submit"
m="15px auto" m="15px auto"
fullWidth fullWidth
size="md" size="md"
loading={status === "loading"} loading={status === 'loading'}
disabled={ disabled={
formRegister.password !== "" && formRegister.password !== '' &&
passwordRegex.test(formRegister.password) &&
formRegister.password === formRegister.confirm_password formRegister.password === formRegister.confirm_password
? false ? false
: true : true
@ -154,7 +120,7 @@ function Register() {
</Button> </Button>
</Box> </Box>
</form> </form>
); )
} }
export default Register; export default Register

View File

@ -1,5 +1,5 @@
import { Card, Text, Box, Flex } from "@mantine/core"; import { Card, Text, Box, Flex } from "@mantine/core";
import type { TLine, TStation } from "../untils/types"; import type { LineConfig, TLine, TStation } from "../untils/types";
import classes from "./Component.module.css"; 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";
@ -12,16 +12,14 @@ const CardLine = ({
setSelectedLines, setSelectedLines,
socket, socket,
stationItem, stationItem,
openTerminal, updateStatus,
loadTerminal,
}: { }: {
line: TLine; line: TLine;
selectedLines: TLine[]; selectedLines: TLine[];
setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void; setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
socket: Socket | null; socket: Socket | null;
stationItem: TStation; stationItem: TStation;
openTerminal: (value: TLine) => void; updateStatus: (value: LineConfig) => void;
loadTerminal: boolean;
}) => { }) => {
return ( return (
<Card <Card
@ -35,11 +33,6 @@ const CardLine = ({
? { backgroundColor: "#8bf55940" } ? { backgroundColor: "#8bf55940" }
: {} : {}
} }
onDoubleClick={(e) => {
e.preventDefault();
e.stopPropagation();
openTerminal(line);
}}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -52,27 +45,16 @@ const CardLine = ({
justify={"space-between"} justify={"space-between"}
direction={"column"} direction={"column"}
// gap={"md"} // gap={"md"}
// align={"center"} align={"center"}
> >
<Flex justify={"space-between"}> <div>
<Text fw={600} style={{ display: "flex", gap: "4px" }}> <Text fw={600} style={{ display: "flex", gap: "4px" }}>
Line: {line.lineNumber || line.line_number} - {line.port}{" "} Line: {line.lineNumber || line.line_number} - {line.port}{" "}
{line.status === "connected" && ( {line.status === "connected" && (
<IconCircleCheckFilled color="green" /> <IconCircleCheckFilled color="green" />
)} )}
</Text> </Text>
<div
style={{
alignItems: "center",
marginLeft: "16px",
fontSize: "12px",
color: "red",
display: "flex",
}}
>
{line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""}
</div> </div>
</Flex>
{/* <Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text> {/* <Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
<div className={classes.info_line}>SN: FGL2240307M</div> <div className={classes.info_line}>SN: FGL2240307M</div>
<div className={classes.info_line}>VID: V01</div> */} <div className={classes.info_line}>VID: V01</div> */}
@ -84,11 +66,9 @@ const CardLine = ({
style={{ height: "175px", width: "300px" }} style={{ height: "175px", width: "300px" }}
> >
<TerminalCLI <TerminalCLI
cliOpened={loadTerminal} cliOpened={true}
socket={socket} socket={socket}
content={line?.output ?? ""} content={line.netOutput ?? ""}
initContent={line?.netOutput ?? ""}
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={false}
@ -102,9 +82,8 @@ const CardLine = ({
padding: "0px", padding: "0px",
paddingBottom: "0px", paddingBottom: "0px",
}} }}
onDoubleClick={() => { onDoubleClick={() => {}}
openTerminal(line); updateStatus={updateStatus}
}}
/> />
</Box> </Box>
</Flex> </Flex>

View File

@ -8,7 +8,6 @@ import {
Grid, Grid,
TextInput, TextInput,
Button, Button,
Checkbox,
} from "@mantine/core"; } from "@mantine/core";
import { IconSettingsPlus } from "@tabler/icons-react"; import { IconSettingsPlus } from "@tabler/icons-react";
import TableRows from "./Scenario/TableRows"; import TableRows from "./Scenario/TableRows";
@ -43,10 +42,11 @@ function DrawerScenario({
send: "", send: "",
delay: "0", delay: "0",
repeat: "1", repeat: "1",
note: "",
}, },
] as IBodyScenario[], ] as IBodyScenario[],
timeout: "30000", timeout: "30000",
isReboot: false, is_reboot: false,
}, },
validate: { validate: {
title: (value) => { title: (value) => {
@ -72,6 +72,7 @@ function DrawerScenario({
send: "", send: "",
delay: "0", delay: "0",
repeat: "1", repeat: "1",
note: "",
}); });
form.setFieldValue("body", newBody); form.setFieldValue("body", newBody);
}; };
@ -83,22 +84,6 @@ function DrawerScenario({
}; };
const handleSave = async () => { const handleSave = async () => {
if (!form.values.title) {
notifications.show({
title: "Error",
message: "Title is required",
color: "red",
});
return;
}
if (!form.values.timeout) {
notifications.show({
title: "Error",
message: "Timeout is required",
color: "red",
});
return;
}
setIsSubmit(true); setIsSubmit(true);
try { try {
const body = form.values.body.map((el: IBodyScenario) => ({ const body = form.values.body.map((el: IBodyScenario) => ({
@ -108,8 +93,7 @@ function DrawerScenario({
})); }));
const payload = { const payload = {
title: form.values.title, ...form.values,
isReboot: form.values.isReboot,
body: body, body: body,
timeout: Number(form.values.timeout), timeout: Number(form.values.timeout),
}; };
@ -127,28 +111,15 @@ function DrawerScenario({
) )
: [...pre, scenario] : [...pre, scenario]
); );
setIsEdit(true);
setDataScenario(scenario);
notifications.show({ notifications.show({
title: "Success", title: "Success",
message: res.data.message, message: res.data.message,
color: "green", color: "green",
}); });
return; return;
} else {
notifications.show({
title: "Error",
message: res.data.message,
color: "red",
});
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
notifications.show({
title: "Error",
message: "Failed to create scenario, please try again!",
color: "red",
});
} finally { } finally {
setIsSubmit(false); setIsSubmit(false);
} }
@ -225,7 +196,7 @@ function DrawerScenario({
form.setFieldValue("title", scenario.title); form.setFieldValue("title", scenario.title);
form.setFieldValue("timeout", scenario.timeout.toString()); form.setFieldValue("timeout", scenario.timeout.toString());
form.setFieldValue("body", JSON.parse(scenario.body)); form.setFieldValue("body", JSON.parse(scenario.body));
form.setFieldValue("isReboot", scenario.isReboot); form.setFieldValue("is_reboot", scenario.is_reboot);
} }
}} }}
> >
@ -260,27 +231,7 @@ function DrawerScenario({
required required
/> />
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col span={6}>
span={3}
style={{
display: "flex",
alignItems: "end",
marginBottom: "8px",
}}
>
<Checkbox
label="Reboot"
style={{ color: "red" }}
checked={form.values.isReboot}
onChange={(event) =>
form.setFieldValue(
"isReboot",
event.currentTarget.checked
)
}
/>
</Grid.Col>
<Grid.Col span={3}>
<div <div
style={{ style={{
display: "flex", display: "flex",
@ -378,7 +329,7 @@ function DrawerScenario({
close={() => { close={() => {
setOpenConfirm(false); setOpenConfirm(false);
}} }}
message={"Are you sure delete this scenario?"} message={"Are you sure delete this station?"}
handle={() => { handle={() => {
setOpenConfirm(false); setOpenConfirm(false);
handleDelete(); handleDelete();

View File

@ -1,120 +0,0 @@
import { Box, Button, Grid, Modal, Text } from "@mantine/core";
import type { 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 { IconCircleCheckFilled } from "@tabler/icons-react";
const ModalTerminal = ({
opened,
onClose,
line,
socket,
stationItem,
scenarios,
}: {
opened: boolean;
onClose: () => void;
line: TLine | undefined;
socket: Socket | null;
stationItem: TStation | undefined;
scenarios: IScenario[];
}) => {
const [isDisable, setIsDisable] = useState<boolean>(false);
// console.log(line);
return (
<Box>
<Modal
opened={opened}
onClose={() => {
onClose();
socket?.emit("close_cli", {
lineId: line?.id,
stationId: line?.station_id,
});
}}
size={"80%"}
style={{ position: "absolute", left: 0 }}
title={
<Box
style={{
display: "flex",
justifyContent: "center",
}}
>
<Text size="md" mr={10}>
Line number: <strong>{line?.line_number || ""}</strong>
</Text>
<Text size="md" mr={10}>
- <strong>{line?.port || ""}</strong>
</Text>
{line?.status === "connected" && (
<IconCircleCheckFilled color="green" />
)}
<div
style={{
alignItems: "center",
marginLeft: "16px",
fontSize: "12px",
color: "red",
display: "flex",
}}
>
{line?.userOpenCLI
? line?.userOpenCLI + " is using"
: "Terminal is used"}
</div>
</Box>
}
>
<Grid>
<Grid.Col span={10} style={{ borderRight: "1px solid #ccc" }}>
<TerminalCLI
cliOpened={opened}
socket={socket}
content={line?.output ?? ""}
initContent={line?.netOutput ?? ""}
loadingContent={line?.loadingOutput}
line_id={Number(line?.id)}
station_id={Number(stationItem?.id)}
isDisabled={false}
line_status={line?.status || ""}
/>
</Grid.Col>
<Grid.Col span={2}>
{scenarios.map((scenario) => (
<Button
disabled={isDisable}
className={classes.buttonScenario}
key={scenario.id}
miw={"100px"}
mb={"6px"}
style={{ minHeight: "24px" }}
mr={"5px"}
variant={"outline"}
onClick={async () => {
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 10000);
if (line)
socket?.emit(
"run_scenario",
Object.assign(line, {
scenario: scenario,
})
);
}}
>
{scenario.title}
</Button>
))}
</Grid.Col>
</Grid>
</Modal>
</Box>
);
};
export default ModalTerminal;

View File

@ -4,11 +4,11 @@ import "xterm/css/xterm.css";
import { FitAddon } from "@xterm/addon-fit"; import { FitAddon } from "@xterm/addon-fit";
import { SOCKET_EVENTS } from "../untils/constanst"; import { SOCKET_EVENTS } from "../untils/constanst";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import type { LineConfig } from "../untils/types";
interface TerminalCLIProps { interface TerminalCLIProps {
socket: Socket | null; socket: Socket | null;
content?: string; content?: string;
initContent?: string;
line_id: number; line_id: number;
line_status: string; line_status: string;
station_id: number; station_id: number;
@ -25,7 +25,7 @@ interface TerminalCLIProps {
onDoubleClick?: () => void; onDoubleClick?: () => void;
fontSize?: number; fontSize?: number;
miniSize?: boolean; miniSize?: boolean;
loadingContent?: boolean; updateStatus: (value: LineConfig) => void;
} }
const TerminalCLI: React.FC<TerminalCLIProps> = ({ const TerminalCLI: React.FC<TerminalCLIProps> = ({
@ -39,8 +39,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
onDoubleClick = () => {}, onDoubleClick = () => {},
fontSize = 14, fontSize = 14,
miniSize = false, miniSize = false,
initContent = "", updateStatus,
loadingContent = false,
}) => { }) => {
const xtermRef = useRef<HTMLDivElement>(null); const xtermRef = useRef<HTMLDivElement>(null);
const terminal = useRef<Terminal>(null); const terminal = useRef<Terminal>(null);
@ -129,7 +128,40 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500); if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
} }
}, [content, loadingContent]); }, [content]);
useEffect(() => {
// Nhận output từ thiết bị và ghi vào terminal
socket?.on("line_output", (data) => {
if (data?.lineId === line_id && terminal.current) {
terminal.current?.write(data.data);
terminal.current?.focus();
}
});
socket?.on("line_error", (data) => {
if (data?.lineId === line_id && terminal.current) {
terminal.current?.write(data.error);
}
});
socket?.on("init", (data) => {
if (Array.isArray(data)) {
data.forEach((value) => {
if (value?.id === line_id && terminal.current) {
terminal.current?.write(value.output);
updateStatus({ ...value, lineId: value.id });
}
});
}
});
return () => {
socket?.off("init");
socket?.off("line_error");
socket?.off("line_output");
};
}, []);
useEffect(() => { useEffect(() => {
if (cliOpened) { if (cliOpened) {
@ -150,7 +182,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
useEffect(() => { useEffect(() => {
if (!loading) { if (!loading) {
if (terminal.current) { if (terminal.current) {
terminal.current?.write(initContent); terminal.current?.write(content);
if (!miniSize && !isDisabled) terminal.current?.focus(); if (!miniSize && !isDisabled) terminal.current?.focus();
terminal.current.scrollToBottom(); terminal.current.scrollToBottom();
} }
@ -179,7 +211,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 ?? "60vh",
}} }}
> >
<div <div
@ -189,8 +221,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 ?? "60vh",
height: customStyle.height ?? "75vh", height: customStyle.height ?? "60vh",
padding: customStyle.padding ?? "4px", padding: customStyle.padding ?? "4px",
}} }}
onDoubleClick={(event) => { onDoubleClick={(event) => {

View File

@ -1,10 +1,4 @@
import React, { import React, { createContext, useContext, useEffect, useState } from "react";
createContext,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { SOCKET_EVENTS } from "../untils/constanst"; import { SOCKET_EVENTS } from "../untils/constanst";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
@ -21,21 +15,9 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
children, children,
}) => { }) => {
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const user = useMemo(() => {
return localStorage.getItem("user") &&
typeof localStorage.getItem("user") === "string"
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
useEffect(() => { useEffect(() => {
if (!user) return; const newSocket = io(SOCKET_URL);
const newSocket = io(SOCKET_URL, {
auth: {
userId: user?.id,
userName: user?.fullName,
},
});
setSocket(newSocket); setSocket(newSocket);
@ -64,7 +46,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
newSocket.disconnect(); newSocket.disconnect();
}; };
}, [user]); }, []);
return ( return (
<SocketContext.Provider value={{ socket }}> <SocketContext.Provider value={{ socket }}>

View File

@ -2,8 +2,3 @@ export const numberOnly = (value: string): string => {
const matched = value.match(/[\d.]+/g); const matched = value.match(/[\d.]+/g);
return matched ? matched.join("") : ""; return matched ? matched.join("") : "";
}; };
export const passwordRegex =
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/;
export const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;

View File

@ -67,8 +67,6 @@ export type TLine = {
inventory?: any; inventory?: any;
status?: string; status?: string;
netOutput?: string; netOutput?: string;
output?: string;
loadingOutput?: boolean;
outlet?: number; outlet?: number;
cliOpened?: boolean; cliOpened?: boolean;
systemLogUrl?: string; systemLogUrl?: string;
@ -86,8 +84,14 @@ export type TLine = {
}; };
export type TUser = { export type TUser = {
userId: number; id: number;
userName: string; email: string;
email_cc: string;
full_name: string;
package_id: string;
zulip: string;
token?: string;
name: string;
}; };
export type APCProps = { export type APCProps = {
@ -140,7 +144,7 @@ export type IScenario = {
title: string; title: string;
body: string; body: string;
timeout: number; timeout: number;
isReboot: boolean; is_reboot: boolean;
updated_at: string; updated_at: string;
}; };
@ -149,4 +153,5 @@ export type IBodyScenario = {
send: string; send: string;
delay: string; delay: string;
repeat: string; repeat: string;
note: string;
}; };