add: responsive layout for sidebar and dashboard
All checks were successful
build / test (pull_request) Successful in 27s
All checks were successful
build / test (pull_request) Successful in 27s
This commit is contained in:
parent
a243f97574
commit
ff89bcba4b
@ -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
4
pkgs/ui/pconf.cjs
Normal file
@ -0,0 +1,4 @@
|
||||
// prettier.config.js
|
||||
module.exports = {
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
|
||||
export const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
palette: {
|
||||
mode: "light",
|
||||
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user