Update
This commit is contained in:
parent
dea4d2b804
commit
0a0dd559f0
|
|
@ -18,6 +18,7 @@
|
|||
"axios": "^1.12.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"xterm": "^5.3.0"
|
||||
},
|
||||
|
|
@ -2216,6 +2217,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -3595,6 +3605,44 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
|
||||
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
|
||||
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
|
|
@ -3753,6 +3801,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"axios": "^1.12.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"xterm": "^5.3.0"
|
||||
},
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
|
|
@ -3,7 +3,7 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: 'Mulish', sans-serif;
|
||||
font-family: "Mulish", sans-serif;
|
||||
}
|
||||
|
||||
.list {
|
||||
|
|
@ -48,7 +48,25 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.content{
|
||||
.content {
|
||||
width: 100%;
|
||||
border-top: 1px #ccc solid;
|
||||
}
|
||||
}
|
||||
|
||||
.userName {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.userName::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: #007bff; /* blue accent */
|
||||
margin: 0.1rem auto 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import "@mantine/notifications/styles.css";
|
|||
import "./App.css";
|
||||
import classes from "./App.module.css";
|
||||
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Tabs,
|
||||
Text,
|
||||
|
|
@ -17,8 +17,16 @@ import {
|
|||
Button,
|
||||
ActionIcon,
|
||||
LoadingOverlay,
|
||||
Avatar,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import type { IScenario, LineConfig, TLine, TStation } from "./untils/types";
|
||||
import type {
|
||||
IScenario,
|
||||
LineConfig,
|
||||
TLine,
|
||||
TStation,
|
||||
TUser,
|
||||
} from "./untils/types";
|
||||
import axios from "axios";
|
||||
import CardLine from "./components/CardLine";
|
||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
||||
|
|
@ -27,6 +35,8 @@ import { ButtonDPELP, ButtonScenario } from "./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";
|
||||
|
||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
|
|
@ -34,6 +44,14 @@ const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
|||
* Main Component
|
||||
*/
|
||||
function App() {
|
||||
const user = useMemo(() => {
|
||||
return localStorage.getItem("user") &&
|
||||
typeof localStorage.getItem("user") === "string"
|
||||
? JSON.parse(localStorage.getItem("user") || "")
|
||||
: null;
|
||||
}, []);
|
||||
if (!user) window.location.href = "/";
|
||||
|
||||
document.title = "Automation Test";
|
||||
const { socket } = useSocket();
|
||||
const [stations, setStations] = useState<TStation[]>([]);
|
||||
|
|
@ -53,6 +71,10 @@ function App() {
|
|||
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[]>([]);
|
||||
|
||||
// function get list station
|
||||
const getStation = async () => {
|
||||
|
|
@ -93,24 +115,74 @@ function App() {
|
|||
if (!socket || !stations?.length) return;
|
||||
|
||||
socket.on("line_connected", updateStatus);
|
||||
|
||||
socket.on("line_disconnected", updateStatus);
|
||||
|
||||
socket?.on("line_output", (data) => {
|
||||
updateValueLineStation(data?.lineId, "netOutput", data.data);
|
||||
});
|
||||
|
||||
socket?.on("line_error", (data) => {
|
||||
updateValueLineStation(data?.lineId, "netOutput", data.error);
|
||||
});
|
||||
|
||||
socket?.on("init", (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((value) => {
|
||||
updateValueLineStation(value?.id, "netOutput", value.output);
|
||||
updateStatus({ ...value, lineId: value.id });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket?.on("user_connecting", (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
setUsersConnecting(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket?.on("user_open_cli", (data) => {
|
||||
setTimeout(() => {
|
||||
updateValueLineStation(data?.lineId, "cliOpened", true);
|
||||
updateValueLineStation(
|
||||
data?.lineId,
|
||||
"userEmailOpenCLI",
|
||||
data?.userEmailOpenCLI
|
||||
);
|
||||
updateValueLineStation(data?.lineId, "userOpenCLI", data?.userOpenCLI);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
socket?.on("user_close_cli", (data) => {
|
||||
setTimeout(() => {
|
||||
updateValueLineStation(data?.lineId, "cliOpened", false);
|
||||
updateValueLineStation(data?.lineId, "userEmailOpenCLI", "");
|
||||
updateValueLineStation(data?.lineId, "userOpenCLI", "");
|
||||
}, 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, stations]);
|
||||
|
||||
const updateStatus = (data: LineConfig) => {
|
||||
const line = getLine(data.lineId, data.stationId);
|
||||
if (line) {
|
||||
updateValueLineStation(line, "status", data.status);
|
||||
if (line?.id) {
|
||||
updateValueLineStation(line.id, "status", data.status);
|
||||
}
|
||||
};
|
||||
|
||||
const updateValueLineStation = <K extends keyof TLine>(
|
||||
currentLine: TLine,
|
||||
lineId: number,
|
||||
field: K,
|
||||
value: TLine[K]
|
||||
) => {
|
||||
|
|
@ -120,10 +192,20 @@ function App() {
|
|||
? {
|
||||
...station,
|
||||
lines: (station?.lines || [])?.map((lineItem: TLine) => {
|
||||
if (lineItem.id === currentLine.id) {
|
||||
if (lineItem.id === lineId) {
|
||||
return {
|
||||
...lineItem,
|
||||
[field]: value,
|
||||
[field]:
|
||||
field === "netOutput"
|
||||
? (lineItem.netOutput || "") + value
|
||||
: value,
|
||||
output: field === "netOutput" ? value : lineItem.output,
|
||||
loadingOutput:
|
||||
field === "netOutput"
|
||||
? lineItem.loadingOutput
|
||||
? false
|
||||
: true
|
||||
: false,
|
||||
};
|
||||
}
|
||||
return lineItem;
|
||||
|
|
@ -132,6 +214,24 @@ function App() {
|
|||
: station
|
||||
)
|
||||
);
|
||||
|
||||
if (selectedLine) {
|
||||
const line = {
|
||||
...selectedLine,
|
||||
[field]:
|
||||
field === "netOutput"
|
||||
? (selectedLine.netOutput || "") + value
|
||||
: value,
|
||||
output: field === "netOutput" ? value : selectedLine.output,
|
||||
loadingOutput:
|
||||
field === "netOutput"
|
||||
? selectedLine.loadingOutput
|
||||
? false
|
||||
: true
|
||||
: false,
|
||||
};
|
||||
setSelectedLine(line);
|
||||
}
|
||||
};
|
||||
|
||||
const getLine = (lineId: number, stationId: number) => {
|
||||
|
|
@ -142,63 +242,106 @@ function App() {
|
|||
} else return null;
|
||||
};
|
||||
|
||||
const openTerminal = (line: TLine) => {
|
||||
setOpenModalTerminal(true);
|
||||
setSelectedLine(line);
|
||||
socket?.emit("open_cli", {
|
||||
lineId: line.id,
|
||||
stationId: line.station_id,
|
||||
userEmail: user?.email,
|
||||
userName: user?.fullName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||
<Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(id) => setActiveTab(id?.toString() || "0")}
|
||||
onChange={(id) => {
|
||||
setActiveTab(id?.toString() || "0");
|
||||
setLoadingTerminal(false);
|
||||
setTimeout(() => {
|
||||
setLoadingTerminal(true);
|
||||
}, 100);
|
||||
}}
|
||||
variant="none"
|
||||
keepMounted={false}
|
||||
>
|
||||
<Tabs.List ref={setRootRef} className={classes.list}>
|
||||
{stations.map((station) => (
|
||||
<Tabs.Tab
|
||||
ref={setControlRef(station.id.toString())}
|
||||
className={classes.tab}
|
||||
key={station.id}
|
||||
value={station.id.toString()}
|
||||
>
|
||||
{station.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
<Flex justify={"space-between"}>
|
||||
<Flex wrap={"wrap"} style={{ maxWidth: "250px" }} gap={"4px"}>
|
||||
{usersConnecting.map((el) => (
|
||||
<Tooltip label={el.userName} key={el.userId}>
|
||||
<Avatar color="cyan" radius="xl" size={"md"}>
|
||||
{el.userName.slice(0, 2)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Flex>
|
||||
<Tabs.List ref={setRootRef} className={classes.list}>
|
||||
{stations.map((station) => (
|
||||
<Tabs.Tab
|
||||
ref={setControlRef(station.id.toString())}
|
||||
className={classes.tab}
|
||||
key={station.id}
|
||||
value={station.id.toString()}
|
||||
>
|
||||
{station.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
|
||||
<FloatingIndicator
|
||||
target={activeTab ? controlsRefs[activeTab] : null}
|
||||
parent={rootRef}
|
||||
className={classes.indicator}
|
||||
/>
|
||||
<Flex gap={"sm"}>
|
||||
{Number(activeTab) ? (
|
||||
<FloatingIndicator
|
||||
target={activeTab ? controlsRefs[activeTab] : null}
|
||||
parent={rootRef}
|
||||
className={classes.indicator}
|
||||
/>
|
||||
<Flex gap={"sm"}>
|
||||
{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="Edit Station"
|
||||
title="Add Station"
|
||||
variant="outline"
|
||||
color="green"
|
||||
onClick={() => {
|
||||
setStationEdit(
|
||||
stations.find((el) => el.id === Number(activeTab))
|
||||
);
|
||||
setIsOpenAddStation(true);
|
||||
setIsEditStation(true);
|
||||
setIsEditStation(false);
|
||||
setStationEdit(undefined);
|
||||
}}
|
||||
>
|
||||
<IconEdit />
|
||||
<IconSettingsPlus />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<ActionIcon
|
||||
title="Add Station"
|
||||
</Flex>
|
||||
</Tabs.List>
|
||||
<Flex gap={"sm"} align={"baseline"}>
|
||||
<Text className={classes.userName}>{user?.fullName}</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="green"
|
||||
color="red"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
setIsOpenAddStation(true);
|
||||
setIsEditStation(false);
|
||||
setStationEdit(undefined);
|
||||
localStorage.removeItem("user");
|
||||
window.location.href = "/";
|
||||
socket?.disconnect();
|
||||
}}
|
||||
>
|
||||
<IconSettingsPlus />
|
||||
</ActionIcon>
|
||||
Logout
|
||||
</Button>
|
||||
</Flex>
|
||||
</Tabs.List>
|
||||
</Flex>
|
||||
|
||||
{stations.map((station) => (
|
||||
<Tabs.Panel
|
||||
|
|
@ -238,7 +381,11 @@ function App() {
|
|||
line={line}
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
updateStatus={updateStatus}
|
||||
openTerminal={openTerminal}
|
||||
loadTerminal={
|
||||
loadingTerminal &&
|
||||
Number(station.id) === Number(activeTab)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
|
|
@ -309,8 +456,9 @@ function App() {
|
|||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
{scenarios.map((el) => (
|
||||
{scenarios.map((el, i) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
|
|
@ -345,11 +493,24 @@ function App() {
|
|||
setActiveTab(stations.length ? stations[0]?.id.toString() : "0")
|
||||
}
|
||||
/>
|
||||
|
||||
<ModalTerminal
|
||||
opened={openModalTerminal}
|
||||
onClose={() => {
|
||||
setOpenModalTerminal(false);
|
||||
setSelectedLine(undefined);
|
||||
}}
|
||||
line={selectedLine}
|
||||
socket={socket}
|
||||
stationItem={stations.find((el) => el.id === Number(activeTab))}
|
||||
scenarios={scenarios}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Main() {
|
||||
const user = localStorage.getItem("user");
|
||||
return (
|
||||
<MantineProvider>
|
||||
<SocketProvider>
|
||||
|
|
@ -363,7 +524,13 @@ export default function Main() {
|
|||
}
|
||||
>
|
||||
<Notifications position="top-right" autoClose={5000} />
|
||||
<App />
|
||||
{user ? (
|
||||
<App />
|
||||
) : (
|
||||
<Container w={"100%"} style={{ maxWidth: "100%", padding: 0 }}>
|
||||
<PageLogin />
|
||||
</Container>
|
||||
)}
|
||||
</Suspense>
|
||||
</SocketProvider>
|
||||
</MantineProvider>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,22 @@
|
|||
.wrapper {
|
||||
min-height: rem(100vh);
|
||||
background-size: cover;
|
||||
background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80);
|
||||
}
|
||||
|
||||
.form {
|
||||
border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
|
||||
min-height: rem(100vh);
|
||||
max-width: rem(500px);
|
||||
padding-top: rem(80px);
|
||||
@media (max-width: var(--mantine-breakpoint-sm)) {
|
||||
max-width: 100%;
|
||||
};
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
}
|
||||
|
||||
.google-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #4285f4;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
margin: 15px auto;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.google-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.google-btn:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.google-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
height: 100vh;
|
||||
background-size: cover;
|
||||
background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 2px;
|
||||
background-color: #007bff; /* blue accent */
|
||||
margin: 0.1rem auto 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.form {
|
||||
border-right: rem(1px) solid
|
||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
|
||||
max-width: rem(450px);
|
||||
padding-top: rem(80px);
|
||||
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family: Greycliff CF, var(--mantine-font-family);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,105 +1,80 @@
|
|||
import { useGoogleLogin } from '@react-oauth/google'
|
||||
import { emailRegex } from '@/utils/formRegexs'
|
||||
import { Button, PasswordInput, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import axios from "axios";
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { AppDispatch, RootState } from '@/rtk/store'
|
||||
import {
|
||||
loginAsync,
|
||||
loginERPAsync,
|
||||
loginWithGoogleAsync,
|
||||
} from '@/rtk/slices/authSlice'
|
||||
|
||||
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
|
||||
import { useForm } from '@mantine/form'
|
||||
|
||||
import classes from './AuthenticationImage.module.css'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import ImgERP from '../../lib/images/erp.jpg'
|
||||
import { useState } from 'react'
|
||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
type TLogin = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { status } = useSelector((state: RootState) => state.auth)
|
||||
const [isLoginERP, setIsLoginERP] = useState(false)
|
||||
|
||||
const formLogin = useForm<TLogin>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
validate: (values) => ({
|
||||
email:
|
||||
values.email === ''
|
||||
? 'Email is required'
|
||||
: isLoginERP
|
||||
? null
|
||||
: emailRegex.test(values.email)
|
||||
? null
|
||||
: 'Invalid email',
|
||||
email: values.email === "" ? "Email is required" : null,
|
||||
|
||||
password: values.password === '' ? 'Password is required' : null,
|
||||
password: values.password === "" ? "Password is required" : null,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
const handleLogin = async (values: TLogin) => {
|
||||
if (isLoginERP) {
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!formLogin.values.email) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Email is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formLogin.values.password) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Password is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
userEmail: values.email,
|
||||
password: values.password,
|
||||
}
|
||||
const resultAction = await dispatch(loginERPAsync(payload))
|
||||
|
||||
if (loginERPAsync.fulfilled.match(resultAction)) {
|
||||
// set interval to wait for localStorage to be set
|
||||
window.location.href = '/dashboard'
|
||||
}
|
||||
} else {
|
||||
const resultAction = await dispatch(loginAsync(values))
|
||||
|
||||
if (loginAsync.fulfilled.match(resultAction)) {
|
||||
navigate('/dashboard')
|
||||
email: formLogin.values.email,
|
||||
password: formLogin.values.password,
|
||||
};
|
||||
const response = await axios.post(apiUrl + "api/auth/login", payload);
|
||||
if (response.data.user) {
|
||||
const user = response.data.user;
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
window.location.href = "/";
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Login fail, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoginGG = useGoogleLogin({
|
||||
onSuccess: async (codeResponse) => {
|
||||
const accessToken = codeResponse.access_token
|
||||
const resultAction = await dispatch(loginWithGoogleAsync(accessToken))
|
||||
|
||||
if (loginWithGoogleAsync.fulfilled.match(resultAction)) {
|
||||
navigate('/dashboard')
|
||||
}
|
||||
},
|
||||
onError: (error) => console.log('Login Failed:', error),
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
padding: "10px 20px",
|
||||
}}
|
||||
onSubmit={formLogin.onSubmit(handleLogin)}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h3 className={classes.title}>
|
||||
{isLoginERP ? 'Login with ERP account' : 'Login with ATC account'}
|
||||
</h3>
|
||||
</div>
|
||||
<TextInput
|
||||
label={isLoginERP ? 'Username/email:' : 'Email address'}
|
||||
label={"Email address"}
|
||||
placeholder="hello@gmail.com"
|
||||
value={formLogin.values.email}
|
||||
error={formLogin.errors.email}
|
||||
onChange={(e) => {
|
||||
formLogin.setFieldValue('email', e.target.value!)
|
||||
formLogin.setFieldValue("email", e.target.value!);
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
|
|
@ -110,7 +85,7 @@ const Login = () => {
|
|||
value={formLogin.values.password}
|
||||
error={formLogin.errors.password}
|
||||
onChange={(e) => {
|
||||
formLogin.setFieldValue('password', e.target.value!)
|
||||
formLogin.setFieldValue("password", e.target.value!);
|
||||
}}
|
||||
required
|
||||
mt="md"
|
||||
|
|
@ -122,60 +97,12 @@ const Login = () => {
|
|||
mt="xl"
|
||||
size="md"
|
||||
type="submit"
|
||||
loading={status === 'loading'}
|
||||
loading={status === "loading"}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
{!isLoginERP ? (
|
||||
<Box ta={'center'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="#228be6"
|
||||
radius={'5px'}
|
||||
className={classes['google-btn']}
|
||||
onClick={() => setIsLoginERP(true)}
|
||||
>
|
||||
<img
|
||||
src={ImgERP}
|
||||
alt="ERP logo"
|
||||
className={classes['google-icon']}
|
||||
/>
|
||||
Sign in with ERP
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box ta={'center'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="#228be6"
|
||||
radius={'5px'}
|
||||
className={classes['google-btn']}
|
||||
onClick={() => setIsLoginERP(false)}
|
||||
>
|
||||
Sign in normally
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box ta={'center'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="#228be6"
|
||||
radius={'5px'}
|
||||
onClick={() => handleLoginGG()}
|
||||
className={classes['google-btn']}
|
||||
>
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/480px-Google_%22G%22_logo.svg.png"
|
||||
alt="Google logo"
|
||||
className={classes['google-icon']}
|
||||
/>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Login
|
||||
export default Login;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { useState } from "react";
|
||||
import { Anchor, Image, Paper, Text } from "@mantine/core";
|
||||
import Login from "./Login";
|
||||
import Register from "./Register";
|
||||
import classes from "./AuthenticationImage.module.css";
|
||||
|
||||
export const PageLogin = () => {
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100vh",
|
||||
}}
|
||||
>
|
||||
<div className={classes.wrapper}>
|
||||
<Paper className={classes.form} radius={0} p={30}>
|
||||
<Image
|
||||
w={"45%"}
|
||||
mt={"sm"}
|
||||
mb={"xs"}
|
||||
m={"0 auto"}
|
||||
src={import.meta.env.VITE_DOMAIN + "logo-ATC-removebg-preview.png"}
|
||||
/>
|
||||
|
||||
{isRegister ? (
|
||||
<>
|
||||
<Register />
|
||||
|
||||
<Text ta="center" mt="md">
|
||||
You have an account?{" "}
|
||||
<Anchor<"a">
|
||||
href="#"
|
||||
fw={700}
|
||||
onClick={() => setIsRegister(false)}
|
||||
>
|
||||
Sign in
|
||||
</Anchor>
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Login />
|
||||
|
||||
<Text ta="center" mt="md">
|
||||
Don't have an account?{" "}
|
||||
<Anchor<"a">
|
||||
href="#"
|
||||
fw={700}
|
||||
onClick={() => setIsRegister(true)}
|
||||
>
|
||||
Register
|
||||
</Anchor>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLogin;
|
||||
|
|
@ -1,48 +1,77 @@
|
|||
import { useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { AppDispatch, RootState } from '@/rtk/store'
|
||||
import { registerAsync } from '@/rtk/slices/authSlice'
|
||||
import { useState } from "react";
|
||||
|
||||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||
import { emailRegex, passwordRegex } from '@/utils/formRegexs'
|
||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||
|
||||
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
|
||||
import { Box, Button, PasswordInput, TextInput } from "@mantine/core";
|
||||
import { emailRegex } from "../../untils/helper";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import axios from "axios";
|
||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
type TRegister = {
|
||||
email: string
|
||||
password: string
|
||||
confirm_password: string
|
||||
full_name: string
|
||||
}
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
full_name: string;
|
||||
};
|
||||
|
||||
function Register() {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { status } = useSelector((state: RootState) => state.auth)
|
||||
|
||||
const [formRegister, setFormRegister] = useState<TRegister>({
|
||||
email: '',
|
||||
full_name: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
email: "",
|
||||
full_name: "",
|
||||
password: "",
|
||||
confirm_password: "",
|
||||
});
|
||||
|
||||
const handleRegister = async () => {
|
||||
// Dispatch action registerAsync với dữ liệu form và đợi kết quả
|
||||
const resultAction = await dispatch(registerAsync(formRegister))
|
||||
|
||||
// Kiểm tra nếu action thành công
|
||||
if (registerAsync.fulfilled.match(resultAction)) {
|
||||
// Tải lại trang web
|
||||
// window.location.reload()
|
||||
try {
|
||||
if (!formRegister.email) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Email is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formRegister.password) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Password is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
email: formRegister.email,
|
||||
password: formRegister.password,
|
||||
full_name: formRegister.full_name,
|
||||
};
|
||||
const response = await axios.post(apiUrl + "api/auth/register", payload);
|
||||
if (response.data.user) {
|
||||
const user = response.data.user;
|
||||
user.fullName = user.full_name;
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: response.data.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Register fail, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleRegister()
|
||||
e.preventDefault();
|
||||
handleRegister();
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
|
|
@ -50,12 +79,12 @@ function Register() {
|
|||
placeholder="hello@gmail.com"
|
||||
value={formRegister.email}
|
||||
error={
|
||||
emailRegex.test(formRegister.email) || formRegister.email === ''
|
||||
emailRegex.test(formRegister.email) || formRegister.email === ""
|
||||
? null
|
||||
: 'Invalid email'
|
||||
: "Invalid email"
|
||||
}
|
||||
onChange={(e) => {
|
||||
setFormRegister({ ...formRegister, email: e.target.value })
|
||||
setFormRegister({ ...formRegister, email: e.target.value });
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
|
|
@ -68,19 +97,22 @@ function Register() {
|
|||
placeholder="Bill Gates"
|
||||
value={formRegister.full_name}
|
||||
onChange={(e) => {
|
||||
setFormRegister({ ...formRegister, full_name: e.target.value })
|
||||
setFormRegister({ ...formRegister, full_name: e.target.value });
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<PasswordRequirementInput
|
||||
requirements={requirementsPassword}
|
||||
value={formRegister}
|
||||
setValue={setFormRegister}
|
||||
<PasswordInput
|
||||
mt="md"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
placeholder="Your password"
|
||||
value={formRegister.password}
|
||||
onChange={(e) => {
|
||||
setFormRegister({ ...formRegister, password: e.target.value });
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
|
|
@ -90,27 +122,29 @@ function Register() {
|
|||
value={formRegister.confirm_password}
|
||||
error={
|
||||
formRegister.confirm_password === formRegister.password ||
|
||||
formRegister.confirm_password === ''
|
||||
formRegister.confirm_password === ""
|
||||
? null
|
||||
: 'Password do not match'
|
||||
: "Password do not match"
|
||||
}
|
||||
onChange={(e) => {
|
||||
setFormRegister({ ...formRegister, confirm_password: e.target.value })
|
||||
setFormRegister({
|
||||
...formRegister,
|
||||
confirm_password: e.target.value,
|
||||
});
|
||||
}}
|
||||
required
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<Box ta={'center'}>
|
||||
<Box ta={"center"}>
|
||||
<Button
|
||||
type="submit"
|
||||
m="15px auto"
|
||||
fullWidth
|
||||
size="md"
|
||||
loading={status === 'loading'}
|
||||
loading={status === "loading"}
|
||||
disabled={
|
||||
formRegister.password !== '' &&
|
||||
passwordRegex.test(formRegister.password) &&
|
||||
formRegister.password !== "" &&
|
||||
formRegister.password === formRegister.confirm_password
|
||||
? false
|
||||
: true
|
||||
|
|
@ -120,7 +154,7 @@ function Register() {
|
|||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Register
|
||||
export default Register;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Card, Text, Box, Flex } from "@mantine/core";
|
||||
import type { LineConfig, TLine, TStation } from "../untils/types";
|
||||
import type { TLine, TStation } from "../untils/types";
|
||||
import classes from "./Component.module.css";
|
||||
import TerminalCLI from "./TerminalXTerm";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
|
@ -12,14 +12,16 @@ const CardLine = ({
|
|||
setSelectedLines,
|
||||
socket,
|
||||
stationItem,
|
||||
updateStatus,
|
||||
openTerminal,
|
||||
loadTerminal,
|
||||
}: {
|
||||
line: TLine;
|
||||
selectedLines: TLine[];
|
||||
setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
|
||||
socket: Socket | null;
|
||||
stationItem: TStation;
|
||||
updateStatus: (value: LineConfig) => void;
|
||||
openTerminal: (value: TLine) => void;
|
||||
loadTerminal: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -33,6 +35,11 @@ const CardLine = ({
|
|||
? { backgroundColor: "#8bf55940" }
|
||||
: {}
|
||||
}
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openTerminal(line);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -45,16 +52,27 @@ const CardLine = ({
|
|||
justify={"space-between"}
|
||||
direction={"column"}
|
||||
// gap={"md"}
|
||||
align={"center"}
|
||||
// align={"center"}
|
||||
>
|
||||
<div>
|
||||
<Flex justify={"space-between"}>
|
||||
<Text fw={600} style={{ display: "flex", gap: "4px" }}>
|
||||
Line: {line.lineNumber || line.line_number} - {line.port}{" "}
|
||||
{line.status === "connected" && (
|
||||
<IconCircleCheckFilled color="green" />
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginLeft: "16px",
|
||||
fontSize: "12px",
|
||||
color: "red",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""}
|
||||
</div>
|
||||
</Flex>
|
||||
{/* <Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
|
||||
<div className={classes.info_line}>SN: FGL2240307M</div>
|
||||
<div className={classes.info_line}>VID: V01</div> */}
|
||||
|
|
@ -66,9 +84,11 @@ const CardLine = ({
|
|||
style={{ height: "175px", width: "300px" }}
|
||||
>
|
||||
<TerminalCLI
|
||||
cliOpened={true}
|
||||
cliOpened={loadTerminal}
|
||||
socket={socket}
|
||||
content={line.netOutput ?? ""}
|
||||
content={line?.output ?? ""}
|
||||
initContent={line?.netOutput ?? ""}
|
||||
loadingContent={line?.loadingOutput}
|
||||
line_id={Number(line?.id)}
|
||||
station_id={Number(stationItem.id)}
|
||||
isDisabled={false}
|
||||
|
|
@ -82,8 +102,9 @@ const CardLine = ({
|
|||
padding: "0px",
|
||||
paddingBottom: "0px",
|
||||
}}
|
||||
onDoubleClick={() => {}}
|
||||
updateStatus={updateStatus}
|
||||
onDoubleClick={() => {
|
||||
openTerminal(line);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
Grid,
|
||||
TextInput,
|
||||
Button,
|
||||
Checkbox,
|
||||
} from "@mantine/core";
|
||||
import { IconSettingsPlus } from "@tabler/icons-react";
|
||||
import TableRows from "./Scenario/TableRows";
|
||||
|
|
@ -42,11 +43,10 @@ function DrawerScenario({
|
|||
send: "",
|
||||
delay: "0",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
] as IBodyScenario[],
|
||||
timeout: "30000",
|
||||
is_reboot: false,
|
||||
isReboot: false,
|
||||
},
|
||||
validate: {
|
||||
title: (value) => {
|
||||
|
|
@ -72,7 +72,6 @@ function DrawerScenario({
|
|||
send: "",
|
||||
delay: "0",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
});
|
||||
form.setFieldValue("body", newBody);
|
||||
};
|
||||
|
|
@ -84,6 +83,22 @@ function DrawerScenario({
|
|||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.values.title) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Title is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!form.values.timeout) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Timeout is required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsSubmit(true);
|
||||
try {
|
||||
const body = form.values.body.map((el: IBodyScenario) => ({
|
||||
|
|
@ -93,7 +108,8 @@ function DrawerScenario({
|
|||
}));
|
||||
|
||||
const payload = {
|
||||
...form.values,
|
||||
title: form.values.title,
|
||||
isReboot: form.values.isReboot,
|
||||
body: body,
|
||||
timeout: Number(form.values.timeout),
|
||||
};
|
||||
|
|
@ -111,15 +127,28 @@ function DrawerScenario({
|
|||
)
|
||||
: [...pre, scenario]
|
||||
);
|
||||
setIsEdit(true);
|
||||
setDataScenario(scenario);
|
||||
notifications.show({
|
||||
title: "Success",
|
||||
message: res.data.message,
|
||||
color: "green",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: res.data.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Failed to create scenario, please try again!",
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
|
|
@ -196,7 +225,7 @@ function DrawerScenario({
|
|||
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);
|
||||
form.setFieldValue("isReboot", scenario.isReboot);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -231,7 +260,27 @@ function DrawerScenario({
|
|||
required
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Grid.Col
|
||||
span={3}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "end",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
label="Reboot"
|
||||
style={{ color: "red" }}
|
||||
checked={form.values.isReboot}
|
||||
onChange={(event) =>
|
||||
form.setFieldValue(
|
||||
"isReboot",
|
||||
event.currentTarget.checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
@ -329,7 +378,7 @@ function DrawerScenario({
|
|||
close={() => {
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
message={"Are you sure delete this station?"}
|
||||
message={"Are you sure delete this scenario?"}
|
||||
handle={() => {
|
||||
setOpenConfirm(false);
|
||||
handleDelete();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
import { Box, Button, Grid, Modal, Text } from "@mantine/core";
|
||||
import type { IScenario, TLine, TStation } from "../untils/types";
|
||||
import TerminalCLI from "./TerminalXTerm";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import classes from "./Component.module.css";
|
||||
import { useState } from "react";
|
||||
import { IconCircleCheckFilled } from "@tabler/icons-react";
|
||||
|
||||
const ModalTerminal = ({
|
||||
opened,
|
||||
onClose,
|
||||
line,
|
||||
socket,
|
||||
stationItem,
|
||||
scenarios,
|
||||
}: {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
line: TLine | undefined;
|
||||
socket: Socket | null;
|
||||
stationItem: TStation | undefined;
|
||||
scenarios: IScenario[];
|
||||
}) => {
|
||||
const [isDisable, setIsDisable] = useState<boolean>(false);
|
||||
// console.log(line);
|
||||
return (
|
||||
<Box>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
socket?.emit("close_cli", {
|
||||
lineId: line?.id,
|
||||
stationId: line?.station_id,
|
||||
});
|
||||
}}
|
||||
size={"80%"}
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
title={
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text size="md" mr={10}>
|
||||
Line number: <strong>{line?.line_number || ""}</strong>
|
||||
</Text>
|
||||
<Text size="md" mr={10}>
|
||||
- <strong>{line?.port || ""}</strong>
|
||||
</Text>
|
||||
{line?.status === "connected" && (
|
||||
<IconCircleCheckFilled color="green" />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginLeft: "16px",
|
||||
fontSize: "12px",
|
||||
color: "red",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{line?.userOpenCLI
|
||||
? line?.userOpenCLI + " is using"
|
||||
: "Terminal is used"}
|
||||
</div>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={10} style={{ borderRight: "1px solid #ccc" }}>
|
||||
<TerminalCLI
|
||||
cliOpened={opened}
|
||||
socket={socket}
|
||||
content={line?.output ?? ""}
|
||||
initContent={line?.netOutput ?? ""}
|
||||
loadingContent={line?.loadingOutput}
|
||||
line_id={Number(line?.id)}
|
||||
station_id={Number(stationItem?.id)}
|
||||
isDisabled={false}
|
||||
line_status={line?.status || ""}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={2}>
|
||||
{scenarios.map((scenario) => (
|
||||
<Button
|
||||
disabled={isDisable}
|
||||
className={classes.buttonScenario}
|
||||
key={scenario.id}
|
||||
miw={"100px"}
|
||||
mb={"6px"}
|
||||
style={{ minHeight: "24px" }}
|
||||
mr={"5px"}
|
||||
variant={"outline"}
|
||||
onClick={async () => {
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
if (line)
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(line, {
|
||||
scenario: scenario,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{scenario.title}
|
||||
</Button>
|
||||
))}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalTerminal;
|
||||
|
|
@ -4,11 +4,11 @@ import "xterm/css/xterm.css";
|
|||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { SOCKET_EVENTS } from "../untils/constanst";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import type { LineConfig } from "../untils/types";
|
||||
|
||||
interface TerminalCLIProps {
|
||||
socket: Socket | null;
|
||||
content?: string;
|
||||
initContent?: string;
|
||||
line_id: number;
|
||||
line_status: string;
|
||||
station_id: number;
|
||||
|
|
@ -25,7 +25,7 @@ interface TerminalCLIProps {
|
|||
onDoubleClick?: () => void;
|
||||
fontSize?: number;
|
||||
miniSize?: boolean;
|
||||
updateStatus: (value: LineConfig) => void;
|
||||
loadingContent?: boolean;
|
||||
}
|
||||
|
||||
const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||
|
|
@ -39,7 +39,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
onDoubleClick = () => {},
|
||||
fontSize = 14,
|
||||
miniSize = false,
|
||||
updateStatus,
|
||||
initContent = "",
|
||||
loadingContent = false,
|
||||
}) => {
|
||||
const xtermRef = useRef<HTMLDivElement>(null);
|
||||
const terminal = useRef<Terminal>(null);
|
||||
|
|
@ -128,40 +129,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
|
||||
if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
// Nhận output từ thiết bị và ghi vào terminal
|
||||
socket?.on("line_output", (data) => {
|
||||
if (data?.lineId === line_id && terminal.current) {
|
||||
terminal.current?.write(data.data);
|
||||
terminal.current?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
socket?.on("line_error", (data) => {
|
||||
if (data?.lineId === line_id && terminal.current) {
|
||||
terminal.current?.write(data.error);
|
||||
}
|
||||
});
|
||||
|
||||
socket?.on("init", (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((value) => {
|
||||
if (value?.id === line_id && terminal.current) {
|
||||
terminal.current?.write(value.output);
|
||||
updateStatus({ ...value, lineId: value.id });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket?.off("init");
|
||||
socket?.off("line_error");
|
||||
socket?.off("line_output");
|
||||
};
|
||||
}, []);
|
||||
}, [content, loadingContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cliOpened) {
|
||||
|
|
@ -182,7 +150,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (terminal.current) {
|
||||
terminal.current?.write(content);
|
||||
terminal.current?.write(initContent);
|
||||
if (!miniSize && !isDisabled) terminal.current?.focus();
|
||||
terminal.current.scrollToBottom();
|
||||
}
|
||||
|
|
@ -211,7 +179,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||
minHeight: customStyle.maxHeight ?? "60vh",
|
||||
minHeight: customStyle.maxHeight ?? "75vh",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -221,8 +189,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
paddingLeft: customStyle.paddingLeft ?? "10px",
|
||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||
fontSize: customStyle.fontSize ?? "9px",
|
||||
maxHeight: customStyle.maxHeight ?? "60vh",
|
||||
height: customStyle.height ?? "60vh",
|
||||
maxHeight: customStyle.maxHeight ?? "75vh",
|
||||
height: customStyle.height ?? "75vh",
|
||||
padding: customStyle.padding ?? "4px",
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { SOCKET_EVENTS } from "../untils/constanst";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
|
@ -15,9 +21,21 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
children,
|
||||
}) => {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const user = useMemo(() => {
|
||||
return localStorage.getItem("user") &&
|
||||
typeof localStorage.getItem("user") === "string"
|
||||
? JSON.parse(localStorage.getItem("user") || "")
|
||||
: null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const newSocket = io(SOCKET_URL);
|
||||
if (!user) return;
|
||||
const newSocket = io(SOCKET_URL, {
|
||||
auth: {
|
||||
userId: user?.id,
|
||||
userName: user?.fullName,
|
||||
},
|
||||
});
|
||||
|
||||
setSocket(newSocket);
|
||||
|
||||
|
|
@ -46,7 +64,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
|
||||
newSocket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={{ socket }}>
|
||||
|
|
|
|||
|
|
@ -2,3 +2,8 @@ export const numberOnly = (value: string): string => {
|
|||
const matched = value.match(/[\d.]+/g);
|
||||
return matched ? matched.join("") : "";
|
||||
};
|
||||
|
||||
export const passwordRegex =
|
||||
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/;
|
||||
|
||||
export const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ export type TLine = {
|
|||
inventory?: any;
|
||||
status?: string;
|
||||
netOutput?: string;
|
||||
output?: string;
|
||||
loadingOutput?: boolean;
|
||||
outlet?: number;
|
||||
cliOpened?: boolean;
|
||||
systemLogUrl?: string;
|
||||
|
|
@ -84,14 +86,8 @@ export type TLine = {
|
|||
};
|
||||
|
||||
export type TUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
email_cc: string;
|
||||
full_name: string;
|
||||
package_id: string;
|
||||
zulip: string;
|
||||
token?: string;
|
||||
name: string;
|
||||
userId: number;
|
||||
userName: string;
|
||||
};
|
||||
|
||||
export type APCProps = {
|
||||
|
|
@ -144,7 +140,7 @@ export type IScenario = {
|
|||
title: string;
|
||||
body: string;
|
||||
timeout: number;
|
||||
is_reboot: boolean;
|
||||
isReboot: boolean;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
|
|
@ -153,5 +149,4 @@ export type IBodyScenario = {
|
|||
send: string;
|
||||
delay: string;
|
||||
repeat: string;
|
||||
note: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue