Merge pull request 'Fixed mobile layout in Pie Chart and in table' (#141) from Qubasa-Qubasa-main into main

This commit is contained in:
clan-bot
2023-08-14 17:41:02 +00:00
6 changed files with 324 additions and 204 deletions

View File

@@ -8,6 +8,7 @@ import {
IconButton, IconButton,
ThemeProvider, ThemeProvider,
useMediaQuery, useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
@@ -38,8 +39,21 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)"); const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)");
const theme = useTheme();
const is_small = useMediaQuery(theme.breakpoints.down("sm"));
let [useDarkTheme, setUseDarkTheme] = useState(false); let [useDarkTheme, setUseDarkTheme] = useState(false);
let [showSidebar, setShowSidebar] = useState(true); let [showSidebar, setShowSidebar] = useState(true);
// If the screen is small, hide the sidebar
React.useEffect(() => {
if (is_small) {
setShowSidebar(false);
} else {
setShowSidebar(true);
}
}, [is_small]);
React.useEffect(() => { React.useEffect(() => {
if (useDarkTheme !== userPrefersDarkmode) { if (useDarkTheme !== userPrefersDarkmode) {
// Enable dark theme if the user prefers dark mode // Enable dark theme if the user prefers dark mode

View File

@@ -27,18 +27,21 @@ import Stack from "@mui/material/Stack/Stack";
import ModeIcon from "@mui/icons-material/Mode"; import ModeIcon from "@mui/icons-material/Mode";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import Fade from "@mui/material/Fade/Fade"; import Fade from "@mui/material/Fade/Fade";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import NodePieChart, { PieData } from "./NodePieChart"; import NodePieChart, { PieData } from "./NodePieChart";
import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2 import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
import { import {
Card, Card,
CardContent, CardContent,
Collapse,
Container, Container,
FormGroup, FormGroup,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import hexRgb from "hex-rgb"; import hexRgb from "hex-rgb";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import { NodeStatus, TableData } from "@/data/nodeData"; import { NodeStatus, NodeStatusKeys, TableData } from "@/data/nodeData";
interface HeadCell { interface HeadCell {
disablePadding: boolean; disablePadding: boolean;
@@ -111,52 +114,6 @@ function stableSort<T>(
return stabilizedThis.map((el) => el[0]); return stabilizedThis.map((el) => el[0]);
} }
interface EnhancedTableProps {
onRequestSort: (
event: React.MouseEvent<unknown>,
property: keyof TableData,
) => void;
order: Order;
orderBy: string;
rowCount: number;
}
function EnhancedTableHead(props: EnhancedTableProps) {
const { order, orderBy, onRequestSort } = props;
const createSortHandler =
(property: keyof TableData) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
{headCells.map((headCell) => (
<TableCell
key={headCell.id}
align={headCell.alignRight ? "right" : "left"}
padding={headCell.disablePadding ? "none" : "normal"}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : "asc"}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}
interface EnhancedTableToolbarProps { interface EnhancedTableToolbarProps {
selected: string | undefined; selected: string | undefined;
tableData: TableData[]; tableData: TableData[];
@@ -194,9 +151,9 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
).length; ).length;
return [ return [
{ name: "Online", value: online, color: "#2E7D32" }, { name: "Online", value: online, color: theme.palette.success.main },
{ name: "Offline", value: offline, color: "#db3927" }, { name: "Offline", value: offline, color: theme.palette.error.main },
{ name: "Pending", value: pending, color: "#FFBB28" }, { name: "Pending", value: pending, color: theme.palette.warning.main },
]; ];
}, [tableData]); }, [tableData]);
@@ -230,7 +187,7 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
height: 110, height: 110,
backgroundColor: hexRgb(pieItem.color, { backgroundColor: hexRgb(pieItem.color, {
format: "css", format: "css",
alpha: 0.18, alpha: 0.25,
}), }),
}} }}
> >
@@ -337,8 +294,8 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
{/* Pie Chart Grid */} {/* Pie Chart Grid */}
<Grid2 <Grid2
key="PieChart" key="PieChart"
lg={6} md={6}
sm={12} xs={12}
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
@@ -353,7 +310,7 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
key="CardStack" key="CardStack"
lg={6} lg={6}
display="flex" display="flex"
sx={{ display: { lg: "flex", sm: "none" } }} sx={{ display: { lg: "flex", xs: "none", md: "flex" } }}
> >
{cardStack} {cardStack}
</Grid2> </Grid2>
@@ -366,66 +323,212 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
); );
} }
function renderLastSeen(last_seen: number) {
return (
<Typography component="div" align="left" variant="body1">
{last_seen} days ago
</Typography>
);
}
function renderName(name: string, id: string) {
return (
<Stack>
<Typography component="div" align="left" variant="body1">
{name}
</Typography>
<Typography color="grey" component="div" align="left" variant="body2">
{id}
</Typography>
</Stack>
);
}
function renderStatus(status: NodeStatus) {
switch (status) {
case NodeStatus.Online:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="success" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Online
</Typography>
</Stack>
);
case NodeStatus.Offline:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="error" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Offline
</Typography>
</Stack>
);
case NodeStatus.Pending:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="warning" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Pending
</Typography>
</Stack>
);
}
}
export interface NodeTableProps { export interface NodeTableProps {
tableData: TableData[]; tableData: TableData[];
} }
interface EnhancedTableProps {
onRequestSort: (
event: React.MouseEvent<unknown>,
property: keyof TableData,
) => void;
order: Order;
orderBy: string;
rowCount: number;
}
function EnhancedTableHead(props: EnhancedTableProps) {
const { order, orderBy, onRequestSort } = props;
const createSortHandler =
(property: keyof TableData) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
<TableCell id="dropdown" colSpan={1} />
{headCells.map((headCell) => (
<TableCell
key={headCell.id}
align={headCell.alignRight ? "right" : "left"}
padding={headCell.disablePadding ? "none" : "normal"}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : "asc"}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}
function Row(props: {
row: TableData;
selected: string | undefined;
setSelected: (a: string | undefined) => void;
}) {
function renderStatus(status: NodeStatusKeys) {
switch (status) {
case NodeStatus.Online:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="success" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Online
</Typography>
</Stack>
);
case NodeStatus.Offline:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="error" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Offline
</Typography>
</Stack>
);
case NodeStatus.Pending:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="warning" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Pending
</Typography>
</Stack>
);
}
}
const { row, selected, setSelected } = props;
const [open, setOpen] = React.useState(false);
//const labelId = `enhanced-table-checkbox-${index}`;
// Speed optimization. We compare string pointers here instead of the string content.
const isSelected = selected == row.name;
const handleClick = (event: React.MouseEvent<unknown>, name: string) => {
if (isSelected) {
setSelected(undefined);
} else {
setSelected(name);
}
};
const debug = true;
const debugSx = debug
? {
"--Grid-borderWidth": "1px",
borderTop: "var(--Grid-borderWidth) solid",
borderLeft: "var(--Grid-borderWidth) solid",
borderColor: "divider",
"& > div": {
borderRight: "var(--Grid-borderWidth) solid",
borderBottom: "var(--Grid-borderWidth) solid",
borderColor: "divider",
},
}
: {};
return (
<React.Fragment>
{/* Rendered Row */}
<TableRow
hover
role="checkbox"
aria-checked={isSelected}
tabIndex={-1}
key={row.name}
selected={isSelected}
sx={{ cursor: "pointer" }}
>
<TableCell padding="none">
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell
component="th"
scope="row"
onClick={(event) => handleClick(event, row.name)}
>
<Stack>
<Typography component="div" align="left" variant="body1">
{row.name}
</Typography>
<Typography
color="grey"
component="div"
align="left"
variant="body2"
>
{row.id}
</Typography>
</Stack>
</TableCell>
<TableCell
align="right"
onClick={(event) => handleClick(event, row.name)}
>
{renderStatus(row.status)}
</TableCell>
<TableCell
align="right"
onClick={(event) => handleClick(event, row.name)}
>
<Typography component="div" align="left" variant="body1">
{row.last_seen} days ago
</Typography>
</TableCell>
</TableRow>
{/* Row Expansion */}
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
Metadata
</Typography>
<Grid2 container spacing={2} paddingLeft={0}>
<Grid2 xs={6} style={{ ...debugSx }} justifyContent="left" display="flex" paddingRight={3}>
<Box >Hello1</Box>
</Grid2>
<Grid2 xs={6} style={{ ...debugSx }} paddingLeft={4}>
<Box>Hello2</Box>
</Grid2>
</Grid2>
</Box>
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
}
export default function NodeTable(props: NodeTableProps) { export default function NodeTable(props: NodeTableProps) {
let { tableData } = props; let { tableData } = props;
const theme = useTheme();
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
const [order, setOrder] = React.useState<Order>("asc"); const [order, setOrder] = React.useState<Order>("asc");
const [orderBy, setOrderBy] = React.useState<keyof TableData>("status"); const [orderBy, setOrderBy] = React.useState<keyof TableData>("status");
const [selected, setSelected] = React.useState<string | undefined>(undefined); const [selected, setSelected] = React.useState<string | undefined>(undefined);
@@ -442,15 +545,6 @@ export default function NodeTable(props: NodeTableProps) {
setOrderBy(property); setOrderBy(property);
}; };
const handleClick = (event: React.MouseEvent<unknown>, name: string) => {
// Speed optimization. We compare string pointers here instead of the string content.
if (selected == name) {
setSelected(undefined);
} else {
setSelected(name);
}
};
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage); setPage(newPage);
}; };
@@ -462,9 +556,6 @@ export default function NodeTable(props: NodeTableProps) {
setPage(0); setPage(0);
}; };
// Speed optimization. We compare string pointers here instead of the string content.
const isSelected = (name: string) => name == selected;
// Avoid a layout jump when reaching the last page with empty rows. // Avoid a layout jump when reaching the last page with empty rows.
const emptyRows = const emptyRows =
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - tableData.length) : 0; page > 0 ? Math.max(0, (1 + page) * rowsPerPage - tableData.length) : 0;
@@ -479,78 +570,62 @@ export default function NodeTable(props: NodeTableProps) {
); );
return ( return (
<Paper elevation={1} sx={{ margin: 5 }}> <Box sx={{ width: "100%" }}>
<Box sx={{ width: "100%" }}> <Paper sx={{ width: "100%", mb: 2 }}>
<Paper sx={{ width: "100%", mb: 2 }}> <EnhancedTableToolbar
<EnhancedTableToolbar tableData={tableData}
tableData={tableData} selected={selected}
selected={selected} onClear={() => setSelected(undefined)}
onClear={() => setSelected(undefined)} />
/> <TableContainer>
<TableContainer> <Table
<Table sx={{ minWidth: 750 }}
sx={{ minWidth: 750 }} aria-labelledby="tableTitle"
aria-labelledby="tableTitle" size={dense ? "small" : "medium"}
size={dense ? "small" : "medium"} >
> <EnhancedTableHead
<EnhancedTableHead order={order}
order={order} orderBy={orderBy}
orderBy={orderBy} onRequestSort={handleRequestSort}
onRequestSort={handleRequestSort} rowCount={tableData.length}
rowCount={tableData.length} />
/> <TableBody>
<TableBody> {visibleRows.map((row, index) => {
{visibleRows.map((row, index) => { const labelId = `enhanced-table-checkbox-${index}`;
const isItemSelected = isSelected(row.name);
const labelId = `enhanced-table-checkbox-${index}`;
return ( return (
<TableRow <Row
hover key={row.id}
onClick={(event) => handleClick(event, row.name)} row={row}
role="checkbox" selected={selected}
aria-checked={isItemSelected} setSelected={setSelected}
tabIndex={-1} />
key={row.name} );
selected={isItemSelected} })}
sx={{ cursor: "pointer" }} {emptyRows > 0 && (
> <TableRow
<TableCell component="th" id={labelId} scope="row"> style={{
{renderName(row.name, row.id)} height: (dense ? 33 : 53) * emptyRows,
</TableCell> }}
<TableCell align="right"> >
{renderStatus(row.status)} <TableCell colSpan={6} />
</TableCell> </TableRow>
<TableCell align="right"> )}
{renderLastSeen(row.last_seen)} </TableBody>
</TableCell> </Table>
</TableRow> </TableContainer>
); {/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
})} <TablePagination
{emptyRows > 0 && ( rowsPerPageOptions={[5, 10, 25]}
<TableRow labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
style={{ component="div"
height: (dense ? 33 : 53) * emptyRows, count={tableData.length}
}} rowsPerPage={rowsPerPage}
> page={page}
<TableCell colSpan={6} /> onPageChange={handleChangePage}
</TableRow> onRowsPerPageChange={handleChangeRowsPerPage}
)} />
</TableBody> </Paper>
</Table> </Box>
</TableContainer>
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={tableData.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
</Box>
</Paper>
); );
} }

