consistent naming & strucutre
This commit is contained in:
82
pkgs/ui/src/components/table/enhancedTableToolbar.tsx
Normal file
82
pkgs/ui/src/components/table/enhancedTableToolbar.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Grid2 from "@mui/material/Unstable_Grid2";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
import { NodeStatus, TableData } from "@/data/nodeData";
|
||||
import { PieCards } from "./pieCards";
|
||||
import { PieData, NodePieChart } from "./nodePieChart";
|
||||
|
||||
interface EnhancedTableToolbarProps {
|
||||
tableData: TableData[];
|
||||
}
|
||||
|
||||
export function EnhancedTableToolbar(
|
||||
props: React.PropsWithChildren<EnhancedTableToolbarProps>,
|
||||
) {
|
||||
const { tableData } = props;
|
||||
const theme = useTheme();
|
||||
const is_lg = useMediaQuery(theme.breakpoints.down("lg"));
|
||||
|
||||
const pieData: PieData[] = 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 (
|
||||
<Grid2 container spacing={1}>
|
||||
{/* 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" } }}
|
||||
>
|
||||
<PieCards pieData={pieData} />
|
||||
</Grid2>
|
||||
|
||||
{/*Toolbar Grid */}
|
||||
<Grid2 key="Toolbar" xs={12}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
pl: { sm: 2 },
|
||||
pr: { xs: 1, sm: 1 },
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Toolbar>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
}
|
||||
1
pkgs/ui/src/components/table/index.tsx
Normal file
1
pkgs/ui/src/components/table/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { NodeTable } from "./nodeTable";
|
||||
57
pkgs/ui/src/components/table/nodePieChart.tsx
Normal file
57
pkgs/ui/src/components/table/nodePieChart.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from "recharts";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export interface PieData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: PieData[];
|
||||
showLabels?: boolean;
|
||||
}
|
||||
|
||||
export function NodePieChart(props: Props) {
|
||||
const theme = useTheme();
|
||||
const { data, showLabels } = props;
|
||||
|
||||
return (
|
||||
<Box height={350}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
innerRadius={85}
|
||||
outerRadius={120}
|
||||
fill={theme.palette.primary.main}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
label={showLabels}
|
||||
legendType="square"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
startAngle={0}
|
||||
endAngle={360}
|
||||
paddingAngle={0}
|
||||
labelLine={true}
|
||||
hide={false}
|
||||
minAngle={0}
|
||||
isAnimationActive={true}
|
||||
animationBegin={0}
|
||||
animationDuration={1000}
|
||||
animationEasing="ease-in"
|
||||
blendStroke={true}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Legend verticalAlign="bottom" />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
171
pkgs/ui/src/components/table/nodeRow.tsx
Normal file
171
pkgs/ui/src/components/table/nodeRow.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
"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 function NodeRow(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.id)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
93
pkgs/ui/src/components/table/nodeTable.tsx
Normal file
93
pkgs/ui/src/components/table/nodeTable.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useState, ChangeEvent, SetStateAction, Dispatch } 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 { useTheme } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
import { TableData } from "@/data/nodeData";
|
||||
import { EnhancedTableToolbar } from "./enhancedTableToolbar";
|
||||
import { StickySpeedDial } from "./stickySpeedDial";
|
||||
import { NodeTableContainer } from "./nodeTableContainer";
|
||||
export interface SearchBarProps {
|
||||
search: string;
|
||||
setSearch: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
function SearchBar(props: SearchBarProps) {
|
||||
const { search, setSearch } = props;
|
||||
const handleSearch = (event: 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[];
|
||||
}
|
||||
|
||||
export function NodeTable(props: NodeTableProps) {
|
||||
let { tableData } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
|
||||
|
||||
const [selected, setSelected] = useState<string | undefined>(undefined);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Paper sx={{ width: "100%", mb: 2 }}>
|
||||
<StickySpeedDial selected={selected} />
|
||||
<EnhancedTableToolbar tableData={tableData}>
|
||||
<SearchBar search={search} setSearch={setSearch} />
|
||||
</EnhancedTableToolbar>
|
||||
<NodeTableContainer
|
||||
tableData={tableData}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
dense={false}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
207
pkgs/ui/src/components/table/nodeTableContainer.tsx
Normal file
207
pkgs/ui/src/components/table/nodeTableContainer.tsx
Normal file
@@ -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<T>(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<Key extends keyof any>(
|
||||
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<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]);
|
||||
}
|
||||
|
||||
interface EnhancedTableProps {
|
||||
onRequestSort: (
|
||||
event: React.MouseEvent<unknown>,
|
||||
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<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>
|
||||
);
|
||||
}
|
||||
|
||||
interface NodeTableContainerProps {
|
||||
tableData: readonly TableData[];
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
dense: boolean;
|
||||
selected: string | undefined;
|
||||
setSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
export function NodeTableContainer(props: NodeTableContainerProps) {
|
||||
const { tableData, page, rowsPerPage, dense, selected, setSelected } = props;
|
||||
const [order, setOrder] = React.useState<NodeOrder>("asc");
|
||||
const [orderBy, setOrderBy] = React.useState<keyof TableData>("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<unknown>,
|
||||
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 (
|
||||
<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) => {
|
||||
return (
|
||||
<NodeRow
|
||||
key={row.name}
|
||||
row={row}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow
|
||||
style={{
|
||||
height: (dense ? 33 : 53) * emptyRows,
|
||||
}}
|
||||
>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
73
pkgs/ui/src/components/table/pieCards.tsx
Normal file
73
pkgs/ui/src/components/table/pieCards.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Card, CardContent, Stack, Typography } from "@mui/material";
|
||||
import hexRgb from "hex-rgb";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface PieData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface PieCardsProps {
|
||||
pieData: PieData[];
|
||||
}
|
||||
|
||||
export function PieCards(props: PieCardsProps) {
|
||||
const { pieData } = props;
|
||||
|
||||
const cardData = useMemo(() => {
|
||||
return pieData
|
||||
.filter((pieItem) => pieItem.value > 0)
|
||||
.concat({
|
||||
name: "Total",
|
||||
value: pieData.reduce((a, b) => a + b.value, 0),
|
||||
color: "#000000",
|
||||
});
|
||||
}, [pieData]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{ 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>
|
||||
);
|
||||
}
|
||||
85
pkgs/ui/src/components/table/stickySpeedDial.tsx
Normal file
85
pkgs/ui/src/components/table/stickySpeedDial.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
|
||||
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||
import EditIcon from "@mui/icons-material/ModeEdit";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import Link from "next/link";
|
||||
|
||||
export 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={`/machines/edit/${selected}`} 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="/machines/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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user