UI: NodeTable exported to small Components
This commit is contained in:
221
pkgs/ui/src/app/nodes/EnhancedTableToolbar.tsx
Normal file
221
pkgs/ui/src/app/nodes/EnhancedTableToolbar.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
||||||
|
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||||
|
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||||
|
import { visuallyHidden } from "@mui/utils";
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
|
import Stack from "@mui/material/Stack/Stack";
|
||||||
|
import EditIcon from "@mui/icons-material/ModeEdit";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import NodePieChart, { PieData } from "./NodePieChart";
|
||||||
|
import Fab from "@mui/material/Fab";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
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, NodeStatusKeys, TableData } from "@/data/nodeData";
|
||||||
|
import StickySpeedDial from "./StickySpeedDial";
|
||||||
|
import { jsx } from "@emotion/react";
|
||||||
|
|
||||||
|
interface EnhancedTableToolbarProps {
|
||||||
|
selected: string | undefined;
|
||||||
|
tableData: TableData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EnhancedTableToolbar(
|
||||||
|
props: React.PropsWithChildren<EnhancedTableToolbarProps>,
|
||||||
|
) {
|
||||||
|
const { selected, tableData } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const is_lg = useMediaQuery(theme.breakpoints.down("lg"));
|
||||||
|
const [debug, setDebug] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const pieData = React.useMemo(() => {
|
||||||
|
const online = tableData.filter(
|
||||||
|
(row) => row.status === NodeStatus.Online,
|
||||||
|
).length;
|
||||||
|
const offline = tableData.filter(
|
||||||
|
(row) => row.status === NodeStatus.Offline,
|
||||||
|
).length;
|
||||||
|
const pending = tableData.filter(
|
||||||
|
(row) => row.status === NodeStatus.Pending,
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ 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, theme]);
|
||||||
|
|
||||||
|
const cardData = React.useMemo(() => {
|
||||||
|
return pieData
|
||||||
|
.filter((pieItem) => pieItem.value > 0)
|
||||||
|
.concat({
|
||||||
|
name: "Total",
|
||||||
|
value: pieData.reduce((a, b) => a + b.value, 0),
|
||||||
|
color: "#000000",
|
||||||
|
});
|
||||||
|
}, [pieData]);
|
||||||
|
|
||||||
|
const cardStack = (
|
||||||
|
<Stack
|
||||||
|
sx={{ ...debugSx, paddingTop: 6 }}
|
||||||
|
height={350}
|
||||||
|
id="cardBox"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
{cardData.map((pieItem) => (
|
||||||
|
<Card
|
||||||
|
key={pieItem.name}
|
||||||
|
sx={{
|
||||||
|
marginBottom: 2,
|
||||||
|
marginRight: 2,
|
||||||
|
width: 110,
|
||||||
|
height: 110,
|
||||||
|
backgroundColor: hexRgb(pieItem.color, {
|
||||||
|
format: "css",
|
||||||
|
alpha: 0.25,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
component="div"
|
||||||
|
gutterBottom={true}
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
{pieItem.value}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{ mb: 1.5 }}
|
||||||
|
color="text.secondary"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
{pieItem.name}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid2 container spacing={1} sx={debugSx}>
|
||||||
|
<StickySpeedDial selected={selected} />
|
||||||
|
<Grid2 key="Header" xs={6}>
|
||||||
|
<Typography
|
||||||
|
sx={{ marginLeft: 3, marginTop: 1 }}
|
||||||
|
variant="h6"
|
||||||
|
id="tableTitle"
|
||||||
|
component="div"
|
||||||
|
>
|
||||||
|
NODES
|
||||||
|
</Typography>
|
||||||
|
</Grid2>
|
||||||
|
{/* Debug Controls */}
|
||||||
|
<Grid2 key="Debug-Controls" xs={6} justifyContent="left" display="flex">
|
||||||
|
<FormGroup>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
onChange={() => {
|
||||||
|
setDebug(!debug);
|
||||||
|
}}
|
||||||
|
checked={debug}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Debug"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Grid2>
|
||||||
|
|
||||||
|
{/* Pie Chart Grid */}
|
||||||
|
<Grid2
|
||||||
|
key="PieChart"
|
||||||
|
md={6}
|
||||||
|
xs={12}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Box height={350} width={400}>
|
||||||
|
<NodePieChart data={pieData} showLabels={is_lg} />
|
||||||
|
</Box>
|
||||||
|
</Grid2>
|
||||||
|
|
||||||
|
{/* Card Stack Grid */}
|
||||||
|
<Grid2
|
||||||
|
key="CardStack"
|
||||||
|
lg={6}
|
||||||
|
display="flex"
|
||||||
|
sx={{ display: { lg: "flex", xs: "none", md: "flex" } }}
|
||||||
|
>
|
||||||
|
{cardStack}
|
||||||
|
</Grid2>
|
||||||
|
|
||||||
|
{/*Toolbar Grid */}
|
||||||
|
<Grid2 key="Toolbar" xs={12}>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
pl: { sm: 2 },
|
||||||
|
pr: { xs: 1, sm: 1 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Toolbar>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,679 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { alpha } from "@mui/material/styles";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Table from "@mui/material/Table";
|
|
||||||
import TableBody from "@mui/material/TableBody";
|
|
||||||
import TableCell from "@mui/material/TableCell";
|
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
|
||||||
import TableHead from "@mui/material/TableHead";
|
|
||||||
import TablePagination from "@mui/material/TablePagination";
|
|
||||||
import TableRow from "@mui/material/TableRow";
|
|
||||||
import TableSortLabel from "@mui/material/TableSortLabel";
|
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
||||||
import Switch from "@mui/material/Switch";
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
|
||||||
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
|
||||||
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
|
||||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
|
||||||
import { visuallyHidden } from "@mui/utils";
|
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
|
||||||
import Stack from "@mui/material/Stack/Stack";
|
|
||||||
import EditIcon from "@mui/icons-material/ModeEdit";
|
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|
||||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
|
||||||
import NodePieChart, { PieData } from "./NodePieChart";
|
|
||||||
import Fab from "@mui/material/Fab";
|
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
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, NodeStatusKeys, TableData } from "@/data/nodeData";
|
|
||||||
|
|
||||||
interface HeadCell {
|
|
||||||
disablePadding: boolean;
|
|
||||||
id: keyof TableData;
|
|
||||||
label: string;
|
|
||||||
alignRight: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headCells: readonly HeadCell[] = [
|
|
||||||
{
|
|
||||||
id: "name",
|
|
||||||
alignRight: false,
|
|
||||||
disablePadding: false,
|
|
||||||
label: "DISPLAY NAME & ID",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "status",
|
|
||||||
alignRight: false,
|
|
||||||
disablePadding: false,
|
|
||||||
label: "STATUS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "last_seen",
|
|
||||||
alignRight: false,
|
|
||||||
disablePadding: false,
|
|
||||||
label: "LAST SEEN",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
|
||||||
if (b[orderBy] < a[orderBy]) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b[orderBy] > a[orderBy]) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Order = "asc" | "desc";
|
|
||||||
|
|
||||||
function getComparator<Key extends keyof any>(
|
|
||||||
order: Order,
|
|
||||||
orderBy: Key,
|
|
||||||
): (
|
|
||||||
a: { [key in Key]: number | string | boolean },
|
|
||||||
b: { [key in Key]: number | string | boolean },
|
|
||||||
) => number {
|
|
||||||
return order === "desc"
|
|
||||||
? (a, b) => descendingComparator(a, b, orderBy)
|
|
||||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
|
|
||||||
// stableSort() brings sort stability to non-modern browsers (notably IE11). If you
|
|
||||||
// only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
|
|
||||||
// with exampleArray.slice().sort(exampleComparator)
|
|
||||||
function stableSort<T>(
|
|
||||||
array: readonly T[],
|
|
||||||
comparator: (a: T, b: T) => number,
|
|
||||||
) {
|
|
||||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
|
||||||
stabilizedThis.sort((a, b) => {
|
|
||||||
const order = comparator(a[0], b[0]);
|
|
||||||
if (order !== 0) {
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
return a[1] - b[1];
|
|
||||||
});
|
|
||||||
return stabilizedThis.map((el) => el[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CrudSpeedDial(props: { selected: string | undefined }) {
|
|
||||||
const { selected } = props;
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
|
|
||||||
function handleClose(event: any, reason: CloseReason) {
|
|
||||||
if (reason === "toggle" || reason === "escapeKeyDown") {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpen(event: any, reason: OpenReason) {
|
|
||||||
if (reason === "toggle") {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSomethingSelected = selected != undefined;
|
|
||||||
|
|
||||||
function editDial() {
|
|
||||||
if (isSomethingSelected) {
|
|
||||||
return (
|
|
||||||
<Link href="/nodes/edit" style={{ marginTop: 7.5 }}>
|
|
||||||
<EditIcon color="action" />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <EditIcon color="disabled" />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
transform: "translateZ(0px)",
|
|
||||||
flexGrow: 1,
|
|
||||||
position: "fixed",
|
|
||||||
right: 20,
|
|
||||||
top: 15,
|
|
||||||
margin: 0,
|
|
||||||
zIndex: 9000,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SpeedDial
|
|
||||||
color="secondary"
|
|
||||||
ariaLabel="SpeedDial basic example"
|
|
||||||
icon={<SpeedDialIcon />}
|
|
||||||
direction="down"
|
|
||||||
onClose={handleClose}
|
|
||||||
onOpen={handleOpen}
|
|
||||||
open={open}
|
|
||||||
>
|
|
||||||
<SpeedDialAction
|
|
||||||
key="Add"
|
|
||||||
icon={
|
|
||||||
<Link href="/nodes/add" style={{ marginTop: 7.5 }}>
|
|
||||||
<AddIcon color="action" />
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
tooltipTitle="Add"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SpeedDialAction
|
|
||||||
key="Delete"
|
|
||||||
icon={
|
|
||||||
<DeleteIcon color={isSomethingSelected ? "action" : "disabled"} />
|
|
||||||
}
|
|
||||||
tooltipTitle="Delete"
|
|
||||||
/>
|
|
||||||
<SpeedDialAction key="Edit" icon={editDial()} tooltipTitle="Edit" />
|
|
||||||
</SpeedDial>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnhancedTableToolbarProps {
|
|
||||||
selected: string | undefined;
|
|
||||||
tableData: TableData[];
|
|
||||||
onClear: () => void;
|
|
||||||
}
|
|
||||||
function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
|
|
||||||
const { selected, onClear, tableData } = props;
|
|
||||||
const theme = useTheme();
|
|
||||||
const is_lg = useMediaQuery(theme.breakpoints.down("lg"));
|
|
||||||
const is_sm = useMediaQuery(theme.breakpoints.down("sm"));
|
|
||||||
const isSelected = selected != undefined;
|
|
||||||
const [debug, setDebug] = React.useState<boolean>(false);
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const pieData = React.useMemo(() => {
|
|
||||||
const online = tableData.filter(
|
|
||||||
(row) => row.status === NodeStatus.Online,
|
|
||||||
).length;
|
|
||||||
const offline = tableData.filter(
|
|
||||||
(row) => row.status === NodeStatus.Offline,
|
|
||||||
).length;
|
|
||||||
const pending = tableData.filter(
|
|
||||||
(row) => row.status === NodeStatus.Pending,
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ 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, theme]);
|
|
||||||
|
|
||||||
const cardData = React.useMemo(() => {
|
|
||||||
return pieData
|
|
||||||
.filter((pieItem) => pieItem.value > 0)
|
|
||||||
.concat({
|
|
||||||
name: "Total",
|
|
||||||
value: pieData.reduce((a, b) => a + b.value, 0),
|
|
||||||
color: "#000000",
|
|
||||||
});
|
|
||||||
}, [pieData]);
|
|
||||||
|
|
||||||
const cardStack = (
|
|
||||||
<Stack
|
|
||||||
sx={{ ...debugSx, paddingTop: 6 }}
|
|
||||||
height={350}
|
|
||||||
id="cardBox"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
flexWrap="wrap"
|
|
||||||
>
|
|
||||||
{cardData.map((pieItem) => (
|
|
||||||
<Card
|
|
||||||
key={pieItem.name}
|
|
||||||
sx={{
|
|
||||||
marginBottom: 2,
|
|
||||||
marginRight: 2,
|
|
||||||
width: 110,
|
|
||||||
height: 110,
|
|
||||||
backgroundColor: hexRgb(pieItem.color, {
|
|
||||||
format: "css",
|
|
||||||
alpha: 0.25,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardContent>
|
|
||||||
<Typography
|
|
||||||
variant="h4"
|
|
||||||
component="div"
|
|
||||||
gutterBottom={true}
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
{pieItem.value}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
sx={{ mb: 1.5 }}
|
|
||||||
color="text.secondary"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
{pieItem.name}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid2 container spacing={1} sx={debugSx}>
|
|
||||||
<CrudSpeedDial selected={selected} />
|
|
||||||
<Grid2 key="Header" xs={6}>
|
|
||||||
<Typography
|
|
||||||
sx={{ marginLeft: 3, marginTop: 1 }}
|
|
||||||
variant="h6"
|
|
||||||
id="tableTitle"
|
|
||||||
component="div"
|
|
||||||
>
|
|
||||||
NODES
|
|
||||||
</Typography>
|
|
||||||
</Grid2>
|
|
||||||
{/* Debug Controls */}
|
|
||||||
<Grid2 key="Debug-Controls" xs={6} justifyContent="left" display="flex">
|
|
||||||
<FormGroup>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
onChange={() => {
|
|
||||||
setDebug(!debug);
|
|
||||||
}}
|
|
||||||
checked={debug}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Debug"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Grid2>
|
|
||||||
|
|
||||||
{/* Pie Chart Grid */}
|
|
||||||
<Grid2
|
|
||||||
key="PieChart"
|
|
||||||
md={6}
|
|
||||||
xs={12}
|
|
||||||
display="flex"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Box height={350} width={400}>
|
|
||||||
<NodePieChart data={pieData} showLabels={is_lg} />
|
|
||||||
</Box>
|
|
||||||
</Grid2>
|
|
||||||
|
|
||||||
{/* Card Stack Grid */}
|
|
||||||
<Grid2
|
|
||||||
key="CardStack"
|
|
||||||
lg={6}
|
|
||||||
display="flex"
|
|
||||||
sx={{ display: { lg: "flex", xs: "none", md: "flex" } }}
|
|
||||||
>
|
|
||||||
{cardStack}
|
|
||||||
</Grid2>
|
|
||||||
|
|
||||||
{/*Toolbar Grid */}
|
|
||||||
<Grid2 key="Toolbar" xs={12}>
|
|
||||||
<Toolbar
|
|
||||||
sx={{
|
|
||||||
pl: { sm: 2 },
|
|
||||||
pr: { xs: 1, sm: 1 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip title="Filter list">
|
|
||||||
<IconButton>
|
|
||||||
<SearchIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Toolbar>
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeTableProps {
|
|
||||||
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={6}>
|
|
||||||
<Box>Hello2</Box>
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
</Box>
|
|
||||||
</Collapse>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Order>("asc");
|
|
||||||
const [orderBy, setOrderBy] = React.useState<keyof TableData>("status");
|
|
||||||
const [selected, setSelected] = React.useState<string | undefined>(undefined);
|
|
||||||
const [page, setPage] = React.useState(0);
|
|
||||||
const [dense, setDense] = React.useState(false);
|
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState(5);
|
|
||||||
|
|
||||||
const handleRequestSort = (
|
|
||||||
event: React.MouseEvent<unknown>,
|
|
||||||
property: keyof TableData,
|
|
||||||
) => {
|
|
||||||
const isAsc = orderBy === property && order === "asc";
|
|
||||||
setOrder(isAsc ? "desc" : "asc");
|
|
||||||
setOrderBy(property);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangePage = (event: unknown, newPage: number) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRowsPerPage = (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
setRowsPerPage(parseInt(event.target.value, 10));
|
|
||||||
setPage(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
const visibleRows = React.useMemo(
|
|
||||||
() =>
|
|
||||||
stableSort(tableData, getComparator(order, orderBy)).slice(
|
|
||||||
page * rowsPerPage,
|
|
||||||
page * rowsPerPage + rowsPerPage,
|
|
||||||
),
|
|
||||||
[order, orderBy, page, rowsPerPage, tableData],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ width: "100%" }}>
|
|
||||||
<Paper sx={{ width: "100%", mb: 2 }}>
|
|
||||||
<EnhancedTableToolbar
|
|
||||||
tableData={tableData}
|
|
||||||
selected={selected}
|
|
||||||
onClear={() => setSelected(undefined)}
|
|
||||||
/>
|
|
||||||
<TableContainer>
|
|
||||||
<Table
|
|
||||||
sx={{ minWidth: 750 }}
|
|
||||||
aria-labelledby="tableTitle"
|
|
||||||
size={dense ? "small" : "medium"}
|
|
||||||
>
|
|
||||||
<EnhancedTableHead
|
|
||||||
order={order}
|
|
||||||
orderBy={orderBy}
|
|
||||||
onRequestSort={handleRequestSort}
|
|
||||||
rowCount={tableData.length}
|
|
||||||
/>
|
|
||||||
<TableBody>
|
|
||||||
{visibleRows.map((row, index) => {
|
|
||||||
const labelId = `enhanced-table-checkbox-${index}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row
|
|
||||||
key={row.id}
|
|
||||||
row={row}
|
|
||||||
selected={selected}
|
|
||||||
setSelected={setSelected}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{emptyRows > 0 && (
|
|
||||||
<TableRow
|
|
||||||
style={{
|
|
||||||
height: (dense ? 33 : 53) * emptyRows,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TableCell colSpan={6} />
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
|
|
||||||
<TablePagination
|
|
||||||
rowsPerPageOptions={[5, 10, 25]}
|
|
||||||
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
|
|
||||||
component="div"
|
|
||||||
count={tableData.length}
|
|
||||||
rowsPerPage={rowsPerPage}
|
|
||||||
page={page}
|
|
||||||
onPageChange={handleChangePage}
|
|
||||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
303
pkgs/ui/src/app/nodes/NodeTable.tsx
Normal file
303
pkgs/ui/src/app/nodes/NodeTable.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
||||||
|
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||||
|
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||||
|
import { visuallyHidden } from "@mui/utils";
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
|
import Stack from "@mui/material/Stack/Stack";
|
||||||
|
import EditIcon from "@mui/icons-material/ModeEdit";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import NodePieChart, { PieData } from "./NodePieChart";
|
||||||
|
import Fab from "@mui/material/Fab";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Row from "./Row";
|
||||||
|
|
||||||
|
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, NodeStatusKeys, TableData } from "@/data/nodeData";
|
||||||
|
import EnhancedTableToolbar from "./EnhancedTableToolbar";
|
||||||
|
import { jsx } from "@emotion/react";
|
||||||
|
|
||||||
|
interface HeadCell {
|
||||||
|
disablePadding: boolean;
|
||||||
|
id: keyof TableData;
|
||||||
|
label: string;
|
||||||
|
alignRight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headCells: readonly HeadCell[] = [
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
alignRight: false,
|
||||||
|
disablePadding: false,
|
||||||
|
label: "DISPLAY NAME & ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
alignRight: false,
|
||||||
|
disablePadding: false,
|
||||||
|
label: "STATUS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "last_seen",
|
||||||
|
alignRight: false,
|
||||||
|
disablePadding: false,
|
||||||
|
label: "LAST SEEN",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||||
|
if (b[orderBy] < a[orderBy]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b[orderBy] > a[orderBy]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order = "asc" | "desc";
|
||||||
|
|
||||||
|
function getComparator<Key extends keyof any>(
|
||||||
|
order: Order,
|
||||||
|
orderBy: Key,
|
||||||
|
): (
|
||||||
|
a: { [key in Key]: number | string | boolean },
|
||||||
|
b: { [key in Key]: number | string | boolean },
|
||||||
|
) => number {
|
||||||
|
return order === "desc"
|
||||||
|
? (a, b) => descendingComparator(a, b, orderBy)
|
||||||
|
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
|
||||||
|
// stableSort() brings sort stability to non-modern browsers (notably IE11). If you
|
||||||
|
// only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
|
||||||
|
// with exampleArray.slice().sort(exampleComparator)
|
||||||
|
function stableSort<T>(
|
||||||
|
array: readonly T[],
|
||||||
|
comparator: (a: T, b: T) => number,
|
||||||
|
) {
|
||||||
|
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||||
|
stabilizedThis.sort((a, b) => {
|
||||||
|
const order = comparator(a[0], b[0]);
|
||||||
|
if (order !== 0) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
return a[1] - b[1];
|
||||||
|
});
|
||||||
|
return stabilizedThis.map((el) => el[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBar() {
|
||||||
|
const [search, setSearch] = React.useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label htmlFor="search">
|
||||||
|
<Tooltip title="Filter list">
|
||||||
|
<IconButton>
|
||||||
|
<SearchIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<input id="search" type="text" value={search} onChange={handleSearch} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeTableProps {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Order>("asc");
|
||||||
|
const [orderBy, setOrderBy] = React.useState<keyof TableData>("status");
|
||||||
|
const [selected, setSelected] = React.useState<string | undefined>(undefined);
|
||||||
|
const [page, setPage] = React.useState(0);
|
||||||
|
const [dense, setDense] = React.useState(false);
|
||||||
|
const [rowsPerPage, setRowsPerPage] = React.useState(5);
|
||||||
|
const [search, setSearch] = React.useState<string>("");
|
||||||
|
|
||||||
|
const filteredTableData = React.useMemo(() => {
|
||||||
|
return tableData.filter((row) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
const handleRequestSort = (
|
||||||
|
event: React.MouseEvent<unknown>,
|
||||||
|
property: keyof TableData,
|
||||||
|
) => {
|
||||||
|
const isAsc = orderBy === property && order === "asc";
|
||||||
|
setOrder(isAsc ? "desc" : "asc");
|
||||||
|
setOrderBy(property);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePage = (event: unknown, newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
|
setPage(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const visibleRows = React.useMemo(
|
||||||
|
() =>
|
||||||
|
stableSort(tableData, getComparator(order, orderBy)).slice(
|
||||||
|
page * rowsPerPage,
|
||||||
|
page * rowsPerPage + rowsPerPage,
|
||||||
|
),
|
||||||
|
[order, orderBy, page, rowsPerPage, tableData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "100%" }}>
|
||||||
|
<Paper sx={{ width: "100%", mb: 2 }}>
|
||||||
|
<EnhancedTableToolbar tableData={tableData} selected={selected} />
|
||||||
|
<TableContainer>
|
||||||
|
<Table
|
||||||
|
sx={{ minWidth: 750 }}
|
||||||
|
aria-labelledby="tableTitle"
|
||||||
|
size={dense ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
<EnhancedTableHead
|
||||||
|
order={order}
|
||||||
|
orderBy={orderBy}
|
||||||
|
onRequestSort={handleRequestSort}
|
||||||
|
rowCount={tableData.length}
|
||||||
|
/>
|
||||||
|
<TableBody>
|
||||||
|
{visibleRows.map((row, index) => {
|
||||||
|
const labelId = `enhanced-table-checkbox-${index}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
selected={selected}
|
||||||
|
setSelected={setSelected}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{emptyRows > 0 && (
|
||||||
|
<TableRow
|
||||||
|
style={{
|
||||||
|
height: (dense ? 33 : 53) * emptyRows,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell colSpan={6} />
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
|
||||||
|
component="div"
|
||||||
|
count={tableData.length}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
page={page}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
205
pkgs/ui/src/app/nodes/Row.tsx
Normal file
205
pkgs/ui/src/app/nodes/Row.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
||||||
|
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||||
|
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||||
|
import { visuallyHidden } from "@mui/utils";
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
|
import Stack from "@mui/material/Stack/Stack";
|
||||||
|
import EditIcon from "@mui/icons-material/ModeEdit";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import NodePieChart, { PieData } from "./NodePieChart";
|
||||||
|
import Fab from "@mui/material/Fab";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
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, NodeStatusKeys, TableData } from "@/data/nodeData";
|
||||||
|
import StickySpeedDial from "./StickySpeedDial";
|
||||||
|
import { jsx } from "@emotion/react";
|
||||||
|
|
||||||
|
export default 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={6}>
|
||||||
|
<Box>Hello2</Box>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
pkgs/ui/src/app/nodes/StickySpeedDial.tsx
Normal file
126
pkgs/ui/src/app/nodes/StickySpeedDial.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
||||||
|
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||||
|
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||||
|
import { visuallyHidden } from "@mui/utils";
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
|
import Stack from "@mui/material/Stack/Stack";
|
||||||
|
import EditIcon from "@mui/icons-material/ModeEdit";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import NodePieChart, { PieData } from "./NodePieChart";
|
||||||
|
import Fab from "@mui/material/Fab";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
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, NodeStatusKeys, TableData } from "@/data/nodeData";
|
||||||
|
import { jsx } from "@emotion/react";
|
||||||
|
|
||||||
|
export default function StickySpeedDial(props: {
|
||||||
|
selected: string | undefined;
|
||||||
|
}) {
|
||||||
|
const { selected } = props;
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
function handleClose(event: any, reason: CloseReason) {
|
||||||
|
if (reason === "toggle" || reason === "escapeKeyDown") {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpen(event: any, reason: OpenReason) {
|
||||||
|
if (reason === "toggle") {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSomethingSelected = selected != undefined;
|
||||||
|
|
||||||
|
function editDial() {
|
||||||
|
if (isSomethingSelected) {
|
||||||
|
return (
|
||||||
|
<Link href="/nodes/edit" style={{ marginTop: 7.5 }}>
|
||||||
|
<EditIcon color="action" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <EditIcon color="disabled" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
transform: "translateZ(0px)",
|
||||||
|
flexGrow: 1,
|
||||||
|
position: "fixed",
|
||||||
|
right: 20,
|
||||||
|
top: 15,
|
||||||
|
margin: 0,
|
||||||
|
zIndex: 9000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SpeedDial
|
||||||
|
color="secondary"
|
||||||
|
ariaLabel="SpeedDial basic example"
|
||||||
|
icon={<SpeedDialIcon />}
|
||||||
|
direction="down"
|
||||||
|
onClose={handleClose}
|
||||||
|
onOpen={handleOpen}
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<SpeedDialAction
|
||||||
|
key="Add"
|
||||||
|
icon={
|
||||||
|
<Link href="/nodes/add" style={{ marginTop: 7.5 }}>
|
||||||
|
<AddIcon color="action" />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
tooltipTitle="Add"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SpeedDialAction
|
||||||
|
key="Delete"
|
||||||
|
icon={
|
||||||
|
<DeleteIcon color={isSomethingSelected ? "action" : "disabled"} />
|
||||||
|
}
|
||||||
|
tooltipTitle="Delete"
|
||||||
|
/>
|
||||||
|
<SpeedDialAction key="Edit" icon={editDial()} tooltipTitle="Edit" />
|
||||||
|
</SpeedDial>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import NodeList from "./NodeList";
|
import NodeTable from "./NodeTable";
|
||||||
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { tableData } from "@/data/nodeData";
|
import { tableData } from "@/data/nodeData";
|
||||||
@@ -9,7 +9,7 @@ import { StrictMode } from "react";
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<NodeList tableData={tableData} />
|
<NodeTable tableData={tableData} />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user