617 lines
19 KiB
TypeScript
617 lines
19 KiB
TypeScript
import "@mantine/core/styles.css";
|
|
import "@mantine/dates/styles.css";
|
|
import "@mantine/notifications/styles.css";
|
|
import "./App.css";
|
|
import classes from "./App.module.css";
|
|
|
|
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
|
import {
|
|
Tabs,
|
|
Text,
|
|
Container,
|
|
Flex,
|
|
MantineProvider,
|
|
Grid,
|
|
ScrollArea,
|
|
LoadingOverlay,
|
|
} from "@mantine/core";
|
|
import type {
|
|
IDataTakeOver,
|
|
IScenario,
|
|
ReceivedFile,
|
|
ResponseData,
|
|
TLine,
|
|
TStation,
|
|
TUser,
|
|
} from "./untils/types";
|
|
import axios from "axios";
|
|
import CardLine from "./components/CardLine";
|
|
import { SocketProvider, useSocket } from "./context/SocketContext";
|
|
import // ButtonConnect,
|
|
// ButtonControlApc,
|
|
// ButtonCopy,
|
|
// ButtonDPELP,
|
|
// ButtonScenario,
|
|
// ButtonSelect,
|
|
"./components/ButtonAction";
|
|
import StationSetting from "./components/FormAddEdit";
|
|
// import DrawerScenario from "./components/DrawerScenario";
|
|
import { Notifications } from "@mantine/notifications";
|
|
import ModalTerminal from "./components/ModalTerminal";
|
|
import PageLogin from "./components/Authentication/LoginPage";
|
|
// import DrawerLogs from "./components/DrawerLogs";
|
|
import DraggableTabs from "./components/DragTabs";
|
|
import { isJsonString } from "./untils/helper";
|
|
import BottomToolBar from "./components/BottomToolBar";
|
|
|
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
|
|
|
/**
|
|
* Main Component
|
|
*/
|
|
function App() {
|
|
const user = useMemo(() => {
|
|
return localStorage.getItem("user") &&
|
|
isJsonString(localStorage.getItem("user"))
|
|
? JSON.parse(localStorage.getItem("user") || "")
|
|
: null;
|
|
}, []);
|
|
|
|
if (!user) {
|
|
localStorage.removeItem("user");
|
|
window.location.href = "/";
|
|
}
|
|
|
|
document.title = "Automation Test";
|
|
const { socket } = useSocket();
|
|
const [stations, setStations] = useState<TStation[]>([]);
|
|
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
|
const [activeTab, setActiveTab] = useState("0");
|
|
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[]>([]);
|
|
const [openModalTerminal, setOpenModalTerminal] = useState(false);
|
|
const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
|
|
const [loadingTerminal, setLoadingTerminal] = useState(true);
|
|
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
|
|
const [disableRequestTakeOver, setDisableRequestTakeOver] = useState(false);
|
|
const [countDownRequest, setCountDownRequest] = useState(0);
|
|
const [dataRequestTakeOver, setDataRequestTakeOver] =
|
|
useState<IDataTakeOver>();
|
|
const [testLogContent, setTestLogContent] = useState("");
|
|
const [isLogModalOpen, setIsLogModalOpen] = useState(false);
|
|
|
|
const connectApcSwitch = (station: TStation) => {
|
|
if (station?.apc_1_ip && station?.apc_1_port) {
|
|
socket?.emit("connect_apc", {
|
|
station: station,
|
|
apcIp: station?.apc_1_ip,
|
|
apcName: "apc_1",
|
|
});
|
|
}
|
|
if (station?.apc_2_ip && station?.apc_2_port) {
|
|
socket?.emit("connect_apc", {
|
|
station: station,
|
|
apcIp: station?.apc_2_ip,
|
|
apcName: "apc_2",
|
|
});
|
|
}
|
|
if (station?.switch_control_ip && station?.switch_control_port) {
|
|
socket?.emit("connect_switch", {
|
|
station: station,
|
|
ip: station?.switch_control_ip,
|
|
});
|
|
}
|
|
};
|
|
|
|
// function get list station
|
|
const getStation = async () => {
|
|
try {
|
|
const response = await axios.get(apiUrl + "api/stations");
|
|
if (response.status) {
|
|
if (Array.isArray(response.data)) {
|
|
setStations(response.data);
|
|
response.data.forEach((station) => {
|
|
connectApcSwitch(station);
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log("Error get station", error);
|
|
}
|
|
};
|
|
|
|
// 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(() => {
|
|
if (!socket) return;
|
|
getStation();
|
|
getScenarios();
|
|
}, [socket]);
|
|
|
|
useEffect(() => {
|
|
if (!socket || !stations?.length) return;
|
|
|
|
socket.on("line_connected", (data) =>
|
|
updateValueLineStation(
|
|
data?.lineId,
|
|
{ status: data.status },
|
|
data?.stationId
|
|
)
|
|
);
|
|
|
|
socket.on("line_disconnected", (data) =>
|
|
updateValueLineStation(
|
|
data?.lineId,
|
|
{ status: data.status },
|
|
data?.stationId
|
|
)
|
|
);
|
|
|
|
socket?.on("line_output", (data) => {
|
|
updateValueLineStation(
|
|
data?.lineId,
|
|
{ netOutput: data.data, commands: data.commands || [] },
|
|
data?.stationId
|
|
);
|
|
});
|
|
|
|
socket?.on("line_error", (data) => {
|
|
updateValueLineStation(
|
|
data?.lineId,
|
|
{ netOutput: data.error },
|
|
data?.stationId
|
|
);
|
|
});
|
|
|
|
socket?.on("init", (data) => {
|
|
if (Array.isArray(data)) {
|
|
data.forEach((value) => {
|
|
updateValueLineStation(
|
|
value?.id,
|
|
{ ...value, netOutput: value.output },
|
|
value?.stationId
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
socket?.on("user_connecting", (data) => {
|
|
if (Array.isArray(data)) {
|
|
setUsersConnecting(data);
|
|
}
|
|
});
|
|
|
|
socket?.on("user_open_cli", (data) => {
|
|
setTimeout(() => {
|
|
updateValueLineStation(
|
|
data.lineId,
|
|
{
|
|
cliOpened: true,
|
|
userEmailOpenCLI: data.userEmailOpenCLI,
|
|
userOpenCLI: data.userOpenCLI,
|
|
},
|
|
data?.stationId
|
|
);
|
|
}, 100);
|
|
});
|
|
|
|
socket?.on("user_close_cli", (data) => {
|
|
setTimeout(() => {
|
|
updateValueLineStation(
|
|
data.lineId,
|
|
{
|
|
cliOpened: false,
|
|
userEmailOpenCLI: undefined,
|
|
userOpenCLI: undefined,
|
|
},
|
|
data?.stationId
|
|
);
|
|
}, 100);
|
|
});
|
|
|
|
socket?.on("confirm_take_over", (data) => {
|
|
setDataRequestTakeOver(data);
|
|
if (data?.userEmail !== user?.email) {
|
|
setCountDownRequest(20);
|
|
const intervalCount = setInterval(() => {
|
|
setCountDownRequest((prev) => {
|
|
if (prev <= 1) {
|
|
setDataRequestTakeOver(undefined);
|
|
clearInterval(intervalCount);
|
|
return 0;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
if (!data?.userEmail) setCountDownRequest(0);
|
|
});
|
|
|
|
const receivedFiles: Record<string, ReceivedFile> = {};
|
|
socket?.on("response_content_log", (data: ResponseData) => {
|
|
if (!data.chunk) {
|
|
const decoder = new TextDecoder("utf-8");
|
|
const str = decoder.decode(data as unknown as ArrayBuffer);
|
|
setTestLogContent(str);
|
|
return;
|
|
}
|
|
|
|
const { fileId, chunkIndex, totalChunks, chunk } = data.chunk;
|
|
|
|
if (!receivedFiles[fileId]) {
|
|
receivedFiles[fileId] = {
|
|
chunks: [],
|
|
receivedChunks: 0,
|
|
totalChunks,
|
|
};
|
|
}
|
|
|
|
let bufferChunk: Buffer;
|
|
|
|
if (chunk instanceof ArrayBuffer) {
|
|
bufferChunk = Buffer.from(new Uint8Array(chunk)); // ✅ convert properly
|
|
} else if (chunk instanceof Uint8Array) {
|
|
bufferChunk = Buffer.from(chunk); // ✅ direct support
|
|
} else {
|
|
bufferChunk = chunk as Buffer; // fallback if server sends Buffer
|
|
}
|
|
|
|
receivedFiles[fileId].chunks[chunkIndex] = bufferChunk;
|
|
receivedFiles[fileId].receivedChunks++;
|
|
|
|
if (receivedFiles[fileId].receivedChunks === totalChunks) {
|
|
const fileBuffer = Buffer.concat(receivedFiles[fileId].chunks);
|
|
const decoder = new TextDecoder("utf-8");
|
|
const str = decoder.decode(fileBuffer);
|
|
|
|
setTestLogContent(str);
|
|
delete receivedFiles[fileId]; // cleanup ✅
|
|
}
|
|
});
|
|
|
|
socket?.on("data_textfsm", (data) => {
|
|
setTimeout(() => {
|
|
updateValueLineStation(
|
|
data.lineId,
|
|
{
|
|
data: data.data,
|
|
inventory: data.inventory,
|
|
latestScenario: data.latestScenario,
|
|
},
|
|
data?.stationId
|
|
);
|
|
}, 100);
|
|
});
|
|
|
|
// ✅ cleanup on unmount or when socket changes
|
|
return () => {
|
|
socket.off("init");
|
|
socket.off("line_output");
|
|
socket.off("line_error");
|
|
socket.off("line_connected");
|
|
socket.off("line_disconnected");
|
|
socket.off("user_connecting");
|
|
socket.off("user_open_cli");
|
|
socket.off("user_close_cli");
|
|
socket.off("confirm_take_over");
|
|
socket.off("response_content_log");
|
|
socket.off("data_textfsm");
|
|
};
|
|
}, [socket, stations, selectedLine]);
|
|
|
|
const updateValueLineStation = useCallback(
|
|
(lineId: number, updates: Partial<TLine>, stationId?: number) => {
|
|
setStations((prevStations) =>
|
|
prevStations?.map((station: TStation) =>
|
|
station.id === stationId
|
|
? {
|
|
...station,
|
|
lines: station.lines?.map((lineItem: TLine) => {
|
|
if (lineItem.id !== lineId) return lineItem;
|
|
|
|
const isNetOutput = typeof updates?.netOutput !== "undefined";
|
|
|
|
return {
|
|
...lineItem,
|
|
...updates,
|
|
...(isNetOutput && {
|
|
netOutput:
|
|
(lineItem.netOutput || "") + (updates.netOutput || ""),
|
|
output: updates.netOutput, // Nếu netOutput thì update luôn output
|
|
loadingOutput: lineItem.loadingOutput ? false : true,
|
|
}),
|
|
};
|
|
}),
|
|
}
|
|
: station
|
|
)
|
|
);
|
|
|
|
// Update selectedLine nếu nó đang được chọn
|
|
setSelectedLine((prevSelected) => {
|
|
if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
|
|
|
|
const isNetOutput = typeof updates?.netOutput !== "undefined";
|
|
|
|
return {
|
|
...prevSelected,
|
|
...updates,
|
|
...(isNetOutput && {
|
|
netOutput:
|
|
(prevSelected.netOutput || "") + (updates.netOutput || ""),
|
|
output: updates.netOutput,
|
|
loadingOutput: prevSelected.loadingOutput ? false : true,
|
|
}),
|
|
};
|
|
});
|
|
},
|
|
[]
|
|
);
|
|
|
|
// const getLine = (lineId: number, stationId: number) => {
|
|
// const station = stations?.find((sta) => sta.id === stationId);
|
|
// if (station) {
|
|
// const line = station.lines?.find((li) => li.id === lineId);
|
|
// return line;
|
|
// } else return null;
|
|
// };
|
|
|
|
const openTerminal = (line: TLine) => {
|
|
setOpenModalTerminal(true);
|
|
const data = { ...line };
|
|
if (!line.userEmailOpenCLI) {
|
|
data.cliOpened = true;
|
|
data.userEmailOpenCLI = user?.email;
|
|
data.userOpenCLI = user?.userName;
|
|
socket?.emit("open_cli", {
|
|
lineId: line.id,
|
|
stationId: line.stationId || line.station_id,
|
|
userEmail: user?.email,
|
|
userName: user?.userName,
|
|
});
|
|
}
|
|
setSelectedLine(data);
|
|
};
|
|
|
|
return (
|
|
<Container w={"100%"} style={{ maxWidth: "100%" }}>
|
|
<DraggableTabs
|
|
socket={socket}
|
|
usersConnecting={usersConnecting}
|
|
setIsEditStation={setIsEditStation}
|
|
setIsOpenAddStation={setIsOpenAddStation}
|
|
setStationEdit={setStationEdit}
|
|
tabsData={stations}
|
|
panels={stations.map((station) => (
|
|
<Tabs.Panel
|
|
className={classes.content}
|
|
key={station.id}
|
|
value={station.id.toString()}
|
|
pt="md"
|
|
>
|
|
<Flex className={classes.containerMain}>
|
|
<Grid>
|
|
<Grid.Col
|
|
span={12}
|
|
style={{
|
|
borderRadius: 8,
|
|
}}
|
|
>
|
|
<ScrollArea h={"63vh"}>
|
|
{station.lines.length > 8 ? (
|
|
<Grid
|
|
style={{
|
|
marginLeft: "3%",
|
|
width: "95%",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<Grid.Col
|
|
span={6}
|
|
style={{ borderRight: "1px solid #ccc" }}
|
|
>
|
|
<Flex wrap="wrap" gap="sm" justify={"center"}>
|
|
{station.lines.slice(0, 8).map((line, i) => (
|
|
<CardLine
|
|
key={i}
|
|
socket={socket}
|
|
stationItem={station}
|
|
line={line}
|
|
selectedLines={selectedLines}
|
|
setSelectedLines={setSelectedLines}
|
|
openTerminal={openTerminal}
|
|
loadTerminal={
|
|
loadingTerminal &&
|
|
Number(station.id) === Number(activeTab)
|
|
}
|
|
/>
|
|
))}
|
|
</Flex>
|
|
</Grid.Col>
|
|
<Grid.Col span={6}>
|
|
<Flex wrap="wrap" gap="sm" justify={"center"}>
|
|
{station.lines
|
|
.slice(8, station.lines.length)
|
|
.map((line, i) => (
|
|
<CardLine
|
|
key={i}
|
|
socket={socket}
|
|
stationItem={station}
|
|
line={line}
|
|
selectedLines={selectedLines}
|
|
setSelectedLines={setSelectedLines}
|
|
openTerminal={openTerminal}
|
|
loadTerminal={
|
|
loadingTerminal &&
|
|
Number(station.id) === Number(activeTab)
|
|
}
|
|
/>
|
|
))}
|
|
</Flex>
|
|
</Grid.Col>
|
|
</Grid>
|
|
) : station.lines.length <= 8 &&
|
|
station.lines.length > 0 ? (
|
|
<Flex wrap="wrap" gap="sm" justify={"center"}>
|
|
{station.lines.map((line, i) => (
|
|
<CardLine
|
|
key={i}
|
|
socket={socket}
|
|
stationItem={station}
|
|
line={line}
|
|
selectedLines={selectedLines}
|
|
setSelectedLines={setSelectedLines}
|
|
openTerminal={openTerminal}
|
|
loadTerminal={
|
|
loadingTerminal &&
|
|
Number(station.id) === Number(activeTab)
|
|
}
|
|
/>
|
|
))}
|
|
</Flex>
|
|
) : (
|
|
<Text ta="center" c="dimmed" mt="lg">
|
|
No lines configured
|
|
</Text>
|
|
)}
|
|
</ScrollArea>
|
|
</Grid.Col>
|
|
</Grid>
|
|
<BottomToolBar
|
|
selectedLines={selectedLines}
|
|
socket={socket}
|
|
setSelectedLines={setSelectedLines}
|
|
isDisable={isDisable}
|
|
setIsDisable={setIsDisable}
|
|
station={station}
|
|
testLogContent={testLogContent}
|
|
isLogModalOpen={isLogModalOpen}
|
|
setIsLogModalOpen={setIsLogModalOpen}
|
|
setTestLogContent={setTestLogContent}
|
|
/>
|
|
</Flex>
|
|
</Tabs.Panel>
|
|
))}
|
|
onChange={(id) => {
|
|
setActiveTab(id?.toString() || "0");
|
|
setSelectedLines([]);
|
|
setLoadingTerminal(false);
|
|
setTimeout(() => {
|
|
setLoadingTerminal(true);
|
|
}, 100);
|
|
}}
|
|
setActive={setActiveTab}
|
|
active={activeTab}
|
|
onSendCommand={(value) => {
|
|
const listLine = selectedLines.length
|
|
? selectedLines
|
|
: stations.find((el) => el.id === Number(activeTab))?.lines;
|
|
if (listLine?.length) {
|
|
socket?.emit("write_command_line_from_web", {
|
|
lineIds: listLine.map((line) => line.id),
|
|
stationId: Number(activeTab),
|
|
command: value + "\n",
|
|
});
|
|
setTimeout(() => {
|
|
socket?.emit("write_command_line_from_web", {
|
|
lineIds: listLine.map((line) => line.id),
|
|
stationId: Number(activeTab),
|
|
command: " \n",
|
|
});
|
|
}, 1000);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<StationSetting
|
|
dataStation={stationEdit}
|
|
isOpen={isOpenAddStation}
|
|
onClose={() => {
|
|
setIsOpenAddStation(false);
|
|
setIsEditStation(false);
|
|
setStationEdit(undefined);
|
|
}}
|
|
isEdit={isEditStation}
|
|
setStations={setStations}
|
|
setActiveTab={(id: string) => {
|
|
setActiveTab(id);
|
|
setLoadingTerminal(false);
|
|
setTimeout(() => {
|
|
setLoadingTerminal(true);
|
|
}, 100);
|
|
}}
|
|
stations={stations}
|
|
/>
|
|
|
|
<ModalTerminal
|
|
opened={openModalTerminal}
|
|
onClose={() => {
|
|
setOpenModalTerminal(false);
|
|
setSelectedLine(undefined);
|
|
}}
|
|
line={selectedLine}
|
|
socket={socket}
|
|
stationItem={stations.find((el) => el.id === Number(activeTab))}
|
|
scenarios={scenarios}
|
|
dataRequestTakeOver={dataRequestTakeOver}
|
|
countDownRequest={countDownRequest}
|
|
setDisableRequestTakeOver={setDisableRequestTakeOver}
|
|
disableRequestTakeOver={disableRequestTakeOver}
|
|
setCountDownRequest={setCountDownRequest}
|
|
setDataRequestTakeOver={setDataRequestTakeOver}
|
|
/>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
export default function Main() {
|
|
const user = useMemo(() => {
|
|
return localStorage.getItem("user") &&
|
|
isJsonString(localStorage.getItem("user"))
|
|
? JSON.parse(localStorage.getItem("user") || "")
|
|
: null;
|
|
}, []);
|
|
|
|
return (
|
|
<MantineProvider>
|
|
<SocketProvider>
|
|
<Suspense
|
|
fallback={
|
|
<LoadingOverlay
|
|
visible={true}
|
|
zIndex={1000}
|
|
overlayProps={{ radius: "sm", blur: 1 }}
|
|
/>
|
|
}
|
|
>
|
|
<Notifications position="top-right" autoClose={5000} />
|
|
{user ? (
|
|
<App />
|
|
) : (
|
|
<Container w={"100%"} style={{ maxWidth: "100%", padding: 0 }}>
|
|
<PageLogin />
|
|
</Container>
|
|
)}
|
|
</Suspense>
|
|
</SocketProvider>
|
|
</MantineProvider>
|
|
);
|
|
}
|