Update FE
This commit is contained in:
		
							parent
							
								
									ef1d585b61
								
							
						
					
					
						commit
						dea4d2b804
					
				| 
						 | 
					@ -91,9 +91,10 @@ export default class ScenariosController {
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async update({ request, response, auth }: HttpContext) {
 | 
					  async update({ request, response, auth }: HttpContext) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const scenarioId = request.param('id')
 | 
					      const scenarioId = request.body().id
 | 
				
			||||||
      const payload = await request.all()
 | 
					      const payload = request.body()
 | 
				
			||||||
      const scenario = await Scenario.findOrFail(scenarioId)
 | 
					      const scenario = await Scenario.find(scenarioId)
 | 
				
			||||||
 | 
					      if (!scenario) return response.status(404).json({ message: 'Scenario not found' })
 | 
				
			||||||
      payload.body = JSON.stringify(payload.body)
 | 
					      payload.body = JSON.stringify(payload.body)
 | 
				
			||||||
      scenario.merge(payload)
 | 
					      scenario.merge(payload)
 | 
				
			||||||
      await scenario.save()
 | 
					      await scenario.save()
 | 
				
			||||||
| 
						 | 
					@ -113,7 +114,7 @@ export default class ScenariosController {
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async delete({ request, response }: HttpContext) {
 | 
					  async delete({ request, response }: HttpContext) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const scenarioId = request.param('id')
 | 
					      const scenarioId = request.body().id
 | 
				
			||||||
      const scenario = await Scenario.findOrFail(scenarioId)
 | 
					      const scenario = await Scenario.findOrFail(scenarioId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!scenario) {
 | 
					      if (!scenario) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import type { HttpContext } from '@adonisjs/core/http'
 | 
					import type { HttpContext } from '@adonisjs/core/http'
 | 
				
			||||||
import Station from '#models/station'
 | 
					import Station from '#models/station'
 | 
				
			||||||
 | 
					import Line from '#models/line'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class StationsController {
 | 
					export default class StationsController {
 | 
				
			||||||
  public async index({}: HttpContext) {
 | 
					  public async index({}: HttpContext) {
 | 
				
			||||||
| 
						 | 
					@ -8,6 +9,8 @@ export default class StationsController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async store({ request, response }: HttpContext) {
 | 
					  public async store({ request, response }: HttpContext) {
 | 
				
			||||||
    let payload = request.only(Array.from(Station.$columnsDefinitions.keys()))
 | 
					    let payload = request.only(Array.from(Station.$columnsDefinitions.keys()))
 | 
				
			||||||
 | 
					    let lines: Line[] = request.body().lines || []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const stationName = await Station.findBy('name', payload.name)
 | 
					      const stationName = await Station.findBy('name', payload.name)
 | 
				
			||||||
      if (stationName) return response.status(400).json({ message: 'Station name exist!' })
 | 
					      if (stationName) return response.status(400).json({ message: 'Station name exist!' })
 | 
				
			||||||
| 
						 | 
					@ -16,10 +19,33 @@ export default class StationsController {
 | 
				
			||||||
      if (stationIP) return response.status(400).json({ message: 'Ip of station is exist!' })
 | 
					      if (stationIP) return response.status(400).json({ message: 'Ip of station is exist!' })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const station = await Station.create(payload)
 | 
					      const station = await Station.create(payload)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newLines: Line[] = []
 | 
				
			||||||
 | 
					      if (lines && Array.isArray(lines)) {
 | 
				
			||||||
 | 
					        lines.forEach(async (line) => {
 | 
				
			||||||
 | 
					          if (line.id) {
 | 
				
			||||||
 | 
					            const value = await Line.find(line.id)
 | 
				
			||||||
 | 
					            if (value) {
 | 
				
			||||||
 | 
					              Object.assign(value, line)
 | 
				
			||||||
 | 
					              await value.save()
 | 
				
			||||||
 | 
					              newLines.push(value)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              const value1 = await Line.create({ ...line, stationId: station.id })
 | 
				
			||||||
 | 
					              newLines.push(value1)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            const value2 = await Line.create({ ...line, stationId: station.id })
 | 
				
			||||||
 | 
					            newLines.push(value2)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const resStation = await Station.query().where('id', station.id).preload('lines')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return response.created({
 | 
					      return response.created({
 | 
				
			||||||
        status: true,
 | 
					        status: true,
 | 
				
			||||||
        message: 'Station created successfully',
 | 
					        message: 'Station created successfully',
 | 
				
			||||||
        data: station,
 | 
					        data: resStation.map((el) => ({ ...el.$original, lines: newLines })),
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      return response.badRequest({ error: error, message: 'Station create failed', status: false })
 | 
					      return response.badRequest({ error: error, message: 'Station create failed', status: false })
 | 
				
			||||||
| 
						 | 
					@ -36,6 +62,8 @@ export default class StationsController {
 | 
				
			||||||
        (f) => f !== 'created_at' && f !== 'updated_at'
 | 
					        (f) => f !== 'created_at' && f !== 'updated_at'
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    let lines: Line[] = request.body().lines || []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const station = await Station.find(request.body().id)
 | 
					      const station = await Station.find(request.body().id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +76,21 @@ export default class StationsController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await station.save()
 | 
					      await station.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return response.ok({ status: true, message: 'Station update successfully', data: station })
 | 
					      if (lines && Array.isArray(lines)) {
 | 
				
			||||||
 | 
					        lines.forEach(async (line) => {
 | 
				
			||||||
 | 
					          if (line.id) {
 | 
				
			||||||
 | 
					            const value = await Line.find(line.id)
 | 
				
			||||||
 | 
					            if (value) {
 | 
				
			||||||
 | 
					              Object.assign(value, line)
 | 
				
			||||||
 | 
					              await value.save()
 | 
				
			||||||
 | 
					            } else await Line.create({ ...line, stationId: station.id })
 | 
				
			||||||
 | 
					          } else await Line.create({ ...line, stationId: station.id })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const resStation = await Station.query().where('id', station.id).preload('lines')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return response.ok({ status: true, message: 'Station update successfully', data: resStation })
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      return response.badRequest({ error: error, message: 'Station update failed', status: false })
 | 
					      return response.badRequest({ error: error, message: 'Station update failed', status: false })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import net from 'node:net'
 | 
					import net from 'node:net'
 | 
				
			||||||
import { cleanData } from '../ultils/helper.js'
 | 
					import { cleanData, sleep } from '../ultils/helper.js'
 | 
				
			||||||
import Scenario from '#models/scenario'
 | 
					import Scenario from '#models/scenario'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface LineConfig {
 | 
					interface LineConfig {
 | 
				
			||||||
| 
						 | 
					@ -59,7 +59,7 @@ export default class LineConnection {
 | 
				
			||||||
      this.client.on('data', (data) => {
 | 
					      this.client.on('data', (data) => {
 | 
				
			||||||
        if (this.connecting) return
 | 
					        if (this.connecting) return
 | 
				
			||||||
        let message = data.toString()
 | 
					        let message = data.toString()
 | 
				
			||||||
        this.outputBuffer += message
 | 
					        if (this.isRunningScript) this.outputBuffer += message
 | 
				
			||||||
        // let output = cleanData(message)
 | 
					        // let output = cleanData(message)
 | 
				
			||||||
        // console.log(`📨 [${this.config.port}] ${message}`)
 | 
					        // console.log(`📨 [${this.config.port}] ${message}`)
 | 
				
			||||||
        // Handle netOutput with backspace support
 | 
					        // Handle netOutput with backspace support
 | 
				
			||||||
| 
						 | 
					@ -109,7 +109,7 @@ export default class LineConnection {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        console.log(`⏳ Connection timeout line ${lineNumber}`)
 | 
					        console.log(`⏳ Connection timeout line ${lineNumber}`)
 | 
				
			||||||
        this.client.destroy()
 | 
					        this.client.destroy()
 | 
				
			||||||
        reject(new Error('Connection timeout'))
 | 
					        // reject(new Error('Connection timeout'))
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -138,10 +138,14 @@ export default class LineConnection {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async runScript(script: Scenario) {
 | 
					  async runScript(script: Scenario) {
 | 
				
			||||||
    if (!this.client || this.client.destroyed) {
 | 
					    if (!this.client || this.client.destroyed) {
 | 
				
			||||||
      throw new Error('Not connected')
 | 
					      console.log('Not connected')
 | 
				
			||||||
 | 
					      this.isRunningScript = false
 | 
				
			||||||
 | 
					      this.outputBuffer = ''
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (this.isRunningScript) {
 | 
					    if (this.isRunningScript) {
 | 
				
			||||||
      throw new Error('Script already running')
 | 
					      console.log('Script already running')
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.isRunningScript = true
 | 
					    this.isRunningScript = true
 | 
				
			||||||
| 
						 | 
					@ -152,10 +156,10 @@ export default class LineConnection {
 | 
				
			||||||
      const timeoutTimer = setTimeout(() => {
 | 
					      const timeoutTimer = setTimeout(() => {
 | 
				
			||||||
        this.isRunningScript = false
 | 
					        this.isRunningScript = false
 | 
				
			||||||
        this.outputBuffer = ''
 | 
					        this.outputBuffer = ''
 | 
				
			||||||
        reject(new Error('Script timeout'))
 | 
					        // reject(new Error('Script timeout'))
 | 
				
			||||||
      }, script.timeout || 300000)
 | 
					      }, script.timeout || 300000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const runStep = (index: number) => {
 | 
					      const runStep = async (index: number) => {
 | 
				
			||||||
        if (index >= steps.length) {
 | 
					        if (index >= steps.length) {
 | 
				
			||||||
          clearTimeout(timeoutTimer)
 | 
					          clearTimeout(timeoutTimer)
 | 
				
			||||||
          this.isRunningScript = false
 | 
					          this.isRunningScript = false
 | 
				
			||||||
| 
						 | 
					@ -166,11 +170,9 @@ export default class LineConnection {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const step = steps[index]
 | 
					        const step = steps[index]
 | 
				
			||||||
        let repeatCount = Number(step.repeat) || 1
 | 
					        let repeatCount = Number(step.repeat) || 1
 | 
				
			||||||
 | 
					 | 
				
			||||||
        const sendCommand = () => {
 | 
					        const sendCommand = () => {
 | 
				
			||||||
          if (repeatCount <= 0) {
 | 
					          if (repeatCount <= 0) {
 | 
				
			||||||
            // Done → next step
 | 
					            // Done → next step
 | 
				
			||||||
            this.client.off('data', onOutput)
 | 
					 | 
				
			||||||
            stepIndex++
 | 
					            stepIndex++
 | 
				
			||||||
            return runStep(stepIndex)
 | 
					            return runStep(stepIndex)
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
| 
						 | 
					@ -183,24 +185,19 @@ export default class LineConnection {
 | 
				
			||||||
          setTimeout(() => sendCommand(), Number(step?.delay) || 500)
 | 
					          setTimeout(() => sendCommand(), Number(step?.delay) || 500)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Lắng nghe output cho expect
 | 
					 | 
				
			||||||
        const onOutput = (data: string) => {
 | 
					 | 
				
			||||||
          const output = data.toString()
 | 
					 | 
				
			||||||
          this.outputBuffer += output
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (output.includes(step.expect)) {
 | 
					 | 
				
			||||||
            this.client.off('data', onOutput)
 | 
					 | 
				
			||||||
            setTimeout(() => sendCommand(), Number(step?.delay) || 500)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Nếu expect rỗng → gửi ngay
 | 
					        // Nếu expect rỗng → gửi ngay
 | 
				
			||||||
        if (!step?.expect || step?.expect.trim() === '') {
 | 
					        if (!step?.expect || step?.expect.trim() === '') {
 | 
				
			||||||
          setTimeout(() => sendCommand(), Number(step?.delay) || 500)
 | 
					          setTimeout(() => sendCommand(), Number(step?.delay) || 500)
 | 
				
			||||||
          return
 | 
					          return
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.client.on('data', onOutput)
 | 
					        while (this.outputBuffer) {
 | 
				
			||||||
 | 
					          await sleep(200)
 | 
				
			||||||
 | 
					          if (this.outputBuffer.includes(step.expect)) {
 | 
				
			||||||
 | 
					            this.outputBuffer = ''
 | 
				
			||||||
 | 
					            setTimeout(() => sendCommand(), Number(step?.delay) || 500)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      runStep(stepIndex)
 | 
					      runStep(stepIndex)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,3 +12,7 @@ export const cleanData = (data: string) => {
 | 
				
			||||||
    .replace(/[^\x20-\x7E\r\n]/g, '') // Remove non-printable characters
 | 
					    .replace(/[^\x20-\x7E\r\n]/g, '') // Remove non-printable characters
 | 
				
			||||||
  // .replace(/\r\n/g, '\n')
 | 
					  // .replace(/\r\n/g, '\n')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function sleep(ms: number) {
 | 
				
			||||||
 | 
					  return new Promise((resolve) => setTimeout(resolve, ms))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ export default class extends BaseSchema {
 | 
				
			||||||
      table.integer('line_clear')
 | 
					      table.integer('line_clear')
 | 
				
			||||||
      table.integer('outlet')
 | 
					      table.integer('outlet')
 | 
				
			||||||
      table.integer('station_id').unsigned().references('id').inTable('stations')
 | 
					      table.integer('station_id').unsigned().references('id').inTable('stations')
 | 
				
			||||||
      table.string('apc_name').notNullable()
 | 
					      table.string('apc_name')
 | 
				
			||||||
      table.timestamps()
 | 
					      table.timestamps()
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,6 @@ export default class extends BaseSchema {
 | 
				
			||||||
    this.schema.createTable(this.tableName, (table) => {
 | 
					    this.schema.createTable(this.tableName, (table) => {
 | 
				
			||||||
      table.increments('id')
 | 
					      table.increments('id')
 | 
				
			||||||
      table.string('title').notNullable()
 | 
					      table.string('title').notNullable()
 | 
				
			||||||
      table.string('name').notNullable()
 | 
					 | 
				
			||||||
      table.text('body').notNullable()
 | 
					      table.text('body').notNullable()
 | 
				
			||||||
      table.integer('timeout').notNullable()
 | 
					      table.integer('timeout').notNullable()
 | 
				
			||||||
      table.boolean('isReboot').defaultTo(false)
 | 
					      table.boolean('isReboot').defaultTo(false)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,7 @@ export class WebSocketIo {
 | 
				
			||||||
  intervalMap: { [key: string]: NodeJS.Timeout } = {}
 | 
					  intervalMap: { [key: string]: NodeJS.Timeout } = {}
 | 
				
			||||||
  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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(protected app: ApplicationService) {}
 | 
					  constructor(protected app: ApplicationService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,15 +95,19 @@ export class WebSocketIo {
 | 
				
			||||||
        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) {
 | 
				
			||||||
 | 
					            this.lineConnecting.filter((el) => el !== lineId)
 | 
				
			||||||
            this.setTimeoutConnect(lineId, line)
 | 
					            this.setTimeoutConnect(lineId, line)
 | 
				
			||||||
            line.writeCommand(command)
 | 
					            line.writeCommand(command)
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
 | 
					            if (this.lineConnecting.includes(lineId)) continue
 | 
				
			||||||
            const linesData = await Line.findBy('id', lineId)
 | 
					            const linesData = await Line.findBy('id', lineId)
 | 
				
			||||||
            const stationData = await Station.findBy('id', stationId)
 | 
					            const stationData = await Station.findBy('id', stationId)
 | 
				
			||||||
            if (linesData && stationData) {
 | 
					            if (linesData && stationData) {
 | 
				
			||||||
 | 
					              this.lineConnecting.push(lineId)
 | 
				
			||||||
              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.setTimeoutConnect(lineId, lineReconnect)
 | 
					                this.setTimeoutConnect(lineId, lineReconnect)
 | 
				
			||||||
                lineReconnect.writeCommand(command)
 | 
					                lineReconnect.writeCommand(command)
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
| 
						 | 
					@ -131,7 +136,7 @@ export class WebSocketIo {
 | 
				
			||||||
          line.runScript(scenario)
 | 
					          line.runScript(scenario)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          const linesData = await Line.findBy('id', lineId)
 | 
					          const linesData = await Line.findBy('id', lineId)
 | 
				
			||||||
          const stationData = await Station.findBy('id', data.stationId)
 | 
					          const stationData = await Station.findBy('id', data.station_id)
 | 
				
			||||||
          if (linesData && stationData) {
 | 
					          if (linesData && stationData) {
 | 
				
			||||||
            await this.connectLine(io, [linesData], stationData)
 | 
					            await this.connectLine(io, [linesData], stationData)
 | 
				
			||||||
            const lineReconnect = this.lineMap.get(lineId)
 | 
					            const lineReconnect = this.lineMap.get(lineId)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
    <meta charset="UTF-8" />
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
 | 
					    <link rel="icon" type="image/svg+xml" href="./public/icon-ATC-removebg.png" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <title>Automation Test</title>
 | 
					    <title>Automation Test</title>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,8 @@
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@mantine/core": "^8.3.5",
 | 
					        "@mantine/core": "^8.3.5",
 | 
				
			||||||
        "@mantine/dates": "^8.3.5",
 | 
					        "@mantine/dates": "^8.3.5",
 | 
				
			||||||
 | 
					        "@mantine/form": "^8.3.5",
 | 
				
			||||||
 | 
					        "@mantine/hooks": "^8.3.5",
 | 
				
			||||||
        "@mantine/notifications": "^8.3.5",
 | 
					        "@mantine/notifications": "^8.3.5",
 | 
				
			||||||
        "@tabler/icons-react": "^3.35.0",
 | 
					        "@tabler/icons-react": "^3.35.0",
 | 
				
			||||||
        "@xterm/addon-fit": "^0.10.0",
 | 
					        "@xterm/addon-fit": "^0.10.0",
 | 
				
			||||||
| 
						 | 
					@ -1114,12 +1116,24 @@
 | 
				
			||||||
        "react-dom": "^18.x || ^19.x"
 | 
					        "react-dom": "^18.x || ^19.x"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@mantine/form": {
 | 
				
			||||||
 | 
					      "version": "8.3.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.3.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-i9UFiHtO1dlrJXZkquyt+71YcNNxPPSkIcJCRp7k0Tif7bPqWK2xijPDEXzqvA53YvMvEMoqaQCEQLVmH7Esdg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "fast-deep-equal": "^3.1.3",
 | 
				
			||||||
 | 
					        "klona": "^2.0.6"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "react": "^18.x || ^19.x"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@mantine/hooks": {
 | 
					    "node_modules/@mantine/hooks": {
 | 
				
			||||||
      "version": "8.3.5",
 | 
					      "version": "8.3.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
 | 
					      "integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "peer": true,
 | 
					 | 
				
			||||||
      "peerDependencies": {
 | 
					      "peerDependencies": {
 | 
				
			||||||
        "react": "^18.x || ^19.x"
 | 
					        "react": "^18.x || ^19.x"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -2631,7 +2645,6 @@
 | 
				
			||||||
      "version": "3.1.3",
 | 
					      "version": "3.1.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
 | 
					      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/fast-glob": {
 | 
					    "node_modules/fast-glob": {
 | 
				
			||||||
| 
						 | 
					@ -3115,6 +3128,15 @@
 | 
				
			||||||
        "json-buffer": "3.0.1"
 | 
					        "json-buffer": "3.0.1"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/klona": {
 | 
				
			||||||
 | 
					      "version": "2.0.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/levn": {
 | 
					    "node_modules/levn": {
 | 
				
			||||||
      "version": "0.4.1",
 | 
					      "version": "0.4.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,8 @@
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@mantine/core": "^8.3.5",
 | 
					    "@mantine/core": "^8.3.5",
 | 
				
			||||||
    "@mantine/dates": "^8.3.5",
 | 
					    "@mantine/dates": "^8.3.5",
 | 
				
			||||||
 | 
					    "@mantine/form": "^8.3.5",
 | 
				
			||||||
 | 
					    "@mantine/hooks": "^8.3.5",
 | 
				
			||||||
    "@mantine/notifications": "^8.3.5",
 | 
					    "@mantine/notifications": "^8.3.5",
 | 
				
			||||||
    "@tabler/icons-react": "^3.35.0",
 | 
					    "@tabler/icons-react": "^3.35.0",
 | 
				
			||||||
    "@xterm/addon-fit": "^0.10.0",
 | 
					    "@xterm/addon-fit": "^0.10.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 85 KiB  | 
| 
						 | 
					@ -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 { useEffect, useState } from "react";
 | 
					import { Suspense, useEffect, useState } from "react";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Tabs,
 | 
					  Tabs,
 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
| 
						 | 
					@ -16,20 +16,24 @@ import {
 | 
				
			||||||
  ScrollArea,
 | 
					  ScrollArea,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  ActionIcon,
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  LoadingOverlay,
 | 
				
			||||||
} from "@mantine/core";
 | 
					} from "@mantine/core";
 | 
				
			||||||
import type { LineConfig, TLine, TStation } from "./untils/types";
 | 
					import type { IScenario, LineConfig, TLine, TStation } from "./untils/types";
 | 
				
			||||||
import axios from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
import CardLine from "./components/CardLine";
 | 
					import CardLine from "./components/CardLine";
 | 
				
			||||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
 | 
					import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
 | 
				
			||||||
import { SocketProvider, useSocket } from "./context/SocketContext";
 | 
					import { SocketProvider, useSocket } from "./context/SocketContext";
 | 
				
			||||||
import { ButtonDPELP } from "./components/ButtonAction";
 | 
					import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
 | 
				
			||||||
 | 
					import StationSetting from "./components/FormAddEdit";
 | 
				
			||||||
 | 
					import DrawerScenario from "./components/DrawerScenario";
 | 
				
			||||||
 | 
					import { Notifications } from "@mantine/notifications";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
					const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Main Component
 | 
					 * Main Component
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function App() {
 | 
					function App() {
 | 
				
			||||||
  document.title = "Automation Test";
 | 
					  document.title = "Automation Test";
 | 
				
			||||||
  const { socket } = useSocket();
 | 
					  const { socket } = useSocket();
 | 
				
			||||||
  const [stations, setStations] = useState<TStation[]>([]);
 | 
					  const [stations, setStations] = useState<TStation[]>([]);
 | 
				
			||||||
| 
						 | 
					@ -45,6 +49,10 @@ export function App() {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  const [showBottomShadow, setShowBottomShadow] = useState(false);
 | 
					  const [showBottomShadow, setShowBottomShadow] = useState(false);
 | 
				
			||||||
  const [isDisable, setIsDisable] = useState(false);
 | 
					  const [isDisable, setIsDisable] = useState(false);
 | 
				
			||||||
 | 
					  const [isOpenAddStation, setIsOpenAddStation] = useState(false);
 | 
				
			||||||
 | 
					  const [isEditStation, setIsEditStation] = useState(false);
 | 
				
			||||||
 | 
					  const [stationEdit, setStationEdit] = useState<TStation | undefined>();
 | 
				
			||||||
 | 
					  const [scenarios, setScenarios] = useState<IScenario[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // function get list station
 | 
					  // function get list station
 | 
				
			||||||
  const getStation = async () => {
 | 
					  const getStation = async () => {
 | 
				
			||||||
| 
						 | 
					@ -62,8 +70,23 @@ export function App() {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // function get list station
 | 
				
			||||||
 | 
					  const getScenarios = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await axios.get(apiUrl + "api/scenarios");
 | 
				
			||||||
 | 
					      if (response.data.status) {
 | 
				
			||||||
 | 
					        if (Array.isArray(response.data.data.data)) {
 | 
				
			||||||
 | 
					          setScenarios(response.data.data.data);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log("Error get station", error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    getStation();
 | 
					    getStation();
 | 
				
			||||||
 | 
					    getScenarios();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
| 
						 | 
					@ -121,7 +144,6 @@ export function App() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
 | 
					    <Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
 | 
				
			||||||
      {/* Tabs (Top Bar) */}
 | 
					 | 
				
			||||||
      <Tabs
 | 
					      <Tabs
 | 
				
			||||||
        value={activeTab}
 | 
					        value={activeTab}
 | 
				
			||||||
        onChange={(id) => setActiveTab(id?.toString() || "0")}
 | 
					        onChange={(id) => setActiveTab(id?.toString() || "0")}
 | 
				
			||||||
| 
						 | 
					@ -146,14 +168,35 @@ export function App() {
 | 
				
			||||||
            className={classes.indicator}
 | 
					            className={classes.indicator}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <Flex gap={"sm"}>
 | 
					          <Flex gap={"sm"}>
 | 
				
			||||||
            <ActionIcon title="Add Station" variant="outline" color="green">
 | 
					            {Number(activeTab) ? (
 | 
				
			||||||
              <IconSettingsPlus />
 | 
					              <ActionIcon
 | 
				
			||||||
            </ActionIcon>
 | 
					                title="Edit Station"
 | 
				
			||||||
            {Number(activeTab) && (
 | 
					                variant="outline"
 | 
				
			||||||
              <ActionIcon title="Edit Station" variant="outline">
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  setStationEdit(
 | 
				
			||||||
 | 
					                    stations.find((el) => el.id === Number(activeTab))
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                  setIsOpenAddStation(true);
 | 
				
			||||||
 | 
					                  setIsEditStation(true);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
                <IconEdit />
 | 
					                <IconEdit />
 | 
				
			||||||
              </ActionIcon>
 | 
					              </ActionIcon>
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              ""
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
 | 
					            <ActionIcon
 | 
				
			||||||
 | 
					              title="Add Station"
 | 
				
			||||||
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					              color="green"
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                setIsOpenAddStation(true);
 | 
				
			||||||
 | 
					                setIsEditStation(false);
 | 
				
			||||||
 | 
					                setStationEdit(undefined);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <IconSettingsPlus />
 | 
				
			||||||
 | 
					            </ActionIcon>
 | 
				
			||||||
          </Flex>
 | 
					          </Flex>
 | 
				
			||||||
        </Tabs.List>
 | 
					        </Tabs.List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -249,6 +292,11 @@ export function App() {
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    Connect
 | 
					                    Connect
 | 
				
			||||||
                  </Button>
 | 
					                  </Button>
 | 
				
			||||||
 | 
					                  <hr style={{ width: "100%" }} />
 | 
				
			||||||
 | 
					                  <DrawerScenario
 | 
				
			||||||
 | 
					                    scenarios={scenarios}
 | 
				
			||||||
 | 
					                    setScenarios={setScenarios}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
                  <ButtonDPELP
 | 
					                  <ButtonDPELP
 | 
				
			||||||
                    socket={socket}
 | 
					                    socket={socket}
 | 
				
			||||||
                    selectedLines={selectedLines}
 | 
					                    selectedLines={selectedLines}
 | 
				
			||||||
| 
						 | 
					@ -261,12 +309,42 @@ export function App() {
 | 
				
			||||||
                      }, 10000);
 | 
					                      }, 10000);
 | 
				
			||||||
                    }}
 | 
					                    }}
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
 | 
					                  {scenarios.map((el) => (
 | 
				
			||||||
 | 
					                    <ButtonScenario
 | 
				
			||||||
 | 
					                      socket={socket}
 | 
				
			||||||
 | 
					                      selectedLines={selectedLines}
 | 
				
			||||||
 | 
					                      isDisable={isDisable || selectedLines.length === 0}
 | 
				
			||||||
 | 
					                      onClick={() => {
 | 
				
			||||||
 | 
					                        setSelectedLines([]);
 | 
				
			||||||
 | 
					                        setIsDisable(true);
 | 
				
			||||||
 | 
					                        setTimeout(() => {
 | 
				
			||||||
 | 
					                          setIsDisable(false);
 | 
				
			||||||
 | 
					                        }, 10000);
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                      scenario={el}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  ))}
 | 
				
			||||||
                </Flex>
 | 
					                </Flex>
 | 
				
			||||||
              </Grid.Col>
 | 
					              </Grid.Col>
 | 
				
			||||||
            </Grid>
 | 
					            </Grid>
 | 
				
			||||||
          </Tabs.Panel>
 | 
					          </Tabs.Panel>
 | 
				
			||||||
        ))}
 | 
					        ))}
 | 
				
			||||||
      </Tabs>
 | 
					      </Tabs>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <StationSetting
 | 
				
			||||||
 | 
					        dataStation={stationEdit}
 | 
				
			||||||
 | 
					        isOpen={isOpenAddStation}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          setIsOpenAddStation(false);
 | 
				
			||||||
 | 
					          setIsEditStation(false);
 | 
				
			||||||
 | 
					          setStationEdit(undefined);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        isEdit={isEditStation}
 | 
				
			||||||
 | 
					        setStations={setStations}
 | 
				
			||||||
 | 
					        setActiveTab={() =>
 | 
				
			||||||
 | 
					          setActiveTab(stations.length ? stations[0]?.id.toString() : "0")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </Container>
 | 
					    </Container>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -275,7 +353,18 @@ export default function Main() {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <MantineProvider>
 | 
					    <MantineProvider>
 | 
				
			||||||
      <SocketProvider>
 | 
					      <SocketProvider>
 | 
				
			||||||
 | 
					        <Suspense
 | 
				
			||||||
 | 
					          fallback={
 | 
				
			||||||
 | 
					            <LoadingOverlay
 | 
				
			||||||
 | 
					              visible={true}
 | 
				
			||||||
 | 
					              zIndex={1000}
 | 
				
			||||||
 | 
					              overlayProps={{ radius: "sm", blur: 1 }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Notifications position="top-right" autoClose={5000} />
 | 
				
			||||||
          <App />
 | 
					          <App />
 | 
				
			||||||
 | 
					        </Suspense>
 | 
				
			||||||
      </SocketProvider>
 | 
					      </SocketProvider>
 | 
				
			||||||
    </MantineProvider>
 | 
					    </MantineProvider>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import type { Socket } from "socket.io-client";
 | 
					import type { Socket } from "socket.io-client";
 | 
				
			||||||
import type { TLine } from "../untils/types";
 | 
					import type { IScenario, TLine } from "../untils/types";
 | 
				
			||||||
import { Button } from "@mantine/core";
 | 
					import { Button } from "@mantine/core";
 | 
				
			||||||
 | 
					import classes from "./Component.module.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ButtonDPELP = ({
 | 
					export const ButtonDPELP = ({
 | 
				
			||||||
  socket,
 | 
					  socket,
 | 
				
			||||||
| 
						 | 
					@ -18,7 +19,7 @@ export const ButtonDPELP = ({
 | 
				
			||||||
      disabled={isDisable}
 | 
					      disabled={isDisable}
 | 
				
			||||||
      miw={"100px"}
 | 
					      miw={"100px"}
 | 
				
			||||||
      // radius="lg"
 | 
					      // radius="lg"
 | 
				
			||||||
      h={"24px"}
 | 
					      h={"28px"}
 | 
				
			||||||
      mr={"5px"}
 | 
					      mr={"5px"}
 | 
				
			||||||
      variant="filled"
 | 
					      variant="filled"
 | 
				
			||||||
      color="#00a164"
 | 
					      color="#00a164"
 | 
				
			||||||
| 
						 | 
					@ -130,3 +131,42 @@ export const ButtonDPELP = ({
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ButtonScenario = ({
 | 
				
			||||||
 | 
					  socket,
 | 
				
			||||||
 | 
					  isDisable,
 | 
				
			||||||
 | 
					  onClick,
 | 
				
			||||||
 | 
					  selectedLines,
 | 
				
			||||||
 | 
					  scenario,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  socket: Socket | null;
 | 
				
			||||||
 | 
					  isDisable: boolean;
 | 
				
			||||||
 | 
					  onClick: () => void;
 | 
				
			||||||
 | 
					  selectedLines: TLine[];
 | 
				
			||||||
 | 
					  scenario: IScenario;
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Button
 | 
				
			||||||
 | 
					      disabled={isDisable}
 | 
				
			||||||
 | 
					      miw={"100px"}
 | 
				
			||||||
 | 
					      style={{ minHeight: "24px", height: "auto" }}
 | 
				
			||||||
 | 
					      mr={"5px"}
 | 
				
			||||||
 | 
					      variant="outline"
 | 
				
			||||||
 | 
					      color="#00a164"
 | 
				
			||||||
 | 
					      className={classes.buttonScenario}
 | 
				
			||||||
 | 
					      onClick={async () => {
 | 
				
			||||||
 | 
					        onClick();
 | 
				
			||||||
 | 
					        selectedLines?.forEach((el) => {
 | 
				
			||||||
 | 
					          socket?.emit(
 | 
				
			||||||
 | 
					            "run_scenario",
 | 
				
			||||||
 | 
					            Object.assign(el, {
 | 
				
			||||||
 | 
					              scenario: scenario,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {scenario.title}
 | 
				
			||||||
 | 
					    </Button>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ const CardLine = ({
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          <Text fw={600} style={{ display: "flex", gap: "4px" }}>
 | 
					          <Text fw={600} style={{ display: "flex", gap: "4px" }}>
 | 
				
			||||||
            Line {line.lineNumber} - {line.port}{" "}
 | 
					            Line: {line.lineNumber || line.line_number} - {line.port}{" "}
 | 
				
			||||||
            {line.status === "connected" && (
 | 
					            {line.status === "connected" && (
 | 
				
			||||||
              <IconCircleCheckFilled color="green" />
 | 
					              <IconCircleCheckFilled color="green" />
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,3 +14,7 @@
 | 
				
			||||||
   margin-top: 4px;
 | 
					   margin-top: 4px;
 | 
				
			||||||
   height: 20px;
 | 
					   height: 20px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.buttonScenario :global(.mantine-Button-label) {
 | 
				
			||||||
 | 
					  white-space: normal !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					import { Box, Button, Group, Modal, Text } from "@mantine/core";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					const DialogConfirm = ({
 | 
				
			||||||
 | 
					  opened,
 | 
				
			||||||
 | 
					  close,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  handle,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  opened: boolean;
 | 
				
			||||||
 | 
					  close: () => void;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  handle: () => void;
 | 
				
			||||||
 | 
					  centered?: boolean;
 | 
				
			||||||
 | 
					  bottom?: boolean;
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  const [disableBtn, setDisableBtn] = useState(false);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box>
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        style={{ position: "absolute", left: 0, border: "solid 2px #ff6c6b" }}
 | 
				
			||||||
 | 
					        title="Confirm"
 | 
				
			||||||
 | 
					        opened={opened}
 | 
				
			||||||
 | 
					        centered
 | 
				
			||||||
 | 
					        withCloseButton
 | 
				
			||||||
 | 
					        shadow="md"
 | 
				
			||||||
 | 
					        onClose={close}
 | 
				
			||||||
 | 
					        size="xs"
 | 
				
			||||||
 | 
					        radius="md"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Text
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          mb={"xl"}
 | 
				
			||||||
 | 
					          fw={700}
 | 
				
			||||||
 | 
					          c={"#ff6c6b"}
 | 
				
			||||||
 | 
					          style={{ textAlign: "center", fontSize: "18px" }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {message}
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Group style={{ display: "flex", justifyContent: "center" }}>
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            variant="gradient"
 | 
				
			||||||
 | 
					            gradient={{ from: "pink", to: "red", deg: 90 }}
 | 
				
			||||||
 | 
					            size="xs"
 | 
				
			||||||
 | 
					            disabled={disableBtn}
 | 
				
			||||||
 | 
					            onClick={async () => {
 | 
				
			||||||
 | 
					              setDisableBtn(true);
 | 
				
			||||||
 | 
					              await handle();
 | 
				
			||||||
 | 
					              setDisableBtn(false);
 | 
				
			||||||
 | 
					              close();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            Yes
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            variant="gradient"
 | 
				
			||||||
 | 
					            size="xs"
 | 
				
			||||||
 | 
					            onClick={async () => {
 | 
				
			||||||
 | 
					              close();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            No
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </Group>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default DialogConfirm;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,344 @@
 | 
				
			||||||
 | 
					import { useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Drawer,
 | 
				
			||||||
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  ScrollArea,
 | 
				
			||||||
 | 
					  Table,
 | 
				
			||||||
 | 
					  Grid,
 | 
				
			||||||
 | 
					  TextInput,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { IconSettingsPlus } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import TableRows from "./Scenario/TableRows";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useForm } from "@mantine/form";
 | 
				
			||||||
 | 
					import DialogConfirm from "./DialogConfirm";
 | 
				
			||||||
 | 
					import type { IBodyScenario, IScenario } from "../untils/types";
 | 
				
			||||||
 | 
					import classes from "./Component.module.css";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
 | 
					const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function DrawerScenario({
 | 
				
			||||||
 | 
					  scenarios,
 | 
				
			||||||
 | 
					  setScenarios,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  scenarios: IScenario[];
 | 
				
			||||||
 | 
					  setScenarios: (value: React.SetStateAction<IScenario[]>) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [opened, { open, close }] = useDisclosure(false);
 | 
				
			||||||
 | 
					  const [isEdit, setIsEdit] = useState(false);
 | 
				
			||||||
 | 
					  const [openConfirm, setOpenConfirm] = useState<boolean>(false);
 | 
				
			||||||
 | 
					  const [isSubmit, setIsSubmit] = useState<boolean>(false);
 | 
				
			||||||
 | 
					  const [dataScenario, setDataScenario] = useState<IScenario>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    initialValues: {
 | 
				
			||||||
 | 
					      title: "",
 | 
				
			||||||
 | 
					      body: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          expect: "",
 | 
				
			||||||
 | 
					          send: "",
 | 
				
			||||||
 | 
					          delay: "0",
 | 
				
			||||||
 | 
					          repeat: "1",
 | 
				
			||||||
 | 
					          note: "",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ] as IBodyScenario[],
 | 
				
			||||||
 | 
					      timeout: "30000",
 | 
				
			||||||
 | 
					      is_reboot: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    validate: {
 | 
				
			||||||
 | 
					      title: (value) => {
 | 
				
			||||||
 | 
					        if (!value) return "Title is required";
 | 
				
			||||||
 | 
					        if (value.length > 100) return "The title cannot exceed 100 characters";
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      body: (value) => {
 | 
				
			||||||
 | 
					        if (value.length === 0) return "The body is required";
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      timeout: (value) => {
 | 
				
			||||||
 | 
					        if (!value) return "Title is required";
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const addRowUnder = (index: number) => {
 | 
				
			||||||
 | 
					    const newBody = [...form.values.body];
 | 
				
			||||||
 | 
					    newBody.splice(index + 1, 0, {
 | 
				
			||||||
 | 
					      expect: "",
 | 
				
			||||||
 | 
					      send: "",
 | 
				
			||||||
 | 
					      delay: "0",
 | 
				
			||||||
 | 
					      repeat: "1",
 | 
				
			||||||
 | 
					      note: "",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    form.setFieldValue("body", newBody);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const deleteRow = (index: number) => {
 | 
				
			||||||
 | 
					    const newBody = [...form.values.body];
 | 
				
			||||||
 | 
					    newBody.splice(index, 1);
 | 
				
			||||||
 | 
					    form.setFieldValue("body", newBody);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSave = async () => {
 | 
				
			||||||
 | 
					    setIsSubmit(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const body = form.values.body.map((el: IBodyScenario) => ({
 | 
				
			||||||
 | 
					        ...el,
 | 
				
			||||||
 | 
					        delay: Number(el.delay),
 | 
				
			||||||
 | 
					        repeat: Number(el.repeat),
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const payload = {
 | 
				
			||||||
 | 
					        ...form.values,
 | 
				
			||||||
 | 
					        body: body,
 | 
				
			||||||
 | 
					        timeout: Number(form.values.timeout),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      const url = isEdit ? "api/scenarios/update" : "api/scenarios/create";
 | 
				
			||||||
 | 
					      const res = await axios.post(
 | 
				
			||||||
 | 
					        apiUrl + url,
 | 
				
			||||||
 | 
					        isEdit ? { ...payload, id: dataScenario?.id } : payload
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (res.data.status === true) {
 | 
				
			||||||
 | 
					        const scenario = res.data.data;
 | 
				
			||||||
 | 
					        setScenarios((pre) =>
 | 
				
			||||||
 | 
					          isEdit
 | 
				
			||||||
 | 
					            ? pre.map((el) =>
 | 
				
			||||||
 | 
					                el.id === scenario.id ? { ...el, ...scenario } : el
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            : [...pre, scenario]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Success",
 | 
				
			||||||
 | 
					          message: res.data.message,
 | 
				
			||||||
 | 
					          color: "green",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsSubmit(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await axios.post(apiUrl + "api/scenarios/delete", {
 | 
				
			||||||
 | 
					        id: dataScenario?.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (response.data.status) {
 | 
				
			||||||
 | 
					        setScenarios((pre) => pre.filter((el) => el.id !== dataScenario?.id));
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Success",
 | 
				
			||||||
 | 
					          message: response.data.message,
 | 
				
			||||||
 | 
					          color: "green",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Error delete scenario",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!opened) {
 | 
				
			||||||
 | 
					      setIsEdit(false);
 | 
				
			||||||
 | 
					      setIsSubmit(false);
 | 
				
			||||||
 | 
					      setDataScenario(undefined);
 | 
				
			||||||
 | 
					      form.reset();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [opened]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Drawer
 | 
				
			||||||
 | 
					        size={"70%"}
 | 
				
			||||||
 | 
					        position="right"
 | 
				
			||||||
 | 
					        style={{ position: "absolute", left: 0 }}
 | 
				
			||||||
 | 
					        offset={8}
 | 
				
			||||||
 | 
					        radius="md"
 | 
				
			||||||
 | 
					        opened={opened}
 | 
				
			||||||
 | 
					        onClose={close}
 | 
				
			||||||
 | 
					        title={isEdit ? "Edit Scenarios" : "Add Scenarios"}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Grid>
 | 
				
			||||||
 | 
					          <Grid.Col span={2} style={{ borderRight: "1px solid #ccc" }}>
 | 
				
			||||||
 | 
					            {scenarios.map((scenario) => (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                disabled={isSubmit}
 | 
				
			||||||
 | 
					                className={classes.buttonScenario}
 | 
				
			||||||
 | 
					                key={scenario.id}
 | 
				
			||||||
 | 
					                miw={"100px"}
 | 
				
			||||||
 | 
					                mb={"6px"}
 | 
				
			||||||
 | 
					                style={{ minHeight: "24px" }}
 | 
				
			||||||
 | 
					                mr={"5px"}
 | 
				
			||||||
 | 
					                variant={
 | 
				
			||||||
 | 
					                  dataScenario && dataScenario?.id === scenario.id
 | 
				
			||||||
 | 
					                    ? "filled"
 | 
				
			||||||
 | 
					                    : "outline"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                onClick={async () => {
 | 
				
			||||||
 | 
					                  if (dataScenario?.id === scenario.id) {
 | 
				
			||||||
 | 
					                    setIsEdit(false);
 | 
				
			||||||
 | 
					                    setDataScenario(undefined);
 | 
				
			||||||
 | 
					                    form.reset();
 | 
				
			||||||
 | 
					                  } else {
 | 
				
			||||||
 | 
					                    setIsEdit(true);
 | 
				
			||||||
 | 
					                    setDataScenario(scenario);
 | 
				
			||||||
 | 
					                    form.setFieldValue("title", scenario.title);
 | 
				
			||||||
 | 
					                    form.setFieldValue("timeout", scenario.timeout.toString());
 | 
				
			||||||
 | 
					                    form.setFieldValue("body", JSON.parse(scenario.body));
 | 
				
			||||||
 | 
					                    form.setFieldValue("is_reboot", scenario.is_reboot);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {scenario.title}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </Grid.Col>
 | 
				
			||||||
 | 
					          <Grid.Col span={10}>
 | 
				
			||||||
 | 
					            <Box>
 | 
				
			||||||
 | 
					              <Grid>
 | 
				
			||||||
 | 
					                <Grid.Col span={4}>
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="Title"
 | 
				
			||||||
 | 
					                    placeholder="Scenario title"
 | 
				
			||||||
 | 
					                    value={form.values.title}
 | 
				
			||||||
 | 
					                    error={form.errors.title}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("title", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    required
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Grid.Col>
 | 
				
			||||||
 | 
					                <Grid.Col span={2}>
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="Timeout (ms)"
 | 
				
			||||||
 | 
					                    placeholder="Timeout (ms)"
 | 
				
			||||||
 | 
					                    value={form.values.timeout}
 | 
				
			||||||
 | 
					                    error={form.errors.timeout}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("timeout", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    required
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Grid.Col>
 | 
				
			||||||
 | 
					                <Grid.Col span={6}>
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    style={{
 | 
				
			||||||
 | 
					                      display: "flex",
 | 
				
			||||||
 | 
					                      alignItems: "end",
 | 
				
			||||||
 | 
					                      gap: "10px",
 | 
				
			||||||
 | 
					                      justifyContent: "flex-end",
 | 
				
			||||||
 | 
					                      height: "100%",
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {isEdit && (
 | 
				
			||||||
 | 
					                      <Button
 | 
				
			||||||
 | 
					                        disabled={isSubmit}
 | 
				
			||||||
 | 
					                        style={{ height: "30px" }}
 | 
				
			||||||
 | 
					                        color="red"
 | 
				
			||||||
 | 
					                        onClick={() => {
 | 
				
			||||||
 | 
					                          setOpenConfirm(true);
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        Delete
 | 
				
			||||||
 | 
					                      </Button>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    <Button
 | 
				
			||||||
 | 
					                      disabled={isSubmit}
 | 
				
			||||||
 | 
					                      style={{ height: "30px" }}
 | 
				
			||||||
 | 
					                      color="green"
 | 
				
			||||||
 | 
					                      onClick={() => {
 | 
				
			||||||
 | 
					                        handleSave();
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      Save
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </Grid.Col>
 | 
				
			||||||
 | 
					              </Grid>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					            <hr style={{ width: "100%" }} />
 | 
				
			||||||
 | 
					            <Box>
 | 
				
			||||||
 | 
					              <ScrollArea h={500} style={{ marginBottom: "20px" }}>
 | 
				
			||||||
 | 
					                <Table
 | 
				
			||||||
 | 
					                  stickyHeader
 | 
				
			||||||
 | 
					                  stickyHeaderOffset={-1}
 | 
				
			||||||
 | 
					                  striped
 | 
				
			||||||
 | 
					                  highlightOnHover
 | 
				
			||||||
 | 
					                  withRowBorders={true}
 | 
				
			||||||
 | 
					                  withTableBorder={true}
 | 
				
			||||||
 | 
					                  withColumnBorders={true}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Table.Thead style={{ zIndex: 100 }}>
 | 
				
			||||||
 | 
					                    <Table.Tr>
 | 
				
			||||||
 | 
					                      <Table.Th>#</Table.Th>
 | 
				
			||||||
 | 
					                      <Table.Th>
 | 
				
			||||||
 | 
					                        {/* <span style={{ marginLeft: '30px' }}>Expect</span> */}
 | 
				
			||||||
 | 
					                        Expect
 | 
				
			||||||
 | 
					                      </Table.Th>
 | 
				
			||||||
 | 
					                      <Table.Th>Send</Table.Th>
 | 
				
			||||||
 | 
					                      <Table.Th w={130}>Delay(ms)</Table.Th>
 | 
				
			||||||
 | 
					                      <Table.Th w={100}>Repeat</Table.Th>
 | 
				
			||||||
 | 
					                      <Table.Th></Table.Th>
 | 
				
			||||||
 | 
					                    </Table.Tr>
 | 
				
			||||||
 | 
					                  </Table.Thead>
 | 
				
			||||||
 | 
					                  <Table.Tbody id="tbody-table">
 | 
				
			||||||
 | 
					                    {form.values.body.map(
 | 
				
			||||||
 | 
					                      (element: IBodyScenario, i: number) => (
 | 
				
			||||||
 | 
					                        <TableRows
 | 
				
			||||||
 | 
					                          key={i}
 | 
				
			||||||
 | 
					                          addRowUnder={addRowUnder}
 | 
				
			||||||
 | 
					                          deleteRow={deleteRow}
 | 
				
			||||||
 | 
					                          element={element}
 | 
				
			||||||
 | 
					                          form={form}
 | 
				
			||||||
 | 
					                          i={i}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  </Table.Tbody>
 | 
				
			||||||
 | 
					                </Table>
 | 
				
			||||||
 | 
					              </ScrollArea>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          </Grid.Col>
 | 
				
			||||||
 | 
					        </Grid>
 | 
				
			||||||
 | 
					      </Drawer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ActionIcon
 | 
				
			||||||
 | 
					        title="Add Scenario"
 | 
				
			||||||
 | 
					        variant="outline"
 | 
				
			||||||
 | 
					        color="green"
 | 
				
			||||||
 | 
					        onClick={() => {
 | 
				
			||||||
 | 
					          open();
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <IconSettingsPlus />
 | 
				
			||||||
 | 
					      </ActionIcon>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <DialogConfirm
 | 
				
			||||||
 | 
					        opened={openConfirm}
 | 
				
			||||||
 | 
					        close={() => {
 | 
				
			||||||
 | 
					          setOpenConfirm(false);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        message={"Are you sure delete this station?"}
 | 
				
			||||||
 | 
					        handle={() => {
 | 
				
			||||||
 | 
					          setOpenConfirm(false);
 | 
				
			||||||
 | 
					          handleDelete();
 | 
				
			||||||
 | 
					          close();
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        centered={true}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default DrawerScenario;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,663 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Flex,
 | 
				
			||||||
 | 
					  Group,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  NumberInput,
 | 
				
			||||||
 | 
					  PasswordInput,
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  Table,
 | 
				
			||||||
 | 
					  TextInput,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm } from "@mantine/form";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import type { TLine, TStation } from "../untils/types";
 | 
				
			||||||
 | 
					import DialogConfirm from "./DialogConfirm";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
 | 
					import { IconInputX } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const lineInit = {
 | 
				
			||||||
 | 
					  port: 0,
 | 
				
			||||||
 | 
					  lineNumber: 0,
 | 
				
			||||||
 | 
					  lineClear: 0,
 | 
				
			||||||
 | 
					  station_id: 0,
 | 
				
			||||||
 | 
					  apc_name: "",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StationSetting = ({
 | 
				
			||||||
 | 
					  isOpen,
 | 
				
			||||||
 | 
					  isEdit,
 | 
				
			||||||
 | 
					  onClose,
 | 
				
			||||||
 | 
					  dataStation,
 | 
				
			||||||
 | 
					  setStations,
 | 
				
			||||||
 | 
					  setActiveTab,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  isOpen: boolean;
 | 
				
			||||||
 | 
					  isEdit: boolean;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					  dataStation?: TStation;
 | 
				
			||||||
 | 
					  setStations: (value: React.SetStateAction<TStation[]>) => void;
 | 
				
			||||||
 | 
					  setActiveTab: () => void;
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  const [lines, setLines] = useState<TLine[]>([lineInit]);
 | 
				
			||||||
 | 
					  const [openConfirm, setOpenConfirm] = useState<boolean>(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ipRegex =
 | 
				
			||||||
 | 
					    /(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\.(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\.(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\.(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\b/g;
 | 
				
			||||||
 | 
					  const form = useForm<TStation>({
 | 
				
			||||||
 | 
					    initialValues: dataStation,
 | 
				
			||||||
 | 
					    validate: (values: TStation) => ({
 | 
				
			||||||
 | 
					      ip: !ipRegex.test(values.ip) ? "IP address invalid" : null,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (dataStation) {
 | 
				
			||||||
 | 
					      form.setFieldValue("name", dataStation.name);
 | 
				
			||||||
 | 
					      form.setFieldValue("ip", dataStation.ip);
 | 
				
			||||||
 | 
					      form.setFieldValue("port", dataStation.port);
 | 
				
			||||||
 | 
					      form.setFieldValue("netmask", dataStation.netmask);
 | 
				
			||||||
 | 
					      form.setFieldValue("network", dataStation.network);
 | 
				
			||||||
 | 
					      form.setFieldValue("gateway", dataStation.gateway);
 | 
				
			||||||
 | 
					      form.setFieldValue("tftp_ip", dataStation.tftp_ip);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_1_ip", dataStation.apc_1_ip);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_1_port", dataStation.apc_1_port);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_1_username", dataStation.apc_1_username);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_1_password", dataStation.apc_1_password);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_2_ip", dataStation.apc_2_ip);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_2_port", dataStation.apc_2_port);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_2_username", dataStation.apc_2_username);
 | 
				
			||||||
 | 
					      form.setFieldValue("apc_2_password", dataStation.apc_2_password);
 | 
				
			||||||
 | 
					      form.setFieldValue("switch_control_ip", dataStation.switch_control_ip);
 | 
				
			||||||
 | 
					      form.setFieldValue(
 | 
				
			||||||
 | 
					        "switch_control_port",
 | 
				
			||||||
 | 
					        dataStation.switch_control_port
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      form.setFieldValue(
 | 
				
			||||||
 | 
					        "switch_control_username",
 | 
				
			||||||
 | 
					        dataStation.switch_control_username
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      form.setFieldValue(
 | 
				
			||||||
 | 
					        "switch_control_password",
 | 
				
			||||||
 | 
					        dataStation.switch_control_password
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const dataLine = dataStation.lines.map((value) => ({
 | 
				
			||||||
 | 
					        id: value.id,
 | 
				
			||||||
 | 
					        lineNumber: value.line_number || 0,
 | 
				
			||||||
 | 
					        port: value.port,
 | 
				
			||||||
 | 
					        lineClear: value.line_clear || 0,
 | 
				
			||||||
 | 
					        apc_name: value.apc_name,
 | 
				
			||||||
 | 
					        outlet: value.outlet,
 | 
				
			||||||
 | 
					        station_id: value.station_id,
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					      setLines(dataLine);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [dataStation]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (lines.length > 0) {
 | 
				
			||||||
 | 
					      const lastLine = lines[lines.length - 1];
 | 
				
			||||||
 | 
					      if (lastLine?.lineNumber || lastLine?.port)
 | 
				
			||||||
 | 
					        setLines((pre) => [...pre, lineInit]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [lines]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!isOpen) {
 | 
				
			||||||
 | 
					      setLines([lineInit]);
 | 
				
			||||||
 | 
					      setOpenConfirm(false);
 | 
				
			||||||
 | 
					      form.reset();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [isOpen]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const renderLinesTable = () => {
 | 
				
			||||||
 | 
					    const rows = lines?.map((row: TLine, index: number) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Table.Tr key={index}>
 | 
				
			||||||
 | 
					          <Table.Td fz={"sm"} p="3px" ta={"center"} fw={700}>
 | 
				
			||||||
 | 
					            <NumberInput
 | 
				
			||||||
 | 
					              value={row?.lineNumber}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                setLines((pre) =>
 | 
				
			||||||
 | 
					                  pre.map((value, i) =>
 | 
				
			||||||
 | 
					                    i === index ? { ...value, lineNumber: Number(e!) } : value
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Table.Td>
 | 
				
			||||||
 | 
					          <Table.Td fz={"sm"} p="3px" ta={"center"} fw={700}>
 | 
				
			||||||
 | 
					            <NumberInput
 | 
				
			||||||
 | 
					              value={row?.port}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                setLines((pre) =>
 | 
				
			||||||
 | 
					                  pre.map((value, i) =>
 | 
				
			||||||
 | 
					                    i === index ? { ...value, port: Number(e!) } : value
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Table.Td>
 | 
				
			||||||
 | 
					          <Table.Td fz={"sm"} p="3px" ta={"center"} fw={700}>
 | 
				
			||||||
 | 
					            <NumberInput
 | 
				
			||||||
 | 
					              value={row?.lineClear}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                setLines((pre) =>
 | 
				
			||||||
 | 
					                  pre.map((value, i) =>
 | 
				
			||||||
 | 
					                    i === index ? { ...value, lineClear: Number(e!) } : value
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Table.Td>
 | 
				
			||||||
 | 
					          <Table.Td fz={"sm"} p="3px" ta={"center"} fw={700}>
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              data={[
 | 
				
			||||||
 | 
					                { label: "APC1", value: "apc_1" },
 | 
				
			||||||
 | 
					                { label: "APC2", value: "apc_2" },
 | 
				
			||||||
 | 
					              ]}
 | 
				
			||||||
 | 
					              value={row?.apc_name}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                setLines((pre) =>
 | 
				
			||||||
 | 
					                  pre.map((value, i) =>
 | 
				
			||||||
 | 
					                    i === index ? { ...value, apc_name: e! } : value
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Table.Td>
 | 
				
			||||||
 | 
					          <Table.Td fz={"sm"} p="3px" ta={"center"} fw={700}>
 | 
				
			||||||
 | 
					            <NumberInput
 | 
				
			||||||
 | 
					              value={row?.outlet}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                setLines((pre) =>
 | 
				
			||||||
 | 
					                  pre.map((value, i) =>
 | 
				
			||||||
 | 
					                    i === index ? { ...value, outlet: Number(e!) } : value
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Table.Td>
 | 
				
			||||||
 | 
					          <Table.Td fz={"sm"} p="3px" ta={"center"} fw={700}>
 | 
				
			||||||
 | 
					            {row?.lineNumber ? (
 | 
				
			||||||
 | 
					              <ActionIcon
 | 
				
			||||||
 | 
					                title="Remove line"
 | 
				
			||||||
 | 
					                variant="outline"
 | 
				
			||||||
 | 
					                color="red"
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  setLines((pre) => [
 | 
				
			||||||
 | 
					                    ...pre.slice(0, index),
 | 
				
			||||||
 | 
					                    ...pre.slice(index + 1, lines.length),
 | 
				
			||||||
 | 
					                  ]);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <IconInputX />
 | 
				
			||||||
 | 
					              </ActionIcon>
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              ""
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Table.Td>
 | 
				
			||||||
 | 
					        </Table.Tr>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return rows;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSave = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const payload = {
 | 
				
			||||||
 | 
					        ...form.values,
 | 
				
			||||||
 | 
					        lines: lines.filter((el) => el.lineNumber && el.port),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      if (isEdit) payload.id = dataStation?.id || 0;
 | 
				
			||||||
 | 
					      const url = isEdit ? "api/stations/update" : "api/stations/create";
 | 
				
			||||||
 | 
					      const response = await axios.post(apiUrl + url, payload);
 | 
				
			||||||
 | 
					      if (response.data.status) {
 | 
				
			||||||
 | 
					        if (response.data.data) {
 | 
				
			||||||
 | 
					          const station = response.data.data[0];
 | 
				
			||||||
 | 
					          setStations((pre) =>
 | 
				
			||||||
 | 
					            isEdit
 | 
				
			||||||
 | 
					              ? pre.map((el) =>
 | 
				
			||||||
 | 
					                  el.id === station.id ? { ...el, ...station } : el
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : [...pre, station]
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Success",
 | 
				
			||||||
 | 
					          message: response.data.message,
 | 
				
			||||||
 | 
					          color: "green",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        onClose();
 | 
				
			||||||
 | 
					      } else
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Error",
 | 
				
			||||||
 | 
					          message: response.data.message,
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      console.log(err);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Error save station",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await axios.post(apiUrl + "api/stations/delete", {
 | 
				
			||||||
 | 
					        id: dataStation?.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (response.data.status) {
 | 
				
			||||||
 | 
					        setStations((pre) => pre.filter((el) => el.id !== dataStation?.id));
 | 
				
			||||||
 | 
					        setActiveTab();
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Success",
 | 
				
			||||||
 | 
					          message: response.data.message,
 | 
				
			||||||
 | 
					          color: "green",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Error delete station",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box>
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              display: "flex",
 | 
				
			||||||
 | 
					              alignItems: "center",
 | 
				
			||||||
 | 
					              gap: "10px",
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {isEdit ? "Edit Station" : "Add Station"}{" "}
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              style={{ height: "30px" }}
 | 
				
			||||||
 | 
					              color="green"
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                handleSave();
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Save
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					            {isEdit && (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                style={{ height: "30px" }}
 | 
				
			||||||
 | 
					                color="red"
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  setOpenConfirm(true);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                Delete
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        size={"60%"}
 | 
				
			||||||
 | 
					        style={{ position: "absolute", left: 0 }}
 | 
				
			||||||
 | 
					        centered
 | 
				
			||||||
 | 
					        opened={isOpen}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          onClose();
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Flex justify={"space-between"} gap={"sm"}>
 | 
				
			||||||
 | 
					          {/* Station info */}
 | 
				
			||||||
 | 
					          <Box w={"40%"}>
 | 
				
			||||||
 | 
					            <TextInput
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					              name="name"
 | 
				
			||||||
 | 
					              label="Name"
 | 
				
			||||||
 | 
					              size="sm"
 | 
				
			||||||
 | 
					              value={form.values.name || ""}
 | 
				
			||||||
 | 
					              onChange={(e) => form.setFieldValue("name", e.target.value)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              style={{
 | 
				
			||||||
 | 
					                display: "flex",
 | 
				
			||||||
 | 
					                alignItems: "center",
 | 
				
			||||||
 | 
					                justifyContent: "space-between",
 | 
				
			||||||
 | 
					                width: "100%",
 | 
				
			||||||
 | 
					                gap: "6px",
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <TextInput
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					                label="IP"
 | 
				
			||||||
 | 
					                size="sm"
 | 
				
			||||||
 | 
					                value={form.values.ip || ""}
 | 
				
			||||||
 | 
					                onChange={(e) => form.setFieldValue("ip", e.target.value)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <NumberInput
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					                size="sm"
 | 
				
			||||||
 | 
					                label="Port"
 | 
				
			||||||
 | 
					                value={form.values.port || ""}
 | 
				
			||||||
 | 
					                onChange={(e) => form.setFieldValue("port", Number(e!))}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              style={{
 | 
				
			||||||
 | 
					                display: "flex",
 | 
				
			||||||
 | 
					                alignItems: "center",
 | 
				
			||||||
 | 
					                justifyContent: "space-between",
 | 
				
			||||||
 | 
					                width: "100%",
 | 
				
			||||||
 | 
					                gap: "6px",
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <TextInput
 | 
				
			||||||
 | 
					                value={form.values.netmask || ""}
 | 
				
			||||||
 | 
					                label={"Netmask"}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  form.setFieldValue("netmask", e.target.value);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <TextInput
 | 
				
			||||||
 | 
					                value={form.values.network || ""}
 | 
				
			||||||
 | 
					                label={"Network"}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  form.setFieldValue("network", e.target.value);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              style={{
 | 
				
			||||||
 | 
					                display: "flex",
 | 
				
			||||||
 | 
					                alignItems: "center",
 | 
				
			||||||
 | 
					                justifyContent: "space-between",
 | 
				
			||||||
 | 
					                width: "100%",
 | 
				
			||||||
 | 
					                gap: "6px",
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <TextInput
 | 
				
			||||||
 | 
					                value={form.values.gateway || ""}
 | 
				
			||||||
 | 
					                label={"Gateway"}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  form.setFieldValue("gateway", e.target.value);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <TextInput
 | 
				
			||||||
 | 
					                value={form.values.tftp_ip || ""}
 | 
				
			||||||
 | 
					                label={"TFTP IP"}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  form.setFieldValue("tftp_ip", e.target.value);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div style={{}}>
 | 
				
			||||||
 | 
					              <Group
 | 
				
			||||||
 | 
					                mt={"md"}
 | 
				
			||||||
 | 
					                title="APC 1"
 | 
				
			||||||
 | 
					                style={{
 | 
				
			||||||
 | 
					                  border: "1px solid #e1e1e1",
 | 
				
			||||||
 | 
					                  padding: "10px",
 | 
				
			||||||
 | 
					                  borderRadius: "5px",
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Box
 | 
				
			||||||
 | 
					                  w={"100%"}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    justifyContent: "space-between",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="APC1 IP"
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    w={"60%"}
 | 
				
			||||||
 | 
					                    value={form.values.apc_1_ip || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_1_ip", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  <NumberInput
 | 
				
			||||||
 | 
					                    label="APC1 Port"
 | 
				
			||||||
 | 
					                    w={"39%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    value={form.values.apc_1_port || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_1_port", parseInt(e.toString()))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					                <Box
 | 
				
			||||||
 | 
					                  w={"100%"}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    justifyContent: "space-between",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="Username"
 | 
				
			||||||
 | 
					                    w={"49%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    autoComplete="new-username"
 | 
				
			||||||
 | 
					                    value={form.values.apc_1_username || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_1_username", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <PasswordInput
 | 
				
			||||||
 | 
					                    label="Password"
 | 
				
			||||||
 | 
					                    w={"49%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    autoComplete="new-password"
 | 
				
			||||||
 | 
					                    value={form.values.apc_1_password || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_1_password", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					              </Group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              {/* APC2 configuration */}
 | 
				
			||||||
 | 
					              <Group
 | 
				
			||||||
 | 
					                mt={"md"}
 | 
				
			||||||
 | 
					                title="APC 2"
 | 
				
			||||||
 | 
					                style={{
 | 
				
			||||||
 | 
					                  border: "1px solid #e1e1e1",
 | 
				
			||||||
 | 
					                  padding: "10px",
 | 
				
			||||||
 | 
					                  borderRadius: "5px",
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Box
 | 
				
			||||||
 | 
					                  w={"100%"}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    justifyContent: "space-between",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="APC2 IP"
 | 
				
			||||||
 | 
					                    w={"60%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    value={form.values.apc_2_ip || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_2_ip", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  <NumberInput
 | 
				
			||||||
 | 
					                    label="APC2 Port"
 | 
				
			||||||
 | 
					                    w={"39%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    value={form.values.apc_2_port || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_2_port", parseInt(e.toString()))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					                <Box
 | 
				
			||||||
 | 
					                  w={"100%"}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    justifyContent: "space-between",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="Username"
 | 
				
			||||||
 | 
					                    w={"49%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    value={form.values.apc_2_username || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_2_username", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <PasswordInput
 | 
				
			||||||
 | 
					                    label="Password"
 | 
				
			||||||
 | 
					                    w={"49%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    value={form.values.apc_2_password || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("apc_2_password", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					              </Group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              {/* APC1 configuration */}
 | 
				
			||||||
 | 
					              <Group
 | 
				
			||||||
 | 
					                mt={"md"}
 | 
				
			||||||
 | 
					                title="Switch control"
 | 
				
			||||||
 | 
					                style={{
 | 
				
			||||||
 | 
					                  border: "1px solid #e1e1e1",
 | 
				
			||||||
 | 
					                  padding: "10px",
 | 
				
			||||||
 | 
					                  borderRadius: "5px",
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Box
 | 
				
			||||||
 | 
					                  w={"100%"}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    justifyContent: "space-between",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="Switch IP"
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    w={"60%"}
 | 
				
			||||||
 | 
					                    value={form.values.switch_control_ip || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue("switch_control_ip", e.target.value)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  <NumberInput
 | 
				
			||||||
 | 
					                    label="Switch Port"
 | 
				
			||||||
 | 
					                    w={"39%"}
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    value={form.values.switch_control_port || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue(
 | 
				
			||||||
 | 
					                        "switch_control_port",
 | 
				
			||||||
 | 
					                        parseInt(e.toString())
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					                <Box
 | 
				
			||||||
 | 
					                  w={"100%"}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    justifyContent: "space-between",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    label="Username"
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    w={"49%"}
 | 
				
			||||||
 | 
					                    value={form.values.switch_control_username || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue(
 | 
				
			||||||
 | 
					                        "switch_control_username",
 | 
				
			||||||
 | 
					                        e.target.value
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  <PasswordInput
 | 
				
			||||||
 | 
					                    label="Password"
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    w={"49%"}
 | 
				
			||||||
 | 
					                    value={form.values.switch_control_password || ""}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue(
 | 
				
			||||||
 | 
					                        "switch_control_password",
 | 
				
			||||||
 | 
					                        e.target.value
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					              </Group>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					          {/* Lines */}
 | 
				
			||||||
 | 
					          <Box w={"60%"}>
 | 
				
			||||||
 | 
					            <Table
 | 
				
			||||||
 | 
					              verticalSpacing="xs"
 | 
				
			||||||
 | 
					              horizontalSpacing="lg"
 | 
				
			||||||
 | 
					              striped
 | 
				
			||||||
 | 
					              highlightOnHover
 | 
				
			||||||
 | 
					              withTableBorder
 | 
				
			||||||
 | 
					              withColumnBorders
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <Table.Thead>
 | 
				
			||||||
 | 
					                <Table.Tr>
 | 
				
			||||||
 | 
					                  <Table.Th fz={"sm"} w={"15%"} ta={"center"}>
 | 
				
			||||||
 | 
					                    Line number
 | 
				
			||||||
 | 
					                  </Table.Th>
 | 
				
			||||||
 | 
					                  <Table.Th fz={"sm"} w={"15%"} ta={"center"}>
 | 
				
			||||||
 | 
					                    Port
 | 
				
			||||||
 | 
					                  </Table.Th>
 | 
				
			||||||
 | 
					                  <Table.Th fz={"sm"} w={"15%"} ta={"center"}>
 | 
				
			||||||
 | 
					                    Clear line
 | 
				
			||||||
 | 
					                  </Table.Th>
 | 
				
			||||||
 | 
					                  <Table.Th fz={"sm"} w={"15%"} ta={"center"}>
 | 
				
			||||||
 | 
					                    APC
 | 
				
			||||||
 | 
					                  </Table.Th>
 | 
				
			||||||
 | 
					                  <Table.Th fz={"sm"} w={"15%"} ta={"center"}>
 | 
				
			||||||
 | 
					                    Outlet
 | 
				
			||||||
 | 
					                  </Table.Th>
 | 
				
			||||||
 | 
					                  <Table.Th fz={"sm"} w={"10%"} ta={"center"}></Table.Th>
 | 
				
			||||||
 | 
					                </Table.Tr>
 | 
				
			||||||
 | 
					              </Table.Thead>
 | 
				
			||||||
 | 
					              <Table.Tbody>{renderLinesTable()}</Table.Tbody>
 | 
				
			||||||
 | 
					            </Table>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Flex>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <DialogConfirm
 | 
				
			||||||
 | 
					        opened={openConfirm}
 | 
				
			||||||
 | 
					        close={() => {
 | 
				
			||||||
 | 
					          setOpenConfirm(false);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        message={"Are you sure delete this station?"}
 | 
				
			||||||
 | 
					        handle={() => {
 | 
				
			||||||
 | 
					          handleDelete();
 | 
				
			||||||
 | 
					          setOpenConfirm(false);
 | 
				
			||||||
 | 
					          onClose();
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        centered={true}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default StationSetting;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,83 @@
 | 
				
			||||||
 | 
					.title {
 | 
				
			||||||
 | 
					  background-color: light-dark(var(white), var(--mantine-color-dark-7));
 | 
				
			||||||
 | 
					  z-index: 100;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
 | 
				
			||||||
 | 
					    var(--mantine-spacing-sm);
 | 
				
			||||||
 | 
					  border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.optionIcon {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-evenly;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.deleteIcon {
 | 
				
			||||||
 | 
					  color: red;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  padding: 2px;
 | 
				
			||||||
 | 
					  border-radius: 25%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.editIcon {
 | 
				
			||||||
 | 
					  color: rgb(9, 132, 132);
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  padding: 2px;
 | 
				
			||||||
 | 
					  border-radius: 25%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.editIcon:hover {
 | 
				
			||||||
 | 
					  background-color: rgba(203, 203, 203, 0.809);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.deleteIcon:hover {
 | 
				
			||||||
 | 
					  background-color: rgba(203, 203, 203, 0.809);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dialog {
 | 
				
			||||||
 | 
					  background-color: light-dark(white, #2d353c);
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  border: solid 1px rgb(255, 145, 0);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dialogText {
 | 
				
			||||||
 | 
					  color: light-dark(#2d353c, white);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dragging {
 | 
				
			||||||
 | 
					  background-color: #f0f0f0;
 | 
				
			||||||
 | 
					  border: 1px dashed #ccc;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rc-tooltipCustom {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rc-tooltip-inner {
 | 
				
			||||||
 | 
					  background-color: #1d1d1d !important;
 | 
				
			||||||
 | 
					  padding: 0px 8px !important;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  background-color: #565656;
 | 
				
			||||||
 | 
					  border-radius: 4px !important;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
 | 
				
			||||||
 | 
					  min-height: 25px !important;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.rc-tooltip-arrow {
 | 
				
			||||||
 | 
					  /* background-color: #ffcc00 !important;
 | 
				
			||||||
 | 
					  color: #ffcc00 !important; */
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.rc-tooltip-content {
 | 
				
			||||||
 | 
					  max-height: 97vh;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.modalFullWidth > div {
 | 
				
			||||||
 | 
					  padding-left: 0 !important;
 | 
				
			||||||
 | 
					  padding-right: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,137 @@
 | 
				
			||||||
 | 
					import { Table, TextInput } from "@mantine/core";
 | 
				
			||||||
 | 
					import { IconRowInsertTop, IconX } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import classes from "./Scenario.module.css";
 | 
				
			||||||
 | 
					import { numberOnly } from "../../untils/helper";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IPayload {
 | 
				
			||||||
 | 
					  element: any;
 | 
				
			||||||
 | 
					  i: any;
 | 
				
			||||||
 | 
					  form: any;
 | 
				
			||||||
 | 
					  deleteRow: any;
 | 
				
			||||||
 | 
					  addRowUnder: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Table.Tr>
 | 
				
			||||||
 | 
					        <Table.Td>
 | 
				
			||||||
 | 
					          <span>{i + 1}</span>
 | 
				
			||||||
 | 
					        </Table.Td>
 | 
				
			||||||
 | 
					        <Table.Td
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            display: "flex",
 | 
				
			||||||
 | 
					            alignItems: "center",
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {/* <Box {...provided.dragHandleProps} style={{ cursor: 'grab' }}>
 | 
				
			||||||
 | 
					            <IconGripVertical />
 | 
				
			||||||
 | 
					          </Box> */}
 | 
				
			||||||
 | 
					          <TextInput
 | 
				
			||||||
 | 
					            style={{ width: "250px" }}
 | 
				
			||||||
 | 
					            value={element.expect}
 | 
				
			||||||
 | 
					            placeholder="Expect previous output"
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              const newBody = [...form.values.body];
 | 
				
			||||||
 | 
					              form.setFieldValue(
 | 
				
			||||||
 | 
					                "body",
 | 
				
			||||||
 | 
					                newBody.map((el, index) =>
 | 
				
			||||||
 | 
					                  i === index
 | 
				
			||||||
 | 
					                    ? {
 | 
				
			||||||
 | 
					                        ...el,
 | 
				
			||||||
 | 
					                        expect: e.target.value,
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    : el
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Table.Td>
 | 
				
			||||||
 | 
					        <Table.Td>
 | 
				
			||||||
 | 
					          <TextInput
 | 
				
			||||||
 | 
					            style={{ width: "250px" }}
 | 
				
			||||||
 | 
					            value={element.send}
 | 
				
			||||||
 | 
					            placeholder="Command send"
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              const newBody = [...form.values.body];
 | 
				
			||||||
 | 
					              form.setFieldValue(
 | 
				
			||||||
 | 
					                "body",
 | 
				
			||||||
 | 
					                newBody.map((el, index) =>
 | 
				
			||||||
 | 
					                  i === index ? { ...el, send: e.target.value } : el
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Table.Td>
 | 
				
			||||||
 | 
					        <Table.Td>
 | 
				
			||||||
 | 
					          <TextInput
 | 
				
			||||||
 | 
					            style={{ width: "100px" }}
 | 
				
			||||||
 | 
					            value={element.delay}
 | 
				
			||||||
 | 
					            placeholder="Delay send"
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              const value = numberOnly(e.target.value);
 | 
				
			||||||
 | 
					              if (Number(value) <= 1000000) {
 | 
				
			||||||
 | 
					                const newBody = [...form.values.body];
 | 
				
			||||||
 | 
					                form.setFieldValue(
 | 
				
			||||||
 | 
					                  "body",
 | 
				
			||||||
 | 
					                  newBody.map((el, index) =>
 | 
				
			||||||
 | 
					                    i === index ? { ...el, delay: value } : el
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Table.Td>
 | 
				
			||||||
 | 
					        <Table.Td>
 | 
				
			||||||
 | 
					          <TextInput
 | 
				
			||||||
 | 
					            style={{ width: "70px" }}
 | 
				
			||||||
 | 
					            value={element.repeat}
 | 
				
			||||||
 | 
					            placeholder="Repeat"
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              const value = numberOnly(e.target.value);
 | 
				
			||||||
 | 
					              if (Number(value) <= 1000) {
 | 
				
			||||||
 | 
					                const newBody = [...form.values.body];
 | 
				
			||||||
 | 
					                form.setFieldValue(
 | 
				
			||||||
 | 
					                  "body",
 | 
				
			||||||
 | 
					                  newBody.map((el, index) =>
 | 
				
			||||||
 | 
					                    i === index
 | 
				
			||||||
 | 
					                      ? {
 | 
				
			||||||
 | 
					                          ...el,
 | 
				
			||||||
 | 
					                          repeat: value,
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      : el
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            onKeyDown={(e) => {
 | 
				
			||||||
 | 
					              if (e.key === ".") e.preventDefault();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Table.Td>
 | 
				
			||||||
 | 
					        <Table.Td>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              display: "flex",
 | 
				
			||||||
 | 
					              alignItems: "center",
 | 
				
			||||||
 | 
					              gap: "4px",
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div role="button" onClick={() => addRowUnder(i)}>
 | 
				
			||||||
 | 
					              <IconRowInsertTop className={classes.editIcon} />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <IconX
 | 
				
			||||||
 | 
					              className={classes.deleteIcon}
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                deleteRow(i);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              width={20}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </Table.Td>
 | 
				
			||||||
 | 
					      </Table.Tr>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default TableRows;
 | 
				
			||||||
| 
						 | 
					@ -10,237 +10,237 @@ import {
 | 
				
			||||||
  IconSettingsAutomation,
 | 
					  IconSettingsAutomation,
 | 
				
			||||||
  IconKey,
 | 
					  IconKey,
 | 
				
			||||||
  IconClipboardList,
 | 
					  IconClipboardList,
 | 
				
			||||||
} from '@tabler/icons-react'
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SOCKET_EVENTS = {
 | 
					export const SOCKET_EVENTS = {
 | 
				
			||||||
  ROOM: {
 | 
					  ROOM: {
 | 
				
			||||||
    JOINED: 'room_joined',
 | 
					    JOINED: "room_joined",
 | 
				
			||||||
    LEFT: 'room_left',
 | 
					    LEFT: "room_left",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  APP_STATUS: { RECEIVED: 'app_status_received' },
 | 
					  APP_STATUS: { RECEIVED: "app_status_received" },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  APP_DATA: {
 | 
					  APP_DATA: {
 | 
				
			||||||
    SENT: 'app_data_sent',
 | 
					    SENT: "app_data_sent",
 | 
				
			||||||
    RECEIVED: 'app_data_received',
 | 
					    RECEIVED: "app_data_received",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SCRIPT_TEST: {
 | 
					  SCRIPT_TEST: {
 | 
				
			||||||
    SENT: 'script_test_sent',
 | 
					    SENT: "script_test_sent",
 | 
				
			||||||
    TIME_RECEIVED: 'script_test_time_received',
 | 
					    TIME_RECEIVED: "script_test_time_received",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  DATA_OUTPUT: { RECEIVED: 'data_output_received' },
 | 
					  DATA_OUTPUT: { RECEIVED: "data_output_received" },
 | 
				
			||||||
  NOTIFICATION: {
 | 
					  NOTIFICATION: {
 | 
				
			||||||
    FROM_APP: 'notification_send_from_app',
 | 
					    FROM_APP: "notification_send_from_app",
 | 
				
			||||||
    SEND_ALL: 'notification_send_to_all',
 | 
					    SEND_ALL: "notification_send_to_all",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  APC_CONTROL: {
 | 
					  APC_CONTROL: {
 | 
				
			||||||
    FROM_WEB: 'apc_control_request_from_web',
 | 
					    FROM_WEB: "apc_control_request_from_web",
 | 
				
			||||||
    TO_APP: 'apc_control_request_to_app',
 | 
					    TO_APP: "apc_control_request_to_app",
 | 
				
			||||||
    FROM_WEB_ALL_APC: 'all_apc_control_request_from_web',
 | 
					    FROM_WEB_ALL_APC: "all_apc_control_request_from_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SYSTEM_LOG: {
 | 
					  SYSTEM_LOG: {
 | 
				
			||||||
    FROM_APP: 'system_log_send_from_app',
 | 
					    FROM_APP: "system_log_send_from_app",
 | 
				
			||||||
    GET_SYSTEM_LOG_FROM_WEB: 'get_system_log_from_web',
 | 
					    GET_SYSTEM_LOG_FROM_WEB: "get_system_log_from_web",
 | 
				
			||||||
    REQUEST_LIST_SYSTEM_LOG_FROM_WEB: 'request_list_system_log_from_web',
 | 
					    REQUEST_LIST_SYSTEM_LOG_FROM_WEB: "request_list_system_log_from_web",
 | 
				
			||||||
    RESPONSE_LIST_SYSTEM_LOG_FROM_APP: 'response_list_system_log_from_app',
 | 
					    RESPONSE_LIST_SYSTEM_LOG_FROM_APP: "response_list_system_log_from_app",
 | 
				
			||||||
    RESPONSE_SYSTEM_LOG_FROM_APP: 'response_system_log_from_app',
 | 
					    RESPONSE_SYSTEM_LOG_FROM_APP: "response_system_log_from_app",
 | 
				
			||||||
    RESPONSE_SYSTEM_LOG_TO_WEB: 'response_system_log_to_web',
 | 
					    RESPONSE_SYSTEM_LOG_TO_WEB: "response_system_log_to_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  CLI: {
 | 
					  CLI: {
 | 
				
			||||||
    OPEN_CLI_LINE_FROM_WEB: 'open_cli_line_from_web',
 | 
					    OPEN_CLI_LINE_FROM_WEB: "open_cli_line_from_web",
 | 
				
			||||||
    CLOSE_CLI_LINE_FROM_WEB: 'close_cli_line_from_web',
 | 
					    CLOSE_CLI_LINE_FROM_WEB: "close_cli_line_from_web",
 | 
				
			||||||
    OPEN_CLI_MULTI_LINE_FROM_WEB: 'open_cli_multi_line_from_web',
 | 
					    OPEN_CLI_MULTI_LINE_FROM_WEB: "open_cli_multi_line_from_web",
 | 
				
			||||||
    CLOSE_CLI_MULTI_LINE_FROM_WEB: 'close_cli_multi_line_from_web',
 | 
					    CLOSE_CLI_MULTI_LINE_FROM_WEB: "close_cli_multi_line_from_web",
 | 
				
			||||||
    WRITE_COMMAND_FROM_WEB: 'write_command_line_from_web',
 | 
					    WRITE_COMMAND_FROM_WEB: "write_command_line_from_web",
 | 
				
			||||||
    WRITE_COMMAND_TO_APP: 'write_command_line_to_app',
 | 
					    WRITE_COMMAND_TO_APP: "write_command_line_to_app",
 | 
				
			||||||
    RECEIVE_COMMAND_DATA_FROM_APP: 'receive_command_data_from_app',
 | 
					    RECEIVE_COMMAND_DATA_FROM_APP: "receive_command_data_from_app",
 | 
				
			||||||
    RECEIVE_COMMAND_DATA_TO_WEB: 'receive_command_data_to_web',
 | 
					    RECEIVE_COMMAND_DATA_TO_WEB: "receive_command_data_to_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  RESCAN: {
 | 
					  RESCAN: {
 | 
				
			||||||
    SEND_LIST_RESCAN_FROM_WEB: 'send_list_rescan_from_web',
 | 
					    SEND_LIST_RESCAN_FROM_WEB: "send_list_rescan_from_web",
 | 
				
			||||||
    SEND_LIST_RESCAN_TO_APP: 'send_list_rescan_to_app',
 | 
					    SEND_LIST_RESCAN_TO_APP: "send_list_rescan_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  LOCK: {
 | 
					  LOCK: {
 | 
				
			||||||
    SEND_LIST_LOCK_FROM_WEB: 'send_list_lock_from_web',
 | 
					    SEND_LIST_LOCK_FROM_WEB: "send_list_lock_from_web",
 | 
				
			||||||
    SEND_LIST_LOCK_TO_APP: 'send_list_lock_to_app',
 | 
					    SEND_LIST_LOCK_TO_APP: "send_list_lock_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  CHANGE_STAGE: {
 | 
					  CHANGE_STAGE: {
 | 
				
			||||||
    SEND_STAGE_FROM_WEB: 'send_stage_from_web',
 | 
					    SEND_STAGE_FROM_WEB: "send_stage_from_web",
 | 
				
			||||||
    SEND_STAGE_TO_APP: 'send_stage_to_app',
 | 
					    SEND_STAGE_TO_APP: "send_stage_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  UPDATE_PROPERTY: {
 | 
					  UPDATE_PROPERTY: {
 | 
				
			||||||
    UPDATE_PROPERTY_FROM_WEB: 'update_property_from_web',
 | 
					    UPDATE_PROPERTY_FROM_WEB: "update_property_from_web",
 | 
				
			||||||
    UPDATE_PROPERTY_TO_APP: 'update_property_to_app',
 | 
					    UPDATE_PROPERTY_TO_APP: "update_property_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  RUN_SCENARIOS: {
 | 
					  RUN_SCENARIOS: {
 | 
				
			||||||
    RUN_SCENARIOS_FROM_WEB: 'run_scenarios_from_web',
 | 
					    RUN_SCENARIOS_FROM_WEB: "run_scenarios_from_web",
 | 
				
			||||||
    RUN_SCENARIOS_TO_APP: 'run_scenarios_to_app',
 | 
					    RUN_SCENARIOS_TO_APP: "run_scenarios_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SEND_BREAK: {
 | 
					  SEND_BREAK: {
 | 
				
			||||||
    SEND_BREAK_FROM_WEB: 'send_break_from_web',
 | 
					    SEND_BREAK_FROM_WEB: "send_break_from_web",
 | 
				
			||||||
    SEND_BREAK_TO_APP: 'send_break_to_app',
 | 
					    SEND_BREAK_TO_APP: "send_break_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  JOIN_MULTI_ROOM: {
 | 
					  JOIN_MULTI_ROOM: {
 | 
				
			||||||
    JOIN_MULTI_ROOM_FROM_WEB: 'join_multi_room_from_web',
 | 
					    JOIN_MULTI_ROOM_FROM_WEB: "join_multi_room_from_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  LEAVE_MULTI_ROOM: {
 | 
					  LEAVE_MULTI_ROOM: {
 | 
				
			||||||
    LEAVE_MULTI_ROOM_FROM_WEB: 'leave_multi_room_from_web',
 | 
					    LEAVE_MULTI_ROOM_FROM_WEB: "leave_multi_room_from_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  TAKE_OVER: {
 | 
					  TAKE_OVER: {
 | 
				
			||||||
    TAKE_OVER_FROM_WEB: 'take_over_from_web',
 | 
					    TAKE_OVER_FROM_WEB: "take_over_from_web",
 | 
				
			||||||
    TAKE_OVER_TO_WEB: 'take_over_to_web',
 | 
					    TAKE_OVER_TO_WEB: "take_over_to_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  CONNECT_APC: {
 | 
					  CONNECT_APC: {
 | 
				
			||||||
    CONNECT_APC_FROM_WEB: 'connect_apc_from_web',
 | 
					    CONNECT_APC_FROM_WEB: "connect_apc_from_web",
 | 
				
			||||||
    CONNECT_APC_TO_APP: 'connect_apc_to_app',
 | 
					    CONNECT_APC_TO_APP: "connect_apc_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  DATA_APC_RECEIVED: {
 | 
					  DATA_APC_RECEIVED: {
 | 
				
			||||||
    DATA_APC_RECEIVED_FROM_APP: 'data_apc_received_from_app',
 | 
					    DATA_APC_RECEIVED_FROM_APP: "data_apc_received_from_app",
 | 
				
			||||||
    DATA_APC_RECEIVED_TO_WEB: 'data_apc_received_to_web',
 | 
					    DATA_APC_RECEIVED_TO_WEB: "data_apc_received_to_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SEND_COMMAND_TO_APC: {
 | 
					  SEND_COMMAND_TO_APC: {
 | 
				
			||||||
    SEND_COMMAND_TO_APC_FROM_WEB: 'send_command_to_apc_from_web',
 | 
					    SEND_COMMAND_TO_APC_FROM_WEB: "send_command_to_apc_from_web",
 | 
				
			||||||
    SEND_COMMAND_TO_APC_TO_APP: 'send_command_to_apc_to_app',
 | 
					    SEND_COMMAND_TO_APC_TO_APP: "send_command_to_apc_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SEND_CLEAR_LINE: {
 | 
					  SEND_CLEAR_LINE: {
 | 
				
			||||||
    SEND_CLEAR_LINE_FROM_WEB: 'send_clear_line_from_web',
 | 
					    SEND_CLEAR_LINE_FROM_WEB: "send_clear_line_from_web",
 | 
				
			||||||
    SEND_CLEAR_LINE_TO_APP: 'send_clear_line_to_app',
 | 
					    SEND_CLEAR_LINE_TO_APP: "send_clear_line_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SEND_CLOSE_LINE: {
 | 
					  SEND_CLOSE_LINE: {
 | 
				
			||||||
    SEND_CLOSE_LINE_FROM_WEB: 'send_close_line_from_web',
 | 
					    SEND_CLOSE_LINE_FROM_WEB: "send_close_line_from_web",
 | 
				
			||||||
    SEND_CLOSE_LINE_TO_APP: 'send_close_line_to_app',
 | 
					    SEND_CLOSE_LINE_TO_APP: "send_close_line_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SEND_OPEN_LINE: {
 | 
					  SEND_OPEN_LINE: {
 | 
				
			||||||
    SEND_OPEN_LINE_FROM_WEB: 'send_open_line_from_web',
 | 
					    SEND_OPEN_LINE_FROM_WEB: "send_open_line_from_web",
 | 
				
			||||||
    SEND_OPEN_LINE_TO_APP: 'send_open_line_to_app',
 | 
					    SEND_OPEN_LINE_TO_APP: "send_open_line_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  CONTROL_APP: {
 | 
					  CONTROL_APP: {
 | 
				
			||||||
    SEND_PAUSE_APP_FROM_WEB: 'send_pause_app_from_web',
 | 
					    SEND_PAUSE_APP_FROM_WEB: "send_pause_app_from_web",
 | 
				
			||||||
    SEND_RESUME_APP_FROM_WEB: 'send_resume_app_from_web',
 | 
					    SEND_RESUME_APP_FROM_WEB: "send_resume_app_from_web",
 | 
				
			||||||
    SEND_RESTART_APP_FROM_WEB: 'send_restart_app_from_web',
 | 
					    SEND_RESTART_APP_FROM_WEB: "send_restart_app_from_web",
 | 
				
			||||||
    SEND_QUIT_APP_FROM_WEB: 'send_quit_app_from_web',
 | 
					    SEND_QUIT_APP_FROM_WEB: "send_quit_app_from_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  CONNECT_SWITCH: {
 | 
					  CONNECT_SWITCH: {
 | 
				
			||||||
    CONNECT_SWITCH_FROM_WEB: 'connect_switch_from_web',
 | 
					    CONNECT_SWITCH_FROM_WEB: "connect_switch_from_web",
 | 
				
			||||||
    CONNECT_SWITCH_TO_APP: 'connect_switch_to_app',
 | 
					    CONNECT_SWITCH_TO_APP: "connect_switch_to_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  DATA_SWITCH_RECEIVED: {
 | 
					  DATA_SWITCH_RECEIVED: {
 | 
				
			||||||
    DATA_SWITCH_RECEIVED_FROM_APP: 'data_switch_received_from_app',
 | 
					    DATA_SWITCH_RECEIVED_FROM_APP: "data_switch_received_from_app",
 | 
				
			||||||
    DATA_SWITCH_RECEIVED_TO_WEB: 'data_switch_received_to_web',
 | 
					    DATA_SWITCH_RECEIVED_TO_WEB: "data_switch_received_to_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SEND_COMMAND_TO_SWITCH: {
 | 
					  SEND_COMMAND_TO_SWITCH: {
 | 
				
			||||||
    SEND_COMMAND_TO_SWITCH_FROM_WEB: 'send_command_to_switch_from_web',
 | 
					    SEND_COMMAND_TO_SWITCH_FROM_WEB: "send_command_to_switch_from_web",
 | 
				
			||||||
    SEND_COMMAND_TO_SWITCH_TO_APP: 'send_command_to_switch_to_app',
 | 
					    SEND_COMMAND_TO_SWITCH_TO_APP: "send_command_to_switch_to_app",
 | 
				
			||||||
    SEND_COMMAND_TO_SWITCH_FROM_APP: 'send_command_to_switch_from_app',
 | 
					    SEND_COMMAND_TO_SWITCH_FROM_APP: "send_command_to_switch_from_app",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  RELOAD_TICKET: {
 | 
					  RELOAD_TICKET: {
 | 
				
			||||||
    RELOAD_TICKET_FROM_WEB: 'reload_ticket_from_web',
 | 
					    RELOAD_TICKET_FROM_WEB: "reload_ticket_from_web",
 | 
				
			||||||
    RELOAD_TICKET_TO_WEB: 'reload_ticket_to_web',
 | 
					    RELOAD_TICKET_TO_WEB: "reload_ticket_to_web",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LINE_STATUS = {
 | 
					export const LINE_STATUS = {
 | 
				
			||||||
  CHECK_INVENTORY: 'CHECK_INVENTORY',
 | 
					  CHECK_INVENTORY: "CHECK_INVENTORY",
 | 
				
			||||||
  STATUS_TEST: 'TESTING',
 | 
					  STATUS_TEST: "TESTING",
 | 
				
			||||||
  CONNECT_FAIL: 'CONNECT_FAIL',
 | 
					  CONNECT_FAIL: "CONNECT_FAIL",
 | 
				
			||||||
  CONNECTED: 'CONNECTED',
 | 
					  CONNECTED: "CONNECTED",
 | 
				
			||||||
  STATUS_READY: 'READY',
 | 
					  STATUS_READY: "READY",
 | 
				
			||||||
  STATUS_DONE: 'DONE',
 | 
					  STATUS_DONE: "DONE",
 | 
				
			||||||
  STATUS_CHECKING: 'CHECKING',
 | 
					  STATUS_CHECKING: "CHECKING",
 | 
				
			||||||
  STATUS_LOCKED: 'LOCKED',
 | 
					  STATUS_LOCKED: "LOCKED",
 | 
				
			||||||
  STATUS_CLOSED: 'CLOSED',
 | 
					  STATUS_CLOSED: "CLOSED",
 | 
				
			||||||
  STATUS_TIMEOUT: 'TIMEOUT',
 | 
					  STATUS_TIMEOUT: "TIMEOUT",
 | 
				
			||||||
  STATUS_PHYSICAL_TEST: 'STATUS_PHYSICAL_TEST',
 | 
					  STATUS_PHYSICAL_TEST: "STATUS_PHYSICAL_TEST",
 | 
				
			||||||
  STATUS_PHYSICAL_TEST_DONE: 'STATUS_PHYSICAL_TEST_DONE',
 | 
					  STATUS_PHYSICAL_TEST_DONE: "STATUS_PHYSICAL_TEST_DONE",
 | 
				
			||||||
  STATUS_UNDIFINED_INVEN: 'INVENTORY_UNIDENTIFIED',
 | 
					  STATUS_UNDIFINED_INVEN: "INVENTORY_UNIDENTIFIED",
 | 
				
			||||||
  STATUS_RUNNING_SCENARIOS: 'RUNNING_SCENARIOS',
 | 
					  STATUS_RUNNING_SCENARIOS: "RUNNING_SCENARIOS",
 | 
				
			||||||
  APC_CONTROL: 'APC_CONTROL',
 | 
					  APC_CONTROL: "APC_CONTROL",
 | 
				
			||||||
  STATUS_STARTING: 'STARTING',
 | 
					  STATUS_STARTING: "STARTING",
 | 
				
			||||||
  STATUS_TURN_OFF: 'TURN_OFF',
 | 
					  STATUS_TURN_OFF: "TURN_OFF",
 | 
				
			||||||
  STATUS_RESTARTING: 'RESTARTING',
 | 
					  STATUS_RESTARTING: "RESTARTING",
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LIST_FAVORITE_COMMANDS = [
 | 
					export const LIST_FAVORITE_COMMANDS = [
 | 
				
			||||||
  'sh inv',
 | 
					  "sh inv",
 | 
				
			||||||
  'sh ver',
 | 
					  "sh ver",
 | 
				
			||||||
  // 'sh diag',
 | 
					  // 'sh diag',
 | 
				
			||||||
  // 'sh post',
 | 
					  // 'sh post',
 | 
				
			||||||
  // 'sh env',
 | 
					  // 'sh env',
 | 
				
			||||||
  // 'sh log',
 | 
					  // 'sh log',
 | 
				
			||||||
  // 'sh platform',
 | 
					  // 'sh platform',
 | 
				
			||||||
]
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const dataPermission = [
 | 
					export const dataPermission = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/dashboard',
 | 
					    link: "/dashboard",
 | 
				
			||||||
    label: 'Dashboard',
 | 
					    label: "Dashboard",
 | 
				
			||||||
    icon: IconHome,
 | 
					    icon: IconHome,
 | 
				
			||||||
    requiredPermissions: [],
 | 
					    requiredPermissions: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/station-setting',
 | 
					    link: "/station-setting",
 | 
				
			||||||
    label: 'Station Setting',
 | 
					    label: "Station Setting",
 | 
				
			||||||
    icon: IconServer,
 | 
					    icon: IconServer,
 | 
				
			||||||
    requiredPermissions: ['station_activity'],
 | 
					    requiredPermissions: ["station_activity"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/monitor',
 | 
					    link: "/monitor",
 | 
				
			||||||
    label: 'Monitoring',
 | 
					    label: "Monitoring",
 | 
				
			||||||
    icon: IconDeviceDesktop,
 | 
					    icon: IconDeviceDesktop,
 | 
				
			||||||
    requiredPermissions: [
 | 
					    requiredPermissions: [
 | 
				
			||||||
      'monitor_power',
 | 
					      "monitor_power",
 | 
				
			||||||
      'monitor_cli',
 | 
					      "monitor_cli",
 | 
				
			||||||
      'monitor_other_items',
 | 
					      "monitor_other_items",
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/control-apc',
 | 
					    link: "/control-apc",
 | 
				
			||||||
    label: 'Control APC',
 | 
					    label: "Control APC",
 | 
				
			||||||
    icon: IconSettingsAutomation,
 | 
					    icon: IconSettingsAutomation,
 | 
				
			||||||
    requiredPermissions: ['control_apc_activity'],
 | 
					    requiredPermissions: ["control_apc_activity"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/group-model',
 | 
					    link: "/group-model",
 | 
				
			||||||
    label: 'Group - Model',
 | 
					    label: "Group - Model",
 | 
				
			||||||
    icon: IconRouter,
 | 
					    icon: IconRouter,
 | 
				
			||||||
    requiredPermissions: ['group_model_activity'],
 | 
					    requiredPermissions: ["group_model_activity"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/keyword',
 | 
					    link: "/keyword",
 | 
				
			||||||
    label: 'Keyword',
 | 
					    label: "Keyword",
 | 
				
			||||||
    icon: IconKey,
 | 
					    icon: IconKey,
 | 
				
			||||||
    requiredPermissions: ['keyword_activity', 'keyword_limit'],
 | 
					    requiredPermissions: ["keyword_activity", "keyword_limit"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/exclude-error',
 | 
					    link: "/exclude-error",
 | 
				
			||||||
    label: 'Exclude Errors',
 | 
					    label: "Exclude Errors",
 | 
				
			||||||
    icon: IconBan,
 | 
					    icon: IconBan,
 | 
				
			||||||
    requiredPermissions: ['exclude_error_activity', 'exclude_error_limit'],
 | 
					    requiredPermissions: ["exclude_error_activity", "exclude_error_limit"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/list-logs',
 | 
					    link: "/list-logs",
 | 
				
			||||||
    label: 'List Logs',
 | 
					    label: "List Logs",
 | 
				
			||||||
    icon: IconFile,
 | 
					    icon: IconFile,
 | 
				
			||||||
    requiredPermissions: [],
 | 
					    requiredPermissions: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/webhooks',
 | 
					    link: "/webhooks",
 | 
				
			||||||
    label: 'Webhooks',
 | 
					    label: "Webhooks",
 | 
				
			||||||
    icon: IconWebhook,
 | 
					    icon: IconWebhook,
 | 
				
			||||||
    requiredPermissions: ['webhook_activity', 'webhook_add_limit'],
 | 
					    requiredPermissions: ["webhook_activity", "webhook_add_limit"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/scenario',
 | 
					    link: "/scenario",
 | 
				
			||||||
    label: 'Scenario',
 | 
					    label: "Scenario",
 | 
				
			||||||
    icon: IconClipboardList,
 | 
					    icon: IconClipboardList,
 | 
				
			||||||
    requiredPermissions: ['scenario_activity', 'scenario_add_limit'],
 | 
					    requiredPermissions: ["scenario_activity", "scenario_add_limit"],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    link: '/upgrade',
 | 
					    link: "/upgrade",
 | 
				
			||||||
    label: 'Upgrade now!',
 | 
					    label: "Upgrade now!",
 | 
				
			||||||
    icon: IconCrown,
 | 
					    icon: IconCrown,
 | 
				
			||||||
    requiredPermissions: [],
 | 
					    requiredPermissions: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
]
 | 
					];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					export const numberOnly = (value: string): string => {
 | 
				
			||||||
 | 
					  const matched = value.match(/[\d.]+/g);
 | 
				
			||||||
 | 
					  return matched ? matched.join("") : "";
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -58,9 +58,10 @@ export type TLine = {
 | 
				
			||||||
  id?: number;
 | 
					  id?: number;
 | 
				
			||||||
  port: number;
 | 
					  port: number;
 | 
				
			||||||
  lineNumber: number;
 | 
					  lineNumber: number;
 | 
				
			||||||
 | 
					  line_number?: number;
 | 
				
			||||||
  lineClear: number;
 | 
					  lineClear: number;
 | 
				
			||||||
 | 
					  line_clear?: number;
 | 
				
			||||||
  station_id: number;
 | 
					  station_id: number;
 | 
				
			||||||
  is_active: string | boolean;
 | 
					 | 
				
			||||||
  data?: string | any;
 | 
					  data?: string | any;
 | 
				
			||||||
  type?: string;
 | 
					  type?: string;
 | 
				
			||||||
  inventory?: any;
 | 
					  inventory?: any;
 | 
				
			||||||
| 
						 | 
					@ -69,7 +70,6 @@ export type TLine = {
 | 
				
			||||||
  outlet?: number;
 | 
					  outlet?: number;
 | 
				
			||||||
  cliOpened?: boolean;
 | 
					  cliOpened?: boolean;
 | 
				
			||||||
  systemLogUrl?: string;
 | 
					  systemLogUrl?: string;
 | 
				
			||||||
  start_round_at: number;
 | 
					 | 
				
			||||||
  apc_name: string;
 | 
					  apc_name: string;
 | 
				
			||||||
  created_at?: string; // or use Date if you're working with Date objects
 | 
					  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
 | 
					  updated_at?: string; // or use Date if you're working with Date objects
 | 
				
			||||||
| 
						 | 
					@ -138,3 +138,20 @@ export type LineConfig = {
 | 
				
			||||||
  output: string;
 | 
					  output: string;
 | 
				
			||||||
  status: string;
 | 
					  status: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type IScenario = {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  body: string;
 | 
				
			||||||
 | 
					  timeout: number;
 | 
				
			||||||
 | 
					  is_reboot: boolean;
 | 
				
			||||||
 | 
					  updated_at: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type IBodyScenario = {
 | 
				
			||||||
 | 
					  expect: string;
 | 
				
			||||||
 | 
					  send: string;
 | 
				
			||||||
 | 
					  delay: string;
 | 
				
			||||||
 | 
					  repeat: string;
 | 
				
			||||||
 | 
					  note: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue