Qubasa-Qubasa-main (#262)

UI: Connected UI to API
This commit is contained in:
Qubasa
2023-09-11 12:59:24 +00:00
parent bf4db16209
commit aa5976f046
6 changed files with 93 additions and 125 deletions

View File

@@ -1,16 +1,12 @@
"use client"; "use client";
import { tableData } from "@/data/nodeDataStatic";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { NodeTable } from "@/components/table"; import { NodeTable } from "@/components/table";
import { useMachines } from "@/components/hooks/useMachines";
export default function Page() { export default function Page() {
//const { data, isLoading } = useMachines();
//console.log({ data, isLoading });
return ( return (
<StrictMode> <StrictMode>
<NodeTable tableData={tableData} /> <NodeTable />
</StrictMode> </StrictMode>
); );
} }

View File

@@ -2,17 +2,17 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Grid2 from "@mui/material/Unstable_Grid2"; import Grid2 from "@mui/material/Unstable_Grid2";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
import { NodeStatus, TableData } from "@/data/nodeData";
import { PieCards } from "./pieCards"; import { PieCards } from "./pieCards";
import { PieData, NodePieChart } from "./nodePieChart"; import { PieData, NodePieChart } from "./nodePieChart";
import { Machine } from "@/api/model/machine";
import { Status } from "@/api/model";
interface EnhancedTableToolbarProps { interface EnhancedTableToolbarProps {
tableData: TableData[]; tableData: readonly Machine[];
} }
export function EnhancedTableToolbar( export function EnhancedTableToolbar(
@@ -24,13 +24,13 @@ export function EnhancedTableToolbar(
const pieData: PieData[] = useMemo(() => { const pieData: PieData[] = useMemo(() => {
const online = tableData.filter( const online = tableData.filter(
(row) => row.status === NodeStatus.Online, (row) => row.status === Status.online,
).length; ).length;
const offline = tableData.filter( const offline = tableData.filter(
(row) => row.status === NodeStatus.Offline, (row) => row.status === Status.offline,
).length; ).length;
const pending = tableData.filter( const pending = tableData.filter(
(row) => row.status === NodeStatus.Pending, (row) => row.status === Status.unknown,
).length; ).length;
return [ return [

View File

@@ -10,15 +10,13 @@ import CircleIcon from "@mui/icons-material/Circle";
import Stack from "@mui/material/Stack/Stack"; import Stack from "@mui/material/Stack/Stack";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2 import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
import { Collapse, useMediaQuery, useTheme } from "@mui/material"; import { Collapse } from "@mui/material";
import { Machine, Status } from "@/api/model";
import { NodeStatus, NodeStatusKeys, TableData } from "@/data/nodeData"; function renderStatus(status: Status) {
function renderStatus(status: NodeStatusKeys) {
switch (status) { switch (status) {
case NodeStatus.Online: case Status.online:
return ( return (
<Stack direction="row" alignItems="center" gap={1}> <Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="success" style={{ fontSize: 15 }} /> <CircleIcon color="success" style={{ fontSize: 15 }} />
@@ -28,7 +26,7 @@ function renderStatus(status: NodeStatusKeys) {
</Stack> </Stack>
); );
case NodeStatus.Offline: case Status.offline:
return ( return (
<Stack direction="row" alignItems="center" gap={1}> <Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="error" style={{ fontSize: 15 }} /> <CircleIcon color="error" style={{ fontSize: 15 }} />
@@ -37,7 +35,7 @@ function renderStatus(status: NodeStatusKeys) {
</Typography> </Typography>
</Stack> </Stack>
); );
case NodeStatus.Pending: case Status.unknown:
return ( return (
<Stack direction="row" alignItems="center" gap={1}> <Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="warning" style={{ fontSize: 15 }} /> <CircleIcon color="warning" style={{ fontSize: 15 }} />
@@ -49,16 +47,12 @@ function renderStatus(status: NodeStatusKeys) {
} }
} }
export function NodeRow(props: { export function NodeRow(props: {
row: TableData; row: Machine;
selected: string | undefined; selected: string | undefined;
setSelected: (a: string | undefined) => void; setSelected: (a: string | undefined) => void;
}) { }) {
const theme = useTheme();
const is_phone = useMediaQuery(theme.breakpoints.down("md"));
const { row, selected, setSelected } = props; const { row, selected, setSelected } = props;
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
//const labelId = `enhanced-table-checkbox-${index}`;
// Speed optimization. We compare string pointers here instead of the string content. // Speed optimization. We compare string pointers here instead of the string content.
const isSelected = selected == row.name; const isSelected = selected == row.name;
@@ -109,15 +103,6 @@ export function NodeRow(props: {
> >
{renderStatus(row.status)} {renderStatus(row.status)}
</TableCell> </TableCell>
<TableCell
align="right"
onClick={(event) => handleClick(event, row.name)}
>
<Typography component="div" align="left" variant="body1">
{String(row.last_seen).padStart(3, "0")}{" "}
{is_phone ? "days" : "days ago"}
</Typography>
</TableCell>
</TableRow> </TableRow>
{/* Row Expansion */} {/* Row Expansion */}

View File

@@ -1,28 +1,22 @@
"use client"; "use client";
import { useState, ChangeEvent, SetStateAction, Dispatch } from "react"; import { useState, ChangeEvent, useMemo } from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import TablePagination from "@mui/material/TablePagination"; import TablePagination from "@mui/material/TablePagination";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton"; import { CircularProgress, Grid, useTheme } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import SearchIcon from "@mui/icons-material/Search";
import { useTheme } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import { TableData } from "@/data/nodeData";
import { EnhancedTableToolbar } from "./enhancedTableToolbar"; import { EnhancedTableToolbar } from "./enhancedTableToolbar";
import { StickySpeedDial } from "./stickySpeedDial"; import { StickySpeedDial } from "./stickySpeedDial";
import { NodeTableContainer } from "./nodeTableContainer"; import { NodeTableContainer } from "./nodeTableContainer";
import { SearchBar } from "./searchBar"; import { SearchBar } from "./searchBar";
import Grid2 from "@mui/material/Unstable_Grid2/Grid2"; import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
import { useMachines } from "../hooks/useMachines";
import { Machine } from "@/api/model/machine";
export interface NodeTableProps { export function NodeTable() {
tableData: TableData[]; const machines = useMachines();
}
export function NodeTable(props: NodeTableProps) {
let { tableData } = props;
const theme = useTheme(); const theme = useTheme();
const is_xs = useMediaQuery(theme.breakpoints.only("xs")); const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
@@ -30,7 +24,15 @@ export function NodeTable(props: NodeTableProps) {
const [selected, setSelected] = useState<string | undefined>(undefined); const [selected, setSelected] = useState<string | undefined>(undefined);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5); const [rowsPerPage, setRowsPerPage] = useState(5);
const [filteredList, setFilteredList] = useState<TableData[]>(tableData); const [filteredList, setFilteredList] = useState<readonly Machine[]>([]);
const tableData = useMemo(() => {
const tableData = machines.data.map((machine) => {
return { name: machine.name, status: machine.status };
});
setFilteredList(tableData);
return tableData;
}, [machines.data]);
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage); setPage(newPage);
@@ -41,38 +43,53 @@ export function NodeTable(props: NodeTableProps) {
setPage(0); setPage(0);
}; };
return ( if (machines.isLoading) {
<Box sx={{ width: "100%" }}> return (
<Paper sx={{ width: "100%", mb: 2 }}> <Grid
<StickySpeedDial selected={selected} /> container
<EnhancedTableToolbar tableData={tableData}> style={{ height: "100vh" }} // make the container fill the screen height
<Grid2 xs={12}> alignItems="center" // center the items vertically
<SearchBar justifyContent="center" // center the items horizontally
tableData={tableData} >
setFilteredList={setFilteredList} <CircularProgress size={80} color="secondary" />
/> </Grid>
</Grid2> );
</EnhancedTableToolbar> } else {
<NodeTableContainer return (
tableData={filteredList} <Box sx={{ width: "100%" }}>
page={page} <Paper sx={{ width: "100%", mb: 2 }}>
rowsPerPage={rowsPerPage} <StickySpeedDial selected={selected} />
dense={false} <EnhancedTableToolbar tableData={tableData}>
selected={selected} <Grid2 xs={12}>
setSelected={setSelected} <SearchBar
/> tableData={tableData}
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */} setFilteredList={setFilteredList}
<TablePagination />
rowsPerPageOptions={[5, 10, 25]} </Grid2>
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"} </EnhancedTableToolbar>
component="div"
count={filteredList.length} <NodeTableContainer
rowsPerPage={rowsPerPage} tableData={filteredList}
page={page} page={page}
onPageChange={handleChangePage} rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleChangeRowsPerPage} dense={false}
/> selected={selected}
</Paper> setSelected={setSelected}
</Box> />
);
{/* 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={filteredList.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
</Box>
);
}
} }

View File

@@ -12,13 +12,11 @@ import TableSortLabel from "@mui/material/TableSortLabel";
import { visuallyHidden } from "@mui/utils"; import { visuallyHidden } from "@mui/utils";
import { NodeRow } from "./nodeRow"; import { NodeRow } from "./nodeRow";
import { TableData } from "@/data/nodeData"; import { Machine } from "@/api/model/machine";
import { useMediaQuery, useTheme } from "@mui/material";
interface HeadCell { interface HeadCell {
disablePadding: boolean; disablePadding: boolean;
id: keyof TableData; id: keyof Machine;
label: string; label: string;
alignRight: boolean; alignRight: boolean;
} }
@@ -28,7 +26,7 @@ const headCells: readonly HeadCell[] = [
id: "name", id: "name",
alignRight: false, alignRight: false,
disablePadding: false, disablePadding: false,
label: "DISPLAY NAME & ID", label: "DOMAIN NAME",
}, },
{ {
id: "status", id: "status",
@@ -36,12 +34,6 @@ const headCells: readonly HeadCell[] = [
disablePadding: false, disablePadding: false,
label: "STATUS", label: "STATUS",
}, },
{
id: "last_seen",
alignRight: false,
disablePadding: false,
label: "LAST SEEN",
},
]; ];
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) { function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
@@ -90,7 +82,7 @@ function stableSort<T>(
interface EnhancedTableProps { interface EnhancedTableProps {
onRequestSort: ( onRequestSort: (
event: React.MouseEvent<unknown>, event: React.MouseEvent<unknown>,
property: keyof TableData, property: keyof Machine,
) => void; ) => void;
order: NodeOrder; order: NodeOrder;
orderBy: string; orderBy: string;
@@ -100,7 +92,7 @@ interface EnhancedTableProps {
function EnhancedTableHead(props: EnhancedTableProps) { function EnhancedTableHead(props: EnhancedTableProps) {
const { order, orderBy, onRequestSort } = props; const { order, orderBy, onRequestSort } = props;
const createSortHandler = const createSortHandler =
(property: keyof TableData) => (event: React.MouseEvent<unknown>) => { (property: keyof Machine) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property); onRequestSort(event, property);
}; };
@@ -135,7 +127,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
} }
interface NodeTableContainerProps { interface NodeTableContainerProps {
tableData: readonly TableData[]; tableData: readonly Machine[];
page: number; page: number;
rowsPerPage: number; rowsPerPage: number;
dense: boolean; dense: boolean;
@@ -146,10 +138,7 @@ interface NodeTableContainerProps {
export function NodeTableContainer(props: NodeTableContainerProps) { export function NodeTableContainer(props: NodeTableContainerProps) {
const { tableData, page, rowsPerPage, dense, selected, setSelected } = props; const { tableData, page, rowsPerPage, dense, selected, setSelected } = props;
const [order, setOrder] = React.useState<NodeOrder>("asc"); const [order, setOrder] = React.useState<NodeOrder>("asc");
const [orderBy, setOrderBy] = React.useState<keyof TableData>("status"); const [orderBy, setOrderBy] = React.useState<keyof Machine>("status");
const theme = useTheme();
const is_phone = useMediaQuery(theme.breakpoints.down("sm"));
// Avoid a layout jump when reaching the last page with empty rows. // Avoid a layout jump when reaching the last page with empty rows.
const emptyRows = const emptyRows =
@@ -157,7 +146,7 @@ export function NodeTableContainer(props: NodeTableContainerProps) {
const handleRequestSort = ( const handleRequestSort = (
event: React.MouseEvent<unknown>, event: React.MouseEvent<unknown>,
property: keyof TableData, property: keyof Machine,
) => { ) => {
const isAsc = orderBy === property && order === "asc"; const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? "desc" : "asc"); setOrder(isAsc ? "desc" : "asc");
@@ -185,7 +174,7 @@ export function NodeTableContainer(props: NodeTableContainerProps) {
{visibleRows.map((row, index) => { {visibleRows.map((row, index) => {
return ( return (
<NodeRow <NodeRow
key={row.name} key={index}
row={row} row={row}
selected={selected} selected={selected}
setSelected={setSelected} setSelected={setSelected}

View File

@@ -1,34 +1,15 @@
"use client"; "use client";
import { import { SetStateAction, Dispatch, useState, useEffect, useMemo } from "react";
SetStateAction,
Dispatch,
useState,
useEffect,
useRef,
useMemo,
ClassAttributes,
JSX,
Key,
LiHTMLAttributes,
} from "react";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { useDebounce } from "../hooks/useDebounce"; import { useDebounce } from "../hooks/useDebounce";
import { TableData } from "@/data/nodeData"; import { Autocomplete, InputAdornment, TextField } from "@mui/material";
import { import { Machine } from "@/api/model/machine";
Autocomplete,
Box,
Container,
InputAdornment,
Stack,
TextField,
} from "@mui/material";
export interface SearchBarProps { export interface SearchBarProps {
tableData: TableData[]; tableData: readonly Machine[];
setFilteredList: Dispatch<SetStateAction<TableData[]>>; setFilteredList: Dispatch<SetStateAction<readonly Machine[]>>;
} }
export function SearchBar(props: SearchBarProps) { export function SearchBar(props: SearchBarProps) {
@@ -52,7 +33,7 @@ export function SearchBar(props: SearchBarProps) {
useEffect(() => { useEffect(() => {
if (debouncedSearch) { if (debouncedSearch) {
const filtered: TableData[] = tableData.filter((row) => { const filtered: Machine[] = tableData.filter((row) => {
return row.name.toLowerCase().includes(debouncedSearch.toLowerCase()); return row.name.toLowerCase().includes(debouncedSearch.toLowerCase());
}); });
setFilteredList(filtered); setFilteredList(filtered);