add: responsive layout for sidebar and dashboard

This commit is contained in:
Johannes Kirschbauer
2023-08-12 12:25:44 +02:00
parent a243f97574
commit ff89bcba4b
9 changed files with 472 additions and 222 deletions

View File

@@ -10,3 +10,11 @@ Update floco dependencies:
`nix run github:aakropotkin/floco -- translate -pt -o ./nix/pdefs.nix` `nix run github:aakropotkin/floco -- translate -pt -o ./nix/pdefs.nix`
The prettier tailwind class sorting is not yet working properly with our devShell integration.
To sort classnames manually:
`cd /clan-core/pkgs/ui/`
`prettier -w ./src/ --config pconf.cjs`

4
pkgs/ui/pconf.cjs Normal file
View File

@@ -0,0 +1,4 @@
// prettier.config.js
module.exports = {
plugins: ["prettier-plugin-tailwindcss"],
};

View File

@@ -3,13 +3,21 @@
import "./globals.css"; import "./globals.css";
import localFont from "next/font/local"; import localFont from "next/font/local";
import * as React from "react"; import * as React from "react";
import { CssBaseline, ThemeProvider } from "@mui/material"; import {
CssBaseline,
IconButton,
ThemeProvider,
useMediaQuery,
} from "@mui/material";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import { StyledEngineProvider } from "@mui/material/styles"; import { StyledEngineProvider } from "@mui/material/styles";
import { darkTheme, lightTheme } from "./theme/themes"; import { darkTheme, lightTheme } from "./theme/themes";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import MenuIcon from "@mui/icons-material/Menu";
import { ChevronLeft } from "@mui/icons-material";
import Image from "next/image";
const roboto = localFont({ const roboto = localFont({
src: [ src: [
@@ -26,12 +34,18 @@ export default function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)");
let [useDarkTheme, setUseDarkTheme] = useState(false); let [useDarkTheme, setUseDarkTheme] = useState(false);
let [theme, setTheme] = useState(useDarkTheme ? darkTheme : lightTheme); let [showSidebar, setShowSidebar] = useState(true);
React.useEffect(() => {
if (useDarkTheme !== userPrefersDarkmode) {
// Enable dark theme if the user prefers dark mode
setUseDarkTheme(userPrefersDarkmode);
}
}, [userPrefersDarkmode, useDarkTheme, setUseDarkTheme]);
const changeThemeHandler = (target: ChangeEvent, currentValue: boolean) => { const changeThemeHandler = (target: ChangeEvent, currentValue: boolean) => {
setUseDarkTheme(currentValue); setUseDarkTheme(currentValue);
setTheme(currentValue ? darkTheme : lightTheme);
}; };
return ( return (
@@ -43,13 +57,42 @@ export default function RootLayout({
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</head> </head>
<StyledEngineProvider injectFirst> <StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}> <ThemeProvider theme={useDarkTheme ? darkTheme : lightTheme}>
<body id="__next" className={roboto.className}> <body id="__next" className={roboto.className}>
<CssBaseline /> <CssBaseline />
<div className="flex h-screen overflow-hidden"> <div className="flex h-screen overflow-hidden">
<Sidebar /> <Sidebar
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden"> show={showSidebar}
<main>{children}</main> onClose={() => setShowSidebar(false)}
/>
<div className="flex flex-col w-full h-full">
<div className="static min-h-10 top-0 mb-2 py-2">
<div className="grid grid-cols-3">
<div className="col-span-1">
<IconButton
hidden={true}
onClick={() => setShowSidebar((c) => !c)}
>
{!showSidebar && <MenuIcon />}
</IconButton>
</div>
<div className="col-span-1 block lg:hidden w-full text-center font-semibold text-white ">
<Image
src="/logo.svg"
alt="Clan Logo"
width={58}
height={58}
priority
/>
</div>
</div>
</div>
<div className="px-1">
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<main>{children}</main>
</div>
</div>
</div> </div>
</div> </div>
</body> </body>

View File

@@ -1,38 +1,43 @@
"use client" "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 Checkbox from '@mui/material/Checkbox';
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 { visuallyHidden } from '@mui/utils';
import CircleIcon from '@mui/icons-material/Circle';
import Stack from '@mui/material/Stack/Stack';
import ModeIcon from '@mui/icons-material/Mode';
import ClearIcon from '@mui/icons-material/Clear';
import Fade from '@mui/material/Fade/Fade';
import NodePieChart, { PieData } from './NodePieChart';
import Grid2 from '@mui/material/Unstable_Grid2'; // Grid version 2
import { Card, CardContent, Container, FormGroup, useTheme } from '@mui/material';
import hexRgb from 'hex-rgb';
import useMediaQuery from '@mui/material/useMediaQuery';
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 Checkbox from "@mui/material/Checkbox";
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 { visuallyHidden } from "@mui/utils";
import CircleIcon from "@mui/icons-material/Circle";
import Stack from "@mui/material/Stack/Stack";
import ModeIcon from "@mui/icons-material/Mode";
import ClearIcon from "@mui/icons-material/Clear";
import Fade from "@mui/material/Fade/Fade";
import NodePieChart, { PieData } from "./NodePieChart";
import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
import {
Card,
CardContent,
Container,
FormGroup,
useTheme,
} from "@mui/material";
import hexRgb from "hex-rgb";
import useMediaQuery from "@mui/material/useMediaQuery";
export interface TableData { export interface TableData {
name: string; name: string;
@@ -47,7 +52,6 @@ export enum NodeStatus {
Pending, Pending,
} }
interface HeadCell { interface HeadCell {
disablePadding: boolean; disablePadding: boolean;
id: keyof TableData; id: keyof TableData;
@@ -57,22 +61,22 @@ interface HeadCell {
const headCells: readonly HeadCell[] = [ const headCells: readonly HeadCell[] = [
{ {
id: 'name', id: "name",
alignRight: false, alignRight: false,
disablePadding: false, disablePadding: false,
label: 'DISPLAY NAME & ID', label: "DISPLAY NAME & ID",
}, },
{ {
id: 'status', id: "status",
alignRight: false, alignRight: false,
disablePadding: false, disablePadding: false,
label: 'STATUS', label: "STATUS",
}, },
{ {
id: 'last_seen', id: "last_seen",
alignRight: false, alignRight: false,
disablePadding: false, disablePadding: false,
label: 'LAST SEEN', label: "LAST SEEN",
}, },
]; ];
@@ -86,7 +90,7 @@ function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
return 0; return 0;
} }
type Order = 'asc' | 'desc'; type Order = "asc" | "desc";
function getComparator<Key extends keyof any>( function getComparator<Key extends keyof any>(
order: Order, order: Order,
@@ -95,7 +99,7 @@ function getComparator<Key extends keyof any>(
a: { [key in Key]: number | string | boolean }, a: { [key in Key]: number | string | boolean },
b: { [key in Key]: number | string | boolean }, b: { [key in Key]: number | string | boolean },
) => number { ) => number {
return order === 'desc' return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy) ? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy); : (a, b) => -descendingComparator(a, b, orderBy);
} }
@@ -104,7 +108,10 @@ function getComparator<Key extends keyof any>(
// stableSort() brings sort stability to non-modern browsers (notably IE11). If you // stableSort() brings sort stability to non-modern browsers (notably IE11). If you
// only support modern browsers you can replace stableSort(exampleArray, exampleComparator) // only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
// with exampleArray.slice().sort(exampleComparator) // with exampleArray.slice().sort(exampleComparator)
function stableSort<T>(array: readonly T[], comparator: (a: T, b: T) => number) { function stableSort<T>(
array: readonly T[],
comparator: (a: T, b: T) => number,
) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]); const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => { stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]); const order = comparator(a[0], b[0]);
@@ -116,18 +123,18 @@ function stableSort<T>(array: readonly T[], comparator: (a: T, b: T) => number)
return stabilizedThis.map((el) => el[0]); return stabilizedThis.map((el) => el[0]);
} }
interface EnhancedTableProps { interface EnhancedTableProps {
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof TableData) => void; onRequestSort: (
event: React.MouseEvent<unknown>,
property: keyof TableData,
) => void;
order: Order; order: Order;
orderBy: string; orderBy: string;
rowCount: number; rowCount: number;
} }
function EnhancedTableHead(props: EnhancedTableProps) { function EnhancedTableHead(props: EnhancedTableProps) {
const { order, orderBy, onRequestSort } = const { order, orderBy, onRequestSort } = props;
props;
const createSortHandler = const createSortHandler =
(property: keyof TableData) => (event: React.MouseEvent<unknown>) => { (property: keyof TableData) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property); onRequestSort(event, property);
@@ -139,19 +146,19 @@ function EnhancedTableHead(props: EnhancedTableProps) {
{headCells.map((headCell) => ( {headCells.map((headCell) => (
<TableCell <TableCell
key={headCell.id} key={headCell.id}
align={headCell.alignRight ? 'right' : 'left'} align={headCell.alignRight ? "right" : "left"}
padding={headCell.disablePadding ? 'none' : 'normal'} padding={headCell.disablePadding ? "none" : "normal"}
sortDirection={orderBy === headCell.id ? order : false} sortDirection={orderBy === headCell.id ? order : false}
> >
<TableSortLabel <TableSortLabel
active={orderBy === headCell.id} active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'} direction={orderBy === headCell.id ? order : "asc"}
onClick={createSortHandler(headCell.id)} onClick={createSortHandler(headCell.id)}
> >
{headCell.label} {headCell.label}
{orderBy === headCell.id ? ( {orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}> <Box component="span" sx={visuallyHidden}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'} {order === "desc" ? "sorted descending" : "sorted ascending"}
</Box> </Box>
) : null} ) : null}
</TableSortLabel> </TableSortLabel>
@@ -162,8 +169,6 @@ function EnhancedTableHead(props: EnhancedTableProps) {
); );
} }
interface EnhancedTableToolbarProps { interface EnhancedTableToolbarProps {
selected: string | undefined; selected: string | undefined;
tableData: TableData[]; tableData: TableData[];
@@ -172,41 +177,49 @@ interface EnhancedTableToolbarProps {
function EnhancedTableToolbar(props: EnhancedTableToolbarProps) { function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
const { selected, onClear, tableData } = props; const { selected, onClear, tableData } = props;
const theme = useTheme(); const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('lg')); const matches = useMediaQuery(theme.breakpoints.down("lg"));
const isSelected = selected != undefined; const isSelected = selected != undefined;
const [debug, setDebug] = React.useState<boolean>(false); const [debug, setDebug] = React.useState<boolean>(false);
const debugSx = debug ? { const debugSx = debug
'--Grid-borderWidth': '1px', ? {
borderTop: 'var(--Grid-borderWidth) solid', "--Grid-borderWidth": "1px",
borderLeft: 'var(--Grid-borderWidth) solid', borderTop: "var(--Grid-borderWidth) solid",
borderColor: 'divider', borderLeft: "var(--Grid-borderWidth) solid",
'& > div': { borderColor: "divider",
borderRight: 'var(--Grid-borderWidth) solid', "& > div": {
borderBottom: 'var(--Grid-borderWidth) solid', borderRight: "var(--Grid-borderWidth) solid",
borderColor: 'divider', borderBottom: "var(--Grid-borderWidth) solid",
} borderColor: "divider",
} : {}; },
}
: {};
const pieData = React.useMemo(() => { const pieData = React.useMemo(() => {
const online = tableData.filter((row) => row.status === NodeStatus.Online).length; const online = tableData.filter(
const offline = tableData.filter((row) => row.status === NodeStatus.Offline).length; (row) => row.status === NodeStatus.Online,
const pending = tableData.filter((row) => row.status === NodeStatus.Pending).length; ).length;
const offline = tableData.filter(
(row) => row.status === NodeStatus.Offline,
).length;
const pending = tableData.filter(
(row) => row.status === NodeStatus.Pending,
).length;
return [ return [
{ name: 'Online', value: online, color: '#2E7D32' }, { name: "Online", value: online, color: "#2E7D32" },
{ name: 'Offline', value: offline, color: '#db3927' }, { name: "Offline", value: offline, color: "#db3927" },
{ name: 'Pending', value: pending, color: '#FFBB28' }, { name: "Pending", value: pending, color: "#FFBB28" },
]; ];
}, [tableData]); }, [tableData]);
const cardData = React.useMemo(() => { const cardData = React.useMemo(() => {
return pieData.filter((pieItem) => pieItem.value > 0).concat( return pieData
{ .filter((pieItem) => pieItem.value > 0)
name: 'Total', .concat({
name: "Total",
value: pieData.reduce((a, b) => a + b.value, 0), value: pieData.reduce((a, b) => a + b.value, 0),
color: '#000000' color: "#000000",
} });
);
}, [pieData]); }, [pieData]);
const cardStack = ( const cardStack = (
@@ -217,17 +230,38 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
display="flex" display="flex"
flexDirection="column" flexDirection="column"
justifyContent="flex-start" justifyContent="flex-start"
flexWrap="wrap"> flexWrap="wrap"
>
{cardData.map((pieItem) => ( {cardData.map((pieItem) => (
<Card key={pieItem.name} sx={{ marginBottom: 2, marginRight: 2, width: 110, height: 110, backgroundColor: hexRgb(pieItem.color, { format: 'css', alpha: 0.18 }) }}> <Card
<CardContent > key={pieItem.name}
<Typography variant="h4" component="div" gutterBottom={true} textAlign="center"> sx={{
marginBottom: 2,
marginRight: 2,
width: 110,
height: 110,
backgroundColor: hexRgb(pieItem.color, {
format: "css",
alpha: 0.18,
}),
}}
>
<CardContent>
<Typography
variant="h4"
component="div"
gutterBottom={true}
textAlign="center"
>
{pieItem.value} {pieItem.value}
</Typography> </Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary" textAlign="center"> <Typography
sx={{ mb: 1.5 }}
color="text.secondary"
textAlign="center"
>
{pieItem.name} {pieItem.name}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@@ -240,15 +274,19 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
pl: { sm: 2 }, pl: { sm: 2 },
pr: { xs: 1, sm: 1 }, pr: { xs: 1, sm: 1 },
bgcolor: (theme) => bgcolor: (theme) =>
alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity), alpha(
}}> theme.palette.primary.main,
theme.palette.action.activatedOpacity,
),
}}
>
<Tooltip title="Clear"> <Tooltip title="Clear">
<IconButton onClick={onClear}> <IconButton onClick={onClear}>
<ClearIcon /> <ClearIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Typography <Typography
sx={{ flex: '1 1 100%' }} sx={{ flex: "1 1 100%" }}
color="inherit" color="inherit"
style={{ fontSize: 18, marginBottom: 3, marginLeft: 3 }} style={{ fontSize: 18, marginBottom: 3, marginLeft: 3 }}
component="div" component="div"
@@ -260,7 +298,7 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
<ModeIcon /> <ModeIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Toolbar > </Toolbar>
); );
const unselectedToolbar = ( const unselectedToolbar = (
@@ -270,16 +308,15 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
pr: { xs: 1, sm: 1 }, pr: { xs: 1, sm: 1 },
}} }}
> >
<Box sx={{ flex: '1 1 100%' }} ></Box> <Box sx={{ flex: "1 1 100%" }}></Box>
<Tooltip title="Filter list"> <Tooltip title="Filter list">
<IconButton> <IconButton>
<FilterListIcon /> <FilterListIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Toolbar > </Toolbar>
); );
return ( return (
<Grid2 container spacing={1} sx={debugSx}> <Grid2 container spacing={1} sx={debugSx}>
<Grid2 key="Header" xs={6}> <Grid2 key="Header" xs={6}>
@@ -295,19 +332,41 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
{/* Debug Controls */} {/* Debug Controls */}
<Grid2 key="Debug-Controls" xs={6} justifyContent="right" display="flex"> <Grid2 key="Debug-Controls" xs={6} justifyContent="right" display="flex">
<FormGroup> <FormGroup>
<FormControlLabel control={<Switch onChange={() => { setDebug(!debug) }} checked={debug} />} label="Debug" /> <FormControlLabel
control={
<Switch
onChange={() => {
setDebug(!debug);
}}
checked={debug}
/>
}
label="Debug"
/>
</FormGroup> </FormGroup>
</Grid2> </Grid2>
{/* Pie Chart Grid */} {/* Pie Chart Grid */}
<Grid2 key="PieChart" lg={6} sm={12} display="flex" justifyContent="center" alignItems="center"> <Grid2
key="PieChart"
lg={6}
sm={12}
display="flex"
justifyContent="center"
alignItems="center"
>
<Box height={350} width={400}> <Box height={350} width={400}>
<NodePieChart data={pieData} showLabels={matches} /> <NodePieChart data={pieData} showLabels={matches} />
</Box> </Box>
</Grid2> </Grid2>
{/* Card Stack Grid */} {/* Card Stack Grid */}
<Grid2 key="CardStack" lg={6} display="flex" sx={{ display: { lg: 'flex', sm: 'none' } }} > <Grid2
key="CardStack"
lg={6}
display="flex"
sx={{ display: { lg: "flex", sm: "none" } }}
>
{cardStack} {cardStack}
</Grid2> </Grid2>
@@ -315,12 +374,10 @@ function EnhancedTableToolbar(props: EnhancedTableToolbarProps) {
<Grid2 key="Toolbar" xs={12}> <Grid2 key="Toolbar" xs={12}>
{isSelected ? selectedToolbar : unselectedToolbar} {isSelected ? selectedToolbar : unselectedToolbar}
</Grid2> </Grid2>
</Grid2> </Grid2>
); );
} }
function renderLastSeen(last_seen: number) { function renderLastSeen(last_seen: number) {
return ( return (
<Typography component="div" align="left" variant="body1"> <Typography component="div" align="left" variant="body1">
@@ -376,13 +433,13 @@ function renderStatus(status: NodeStatus) {
} }
export interface NodeTableProps { export interface NodeTableProps {
tableData: TableData[] tableData: TableData[];
} }
export default function NodeTable(props: NodeTableProps) { export default function NodeTable(props: NodeTableProps) {
let { tableData } = props; let { tableData } = props;
const [order, setOrder] = React.useState<Order>('asc'); const [order, setOrder] = React.useState<Order>("asc");
const [orderBy, setOrderBy] = React.useState<keyof TableData>('status'); const [orderBy, setOrderBy] = React.useState<keyof TableData>("status");
const [selected, setSelected] = React.useState<string | undefined>(undefined); const [selected, setSelected] = React.useState<string | undefined>(undefined);
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [dense, setDense] = React.useState(false); const [dense, setDense] = React.useState(false);
@@ -392,8 +449,8 @@ export default function NodeTable(props: NodeTableProps) {
event: React.MouseEvent<unknown>, event: React.MouseEvent<unknown>,
property: keyof TableData, property: keyof TableData,
) => { ) => {
const isAsc = orderBy === property && order === 'asc'; const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? 'desc' : 'asc'); setOrder(isAsc ? "desc" : "asc");
setOrderBy(property); setOrderBy(property);
}; };
@@ -410,7 +467,9 @@ export default function NodeTable(props: NodeTableProps) {
setPage(newPage); setPage(newPage);
}; };
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setRowsPerPage(parseInt(event.target.value, 10)); setRowsPerPage(parseInt(event.target.value, 10));
setPage(0); setPage(0);
}; };
@@ -431,18 +490,20 @@ export default function NodeTable(props: NodeTableProps) {
[order, orderBy, page, rowsPerPage, tableData], [order, orderBy, page, rowsPerPage, tableData],
); );
return ( return (
<Paper elevation={1} sx={{ margin: 5 }}> <Paper elevation={1} sx={{ margin: 5 }}>
<Box sx={{ width: '100%' }}> <Box sx={{ width: "100%" }}>
<Paper sx={{ width: '100%', mb: 2 }}> <Paper sx={{ width: "100%", mb: 2 }}>
<EnhancedTableToolbar tableData={tableData} selected={selected} onClear={() => setSelected(undefined)} /> <EnhancedTableToolbar
tableData={tableData}
selected={selected}
onClear={() => setSelected(undefined)}
/>
<TableContainer> <TableContainer>
<Table <Table
sx={{ minWidth: 750 }} sx={{ minWidth: 750 }}
aria-labelledby="tableTitle" aria-labelledby="tableTitle"
size={dense ? 'small' : 'medium'} size={dense ? "small" : "medium"}
> >
<EnhancedTableHead <EnhancedTableHead
order={order} order={order}
@@ -464,17 +525,17 @@ export default function NodeTable(props: NodeTableProps) {
tabIndex={-1} tabIndex={-1}
key={row.name} key={row.name}
selected={isItemSelected} selected={isItemSelected}
sx={{ cursor: 'pointer' }} sx={{ cursor: "pointer" }}
> >
<TableCell <TableCell component="th" id={labelId} scope="row">
component="th"
id={labelId}
scope="row"
>
{renderName(row.name, row.id)} {renderName(row.name, row.id)}
</TableCell> </TableCell>
<TableCell align="right">{renderStatus(row.status)}</TableCell> <TableCell align="right">
<TableCell align="right">{renderLastSeen(row.last_seen)}</TableCell> {renderStatus(row.status)}
</TableCell>
<TableCell align="right">
{renderLastSeen(row.last_seen)}
</TableCell>
</TableRow> </TableRow>
); );
})} })}

View File

@@ -1,45 +1,50 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from "react";
import { PieChart, Pie, Sector, Cell, ResponsiveContainer, Legend } from 'recharts'; import {
import { useTheme } from '@mui/material/styles'; PieChart,
import { Box, Color } from '@mui/material'; Pie,
Sector,
Cell,
ResponsiveContainer,
Legend,
} from "recharts";
import { useTheme } from "@mui/material/styles";
import { Box, Color } from "@mui/material";
export interface PieData { export interface PieData {
name: string; name: string;
value: number; value: number;
color: string; color: string;
}; }
interface Props { interface Props {
data: PieData[]; data: PieData[];
showLabels?: boolean; showLabels?: boolean;
}; }
export default function NodePieChart(props: Props ) { export default function NodePieChart(props: Props) {
const theme = useTheme(); const theme = useTheme();
const {data, showLabels} = props; const { data, showLabels } = props;
return (
return ( <Box height={350}>
<Box height={350}> <ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%"> <PieChart>
<PieChart> <Pie
<Pie data={data}
data={data} innerRadius={85}
innerRadius={85} outerRadius={120}
outerRadius={120} fill={theme.palette.primary.main}
fill={theme.palette.primary.main} dataKey="value"
dataKey="value" nameKey="name"
nameKey="name" label={showLabels}
label={showLabels} >
> {data.map((entry, index) => (
{data.map((entry, index) => ( <Cell key={`cell-${index}`} fill={entry.color} />
<Cell key={`cell-${index}`} fill={entry.color} /> ))}
))} </Pie>
</Pie> <Legend verticalAlign="bottom" />
<Legend verticalAlign="bottom" /> </PieChart>
</PieChart> </ResponsiveContainer>
</ResponsiveContainer> </Box>
</Box> );
); }
};

View File

@@ -1,4 +1,4 @@
"use client" "use client";
import { StrictMode } from "react"; import { StrictMode } from "react";
import NodeList, { NodeStatus, TableData } from "./NodeList"; import NodeList, { NodeStatus, TableData } from "./NodeList";
@@ -6,42 +6,98 @@ import NodeList, { NodeStatus, TableData } from "./NodeList";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
function createData( function createData(
name: string, name: string,
id: string, id: string,
status: NodeStatus, status: NodeStatus,
last_seen: number, last_seen: number,
): TableData { ): TableData {
return {
name,
return { id,
name, status,
id, last_seen: last_seen,
status, };
last_seen: last_seen,
};
} }
const tableData = [ const tableData = [
createData('Matchbox', "42:0:f21:6916:e333:c47e:4b5c:e74c", NodeStatus.Pending, 0), createData(
createData('Ahorn', "42:0:3c46:b51c:b34d:b7e1:3b02:8d24", NodeStatus.Online, 0), "Matchbox",
createData('Yellow', "42:0:3c46:98ac:9c80:4f25:50e3:1d8f", NodeStatus.Offline, 16.0), "42:0:f21:6916:e333:c47e:4b5c:e74c",
createData('Rauter', "42:0:61ea:b777:61ea:803:f885:3523", NodeStatus.Offline, 6.0), NodeStatus.Pending,
createData('Porree', "42:0:e644:4499:d034:895e:34c8:6f9a", NodeStatus.Offline, 13), 0,
createData('Helsinki', "42:0:3c46:fd4a:acf9:e971:6036:8047", NodeStatus.Online, 0), ),
createData('Kelle', "42:0:3c46:362d:a9aa:4996:c78e:839a", NodeStatus.Online, 0), createData(
createData('Shodan', "42:0:3c46:6745:adf4:a844:26c4:bf91", NodeStatus.Online, 0.0), "Ahorn",
createData('Qubasa', "42:0:3c46:123e:bbea:3529:db39:6764", NodeStatus.Offline, 7.0), "42:0:3c46:b51c:b34d:b7e1:3b02:8d24",
createData('Green', "42:0:a46e:5af:632c:d2fe:a71d:cde0", NodeStatus.Offline, 2), NodeStatus.Online,
createData('Gum', "42:0:e644:238d:3e46:c884:6ec5:16c", NodeStatus.Offline, 0), 0,
createData('Xu', "42:0:ca48:c2c2:19fb:a0e9:95b9:794f", NodeStatus.Online, 0), ),
createData('Zaatar', "42:0:3c46:156e:10b6:3bd6:6e82:b2cd", NodeStatus.Online, 0), createData(
"Yellow",
"42:0:3c46:98ac:9c80:4f25:50e3:1d8f",
NodeStatus.Offline,
16.0,
),
createData(
"Rauter",
"42:0:61ea:b777:61ea:803:f885:3523",
NodeStatus.Offline,
6.0,
),
createData(
"Porree",
"42:0:e644:4499:d034:895e:34c8:6f9a",
NodeStatus.Offline,
13,
),
createData(
"Helsinki",
"42:0:3c46:fd4a:acf9:e971:6036:8047",
NodeStatus.Online,
0,
),
createData(
"Kelle",
"42:0:3c46:362d:a9aa:4996:c78e:839a",
NodeStatus.Online,
0,
),
createData(
"Shodan",
"42:0:3c46:6745:adf4:a844:26c4:bf91",
NodeStatus.Online,
0.0,
),
createData(
"Qubasa",
"42:0:3c46:123e:bbea:3529:db39:6764",
NodeStatus.Offline,
7.0,
),
createData(
"Green",
"42:0:a46e:5af:632c:d2fe:a71d:cde0",
NodeStatus.Offline,
2,
),
createData("Gum", "42:0:e644:238d:3e46:c884:6ec5:16c", NodeStatus.Offline, 0),
createData("Xu", "42:0:ca48:c2c2:19fb:a0e9:95b9:794f", NodeStatus.Online, 0),
createData(
"Zaatar",
"42:0:3c46:156e:10b6:3bd6:6e82:b2cd",
NodeStatus.Online,
0,
),
]; ];
export default function Page() { export default function Page() {
return ( return (
<Box sx={{ backgroundColor: "#e9ecf5", height: "100%", width: "100%" }} display="inline-block" id="rootBox"> <Box
<NodeList tableData={tableData} /> sx={{ backgroundColor: "#e9ecf5", height: "100%", width: "100%" }}
</Box> display="inline-block"
); id="rootBox"
} >
<NodeList tableData={tableData} />
</Box>
);
}

View File

@@ -1,13 +1,60 @@
import { Button } from "@mui/material"; interface DashboardCardProps {
children?: React.ReactNode;
}
const DashboardCard = (props: DashboardCardProps) => {
const { children } = props;
return (
<div className="col-span-full border border-dashed border-slate-400 lg:col-span-1">
{children}
</div>
);
};
interface DashboardPanelProps {
children?: React.ReactNode;
}
const DashboardPanel = (props: DashboardPanelProps) => {
const { children } = props;
return (
<div className="col-span-full border border-dashed border-slate-400 lg:col-span-2">
{children}
</div>
);
};
interface SplitDashboardCardProps {
children?: React.ReactNode[];
}
const SplitDashboardCard = (props: SplitDashboardCardProps) => {
const { children } = props;
return (
<div className="col-span-full lg:col-span-1">
<div className="grid h-full grid-cols-1 gap-4">
{children?.map((row, idx) => (
<div
key={idx}
className="col-span-full border border-dashed border-slate-400"
>
{row}
</div>
))}
</div>
</div>
);
};
export default function Dashboard() { export default function Dashboard() {
return ( return (
<div className="w-full flex justify-center items-center h-screen"> <div className="flex h-screen w-full">
<div className="grid"> <div className="grid w-full grid-cols-3 gap-4">
Welcome to the Dashboard <DashboardCard>Current CLAN Overview</DashboardCard>
<Button variant="contained" color="primary"> <DashboardCard>Recent Activity Log</DashboardCard>
LOL <SplitDashboardCard>
</Button> <div>Notifications</div>
<div>Quick Action</div>
</SplitDashboardCard>
<DashboardPanel>Panel</DashboardPanel>
<DashboardCard>Side Bar (misc)</DashboardCard>
</div> </div>
</div> </div>
); );

View File

@@ -1,16 +1,13 @@
import { createTheme } from "@mui/material/styles"; import { createTheme } from "@mui/material/styles";
export const darkTheme = createTheme({ export const darkTheme = createTheme({
palette: { palette: {
mode: "dark", mode: "dark",
}, },
}); });
export const lightTheme = createTheme({ export const lightTheme = createTheme({
palette: { palette: {
mode: "light", mode: "light",
}, },
}); });

View File

@@ -1,5 +1,7 @@
import { import {
Divider, Divider,
Icon,
IconButton,
List, List,
ListItem, ListItem,
ListItemButton, ListItemButton,
@@ -7,7 +9,7 @@ import {
ListItemText, ListItemText,
} from "@mui/material"; } from "@mui/material";
import Image from "next/image"; import Image from "next/image";
import { ReactNode } from "react"; import { ReactNode, useState } from "react";
import DashboardIcon from "@mui/icons-material/Dashboard"; import DashboardIcon from "@mui/icons-material/Dashboard";
import DevicesIcon from "@mui/icons-material/Devices"; import DevicesIcon from "@mui/icons-material/Devices";
@@ -16,6 +18,9 @@ import AppsIcon from "@mui/icons-material/Apps";
import DesignServicesIcon from "@mui/icons-material/DesignServices"; import DesignServicesIcon from "@mui/icons-material/DesignServices";
import BackupIcon from "@mui/icons-material/Backup"; import BackupIcon from "@mui/icons-material/Backup";
import Link from "next/link"; import Link from "next/link";
import { tw } from "@/utils/tailwind";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
type MenuEntry = { type MenuEntry = {
icon: ReactNode; icon: ReactNode;
@@ -58,11 +63,23 @@ const menuEntries: MenuEntry[] = [
}, },
]; ];
export function Sidebar() { const hideSidebar = tw`-translate-x-12 absolute lg:-translate-x-64`;
const showSidebar = tw`lg:translate-x-0 static`;
interface SidebarProps {
show: boolean;
onClose: () => void;
}
export function Sidebar(props: SidebarProps) {
const { show, onClose } = props;
return ( return (
<aside className="absolute left-0 top-0 z-9999 flex h-screen w-12 sm:w-64 flex-col overflow-y-hidden bg-zinc-950 dark:bg-boxdark sm:static"> <aside
<div className="flex items-center justify-between gap-2 px-6 py-5.5 lg:py-6.5"> className={tw`${
<div className="mt-8 font-semibold text-white w-full text-center hidden sm:block"> show ? showSidebar : hideSidebar
} z-9999 dark:bg-boxdark left-0 top-0 flex h-screen w-12 flex-col overflow-x-hidden overflow-y-hidden bg-zinc-950 lg:w-64 transition ease-in-out duration-150`}
>
<div className="py-5.5 lg:py-6.5 flex items-center justify-between gap-2 overflow-hidden px-0 lg:px-6">
<div className="mt-8 hidden w-full text-center font-semibold text-white lg:block">
<Image <Image
src="/logo.svg" src="/logo.svg"
alt="Clan Logo" alt="Clan Logo"
@@ -72,20 +89,32 @@ export function Sidebar() {
/> />
</div> </div>
</div> </div>
<Divider flexItem className="bg-zinc-600 my-9 mx-8" /> <Divider
<div className="overflow-hidden flex flex-col overflow-y-auto duration-200 ease-linear"> flexItem
<List className="pb-4 mb-14 px-4 lg:mt-1 lg:px-6 text-white"> className="mx-8 mb-4 mt-9 bg-zinc-600 hidden lg:block"
/>
<div className="w-full flex justify-center">
<IconButton size="large" className="text-white" onClick={onClose}>
<ChevronLeftIcon fontSize="inherit" />
</IconButton>
</div>
<div className="flex flex-col overflow-hidden overflow-y-auto">
<List className="mb-14 px-0 pb-4 text-white lg:px-4 lg:mt-1">
{menuEntries.map((menuEntry, idx) => { {menuEntries.map((menuEntry, idx) => {
return ( return (
<ListItem key={idx}> <ListItem
key={idx}
disablePadding
className="!overflow-hidden py-2"
>
<ListItemButton <ListItemButton
className="justify-center sm:justify-normal" className="justify-center lg:justify-normal"
LinkComponent={Link} LinkComponent={Link}
href={menuEntry.to} href={menuEntry.to}
> >
<ListItemIcon <ListItemIcon
color="inherit" color="inherit"
className="justify-center sm:justify-normal text-white" className="justify-center overflow-hidden text-white lg:justify-normal"
> >
{menuEntry.icon} {menuEntry.icon}
</ListItemIcon> </ListItemIcon>
@@ -94,24 +123,24 @@ export function Sidebar() {
primaryTypographyProps={{ primaryTypographyProps={{
color: "inherit", color: "inherit",
}} }}
className="hidden sm:block" className="hidden lg:block"
/> />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
); );
})} })}
</List> </List>
<Divider flexItem className="bg-zinc-600 mx-8 my-10" />
<div className="hidden sm:block mx-auto mb-8 w-full max-w-60 rounded-sm py-6 px-4 text-center shadow-default align-bottom"> <Divider flexItem className="mx-8 my-10 bg-zinc-600 hidden lg:block" />
<div className="max-w-60 shadow-default mx-auto mb-8 hidden w-full rounded-sm px-4 py-6 text-center align-bottom lg:block">
<h3 className="mb-1 w-full font-semibold text-white"> <h3 className="mb-1 w-full font-semibold text-white">
Clan.lol Admin Clan.lol Admin
</h3> </h3>
<a <a
href="" href=""
target="_blank" target="_blank"
rel="nofollow" rel="nofollow"
className="w-full text-center rounded-md bg-primary p-2 text-white hover:bg-opacity-95" className="bg-primary w-full rounded-md p-2 text-center text-white hover:bg-opacity-95"
> >
Donate Donate
</a> </a>