diff --git a/FRONTEND/package-lock.json b/FRONTEND/package-lock.json index 141d0c1..4f1a129 100644 --- a/FRONTEND/package-lock.json +++ b/FRONTEND/package-lock.json @@ -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", diff --git a/FRONTEND/package.json b/FRONTEND/package.json index fc6e012..913d62b 100644 --- a/FRONTEND/package.json +++ b/FRONTEND/package.json @@ -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" }, diff --git a/FRONTEND/public/logo-ATC-removebg-preview.png b/FRONTEND/public/logo-ATC-removebg-preview.png new file mode 100644 index 0000000..fab4848 Binary files /dev/null and b/FRONTEND/public/logo-ATC-removebg-preview.png differ diff --git a/FRONTEND/src/App.module.css b/FRONTEND/src/App.module.css index 8a4c804..821b93d 100644 --- a/FRONTEND/src/App.module.css +++ b/FRONTEND/src/App.module.css @@ -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; -} \ No newline at end of file +} + +.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; +} diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index f02baa0..5f1b4d0 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -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([]); @@ -53,6 +71,10 @@ function App() { const [isEditStation, setIsEditStation] = useState(false); const [stationEdit, setStationEdit] = useState(); const [scenarios, setScenarios] = useState([]); + const [openModalTerminal, setOpenModalTerminal] = useState(false); + const [selectedLine, setSelectedLine] = useState(); + const [loadingTerminal, setLoadingTerminal] = useState(true); + const [usersConnecting, setUsersConnecting] = useState([]); // 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 = ( - 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 ( - + setActiveTab(id?.toString() || "0")} + onChange={(id) => { + setActiveTab(id?.toString() || "0"); + setLoadingTerminal(false); + setTimeout(() => { + setLoadingTerminal(true); + }, 100); + }} variant="none" keepMounted={false} > - - {stations.map((station) => ( - - {station.name} - - ))} + + + {usersConnecting.map((el) => ( + + + {el.userName.slice(0, 2)} + + + ))} + + + {stations.map((station) => ( + + {station.name} + + ))} - - - {Number(activeTab) ? ( + + + {Number(activeTab) ? ( + { + setStationEdit( + stations.find((el) => el.id === Number(activeTab)) + ); + setIsOpenAddStation(true); + setIsEditStation(true); + }} + > + + + ) : ( + "" + )} { - setStationEdit( - stations.find((el) => el.id === Number(activeTab)) - ); setIsOpenAddStation(true); - setIsEditStation(true); + setIsEditStation(false); + setStationEdit(undefined); }} > - + - ) : ( - "" - )} - + + + {user?.fullName} + - + {stations.map((station) => ( ))} @@ -309,8 +456,9 @@ function App() { }, 10000); }} /> - {scenarios.map((el) => ( + {scenarios.map((el, i) => ( + + { + setOpenModalTerminal(false); + setSelectedLine(undefined); + }} + line={selectedLine} + socket={socket} + stationItem={stations.find((el) => el.id === Number(activeTab))} + scenarios={scenarios} + /> ); } export default function Main() { + const user = localStorage.getItem("user"); return ( @@ -363,7 +524,13 @@ export default function Main() { } > - + {user ? ( + + ) : ( + + + + )} diff --git a/FRONTEND/src/components/Authentication/AuthenticationImage.module.css b/FRONTEND/src/components/Authentication/AuthenticationImage.module.css index 9abac98..f67c13a 100644 --- a/FRONTEND/src/components/Authentication/AuthenticationImage.module.css +++ b/FRONTEND/src/components/Authentication/AuthenticationImage.module.css @@ -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; -} \ No newline at end of file +.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); +} diff --git a/FRONTEND/src/components/Authentication/Login.tsx b/FRONTEND/src/components/Authentication/Login.tsx index fd5febc..0f3e6c4 100644 --- a/FRONTEND/src/components/Authentication/Login.tsx +++ b/FRONTEND/src/components/Authentication/Login.tsx @@ -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() - const { status } = useSelector((state: RootState) => state.auth) - const [isLoginERP, setIsLoginERP] = useState(false) - const formLogin = useForm({ 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 (
-
-

- {isLoginERP ? 'Login with ERP account' : 'Login with ATC account'} -

-
{ - 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 - - {!isLoginERP ? ( - - - - ) : ( - - - - )} - - - - - ) -} + ); +}; -export default Login +export default Login; diff --git a/FRONTEND/src/components/Authentication/LoginPage.tsx b/FRONTEND/src/components/Authentication/LoginPage.tsx new file mode 100644 index 0000000..59c0be1 --- /dev/null +++ b/FRONTEND/src/components/Authentication/LoginPage.tsx @@ -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 ( +
+
+ + + + {isRegister ? ( + <> + + + + You have an account?{" "} + + href="#" + fw={700} + onClick={() => setIsRegister(false)} + > + Sign in + + + + ) : ( + <> + + + + Don't have an account?{" "} + + href="#" + fw={700} + onClick={() => setIsRegister(true)} + > + Register + + + + )} + +
+
+ ); +}; + +export default PageLogin; diff --git a/FRONTEND/src/components/Authentication/Register.tsx b/FRONTEND/src/components/Authentication/Register.tsx index dee2796..0ad3f48 100644 --- a/FRONTEND/src/components/Authentication/Register.tsx +++ b/FRONTEND/src/components/Authentication/Register.tsx @@ -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() - const { status } = useSelector((state: RootState) => state.auth) - const [formRegister, setFormRegister] = useState({ - 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 (
{ - e.preventDefault() - handleRegister() + e.preventDefault(); + handleRegister(); }} > { - 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" /> - { + setFormRegister({ ...formRegister, password: e.target.value }); + }} + required + size="md" /> { - setFormRegister({ ...formRegister, confirm_password: e.target.value }) + setFormRegister({ + ...formRegister, + confirm_password: e.target.value, + }); }} required size="md" /> - + + ))} + + + + + ); +}; + +export default ModalTerminal; diff --git a/FRONTEND/src/components/TerminalXTerm.tsx b/FRONTEND/src/components/TerminalXTerm.tsx index a80bcb6..34bf95e 100644 --- a/FRONTEND/src/components/TerminalXTerm.tsx +++ b/FRONTEND/src/components/TerminalXTerm.tsx @@ -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 = ({ @@ -39,7 +39,8 @@ const TerminalCLI: React.FC = ({ onDoubleClick = () => {}, fontSize = 14, miniSize = false, - updateStatus, + initContent = "", + loadingContent = false, }) => { const xtermRef = useRef(null); const terminal = useRef(null); @@ -128,40 +129,7 @@ const TerminalCLI: React.FC = ({ 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 = ({ 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 = ({ height: "100%", backgroundColor: "black", paddingBottom: customStyle.paddingBottom ?? "10px", - minHeight: customStyle.maxHeight ?? "60vh", + minHeight: customStyle.maxHeight ?? "75vh", }} >
= ({ 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) => { diff --git a/FRONTEND/src/context/SocketContext.tsx b/FRONTEND/src/context/SocketContext.tsx index b20bab7..a215894 100644 --- a/FRONTEND/src/context/SocketContext.tsx +++ b/FRONTEND/src/context/SocketContext.tsx @@ -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(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 ( diff --git a/FRONTEND/src/untils/helper.ts b/FRONTEND/src/untils/helper.ts index adf8ea9..ab956b4 100644 --- a/FRONTEND/src/untils/helper.ts +++ b/FRONTEND/src/untils/helper.ts @@ -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}$/; diff --git a/FRONTEND/src/untils/types.ts b/FRONTEND/src/untils/types.ts index a763653..7cf1eba 100644 --- a/FRONTEND/src/untils/types.ts +++ b/FRONTEND/src/untils/types.ts @@ -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; };