diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index 5adb02d..5f25f3f 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -44,7 +44,7 @@ import // ButtonConnect, import StationSetting from "./components/FormAddEdit"; // import DrawerScenario from "./components/DrawerScenario"; import { Notifications } from "@mantine/notifications"; -import ModalTerminal from "./components/ModalTerminal"; +import ModalTerminal from "./components/Modal/ModalTerminal"; import PageLogin from "./components/Authentication/LoginPage"; // import DrawerLogs from "./components/DrawerLogs"; import DraggableTabs from "./components/DragTabs"; diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index 1fc38f6..3d79cfa 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -5,7 +5,6 @@ import { CloseButton, Flex, Grid, - Input, ScrollArea, Tabs, Text, @@ -17,9 +16,9 @@ import classes from "./Component.module.css"; import type { IScenario, TLine, TStation, TUser } from "../untils/types"; import type { Socket } from "socket.io-client"; import { ButtonDPELP, ButtonSelect } from "./ButtonAction"; -import DrawerLogs from "./DrawerLogs"; -import { DrawerAPCControl, DrawerSwitchControl } from "./DrawerControl"; -import DrawerScenario from "./DrawerScenario"; +import DrawerLogs from "./Drawer/DrawerLogs"; +import { DrawerAPCControl, DrawerSwitchControl } from "./Drawer/DrawerControl"; +import DrawerScenario from "./Modal/ModalScenario"; import { isJsonString } from "../untils/helper"; import { motion } from "motion/react"; import { @@ -28,6 +27,7 @@ import { IconPlayerPlay, IconPlus, } from "@tabler/icons-react"; +import InputHistory from "./InputHistory"; interface TabsProps { selectedLines: TLine[]; @@ -339,19 +339,11 @@ const BottomToolBar = ({ ? JSON.parse(localStorage.getItem("user") || "") : null; }, []); - const [valueInput, setValueInput] = useState(""); - const inputRef = useRef(null); const [openScenarioModal, setOpenScenarioModal] = useState(false); const [openDrawerScenario, setOpenDrawerScenario] = useState(false); // const [activeTabBottom, setActiveTabBottom] = useState("command"); // const [isExpand, setIsExpand] = useState(true); - useEffect(() => { - if (selectedLines?.length > 1 && inputRef?.current) { - inputRef?.current?.focus(); - } - }, [selectedLines?.length]); - return ( <> {/* Modal chọn Scenario - Custom Simple Modal */} @@ -718,50 +710,10 @@ const BottomToolBar = ({ - { - const newValue = event.currentTarget.value; - setValueInput(newValue); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - const listLine = selectedLines.length - ? selectedLines - : station?.lines; - if (listLine?.length) { - socket?.emit("write_command_line_from_web", { - lineIds: listLine.map((line) => line.id), - stationId: station.id, - command: valueInput + "\r\n", - }); - // setTimeout(() => { - // socket?.emit("write_command_line_from_web", { - // lineIds: listLine.map((line) => line.id), - // stationId: station.id, - // command: " \n", - // }); - // }, 1000); - } - setValueInput(""); - } - }} - rightSectionPointerEvents="all" - rightSection={ - setValueInput("")} - style={{ - display: valueInput ? undefined : "none", - }} - /> - } + diff --git a/FRONTEND/src/components/ButtonAction.tsx b/FRONTEND/src/components/ButtonAction.tsx index 583d23f..c61440a 100644 --- a/FRONTEND/src/components/ButtonAction.tsx +++ b/FRONTEND/src/components/ButtonAction.tsx @@ -150,7 +150,7 @@ export const ButtonDPELP = ({ id: 0, is_reboot: 0, title: "DPELP", - timeout: 300000, + timeout: 360000, body: JSON.stringify(body), }, }) diff --git a/FRONTEND/src/components/DragTabs.tsx b/FRONTEND/src/components/DragTabs.tsx index fef5db8..7b69b2c 100644 --- a/FRONTEND/src/components/DragTabs.tsx +++ b/FRONTEND/src/components/DragTabs.tsx @@ -1,335 +1,402 @@ -import { ActionIcon, Avatar, Box, Button, Flex, Group, Menu, Tabs, Text, Tooltip, UnstyledButton } from "@mantine/core"; -import { DndContext, useSensor, useSensors, PointerSensor, closestCenter, type DragEndEvent } from "@dnd-kit/core"; -import { arrayMove, SortableContext, useSortable, horizontalListSortingStrategy } from "@dnd-kit/sortable"; +import { + ActionIcon, + Avatar, + Box, + Button, + Flex, + Group, + Menu, + Tabs, + Text, + Tooltip, + UnstyledButton, +} from "@mantine/core"; +import { + DndContext, + useSensor, + useSensors, + PointerSensor, + closestCenter, + type DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + useSortable, + horizontalListSortingStrategy, +} from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useEffect, useMemo, useState, type JSX } from "react"; -import { IconChevronRight, IconEdit, IconLogout, IconSettings, IconSettingsPlus, IconListDetails, IconUsersGroup } from "@tabler/icons-react"; +import { + IconChevronRight, + IconEdit, + IconLogout, + IconSettings, + IconSettingsPlus, + IconListDetails, + IconUsersGroup, +} from "@tabler/icons-react"; import classes from "./Component.module.css"; import type { IScenario, TStation, TUser } from "../untils/types"; import type { Socket } from "socket.io-client"; -import ModalHistory from "./ModalHistory"; -import ModalConfig from "./ModalConfig"; -import DrawerScenario from "./DrawerScenario"; +import ModalHistory from "./Modal/ModalHistory"; +import ModalConfig from "./Modal/ModalConfig"; +import DrawerScenario from "./Modal/ModalScenario"; interface DraggableTabsProps { - tabsData: TStation[]; - panels: JSX.Element[]; - storageKey?: string; - onChange: (activeTabId: string | null) => void; - w?: string | number; - isStationSettings?: boolean; - socket: Socket | null; - usersConnecting: TUser[]; - setIsEditStation: (value: React.SetStateAction) => void; - setIsOpenAddStation: (value: React.SetStateAction) => void; - setStationEdit: (value: React.SetStateAction) => void; - active: string; - setActive: (value: React.SetStateAction) => void; - onSendCommand: (value: string) => void; - scenarios: IScenario[]; - setScenarios: (value: React.SetStateAction) => void; + tabsData: TStation[]; + panels: JSX.Element[]; + storageKey?: string; + onChange: (activeTabId: string | null) => void; + w?: string | number; + isStationSettings?: boolean; + socket: Socket | null; + usersConnecting: TUser[]; + setIsEditStation: (value: React.SetStateAction) => void; + setIsOpenAddStation: (value: React.SetStateAction) => void; + setStationEdit: (value: React.SetStateAction) => void; + active: string; + setActive: (value: React.SetStateAction) => void; + onSendCommand: (value: string) => void; + scenarios: IScenario[]; + setScenarios: (value: React.SetStateAction) => void; } function SortableTab({ - tab, - active, - onChange, + tab, + active, + onChange, }: { - tab: TStation; - active: string | null; - onChange: (id: string) => void; - isStationSettings?: boolean; + tab: TStation; + active: string | null; + onChange: (id: string) => void; + isStationSettings?: boolean; }) { - const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: tab.id.toString() }); + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: tab.id.toString() }); - return ( - { - listeners?.onPointerDown?.(e); - onChange(tab.id.toString()); - }} - value={tab.id.toString()} - style={{ - transform: CSS.Transform.toString(transform), - transition, - cursor: "grab", - userSelect: "none", - backgroundColor: active === tab.id.toString() ? "#deffde" : "", - }} - color={active === tab.id.toString() ? "green" : ""} - fw={600} - fz="md" - c="#747474"> - - - {tab.name} - - - - ); + return ( + { + listeners?.onPointerDown?.(e); + onChange(tab.id.toString()); + }} + value={tab.id.toString()} + style={{ + transform: CSS.Transform.toString(transform), + transition, + cursor: "grab", + userSelect: "none", + backgroundColor: active === tab.id.toString() ? "#deffde" : "", + }} + color={active === tab.id.toString() ? "green" : ""} + fw={600} + fz="md" + c="#747474" + > + + + {tab.name} + + + + ); } export default function DraggableTabs({ - tabsData, - panels, - storageKey = "draggable-tabs-order", - onChange, - w, - isStationSettings = false, - socket, - usersConnecting, - setIsEditStation, - setIsOpenAddStation, - setStationEdit, - active, - setActive, - scenarios, - setScenarios, + tabsData, + panels, + storageKey = "draggable-tabs-order", + onChange, + w, + isStationSettings = false, + socket, + usersConnecting, + setIsEditStation, + setIsOpenAddStation, + setStationEdit, + active, + setActive, + scenarios, + setScenarios, }: DraggableTabsProps) { - const user = useMemo(() => { - return localStorage.getItem("user") && typeof localStorage.getItem("user") === "string" ? JSON.parse(localStorage.getItem("user") || "") : null; - }, []); - const [tabs, setTabs] = useState(tabsData); - const [isChangeTab, setIsChangeTab] = useState(false); - const [isSetActive, setIsSetActive] = useState(false); - const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); - const [openConfig, setOpenConfig] = useState(false); - const [openDrawerScenario, setOpenDrawerScenario] = useState(false); + const user = useMemo(() => { + return localStorage.getItem("user") && + typeof localStorage.getItem("user") === "string" + ? JSON.parse(localStorage.getItem("user") || "") + : null; + }, []); + const [tabs, setTabs] = useState(tabsData); + const [isChangeTab, setIsChangeTab] = useState(false); + const [isSetActive, setIsSetActive] = useState(false); + const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); + const [openConfig, setOpenConfig] = useState(false); + const [openDrawerScenario, setOpenDrawerScenario] = useState(false); - const sensors = useSensors(useSensor(PointerSensor)); + const sensors = useSensors(useSensor(PointerSensor)); - // Load saved order from localStorage - useEffect(() => { - if (isChangeTab) { - setTabs((pre) => - pre - .map((t) => { - const updatedTab = tabsData.find((td) => td.id === t.id); - return updatedTab ? updatedTab : t; - }) - .filter((t) => (tabsData.find((td) => td.id === t.id) ? true : false)) - ); - } else { - const saved = localStorage.getItem(storageKey); - let tabSelected = tabsData?.length > 0 ? tabsData[0]?.id.toString() : null; - if (saved) { - try { - const order = JSON.parse(saved) as { id: string; index: number }[]; + // Load saved order from localStorage + useEffect(() => { + if (isChangeTab) { + setTabs((pre) => + pre + .map((t) => { + const updatedTab = tabsData.find((td) => td.id === t.id); + return updatedTab ? updatedTab : t; + }) + .filter((t) => (tabsData.find((td) => td.id === t.id) ? true : false)) + ); + } else { + const saved = localStorage.getItem(storageKey); + let tabSelected = + tabsData?.length > 0 ? tabsData[0]?.id.toString() : null; + if (saved) { + try { + const order = JSON.parse(saved) as { id: string; index: number }[]; - // Find the max index in saved order - const maxIndex = Math.max(...order.map((o) => o.index), 0); + // Find the max index in saved order + const maxIndex = Math.max(...order.map((o) => o.index), 0); - const sorted = [...tabsData].sort((a, b) => { - const aOrder = order.find((o) => o.id.toString() === a.id.toString())?.index; - const bOrder = order.find((o) => o.id.toString() === b.id.toString())?.index; + const sorted = [...tabsData].sort((a, b) => { + const aOrder = order.find( + (o) => o.id.toString() === a.id.toString() + )?.index; + const bOrder = order.find( + (o) => o.id.toString() === b.id.toString() + )?.index; - // If not found, assign index after all existing ones - const aIndex = aOrder !== undefined ? aOrder : maxIndex + 1; - const bIndex = bOrder !== undefined ? bOrder : maxIndex + 1; + // If not found, assign index after all existing ones + const aIndex = aOrder !== undefined ? aOrder : maxIndex + 1; + const bIndex = bOrder !== undefined ? bOrder : maxIndex + 1; - return aIndex - bIndex; - }); + return aIndex - bIndex; + }); - tabSelected = sorted?.length > 0 ? sorted[0]?.id.toString() : null; - setTabs(sorted); - } catch { - setTabs(tabsData); - } - } else { - setTabs(tabsData); - } + tabSelected = sorted?.length > 0 ? sorted[0]?.id.toString() : null; + setTabs(sorted); + } catch { + setTabs(tabsData); + } + } else { + setTabs(tabsData); + } - if (!isSetActive && tabSelected) { - setActive(tabSelected); - setTimeout(() => { - onChange(tabSelected); - }, 100); - setIsSetActive(true); - } - } - }, [tabsData, storageKey]); + if (!isSetActive && tabSelected) { + setActive(tabSelected); + setTimeout(() => { + onChange(tabSelected); + }, 100); + setIsSetActive(true); + } + } + }, [tabsData, storageKey]); - // Handle reorder - const handleDragEnd = (event: DragEndEvent) => { - const { active: dragActive, over } = event; - if (dragActive.id !== over?.id && over?.id) { - const oldIndex = tabs.findIndex((t) => t.id.toString() === dragActive.id.toString()); - const newIndex = tabs.findIndex((t) => t.id.toString() === over?.id.toString()); - const newTabs = arrayMove(tabs, oldIndex, newIndex); - setTabs(newTabs); + // Handle reorder + const handleDragEnd = (event: DragEndEvent) => { + const { active: dragActive, over } = event; + if (dragActive.id !== over?.id && over?.id) { + const oldIndex = tabs.findIndex( + (t) => t.id.toString() === dragActive.id.toString() + ); + const newIndex = tabs.findIndex( + (t) => t.id.toString() === over?.id.toString() + ); + const newTabs = arrayMove(tabs, oldIndex, newIndex); + setTabs(newTabs); - const order = newTabs.map((t, i) => ({ id: t.id, index: i })); - localStorage.setItem(storageKey, JSON.stringify(order)); - } - }; + const order = newTabs.map((t, i) => ({ id: t.id, index: i })); + localStorage.setItem(storageKey, JSON.stringify(order)); + } + }; - // Clean up - useEffect(() => { - return () => { - setIsChangeTab(false); - setIsSetActive(false); - setTabs([]); - setActive("0"); - }; - }, []); + // Clean up + useEffect(() => { + return () => { + setIsChangeTab(false); + setIsSetActive(false); + setTabs([]); + setActive("0"); + }; + }, []); - return ( - - { - setIsChangeTab(true); - onChange(val); - setActive(val || "0"); - }} - w={w}> - - - setOpenConfig(true)}> - - - - - - - {tabs.map((tab) => ( - { - setIsChangeTab(true); - onChange(id); - setActive(id); - }} - isStationSettings={isStationSettings} - /> - ))} - + size="xs" + leftSection={} + onClick={() => setOpenDrawerScenario(true)} + > + Scenario + + + + + {tabs.map((tab) => ( + { + setIsChangeTab(true); + onChange(id); + setActive(id); + }} + isStationSettings={isStationSettings} + /> + ))} + - - {Number(active) ? ( - { - setStationEdit(tabsData.find((el) => el.id === Number(active))); - setIsOpenAddStation(true); - setIsEditStation(true); - }}> - - - ) : ( - "" - )} - { - setIsOpenAddStation(true); - setIsEditStation(false); - setStationEdit(undefined); - }}> - - - - - - - ( - {el.userName} - ))}> - - - - - - - - -
- - {user?.userName || user?.user_name || ""} - + + {Number(active) ? ( + { + setStationEdit( + tabsData.find((el) => el.id === Number(active)) + ); + setIsOpenAddStation(true); + setIsEditStation(true); + }} + > + + + ) : ( + "" + )} + { + setIsOpenAddStation(true); + setIsEditStation(false); + setStationEdit(undefined); + }} + > + + + + + + + ( + {el.userName} + ))} + > + + + + + + + + +
+ + {user?.userName || user?.user_name || ""} + - - {user?.email} - -
+ + {user?.email} + +
- -
-
-
- - { - localStorage.removeItem("user"); - window.location.href = "/"; - socket?.disconnect(); - }} - color="red" - leftSection={}> - Logout - - -
-
- + + + + + + { + localStorage.removeItem("user"); + window.location.href = "/"; + socket?.disconnect(); + }} + color="red" + leftSection={} + > + Logout + + + + + - {panels} -
+ {panels} + - setIsHistoryModalOpen(false)} - socket={socket} - stationIds={tabs.map((el) => el.id)} - tabs={tabs} - /> + setIsHistoryModalOpen(false)} + socket={socket} + stationIds={tabs.map((el) => el.id)} + tabs={tabs} + /> - setOpenConfig(false)} - onSave={() => { - onChange(active); - }} - /> + setOpenConfig(false)} + onSave={() => { + onChange(active); + }} + /> - setOpenDrawerScenario(false)} - /> -
- ); + setOpenDrawerScenario(false)} + /> + + ); } diff --git a/FRONTEND/src/components/DrawerControl.tsx b/FRONTEND/src/components/Drawer/DrawerControl.tsx similarity index 99% rename from FRONTEND/src/components/DrawerControl.tsx rename to FRONTEND/src/components/Drawer/DrawerControl.tsx index d269859..d39c3b1 100644 --- a/FRONTEND/src/components/DrawerControl.tsx +++ b/FRONTEND/src/components/Drawer/DrawerControl.tsx @@ -12,8 +12,8 @@ import { } from "@mantine/core"; import { IconRepeat } from "@tabler/icons-react"; import { useEffect, useState } from "react"; -import classes from "./Component.module.css"; -import type { APCProps, SwitchPortsProps, TStation } from "../untils/types"; +import classes from "../Component.module.css"; +import type { APCProps, SwitchPortsProps, TStation } from "../../untils/types"; import type { Socket } from "socket.io-client"; interface DrawerProps { diff --git a/FRONTEND/src/components/Drawer/DrawerLogs.tsx b/FRONTEND/src/components/Drawer/DrawerLogs.tsx new file mode 100644 index 0000000..ce2f161 --- /dev/null +++ b/FRONTEND/src/components/Drawer/DrawerLogs.tsx @@ -0,0 +1,314 @@ +import { useDisclosure } from "@mantine/hooks"; +import { + Button, + Box, + Drawer, + Grid, + Table, + Text, + ScrollArea, + Tooltip, + TextInput, +} from "@mantine/core"; +import { DateInput } from "@mantine/dates"; +import { useEffect, useState } from "react"; +import type { ISystemLog } from "../../untils/types"; +import { + IconDownload, + IconEye, + IconInfoCircle, + IconX, +} from "@tabler/icons-react"; +import classes from "../Component.module.css"; +import moment from "moment"; +import type { Socket } from "socket.io-client"; +import ModalLog from "../Modal/ModalLog"; + +function DrawerLogs({ + socket, + isLogModalOpen, + setIsLogModalOpen, + testLogContent, + setTestLogContent, +}: { + socket: Socket | null; + isLogModalOpen: boolean; + setIsLogModalOpen: (value: React.SetStateAction) => void; + testLogContent: string; + setTestLogContent: (value: React.SetStateAction) => void; +}) { + const [opened, { open, close }] = useDisclosure(false); + const [systemLogs, setSystemLogs] = useState([]); + const [isDownloadLog, setIsDownloadLog] = useState(false); + // const [testLogContent, setTestLogContent] = useState(""); + // const [isLogModalOpen, setIsLogModalOpen] = useState(false); + const [downloadName, setDownloadName] = useState(""); + const [searchFileName, setSearchFileName] = useState(""); + const [fromDate, setFromDate] = useState(null); + const [toDate, setToDate] = useState(null); + + const [filteredLogs, setFilteredLogs] = useState([]); + + useEffect(() => { + if (opened) { + socket?.emit("get_list_logs"); + } + }, [opened]); + + useEffect(() => { + socket?.on("list_logs", (files: string[]) => { + const list: ISystemLog[] = files.map((file) => { + const filename = file.replace(/^.*[\\/]/, ""); + const createAt = filename.match(/\d{8}/); + return { + fileName: + file.split("/")[3] || file.split("/")[2] || file.split("/")[1], + createdAt: createAt ? createAt[0] : "N/A", + path: file, + }; + }); + setSystemLogs( + list.sort( + (a: ISystemLog, b: ISystemLog) => + parseInt(b.createdAt) - parseInt(a.createdAt) + ) + ); + }); + }, [socket]); + + useEffect(() => { + if (isDownloadLog && testLogContent && downloadName) { + const blob = new Blob([testLogContent], { type: "text/plain" }); + // Create a temporary link element + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = downloadName; + // Trigger the download by clicking the link + link.click(); + // Clean up + URL.revokeObjectURL(link.href); + setIsDownloadLog(false); + setTestLogContent(""); + setDownloadName(""); + } + }, [testLogContent]); + + useEffect(() => { + // Chuẩn bị trước các giá trị search/date để tránh tính lại trong filter cho từng phần tử + const trimmedSearch = searchFileName.trim().toLowerCase(); + const hasSearch = trimmedSearch.length > 0; + const fromMoment = fromDate ? moment(fromDate).startOf("day") : null; + const toMoment = toDate ? moment(toDate).endOf("day") : null; + + const delayDebounceFn = setTimeout(() => { + // Nếu không có filter nào, tránh filter tốn công, gán thẳng + if (!hasSearch && !fromMoment && !toMoment) { + setFilteredLogs(systemLogs); + return; + } + + const next = systemLogs.filter((log) => { + if (hasSearch && !log.fileName.toLowerCase().includes(trimmedSearch)) { + return false; + } + + const logDate = moment(log.createdAt, "YYYYMMDD"); + if (fromMoment && !logDate.isSameOrAfter(fromMoment)) { + return false; + } + if (toMoment && !logDate.isSameOrBefore(toMoment)) { + return false; + } + + return true; + }); + + setFilteredLogs(next); + }, 500); + + return () => clearTimeout(delayDebounceFn); + }, [searchFileName, fromDate, toDate, systemLogs]); + + return ( + <> + + + Format: + + YYYYMMDD-AUTO-Session.{`{Station name}`}-{`{Station ID}`}- + {`{Station IP}`}-{`{Line number}`} + .log + + + } + position="right" + > + + List Logs + + + + } + > + + + + + + + setSearchFileName(event.currentTarget.value) + } + rightSection={ + searchFileName ? ( + setSearchFileName("")} + /> + ) : null + } + rightSectionPointerEvents="auto" + size="xs" + /> + + + setFromDate(value as Date | null)} + placeholder="From date" + valueFormat="DD/MM/YYYY" + size="xs" + clearable + /> + + + setToDate(value as Date | null)} + placeholder="To date" + valueFormat="DD/MM/YYYY" + size="xs" + clearable + /> + + + + + + + + File name + Created at + + + + + {filteredLogs.map((element) => ( + + {element.fileName} + + + {moment(element.createdAt).format("DD/MM/YYYY")} + + + + + { + setTestLogContent(""); + socket?.emit("get_content_log", { + line: { systemLogUrl: element.path }, + }); + setIsLogModalOpen(true); + }} + width={20} + /> + { + socket?.emit("get_content_log", { + line: { systemLogUrl: element.path }, + }); + setIsDownloadLog(true); + setTestLogContent(""); + setDownloadName( + element.path.split("/")[3] || + element.path.split("/")[2] || + element.path.split("/")[1] + ); + }} + width={20} + /> + + + + ))} + +
+
+
+
+ + {isLogModalOpen && ( + { + setIsLogModalOpen(false); + }} + testLogContent={testLogContent} + /> + )} +
+ + + + ); +} + +export default DrawerLogs; diff --git a/FRONTEND/src/components/DrawerLogs.tsx b/FRONTEND/src/components/DrawerLogs.tsx deleted file mode 100644 index cc2511d..0000000 --- a/FRONTEND/src/components/DrawerLogs.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { useDisclosure } from "@mantine/hooks"; -import { Button, Box, Drawer, Grid, Table, Text, ScrollArea, Tooltip, TextInput } from "@mantine/core"; -import { DateInput } from "@mantine/dates"; -import { useEffect, useState } from "react"; -import type { ISystemLog } from "../untils/types"; -import { IconDownload, IconEye, IconInfoCircle, IconX } from "@tabler/icons-react"; -import classes from "./Component.module.css"; -import moment from "moment"; -import type { Socket } from "socket.io-client"; -import ModalLog from "./ModalLog"; - -function DrawerLogs({ - socket, - isLogModalOpen, - setIsLogModalOpen, - testLogContent, - setTestLogContent, -}: { - socket: Socket | null; - isLogModalOpen: boolean; - setIsLogModalOpen: (value: React.SetStateAction) => void; - testLogContent: string; - setTestLogContent: (value: React.SetStateAction) => void; -}) { - const [opened, { open, close }] = useDisclosure(false); - const [systemLogs, setSystemLogs] = useState([]); - const [isDownloadLog, setIsDownloadLog] = useState(false); - // const [testLogContent, setTestLogContent] = useState(""); - // const [isLogModalOpen, setIsLogModalOpen] = useState(false); - const [downloadName, setDownloadName] = useState(""); - const [searchFileName, setSearchFileName] = useState(""); - const [fromDate, setFromDate] = useState(null); - const [toDate, setToDate] = useState(null); - - const [filteredLogs, setFilteredLogs] = useState([]); - - useEffect(() => { - if (opened) { - socket?.emit("get_list_logs"); - } - }, [opened]); - - useEffect(() => { - socket?.on("list_logs", (files: string[]) => { - const list: ISystemLog[] = files.map((file) => { - const filename = file.replace(/^.*[\\/]/, ""); - const createAt = filename.match(/\d{8}/); - return { - fileName: file.split("/")[3] || file.split("/")[2] || file.split("/")[1], - createdAt: createAt ? createAt[0] : "N/A", - path: file, - }; - }); - setSystemLogs(list.sort((a: ISystemLog, b: ISystemLog) => parseInt(b.createdAt) - parseInt(a.createdAt))); - }); - }, [socket]); - - useEffect(() => { - if (isDownloadLog && testLogContent && downloadName) { - const blob = new Blob([testLogContent], { type: "text/plain" }); - // Create a temporary link element - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = downloadName; - // Trigger the download by clicking the link - link.click(); - // Clean up - URL.revokeObjectURL(link.href); - setIsDownloadLog(false); - setTestLogContent(""); - setDownloadName(""); - } - }, [testLogContent]); - - useEffect(() => { - // Chuẩn bị trước các giá trị search/date để tránh tính lại trong filter cho từng phần tử - const trimmedSearch = searchFileName.trim().toLowerCase(); - const hasSearch = trimmedSearch.length > 0; - const fromMoment = fromDate ? moment(fromDate).startOf("day") : null; - const toMoment = toDate ? moment(toDate).endOf("day") : null; - - const delayDebounceFn = setTimeout(() => { - // Nếu không có filter nào, tránh filter tốn công, gán thẳng - if (!hasSearch && !fromMoment && !toMoment) { - setFilteredLogs(systemLogs); - return; - } - - const next = systemLogs.filter((log) => { - if (hasSearch && !log.fileName.toLowerCase().includes(trimmedSearch)) { - return false; - } - - const logDate = moment(log.createdAt, "YYYYMMDD"); - if (fromMoment && !logDate.isSameOrAfter(fromMoment)) { - return false; - } - if (toMoment && !logDate.isSameOrBefore(toMoment)) { - return false; - } - - return true; - }); - - setFilteredLogs(next); - }, 500); - - return () => clearTimeout(delayDebounceFn); - }, [searchFileName, fromDate, toDate, systemLogs]); - - return ( - <> - - - Format: - - YYYYMMDD-AUTO-Session.{`{Station name}`}-{`{Station ID}`}-{`{Station IP}`}-{`{Line number}`} - .log - - - } - position="right"> - - List Logs - - - - }> - - - - - - setSearchFileName(event.currentTarget.value)} - rightSection={ - searchFileName ? ( - setSearchFileName("")} - /> - ) : null - } - rightSectionPointerEvents="auto" - size="xs" - /> - - - setFromDate(value as Date | null)} - placeholder="From date" - valueFormat="DD/MM/YYYY" - size="xs" - clearable - /> - - - setToDate(value as Date | null)} - placeholder="To date" - valueFormat="DD/MM/YYYY" - size="xs" - clearable - /> - - - - - - - - File name - Created at - - - - - {filteredLogs.map((element) => ( - - {element.fileName} - - {moment(element.createdAt).format("DD/MM/YYYY")} - - - - { - setTestLogContent(""); - socket?.emit("get_content_log", { - line: { systemLogUrl: element.path }, - }); - setIsLogModalOpen(true); - }} - width={20} - /> - { - socket?.emit("get_content_log", { - line: { systemLogUrl: element.path }, - }); - setIsDownloadLog(true); - setTestLogContent(""); - setDownloadName(element.path.split("/")[3] || element.path.split("/")[2] || element.path.split("/")[1]); - }} - width={20} - /> - - - - ))} - -
-
-
-
- - {isLogModalOpen && ( - { - setIsLogModalOpen(false); - }} - testLogContent={testLogContent} - /> - )} -
- - - - ); -} - -export default DrawerLogs; diff --git a/FRONTEND/src/components/InputHistory.tsx b/FRONTEND/src/components/InputHistory.tsx new file mode 100644 index 0000000..ab03ec6 --- /dev/null +++ b/FRONTEND/src/components/InputHistory.tsx @@ -0,0 +1,135 @@ +import { useEffect, useState, useCallback, useRef } from "react"; +import { CloseButton, TextInput } from "@mantine/core"; +import type { TLine, TStation } from "../untils/types"; +import type { Socket } from "socket.io-client"; + +const HISTORY_KEY = "command_history"; +const MAX_HISTORY = 20; + +export default function InputHistory({ + selectedLines, + socket, + station, +}: { + selectedLines: TLine[]; + socket: Socket | null; + station: TStation; +}) { + const [value, setValue] = useState(""); + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const inputRef = useRef(null); + + useEffect(() => { + if (selectedLines?.length > 1 && inputRef?.current) { + inputRef?.current?.focus(); + } + }, [selectedLines?.length]); + + // Load history từ localStorage + useEffect(() => { + try { + const saved = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]"); + if (Array.isArray(saved)) setHistory(saved); + } catch (error) { + console.error("Failed to load command history:", error); + } + }, []); + + // Lưu history khi thay đổi + const saveHistory = (list: string[]) => { + localStorage.setItem(HISTORY_KEY, JSON.stringify(list)); + }; + + // Xử lý Enter + const handleEnter = useCallback(() => { + const listLine = selectedLines.length ? selectedLines : station?.lines; + if (listLine?.length) { + socket?.emit("write_command_line_from_web", { + lineIds: listLine.map((line) => line.id), + stationId: station.id, + command: value + "\r\n", + }); + } + + // Reset + setHistoryIndex(-1); + setValue(""); + + if (!value.trim()) return; + + let newHistory = [...history]; + + // Không thêm nếu giống command cuối cùng + if (newHistory[newHistory.length - 1] !== value.trim()) { + newHistory.push(value.trim()); + } + + // Giới hạn 10 lệnh + if (newHistory.length > MAX_HISTORY) { + newHistory = newHistory.slice(newHistory.length - MAX_HISTORY); + } + + setHistory(newHistory); + saveHistory(newHistory); + + // Reset + // setHistoryIndex(-1); + // setValue(""); + + // console.log("Command:", value); + }, [value, history, selectedLines, socket, station]); + + // Xử lý phím + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleEnter(); + } + + if (e.key === "ArrowUp") { + e.preventDefault(); + if (history.length === 0) return; + + const newIndex = + historyIndex < 0 ? history.length - 1 : Math.max(0, historyIndex - 1); + + setHistoryIndex(newIndex); + setValue(history[newIndex]); + } + + if (e.key === "ArrowDown") { + e.preventDefault(); + if (history.length === 0) return; + if (historyIndex === -1) return; + const newIndex = + historyIndex >= history.length - 1 ? -1 : historyIndex + 1; + + setHistoryIndex(newIndex); + setValue(newIndex === -1 ? "" : history[newIndex]); + } + }; + + return ( + setValue(e.target.value)} + onKeyDown={handleKeyDown} + radius="md" + rightSectionPointerEvents="all" + rightSection={ + setValue("")} + style={{ + display: value ? undefined : "none", + }} + /> + } + /> + ); +} diff --git a/FRONTEND/src/components/ModalConfig.tsx b/FRONTEND/src/components/Modal/ModalConfig.tsx similarity index 100% rename from FRONTEND/src/components/ModalConfig.tsx rename to FRONTEND/src/components/Modal/ModalConfig.tsx diff --git a/FRONTEND/src/components/ModalHistory.tsx b/FRONTEND/src/components/Modal/ModalHistory.tsx similarity index 71% rename from FRONTEND/src/components/ModalHistory.tsx rename to FRONTEND/src/components/Modal/ModalHistory.tsx index 264b6f0..8a4879c 100644 --- a/FRONTEND/src/components/ModalHistory.tsx +++ b/FRONTEND/src/components/Modal/ModalHistory.tsx @@ -7,9 +7,9 @@ import { CloseButton, Button, } from "@mantine/core"; -import classes from "./Component.module.css"; +import classes from "../Component.module.css"; import type { Socket } from "socket.io-client"; -import type { TStation } from "../untils/types"; +import type { TStation } from "../../untils/types"; interface HistoryItem { id: number; @@ -101,7 +101,7 @@ function ModalHistory({ }); } }); - + // Sort by tabs order if (tabs.length > 0) { merged.sort((a, b) => { @@ -113,7 +113,7 @@ function ModalHistory({ return 0; }); } - + if (merged.length > 0 && !activeStation) { setActiveStation(merged[0].stationId.toString()); } @@ -132,7 +132,7 @@ function ModalHistory({ isBlockingScrollRef.current = false; lastScrollTopRef.current = 0; } - + return () => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); @@ -145,16 +145,17 @@ function ModalHistory({ if (!scrollViewportRef.current || !opened) return; const scrollContainer = scrollViewportRef.current; - + const handleWheel = (e: WheelEvent) => { if (!isBlockingScrollRef.current) return; - + const scrollHeight = scrollContainer.scrollHeight; const clientHeight = scrollContainer.clientHeight; const currentScrollTop = scrollContainer.scrollTop; const maxScrollTop = scrollHeight - clientHeight; - const isAtBottom = Math.abs(currentScrollTop + clientHeight - scrollHeight) < 20; - + const isAtBottom = + Math.abs(currentScrollTop + clientHeight - scrollHeight) < 20; + // If at bottom and trying to scroll down, prevent it if (isAtBottom && e.deltaY > 0) { e.preventDefault(); @@ -170,7 +171,7 @@ function ModalHistory({ }; scrollContainer.addEventListener("wheel", handleWheel, { passive: false }); - + return () => { scrollContainer.removeEventListener("wheel", handleWheel); }; @@ -178,7 +179,7 @@ function ModalHistory({ // Scroll to station when activeStation changes (only when clicked, not when auto-detected) const isManualScrollRef = useRef(false); - + useEffect(() => { if (activeStation && isManualScrollRef.current) { setTimeout(() => { @@ -192,7 +193,6 @@ function ModalHistory({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeStation]); - // Utility function to format timestamp (can be used later if needed) // const formatTimestamp = (timestamp: number) => { // const date = new Date(timestamp); @@ -263,12 +263,10 @@ function ModalHistory({ // Sort by tabs order if tabs is provided const mergedHistoryData: StationHistory[] = (() => { const result: StationHistory[] = [...historyData]; - + if (tabs.length > 0) { tabs.forEach((tab) => { - const existingIndex = result.findIndex( - (h) => h.stationId === tab.id - ); + const existingIndex = result.findIndex((h) => h.stationId === tab.id); if (existingIndex === -1) { // Station from tabs not found in historyData, add it with empty history result.push({ @@ -281,12 +279,12 @@ function ModalHistory({ result[existingIndex].stationName = tab.name; } }); - + // Sort by tabs order to maintain the same order as tabs result.sort((a, b) => { const aIndex = tabs.findIndex((tab) => tab.id === a.stationId); const bIndex = tabs.findIndex((tab) => tab.id === b.stationId); - + // If both are in tabs, sort by tabs order if (aIndex !== -1 && bIndex !== -1) { return aIndex - bIndex; @@ -299,7 +297,7 @@ function ModalHistory({ return 0; }); } - + return result; })(); @@ -315,12 +313,13 @@ function ModalHistory({ // Get lines from tabs for this station const tabStation = tabs.find((tab) => tab.id === station.stationId); const tabLines = tabStation?.lines || []; - + // Group filtered history by line number - const groupedHistory = station.filteredHistory.length > 0 - ? groupHistoryByLine(station.filteredHistory) - : new Map(); - + const groupedHistory = + station.filteredHistory.length > 0 + ? groupHistoryByLine(station.filteredHistory) + : new Map(); + // Ensure all lines from tabs are included in groupedHistory (even if empty) tabLines.forEach((line) => { const lineNumber = line.lineNumber || line.line_number || line.port; @@ -329,7 +328,7 @@ function ModalHistory({ groupedHistory.set(lineNumber, []); } }); - + return { ...station, groupedHistory, @@ -341,29 +340,30 @@ function ModalHistory({ // Try to scroll to content first (line group đầu tiên hoặc message) const contentElement = stationContentRefs.current.get(stationId); const stationHeader = stationRefs.current.get(stationId); - + if (contentElement && scrollViewportRef.current) { const scrollContainer = scrollViewportRef.current; const containerRect = scrollContainer.getBoundingClientRect(); const contentRect = contentElement.getBoundingClientRect(); const currentScrollTop = scrollContainer.scrollTop; - + // Tính toán vị trí scroll để content nằm ngay dưới sticky header // Sticky header có position: sticky, top: 0, nên luôn ở top của viewport const headerHeight = stationHeader ? stationHeader.offsetHeight : 0; - + // Scroll đến vị trí sao cho content nằm ngay dưới sticky header // contentRect.top - containerRect.top là khoảng cách từ content đến top của container // Trừ đi headerHeight để content nằm ngay dưới header - const targetScrollTop = currentScrollTop + (contentRect.top - containerRect.top) - headerHeight; - + const targetScrollTop = + currentScrollTop + (contentRect.top - containerRect.top) - headerHeight; + scrollContainer.scrollTo({ top: Math.max(0, targetScrollTop), behavior: "smooth", }); return; } - + // Fallback: scroll to header if content ref not found const stationElement = stationRefs.current.get(stationId); if (stationElement && scrollViewportRef.current) { @@ -371,7 +371,7 @@ function ModalHistory({ const containerTop = scrollContainer.getBoundingClientRect().top; const elementTop = stationElement.getBoundingClientRect().top; const scrollTop = scrollContainer.scrollTop; - + scrollContainer.scrollTo({ top: scrollTop + elementTop - containerTop, behavior: "smooth", @@ -511,7 +511,8 @@ function ModalHistory({ h="calc(75vh - 80px)" viewportRef={scrollViewportRef} onScrollPositionChange={() => { - if (isManualScrollRef.current || !scrollViewportRef.current) return; + if (isManualScrollRef.current || !scrollViewportRef.current) + return; // Debounce để scroll mượt hơn if (scrollTimeoutRef.current) { @@ -522,33 +523,42 @@ function ModalHistory({ if (!scrollViewportRef.current) return; const scrollContainer = scrollViewportRef.current; - const containerRect = scrollContainer.getBoundingClientRect(); + const containerRect = + scrollContainer.getBoundingClientRect(); const topThreshold = containerRect.top + 10; // Sát top với threshold 10px - + // Check if scrolled to bottom const scrollHeight = scrollContainer.scrollHeight; const clientHeight = scrollContainer.clientHeight; const currentScrollTop = scrollContainer.scrollTop; - const isAtBottom = Math.abs(currentScrollTop + clientHeight - scrollHeight) < 20; + const isAtBottom = + Math.abs( + currentScrollTop + clientHeight - scrollHeight + ) < 20; // Check if currently at last station with no data - prevent scrolling if (allGroupedStations.length > 0) { - const lastStation = allGroupedStations[allGroupedStations.length - 1]; - const lastStationHasNoData = lastStation.groupedHistory.size === 0; + const lastStation = + allGroupedStations[allGroupedStations.length - 1]; + const lastStationHasNoData = + lastStation.groupedHistory.size === 0; const lastStationIdStr = String(lastStation.stationId); - + // If at bottom and last station has no data, prevent scrolling if (isAtBottom && lastStationHasNoData) { if (lastStationIdStr !== activeStation) { setActiveStation(lastStationIdStr); } - + // Block scroll if trying to scroll down isBlockingScrollRef.current = true; const maxScrollTop = scrollHeight - clientHeight; - + // If trying to scroll down (scrollTop increased), reset to bottom - if (currentScrollTop > lastScrollTopRef.current && currentScrollTop < maxScrollTop - 5) { + if ( + currentScrollTop > lastScrollTopRef.current && + currentScrollTop < maxScrollTop - 5 + ) { scrollContainer.scrollTo({ top: maxScrollTop, behavior: "auto", // Use auto for instant reset @@ -556,25 +566,30 @@ function ModalHistory({ lastScrollTopRef.current = maxScrollTop; return; } - + // Keep scroll at bottom lastScrollTopRef.current = maxScrollTop; return; } - + // If currently viewing last station with no data const currentStation = allGroupedStations.find( (s) => s.stationId.toString() === activeStation ); - const isLastStation = currentStation?.stationId === lastStation.stationId; - const hasNoData = currentStation?.groupedHistory.size === 0; - + const isLastStation = + currentStation?.stationId === lastStation.stationId; + const hasNoData = + currentStation?.groupedHistory.size === 0; + if (isLastStation && hasNoData) { isBlockingScrollRef.current = true; const maxScrollTop = scrollHeight - clientHeight; - + // If trying to scroll down past bottom, reset to bottom - if (currentScrollTop > lastScrollTopRef.current && currentScrollTop < maxScrollTop - 5) { + if ( + currentScrollTop > lastScrollTopRef.current && + currentScrollTop < maxScrollTop - 5 + ) { scrollContainer.scrollTo({ top: maxScrollTop, behavior: "auto", @@ -582,7 +597,7 @@ function ModalHistory({ lastScrollTopRef.current = maxScrollTop; return; } - + // Keep scroll at bottom if already there if (isAtBottom) { lastScrollTopRef.current = maxScrollTop; @@ -600,7 +615,8 @@ function ModalHistory({ // If at bottom, select the last station if (isAtBottom && allGroupedStations.length > 0) { - const lastStation = allGroupedStations[allGroupedStations.length - 1]; + const lastStation = + allGroupedStations[allGroupedStations.length - 1]; const lastStationIdStr = String(lastStation.stationId); if (lastStationIdStr !== activeStation) { setActiveStation(lastStationIdStr); @@ -609,34 +625,53 @@ function ModalHistory({ // Find which station header is closest to top // Ưu tiên: header đã vượt qua top threshold (ở trên) > header chưa đến (ở dưới) - type StationInfo = { id: number; distance: number; isAbove: boolean }; + type StationInfo = { + id: number; + distance: number; + isAbove: boolean; + }; let bestStation: StationInfo | null = null; - Array.from(stationRefs.current.entries()).forEach(([stationId, element]) => { - const elementRect = element.getBoundingClientRect(); - const elementTop = elementRect.top; - const elementBottom = elementRect.bottom; + Array.from(stationRefs.current.entries()).forEach( + ([stationId, element]) => { + const elementRect = element.getBoundingClientRect(); + const elementTop = elementRect.top; + const elementBottom = elementRect.bottom; - // Check if station header is visible (có phần nào đó trong viewport) - const isVisible = elementBottom >= containerRect.top && elementTop <= containerRect.bottom; + // Check if station header is visible (có phần nào đó trong viewport) + const isVisible = + elementBottom >= containerRect.top && + elementTop <= containerRect.bottom; - if (isVisible) { - const isAbove = elementTop <= topThreshold; // Header đã vượt qua top - const distance = Math.abs(elementTop - topThreshold); - const stationIdNum = Number(stationId); + if (isVisible) { + const isAbove = elementTop <= topThreshold; // Header đã vượt qua top + const distance = Math.abs( + elementTop - topThreshold + ); + const stationIdNum = Number(stationId); - // Ưu tiên header đã vượt qua top (isAbove = true) - if (!bestStation || - (isAbove && !bestStation.isAbove) || - (isAbove === bestStation.isAbove && distance < bestStation.distance)) { - bestStation = { id: stationIdNum, distance, isAbove }; + // Ưu tiên header đã vượt qua top (isAbove = true) + if ( + !bestStation || + (isAbove && !bestStation.isAbove) || + (isAbove === bestStation.isAbove && + distance < bestStation.distance) + ) { + bestStation = { + id: stationIdNum, + distance, + isAbove, + }; + } } } - }); + ); // Update active station if found if (bestStation) { - const stationIdStr = String((bestStation as StationInfo).id); + const stationIdStr = String( + (bestStation as StationInfo).id + ); if (stationIdStr !== activeStation) { setActiveStation(stationIdStr); } @@ -648,18 +683,24 @@ function ModalHistory({ {allGroupedStations.length > 0 ? ( <> {allGroupedStations.map((station) => ( - {/* Station Title */} { if (el) { - stationRefs.current.set(station.stationId, el); + stationRefs.current.set( + station.stationId, + el + ); } }} style={{ @@ -684,110 +725,116 @@ function ModalHistory({ Array.from(station.groupedHistory.entries()) .sort(([lineA], [lineB]) => lineA - lineB) .map(([lineNumber, items], lineIndex) => ( - { - // Set ref cho line group đầu tiên (nội dung đầu tiên của station) - if (el && lineIndex === 0) { - stationContentRefs.current.set(station.stationId, el); - } - }} - style={{ - marginBottom: "8px", - border: "1px solid #dee2e6", - borderRadius: "4px", - overflow: "hidden", - }} - > - {/* Header của nhóm - hiển thị line number */} - - - Line {lineNumber} - - - - {/* Các items trong nhóm */} - {items - ?.filter((_, i) => - activeTimePeriod === "current" - ? i === 0 - : true - ) - .map((item, itemIndex) => ( { + // Set ref cho line group đầu tiên (nội dung đầu tiên của station) + if (el && lineIndex === 0) { + stationContentRefs.current.set( + station.stationId, + el + ); + } + }} style={{ - padding: "8px 16px 8px 32px", // Tăng padding-left lên 32px - borderTop: - itemIndex > 0 - ? "1px solid #f1f3f5" - : "none", - backgroundColor: - itemIndex % 2 === 0 - ? "white" - : "#f8f9fa", + marginBottom: "8px", + border: "1px solid #dee2e6", + borderRadius: "4px", + overflow: "hidden", }} > - - - + Line {lineNumber} + + + + {/* Các items trong nhóm */} + {items + ?.filter((_, i) => + activeTimePeriod === "current" + ? i === 0 + : true + ) + .map((item, itemIndex) => ( + 0 + ? "1px solid #f1f3f5" + : "none", + backgroundColor: + itemIndex % 2 === 0 + ? "white" + : "#f8f9fa", }} > - {item.pid} - {" "} - | {item.vid} SN:{" "} - - {item.sn} - - - - - {item.scenario} | - - - {new Date( - item.timestamp - ).toLocaleString()} - - - + + + + {item.pid} + {" "} + | {item.vid} SN:{" "} + + {item.sn} + + + + + {item.scenario} | + + + {new Date( + item.timestamp + ).toLocaleString()} + + + + + ))} - ))} - - )) + )) ) : ( { // Set ref cho message "No history" (nội dung đầu tiên của station) if (el) { - stationContentRefs.current.set(station.stationId, el); + stationContentRefs.current.set( + station.stationId, + el + ); } }} style={{ @@ -797,9 +844,12 @@ function ModalHistory({ }} > - No history data available for {TIME_PERIODS.find( - (p) => p.value === activeTimePeriod - )?.label} + No history data available for{" "} + { + TIME_PERIODS.find( + (p) => p.value === activeTimePeriod + )?.label + } )} diff --git a/FRONTEND/src/components/ModalLog.tsx b/FRONTEND/src/components/Modal/ModalLog.tsx similarity index 95% rename from FRONTEND/src/components/ModalLog.tsx rename to FRONTEND/src/components/Modal/ModalLog.tsx index 247e192..25d0448 100644 --- a/FRONTEND/src/components/ModalLog.tsx +++ b/FRONTEND/src/components/Modal/ModalLog.tsx @@ -1,6 +1,6 @@ import { Modal, Text } from "@mantine/core"; -import classes from "./Component.module.css"; -import { convertTimestampToDate } from "../untils/helper"; +import classes from "../Component.module.css"; +import { convertTimestampToDate } from "../../untils/helper"; const ModalLog = ({ opened, diff --git a/FRONTEND/src/components/DrawerScenario.tsx b/FRONTEND/src/components/Modal/ModalScenario.tsx similarity index 52% rename from FRONTEND/src/components/DrawerScenario.tsx rename to FRONTEND/src/components/Modal/ModalScenario.tsx index 75ef636..09ec6b4 100644 --- a/FRONTEND/src/components/DrawerScenario.tsx +++ b/FRONTEND/src/components/Modal/ModalScenario.tsx @@ -10,17 +10,17 @@ import { Flex, CloseButton, } from "@mantine/core"; -import classes from "./Component.module.css"; +import classes from "../Component.module.css"; import TableRows from "./Scenario/TableRows"; import { useEffect, useState } from "react"; import { useForm } from "@mantine/form"; -import DialogConfirm from "./DialogConfirm"; -import type { IBodyScenario, IScenario } from "../untils/types"; +import DialogConfirm from "../DialogConfirm"; +import type { IBodyScenario, IScenario } from "../../untils/types"; import axios from "axios"; import { notifications } from "@mantine/notifications"; const apiUrl = import.meta.env.VITE_BACKEND_URL; -function DrawerScenario({ +function ModalScenario({ scenarios, setScenarios, externalOpened, @@ -32,10 +32,10 @@ function DrawerScenario({ onExternalClose?: () => void; }) { const [opened, { close }] = useDisclosure(false); - + // Sử dụng external state nếu được provide, nếu không thì dùng internal state const isOpened = externalOpened !== undefined ? externalOpened : opened; - + const handleClose = () => { if (onExternalClose) { onExternalClose(); @@ -267,92 +267,95 @@ function DrawerScenario({ }} className={classes.hideScrollBar} > - - {/* Sidebar - List Scenarios */} - - - - {scenarios.map((scenario) => ( - - ))} - - - - - {/* Main Content */} - - - - - - form.setFieldValue("title", e.target.value) - } - required - /> - - - - form.setFieldValue("timeout", e.target.value) - } - required - /> - - + {/* Sidebar - List Scenarios */} + - {/* + + {scenarios.map((scenario) => ( + + ))} + + + + + {/* Main Content */} + + + + + + form.setFieldValue("title", e.target.value) + } + required + /> + + + + form.setFieldValue("timeout", e.target.value) + } + required + /> + + + {/* */} - - -
- {isEdit && ( - - )} - + )} + +
+
+
+
+
+ + - Save - - -
-
-
-
- - - - - - # - - {/* Expect */} - Expect - - Send - Delay(ms) - Repeat - - - - - {form.values.body.map( - (element: IBodyScenario, i: number) => ( - - ) - )} - -
-
-
-
-
+ + + + # + + {/* Expect */} + Expect + + Send + Delay(ms) + Repeat + + + + + {form.values.body.map( + (element: IBodyScenario, i: number) => ( + + ) + )} + +
+ +
+ + @@ -468,4 +471,4 @@ function DrawerScenario({ ); } -export default DrawerScenario; +export default ModalScenario; diff --git a/FRONTEND/src/components/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx similarity index 99% rename from FRONTEND/src/components/ModalTerminal.tsx rename to FRONTEND/src/components/Modal/ModalTerminal.tsx index fbc8fb6..7626668 100644 --- a/FRONTEND/src/components/ModalTerminal.tsx +++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx @@ -21,8 +21,8 @@ import type { THistoryTicket, TLine, TStation, -} from "../untils/types"; -import TerminalCLI from "./TerminalXTerm"; +} from "../../untils/types"; +import TerminalCLI from "../TerminalXTerm"; import type { Socket } from "socket.io-client"; import { useEffect, useMemo, useState } from "react"; import { @@ -30,13 +30,13 @@ import { IconCircleDot, IconInfoCircle, } from "@tabler/icons-react"; -import { ButtonDPELP, ButtonScenario } from "./ButtonAction"; -import CopyIcon from "./CopyIcon"; +import { ButtonDPELP, ButtonScenario } from "../ButtonAction"; +import CopyIcon from "../CopyIcon"; import moment from "moment"; import axios from "axios"; import { notifications } from "@mantine/notifications"; -import classes from "./Component.module.css"; -import { listBaudDefault } from "../untils/constanst"; +import classes from "../Component.module.css"; +import { listBaudDefault } from "../../untils/constanst"; import { motion } from "motion/react"; const apiUrl = import.meta.env.VITE_BACKEND_URL; diff --git a/FRONTEND/src/components/Scenario/Scenario.module.css b/FRONTEND/src/components/Modal/Scenario/Scenario.module.css similarity index 100% rename from FRONTEND/src/components/Scenario/Scenario.module.css rename to FRONTEND/src/components/Modal/Scenario/Scenario.module.css diff --git a/FRONTEND/src/components/Scenario/TableRows.tsx b/FRONTEND/src/components/Modal/Scenario/TableRows.tsx similarity index 98% rename from FRONTEND/src/components/Scenario/TableRows.tsx rename to FRONTEND/src/components/Modal/Scenario/TableRows.tsx index 53bb3a0..efbf242 100644 --- a/FRONTEND/src/components/Scenario/TableRows.tsx +++ b/FRONTEND/src/components/Modal/Scenario/TableRows.tsx @@ -1,7 +1,7 @@ import { Table, TextInput } from "@mantine/core"; import { IconRowInsertTop, IconX } from "@tabler/icons-react"; import classes from "./Scenario.module.css"; -import { numberOnly } from "../../untils/helper"; +import { numberOnly } from "../../../untils/helper"; interface IPayload { element: any; diff --git a/README.md b/README.md new file mode 100644 index 0000000..60564fc --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# BACKEND + +### 1. Copy .env.example -> .env + +### 2. npm install + +### 3. node ace migration:run + +### 4. npm run dev + +# FRONTEND + +### 1. Copy .env.example -> .env + +### 2. npm install + +### 3. npm run dev + +# Server Redis + +### 1. sudo apt install redis-server + +### 2. sudo systemctl enable redis-server + +### 3. sudo systemctl start redis-server