Compare commits
2 Commits
dea4d2b804
...
cbc8397ea8
| Author | SHA1 | Date |
|---|---|---|
|
|
cbc8397ea8 | |
|
|
0a0dd559f0 |
|
|
@ -23,3 +23,5 @@ yarn-error.log
|
||||||
|
|
||||||
# Platform specific
|
# Platform specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
storage/system_logs
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,20 @@ 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) {
|
||||||
const data = request.only(['email', 'password', 'full_name'])
|
try {
|
||||||
const user = await User.create(data)
|
const data = request.only(['email', 'password', 'full_name'])
|
||||||
return response.json({ message: 'User created', user })
|
|
||||||
|
const user = await User.query().where('email', data.email).first()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -24,11 +35,9 @@ 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, token },
|
user: { id: user.id, email: user.email, fullName: user.fullName },
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return response.status(401).json({ message: 'Invalid credentials' })
|
return response.status(401).json({ message: 'Invalid credentials' })
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
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 {
|
||||||
/**
|
/**
|
||||||
|
|
@ -36,7 +34,6 @@ 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(
|
||||||
|
|
@ -44,7 +41,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.is_reboot,
|
isReboot: payload.isReboot,
|
||||||
},
|
},
|
||||||
{ client: trx }
|
{ client: trx }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import net from 'node:net'
|
import net from 'node:net'
|
||||||
import { cleanData, sleep } from '../ultils/helper.js'
|
import { appendLog, cleanData, sleep } from '../ultils/helper.js'
|
||||||
import Scenario from '#models/scenario'
|
import Scenario from '#models/scenario'
|
||||||
|
|
||||||
interface LineConfig {
|
interface LineConfig {
|
||||||
|
|
@ -11,6 +11,14 @@ 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 {
|
||||||
|
|
@ -77,6 +85,7 @@ 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) => {
|
||||||
|
|
@ -149,6 +158,11 @@ 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
|
||||||
|
|
||||||
|
|
@ -156,6 +170,13 @@ 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)
|
||||||
|
|
||||||
|
|
@ -164,11 +185,21 @@ 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) {
|
||||||
|
|
@ -203,4 +234,27 @@ 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: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
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.
|
||||||
|
|
@ -16,3 +19,20 @@ 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('isReboot').defaultTo(false)
|
table.boolean('is_reboot').defaultTo(false)
|
||||||
table.timestamps()
|
table.timestamps()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ 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) {}
|
||||||
|
|
||||||
|
|
@ -70,8 +71,14 @@ 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(
|
||||||
|
|
@ -82,6 +89,10 @@ 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
|
||||||
|
|
@ -94,8 +105,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) {
|
if (line && line.config.status === 'connected') {
|
||||||
this.lineConnecting.filter((el) => el !== lineId)
|
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
|
||||||
this.setTimeoutConnect(lineId, line)
|
this.setTimeoutConnect(lineId, line)
|
||||||
line.writeCommand(command)
|
line.writeCommand(command)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -107,7 +118,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.filter((el) => el !== lineId)
|
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
|
||||||
this.setTimeoutConnect(lineId, lineReconnect)
|
this.setTimeoutConnect(lineId, lineReconnect)
|
||||||
lineReconnect.writeCommand(command)
|
lineReconnect.writeCommand(command)
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +138,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) {
|
if (line && line.config.status === 'connected') {
|
||||||
this.setTimeoutConnect(
|
this.setTimeoutConnect(
|
||||||
lineId,
|
lineId,
|
||||||
line,
|
line,
|
||||||
|
|
@ -155,9 +166,44 @@ export class WebSocketIo {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// FE yêu cầu ngắt kết nối 1 station
|
socket.on('open_cli', async (data) => {
|
||||||
socket.on('disconnect_station', (stationId) => {
|
const { lineId, userEmail, userName: name, stationId } = data
|
||||||
this.disconnectStation(stationId)
|
const line = this.lineMap.get(lineId)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -182,6 +228,9 @@ export class WebSocketIo {
|
||||||
apcName: line.apcName,
|
apcName: line.apcName,
|
||||||
output: '',
|
output: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
openCLI: false,
|
||||||
|
userEmailOpenCLI: '',
|
||||||
|
userOpenCLI: '',
|
||||||
},
|
},
|
||||||
socket
|
socket
|
||||||
)
|
)
|
||||||
|
|
@ -195,22 +244,6 @@ 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}`])
|
||||||
|
|
@ -218,7 +251,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}`]
|
||||||
|
|
|
||||||
|
|
@ -67,5 +67,6 @@ 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')
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
|
@ -2216,6 +2217,15 @@
|
||||||
"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",
|
||||||
|
|
@ -3595,6 +3605,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
|
@ -3753,6 +3801,12 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"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.
|
After Width: | Height: | Size: 104 KiB |
|
|
@ -3,7 +3,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Mulish', sans-serif;
|
font-family: "Mulish", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
|
@ -48,7 +48,25 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, useState } from "react";
|
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -17,8 +17,16 @@ import {
|
||||||
Button,
|
Button,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
|
Avatar,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import type { IScenario, LineConfig, TLine, TStation } from "./untils/types";
|
import type {
|
||||||
|
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";
|
||||||
|
|
@ -27,6 +35,8 @@ 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;
|
||||||
|
|
||||||
|
|
@ -34,6 +44,14 @@ 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[]>([]);
|
||||||
|
|
@ -53,6 +71,10 @@ 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 () => {
|
||||||
|
|
@ -93,24 +115,74 @@ 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) {
|
if (line?.id) {
|
||||||
updateValueLineStation(line, "status", data.status);
|
updateValueLineStation(line.id, "status", data.status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateValueLineStation = <K extends keyof TLine>(
|
const updateValueLineStation = <K extends keyof TLine>(
|
||||||
currentLine: TLine,
|
lineId: number,
|
||||||
field: K,
|
field: K,
|
||||||
value: TLine[K]
|
value: TLine[K]
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -120,10 +192,20 @@ function App() {
|
||||||
? {
|
? {
|
||||||
...station,
|
...station,
|
||||||
lines: (station?.lines || [])?.map((lineItem: TLine) => {
|
lines: (station?.lines || [])?.map((lineItem: TLine) => {
|
||||||
if (lineItem.id === currentLine.id) {
|
if (lineItem.id === lineId) {
|
||||||
return {
|
return {
|
||||||
...lineItem,
|
...lineItem,
|
||||||
[field]: value,
|
[field]:
|
||||||
|
field === "netOutput"
|
||||||
|
? (lineItem.netOutput || "") + value
|
||||||
|
: value,
|
||||||
|
output: field === "netOutput" ? value : lineItem.output,
|
||||||
|
loadingOutput:
|
||||||
|
field === "netOutput"
|
||||||
|
? lineItem.loadingOutput
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return lineItem;
|
return lineItem;
|
||||||
|
|
@ -132,6 +214,24 @@ 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) => {
|
||||||
|
|
@ -142,63 +242,106 @@ 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="xl" w={"100%"} style={{ maxWidth: "100%" }}>
|
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={(id) => setActiveTab(id?.toString() || "0")}
|
onChange={(id) => {
|
||||||
|
setActiveTab(id?.toString() || "0");
|
||||||
|
setLoadingTerminal(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoadingTerminal(true);
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
variant="none"
|
variant="none"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<Tabs.List ref={setRootRef} className={classes.list}>
|
<Flex justify={"space-between"}>
|
||||||
{stations.map((station) => (
|
<Flex wrap={"wrap"} style={{ maxWidth: "250px" }} gap={"4px"}>
|
||||||
<Tabs.Tab
|
{usersConnecting.map((el) => (
|
||||||
ref={setControlRef(station.id.toString())}
|
<Tooltip label={el.userName} key={el.userId}>
|
||||||
className={classes.tab}
|
<Avatar color="cyan" radius="xl" size={"md"}>
|
||||||
key={station.id}
|
{el.userName.slice(0, 2)}
|
||||||
value={station.id.toString()}
|
</Avatar>
|
||||||
>
|
</Tooltip>
|
||||||
{station.name}
|
))}
|
||||||
</Tabs.Tab>
|
</Flex>
|
||||||
))}
|
<Tabs.List ref={setRootRef} className={classes.list}>
|
||||||
|
{stations.map((station) => (
|
||||||
|
<Tabs.Tab
|
||||||
|
ref={setControlRef(station.id.toString())}
|
||||||
|
className={classes.tab}
|
||||||
|
key={station.id}
|
||||||
|
value={station.id.toString()}
|
||||||
|
>
|
||||||
|
{station.name}
|
||||||
|
</Tabs.Tab>
|
||||||
|
))}
|
||||||
|
|
||||||
<FloatingIndicator
|
<FloatingIndicator
|
||||||
target={activeTab ? controlsRefs[activeTab] : null}
|
target={activeTab ? controlsRefs[activeTab] : null}
|
||||||
parent={rootRef}
|
parent={rootRef}
|
||||||
className={classes.indicator}
|
className={classes.indicator}
|
||||||
/>
|
/>
|
||||||
<Flex gap={"sm"}>
|
<Flex gap={"sm"}>
|
||||||
{Number(activeTab) ? (
|
{Number(activeTab) ? (
|
||||||
|
<ActionIcon
|
||||||
|
title="Edit Station"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setStationEdit(
|
||||||
|
stations.find((el) => el.id === Number(activeTab))
|
||||||
|
);
|
||||||
|
setIsOpenAddStation(true);
|
||||||
|
setIsEditStation(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
title="Edit Station"
|
title="Add Station"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
color="green"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStationEdit(
|
|
||||||
stations.find((el) => el.id === Number(activeTab))
|
|
||||||
);
|
|
||||||
setIsOpenAddStation(true);
|
setIsOpenAddStation(true);
|
||||||
setIsEditStation(true);
|
setIsEditStation(false);
|
||||||
|
setStationEdit(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconEdit />
|
<IconSettingsPlus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
) : (
|
</Flex>
|
||||||
""
|
</Tabs.List>
|
||||||
)}
|
<Flex gap={"sm"} align={"baseline"}>
|
||||||
<ActionIcon
|
<Text className={classes.userName}>{user?.fullName}</Text>
|
||||||
title="Add Station"
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="green"
|
color="red"
|
||||||
|
style={{ height: "30px", width: "100px" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpenAddStation(true);
|
localStorage.removeItem("user");
|
||||||
setIsEditStation(false);
|
window.location.href = "/";
|
||||||
setStationEdit(undefined);
|
socket?.disconnect();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconSettingsPlus />
|
Logout
|
||||||
</ActionIcon>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tabs.List>
|
</Flex>
|
||||||
|
|
||||||
{stations.map((station) => (
|
{stations.map((station) => (
|
||||||
<Tabs.Panel
|
<Tabs.Panel
|
||||||
|
|
@ -238,7 +381,11 @@ function App() {
|
||||||
line={line}
|
line={line}
|
||||||
selectedLines={selectedLines}
|
selectedLines={selectedLines}
|
||||||
setSelectedLines={setSelectedLines}
|
setSelectedLines={setSelectedLines}
|
||||||
updateStatus={updateStatus}
|
openTerminal={openTerminal}
|
||||||
|
loadTerminal={
|
||||||
|
loadingTerminal &&
|
||||||
|
Number(station.id) === Number(activeTab)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
@ -309,8 +456,9 @@ function App() {
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{scenarios.map((el) => (
|
{scenarios.map((el, i) => (
|
||||||
<ButtonScenario
|
<ButtonScenario
|
||||||
|
key={i}
|
||||||
socket={socket}
|
socket={socket}
|
||||||
selectedLines={selectedLines}
|
selectedLines={selectedLines}
|
||||||
isDisable={isDisable || selectedLines.length === 0}
|
isDisable={isDisable || selectedLines.length === 0}
|
||||||
|
|
@ -345,11 +493,24 @@ 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>
|
||||||
|
|
@ -363,7 +524,13 @@ export default function Main() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Notifications position="top-right" autoClose={5000} />
|
<Notifications position="top-right" autoClose={5000} />
|
||||||
<App />
|
{user ? (
|
||||||
|
<App />
|
||||||
|
) : (
|
||||||
|
<Container w={"100%"} style={{ maxWidth: "100%", padding: 0 }}>
|
||||||
|
<PageLogin />
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,22 @@
|
||||||
.wrapper {
|
.wrapper {
|
||||||
min-height: rem(100vh);
|
height: 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;
|
||||||
.form {
|
align-items: center;
|
||||||
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 {
|
.form {
|
||||||
content: "";
|
border-right: rem(1px) solid
|
||||||
display: block;
|
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
|
||||||
width: 200px;
|
max-width: rem(450px);
|
||||||
height: 2px;
|
padding-top: rem(80px);
|
||||||
background-color: #007bff; /* blue accent */
|
|
||||||
margin: 0.1rem auto 0;
|
font-weight: 600;
|
||||||
border-radius: 3px;
|
}
|
||||||
}
|
|
||||||
|
.title {
|
||||||
|
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||||
|
font-family: Greycliff CF, var(--mantine-font-family);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,105 +1,80 @@
|
||||||
import { useGoogleLogin } from '@react-oauth/google'
|
import { Button, PasswordInput, TextInput } from "@mantine/core";
|
||||||
import { emailRegex } from '@/utils/formRegexs'
|
import { useForm } from "@mantine/form";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
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:
|
email: values.email === "" ? "Email is required" : null,
|
||||||
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 (values: TLogin) => {
|
const handleLogin = async () => {
|
||||||
if (isLoginERP) {
|
try {
|
||||||
|
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 = {
|
||||||
userEmail: values.email,
|
email: formLogin.values.email,
|
||||||
password: values.password,
|
password: formLogin.values.password,
|
||||||
}
|
};
|
||||||
const resultAction = await dispatch(loginERPAsync(payload))
|
const response = await axios.post(apiUrl + "api/auth/login", payload);
|
||||||
|
if (response.data.user) {
|
||||||
if (loginERPAsync.fulfilled.match(resultAction)) {
|
const user = response.data.user;
|
||||||
// set interval to wait for localStorage to be set
|
localStorage.setItem("user", JSON.stringify(user));
|
||||||
window.location.href = '/dashboard'
|
window.location.href = "/";
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const resultAction = await dispatch(loginAsync(values))
|
|
||||||
|
|
||||||
if (loginAsync.fulfilled.match(resultAction)) {
|
|
||||||
navigate('/dashboard')
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
notifications.show({
|
||||||
|
title: "Error",
|
||||||
|
message: "Login fail, please try again!",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
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={isLoginERP ? 'Username/email:' : 'Email address'}
|
label={"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"
|
||||||
|
|
@ -110,7 +85,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"
|
||||||
|
|
@ -122,60 +97,12 @@ 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>
|
||||||
|
|
||||||
{!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>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Login
|
export default Login;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
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't have an account?{" "}
|
||||||
|
<Anchor<"a">
|
||||||
|
href="#"
|
||||||
|
fw={700}
|
||||||
|
onClick={() => setIsRegister(true)}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageLogin;
|
||||||
|
|
@ -1,48 +1,77 @@
|
||||||
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 PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
import { Box, Button, PasswordInput, TextInput } from "@mantine/core";
|
||||||
import { emailRegex, passwordRegex } from '@/utils/formRegexs'
|
import { emailRegex } from "../../untils/helper";
|
||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import axios from "axios";
|
||||||
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
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 () => {
|
||||||
// Dispatch action registerAsync với dữ liệu form và đợi kết quả
|
try {
|
||||||
const resultAction = await dispatch(registerAsync(formRegister))
|
if (!formRegister.email) {
|
||||||
|
notifications.show({
|
||||||
// Kiểm tra nếu action thành công
|
title: "Error",
|
||||||
if (registerAsync.fulfilled.match(resultAction)) {
|
message: "Email is required",
|
||||||
// Tải lại trang web
|
color: "red",
|
||||||
// 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
|
||||||
|
|
@ -50,12 +79,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"
|
||||||
|
|
@ -68,19 +97,22 @@ 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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordRequirementInput
|
<PasswordInput
|
||||||
requirements={requirementsPassword}
|
mt="md"
|
||||||
value={formRegister}
|
|
||||||
setValue={setFormRegister}
|
|
||||||
label="Password"
|
label="Password"
|
||||||
placeholder="Password"
|
placeholder="Your password"
|
||||||
name="password"
|
value={formRegister.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFormRegister({ ...formRegister, password: e.target.value });
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
|
|
@ -90,27 +122,29 @@ 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({ ...formRegister, confirm_password: e.target.value })
|
setFormRegister({
|
||||||
|
...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
|
||||||
|
|
@ -120,7 +154,7 @@ function Register() {
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Register
|
export default Register;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Card, Text, Box, Flex } from "@mantine/core";
|
import { Card, Text, Box, Flex } from "@mantine/core";
|
||||||
import type { LineConfig, TLine, TStation } 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 TerminalCLI from "./TerminalXTerm";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
@ -12,14 +12,16 @@ const CardLine = ({
|
||||||
setSelectedLines,
|
setSelectedLines,
|
||||||
socket,
|
socket,
|
||||||
stationItem,
|
stationItem,
|
||||||
updateStatus,
|
openTerminal,
|
||||||
|
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;
|
||||||
updateStatus: (value: LineConfig) => void;
|
openTerminal: (value: TLine) => void;
|
||||||
|
loadTerminal: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|
@ -33,6 +35,11 @@ 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();
|
||||||
|
|
@ -45,16 +52,27 @@ const CardLine = ({
|
||||||
justify={"space-between"}
|
justify={"space-between"}
|
||||||
direction={"column"}
|
direction={"column"}
|
||||||
// gap={"md"}
|
// gap={"md"}
|
||||||
align={"center"}
|
// align={"center"}
|
||||||
>
|
>
|
||||||
<div>
|
<Flex justify={"space-between"}>
|
||||||
<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>
|
<div
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
marginLeft: "16px",
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "red",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""}
|
||||||
|
</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> */}
|
||||||
|
|
@ -66,9 +84,11 @@ const CardLine = ({
|
||||||
style={{ height: "175px", width: "300px" }}
|
style={{ height: "175px", width: "300px" }}
|
||||||
>
|
>
|
||||||
<TerminalCLI
|
<TerminalCLI
|
||||||
cliOpened={true}
|
cliOpened={loadTerminal}
|
||||||
socket={socket}
|
socket={socket}
|
||||||
content={line.netOutput ?? ""}
|
content={line?.output ?? ""}
|
||||||
|
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}
|
||||||
|
|
@ -82,8 +102,9 @@ const CardLine = ({
|
||||||
padding: "0px",
|
padding: "0px",
|
||||||
paddingBottom: "0px",
|
paddingBottom: "0px",
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => {}}
|
onDoubleClick={() => {
|
||||||
updateStatus={updateStatus}
|
openTerminal(line);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ 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";
|
||||||
|
|
@ -42,11 +43,10 @@ function DrawerScenario({
|
||||||
send: "",
|
send: "",
|
||||||
delay: "0",
|
delay: "0",
|
||||||
repeat: "1",
|
repeat: "1",
|
||||||
note: "",
|
|
||||||
},
|
},
|
||||||
] as IBodyScenario[],
|
] as IBodyScenario[],
|
||||||
timeout: "30000",
|
timeout: "30000",
|
||||||
is_reboot: false,
|
isReboot: false,
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
title: (value) => {
|
title: (value) => {
|
||||||
|
|
@ -72,7 +72,6 @@ function DrawerScenario({
|
||||||
send: "",
|
send: "",
|
||||||
delay: "0",
|
delay: "0",
|
||||||
repeat: "1",
|
repeat: "1",
|
||||||
note: "",
|
|
||||||
});
|
});
|
||||||
form.setFieldValue("body", newBody);
|
form.setFieldValue("body", newBody);
|
||||||
};
|
};
|
||||||
|
|
@ -84,6 +83,22 @@ 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) => ({
|
||||||
|
|
@ -93,7 +108,8 @@ function DrawerScenario({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...form.values,
|
title: form.values.title,
|
||||||
|
isReboot: form.values.isReboot,
|
||||||
body: body,
|
body: body,
|
||||||
timeout: Number(form.values.timeout),
|
timeout: Number(form.values.timeout),
|
||||||
};
|
};
|
||||||
|
|
@ -111,15 +127,28 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -196,7 +225,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("is_reboot", scenario.is_reboot);
|
form.setFieldValue("isReboot", scenario.isReboot);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -231,7 +260,27 @@ function DrawerScenario({
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={6}>
|
<Grid.Col
|
||||||
|
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",
|
||||||
|
|
@ -329,7 +378,7 @@ function DrawerScenario({
|
||||||
close={() => {
|
close={() => {
|
||||||
setOpenConfirm(false);
|
setOpenConfirm(false);
|
||||||
}}
|
}}
|
||||||
message={"Are you sure delete this station?"}
|
message={"Are you sure delete this scenario?"}
|
||||||
handle={() => {
|
handle={() => {
|
||||||
setOpenConfirm(false);
|
setOpenConfirm(false);
|
||||||
handleDelete();
|
handleDelete();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
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;
|
||||||
|
|
@ -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;
|
||||||
updateStatus: (value: LineConfig) => void;
|
loadingContent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
|
|
@ -39,7 +39,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
onDoubleClick = () => {},
|
onDoubleClick = () => {},
|
||||||
fontSize = 14,
|
fontSize = 14,
|
||||||
miniSize = false,
|
miniSize = false,
|
||||||
updateStatus,
|
initContent = "",
|
||||||
|
loadingContent = false,
|
||||||
}) => {
|
}) => {
|
||||||
const xtermRef = useRef<HTMLDivElement>(null);
|
const xtermRef = useRef<HTMLDivElement>(null);
|
||||||
const terminal = useRef<Terminal>(null);
|
const terminal = useRef<Terminal>(null);
|
||||||
|
|
@ -128,40 +129,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
|
|
||||||
if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
|
if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
|
||||||
}
|
}
|
||||||
}, [content]);
|
}, [content, loadingContent]);
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -182,7 +150,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
if (terminal.current) {
|
if (terminal.current) {
|
||||||
terminal.current?.write(content);
|
terminal.current?.write(initContent);
|
||||||
if (!miniSize && !isDisabled) terminal.current?.focus();
|
if (!miniSize && !isDisabled) terminal.current?.focus();
|
||||||
terminal.current.scrollToBottom();
|
terminal.current.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +179,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 ?? "60vh",
|
minHeight: customStyle.maxHeight ?? "75vh",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -221,8 +189,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 ?? "60vh",
|
maxHeight: customStyle.maxHeight ?? "75vh",
|
||||||
height: customStyle.height ?? "60vh",
|
height: customStyle.height ?? "75vh",
|
||||||
padding: customStyle.padding ?? "4px",
|
padding: customStyle.padding ?? "4px",
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import 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";
|
||||||
|
|
@ -15,9 +21,21 @@ 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(() => {
|
||||||
const newSocket = io(SOCKET_URL);
|
if (!user) return;
|
||||||
|
const newSocket = io(SOCKET_URL, {
|
||||||
|
auth: {
|
||||||
|
userId: user?.id,
|
||||||
|
userName: user?.fullName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
|
|
||||||
|
|
@ -46,7 +64,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
|
||||||
newSocket.disconnect();
|
newSocket.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={{ socket }}>
|
<SocketContext.Provider value={{ socket }}>
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,8 @@ 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}$/;
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ 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;
|
||||||
|
|
@ -84,14 +86,8 @@ export type TLine = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUser = {
|
export type TUser = {
|
||||||
id: number;
|
userId: number;
|
||||||
email: string;
|
userName: string;
|
||||||
email_cc: string;
|
|
||||||
full_name: string;
|
|
||||||
package_id: string;
|
|
||||||
zulip: string;
|
|
||||||
token?: string;
|
|
||||||
name: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type APCProps = {
|
export type APCProps = {
|
||||||
|
|
@ -144,7 +140,7 @@ export type IScenario = {
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
is_reboot: boolean;
|
isReboot: boolean;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -153,5 +149,4 @@ export type IBodyScenario = {
|
||||||
send: string;
|
send: string;
|
||||||
delay: string;
|
delay: string;
|
||||||
repeat: string;
|
repeat: string;
|
||||||
note: string;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue