diff --git a/pkgs/ui/src/app/layout.tsx b/pkgs/ui/src/app/layout.tsx index 208d3e8d5..804349216 100644 --- a/pkgs/ui/src/app/layout.tsx +++ b/pkgs/ui/src/app/layout.tsx @@ -8,6 +8,7 @@ import { IconButton, ThemeProvider, useMediaQuery, + useTheme, } from "@mui/material"; import { ChangeEvent, useState } from "react"; @@ -38,8 +39,21 @@ export default function RootLayout({ children: React.ReactNode; }) { const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)"); + const theme = useTheme(); + const is_small = useMediaQuery(theme.breakpoints.down("sm")); + let [useDarkTheme, setUseDarkTheme] = useState(false); 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(() => { if (useDarkTheme !== userPrefersDarkmode) { // Enable dark theme if the user prefers dark mode diff --git a/pkgs/ui/src/app/nodes/NodeList.tsx b/pkgs/ui/src/app/nodes/NodeList.tsx index f00dee905..2784a2d39 100644 --- a/pkgs/ui/src/app/nodes/NodeList.tsx +++ b/pkgs/ui/src/app/nodes/NodeList.tsx @@ -27,18 +27,21 @@ import Stack from "@mui/material/Stack/Stack"; import ModeIcon from "@mui/icons-material/Mode"; import ClearIcon from "@mui/icons-material/Clear"; 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 Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2 import { Card, CardContent, + Collapse, Container, FormGroup, useTheme, } from "@mui/material"; import hexRgb from "hex-rgb"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { NodeStatus, TableData } from "@/data/nodeData"; +import { NodeStatus, NodeStatusKeys, TableData } from "@/data/nodeData"; interface HeadCell { disablePadding: boolean; @@ -111,52 +114,6 @@ function stableSort( return stabilizedThis.map((el) => el[0]); } -interface EnhancedTableProps { - onRequestSort: ( - event: React.MouseEvent, - 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) => { - onRequestSort(event, property); - }; - - return ( - - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === "desc" ? "sorted descending" : "sorted ascending"} - - ) : null} - - - ))} - - - ); -} - interface EnhancedTableToolbarProps { selected: string | undefined; tableData: TableData[]; @@ -194,9 +151,9 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { ).length; return [ - { name: "Online", value: online, color: "#2E7D32" }, - { name: "Offline", value: offline, color: "#db3927" }, - { name: "Pending", value: pending, color: "#FFBB28" }, + { name: "Online", value: online, color: theme.palette.success.main }, + { name: "Offline", value: offline, color: theme.palette.error.main }, + { name: "Pending", value: pending, color: theme.palette.warning.main }, ]; }, [tableData]); @@ -230,7 +187,7 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { height: 110, backgroundColor: hexRgb(pieItem.color, { format: "css", - alpha: 0.18, + alpha: 0.25, }), }} > @@ -337,8 +294,8 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { {/* Pie Chart Grid */} {cardStack} @@ -366,66 +323,212 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { ); } -function renderLastSeen(last_seen: number) { - return ( - - {last_seen} days ago - - ); -} - -function renderName(name: string, id: string) { - return ( - - - {name} - - - {id} - - - ); -} - -function renderStatus(status: NodeStatus) { - switch (status) { - case NodeStatus.Online: - return ( - - - - Online - - - ); - - case NodeStatus.Offline: - return ( - - - - Offline - - - ); - case NodeStatus.Pending: - return ( - - - - Pending - - - ); - } -} - export interface NodeTableProps { tableData: TableData[]; } +interface EnhancedTableProps { + onRequestSort: ( + event: React.MouseEvent, + 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) => { + onRequestSort(event, property); + }; + + return ( + + + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + + ))} + + + ); +} + +function Row(props: { + row: TableData; + selected: string | undefined; + setSelected: (a: string | undefined) => void; +}) { + function renderStatus(status: NodeStatusKeys) { + switch (status) { + case NodeStatus.Online: + return ( + + + + Online + + + ); + + case NodeStatus.Offline: + return ( + + + + Offline + + + ); + case NodeStatus.Pending: + return ( + + + + Pending + + + ); + } + } + + 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, 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 ( + + {/* Rendered Row */} + + + setOpen(!open)} + > + {open ? : } + + + handleClick(event, row.name)} + > + + + {row.name} + + + {row.id} + + + + handleClick(event, row.name)} + > + {renderStatus(row.status)} + + handleClick(event, row.name)} + > + + {row.last_seen} days ago + + + + + {/* Row Expansion */} + + + + + + Metadata + + + + Hello1 + + + Hello2 + + + + + + + + + ); +} + export default function NodeTable(props: NodeTableProps) { let { tableData } = props; + + const theme = useTheme(); + const is_xs = useMediaQuery(theme.breakpoints.only("xs")); + const [order, setOrder] = React.useState("asc"); const [orderBy, setOrderBy] = React.useState("status"); const [selected, setSelected] = React.useState(undefined); @@ -442,15 +545,6 @@ export default function NodeTable(props: NodeTableProps) { setOrderBy(property); }; - const handleClick = (event: React.MouseEvent, 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) => { setPage(newPage); }; @@ -462,9 +556,6 @@ export default function NodeTable(props: NodeTableProps) { 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. const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - tableData.length) : 0; @@ -479,78 +570,62 @@ export default function NodeTable(props: NodeTableProps) { ); return ( - - - - setSelected(undefined)} - /> - - - - - {visibleRows.map((row, index) => { - const isItemSelected = isSelected(row.name); - const labelId = `enhanced-table-checkbox-${index}`; + + + setSelected(undefined)} + /> + +
+ + + {visibleRows.map((row, index) => { + const labelId = `enhanced-table-checkbox-${index}`; - return ( - handleClick(event, row.name)} - role="checkbox" - aria-checked={isItemSelected} - tabIndex={-1} - key={row.name} - selected={isItemSelected} - sx={{ cursor: "pointer" }} - > - - {renderName(row.name, row.id)} - - - {renderStatus(row.status)} - - - {renderLastSeen(row.last_seen)} - - - ); - })} - {emptyRows > 0 && ( - - - - )} - -
-
- {/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */} - -
-
-
+ return ( + + ); + })} + {emptyRows > 0 && ( + + + + )} + + + + {/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */} + + + ); } diff --git a/pkgs/ui/src/app/nodes/NodePieChart.tsx b/pkgs/ui/src/app/nodes/NodePieChart.tsx index 12ed80418..e076656cc 100644 --- a/pkgs/ui/src/app/nodes/NodePieChart.tsx +++ b/pkgs/ui/src/app/nodes/NodePieChart.tsx @@ -37,6 +37,20 @@ export default function NodePieChart(props: Props) { dataKey="value" nameKey="name" 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) => ( diff --git a/pkgs/ui/src/app/nodes/page.tsx b/pkgs/ui/src/app/nodes/page.tsx index 3e4190ba9..b269e9773 100644 --- a/pkgs/ui/src/app/nodes/page.tsx +++ b/pkgs/ui/src/app/nodes/page.tsx @@ -4,15 +4,12 @@ import NodeList from "./NodeList"; import Box from "@mui/material/Box"; import { tableData } from "@/data/nodeData"; +import { StrictMode } from "react"; export default function Page() { return ( - + - + ); } diff --git a/pkgs/ui/src/app/theme/themes.ts b/pkgs/ui/src/app/theme/themes.ts index fd845dec0..edd2786ee 100644 --- a/pkgs/ui/src/app/theme/themes.ts +++ b/pkgs/ui/src/app/theme/themes.ts @@ -1,12 +1,30 @@ import { createTheme } from "@mui/material/styles"; export const darkTheme = createTheme({ + breakpoints: { + values: { + xs: 0, + sm: 400, + md: 900, + lg: 1200, + xl: 1536, + }, + }, palette: { mode: "dark", }, }); export const lightTheme = createTheme({ + breakpoints: { + values: { + xs: 0, + sm: 400, + md: 900, + lg: 1200, + xl: 1536, + }, + }, palette: { mode: "light", }, diff --git a/pkgs/ui/src/data/nodeData.tsx b/pkgs/ui/src/data/nodeData.tsx index ed15e1e8c..9a73d03f5 100644 --- a/pkgs/ui/src/data/nodeData.tsx +++ b/pkgs/ui/src/data/nodeData.tsx @@ -1,20 +1,22 @@ export interface TableData { name: string; id: string; - status: NodeStatus; + status: NodeStatusKeys; last_seen: number; } -export enum NodeStatus { - Online, - Offline, - Pending, +export const NodeStatus = { + Online: "Online", + Offline: "Offline", + Pending: "Pending", } +export type NodeStatusKeys = typeof NodeStatus[keyof typeof NodeStatus]; + function createData( name: string, id: string, - status: NodeStatus, + status: NodeStatusKeys, last_seen: number, ): TableData { return {