This commit is contained in:
nguyentrungthat 2025-11-03 16:33:57 +07:00
parent 01485bf1d9
commit 3b55644bc1
10 changed files with 1026 additions and 126 deletions

View File

@ -0,0 +1,267 @@
import net, { Socket } from 'node:net'
interface APCOptions {
host: string
port?: number
username: string
password: string
onData?: (data: string) => void
number?: number
keep_connect?: boolean
}
interface PromptCallback {
prompt: string
callback: (data: string) => void
}
class APCController {
private apc_number?: number
private apc_ip: string
private apc_port: number
private apc_username: string
private apc_password: string
private status: 'CONNECTED' | 'DISCONNECTED' | 'TIMEOUT'
private socket: Socket
private buffer: string
private output: string
private promptCallbacks: PromptCallback[]
private onData: (data: string) => void
private retryConnect: number
constructor({ host, port = 23, username, password, onData, number }: APCOptions) {
this.apc_number = number
this.apc_ip = host
this.apc_port = port
this.apc_username = username
this.apc_password = password
this.status = 'DISCONNECTED'
this.socket = new net.Socket()
this.buffer = ''
this.output = '... Starting ...\n'
this.promptCallbacks = []
this.onData = onData || (() => {})
this.retryConnect = 0
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
public async connect(): Promise<void> {
try {
return new Promise((resolve, reject) => {
this.socket.connect(this.apc_port, this.apc_ip, () => {
this.status = 'CONNECTED'
this.socket.setEncoding('utf8')
this.socket.on('data', (data) => this._handleData(data.toString()))
this.socket.on('close', () => this._handleClose())
this.socket.on('timeout', () => this._handleTimeout())
this.socket.on('error', (err) => this._handleError(err))
resolve()
})
})
} catch (e) {
console.error(e)
}
}
public disconnect(): void {
if (this.socket && !this.socket.destroyed) {
this.socket.removeAllListeners()
this.socket.destroy()
this.socket.unref()
}
}
private _handleData(data: string): void {
this.output += data
this.output = this.output.slice(-10000)
this.buffer += data
this.buffer = this.buffer.slice(-1000)
this.onData(this.buffer)
if (this.promptCallbacks.length > 0) {
const { prompt, callback } = this.promptCallbacks[0]
if (this.buffer.includes(prompt)) {
const cb = this.promptCallbacks.shift()
if (cb) cb.callback(this.buffer)
this.buffer = ''
}
}
}
private _handleClose(): void {
this.status = 'DISCONNECTED'
this.output += '\r\n\r\n[DISCONNECTED] Socket closed'
this.onData(this.output)
this._cleanup()
}
private async _handleTimeout(): Promise<void> {
this.status = 'TIMEOUT'
this.output += '\r\n\r\n[TIMEOUT] Connection timed out'
this.onData(this.output)
if (this.retryConnect <= 5) {
await this.sleep(5000)
console.log('Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
}
}
private _handleError(err: NodeJS.ErrnoException): void {
this.output += `\r\n\r\n[ERROR] ${err.message}`
this.onData(this.output)
if (err.code === 'ECONNRESET') {
setTimeout(() => {
console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip)
this.reconnect()
}, 10000)
}
}
private _waitFor(prompt: string, timeout = 5000): Promise<string> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._handleTimeout()
reject(new Error(`Timeout waiting for: ${prompt}`))
}, timeout)
this.promptCallbacks.push({
prompt,
callback: (data) => {
clearTimeout(timer)
resolve(data)
},
})
})
}
private _send(command: string): void {
if (this.socket && !this.socket.destroyed && this.socket.readyState) {
this.socket.write(this._convertSpecialKey(command) + '\r\n')
}
}
private _cleanup(): void {
this.promptCallbacks = []
this.socket.removeAllListeners()
this.socket.unref()
this.socket.destroy()
}
private _convertSpecialKey(key: string): string {
switch (key) {
case 'ENTER':
return ''
case 'ESC':
return '\x1B'
case 'CTRL-L':
return '\x0C'
case 'SPACE':
return ' '
case 'D':
return '\x44'
default:
return key
}
}
public async login(): Promise<void> {
await this.sleep(500)
this._send(this.apc_username)
await this.sleep(500)
this._send(this.apc_password)
await this.sleep(1000)
this._send('1')
await this.sleep(5000)
this._send('ENTER')
}
public async returnToMainMenu(maxAttempts = 5): Promise<void> {
for (let i = 0; i < maxAttempts; i++) {
this._send('\x1B')
const menuText = await this._waitFor('Main Menu', 5000)
if (menuText.includes('Control Console') || menuText.includes('Device Manager')) {
return
}
}
throw new Error('Unable to return to main menu after ESC attempts')
}
public async navigateToOutlet(outletNumber: number): Promise<void> {
await this.returnToMainMenu()
this._send('1')
await this.sleep(500)
this._send(outletNumber.toString())
await this.sleep(500)
this._send('1')
}
public async turnOnOutlet(outletNumber: number): Promise<void> {
await this.navigateToOutlet(outletNumber)
this._send('1')
await this.sleep(500)
this._send('YES')
await this.sleep(500)
this._send('')
await this.sleep(500)
this._send('\x1B')
await this.sleep(500)
this._send('\x1B')
await this.sleep(2000)
this._send('')
}
public async turnOffOutlet(outletNumber: number): Promise<void> {
await this.navigateToOutlet(outletNumber)
this._send('2')
await this.sleep(500)
this._send('YES')
await this.sleep(500)
this._send('')
await this.sleep(500)
this._send('\x1B')
await this.sleep(500)
this._send('\x1B')
await this.sleep(2000)
this._send('')
}
public async restartOutlet(outletNumber: number): Promise<void> {
await this.navigateToOutlet(outletNumber)
this._send('3')
await this.sleep(500)
this._send('YES')
await this.sleep(500)
this._send('')
await this.sleep(500)
this._send('\x1B')
await this.sleep(500)
this._send('\x1B')
await this.sleep(2000)
this._send('')
}
public async reconnect(): Promise<boolean> {
try {
this.disconnect()
await this.sleep(1000)
console.log('RECONNECT APC:', this.apc_number, 'IP:', this.apc_ip)
this.socket = new net.Socket()
await this.connect()
await this.login()
return true
} catch (err) {
this._handleError(err as NodeJS.ErrnoException)
return false
}
}
}
export default APCController

