add: responsive layout for sidebar and dashboard
All checks were successful
build / test (pull_request) Successful in 27s

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`
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 localFont from "next/font/local";
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 { StyledEngineProvider } from "@mui/material/styles";
import { darkTheme, lightTheme } from "./theme/themes";
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({
src: [
@ -26,12 +34,18 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)");
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) => {
setUseDarkTheme(currentValue);
setTheme(currentValue ? darkTheme : lightTheme);
};
return (
@ -43,13 +57,42 @@ export default function RootLayout({
<link rel="icon" href="/favicon.ico" />
</head>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<ThemeProvider theme={useDarkTheme ? darkTheme : lightTheme}>
<body id="__next" className={roboto.className}>
<CssBaseline />
<div className="flex h-screen overflow-hidden">
<Sidebar />
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<main>{children}</main>
<Sidebar
show={showSidebar}
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>
</body>

View File

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

View File

@ -1,45 +1,50 @@
import React, { PureComponent } from 'react';
import { PieChart, Pie, Sector, Cell, ResponsiveContainer, Legend } from 'recharts';
import { useTheme } from '@mui/material/styles';
import { Box, Color } from '@mui/material';
import React, { PureComponent } from "react";
import {
PieChart,
Pie,
Sector,
Cell,
ResponsiveContainer,
Legend,
} from "recharts";
import { useTheme } from "@mui/material/styles";
import { Box, Color } from "@mui/material";
export interface PieData {
name: string;
value: number;
color: string;
};
name: string;
value: number;
color: string;
}
interface Props {
data: PieData[];
showLabels?: boolean;
};
data: PieData[];
showLabels?: boolean;
}
export default function NodePieChart(props: Props ) {
const theme = useTheme();
const {data, showLabels} = props;
export default 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}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Legend verticalAlign="bottom" />
</PieChart>
</ResponsiveContainer>
</Box>
);
};
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}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Legend verticalAlign="bottom" />
</PieChart>
</ResponsiveContainer>
</Box>
);
}

View File

@ -1,4 +1,4 @@
"use client"
"use client";
import { StrictMode } from "react";
import NodeList, { NodeStatus, TableData } from "./NodeList";
@ -6,42 +6,98 @@ import NodeList, { NodeStatus, TableData } from "./NodeList";
import Box from "@mui/material/Box";
function createData(
name: string,
id: string,
status: NodeStatus,
last_seen: number,
name: string,
id: string,
status: NodeStatus,
last_seen: number,
): TableData {
return {
name,
id,
status,
last_seen: last_seen,
};
return {
name,
id,
status,
last_seen: last_seen,
};
}
const tableData = [
createData('Matchbox', "42:0:f21:6916:e333:c47e:4b5c:e74c", NodeStatus.Pending, 0),
createData('Ahorn', "42:0:3c46:b51c:b34d:b7e1:3b02:8d24", 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),
createData(
"Matchbox",
"42:0:f21:6916:e333:c47e:4b5c:e74c",
NodeStatus.Pending,
0,
),
createData(
"Ahorn",
"42:0:3c46:b51c:b34d:b7e1:3b02:8d24",
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() {
return (
<Box sx={{ backgroundColor: "#e9ecf5", height: "100%", width: "100%" }} display="inline-block" id="rootBox">
<NodeList tableData={tableData} />
</Box>
);
}
return (
<Box
sx={{ backgroundColor: "#e9ecf5", height: "100%", width: "100%" }}
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() {
return (
<div className="w-full flex justify-center items-center h-screen">
<div className="grid">
Welcome to the Dashboard
<Button variant="contained" color="primary">
LOL
</Button>
<div className="flex h-screen w-full">
<div className="grid w-full grid-cols-3 gap-4">
<DashboardCard>Current CLAN Overview</DashboardCard>
<DashboardCard>Recent Activity Log</DashboardCard>
<SplitDashboardCard>
<div>Notifications</div>
<div>Quick Action</div>
</SplitDashboardCard>
<DashboardPanel>Panel</DashboardPanel>
<DashboardCard>Side Bar (misc)</DashboardCard>
</div>
</div>
);

View File

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

View File

@ -1,5 +1,7 @@
import {
Divider,
Icon,
IconButton,
List,
ListItem,
ListItemButton,
@ -7,7 +9,7 @@ import {
ListItemText,
} from "@mui/material";
import Image from "next/image";
import { ReactNode } from "react";
import { ReactNode, useState } from "react";
import DashboardIcon from "@mui/icons-material/Dashboard";
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 BackupIcon from "@mui/icons-material/Backup";
import Link from "next/link";
import { tw } from "@/utils/tailwind";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
type MenuEntry = {
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 (
<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">
<div className="flex items-center justify-between gap-2 px-6 py-5.5 lg:py-6.5">
<div className="mt-8 font-semibold text-white w-full text-center hidden sm:block">
<aside
className={tw`${
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
src="/logo.svg"
alt="Clan Logo"
@ -72,20 +89,32 @@ export function Sidebar() {
/>
</div>
</div>
<Divider flexItem className="bg-zinc-600 my-9 mx-8" />
<div className="overflow-hidden flex flex-col overflow-y-auto duration-200 ease-linear">
<List className="pb-4 mb-14 px-4 lg:mt-1 lg:px-6 text-white">
<Divider
flexItem
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) => {
return (
<ListItem key={idx}>
<ListItem
key={idx}
disablePadding
className="!overflow-hidden py-2"
>
<ListItemButton
className="justify-center sm:justify-normal"
className="justify-center lg:justify-normal"
LinkComponent={Link}
href={menuEntry.to}
>
<ListItemIcon
color="inherit"
className="justify-center sm:justify-normal text-white"
className="justify-center overflow-hidden text-white lg:justify-normal"
>
{menuEntry.icon}
</ListItemIcon>
@ -94,24 +123,24 @@ export function Sidebar() {
primaryTypographyProps={{
color: "inherit",
}}
className="hidden sm:block"
className="hidden lg:block"
/>
</ListItemButton>
</ListItem>
);
})}
</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">
Clan.lol Admin
</h3>
<a
href=""
target="_blank"
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
</a>