ATC_SIMPLE/FRONTEND/src/App.tsx

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