diff --git a/pkgs/ui/src/app/nodes/EnhancedTableToolbar.tsx b/pkgs/ui/src/app/nodes/EnhancedTableToolbar.tsx
new file mode 100644
index 00000000..4f921e65
--- /dev/null
+++ b/pkgs/ui/src/app/nodes/EnhancedTableToolbar.tsx
@@ -0,0 +1,194 @@
+"use client";
+
+import * as React from "react";
+import Box from "@mui/material/Box";
+import Toolbar from "@mui/material/Toolbar";
+import Typography from "@mui/material/Typography";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Switch from "@mui/material/Switch";
+import Stack from "@mui/material/Stack/Stack";
+import NodePieChart from "./NodePieChart";
+
+import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
+import { Card, CardContent, FormGroup, useTheme } from "@mui/material";
+import hexRgb from "hex-rgb";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import { NodeStatus, TableData } from "@/data/nodeData";
+
+interface EnhancedTableToolbarProps {
+ tableData: TableData[];
+}
+
+function PieCardData(props: { pieData: PieData[]; debugSx: any }) {
+ const { pieData, debugSx } = props;
+
+ 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]);
+
+ return (
+
+ {cardData.map((pieItem) => (
+
+
+
+ {pieItem.value}
+
+
+ {pieItem.name}
+
+
+
+ ))}
+
+ );
+}
+
+interface PieData {
+ name: string;
+ value: number;
+ color: string;
+}
+
+export default function EnhancedTableToolbar(
+ props: React.PropsWithChildren,
+) {
+ const { tableData } = props;
+ const theme = useTheme();
+ const is_lg = useMediaQuery(theme.breakpoints.down("lg"));
+ const [debug, setDebug] = React.useState(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: 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]);
+
+ return (
+
+
+
+ NODES
+
+
+ {/* Debug Controls */}
+
+
+ {
+ setDebug(!debug);
+ }}
+ checked={debug}
+ />
+ }
+ label="Debug"
+ />
+
+
+
+ {/* Pie Chart Grid */}
+
+
+
+
+
+
+ {/* Card Stack Grid */}
+
+
+
+
+ {/*Toolbar Grid */}
+
+
+ {props.children}
+
+
+
+ );
+}
diff --git a/pkgs/ui/src/app/nodes/NodeList.tsx b/pkgs/ui/src/app/nodes/NodeList.tsx
deleted file mode 100644
index 9f7a324a..00000000
--- a/pkgs/ui/src/app/nodes/NodeList.tsx
+++ /dev/null
@@ -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(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(
- 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(
- 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 (
-
-
-
- );
- } else {
- return ;
- }
- }
-
- return (
-
- }
- direction="down"
- onClose={handleClose}
- onOpen={handleOpen}
- open={open}
- >
-
-
-
- }
- tooltipTitle="Add"
- />
-
-
- }
- tooltipTitle="Delete"
- />
-
-
-
- );
-}
-
-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(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 = (
-
- {cardData.map((pieItem) => (
-
-
-
- {pieItem.value}
-
-
- {pieItem.name}
-
-
-
- ))}
-
- );
-
- return (
-
-
-
-
- NODES
-
-
- {/* Debug Controls */}
-
-
- {
- setDebug(!debug);
- }}
- checked={debug}
- />
- }
- label="Debug"
- />
-
-
-
- {/* Pie Chart Grid */}
-
-
-
-
-
-
- {/* Card Stack Grid */}
-
- {cardStack}
-
-
- {/*Toolbar Grid */}
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-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);
- const [page, setPage] = React.useState(0);
- const [dense, setDense] = React.useState(false);
- const [rowsPerPage, setRowsPerPage] = React.useState(5);
-
- const handleRequestSort = (
- event: React.MouseEvent,
- 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,
- ) => {
- 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 (
-
-
- setSelected(undefined)}
- />
-
-
-
-
- {visibleRows.map((row, index) => {
- const labelId = `enhanced-table-checkbox-${index}`;
-
- 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/NodeRow.tsx b/pkgs/ui/src/app/nodes/NodeRow.tsx
new file mode 100644
index 00000000..7830e9be
--- /dev/null
+++ b/pkgs/ui/src/app/nodes/NodeRow.tsx
@@ -0,0 +1,170 @@
+"use client";
+
+import * as React from "react";
+import Box from "@mui/material/Box";
+import TableCell from "@mui/material/TableCell";
+import TableRow from "@mui/material/TableRow";
+import Typography from "@mui/material/Typography";
+import IconButton from "@mui/material/IconButton";
+import CircleIcon from "@mui/icons-material/Circle";
+import Stack from "@mui/material/Stack/Stack";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
+import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
+
+import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
+import { Collapse } from "@mui/material";
+import { NodeStatus, NodeStatusKeys, TableData } from "@/data/nodeData";
+
+export default function NodeRow(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.id;
+
+ const handleClick = (event: React.MouseEvent, id: string) => {
+ if (isSelected) {
+ setSelected(undefined);
+ } else {
+ setSelected(id);
+ }
+ };
+
+ 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.id)}
+ >
+
+
+ {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
+
+
+
+
+
+
+
+ );
+}
diff --git a/pkgs/ui/src/app/nodes/NodeTable.tsx b/pkgs/ui/src/app/nodes/NodeTable.tsx
new file mode 100644
index 00000000..26e97b8d
--- /dev/null
+++ b/pkgs/ui/src/app/nodes/NodeTable.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import * as React from "react";
+import Box from "@mui/material/Box";
+import TablePagination from "@mui/material/TablePagination";
+import Paper from "@mui/material/Paper";
+import IconButton from "@mui/material/IconButton";
+import Tooltip from "@mui/material/Tooltip";
+import SearchIcon from "@mui/icons-material/Search";
+import NodeTableContainer from "./NodeTableContainer";
+
+import { useTheme } from "@mui/material";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import { TableData } from "@/data/nodeData";
+import EnhancedTableToolbar from "./EnhancedTableToolbar";
+import { table } from "console";
+import StickySpeedDial from "./StickySpeedDial";
+
+export interface SearchBarProps {
+ search: string;
+ setSearch: React.Dispatch>;
+}
+
+function SearchBar(props: SearchBarProps) {
+ const { search, setSearch } = props;
+ const handleSearch = (event: React.ChangeEvent) => {
+ setSearch(event.target.value);
+ };
+
+ return (
+
+ );
+}
+
+export interface NodeTableProps {
+ tableData: TableData[];
+}
+
+export default function NodeTable(props: NodeTableProps) {
+ let { tableData } = props;
+
+ const theme = useTheme();
+ const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
+
+ const [selected, setSelected] = React.useState(undefined);
+ const [page, setPage] = React.useState(0);
+ const [rowsPerPage, setRowsPerPage] = React.useState(5);
+ const [search, setSearch] = React.useState("");
+
+ const handleChangePage = (event: unknown, newPage: number) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (
+ event: React.ChangeEvent,
+ ) => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ };
+
+ return (
+
+
+
+
+
+
+
+ {/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
+
+
+
+ );
+}
diff --git a/pkgs/ui/src/app/nodes/NodeTableContainer.tsx b/pkgs/ui/src/app/nodes/NodeTableContainer.tsx
new file mode 100644
index 00000000..67af5718
--- /dev/null
+++ b/pkgs/ui/src/app/nodes/NodeTableContainer.tsx
@@ -0,0 +1,207 @@
+"use client";
+
+import * as React from "react";
+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 TableRow from "@mui/material/TableRow";
+import TableSortLabel from "@mui/material/TableSortLabel";
+import { visuallyHidden } from "@mui/utils";
+import NodeRow from "./NodeRow";
+
+import { 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(a: T, b: T, orderBy: keyof T) {
+ if (b[orderBy] < a[orderBy]) {
+ return -1;
+ }
+ if (b[orderBy] > a[orderBy]) {
+ return 1;
+ }
+ return 0;
+}
+
+export type NodeOrder = "asc" | "desc";
+
+function getComparator(
+ order: NodeOrder,
+ 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(
+ 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]);
+}
+
+interface EnhancedTableProps {
+ onRequestSort: (
+ event: React.MouseEvent,
+ property: keyof TableData,
+ ) => void;
+ order: NodeOrder;
+ 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 NodeTableContainerProps {
+ tableData: readonly TableData[];
+ page: number;
+ rowsPerPage: number;
+ dense: boolean;
+ selected: string | undefined;
+ setSelected: React.Dispatch>;
+}
+
+export default function NodeTableContainer(props: NodeTableContainerProps) {
+ const { tableData, page, rowsPerPage, dense, selected, setSelected } = props;
+ const [order, setOrder] = React.useState("asc");
+ const [orderBy, setOrderBy] = React.useState("status");
+
+ // 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 handleRequestSort = (
+ event: React.MouseEvent,
+ property: keyof TableData,
+ ) => {
+ const isAsc = orderBy === property && order === "asc";
+ setOrder(isAsc ? "desc" : "asc");
+ setOrderBy(property);
+ };
+
+ const visibleRows = React.useMemo(
+ () =>
+ stableSort(tableData, getComparator(order, orderBy)).slice(
+ page * rowsPerPage,
+ page * rowsPerPage + rowsPerPage,
+ ),
+ [order, orderBy, page, rowsPerPage, tableData],
+ );
+ return (
+
+
+
+
+ {visibleRows.map((row, index) => {
+ return (
+
+ );
+ })}
+ {emptyRows > 0 && (
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/pkgs/ui/src/app/nodes/StickySpeedDial.tsx b/pkgs/ui/src/app/nodes/StickySpeedDial.tsx
new file mode 100644
index 00000000..d32a1fcf
--- /dev/null
+++ b/pkgs/ui/src/app/nodes/StickySpeedDial.tsx
@@ -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 (
+
+
+
+ );
+ } else {
+ return ;
+ }
+ }
+
+ return (
+
+ }
+ direction="down"
+ onClose={handleClose}
+ onOpen={handleOpen}
+ open={open}
+ >
+
+
+
+ }
+ tooltipTitle="Add"
+ />
+
+
+ }
+ tooltipTitle="Delete"
+ />
+
+
+
+ );
+}
diff --git a/pkgs/ui/src/app/nodes/page.tsx b/pkgs/ui/src/app/nodes/page.tsx
index d912c5fc..6a250f29 100644
--- a/pkgs/ui/src/app/nodes/page.tsx
+++ b/pkgs/ui/src/app/nodes/page.tsx
@@ -1,15 +1,15 @@
"use client";
-import NodeList from "./NodeList";
+import NodeTable from "./NodeTable";
import Box from "@mui/material/Box";
-import { tableData } from "@/data/nodeData";
+import { tableData } from "@/data/nodeDataStatic";
import { StrictMode } from "react";
export default function Page() {
return (
-
+
);
}
diff --git a/pkgs/ui/src/data/nodeData.tsx b/pkgs/ui/src/data/nodeData.tsx
index 60c982bd..5e536e3a 100644
--- a/pkgs/ui/src/data/nodeData.tsx
+++ b/pkgs/ui/src/data/nodeData.tsx
@@ -19,6 +19,10 @@ function createData(
status: NodeStatusKeys,
last_seen: number,
): TableData {
+ if (status == NodeStatus.Online) {
+ last_seen = 0;
+ }
+
return {
name,
id,
@@ -27,6 +31,67 @@ function createData(
};
}
+// A function to generate random names
+function getRandomName(): string {
+ let names = [
+ "Alice",
+ "Bob",
+ "Charlie",
+ "David",
+ "Eve",
+ "Frank",
+ "Grace",
+ "Heidi",
+ "Ivan",
+ "Judy",
+ "Mallory",
+ "Oscar",
+ "Peggy",
+ "Sybil",
+ "Trent",
+ "Victor",
+ "Walter",
+ "Wendy",
+ "Zoe",
+ ];
+ let index = Math.floor(Math.random() * names.length);
+ return names[index];
+}
+
+// A function to generate random IPv6 addresses
+function getRandomId(): string {
+ let hex = "0123456789abcdef";
+ let id = "";
+ for (let i = 0; i < 8; i++) {
+ for (let j = 0; j < 4; j++) {
+ let index = Math.floor(Math.random() * hex.length);
+ id += hex[index];
+ }
+ if (i < 7) {
+ id += ":";
+ }
+ }
+ return id;
+}
+
+// A function to generate random status keys
+function getRandomStatus(): NodeStatusKeys {
+ let statusKeys = [NodeStatus.Online, NodeStatus.Offline, NodeStatus.Pending];
+ let index = Math.floor(Math.random() * statusKeys.length);
+ return statusKeys[index];
+}
+
+// A function to generate random last seen values
+function getRandomLastSeen(status: NodeStatusKeys): number {
+ if (status === "online") {
+ return 0;
+ } else {
+ let min = 1; // One day ago
+ let max = 360; // One year ago
+ return Math.floor(Math.random() * (max - min + 1) + min);
+ }
+}
+
export const tableData = [
createData(
"Matchbox",
@@ -97,3 +162,19 @@ export const tableData = [
0,
),
];
+
+// A function to execute the createData function with dummy data in a loop 100 times and return an array
+export function executeCreateData(): TableData[] {
+ let result: TableData[] = [];
+ for (let i = 0; i < 100; i++) {
+ // Generate dummy data
+ let name = getRandomName();
+ let id = getRandomId();
+ let status = getRandomStatus();
+ let last_seen = getRandomLastSeen(status);
+
+ // Call the createData function and push the result to the array
+ result.push(createData(name, id, status, last_seen));
+ }
+ return result;
+}
diff --git a/pkgs/ui/src/data/nodeDataStatic.tsx b/pkgs/ui/src/data/nodeDataStatic.tsx
new file mode 100644
index 00000000..cd97ea65
--- /dev/null
+++ b/pkgs/ui/src/data/nodeDataStatic.tsx
@@ -0,0 +1,602 @@
+export const tableData = [
+ {
+ name: "Bob",
+ id: "f435:9678:eff4:cedf:7725:421c:82c0:611c",
+ status: "Pending",
+ last_seen: 83,
+ },
+ {
+ name: "Peggy",
+ id: "aa49:f231:04ac:f0d2:cf9a:4635:57e3:4b78",
+ status: "Offline",
+ last_seen: 294,
+ },
+ {
+ name: "Wendy",
+ id: "a453:4ec6:c401:30dc:89a0:1567:90c9:4a72",
+ status: "Pending",
+ last_seen: 332,
+ },
+ {
+ name: "Sybil",
+ id: "3f14:92c3:1090:bef0:7dc6:9eaf:755c:6f7f",
+ status: "Offline",
+ last_seen: 48,
+ },
+ {
+ name: "Ivan",
+ id: "855e:d1ff:d20c:d1b8:86c4:9ffe:ca2a:fefa",
+ status: "Offline",
+ last_seen: 292,
+ },
+ {
+ name: "Ivan",
+ id: "7bfb:3980:dc1a:37e2:2464:6028:393b:315f",
+ status: "Pending",
+ last_seen: 240,
+ },
+ {
+ name: "Mallory",
+ id: "d457:0a59:ea77:963f:f20b:ca55:57f2:a54b",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Mallory",
+ id: "bbef:a90a:f7ff:5f40:23fc:a1d8:6b2f:7b0a",
+ status: "Offline",
+ last_seen: 95,
+ },
+ {
+ name: "Zoe",
+ id: "1057:7b4d:0708:66a1:5d36:5d8c:6620:4f5f",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Ivan",
+ id: "74d3:60dc:5e12:dc52:7c7f:6903:4ff1:ba20",
+ status: "Pending",
+ last_seen: 193,
+ },
+ {
+ name: "Ivan",
+ id: "6770:c213:bdd1:0226:26e0:a8d4:949a:f2cc",
+ status: "Pending",
+ last_seen: 181,
+ },
+ {
+ name: "Bob",
+ id: "5551:7363:ba70:6927:57f3:24ea:386d:e2d9",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Victor",
+ id: "12eb:9b2b:0c94:0731:15e6:7cd1:8984:7669",
+ status: "Offline",
+ last_seen: 130,
+ },
+ {
+ name: "Victor",
+ id: "785e:b8a0:289b:5108:a982:ecff:8f0b:9db2",
+ status: "Offline",
+ last_seen: 336,
+ },
+ {
+ name: "Judy",
+ id: "ee4b:ce86:4885:2729:089e:7f93:d3dd:7a09",
+ status: "Pending",
+ last_seen: 241,
+ },
+ {
+ name: "Frank",
+ id: "6c05:c65d:ef3b:be64:ae58:e1fd:7aaa:bd30",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Alice",
+ id: "0fe6:f3b1:58c2:ef75:12de:6514:d1a6:eda5",
+ status: "Pending",
+ last_seen: 94,
+ },
+ {
+ name: "Charlie",
+ id: "095b:1b3f:cfeb:72f8:e84c:91f7:2c62:06e9",
+ status: "Pending",
+ last_seen: 131,
+ },
+ {
+ name: "Walter",
+ id: "8d0a:56b0:1537:1b2e:9581:d640:cd29:21cf",
+ status: "Offline",
+ last_seen: 273,
+ },
+ {
+ name: "Walter",
+ id: "d8ab:813a:cbc6:d379:d3be:cc4b:20b8:94ae",
+ status: "Offline",
+ last_seen: 200,
+ },
+ {
+ name: "Trent",
+ id: "0199:cb5c:a8cd:af82:2d9a:e63a:a157:6ee6",
+ status: "Offline",
+ last_seen: 352,
+ },
+ {
+ name: "Peggy",
+ id: "1b7b:8da6:623a:75ea:2385:8777:76b9:9ba8",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Zoe",
+ id: "6ed3:ee3b:f7df:044a:355a:7790:0bf3:a308",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Wendy",
+ id: "efed:821d:a944:0775:ead4:e520:6402:8dc4",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Heidi",
+ id: "b616:82c5:4a37:c020:9568:aaa4:6390:22d5",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Charlie",
+ id: "2c05:e6fc:a359:bf63:65e7:ee0e:49e3:6cc5",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Frank",
+ id: "e9cb:eddc:1a94:22e4:4e3d:e927:c599:db04",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Bob",
+ id: "be03:3537:719f:5152:f277:03ef:c6c9:c953",
+ status: "Offline",
+ last_seen: 69,
+ },
+ {
+ name: "Oscar",
+ id: "cede:fa4a:518d:b885:d1fa:00f4:d97a:2258",
+ status: "Pending",
+ last_seen: 280,
+ },
+ {
+ name: "Judy",
+ id: "9337:53e7:1aae:1b94:6035:e601:e562:350c",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Judy",
+ id: "d50b:0ddc:0c9d:cd34:40ee:34c9:75ff:d16c",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Charlie",
+ id: "368f:8855:59ed:b8c3:4fff:7630:9948:877f",
+ status: "Offline",
+ last_seen: 63,
+ },
+ {
+ name: "Judy",
+ id: "0fcd:74ae:8dad:8d10:370b:46c5:a403:eab8",
+ status: "Pending",
+ last_seen: 242,
+ },
+ {
+ name: "Wendy",
+ id: "285f:3015:2f09:2fde:25ee:87be:6d2c:f4f3",
+ status: "Pending",
+ last_seen: 60,
+ },
+ {
+ name: "Ivan",
+ id: "942d:9c5c:1f4a:95ba:1046:c643:a874:83ce",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Frank",
+ id: "06f5:5b67:98e9:31b4:ddba:fb81:afb1:7677",
+ status: "Offline",
+ last_seen: 154,
+ },
+ {
+ name: "Zoe",
+ id: "ec58:13b0:b7ef:6e19:6c78:2c35:4fa3:092a",
+ status: "Offline",
+ last_seen: 264,
+ },
+ {
+ name: "Charlie",
+ id: "7151:20e3:3969:2933:c23b:6d9a:9723:d1bd",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "David",
+ id: "0466:bd3f:0fff:2119:b9c0:ee60:2a90:6b54",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "David",
+ id: "814d:b00c:4a98:7aa2:5354:7076:f48e:1609",
+ status: "Offline",
+ last_seen: 249,
+ },
+ {
+ name: "David",
+ id: "8633:f51b:b643:829a:08b0:fc2a:7cad:abda",
+ status: "Offline",
+ last_seen: 359,
+ },
+ {
+ name: "Eve",
+ id: "0fd5:7b72:4445:0e27:95c6:1b80:99dc:6590",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Wendy",
+ id: "dc9b:bac9:8ec8:9167:5da5:8530:fcb7:1458",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Bob",
+ id: "0412:6da1:82ef:6cb1:467e:34e9:34df:5742",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Ivan",
+ id: "d03b:aba5:ee40:1961:f824:0d9b:9669:2e3e",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Zoe",
+ id: "ad06:abfd:05c2:fc17:a8e0:857e:b04c:d3fe",
+ status: "Offline",
+ last_seen: 25,
+ },
+ {
+ name: "Ivan",
+ id: "d2ca:12cb:d271:44a2:2813:e826:964b:b292",
+ status: "Pending",
+ last_seen: 11,
+ },
+ {
+ name: "Charlie",
+ id: "aaa9:9754:34e2:7251:af6e:df7a:5422:96cd",
+ status: "Offline",
+ last_seen: 131,
+ },
+ {
+ name: "Mallory",
+ id: "8511:8f44:45d7:cc8a:43ff:908c:a35f:19bc",
+ status: "Offline",
+ last_seen: 141,
+ },
+ {
+ name: "Alice",
+ id: "8bc2:8f83:3081:d90c:087e:13bf:002b:934b",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Peggy",
+ id: "9b33:5e44:310f:f6ab:81ae:df8e:be7b:ddc4",
+ status: "Pending",
+ last_seen: 354,
+ },
+ {
+ name: "Victor",
+ id: "9776:1f62:b8dc:1d14:bf9c:bf41:99b7:5c11",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "David",
+ id: "874a:a4df:321a:5367:4b63:4a0c:48d6:0a0d",
+ status: "Offline",
+ last_seen: 199,
+ },
+ {
+ name: "Charlie",
+ id: "05f0:7188:a2fb:cfe3:7d85:e3de:d6c2:07e9",
+ status: "Pending",
+ last_seen: 46,
+ },
+ {
+ name: "Grace",
+ id: "dcbd:5ddf:496d:dd27:0e98:9905:7c24:664d",
+ status: "Pending",
+ last_seen: 284,
+ },
+ {
+ name: "Ivan",
+ id: "3cb9:d178:2cb1:ae2c:dd3d:ce93:34e0:cc68",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Frank",
+ id: "c974:9ef6:df64:188c:4622:aa71:497b:1fd8",
+ status: "Pending",
+ last_seen: 18,
+ },
+ {
+ name: "Frank",
+ id: "3ba0:9362:3b2f:ec10:03c2:7bd7:67ad:759c",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Walter",
+ id: "b57d:3d13:6654:d944:0e7c:61d6:9dc0:15d8",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Sybil",
+ id: "de00:f53f:0be4:8a57:047a:f99d:8ab2:fe7c",
+ status: "Pending",
+ last_seen: 41,
+ },
+ {
+ name: "Trent",
+ id: "bb32:acd8:bbfb:ea3e:1b58:dc17:4cc4:64ce",
+ status: "Offline",
+ last_seen: 326,
+ },
+ {
+ name: "Oscar",
+ id: "a3e9:1060:b0df:2adb:a340:3f34:5348:d882",
+ status: "Offline",
+ last_seen: 57,
+ },
+ {
+ name: "Wendy",
+ id: "6b49:c86c:41c8:c021:2103:b707:26a9:6d6a",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Sybil",
+ id: "aaf2:68cb:da7e:0844:8a12:5fb9:cff5:9de8",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Victor",
+ id: "b87d:0cc5:e3be:a600:045e:4a26:f7e2:6b3d",
+ status: "Offline",
+ last_seen: 342,
+ },
+ {
+ name: "Frank",
+ id: "13e6:1b80:ba7e:6275:21c8:dc96:6ab1:ac69",
+ status: "Pending",
+ last_seen: 213,
+ },
+ {
+ name: "Sybil",
+ id: "14ad:2010:7648:fbc8:337c:6984:d7e2:5202",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Charlie",
+ id: "f79c:e3b8:1082:7a9d:7adf:f5b6:e333:affd",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Sybil",
+ id: "534c:0d80:97d5:7b54:a1c3:5457:c5e8:62ee",
+ status: "Pending",
+ last_seen: 133,
+ },
+ {
+ name: "Victor",
+ id: "e829:f930:b7d7:6005:cc34:7bca:0163:7903",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Ivan",
+ id: "18c3:6ab2:93ea:0756:52e4:33a6:0c2b:bd79",
+ status: "Pending",
+ last_seen: 109,
+ },
+ {
+ name: "Eve",
+ id: "49a3:28ad:6121:2e35:1b72:e5d7:5efc:0626",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Trent",
+ id: "d900:540b:b764:2468:ad9e:2716:ab4f:9955",
+ status: "Pending",
+ last_seen: 117,
+ },
+ {
+ name: "Zoe",
+ id: "9d3d:de15:014a:ac0d:74b9:2ae8:08d4:8848",
+ status: "Pending",
+ last_seen: 114,
+ },
+ {
+ name: "Peggy",
+ id: "2145:0e6a:f66a:8e93:71b7:385d:c25b:6a2b",
+ status: "Offline",
+ last_seen: 71,
+ },
+ {
+ name: "Sybil",
+ id: "834c:adcc:a3d7:b5fc:d5cd:4209:e280:e625",
+ status: "Pending",
+ last_seen: 200,
+ },
+ {
+ name: "Heidi",
+ id: "5ecc:904c:2d31:177b:346d:4c23:5169:a982",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Ivan",
+ id: "0446:9092:762c:48b6:310a:e6fb:be48:4631",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "David",
+ id: "160c:97aa:f4ff:52d0:717e:2263:7262:90ed",
+ status: "Offline",
+ last_seen: 275,
+ },
+ {
+ name: "Charlie",
+ id: "6832:d934:2a76:27c0:88ea:2b05:5bf8:4f86",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Eve",
+ id: "5363:e485:d9b9:89a8:3ee4:5d24:b2b5:2ab5",
+ status: "Offline",
+ last_seen: 343,
+ },
+ {
+ name: "Peggy",
+ id: "5673:e221:3236:0a33:29a9:5c5f:ff03:c98d",
+ status: "Offline",
+ last_seen: 130,
+ },
+ {
+ name: "Grace",
+ id: "8946:27f1:2abe:f009:b109:f0af:92d1:4c5b",
+ status: "Pending",
+ last_seen: 72,
+ },
+ {
+ name: "Walter",
+ id: "7bc3:a683:660d:ffc3:a40f:b8ab:2246:ed38",
+ status: "Offline",
+ last_seen: 264,
+ },
+ {
+ name: "Mallory",
+ id: "5b5d:129e:8c3e:4f34:94bf:ffe5:7a8a:b3ae",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Eve",
+ id: "d7db:981a:8885:838b:70b6:a691:1c26:4d59",
+ status: "Offline",
+ last_seen: 172,
+ },
+ {
+ name: "Peggy",
+ id: "1255:426b:034c:0171:d67e:ef4c:d9a5:7da6",
+ status: "Pending",
+ last_seen: 81,
+ },
+ {
+ name: "Frank",
+ id: "1c79:ef2f:c01a:d53b:016d:e2fc:3566:6b85",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Judy",
+ id: "b159:2924:969d:e1f6:e295:f19d:9bf2:8f58",
+ status: "Offline",
+ last_seen: 221,
+ },
+ {
+ name: "Judy",
+ id: "bba4:85aa:bcc7:5ef6:920c:7d74:e921:7d47",
+ status: "Offline",
+ last_seen: 168,
+ },
+ {
+ name: "Victor",
+ id: "dc3f:5b3a:3e71:552a:9cdb:10f2:699a:d8e9",
+ status: "Offline",
+ last_seen: 217,
+ },
+ {
+ name: "Victor",
+ id: "e084:f4cf:be5b:83c1:f1f5:1159:d24d:dea2",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Trent",
+ id: "113c:335a:c844:7307:b192:1c44:fb34:4cc0",
+ status: "Offline",
+ last_seen: 77,
+ },
+ {
+ name: "Sybil",
+ id: "05a0:cbe8:4b89:47e3:81e9:39b7:4966:bbed",
+ status: "Offline",
+ last_seen: 145,
+ },
+ {
+ name: "Alice",
+ id: "0f33:461d:c779:067e:424e:b933:c855:c376",
+ status: "Online",
+ last_seen: 0,
+ },
+ {
+ name: "Trent",
+ id: "a883:16c8:abb4:3ab3:7d6f:beb4:1398:bc9c",
+ status: "Pending",
+ last_seen: 189,
+ },
+ {
+ name: "Wendy",
+ id: "2e7c:1162:1488:6a1e:34fe:0725:bd56:4461",
+ status: "Pending",
+ last_seen: 338,
+ },
+ {
+ name: "Judy",
+ id: "c4e1:6d39:5079:097b:3228:1391:4f59:1be6",
+ status: "Offline",
+ last_seen: 352,
+ },
+ {
+ name: "Victor",
+ id: "d32c:1045:4297:251a:6ec8:16c8:541d:7925",
+ status: "Offline",
+ last_seen: 61,
+ },
+ {
+ name: "Zoe",
+ id: "b6cf:6f86:f510:9f39:93a9:aba9:4632:bb69",
+ status: "Online",
+ last_seen: 0,
+ },
+];