Refactor BottomToolBar and DrawerControl UI layout

Improved the BottomToolBar layout by wrapping it in a Grid and adjusting tab styles and heights for better alignment. Updated DrawerControl to refine port group rendering, including scrollable areas for large groups, reduced card widths, and minor style tweaks for consistency and usability.
This commit is contained in:
nguyentrungthat 2025-11-14 16:29:35 +07:00
parent c6b3962542
commit abfae279da
3 changed files with 424 additions and 299 deletions

View File

@ -419,7 +419,7 @@ function App() {
borderRadius: 8,
}}
>
<ScrollArea h={"63vh"}>
<ScrollArea h={"73vh"}>
{station.lines.length > 8 ? (
<Grid
style={{

View File

@ -3,6 +3,7 @@ import {
Button,
CloseButton,
Flex,
Grid,
Input,
Menu,
ScrollArea,
@ -55,268 +56,285 @@ const BottomToolBar = ({
const [activeTabBottom, setActiveBottom] = useState<string>("command");
return (
<Tabs
defaultValue="command"
orientation="vertical"
value={activeTabBottom}
onChange={(val) => {
setActiveBottom(val || "command");
}}
className={classes.containerBottom}
>
<Tabs.List>
<Tabs.Tab
style={{
backgroundColor: activeTabBottom === "command" ? "#c8d9fd" : "",
<Grid>
<Grid.Col span={1}></Grid.Col>
<Grid.Col span={10}>
<Tabs
defaultValue="command"
orientation="vertical"
value={activeTabBottom}
onChange={(val) => {
setActiveBottom(val || "command");
}}
value="command"
className={classes.containerBottom}
style={{ height: "14vh" }}
>
Command Line
</Tabs.Tab>
<Tabs.Tab
style={{
backgroundColor: activeTabBottom === "apc" ? "#c8d9fd" : "",
}}
value="apc"
>
APC
</Tabs.Tab>
<Tabs.Tab
style={{
backgroundColor: activeTabBottom === "switch" ? "#c8d9fd" : "",
}}
value="switch"
>
Switch
</Tabs.Tab>
</Tabs.List>
<Tabs.List>
<Tabs.Tab
style={{
backgroundColor: activeTabBottom === "command" ? "#c8d9fd" : "",
fontSize: "13px",
paddingTop: "8px",
paddingBottom: "8px",
}}
value="command"
>
Command Line
</Tabs.Tab>
<Tabs.Tab
style={{
backgroundColor: activeTabBottom === "apc" ? "#c8d9fd" : "",
fontSize: "13px",
paddingTop: "8px",
paddingBottom: "8px",
}}
value="apc"
>
APC
</Tabs.Tab>
<Tabs.Tab
style={{
backgroundColor: activeTabBottom === "switch" ? "#c8d9fd" : "",
fontSize: "13px",
paddingTop: "8px",
paddingBottom: "8px",
}}
value="switch"
>
Switch
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="command" p={"xs"}>
<Flex justify={"space-between"}>
<ScrollArea h={"17vh"}>
<Flex wrap={"wrap"} gap={"xs"} w={"400px"}>
{selectedLines.map((el) => (
<Box
key={el.id}
style={{
paddingLeft: "4px",
height: "30px",
width: "80px",
backgroundColor: "#d4e3ff",
borderRadius: "8px",
}}
>
<Flex align={"center"} justify={"center"} gap={"4px"}>
<Text fz={"12px"}>Line {el.lineNumber}</Text>
<CloseButton
style={{ minWidth: "24px" }}
aria-label="Clear input"
onClick={() => {
setSelectedLines(
selectedLines.filter((line) => line.id !== el.id)
);
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el.stationId || el.station_id,
});
<Tabs.Panel value="command" p={"xs"}>
<Flex justify={"space-between"}>
<ScrollArea h={"10vh"}>
<Flex wrap={"wrap"} gap={"xs"} w={"400px"}>
{selectedLines.map((el) => (
<Box
key={el.id}
style={{
paddingLeft: "4px",
height: "30px",
width: "80px",
backgroundColor: "#d4e3ff",
borderRadius: "8px",
}}
/>
</Flex>
</Box>
))}
</Flex>
</ScrollArea>
<Box pl={"md"} pr={"md"}>
<Flex justify={"space-between"} mb={"xs"}>
<Flex></Flex>
<Button
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="orange"
size="xs"
radius="md"
onClick={() => {
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: "spam_break",
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}
}}
>
Send Break
</Button>
</Flex>
<Box>
<Input
style={{
width: "600px",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}}
placeholder={"Send command to port(s)"}
value={valueInput}
onChange={(event) => {
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 + "\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={
<CloseButton
aria-label="Clear input"
onClick={() => setValueInput("")}
style={{
display: valueInput ? undefined : "none",
}}
/>
}
/>
</Box>
</Box>
<Box style={{ width: "220px" }}>
<Flex align={"center"} wrap={"wrap"} gap={"xs"}>
<ButtonSelect
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
station={station}
userName={user?.userName}
onClick={() => {
const lines = station.lines.filter(
(line) =>
!line?.userOpenCLI || line?.userOpenCLI === user?.userName
);
if (selectedLines.length !== lines.length) {
setSelectedLines(lines);
lines.forEach((line) => {
socket?.emit("open_cli", {
lineId: line.id,
stationId: line.stationId || line.station_id,
userEmail: user?.email,
userName: user?.userName,
});
});
} else {
lines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
stationId: line.stationId || line.station_id,
});
});
setSelectedLines([]);
}
}}
/>
<ButtonDPELP
socket={socket}
selectedLines={selectedLines}
isDisable={isDisable || selectedLines.length === 0}
onClick={() => {
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
/>
<Menu shadow="md" position="top">
<Menu.Target>
>
<Flex align={"center"} justify={"center"} gap={"4px"}>
<Text fz={"12px"}>Line {el.lineNumber}</Text>
<CloseButton
style={{ minWidth: "24px" }}
aria-label="Clear input"
onClick={() => {
setSelectedLines(
selectedLines.filter((line) => line.id !== el.id)
);
socket?.emit("close_cli", {
lineId: el?.id,
stationId: el.stationId || el.station_id,
});
}}
/>
</Flex>
</Box>
))}
</Flex>
</ScrollArea>
<Box pl={"md"} pr={"md"}>
<Flex justify={"space-between"} mb={"xs"}>
<Flex></Flex>
<Button
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="yellow"
style={{ height: "30px", width: "100px" }}
onClick={() => {}}
>
Scenario
</Button>
</Menu.Target>
<Menu.Dropdown>
<Box
px="xs"
py="sm"
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
color="orange"
size="xs"
radius="md"
onClick={() => {
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: "spam_break",
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}
}}
>
{scenarios.map((el, i) => (
<ButtonScenario
key={i}
socket={socket}
selectedLines={selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
)}
isDisable={
isDisable ||
selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
).length === 0
Send Break
</Button>
</Flex>
<Box>
<Input
style={{
width: "600px",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}}
placeholder={"Send command to port(s)"}
value={valueInput}
onChange={(event) => {
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 + "\n",
});
// setTimeout(() => {
// socket?.emit("write_command_line_from_web", {
// lineIds: listLine.map((line) => line.id),
// stationId: station.id,
// command: " \n",
// });
// }, 1000);
}
onClick={() => {
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
setValueInput("");
}
}}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValueInput("")}
style={{
display: valueInput ? undefined : "none",
}}
scenario={el}
/>
))}
</Box>
</Menu.Dropdown>
</Menu>
<DrawerLogs
socket={socket}
isLogModalOpen={isLogModalOpen}
setIsLogModalOpen={setIsLogModalOpen}
testLogContent={testLogContent}
setTestLogContent={setTestLogContent}
/>
}
/>
</Box>
</Box>
<Box style={{ width: "220px" }}>
<Flex align={"center"} wrap={"wrap"} gap={"xs"}>
<ButtonSelect
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
station={station}
userName={user?.userName}
onClick={() => {
const lines = station.lines.filter(
(line) =>
!line?.userOpenCLI ||
line?.userOpenCLI === user?.userName
);
if (selectedLines.length !== lines.length) {
setSelectedLines(lines);
lines.forEach((line) => {
socket?.emit("open_cli", {
lineId: line.id,
stationId: line.stationId || line.station_id,
userEmail: user?.email,
userName: user?.userName,
});
});
} else {
lines.forEach((line) => {
socket?.emit("close_cli", {
lineId: line?.id,
stationId: line.stationId || line.station_id,
});
});
setSelectedLines([]);
}
}}
/>
<ButtonDPELP
socket={socket}
selectedLines={selectedLines}
isDisable={isDisable || selectedLines.length === 0}
onClick={() => {
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
/>
<Menu shadow="md" position="top">
<Menu.Target>
<Button
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="yellow"
style={{ height: "30px", width: "100px" }}
onClick={() => {}}
>
Scenario
</Button>
</Menu.Target>
<Menu.Dropdown>
<Box
px="xs"
py="sm"
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
{scenarios.map((el, i) => (
<ButtonScenario
key={i}
socket={socket}
selectedLines={selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
)}
isDisable={
isDisable ||
selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
).length === 0
}
onClick={() => {
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
scenario={el}
/>
))}
</Box>
</Menu.Dropdown>
</Menu>
<DrawerLogs
socket={socket}
isLogModalOpen={isLogModalOpen}
setIsLogModalOpen={setIsLogModalOpen}
testLogContent={testLogContent}
setTestLogContent={setTestLogContent}
/>
</Flex>
</Box>
</Flex>
</Box>
</Flex>
</Tabs.Panel>
<Tabs.Panel value="apc" ps={"xs"}>
<DrawerAPCControl socket={socket} stationAPI={station} />
</Tabs.Panel>
<Tabs.Panel value="switch" ps={"xs"}>
<DrawerSwitchControl socket={socket} stationAPI={station} />
</Tabs.Panel>
</Tabs>
</Tabs.Panel>
<Tabs.Panel value="apc" ps={"xs"}>
<DrawerAPCControl socket={socket} stationAPI={station} />
</Tabs.Panel>
<Tabs.Panel value="switch" ps={"xs"}>
<DrawerSwitchControl socket={socket} stationAPI={station} />
</Tabs.Panel>
</Tabs>
</Grid.Col>
<Grid.Col span={1}></Grid.Col>
</Grid>
);
};

View File

@ -1,5 +1,14 @@
import { Box, Button, Card, Grid, Loader, Text } from "@mantine/core";
import { IconRepeat, IconSection } from "@tabler/icons-react";
import {
Box,
Button,
Card,
Flex,
Grid,
Loader,
ScrollArea,
Text,
} 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";
@ -270,8 +279,10 @@ export const DrawerAPCControl: React.FC<DrawerProps> = ({
withBorder
className={`${isSubmit ? classes.isDisabled : ""}`}
style={{
paddingLeft: 0,
paddingRight: 0,
width: "65px",
position: "relative",
width: "80px",
cursor: "pointer",
textAlign: "center",
border: listOutletSelected.find(
@ -303,7 +314,7 @@ export const DrawerAPCControl: React.FC<DrawerProps> = ({
display: "flex",
justifyContent: "left",
marginTop: "10px",
marginBottom: "10px",
// marginBottom: "10px",
gap: "20px",
}}
>
@ -526,8 +537,10 @@ export const DrawerAPCControl: React.FC<DrawerProps> = ({
withBorder
className={`${isSubmit ? classes.isDisabled : ""}`}
style={{
paddingLeft: 0,
paddingRight: 0,
width: "65px",
position: "relative",
width: "80px",
cursor: "pointer",
textAlign: "center",
border: listOutletSelected.find(
@ -559,7 +572,7 @@ export const DrawerAPCControl: React.FC<DrawerProps> = ({
display: "flex",
justifyContent: "left",
marginTop: "10px",
marginBottom: "10px",
// marginBottom: "10px",
gap: "20px",
}}
>
@ -916,7 +929,6 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
? "Deselect All"
: "Select All"
}
// mt={'xs'}
miw={"80px"}
size="xs"
fz={"xs"}
@ -1105,45 +1117,139 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
</div>
</div>
<Grid.Col span={12} pt={0}>
<Grid.Col span={12} pt={"4px"}>
{listPorts?.length > 0 && (
<Box>
<Grid>
{listPorts?.map((group, key) => (
<Grid.Col
key={key}
span={
group?.length > 20
? 11
: group?.length > 0 && group?.length < 4
? 1
: 12
}
>
<fieldset
style={{
padding: "4px 6px",
paddingBottom: "12px",
height: "-webkit-fill-available",
}}
{listPorts?.map((group, key) => {
const isMini = group?.length > 0 && group?.length < 4;
const isLarge = group?.length > 20;
return (
<Grid.Col
pt={0}
key={key}
span={isLarge ? 11 : isMini ? 1 : 12}
>
<legend>
<Text fw={700} c={"#514d4d"} fz={"xs"}>
{group[0]?.name.substring(0, 2) || ""}
</Text>
</legend>
<Box
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
gap: "10px",
overflowY: "auto",
maxHeight: "100px",
}}
>
{group?.length > 0 &&
sortedPorts(group)?.map((port, i) => (
{isLarge ? (
<ScrollArea h={"7vh"} w={"68vw"}>
<Flex gap={"8px"} wrap={"nowrap"}>
{sortedPorts(group)
.slice(0, sortedPorts(group).length / 2)
?.map((port, i) => (
<Card
key={i}
shadow="sm"
padding="xs"
radius="md"
withBorder
style={{
flex: "0 0 auto",
position: "relative",
width: "50px",
backgroundColor:
port.poe === "ON"
? "#f2dcf8"
: port.status === "ON"
? "#d4f1d3"
: "#f5f5f5",
cursor: "pointer",
border: listPortsSelected.find(
(el) => el.name === port.name
)?.name
? "1px solid #0018ff"
: "",
}}
className={`${
isSubmit ? classes.isDisabled : ""
}`}
onClick={() => {
toggleSelect(port);
}}
>
<Box
style={{
display: "flex",
alignItems: "center",
gap: "2px",
flexDirection: "column",
flexWrap: "wrap",
}}
>
<Text fw={500} fz={"11px"}>
{port.name}
</Text>
</Box>
</Card>
))}
</Flex>
<Flex gap={"8px"} wrap={"nowrap"} mt={"8px"} pb={"xs"}>
{sortedPorts(group)
.slice(
sortedPorts(group).length / 2,
sortedPorts(group).length
)
?.map((port, i) => (
<Card
key={i}
shadow="sm"
padding="xs"
radius="md"
withBorder
style={{
flex: "0 0 auto",
position: "relative",
width: "50px",
backgroundColor:
port.poe === "ON"
? "#f2dcf8"
: port.status === "ON"
? "#d4f1d3"
: "#f5f5f5",
cursor: "pointer",
border: listPortsSelected.find(
(el) => el.name === port.name
)?.name
? "1px solid #0018ff"
: "",
}}
className={`${
isSubmit ? classes.isDisabled : ""
}`}
onClick={() => {
toggleSelect(port);
}}
>
<Box
style={{
display: "flex",
alignItems: "center",
gap: "2px",
flexDirection: "column",
flexWrap: "wrap",
}}
>
<Text fw={500} fz={"11px"}>
{port.name}
</Text>
</Box>
</Card>
))}
</Flex>
</ScrollArea>
) : (
<Box
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
gap: "10px",
overflow: "auto",
maxHeight: "7vh",
maxWidth: "70vw",
borderLeft: "1px solid #dedede",
}}
>
{sortedPorts(group)?.map((port, i) => (
<Card
key={i}
shadow="sm"
@ -1152,7 +1258,7 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
withBorder
style={{
position: "relative",
width: "60px",
width: "50px",
backgroundColor:
port.poe === "ON"
? "#f2dcf8"
@ -1180,7 +1286,7 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
flexWrap: "wrap",
}}
>
<IconSection
{/* <IconSection
size={"12px"}
color={
port.poe === "ON"
@ -1189,17 +1295,18 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
? "#40c057"
: "#b8b8b8"
}
/>
/> */}
<Text fw={500} fz={"11px"}>
{port.name}
</Text>
</Box>
</Card>
))}
</Box>
</fieldset>
</Grid.Col>
))}
</Box>
)}
</Grid.Col>
);
})}
</Grid>
</Box>
)}