Update FE

This commit is contained in:
nguyentrungthat 2025-10-27 16:55:04 +07:00
parent ef1d585b61
commit dea4d2b804
23 changed files with 1698 additions and 174 deletions

View File

@ -91,9 +91,10 @@ export default class ScenariosController {
*/
async update({ request, response, auth }: HttpContext) {
try {
const scenarioId = request.param('id')
const payload = await request.all()
const scenario = await Scenario.findOrFail(scenarioId)
const scenarioId = request.body().id
const payload = request.body()
const scenario = await Scenario.find(scenarioId)
if (!scenario) return response.status(404).json({ message: 'Scenario not found' })
payload.body = JSON.stringify(payload.body)
scenario.merge(payload)
await scenario.save()
@ -113,7 +114,7 @@ export default class ScenariosController {
*/
async delete({ request, response }: HttpContext) {
try {
const scenarioId = request.param('id')
const scenarioId = request.body().id
const scenario = await Scenario.findOrFail(scenarioId)
if (!scenario) {

View File

@ -1,5 +1,6 @@
import type { HttpContext } from '@adonisjs/core/http'
import Station from '#models/station'
import Line from '#models/line'
export default class StationsController {
public async index({}: HttpContext) {
@ -8,6 +9,8 @@ export default class StationsController {
public async store({ request, response }: HttpContext) {
let payload = request.only(Array.from(Station.$columnsDefinitions.keys()))
let lines: Line[] = request.body().lines || []
try {
const stationName = await Station.findBy('name', payload.name)
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!' })
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({
status: true,
message: 'Station created successfully',
data: station,
data: resStation.map((el) => ({ ...el.$original, lines: newLines })),
})
} catch (error) {
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'
)
)
let lines: Line[] = request.body().lines || []
try {
const station = await Station.find(request.body().id)
@ -48,7 +76,21 @@ export default class StationsController {
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) {
return response.badRequest({ error: error, message: 'Station update failed', status: false })
}

View File

@ -1,5 +1,5 @@
import net from 'node:net'
import { cleanData } from '../ultils/helper.js'
import { cleanData, sleep } from '../ultils/helper.js'
import Scenario from '#models/scenario'
interface LineConfig {
@ -59,7 +59,7 @@ export default class LineConnection {
this.client.on('data', (data) => {
if (this.connecting) return
let message = data.toString()
this.outputBuffer += message
if (this.isRunningScript) this.outputBuffer += message
// let output = cleanData(message)
// console.log(`📨 [${this.config.port}] ${message}`)
// Handle netOutput with backspace support
@ -109,7 +109,7 @@ export default class LineConnection {
console.log(`⏳ Connection timeout line ${lineNumber}`)
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) {
if (!this.client || this.client.destroyed) {
throw new Error('Not connected')
console.log('Not connected')
this.isRunningScript = false
this.outputBuffer = ''
return
}
if (this.isRunningScript) {
throw new Error('Script already running')
console.log('Script already running')
return
}
this.isRunningScript = true
@ -152,10 +156,10 @@ export default class LineConnection {
const timeoutTimer = setTimeout(() => {
this.isRunningScript = false
this.outputBuffer = ''
reject(new Error('Script timeout'))
// reject(new Error('Script timeout'))
}, script.timeout || 300000)
const runStep = (index: number) => {
const runStep = async (index: number) => {
if (index >= steps.length) {
clearTimeout(timeoutTimer)
this.isRunningScript = false
@ -166,11 +170,9 @@ export default class LineConnection {
const step = steps[index]
let repeatCount = Number(step.repeat) || 1
const sendCommand = () => {
if (repeatCount <= 0) {
// Done → next step
this.client.off('data', onOutput)
stepIndex++
return runStep(stepIndex)
}
@ -183,24 +185,19 @@ export default class LineConnection {
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
if (!step?.expect || step?.expect.trim() === '') {
setTimeout(() => sendCommand(), Number(step?.delay) || 500)
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)

View File

@ -12,3 +12,7 @@ export const cleanData = (data: string) => {
.replace(/[^\x20-\x7E\r\n]/g, '') // Remove non-printable characters
// .replace(/\r\n/g, '\n')
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

View File

@ -11,7 +11,7 @@ export default class extends BaseSchema {
table.integer('line_clear')
table.integer('outlet')
table.integer('station_id').unsigned().references('id').inTable('stations')
table.string('apc_name').notNullable()
table.string('apc_name')
table.timestamps()
})
}

View File

@ -7,7 +7,6 @@ export default class extends BaseSchema {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('title').notNullable()
table.string('name').notNullable()
table.text('body').notNullable()
table.integer('timeout').notNullable()
table.boolean('isReboot').defaultTo(false)

View File

@ -50,6 +50,7 @@ export class WebSocketIo {
intervalMap: { [key: string]: NodeJS.Timeout } = {}
stationMap: Map<number, Station> = new Map()
lineMap: Map<number, LineConnection> = new Map() // key = lineId
lineConnecting: number[] = [] // key = lineId
constructor(protected app: ApplicationService) {}
@ -94,15 +95,19 @@ export class WebSocketIo {
for (const lineId of lineIds) {
const line = this.lineMap.get(lineId)
if (line) {
this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, line)
line.writeCommand(command)
} else {
if (this.lineConnecting.includes(lineId)) continue
const linesData = await Line.findBy('id', lineId)
const stationData = await Station.findBy('id', stationId)
if (linesData && stationData) {
this.lineConnecting.push(lineId)
await this.connectLine(io, [linesData], stationData)
const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) {
this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, lineReconnect)
lineReconnect.writeCommand(command)
}
@ -131,7 +136,7 @@ export class WebSocketIo {
line.runScript(scenario)
} else {
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) {
await this.connectLine(io, [linesData], stationData)
const lineReconnect = this.lineMap.get(lineId)

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<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" />
<title>Automation Test</title>
</head>

View File

@ -10,6 +10,8 @@
"dependencies": {
"@mantine/core": "^8.3.5",
"@mantine/dates": "^8.3.5",
"@mantine/form": "^8.3.5",
"@mantine/hooks": "^8.3.5",
"@mantine/notifications": "^8.3.5",
"@tabler/icons-react": "^3.35.0",
"@xterm/addon-fit": "^0.10.0",
@ -1114,12 +1116,24 @@
"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": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
"integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@ -2631,7 +2645,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@ -3115,6 +3128,15 @@
"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": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",

View File

@ -12,6 +12,8 @@
"dependencies": {
"@mantine/core": "^8.3.5",
"@mantine/dates": "^8.3.5",
"@mantine/form": "^8.3.5",
"@mantine/hooks": "^8.3.5",
"@mantine/notifications": "^8.3.5",
"@tabler/icons-react": "^3.35.0",
"@xterm/addon-fit": "^0.10.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -4,7 +4,7 @@ import "@mantine/notifications/styles.css";
import "./App.css";
import classes from "./App.module.css";
import { useEffect, useState } from "react";
import { Suspense, useEffect, useState } from "react";
import {
Tabs,
Text,
@ -16,20 +16,24 @@ import {
ScrollArea,
Button,
ActionIcon,
LoadingOverlay,
} 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 CardLine from "./components/CardLine";
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
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;
/**
* Main Component
*/
export function App() {
function App() {
document.title = "Automation Test";
const { socket } = useSocket();
const [stations, setStations] = useState<TStation[]>([]);
@ -45,6 +49,10 @@ export function App() {
};
const [showBottomShadow, setShowBottomShadow] = 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
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(() => {
getStation();
getScenarios();
}, []);
useEffect(() => {
@ -121,7 +144,6 @@ export function App() {
return (
<Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
{/* Tabs (Top Bar) */}
<Tabs
value={activeTab}
onChange={(id) => setActiveTab(id?.toString() || "0")}
@ -146,14 +168,35 @@ export function App() {
className={classes.indicator}
/>
<Flex gap={"sm"}>
<ActionIcon title="Add Station" variant="outline" color="green">
<IconSettingsPlus />
</ActionIcon>
{Number(activeTab) && (
<ActionIcon title="Edit Station" variant="outline">
{Number(activeTab) ? (
<ActionIcon
title="Edit Station"
variant="outline"
onClick={() => {
setStationEdit(
stations.find((el) => el.id === Number(activeTab))
);
setIsOpenAddStation(true);
setIsEditStation(true);
}}
>
<IconEdit />
</ActionIcon>
) : (
""
)}
<ActionIcon
title="Add Station"
variant="outline"
color="green"
onClick={() => {
setIsOpenAddStation(true);
setIsEditStation(false);
setStationEdit(undefined);
}}
>
<IconSettingsPlus />
</ActionIcon>
</Flex>
</Tabs.List>
@ -249,6 +292,11 @@ export function App() {
>
Connect
</Button>
<hr style={{ width: "100%" }} />
<DrawerScenario
scenarios={scenarios}
setScenarios={setScenarios}
/>
<ButtonDPELP
socket={socket}
selectedLines={selectedLines}
@ -261,12 +309,42 @@ export function App() {
}, 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>
</Grid.Col>
</Grid>
</Tabs.Panel>
))}
</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>
);
}
@ -275,7 +353,18 @@ export default function Main() {
return (
<MantineProvider>
<SocketProvider>
<App />
<Suspense
fallback={
<LoadingOverlay
visible={true}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 1 }}
/>
}
>
<Notifications position="top-right" autoClose={5000} />
<App />
</Suspense>
</SocketProvider>
</MantineProvider>
);

View File

@ -1,6 +1,7 @@
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 classes from "./Component.module.css";
export const ButtonDPELP = ({
socket,
@ -18,7 +19,7 @@ export const ButtonDPELP = ({
disabled={isDisable}
miw={"100px"}
// radius="lg"
h={"24px"}
h={"28px"}
mr={"5px"}
variant="filled"
color="#00a164"
@ -130,3 +131,42 @@ export const ButtonDPELP = ({
</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>
);
};

View File

@ -49,7 +49,7 @@ const CardLine = ({
>
<div>
<Text fw={600} style={{ display: "flex", gap: "4px" }}>
Line {line.lineNumber} - {line.port}{" "}
Line: {line.lineNumber || line.line_number} - {line.port}{" "}
{line.status === "connected" && (
<IconCircleCheckFilled color="green" />
)}

View File

@ -13,4 +13,8 @@
gap: 4px;
margin-top: 4px;
height: 20px;
}
.buttonScenario :global(.mantine-Button-label) {
white-space: normal !important;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -10,237 +10,237 @@ import {
IconSettingsAutomation,
IconKey,
IconClipboardList,
} from '@tabler/icons-react'
} from "@tabler/icons-react";
export const SOCKET_EVENTS = {
ROOM: {
JOINED: 'room_joined',
LEFT: 'room_left',
JOINED: "room_joined",
LEFT: "room_left",
},
APP_STATUS: { RECEIVED: 'app_status_received' },
APP_STATUS: { RECEIVED: "app_status_received" },
APP_DATA: {
SENT: 'app_data_sent',
RECEIVED: 'app_data_received',
SENT: "app_data_sent",
RECEIVED: "app_data_received",
},
SCRIPT_TEST: {
SENT: 'script_test_sent',
TIME_RECEIVED: 'script_test_time_received',
SENT: "script_test_sent",
TIME_RECEIVED: "script_test_time_received",
},
DATA_OUTPUT: { RECEIVED: 'data_output_received' },
DATA_OUTPUT: { RECEIVED: "data_output_received" },
NOTIFICATION: {
FROM_APP: 'notification_send_from_app',
SEND_ALL: 'notification_send_to_all',
FROM_APP: "notification_send_from_app",
SEND_ALL: "notification_send_to_all",
},
APC_CONTROL: {
FROM_WEB: 'apc_control_request_from_web',
TO_APP: 'apc_control_request_to_app',
FROM_WEB_ALL_APC: 'all_apc_control_request_from_web',
FROM_WEB: "apc_control_request_from_web",
TO_APP: "apc_control_request_to_app",
FROM_WEB_ALL_APC: "all_apc_control_request_from_web",
},
SYSTEM_LOG: {
FROM_APP: 'system_log_send_from_app',
GET_SYSTEM_LOG_FROM_WEB: 'get_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_SYSTEM_LOG_FROM_APP: 'response_system_log_from_app',
RESPONSE_SYSTEM_LOG_TO_WEB: 'response_system_log_to_web',
FROM_APP: "system_log_send_from_app",
GET_SYSTEM_LOG_FROM_WEB: "get_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_SYSTEM_LOG_FROM_APP: "response_system_log_from_app",
RESPONSE_SYSTEM_LOG_TO_WEB: "response_system_log_to_web",
},
CLI: {
OPEN_CLI_LINE_FROM_WEB: 'open_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',
CLOSE_CLI_MULTI_LINE_FROM_WEB: 'close_cli_multi_line_from_web',
WRITE_COMMAND_FROM_WEB: 'write_command_line_from_web',
WRITE_COMMAND_TO_APP: 'write_command_line_to_app',
RECEIVE_COMMAND_DATA_FROM_APP: 'receive_command_data_from_app',
RECEIVE_COMMAND_DATA_TO_WEB: 'receive_command_data_to_web',
OPEN_CLI_LINE_FROM_WEB: "open_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",
CLOSE_CLI_MULTI_LINE_FROM_WEB: "close_cli_multi_line_from_web",
WRITE_COMMAND_FROM_WEB: "write_command_line_from_web",
WRITE_COMMAND_TO_APP: "write_command_line_to_app",
RECEIVE_COMMAND_DATA_FROM_APP: "receive_command_data_from_app",
RECEIVE_COMMAND_DATA_TO_WEB: "receive_command_data_to_web",
},
RESCAN: {
SEND_LIST_RESCAN_FROM_WEB: 'send_list_rescan_from_web',
SEND_LIST_RESCAN_TO_APP: 'send_list_rescan_to_app',
SEND_LIST_RESCAN_FROM_WEB: "send_list_rescan_from_web",
SEND_LIST_RESCAN_TO_APP: "send_list_rescan_to_app",
},
LOCK: {
SEND_LIST_LOCK_FROM_WEB: 'send_list_lock_from_web',
SEND_LIST_LOCK_TO_APP: 'send_list_lock_to_app',
SEND_LIST_LOCK_FROM_WEB: "send_list_lock_from_web",
SEND_LIST_LOCK_TO_APP: "send_list_lock_to_app",
},
CHANGE_STAGE: {
SEND_STAGE_FROM_WEB: 'send_stage_from_web',
SEND_STAGE_TO_APP: 'send_stage_to_app',
SEND_STAGE_FROM_WEB: "send_stage_from_web",
SEND_STAGE_TO_APP: "send_stage_to_app",
},
UPDATE_PROPERTY: {
UPDATE_PROPERTY_FROM_WEB: 'update_property_from_web',
UPDATE_PROPERTY_TO_APP: 'update_property_to_app',
UPDATE_PROPERTY_FROM_WEB: "update_property_from_web",
UPDATE_PROPERTY_TO_APP: "update_property_to_app",
},
RUN_SCENARIOS: {
RUN_SCENARIOS_FROM_WEB: 'run_scenarios_from_web',
RUN_SCENARIOS_TO_APP: 'run_scenarios_to_app',
RUN_SCENARIOS_FROM_WEB: "run_scenarios_from_web",
RUN_SCENARIOS_TO_APP: "run_scenarios_to_app",
},
SEND_BREAK: {
SEND_BREAK_FROM_WEB: 'send_break_from_web',
SEND_BREAK_TO_APP: 'send_break_to_app',
SEND_BREAK_FROM_WEB: "send_break_from_web",
SEND_BREAK_TO_APP: "send_break_to_app",
},
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_FROM_WEB: 'leave_multi_room_from_web',
LEAVE_MULTI_ROOM_FROM_WEB: "leave_multi_room_from_web",
},
TAKE_OVER: {
TAKE_OVER_FROM_WEB: 'take_over_from_web',
TAKE_OVER_TO_WEB: 'take_over_to_web',
TAKE_OVER_FROM_WEB: "take_over_from_web",
TAKE_OVER_TO_WEB: "take_over_to_web",
},
CONNECT_APC: {
CONNECT_APC_FROM_WEB: 'connect_apc_from_web',
CONNECT_APC_TO_APP: 'connect_apc_to_app',
CONNECT_APC_FROM_WEB: "connect_apc_from_web",
CONNECT_APC_TO_APP: "connect_apc_to_app",
},
DATA_APC_RECEIVED: {
DATA_APC_RECEIVED_FROM_APP: 'data_apc_received_from_app',
DATA_APC_RECEIVED_TO_WEB: 'data_apc_received_to_web',
DATA_APC_RECEIVED_FROM_APP: "data_apc_received_from_app",
DATA_APC_RECEIVED_TO_WEB: "data_apc_received_to_web",
},
SEND_COMMAND_TO_APC: {
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_FROM_WEB: "send_command_to_apc_from_web",
SEND_COMMAND_TO_APC_TO_APP: "send_command_to_apc_to_app",
},
SEND_CLEAR_LINE: {
SEND_CLEAR_LINE_FROM_WEB: 'send_clear_line_from_web',
SEND_CLEAR_LINE_TO_APP: 'send_clear_line_to_app',
SEND_CLEAR_LINE_FROM_WEB: "send_clear_line_from_web",
SEND_CLEAR_LINE_TO_APP: "send_clear_line_to_app",
},
SEND_CLOSE_LINE: {
SEND_CLOSE_LINE_FROM_WEB: 'send_close_line_from_web',
SEND_CLOSE_LINE_TO_APP: 'send_close_line_to_app',
SEND_CLOSE_LINE_FROM_WEB: "send_close_line_from_web",
SEND_CLOSE_LINE_TO_APP: "send_close_line_to_app",
},
SEND_OPEN_LINE: {
SEND_OPEN_LINE_FROM_WEB: 'send_open_line_from_web',
SEND_OPEN_LINE_TO_APP: 'send_open_line_to_app',
SEND_OPEN_LINE_FROM_WEB: "send_open_line_from_web",
SEND_OPEN_LINE_TO_APP: "send_open_line_to_app",
},
CONTROL_APP: {
SEND_PAUSE_APP_FROM_WEB: 'send_pause_app_from_web',
SEND_RESUME_APP_FROM_WEB: 'send_resume_app_from_web',
SEND_RESTART_APP_FROM_WEB: 'send_restart_app_from_web',
SEND_QUIT_APP_FROM_WEB: 'send_quit_app_from_web',
SEND_PAUSE_APP_FROM_WEB: "send_pause_app_from_web",
SEND_RESUME_APP_FROM_WEB: "send_resume_app_from_web",
SEND_RESTART_APP_FROM_WEB: "send_restart_app_from_web",
SEND_QUIT_APP_FROM_WEB: "send_quit_app_from_web",
},
CONNECT_SWITCH: {
CONNECT_SWITCH_FROM_WEB: 'connect_switch_from_web',
CONNECT_SWITCH_TO_APP: 'connect_switch_to_app',
CONNECT_SWITCH_FROM_WEB: "connect_switch_from_web",
CONNECT_SWITCH_TO_APP: "connect_switch_to_app",
},
DATA_SWITCH_RECEIVED: {
DATA_SWITCH_RECEIVED_FROM_APP: 'data_switch_received_from_app',
DATA_SWITCH_RECEIVED_TO_WEB: 'data_switch_received_to_web',
DATA_SWITCH_RECEIVED_FROM_APP: "data_switch_received_from_app",
DATA_SWITCH_RECEIVED_TO_WEB: "data_switch_received_to_web",
},
SEND_COMMAND_TO_SWITCH: {
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_FROM_APP: 'send_command_to_switch_from_app',
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_FROM_APP: "send_command_to_switch_from_app",
},
RELOAD_TICKET: {
RELOAD_TICKET_FROM_WEB: 'reload_ticket_from_web',
RELOAD_TICKET_TO_WEB: 'reload_ticket_to_web',
RELOAD_TICKET_FROM_WEB: "reload_ticket_from_web",
RELOAD_TICKET_TO_WEB: "reload_ticket_to_web",
},
}
};
export const LINE_STATUS = {
CHECK_INVENTORY: 'CHECK_INVENTORY',
STATUS_TEST: 'TESTING',
CONNECT_FAIL: 'CONNECT_FAIL',
CONNECTED: 'CONNECTED',
STATUS_READY: 'READY',
STATUS_DONE: 'DONE',
STATUS_CHECKING: 'CHECKING',
STATUS_LOCKED: 'LOCKED',
STATUS_CLOSED: 'CLOSED',
STATUS_TIMEOUT: 'TIMEOUT',
STATUS_PHYSICAL_TEST: 'STATUS_PHYSICAL_TEST',
STATUS_PHYSICAL_TEST_DONE: 'STATUS_PHYSICAL_TEST_DONE',
STATUS_UNDIFINED_INVEN: 'INVENTORY_UNIDENTIFIED',
STATUS_RUNNING_SCENARIOS: 'RUNNING_SCENARIOS',
APC_CONTROL: 'APC_CONTROL',
STATUS_STARTING: 'STARTING',
STATUS_TURN_OFF: 'TURN_OFF',
STATUS_RESTARTING: 'RESTARTING',
}
CHECK_INVENTORY: "CHECK_INVENTORY",
STATUS_TEST: "TESTING",
CONNECT_FAIL: "CONNECT_FAIL",
CONNECTED: "CONNECTED",
STATUS_READY: "READY",
STATUS_DONE: "DONE",
STATUS_CHECKING: "CHECKING",
STATUS_LOCKED: "LOCKED",
STATUS_CLOSED: "CLOSED",
STATUS_TIMEOUT: "TIMEOUT",
STATUS_PHYSICAL_TEST: "STATUS_PHYSICAL_TEST",
STATUS_PHYSICAL_TEST_DONE: "STATUS_PHYSICAL_TEST_DONE",
STATUS_UNDIFINED_INVEN: "INVENTORY_UNIDENTIFIED",
STATUS_RUNNING_SCENARIOS: "RUNNING_SCENARIOS",
APC_CONTROL: "APC_CONTROL",
STATUS_STARTING: "STARTING",
STATUS_TURN_OFF: "TURN_OFF",
STATUS_RESTARTING: "RESTARTING",
};
export const LIST_FAVORITE_COMMANDS = [
'sh inv',
'sh ver',
"sh inv",
"sh ver",
// 'sh diag',
// 'sh post',
// 'sh env',
// 'sh log',
// 'sh platform',
]
];
export const dataPermission = [
{
link: '/dashboard',
label: 'Dashboard',
link: "/dashboard",
label: "Dashboard",
icon: IconHome,
requiredPermissions: [],
},
{
link: '/station-setting',
label: 'Station Setting',
link: "/station-setting",
label: "Station Setting",
icon: IconServer,
requiredPermissions: ['station_activity'],
requiredPermissions: ["station_activity"],
},
{
link: '/monitor',
label: 'Monitoring',
link: "/monitor",
label: "Monitoring",
icon: IconDeviceDesktop,
requiredPermissions: [
'monitor_power',
'monitor_cli',
'monitor_other_items',
"monitor_power",
"monitor_cli",
"monitor_other_items",
],
},
{
link: '/control-apc',
label: 'Control APC',
link: "/control-apc",
label: "Control APC",
icon: IconSettingsAutomation,
requiredPermissions: ['control_apc_activity'],
requiredPermissions: ["control_apc_activity"],
},
{
link: '/group-model',
label: 'Group - Model',
link: "/group-model",
label: "Group - Model",
icon: IconRouter,
requiredPermissions: ['group_model_activity'],
requiredPermissions: ["group_model_activity"],
},
{
link: '/keyword',
label: 'Keyword',
link: "/keyword",
label: "Keyword",
icon: IconKey,
requiredPermissions: ['keyword_activity', 'keyword_limit'],
requiredPermissions: ["keyword_activity", "keyword_limit"],
},
{
link: '/exclude-error',
label: 'Exclude Errors',
link: "/exclude-error",
label: "Exclude Errors",
icon: IconBan,
requiredPermissions: ['exclude_error_activity', 'exclude_error_limit'],
requiredPermissions: ["exclude_error_activity", "exclude_error_limit"],
},
{
link: '/list-logs',
label: 'List Logs',
link: "/list-logs",
label: "List Logs",
icon: IconFile,
requiredPermissions: [],
},
{
link: '/webhooks',
label: 'Webhooks',
link: "/webhooks",
label: "Webhooks",
icon: IconWebhook,
requiredPermissions: ['webhook_activity', 'webhook_add_limit'],
requiredPermissions: ["webhook_activity", "webhook_add_limit"],
},
{
link: '/scenario',
label: 'Scenario',
link: "/scenario",
label: "Scenario",
icon: IconClipboardList,
requiredPermissions: ['scenario_activity', 'scenario_add_limit'],
requiredPermissions: ["scenario_activity", "scenario_add_limit"],
},
{
link: '/upgrade',
label: 'Upgrade now!',
link: "/upgrade",
label: "Upgrade now!",
icon: IconCrown,
requiredPermissions: [],
},
]
];

View File

@ -0,0 +1,4 @@
export const numberOnly = (value: string): string => {
const matched = value.match(/[\d.]+/g);
return matched ? matched.join("") : "";
};

View File

@ -58,9 +58,10 @@ export type TLine = {
id?: number;
port: number;
lineNumber: number;
line_number?: number;
lineClear: number;
line_clear?: number;
station_id: number;
is_active: string | boolean;
data?: string | any;
type?: string;
inventory?: any;
@ -69,7 +70,6 @@ export type TLine = {
outlet?: number;
cliOpened?: boolean;
systemLogUrl?: string;
start_round_at: number;
apc_name: string;
created_at?: string; // or use Date if you're working with Date objects
updated_at?: string; // or use Date if you're working with Date objects
@ -138,3 +138,20 @@ export type LineConfig = {
output: 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;
};