Update
This commit is contained in:
parent
85c4bb9a26
commit
077a2ddc35
|
|
@ -0,0 +1,42 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import User from '../models/user.js'
|
||||||
|
|
||||||
|
export default class AuthController {
|
||||||
|
// Đăng ký
|
||||||
|
async register({ request, response }: HttpContext) {
|
||||||
|
const data = request.only(['email', 'password', 'full_name'])
|
||||||
|
const user = await User.create(data)
|
||||||
|
return response.json({ message: 'User created', user })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đăng nhập
|
||||||
|
async login({ request, auth, response }: HttpContext) {
|
||||||
|
const { email, password } = request.only(['email', 'password'])
|
||||||
|
const user = await User.query().where('email', email).first()
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response.status(401).json({ message: 'Invalid email or password' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// So sánh password
|
||||||
|
if (user.password !== 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({
|
||||||
|
message: 'Login successful',
|
||||||
|
user: { id: user.id, email: user.email, token },
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return response.status(401).json({ message: 'Invalid credentials' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đăng xuất
|
||||||
|
async logout({ auth, response }: HttpContext) {
|
||||||
|
return response.json({ message: 'Logged out successfully' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import User from '#models/user'
|
import User from '#models/user'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import hash from '@adonisjs/core/services/hash'
|
|
||||||
|
|
||||||
export default class UsersController {
|
export default class UsersController {
|
||||||
async index({ request, response }: HttpContext) {
|
async index({ request, response }: HttpContext) {
|
||||||
|
|
@ -37,13 +36,10 @@ export default class UsersController {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the password before saving
|
|
||||||
const hashedPassword = await hash.make(data.password)
|
|
||||||
|
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
fullName: data.full_name,
|
fullName: data.full_name,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: hashedPassword,
|
password: data.password,
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.created({
|
return response.created({
|
||||||
|
|
@ -73,11 +69,6 @@ export default class UsersController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the password if it is provided
|
|
||||||
if (data.password) {
|
|
||||||
data.password = await hash.make(data.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
user.merge(data)
|
user.merge(data)
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@ export default class AuthMiddleware {
|
||||||
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
|
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ export default class Line extends BaseModel {
|
||||||
declare port: number
|
declare port: number
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare line_number: number
|
declare lineNumber: number
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare line_clear: number
|
declare lineClear: number
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare stationId: number
|
declare stationId: number
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare apc_name: number
|
declare apcName: string
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare outlet: number
|
declare outlet: number
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import net from 'node:net'
|
import net from 'node:net'
|
||||||
|
import { cleanData } from '../ultils/helper.js'
|
||||||
|
|
||||||
interface LineConfig {
|
interface LineConfig {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -7,6 +8,8 @@ interface LineConfig {
|
||||||
ip: string
|
ip: string
|
||||||
stationId: number
|
stationId: number
|
||||||
apcName?: string
|
apcName?: string
|
||||||
|
output: string
|
||||||
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LineConnection {
|
export default class LineConnection {
|
||||||
|
|
@ -20,12 +23,19 @@ export default class LineConnection {
|
||||||
this.client = new net.Socket()
|
this.client = new net.Socket()
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect(timeoutMs = 5000) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const { ip, port, lineNumber, id, stationId } = this.config
|
const { ip, port, lineNumber, id, stationId } = this.config
|
||||||
|
let resolvedOrRejected = false
|
||||||
|
// Set timeout
|
||||||
|
this.client.setTimeout(timeoutMs)
|
||||||
|
|
||||||
this.client.connect(port, ip, () => {
|
this.client.connect(port, ip, () => {
|
||||||
|
if (resolvedOrRejected) return
|
||||||
|
resolvedOrRejected = true
|
||||||
|
|
||||||
console.log(`✅ Connected to line ${lineNumber} (${ip}:${port})`)
|
console.log(`✅ Connected to line ${lineNumber} (${ip}:${port})`)
|
||||||
|
this.config.status = 'connected'
|
||||||
this.socketIO.emit('line_connected', {
|
this.socketIO.emit('line_connected', {
|
||||||
stationId,
|
stationId,
|
||||||
lineId: id,
|
lineId: id,
|
||||||
|
|
@ -36,8 +46,19 @@ export default class LineConnection {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.client.on('data', (data) => {
|
this.client.on('data', (data) => {
|
||||||
const message = data.toString().trim()
|
let message = data.toString()
|
||||||
console.log(`📨 [${this.config.apcName}] ${message}`)
|
// let output = cleanData(message)
|
||||||
|
// console.log(`📨 [${this.config.port}] ${message}`)
|
||||||
|
// Handle netOutput with backspace support
|
||||||
|
for (const char of message) {
|
||||||
|
if (char === '\x7F' || char === '\x08') {
|
||||||
|
this.config.output = this.config.output.slice(0, -1)
|
||||||
|
// message = message.slice(0, -1)
|
||||||
|
} else {
|
||||||
|
this.config.output += cleanData(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.config.output = this.config.output.slice(-15000)
|
||||||
this.socketIO.emit('line_output', {
|
this.socketIO.emit('line_output', {
|
||||||
stationId,
|
stationId,
|
||||||
lineId: id,
|
lineId: id,
|
||||||
|
|
@ -46,7 +67,10 @@ export default class LineConnection {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.client.on('error', (err) => {
|
this.client.on('error', (err) => {
|
||||||
|
if (resolvedOrRejected) return
|
||||||
|
resolvedOrRejected = true
|
||||||
console.error(`❌ Error line ${lineNumber}:`, err.message)
|
console.error(`❌ Error line ${lineNumber}:`, err.message)
|
||||||
|
this.config.output += err.message
|
||||||
this.socketIO.emit('line_error', {
|
this.socketIO.emit('line_error', {
|
||||||
stationId,
|
stationId,
|
||||||
lineId: id,
|
lineId: id,
|
||||||
|
|
@ -57,12 +81,23 @@ export default class LineConnection {
|
||||||
|
|
||||||
this.client.on('close', () => {
|
this.client.on('close', () => {
|
||||||
console.log(`🔌 Line ${lineNumber} disconnected`)
|
console.log(`🔌 Line ${lineNumber} disconnected`)
|
||||||
|
this.config.status = 'disconnected'
|
||||||
this.socketIO.emit('line_disconnected', {
|
this.socketIO.emit('line_disconnected', {
|
||||||
stationId,
|
stationId,
|
||||||
lineId: id,
|
lineId: id,
|
||||||
lineNumber,
|
lineNumber,
|
||||||
|
status: 'disconnected',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.client.on('timeout', () => {
|
||||||
|
if (resolvedOrRejected) return
|
||||||
|
resolvedOrRejected = true
|
||||||
|
|
||||||
|
console.log(`⏳ Connection timeout line ${lineNumber}`)
|
||||||
|
this.client.destroy()
|
||||||
|
reject(new Error('Connection timeout'))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,13 +106,26 @@ export default class LineConnection {
|
||||||
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log(`➡️ [${this.config.apcName}] SEND:`, cmd)
|
// console.log(`➡️ [${this.config.apcName}] SEND:`, cmd)
|
||||||
this.client.write(`${cmd}\r\n`)
|
this.client.write(`${cmd}\r\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeCommand(cmd: string) {
|
||||||
|
if (this.client.destroyed) {
|
||||||
|
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.client.write(`${cmd}`)
|
||||||
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
try {
|
try {
|
||||||
this.client.destroy()
|
this.client.destroy()
|
||||||
|
this.config.status = 'disconnected'
|
||||||
|
this.socketIO.emit('line_disconnected', {
|
||||||
|
...this.config,
|
||||||
|
status: 'disconnected',
|
||||||
|
})
|
||||||
console.log(`🔻 Closed connection to line ${this.config.lineNumber}`)
|
console.log(`🔻 Closed connection to line ${this.config.lineNumber}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error closing line:', e)
|
console.error('Error closing line:', e)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Function to clean up unwanted characters from the output data.
|
||||||
|
* @param {string} data - The raw data to be cleaned.
|
||||||
|
* @returns {string} - The cleaned data.
|
||||||
|
*/
|
||||||
|
export const cleanData = (data) => {
|
||||||
|
return data
|
||||||
|
.replace(/--More--\s*BS\s*BS\s*BS\s*BS\s*BS\s*BS/g, '')
|
||||||
|
.replace(/\s*--More--\s*/g, '')
|
||||||
|
.replace(/\x1b\[[0-9;]*m/g, '') // Remove ANSI escape codes
|
||||||
|
.replace(/\x08/g, '')
|
||||||
|
.replace(/[^\x20-\x7E\r\n]/g, '') // Remove non-printable characters
|
||||||
|
// .replace(/\r\n/g, '\n')
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^9.4.0",
|
"@adonisjs/auth": "^9.5.1",
|
||||||
"@adonisjs/core": "^6.18.0",
|
"@adonisjs/core": "^6.18.0",
|
||||||
"@adonisjs/cors": "^2.2.1",
|
"@adonisjs/cors": "^2.2.1",
|
||||||
"@adonisjs/lucid": "^21.6.1",
|
"@adonisjs/lucid": "^21.6.1",
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
"typescript": "~5.8"
|
"typescript": "~5.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^9.4.0",
|
"@adonisjs/auth": "^9.5.1",
|
||||||
"@adonisjs/core": "^6.18.0",
|
"@adonisjs/core": "^6.18.0",
|
||||||
"@adonisjs/cors": "^2.2.1",
|
"@adonisjs/cors": "^2.2.1",
|
||||||
"@adonisjs/lucid": "^21.6.1",
|
"@adonisjs/lucid": "^21.6.1",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import LineConnection from '../app/services/line_connection.js'
|
||||||
import { ApplicationService } from '@adonisjs/core/types'
|
import { ApplicationService } from '@adonisjs/core/types'
|
||||||
import env from '#start/env'
|
import env from '#start/env'
|
||||||
import { CustomServer, CustomSocket } from '../app/ultils/types.js'
|
import { CustomServer, CustomSocket } from '../app/ultils/types.js'
|
||||||
|
import Line from '#models/line'
|
||||||
|
|
||||||
interface Station {
|
interface Station {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -77,24 +78,37 @@ export class WebSocketIo {
|
||||||
console.log('Socket connected:', socket.id)
|
console.log('Socket connected:', socket.id)
|
||||||
socket.connectionTime = new Date()
|
socket.connectionTime = new Date()
|
||||||
|
|
||||||
|
const lineConnectionArray: LineConnection[] = Array.from(this.lineMap.values())
|
||||||
|
io.to(socket.id).emit(
|
||||||
|
'init',
|
||||||
|
lineConnectionArray.map((el) => el.config)
|
||||||
|
)
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.log(`🔴 FE disconnected: ${socket.id}`)
|
console.log(`FE disconnected: ${socket.id}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// FE gửi yêu cầu connect lines
|
// FE gửi yêu cầu connect lines
|
||||||
socket.on('connect_lines', async (stationData: Station) => {
|
socket.on('connect_lines', async (data) => {
|
||||||
console.log('📡 Yêu cầu connect station:', stationData.name)
|
const { stationData, linesData } = data
|
||||||
await this.connectStation(socket, stationData)
|
await this.connectLine(io, linesData, stationData)
|
||||||
})
|
})
|
||||||
|
|
||||||
// FE gửi command đến line cụ thể
|
socket.on('write_command_line_from_web', (data) => {
|
||||||
socket.on('send_command', (data) => {
|
const { lineIds, stationId, command } = data
|
||||||
const { lineId, command } = data
|
for (const lineId of lineIds) {
|
||||||
const line = this.lineMap.get(lineId)
|
const line = this.lineMap.get(lineId)
|
||||||
if (line) {
|
if (line) {
|
||||||
line.sendCommand(command)
|
this.setTimeoutConnect(lineId, line)
|
||||||
} else {
|
line.writeCommand(command)
|
||||||
socket.emit('line_error', { lineId, error: 'Line not connected' })
|
} else {
|
||||||
|
io.emit('line_disconnected', {
|
||||||
|
stationId,
|
||||||
|
lineId,
|
||||||
|
status: 'disconnected',
|
||||||
|
})
|
||||||
|
io.emit('line_error', { lineId, error: 'Line not connected\r\n' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -111,22 +125,30 @@ export class WebSocketIo {
|
||||||
return io
|
return io
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connectStation(socket, station: Station) {
|
private async connectLine(socket: any, lines: Line[], station: Station) {
|
||||||
this.stationMap.set(station.id, station)
|
try {
|
||||||
for (const line of station.lines) {
|
this.stationMap.set(station.id, station)
|
||||||
const lineConn = new LineConnection(
|
for (const line of lines) {
|
||||||
{
|
const lineConn = new LineConnection(
|
||||||
id: line.id,
|
{
|
||||||
port: line.port,
|
id: line.id,
|
||||||
ip: station.ip,
|
port: line.port,
|
||||||
lineNumber: line.lineNumber,
|
ip: station.ip,
|
||||||
stationId: station.id,
|
lineNumber: line.lineNumber,
|
||||||
apcName: line.apcName,
|
stationId: station.id,
|
||||||
},
|
apcName: line.apcName,
|
||||||
this.io
|
output: '',
|
||||||
)
|
status: '',
|
||||||
await lineConn.connect()
|
},
|
||||||
this.lineMap.set(line.id, lineConn)
|
socket
|
||||||
|
)
|
||||||
|
await lineConn.connect()
|
||||||
|
lineConn.writeCommand('\r\n\r\n')
|
||||||
|
this.lineMap.set(line.id, lineConn)
|
||||||
|
this.setTimeoutConnect(line.id, lineConn)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,4 +167,17 @@ export class WebSocketIo {
|
||||||
this.stationMap.delete(stationId)
|
this.stationMap.delete(stationId)
|
||||||
console.log(`🔻 Station ${station.name} disconnected`)
|
console.log(`🔻 Station ${station.name} disconnected`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setTimeoutConnect = (lineId: number, lineConn: LineConnection) => {
|
||||||
|
if (this.intervalMap[`${lineId}`]) {
|
||||||
|
clearInterval(this.intervalMap[`${lineId}`])
|
||||||
|
delete this.intervalMap[`${lineId}`]
|
||||||
|
}
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
lineConn.disconnect()
|
||||||
|
this.lineMap.delete(lineId)
|
||||||
|
}, 120000)
|
||||||
|
|
||||||
|
this.intervalMap[`${lineId}`] = interval
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,9 @@ router
|
||||||
router.post('delete', '#controllers/scenarios_controller.delete')
|
router.post('delete', '#controllers/scenarios_controller.delete')
|
||||||
})
|
})
|
||||||
.prefix('api/scenarios')
|
.prefix('api/scenarios')
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.post('/login', '#controllers/auth_controller.login')
|
||||||
|
})
|
||||||
|
.prefix('api/auth')
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>Automation Test</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,12 @@
|
||||||
"@mantine/dates": "^8.3.5",
|
"@mantine/dates": "^8.3.5",
|
||||||
"@mantine/notifications": "^8.3.5",
|
"@mantine/notifications": "^8.3.5",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"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",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"xterm": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
|
@ -1913,6 +1915,22 @@
|
||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
|
|
@ -4280,6 +4298,13 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@
|
||||||
"@mantine/dates": "^8.3.5",
|
"@mantine/dates": "^8.3.5",
|
||||||
"@mantine/notifications": "^8.3.5",
|
"@mantine/notifications": "^8.3.5",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"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",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"xterm": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,16 @@ import type { TLine, TStation } 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";
|
||||||
|
import { SocketProvider, useSocket } from "./context/SocketContext";
|
||||||
|
|
||||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Component
|
* Main Component
|
||||||
*/
|
*/
|
||||||
export default function App() {
|
export function App() {
|
||||||
document.title = "Automation Test";
|
document.title = "Automation Test";
|
||||||
|
const { socket } = useSocket();
|
||||||
const [stations, setStations] = useState<TStation[]>([]);
|
const [stations, setStations] = useState<TStation[]>([]);
|
||||||
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState("0");
|
const [activeTab, setActiveTab] = useState("0");
|
||||||
|
|
@ -62,95 +64,162 @@ export default function App() {
|
||||||
getStation();
|
getStation();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<MantineProvider>
|
if (!socket || !stations?.length) return;
|
||||||
<Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
|
|
||||||
{/* Tabs (Top Bar) */}
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={(id) => setActiveTab(id?.toString() || "0")}
|
|
||||||
variant="none"
|
|
||||||
keepMounted={false}
|
|
||||||
>
|
|
||||||
<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
|
const updateStatus = (data: any) => {
|
||||||
target={activeTab ? controlsRefs[activeTab] : null}
|
const line = getLine(data.lineId, data.stationId);
|
||||||
parent={rootRef}
|
if (line) {
|
||||||
className={classes.indicator}
|
updateValueLineStation(line, "status", data.status);
|
||||||
/>
|
}
|
||||||
<Flex gap={"sm"}>
|
};
|
||||||
<ActionIcon title="Add Station" variant="outline" color="green">
|
|
||||||
<IconSettingsPlus />
|
socket.on("line_connected", updateStatus);
|
||||||
</ActionIcon>
|
socket.on("line_disconnected", updateStatus);
|
||||||
|
socket?.on("init", (data) => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach((value) => {
|
||||||
|
updateStatus(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ cleanup on unmount or when socket changes
|
||||||
|
return () => {
|
||||||
|
socket.off("line_connected");
|
||||||
|
socket.off("line_disconnected");
|
||||||
|
socket.off("line_disconnected");
|
||||||
|
};
|
||||||
|
}, [socket, stations]);
|
||||||
|
|
||||||
|
const updateValueLineStation = (
|
||||||
|
currentLine: TLine,
|
||||||
|
field: string,
|
||||||
|
value: any
|
||||||
|
) => {
|
||||||
|
setStations((el) =>
|
||||||
|
el?.map((station: TStation) =>
|
||||||
|
station.id.toString() === activeTab
|
||||||
|
? {
|
||||||
|
...station,
|
||||||
|
lines: (station?.lines || [])?.map((lineItem: TLine) => {
|
||||||
|
if (lineItem.id === currentLine.id) {
|
||||||
|
return {
|
||||||
|
...lineItem,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return lineItem;
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: station
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLine = (lineId: number, stationId: number) => {
|
||||||
|
const station = stations?.find((sta) => sta.id === stationId);
|
||||||
|
if (station) {
|
||||||
|
const line = station.lines?.find((li) => li.id === lineId);
|
||||||
|
return line;
|
||||||
|
} else return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||||
|
{/* Tabs (Top Bar) */}
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(id) => setActiveTab(id?.toString() || "0")}
|
||||||
|
variant="none"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<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"}>
|
||||||
|
<ActionIcon title="Add Station" variant="outline" color="green">
|
||||||
|
<IconSettingsPlus />
|
||||||
|
</ActionIcon>
|
||||||
|
{Number(activeTab) && (
|
||||||
<ActionIcon title="Edit Station" variant="outline">
|
<ActionIcon title="Edit Station" variant="outline">
|
||||||
<IconEdit />
|
<IconEdit />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Flex>
|
)}
|
||||||
</Tabs.List>
|
</Flex>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
{stations.map((station) => (
|
{stations.map((station) => (
|
||||||
<Tabs.Panel
|
<Tabs.Panel
|
||||||
className={classes.content}
|
className={classes.content}
|
||||||
key={station.id}
|
key={station.id}
|
||||||
value={station.id.toString()}
|
value={station.id.toString()}
|
||||||
pt="md"
|
pt="md"
|
||||||
>
|
>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col
|
<Grid.Col
|
||||||
span={10}
|
span={11}
|
||||||
style={{
|
style={{
|
||||||
boxShadow: showBottomShadow
|
boxShadow: showBottomShadow
|
||||||
? "inset 0 -12px 10px -10px rgba(0, 0, 0, 0.2)"
|
? "inset 0 -12px 10px -10px rgba(0, 0, 0, 0.2)"
|
||||||
: "none",
|
: "none",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
h={"84vh"}
|
||||||
|
onScrollPositionChange={({ y }) => {
|
||||||
|
const el = document.querySelector(
|
||||||
|
".mantine-ScrollArea-viewport"
|
||||||
|
);
|
||||||
|
if (!el) return;
|
||||||
|
const maxScroll = el.scrollHeight - el.clientHeight;
|
||||||
|
setShowBottomShadow(y < maxScroll - 2);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollArea
|
{station.lines.length > 0 ? (
|
||||||
h={"84vh"}
|
<Flex wrap="wrap" gap="sm" justify={"center"}>
|
||||||
onScrollPositionChange={({ y }) => {
|
{station.lines.map((line, i) => (
|
||||||
const el = document.querySelector(
|
<CardLine
|
||||||
".mantine-ScrollArea-viewport"
|
key={i}
|
||||||
);
|
socket={socket}
|
||||||
if (!el) return;
|
stationItem={station}
|
||||||
const maxScroll = el.scrollHeight - el.clientHeight;
|
line={line}
|
||||||
setShowBottomShadow(y < maxScroll - 2);
|
selectedLines={selectedLines}
|
||||||
}}
|
setSelectedLines={setSelectedLines}
|
||||||
>
|
/>
|
||||||
{station.lines.length > 0 ? (
|
))}
|
||||||
<Flex wrap="wrap" gap="sm" justify={"center"}>
|
</Flex>
|
||||||
{[
|
) : (
|
||||||
...station.lines,
|
<Text ta="center" c="dimmed" mt="lg">
|
||||||
...station.lines,
|
No lines configured
|
||||||
...station.lines,
|
</Text>
|
||||||
].map((line) => (
|
)}
|
||||||
<CardLine
|
</ScrollArea>
|
||||||
line={line}
|
</Grid.Col>
|
||||||
selectedLines={selectedLines}
|
<Grid.Col
|
||||||
setSelectedLines={setSelectedLines}
|
span={1}
|
||||||
/>
|
style={{ backgroundColor: "#f1f1f1", borderRadius: 8 }}
|
||||||
))}
|
>
|
||||||
</Flex>
|
<Flex
|
||||||
) : (
|
direction={"column"}
|
||||||
<Text ta="center" c="dimmed" mt="lg">
|
align={"center"}
|
||||||
No lines configured
|
gap={"xs"}
|
||||||
</Text>
|
wrap={"wrap"}
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col
|
|
||||||
span={2}
|
|
||||||
style={{ backgroundColor: "#f1f1f1", borderRadius: 8 }}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
|
@ -165,12 +234,42 @@ export default function App() {
|
||||||
? "Select All"
|
? "Select All"
|
||||||
: "Deselect All"}
|
: "Deselect All"}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid.Col>
|
<Button
|
||||||
</Grid>
|
disabled={
|
||||||
</Tabs.Panel>
|
selectedLines.filter((el) => el.status !== "connected")
|
||||||
))}
|
.length === 0
|
||||||
</Tabs>
|
}
|
||||||
</Container>
|
variant="outline"
|
||||||
|
style={{ height: "30px", width: "120px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const lines = selectedLines.filter(
|
||||||
|
(el) => el.status !== "connected"
|
||||||
|
);
|
||||||
|
socket?.emit("connect_lines", {
|
||||||
|
stationData: station,
|
||||||
|
linesData: lines,
|
||||||
|
});
|
||||||
|
setSelectedLines([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Tabs.Panel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Main() {
|
||||||
|
return (
|
||||||
|
<MantineProvider>
|
||||||
|
<SocketProvider>
|
||||||
|
<App />
|
||||||
|
</SocketProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
.wrapper {
|
||||||
|
min-height: rem(100vh);
|
||||||
|
background-size: cover;
|
||||||
|
background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
|
||||||
|
min-height: rem(100vh);
|
||||||
|
max-width: rem(500px);
|
||||||
|
padding-top: rem(80px);
|
||||||
|
@media (max-width: var(--mantine-breakpoint-sm)) {
|
||||||
|
max-width: 100%;
|
||||||
|
};
|
||||||
|
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::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #007bff; /* blue accent */
|
||||||
|
margin: 0.1rem auto 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '@/rtk/store'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
PasswordInput,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { notifications } from '@mantine/notifications'
|
||||||
|
|
||||||
|
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||||
|
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||||
|
import { passwordRegex } from '@/utils/formRegexs'
|
||||||
|
|
||||||
|
import { post } from '@/rtk/helpers/apiService'
|
||||||
|
import { changePassword } from '@/api/Auth'
|
||||||
|
|
||||||
|
type TChangePassword = {
|
||||||
|
opened: boolean
|
||||||
|
setOpened: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangePassword({ opened, setOpened }: TChangePassword) {
|
||||||
|
const { user } = useSelector((state: RootState) => state.auth)
|
||||||
|
|
||||||
|
const [countSpam, setCountSpam] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
current_password: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChangePassword = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
if (countSpam > 5) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Password error more than 5 times. Logout after 3s',
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
window.location.reload()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await post(changePassword, {
|
||||||
|
email: user!.email,
|
||||||
|
current_password: formData.current_password,
|
||||||
|
password: formData.password,
|
||||||
|
confirm_password: formData.confirm_password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: res.message,
|
||||||
|
color: 'green',
|
||||||
|
})
|
||||||
|
|
||||||
|
setOpened(false)
|
||||||
|
setFormData({
|
||||||
|
current_password: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCountSpam(countSpam + 1)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid =
|
||||||
|
formData.current_password &&
|
||||||
|
formData.password &&
|
||||||
|
passwordRegex.test(formData.password) &&
|
||||||
|
formData.password === formData.confirm_password
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={() => {
|
||||||
|
setOpened(false)
|
||||||
|
setFormData({
|
||||||
|
current_password: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Text fw={700} fz={'1.2rem'}>
|
||||||
|
Change password
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box p="sm">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleChangePassword()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="E-mail"
|
||||||
|
value={user!.email}
|
||||||
|
disabled
|
||||||
|
mb="md"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label="Current password"
|
||||||
|
required
|
||||||
|
placeholder="Current password"
|
||||||
|
maxLength={32}
|
||||||
|
value={formData.current_password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
current_password: e.target.value,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
mb="md"
|
||||||
|
error={
|
||||||
|
formData.current_password.length < 8 &&
|
||||||
|
formData.current_password !== '' &&
|
||||||
|
'Length 8 characters or more'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordRequirementInput
|
||||||
|
requirements={requirementsPassword}
|
||||||
|
value={formData}
|
||||||
|
setValue={setFormData}
|
||||||
|
label="New password"
|
||||||
|
placeholder="New password"
|
||||||
|
name="password"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label="Confirm password"
|
||||||
|
required
|
||||||
|
placeholder="Confirm password"
|
||||||
|
maxLength={32}
|
||||||
|
value={formData.confirm_password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
confirm_password: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
error={
|
||||||
|
formData.password !== formData.confirm_password &&
|
||||||
|
formData.confirm_password !== '' &&
|
||||||
|
'Password do not match'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
style={{ float: 'right' }}
|
||||||
|
mb={'lg'}
|
||||||
|
mt={'lg'}
|
||||||
|
loading={loading}
|
||||||
|
disabled={!isFormValid}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangePassword
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { useGoogleLogin } from '@react-oauth/google'
|
||||||
|
import { emailRegex } from '@/utils/formRegexs'
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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>({
|
||||||
|
initialValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
validate: (values) => ({
|
||||||
|
email:
|
||||||
|
values.email === ''
|
||||||
|
? 'Email is required'
|
||||||
|
: isLoginERP
|
||||||
|
? null
|
||||||
|
: emailRegex.test(values.email)
|
||||||
|
? null
|
||||||
|
: 'Invalid email',
|
||||||
|
|
||||||
|
password: values.password === '' ? 'Password is required' : null,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLogin = async (values: TLogin) => {
|
||||||
|
if (isLoginERP) {
|
||||||
|
const payload = {
|
||||||
|
userEmail: values.email,
|
||||||
|
password: values.password,
|
||||||
|
}
|
||||||
|
const resultAction = await dispatch(loginERPAsync(payload))
|
||||||
|
|
||||||
|
if (loginERPAsync.fulfilled.match(resultAction)) {
|
||||||
|
// set interval to wait for localStorage to be set
|
||||||
|
window.location.href = '/dashboard'
|
||||||
|
}
|
||||||
|
} 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 (
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
}}
|
||||||
|
onSubmit={formLogin.onSubmit(handleLogin)}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<h3 className={classes.title}>
|
||||||
|
{isLoginERP ? 'Login with ERP account' : 'Login with ATC account'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<TextInput
|
||||||
|
label={isLoginERP ? 'Username/email:' : 'Email address'}
|
||||||
|
placeholder="hello@gmail.com"
|
||||||
|
value={formLogin.values.email}
|
||||||
|
error={formLogin.errors.email}
|
||||||
|
onChange={(e) => {
|
||||||
|
formLogin.setFieldValue('email', e.target.value!)
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password"
|
||||||
|
value={formLogin.values.password}
|
||||||
|
error={formLogin.errors.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
formLogin.setFieldValue('password', e.target.value!)
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
mt="md"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
mt="xl"
|
||||||
|
size="md"
|
||||||
|
type="submit"
|
||||||
|
loading={status === 'loading'}
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!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
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { AppDispatch, RootState } from '@/rtk/store'
|
||||||
|
import { registerAsync } from '@/rtk/slices/authSlice'
|
||||||
|
|
||||||
|
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||||
|
import { emailRegex, passwordRegex } from '@/utils/formRegexs'
|
||||||
|
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||||
|
|
||||||
|
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
|
||||||
|
|
||||||
|
type TRegister = {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
confirm_password: string
|
||||||
|
full_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function Register() {
|
||||||
|
const dispatch = useDispatch<AppDispatch>()
|
||||||
|
const { status } = useSelector((state: RootState) => state.auth)
|
||||||
|
|
||||||
|
const [formRegister, setFormRegister] = useState<TRegister>({
|
||||||
|
email: '',
|
||||||
|
full_name: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
// Dispatch action registerAsync với dữ liệu form và đợi kết quả
|
||||||
|
const resultAction = await dispatch(registerAsync(formRegister))
|
||||||
|
|
||||||
|
// Kiểm tra nếu action thành công
|
||||||
|
if (registerAsync.fulfilled.match(resultAction)) {
|
||||||
|
// Tải lại trang web
|
||||||
|
// window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleRegister()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Email address"
|
||||||
|
placeholder="hello@gmail.com"
|
||||||
|
value={formRegister.email}
|
||||||
|
error={
|
||||||
|
emailRegex.test(formRegister.email) || formRegister.email === ''
|
||||||
|
? null
|
||||||
|
: 'Invalid email'
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormRegister({ ...formRegister, email: e.target.value })
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
mb="md"
|
||||||
|
label="Full name"
|
||||||
|
placeholder="Bill Gates"
|
||||||
|
value={formRegister.full_name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormRegister({ ...formRegister, full_name: e.target.value })
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordRequirementInput
|
||||||
|
requirements={requirementsPassword}
|
||||||
|
value={formRegister}
|
||||||
|
setValue={setFormRegister}
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
mt="md"
|
||||||
|
label="Confirm password"
|
||||||
|
placeholder="Your password"
|
||||||
|
value={formRegister.confirm_password}
|
||||||
|
error={
|
||||||
|
formRegister.confirm_password === formRegister.password ||
|
||||||
|
formRegister.confirm_password === ''
|
||||||
|
? null
|
||||||
|
: 'Password do not match'
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormRegister({ ...formRegister, confirm_password: e.target.value })
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box ta={'center'}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
m="15px auto"
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
loading={status === 'loading'}
|
||||||
|
disabled={
|
||||||
|
formRegister.password !== '' &&
|
||||||
|
passwordRegex.test(formRegister.password) &&
|
||||||
|
formRegister.password === formRegister.confirm_password
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
.resetForm {
|
||||||
|
background-color: light-dark(#ffffffdb, var(--mantine-color-dark-5));
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import { Card, Text, Box, Flex } from "@mantine/core";
|
import { Card, Text, Box, Flex } from "@mantine/core";
|
||||||
import type { TLine } from "../untils/types";
|
import type { TLine, TStation } from "../untils/types";
|
||||||
import classes from "./Component.module.css";
|
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";
|
||||||
|
|
||||||
const CardLine = ({
|
const CardLine = ({
|
||||||
line,
|
line,
|
||||||
selectedLines,
|
selectedLines,
|
||||||
setSelectedLines,
|
setSelectedLines,
|
||||||
|
socket,
|
||||||
|
stationItem,
|
||||||
}: {
|
}: {
|
||||||
line: TLine;
|
line: TLine;
|
||||||
selectedLines: TLine[];
|
selectedLines: TLine[];
|
||||||
setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
|
setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
|
||||||
|
socket: Socket | null;
|
||||||
|
stationItem: TStation;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|
@ -31,24 +39,50 @@ const CardLine = ({
|
||||||
else setSelectedLines((pre) => [...pre, line]);
|
else setSelectedLines((pre) => [...pre, line]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex justify={"space-between"}>
|
<Flex
|
||||||
<Box>
|
justify={"space-between"}
|
||||||
<div>
|
direction={"column"}
|
||||||
<Text fw={600}>
|
// gap={"md"}
|
||||||
Line {line.lineNumber} - {line.port}
|
align={"center"}
|
||||||
</Text>
|
>
|
||||||
</div>
|
<div>
|
||||||
<Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
|
<Text fw={600} style={{ display: "flex", gap: "4px" }}>
|
||||||
|
Line {line.lineNumber} - {line.port}{" "}
|
||||||
|
{line.status === "connected" && (
|
||||||
|
<IconCircleCheckFilled color="green" />
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{/* <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> */}
|
||||||
</Box>
|
|
||||||
<Box
|
<Box
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
style={{ backgroundColor: "black", height: "130px", width: "220px" }}
|
style={{ height: "175px", width: "300px" }}
|
||||||
></Box>
|
>
|
||||||
|
<TerminalCLI
|
||||||
|
cliOpened={true}
|
||||||
|
socket={socket}
|
||||||
|
content={line.netOutput ?? ""}
|
||||||
|
line_id={Number(line?.id)}
|
||||||
|
station_id={Number(stationItem.id)}
|
||||||
|
isDisabled={false}
|
||||||
|
line_status={line?.status || ""}
|
||||||
|
fontSize={11}
|
||||||
|
miniSize={true}
|
||||||
|
customStyle={{
|
||||||
|
maxHeight: "175px",
|
||||||
|
height: "175px",
|
||||||
|
fontSize: "7px",
|
||||||
|
padding: "0px",
|
||||||
|
paddingBottom: "0px",
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* <Flex justify={"flex-end"}>
|
{/* <Flex justify={"flex-end"}>
|
||||||
<Button variant="filled" style={{ height: "30px", width: "70px" }}>
|
<Button variant="filled" style={{ height: "30px", width: "70px" }}>
|
||||||
|
|
@ -59,4 +93,4 @@ const CardLine = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CardLine;
|
export default memo(CardLine);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.card_line {
|
.card_line {
|
||||||
width: 400px;
|
width: 320px;
|
||||||
height: 150px;
|
height: 220px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Terminal } from "xterm";
|
||||||
|
import "xterm/css/xterm.css";
|
||||||
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { SOCKET_EVENTS } from "../untils/constanst";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
|
interface TerminalCLIProps {
|
||||||
|
socket: Socket | null;
|
||||||
|
content?: string;
|
||||||
|
line_id: number;
|
||||||
|
line_status: string;
|
||||||
|
station_id: number;
|
||||||
|
cliOpened: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
customStyle?: {
|
||||||
|
fontSize?: string;
|
||||||
|
maxHeight?: string;
|
||||||
|
height?: string;
|
||||||
|
padding?: string;
|
||||||
|
paddingBottom?: string;
|
||||||
|
paddingLeft?: string;
|
||||||
|
};
|
||||||
|
onDoubleClick?: () => void;
|
||||||
|
fontSize?: number;
|
||||||
|
miniSize?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
|
socket,
|
||||||
|
content = "",
|
||||||
|
line_id,
|
||||||
|
station_id,
|
||||||
|
cliOpened = false,
|
||||||
|
isDisabled = false,
|
||||||
|
line_status = "",
|
||||||
|
customStyle = {},
|
||||||
|
onDoubleClick = () => {},
|
||||||
|
fontSize = 14,
|
||||||
|
miniSize = false,
|
||||||
|
}) => {
|
||||||
|
const xtermRef = useRef<HTMLDivElement>(null);
|
||||||
|
const terminal = useRef<Terminal>(null);
|
||||||
|
const fitRef = useRef<FitAddon>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cliOpened || fitRef.current) return;
|
||||||
|
|
||||||
|
terminal.current = new Terminal({
|
||||||
|
disableStdin: isDisabled,
|
||||||
|
cursorBlink: true,
|
||||||
|
convertEol: true,
|
||||||
|
fontFamily: "Consolas, 'Courier New', monospace",
|
||||||
|
fontSize: fontSize,
|
||||||
|
theme: {
|
||||||
|
background: "#000000",
|
||||||
|
foreground: "#41ee4a",
|
||||||
|
cursor: "#ffffff",
|
||||||
|
},
|
||||||
|
// rows: 24,
|
||||||
|
// cols: 80,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
fitRef.current = fitAddon;
|
||||||
|
if (!miniSize) terminal.current.focus();
|
||||||
|
terminal.current.loadAddon(fitAddon);
|
||||||
|
terminal.current.open(xtermRef.current!);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Gửi input từ người dùng lên server
|
||||||
|
terminal.current.onData((data) => {
|
||||||
|
socket?.emit(SOCKET_EVENTS.CLI.WRITE_COMMAND_FROM_WEB, {
|
||||||
|
lineIds: [line_id],
|
||||||
|
stationId: station_id,
|
||||||
|
command: data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal.current.onSelectionChange(() => {
|
||||||
|
const selectedText = terminal?.current?.getSelection();
|
||||||
|
if (selectedText) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(selectedText)
|
||||||
|
.then(() => console.log("Copied to clipboard"))
|
||||||
|
.catch((err) => console.error("Clipboard copy failed", err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal.current.attachCustomKeyEventHandler(
|
||||||
|
(e: KeyboardEvent): boolean => {
|
||||||
|
// Handle Ctrl+V (Paste)
|
||||||
|
if (e.ctrlKey && e.key.toLowerCase() === "v") return false;
|
||||||
|
// Handle Esc
|
||||||
|
if (e.key === "Escape") return false;
|
||||||
|
|
||||||
|
return true; // allow all other keys through
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContextMenu = async (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
terminal?.current?.paste(text);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Clipboard read failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (xtermRef.current)
|
||||||
|
xtermRef.current?.addEventListener("contextmenu", handleContextMenu);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
xtermRef?.current?.removeEventListener("contextmenu", handleContextMenu);
|
||||||
|
};
|
||||||
|
}, [cliOpened]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cliOpened) {
|
||||||
|
if (terminal.current)
|
||||||
|
setTimeout(() => terminal.current?.write(content), 200);
|
||||||
|
|
||||||
|
if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
|
||||||
|
}
|
||||||
|
}, [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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket?.off("init");
|
||||||
|
socket?.off("line_error");
|
||||||
|
socket?.off("line_output");
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cliOpened) {
|
||||||
|
setTimeout(() => {
|
||||||
|
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(() => {
|
||||||
|
if (!loading) {
|
||||||
|
if (terminal.current) {
|
||||||
|
terminal.current?.write(content);
|
||||||
|
if (!miniSize && !isDisabled) terminal.current?.focus();
|
||||||
|
terminal.current.scrollToBottom();
|
||||||
|
}
|
||||||
|
if (fitRef.current) fitRef.current?.fit();
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (terminal.current) {
|
||||||
|
terminal.current.options.disableStdin = isDisabled;
|
||||||
|
}
|
||||||
|
}, [isDisabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setLoading(true);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "black",
|
||||||
|
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||||
|
minHeight: customStyle.maxHeight ?? "60vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={xtermRef}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
paddingLeft: customStyle.paddingLeft ?? "10px",
|
||||||
|
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||||
|
fontSize: customStyle.fontSize ?? "9px",
|
||||||
|
maxHeight: customStyle.maxHeight ?? "60vh",
|
||||||
|
height: customStyle.height ?? "60vh",
|
||||||
|
padding: customStyle.padding ?? "4px",
|
||||||
|
}}
|
||||||
|
onDoubleClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onDoubleClick();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TerminalCLI;
|
||||||
Loading…
Reference in New Issue