View File

@ -10,6 +10,8 @@ import {
sleep,
} from '../ultils/helper.js'
import Scenario from '#models/scenario'
import Station from '#models/station'
import APCController from './apc_connection.js'
interface LineConfig {
id: number
@ -48,7 +50,6 @@ export default class LineConnection {
private outputBuffer: string
private isRunningScript: boolean
private connecting: boolean
private isSendPlatform: boolean
constructor(config: LineConfig, socketIO: any) {
this.config = config
@ -57,7 +58,6 @@ export default class LineConnection {
this.outputBuffer = ''
this.isRunningScript = false
this.connecting = false
this.isSendPlatform = false
}
connect(timeoutMs = 5000) {
@ -89,6 +89,8 @@ export default class LineConnection {
this.client.on('data', (data) => {
if (this.connecting) return
let message = data.toString()
if (message.includes('--More--')) this.writeCommand(' ')
if (this.isRunningScript) this.outputBuffer += message
// let output = cleanData(message)
// console.log(`📨 [${this.config.port}] ${message}`)
@ -168,9 +170,7 @@ export default class LineConnection {
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
return
}
if (cmd.includes('show platform') || cmd.includes('sh platform')) {
this.isSendPlatform = true
} else this.isSendPlatform = false
this.client.write(`${cmd}`)
}
@ -366,4 +366,87 @@ export default class LineConnection {
}
return false
}
async apcControl(action: 'on' | 'off' | 'restart') {
try {
const station = await Station.find(this.config.stationId)
if (!station) throw new Error('Station not found')
const apcName = this.config.apcName || 'apc_1'
const ip = (station as any)[`${apcName}_ip`] as string
const port = (station as any)[`${apcName}_port`] as number
const username = (station as any)[`${apcName}_username`] as string
const password = (station as any)[`${apcName}_password`] as string
if (!ip || !port || !username || !password)
throw new Error(`Missing APC configuration for ${apcName}`)
// Tạo APC Controller instance
const apc = new APCController({
host: ip,
port,
username,
password,
number: this.config.lineNumber,
onData: (data: string) => {
this.config.output += data
this.socketIO.emit('line_output', {
stationId: this.config.stationId,
lineId: this.config.id,
data: data,
})
appendLog(
cleanData(data),
this.config.stationId,
this.config.lineNumber,
this.config.port
)
},
})
// Connect và login
await apc.connect()
await apc.login()
// Thực thi hành động
this.socketIO.emit('apc_status', {
stationId: this.config.stationId,
lineId: this.config.id,
action,
status: 'running',
})
switch (action) {
case 'on':
await apc.turnOnOutlet(this.config.outlet)
break
case 'off':
await apc.turnOffOutlet(this.config.outlet)
break
case 'restart':
await apc.restartOutlet(this.config.outlet)
break
}
// Hoàn thành
this.socketIO.emit('apc_status', {
stationId: this.config.stationId,
lineId: this.config.id,
action,
status: 'done',
})
apc.disconnect()
} catch (error) {
const msg = (error as Error).message
console.error('APC Control error:', msg)
this.socketIO.emit('apc_status', {
stationId: this.config.stationId,
lineId: this.config.id,
action,
status: 'error',
message: msg,
})
}
}
}

View File

@ -0,0 +1,317 @@
import net from 'node:net'
type PromptCallback = {
prompt: string
callback: (data: string) => void
}
type PortInfo = {
name: string
status: string
poe: string
}
interface SwitchControllerOptions {
host: string
port?: number
username: string
password: string
onData?: (data?: any) => void
keep_connect?: boolean
}
export default class SwitchController {
private host: string
private port: number
private username: string
private password: string
private keep_connect: boolean
private onData: (data?: any) => void
private socket: net.Socket
private status: 'CONNECTED' | 'DISCONNECTED'
private buffer: string
private output: string
private promptCallbacks: PromptCallback[]
public ports: PortInfo[]
public portGroups: PortInfo[][]
private isEnable: boolean
private checkingPorts: boolean
constructor({
host,
port = 23,
username,
password,
onData,
keep_connect = false,
}: SwitchControllerOptions) {
this.host = host
this.port = port
this.username = username
this.password = password
this.keep_connect = keep_connect
this.onData = onData || (() => {})
this.socket = new net.Socket()
this.status = 'DISCONNECTED'
this.buffer = ''
this.output = ''
this.promptCallbacks = []
this.ports = []
this.portGroups = []
this.isEnable = false
this.checkingPorts = false
}
private sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
private _handleData(data: string) {
this.buffer += data
if (this.promptCallbacks.length > 0) {
const { prompt, callback } = this.promptCallbacks[0]
if (this.buffer.includes(prompt)) {
this.promptCallbacks.shift()?.callback(this.buffer)
this.buffer = ''
}
}
}
private _handleClose() {
this.status = 'DISCONNECTED'
this.isEnable = false
this.onData(`[DISCONNECTED]`)
}
private _handleError(err: Error & { code?: string }) {
this.onData(`[ERROR] ${err.message}`)
}
private _waitFor(prompt: string, timeout = 5000): Promise<string> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout waiting for: ${prompt}`))
}, timeout)
this.promptCallbacks.push({
prompt,
callback: (data: string) => {
clearTimeout(timer)
resolve(data)
},
})
})
}
private _send(command: string) {
if (this.socket && !this.socket.destroyed && this.status === 'CONNECTED') {
this.socket.write(command + '\r\n')
}
}
public connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.socket.connect(this.port, this.host, () => {
this.status = 'CONNECTED'
this.isEnable = false
this.socket.setEncoding('utf8')
this.checkStatusAllPorts()
this.socket.on('data', (data) => this._handleData(data.toString()))
resolve()
})
this.socket.on('close', () => this._handleClose())
this.socket.on('error', (err) => this._handleError(err))
} catch (error: any) {
console.log('[ERROR CONNECT SWITCH]:', error.message)
reject(error)
}
})
}
public disconnect() {
this.socket.end()
this.status = 'DISCONNECTED'
this.isEnable = false
}
public checkStatusAllPorts() {
setInterval(async () => {
try {
await this.getPorts()
if (this.promptCallbacks.length === 0) this.buffer = ''
} catch (err: any) {
console.error('Error checking port status:', err)
this.onData(`[ERROR] ${err.message}`)
}
}, 5000)
}
public async enterEnableMode() {
if (this.isEnable) return
await this.sleep(1000)
this._send('')
await this.sleep(1000)
this._send('')
await this.sleep(1000)
this._send('enable')
await this.sleep(1000)
this._send(this.password)
this.isEnable = true
}
public async login() {
await this.enterEnableMode()
this._send('terminal length 0')
}
public async checkPortStatus(port: string) {
this._send(`show interface ${port}`)
const result = await this._waitFor('#')
return result
}
public async checkPoEStatus(port: string) {
this._send(`show power inline ${port}`)
const result = await this._waitFor('#')
return result
}
public async turnPortOff(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
await this._waitFor('(config)#')
this._send(`interface ${port}`)
await this._waitFor('(config-if)#')
this._send(`shutdown`)
await this._waitFor('(config-if)#')
this._send(`end`)
}
public async turnPortOn(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
await this._waitFor('(config)#')
this._send(`interface ${port}`)
await this._waitFor('(config-if)#')
this._send(`no shutdown`)
await this._waitFor('(config-if)#')
this._send(`end`)
}
public async restartPort(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
await this._waitFor('(config)#')
this._send(`interface ${port}`)
await this._waitFor('(config-if)#')
this._send(`shutdown`)
await this._waitFor('(config-if)#')
await this.sleep(2000)
this._send(`no shutdown`)
await this._waitFor('(config-if)#')
this._send(`end`)
}
public async disablePoE(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
await this._waitFor('(config)#')
this._send(`interface ${port}`)
await this._waitFor('(config-if)#')
this._send(`power inline never`)
await this._waitFor('(config-if)#')
this._send(`end`)
}
public async enablePoE(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
await this._waitFor('(config)#')
this._send(`interface ${port}`)
await this._waitFor('(config-if)#')
this._send(`power inline auto`)
await this._waitFor('(config-if)#')
this._send(`end`)
}
public async restartPoE(port: string) {
await this.disablePoE(port)
await this.sleep(3000)
await this.enablePoE(port)
}
public async getPorts(): Promise<boolean> {
this.checkingPorts = true
this._send('show interface status')
await this.sleep(2000)
const statusOutput = this.buffer
this.buffer = ''
const lines = statusOutput.split('\n')
const ports = this.ports?.length > 0 ? [...this.ports] : []
for (const line of lines) {
const match = line.match(/^(\S+)\s+(connected|notconnect|disabled|inactive)/i)
if (match) {
const name = match[1]
const rawStatus = match[2].toLowerCase()
const status = rawStatus === 'connected' ? 'ON' : 'OFF'
const port = ports.find((p) => p.name === name)
if (port) {
port.status = status
} else ports.push({ name, status, poe: 'UNKNOWN' })
}
}
// PoE check
this._send('show power inline')
await this.sleep(2000)
const poeOutput = this.buffer
this.buffer = ''
const poeLines = poeOutput.split('\n')
for (const line of poeLines) {
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
if (match) {
const name = match[1]
const poeStatus = match[2].toLowerCase() === 'on' ? 'ON' : 'OFF'
const port = ports.find((p) => p.name === name)
if (port) port.poe = poeStatus
}
}
const grouped: Record<string, PortInfo[]> = {}
for (const port of ports) {
const prefixMatch = port.name.match(/^([A-Za-z]+)/)
const prefix = prefixMatch ? prefixMatch[1] : 'Unknown'
if (!grouped[prefix]) grouped[prefix] = []
const existing = grouped[prefix].find((el) => el.name === port.name)
if (!existing) grouped[prefix].push(port)
}
const groupedArray = Object.values(grouped)
this.ports = ports
this.portGroups = groupedArray
this.onData()
this.checkingPorts = false
return true
}
public async reconnect(): Promise<boolean> {
try {
this.disconnect()
this.socket = new net.Socket()
await this.sleep(1000)
await this.connect()
await this.login()
await this.getPorts()
return true
} catch (err: any) {
this._handleError(err)
return false
}
}
}

View File

@ -8,6 +8,15 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js'
import Line from '#models/line'
import Station from '#models/station'
interface HandleOptions {
command?: string
actionApc?: string
scenario?: any
timeout?: number
}
type LineAction = (line: LineConnection, options?: HandleOptions) => Promise<void | unknown> | void
export default class SocketIoProvider {
private static _io: CustomServer
constructor(protected app: ApplicationService) {}
@ -108,67 +117,29 @@ export class WebSocketIo {
socket.on('write_command_line_from_web', async (data) => {
const { lineIds, stationId, command } = data
for (const lineId of lineIds) {
const line = this.lineMap.get(lineId)
if (line && line.config.status === 'connected') {
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, line)
line.writeCommand(command)
} else {
if (this.lineConnecting.includes(lineId)) continue
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)
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) {
this.setTimeoutConnect(lineId, lineReconnect)
lineReconnect.writeCommand(command)
}
} else {
io.emit('line_disconnected', {
await this.handleLineOperation(
io,
stationId,
lineId,
status: 'disconnected',
})
io.emit('line_error', { lineId, error: 'Line not connected\r\n' })
}
}
}
lineIds,
async (line) => line.writeCommand(command),
{ command, timeout: 180000 }
)
})
socket.on('run_scenario', async (data) => {
const lineId = data.id
const scenario = data.scenario
const line = this.lineMap.get(lineId)
if (line && line.config.status === 'connected') {
this.setTimeoutConnect(
lineId,
line,
scenario?.timeout ? Number(scenario?.timeout) + 180000 : 300000
await this.handleLineOperation(
io,
data.stationId,
[lineId],
async (line) => line.runScript(scenario),
{
scenario,
timeout: scenario?.timeout ? Number(scenario.timeout) + 180000 : 300000,
}
)
line.runScript(scenario)
} else {
const linesData = await Line.findBy('id', lineId)
const stationData = await Station.findBy('id', data.station_id)
if (linesData && stationData) {
await this.connectLine(io, [linesData], stationData)
const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) {
this.setTimeoutConnect(lineId, lineReconnect, 300000)
lineReconnect.runScript(scenario)
}
} else {
io.emit('line_disconnected', {
stationId: data.stationId,
lineId,
status: 'disconnected',
})
io.emit('line_error', { lineId, error: 'Line not connected\r\n' })
}
}
})
socket.on('open_cli', async (data) => {
@ -261,6 +232,18 @@ export class WebSocketIo {
console.log(error)
}
})
socket.on('control_apc', async (data) => {
const { lineIds, stationId, action } = data
await this.handleLineOperation(
io,
stationId,
lineIds,
async (line) => line.apcControl(action),
{ actionApc: action, timeout: 180000 }
)
})
})
socketServer.listen(SOCKET_IO_PORT, () => {
@ -318,4 +301,53 @@ export class WebSocketIo {
this.intervalMap[`${lineId}`] = interval
}
/**
* Hàm xử chung cho mọi action (writeCommand, runScript, v.v.)
*/
async handleLineOperation(
io: CustomServer,
stationId: number,
lineIds: number[],
action: LineAction,
options: HandleOptions = {}
): Promise<void> {
for (const lineId of lineIds) {
try {
const line = this.lineMap.get(lineId)
if (line && line.config.status === 'connected') {
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, line, options.timeout)
await action(line, options)
} else {
if (this.lineConnecting.includes(lineId)) continue
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)
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) {
this.setTimeoutConnect(lineId, lineReconnect, options.timeout)
await action(lineReconnect, options)
}
} else {
io.emit('line_disconnected', {
stationId,
lineId,
status: 'disconnected',
})
io.emit('line_error', { lineId, error: 'Line not connected\r\n', stationId })
}
}
} catch (err: any) {
io.emit('line_error', { lineId, error: `[ERROR] ${err.message}\r\n`, stationId })
}
}
}
}

View File

@ -28,7 +28,8 @@ import axios from "axios";
import CardLine from "./components/CardLine";
import { SocketProvider, useSocket } from "./context/SocketContext";
import {
ButtonConnect,
// ButtonConnect,
ButtonControlApc,
ButtonCopy,
ButtonDPELP,
ButtonScenario,
@ -122,11 +123,19 @@ function App() {
if (!socket || !stations?.length) return;
socket.on("line_connected", (data) =>
updateValueLineStation(data?.id, { status: data.status }, data?.stationId)
updateValueLineStation(
data?.lineId,
{ status: data.status },
data?.stationId
)
);
socket.on("line_disconnected", (data) =>
updateValueLineStation(data?.id, { status: data.status }, data?.stationId)
updateValueLineStation(
data?.lineId,
{ status: data.status },
data?.stationId
)
);
socket?.on("line_output", (data) => {
@ -347,7 +356,7 @@ function App() {
data.userOpenCLI = user?.userName;
socket?.emit("open_cli", {
lineId: line.id,
stationId: line.station_id,
stationId: line.stationId || line.station_id,
userEmail: user?.email,
userName: user?.userName,
});
@ -438,7 +447,13 @@ function App() {
setSelectedLines={setSelectedLines}
station={station}
/>
<ButtonConnect
{/* <ButtonConnect
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
station={station}
socket={socket}
/> */}
<ButtonControlApc
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
station={station}

View File

@ -1,7 +1,13 @@
import type { Socket } from "socket.io-client";
import type { IScenario, TextFSM, TLine, TStation } from "../untils/types";
import { Button } from "@mantine/core";
import { Button, Menu, rem, Text } from "@mantine/core";
import classes from "./Component.module.css";
import { notifications } from "@mantine/notifications";
import {
IconPlayerPlay,
IconPlugConnectedX,
IconRotate,
} from "@tabler/icons-react";
export const ButtonDPELP = ({
socket,
@ -307,3 +313,133 @@ export const ButtonConnect = ({
</Button>
);
};
export const ButtonControlApc = ({
selectedLines,
setSelectedLines,
station,
socket,
}: {
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
selectedLines: TLine[];
station: TStation;
socket: Socket | null;
}) => {
return (
<Menu
closeOnItemClick={false}
position="left-start"
transitionProps={{
transition: "rotate-left",
duration: 150,
}}
>
<Menu.Target>
<Button
color="green"
disabled={selectedLines.length === 0}
variant="outline"
style={{ height: "30px", width: "100px" }}
onClick={() => {}}
>
APC
</Button>
</Menu.Target>
<Menu.Dropdown>
{["APC 1", "APC 2"].map((item, i) => (
<Menu.Item
key={i}
onClick={() => {
if (
(i === 0 && !station.apc_1_ip) ||
(i === 1 && !station.apc_2_ip)
) {
notifications.show({
title: "Warning",
message: "Please set APC IP address in station settings!",
color: "orange",
});
}
}}
>
<Menu
disabled={
(i === 0 && !station.apc_1_ip) || (i === 1 && !station.apc_2_ip)
}
withArrow
offset={20}
position="left-start"
transitionProps={{
transition: "rotate-left",
duration: 150,
}}
>
<Menu.Target>
<div>{item}</div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item>
<div
className={classes.itemMenuPower}
onClick={() => {
setSelectedLines([]);
socket?.emit("control_apc", {
lineIds: selectedLines.map((el) => el.id),
stationId: station.id,
action: "on",
});
}}
>
<IconPlayerPlay
color="green"
style={{ width: rem(18), height: rem(18) }}
/>
<Text size="sm">Turn on All</Text>
</div>
</Menu.Item>
<Menu.Item>
<div
className={classes.itemMenuPower}
onClick={() => {
setSelectedLines([]);
socket?.emit("control_apc", {
lineIds: selectedLines.map((el) => el.id),
stationId: station.id,
action: "off",
});
}}
>
<IconPlugConnectedX
color="red"
style={{ width: rem(18), height: rem(18) }}
/>
<Text size="sm">Turn off All</Text>
</div>
</Menu.Item>
<Menu.Item>
<div
className={classes.itemMenuPower}
onClick={() => {
setSelectedLines([]);
socket?.emit("control_apc", {
lineIds: selectedLines.map((el) => el.id),
stationId: station.id,
action: "restart",
});
}}
>
<IconRotate
color="orange"
style={{ width: rem(18), height: rem(18) }}
/>
<Text size="sm">Restart All</Text>
</div>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
};

View File

@ -75,3 +75,12 @@
display: flex;
justify-content: space-evenly;
}
.itemMenuPower {
display: flex;
align-items: center;
justify-content: space-around;
gap: 4px;
width: 100px;
padding: 6px 4px;
}

View File

@ -91,12 +91,12 @@ const StationSetting = ({
const dataLine = dataStation.lines.map((value) => ({
id: value.id,
lineNumber: value.line_number || 0,
lineNumber: value.lineNumber || value.line_number || 0,
port: value.port,
lineClear: value.line_clear || 0,
apc_name: value.apc_name,
lineClear: value.lineClear || value.line_clear || 0,
apc_name: value.apcName || value.apc_name,
outlet: value.outlet,
station_id: value.station_id,
station_id: value.stationId || value.station_id,
}));
setLines(dataLine);
}

View File

@ -6,6 +6,7 @@ import {
Grid,
Group,
Modal,
ScrollArea,
Text,
} from "@mantine/core";
import type {
@ -19,6 +20,7 @@ import type { Socket } from "socket.io-client";
import classes from "./Component.module.css";
import { useEffect, useMemo, useRef, useState } from "react";
import { IconCircleCheckFilled } from "@tabler/icons-react";
import { ButtonDPELP } from "./ButtonAction";
const ModalTerminal = ({
opened,
@ -103,10 +105,12 @@ const ModalTerminal = ({
size={"80%"}
style={{ position: "absolute", left: 0 }}
title={
<Flex align={"center"} justify={"space-between"} w={"100%"}>
<Box
style={{
display: "flex",
justifyContent: "center",
// justifyContent: "center",
width: "400px",
}}
>
<Text size="md" mr={10}>
@ -132,6 +136,26 @@ const ModalTerminal = ({
: "Terminal is used"}
</div>
</Box>
<Flex
align={"center"}
justify={"space-between"}
gap={"md"}
style={{
width: "400px",
}}
>
<div className={classes.info_line} style={{ fontSize: "14px" }}>
PID: {line?.inventory?.pid || ""}
</div>
<div className={classes.info_line} style={{ fontSize: "14px" }}>
SN: {line?.inventory?.sn || ""}
</div>
<div className={classes.info_line} style={{ fontSize: "14px" }}>
VID: {line?.inventory?.vid || ""}
</div>
</Flex>
<Box></Box>
</Flex>
}
>
<Grid>
@ -152,6 +176,19 @@ const ModalTerminal = ({
/>
</Grid.Col>
<Grid.Col span={2}>
<ScrollArea h={"60vh"} style={{ paddingBottom: "12px" }}>
<Flex w={"100%"} direction={"column"} wrap={"wrap"} gap={"6px"}>
<ButtonDPELP
socket={socket}
selectedLines={line ? [line] : []}
isDisable={isDisable}
onClick={() => {
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 10000);
}}
/>
{scenarios.map((scenario) => (
<Button
disabled={
@ -183,6 +220,8 @@ const ModalTerminal = ({
{scenario.title}
</Button>
))}
</Flex>
</ScrollArea>
</Grid.Col>
</Grid>
<Flex justify={"space-between"}>

View File

@ -62,6 +62,7 @@ export type TLine = {
lineClear: number;
line_clear?: number;
station_id: number;
stationId?: number;
data?: string | any;
type?: string;
inventory?: any;
@ -73,6 +74,7 @@ export type TLine = {
cliOpened?: boolean;
systemLogUrl?: string;
apc_name: string;
apcName?: string;
created_at?: string; // or use Date if you're working with Date objects
updated_at?: string; // or use Date if you're working with Date objects
notes?: string;