View File

@@ -37,6 +37,20 @@ export default function NodePieChart(props: Props) {
dataKey="value" dataKey="value"
nameKey="name" nameKey="name"
label={showLabels} label={showLabels}
legendType="square"
cx="50%"
cy="50%"
startAngle={0}
endAngle={360}
paddingAngle={0}
labelLine={true}
hide={false}
minAngle={0}
isAnimationActive={true}
animationBegin={0}
animationDuration={1000}
animationEasing="ease-in"
blendStroke={true}
> >
{data.map((entry, index) => ( {data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />

View File

@@ -4,15 +4,12 @@ import NodeList from "./NodeList";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { tableData } from "@/data/nodeData"; import { tableData } from "@/data/nodeData";
import { StrictMode } from "react";
export default function Page() { export default function Page() {
return ( return (
<Box <StrictMode>
sx={{ backgroundColor: "#e9ecf5", height: "100%", width: "100%" }}
display="inline-block"
id="rootBox"
>
<NodeList tableData={tableData} /> <NodeList tableData={tableData} />
</Box> </StrictMode>
); );
} }

View File

@@ -1,12 +1,30 @@
import { createTheme } from "@mui/material/styles"; import { createTheme } from "@mui/material/styles";
export const darkTheme = createTheme({ export const darkTheme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 400,
md: 900,
lg: 1200,
xl: 1536,
},
},
palette: { palette: {
mode: "dark", mode: "dark",
}, },
}); });
export const lightTheme = createTheme({ export const lightTheme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 400,
md: 900,
lg: 1200,
xl: 1536,
},
},
palette: { palette: {
mode: "light", mode: "light",
}, },

View File

@@ -1,20 +1,22 @@
export interface TableData { export interface TableData {
name: string; name: string;
id: string; id: string;
status: NodeStatus; status: NodeStatusKeys;
last_seen: number; last_seen: number;
} }
export enum NodeStatus { export const NodeStatus = {
Online, Online: "Online",
Offline, Offline: "Offline",
Pending, Pending: "Pending",
} }
export type NodeStatusKeys = typeof NodeStatus[keyof typeof NodeStatus];
function createData( function createData(
name: string, name: string,
id: string, id: string,
status: NodeStatus, status: NodeStatusKeys,
last_seen: number, last_seen: number,
): TableData { ): TableData {
return { return {