Sync line config updates and improve APC control

Added socket event handling to update line configuration in real time between frontend and backend. Improved APC outlet control in the terminal modal, including validation and user feedback. Refactored BottomToolBar and related components to share active tab state, and fixed command formatting. Minor bug fixes and UI improvements for terminal and scenario actions.
This commit is contained in:
nguyentrungthat 2025-11-19 16:50:12 +07:00
parent c36b9f69df
commit 1e058636b2
7 changed files with 131 additions and 26 deletions

View File

@ -4,7 +4,6 @@ import {
appendLog,
cleanData,
getLogWithTimeScenario,
getPathLog,
isValidJson,
sleep,
} from '../ultils/helper.js'
@ -45,7 +44,7 @@ interface User {
export default class LineConnection {
public client: net.Socket
public readonly config: LineConfig
public config: LineConfig
public readonly socketIO: any
private outputBuffer: string
private isRunningScript: boolean
@ -202,7 +201,7 @@ export default class LineConnection {
return
}
this.client.write(`${cmd}`)
this.client.write(cmd)
if (userName) {
// appendLog(
// `\n---${userName}---\n`,
@ -545,7 +544,7 @@ export default class LineConnection {
clearInterval(escInterval)
return
}
this.writeCommand(Buffer.from([0xff, 0xf3])) // Ctrl + Break
this.client.write(Buffer.from([0xff, 0xf3])) // Ctrl + Break
count++
}, 1)
}

View File

@ -452,6 +452,14 @@ export class WebSocketIo {
socket.on('update_ticket', async (data) => {
io.emit('update_ticket', data)
})
socket.on('update_line_value', async (data) => {
const { lineId, update } = data
const line = this.lineMap.get(lineId)
if (line) {
line.config = { ...line.config, ...update }
}
})
})
socketServer.listen(SOCKET_IO_PORT, () => {

View File

@ -78,6 +78,7 @@ function App() {
const [testLogContent, setTestLogContent] = useState("");
const [isLogModalOpen, setIsLogModalOpen] = useState(false);
const [expandedBottomBar, setExpandedBottomBar] = useState(true);
const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
const connectApcSwitch = (station: TStation) => {
if (station?.apc_1_ip && station?.apc_1_port) {
@ -507,11 +508,25 @@ function App() {
setTestLogContent={setTestLogContent}
scenarios={scenarios}
setExpanded={setExpandedBottomBar}
activeTabBottom={activeTabBottom}
setActiveTabBottom={setActiveTabBottom}
/>
</Flex>
</Tabs.Panel>
))}
onChange={(id) => {
if (selectedLines.length > 0) {
selectedLines.forEach((el) => {
if (
el?.userOpenCLI === user?.userName &&
!selectedLines.find((value) => value.id === el?.id)
)
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el?.station_id,
});
});
}
setActiveTab(id?.toString() || "0");
setSelectedLines([]);
setLoadingTerminal(false);
@ -560,6 +575,7 @@ function App() {
}, 100);
}}
stations={stations}
socket={socket}
/>
<ModalTerminal

View File

@ -35,6 +35,8 @@ interface TabsProps {
setTestLogContent: (value: React.SetStateAction<string>) => void;
scenarios: IScenario[];
setExpanded: (value: React.SetStateAction<boolean>) => void;
activeTabBottom: string;
setActiveTabBottom: (value: React.SetStateAction<string>) => void;
}
const BottomToolBar = ({
@ -50,6 +52,8 @@ const BottomToolBar = ({
setTestLogContent,
scenarios,
setExpanded,
setActiveTabBottom,
activeTabBottom,
}: TabsProps) => {
const user = useMemo(() => {
return localStorage.getItem("user") &&
@ -58,7 +62,7 @@ const BottomToolBar = ({
: null;
}, []);
const [valueInput, setValueInput] = useState<string>("");
const [activeTabBottom, setActiveBottom] = useState<string>("command");
// const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
const [isExpand, setIsExpand] = useState<boolean>(true);
return (
@ -107,7 +111,7 @@ const BottomToolBar = ({
orientation="vertical"
value={activeTabBottom}
onChange={(val) => {
setActiveBottom(val || "command");
setActiveTabBottom(val || "command");
}}
className={classes.containerBottom}
style={{ height: "14vh" }}
@ -238,7 +242,7 @@ const BottomToolBar = ({
socket?.emit("write_command_line_from_web", {
lineIds: listLine.map((line) => line.id),
stationId: station.id,
command: valueInput + "\n",
command: valueInput + "\r\n",
});
// setTimeout(() => {
// socket?.emit("write_command_line_from_web", {

View File

@ -19,6 +19,7 @@ import DialogConfirm from "./DialogConfirm";
import axios from "axios";
import { notifications } from "@mantine/notifications";
import { IconInputX } from "@tabler/icons-react";
import type { Socket } from "socket.io-client";
const apiUrl = import.meta.env.VITE_BACKEND_URL;
@ -38,6 +39,7 @@ const StationSetting = ({
setStations,
setActiveTab,
stations,
socket,
}: {
isOpen: boolean;
isEdit: boolean;
@ -46,6 +48,7 @@ const StationSetting = ({
setStations: (value: React.SetStateAction<TStation[]>) => void;
setActiveTab: (value: string) => void;
stations: TStation[];
socket: Socket | null;
}) => {
const [lines, setLines] = useState<TLine[]>([lineInit]);
const [openConfirm, setOpenConfirm] = useState<boolean>(false);
@ -255,9 +258,10 @@ const StationSetting = ({
});
return;
}
const lineUpdate = lines.filter((el) => el.lineNumber && el.port);
const payload = {
...form.values,
lines: lines.filter((el) => el.lineNumber && el.port),
lines: lineUpdate,
};
if (isEdit) payload.id = dataStation?.id || 0;
const url = isEdit ? "api/stations/update" : "api/stations/create";
@ -268,10 +272,41 @@ const StationSetting = ({
setStations((pre) =>
isEdit
? pre.map((el) =>
el.id === station.id ? { ...el, ...station } : el
el.id === station.id
? {
...el,
...station,
lines: dataStation?.lines?.map((el) =>
lineUpdate?.find((value) => value?.id === el.id)
? {
...el,
...lineUpdate?.find(
(value) => value?.id === el.id
),
}
: el
),
}
: el
)
: [...pre, station]
);
if (isEdit) {
lineUpdate.forEach((el) => {
socket?.emit("update_line_value", {
lineId: el.id,
update: {
port: el.port,
lineNumber: el.lineNumber,
apcName: el.apc_name || el.apcName,
outlet: el.outlet,
lineClear: el.lineClear,
interface: el.interface,
baud: el.baud,
},
});
});
}
}
notifications.show({
title: "Success",

View File

@ -272,6 +272,47 @@ const ModalTerminal = ({
}
};
const controlApc = (action: string) => {
if (!line?.outlet) {
notifications.show({
title: "Error",
message: "Hasn't config outlet number",
color: "red",
});
return;
}
const apcName = line.apcName || line.apc_name;
if (!apcName) {
notifications.show({
title: "Error",
message: "Hasn't config apc",
color: "red",
});
return;
}
if (
(apcName === "apc_1" && !stationItem?.apc_1_ip) ||
(apcName === "apc_2" && !stationItem?.apc_2_ip)
) {
notifications.show({
title: "Error",
message: "Hasn't config apc ip",
color: "red",
});
return;
}
socket?.emit("control_apc", {
outletNumbers: [line.outlet],
station: stationItem,
action: action,
apcName: line.apcName || line.apc_name,
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
};
return (
<Box>
<Modal
@ -480,7 +521,7 @@ const ModalTerminal = ({
<Menu.Target>
<Button
fw={400}
disabled={isDisable || selectedLines.length === 0}
disabled={isDisable}
variant="filled"
color="yellow"
style={{ height: "30px", width: "100px" }}
@ -503,19 +544,8 @@ const ModalTerminal = ({
<ButtonScenario
key={i}
socket={socket}
selectedLines={selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
)}
isDisable={
isDisable ||
selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
).length === 0
}
selectedLines={line ? [line] : []}
isDisable={isDisable}
onClick={() => {
// setSelectedLines([]);
setIsDisable(true);
@ -530,6 +560,7 @@ const ModalTerminal = ({
</Menu.Dropdown>
</Menu>
<Button
disabled={isDisable}
fw={400}
variant="filled"
color="green"
@ -539,6 +570,7 @@ const ModalTerminal = ({
Select license
</Button>
<Button
disabled={isDisable}
fw={400}
variant="filled"
color="green"
@ -569,31 +601,40 @@ const ModalTerminal = ({
</Button>
<Flex justify={"end"}>
<Button
disabled={isDisable}
mr={"8px"}
fw={400}
variant="outline"
color="green"
size="xs"
onClick={() => {}}
onClick={() => {
controlApc("on");
}}
>
ON
</Button>
<Button
disabled={isDisable}
mr={"8px"}
fw={400}
variant="outline"
color="red"
size="xs"
onClick={() => {}}
onClick={() => {
controlApc("off");
}}
>
OFF
</Button>
<Button
disabled={isDisable}
fw={400}
variant="outline"
color="orange"
size="xs"
onClick={() => {}}
onClick={() => {
controlApc("restart");
}}
>
Restart
</Button>

View File

@ -145,6 +145,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
setTimeout(() => {
setLoading(false);
}, 500);
} else {
setIsInit(false);
}
}, [cliOpened]);