Merge pull request 'start machine list cleanup' (#532) from hsjobeki-main into main

This commit is contained in:
clan-bot
2023-11-18 08:38:18 +00:00
16 changed files with 1264 additions and 1133 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ export const config: PaletteConfig = {
*/
baseColors: {
neutral: {
keyColor: "#92898a",
keyColor: "#808080",
tones: [2, 5, 8, 92, 95, 98],
},
green: {
@@ -43,7 +43,7 @@ export const config: PaletteConfig = {
},
blue: {
keyColor: "#1B7AC5",
tones: [5, 95],
tones: [1, 2, 3, 5, 95, 98],
},
},

View File

@@ -3,11 +3,8 @@ import { Sidebar } from "@/components/sidebar";
import { tw } from "@/utils/tailwind";
import MenuIcon from "@mui/icons-material/Menu";
import {
Button,
CssBaseline,
IconButton,
MenuItem,
Select,
ThemeProvider,
useMediaQuery,
} from "@mui/material";
@@ -71,35 +68,11 @@ export default function RootLayout({
return (
<>
<Background />
<div className="flex h-screen overflow-hidden">
<div className="flex h-screen overflow-hidden bg-neutral-95">
<ThemeProvider theme={darkTheme}>
<Sidebar
show={showSidebarDerived}
onClose={() => setShowSidebar(false)}
clanSelect={
appState.data.clanDir && (
<Select
color="secondary"
label="clan"
fullWidth
variant="standard"
disableUnderline
value={appState.data.clanDir}
onChange={(ev) => {
appState.setAppState((c) => ({
...c,
clanDir: ev.target.value,
}));
}}
>
{appState.data.flakes?.map((clan) => (
<MenuItem value={clan} key={clan}>
{clan}
</MenuItem>
))}
</Select>
)
}
/>
</ThemeProvider>
<div
@@ -133,21 +106,7 @@ export default function RootLayout({
<div className="px-1">
<div className="relative flex h-full flex-1 flex-col">
<main>
<Button
fullWidth
onClick={() => {
appState.setAppState((s) => ({
...s,
isJoined: !s.isJoined,
}));
}}
>
Toggle Joined
</Button>
{children}
</main>
<main>{children}</main>
</div>
</div>
</div>

View File

@@ -10,7 +10,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<>
{!clanDir && <div>No clan selected</div>}
{clanDir && (
<MachineContextProvider flakeName={clanDir}>
<MachineContextProvider clanDir={clanDir}>
{children}
</MachineContextProvider>
)}

View File

@@ -21,19 +21,19 @@ const commonOptions: Partial<ThemeOptions> = {
const commonPalette: Partial<PaletteOptions> = {
primary: {
main: palette.green50.value,
main: palette.blue50.value,
},
secondary: {
main: palette.green50.value,
main: palette.green60.value,
},
info: {
main: palette.blue50.value,
},
success: {
main: palette.green50.value,
main: palette.green60.value,
},
warning: {
main: palette.yellow50.value,
main: palette.yellow80.value,
},
error: {
main: palette.red50.value,

View File

@@ -8,13 +8,9 @@ interface DashboardCardProps {
const DashboardCard = (props: DashboardCardProps) => {
const { children, title } = props;
return (
<div
className="h-full w-full
border border-solid border-neutral-80 bg-neutral-98
shadow-sm shadow-neutral-60 dark:border-none dark:bg-neutral-5 dark:shadow-none"
>
<div className="h-full w-full bg-white dark:bg-neutral-5">
<div className="h-full w-full px-3 py-2">
<Typography variant="h6" color={"secondary"}>
<Typography variant="h6" color={"primary"}>
{title}
</Typography>
{children}

View File

@@ -13,7 +13,7 @@ const AppCard = (props: AppCardProps) => {
<div
role="button"
className="flex h-40 w-40 cursor-pointer items-center justify-center rounded-3xl p-2
align-middle shadow-md ring-2 ring-inset ring-purple-50
align-middle shadow-md ring-2 ring-inset ring-blue-90
hover:bg-neutral-90 focus:bg-neutral-90 active:bg-neutral-80
dark:hover:bg-neutral-10 dark:focus:bg-neutral-10 dark:active:bg-neutral-20"
>

View File

@@ -48,7 +48,7 @@ export const QuickActions = () => {
{actions.map(({ id, icon, label, eventHandler }) => (
<Fab
className="w-fit self-center shadow-none"
color="secondary"
color="primary"
key={id}
onClick={eventHandler}
variant="extended"

View File

@@ -34,7 +34,7 @@ interface AppContextProviderProps {
children: ReactNode;
}
const mock = {
data: { flakes: [] },
data: { flakes: ["example_clan"] },
};
// list_clans

View File

@@ -2,62 +2,54 @@
import { useListMachines } from "@/api/machine/machine";
import { Machine, MachinesResponse } from "@/api/model";
import { clanErrorToast } from "@/error/errorToast";
import { AxiosError, AxiosResponse } from "axios";
import React, {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useEffect,
useMemo,
useState,
} from "react";
import { KeyedMutator } from "swr";
type Filter = {
name: keyof Machine;
value: Machine[keyof Machine];
type PartialRecord<K extends keyof any, T> = {
[P in K]?: T;
};
type Filters = Filter[];
type MachineContextType =
| {
export type MachineFilter = PartialRecord<
keyof Machine,
Machine[keyof Machine]
>;
type MachineContextType = {
rawData: AxiosResponse<MachinesResponse, any> | undefined;
data: Machine[];
isLoading: boolean;
flakeName: string;
error: AxiosError<any> | undefined;
isValidating: boolean;
filters: Filters;
setFilters: Dispatch<SetStateAction<Filters>>;
filters: MachineFilter;
setFilters: Dispatch<SetStateAction<MachineFilter>>;
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
swrKey: string | false | Record<any, any>;
}
| {
isLoading: true;
data: readonly [];
};
const initialState = {
isLoading: true,
data: [],
} as const;
};
export function CreateMachineContext() {
return createContext<MachineContextType>({
...initialState,
});
return createContext<MachineContextType>({} as MachineContextType);
}
interface MachineContextProviderProps {
children: ReactNode;
flakeName: string;
clanDir: string;
}
const MachineContext = CreateMachineContext();
export const MachineContextProvider = (props: MachineContextProviderProps) => {
const { children, flakeName } = props;
const { children, clanDir } = props;
const {
data: rawData,
isLoading,
@@ -65,18 +57,27 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => {
isValidating,
mutate,
swrKey,
} = useListMachines({ flake_dir: flakeName });
const [filters, setFilters] = useState<Filters>([]);
} = useListMachines({ flake_dir: clanDir });
const [filters, setFilters] = useState<MachineFilter>({});
useEffect(() => {
if (error) {
clanErrorToast(error);
}
}, [error]);
const data = useMemo(() => {
if (!isLoading && !error && !isValidating && rawData) {
if (!isLoading && rawData) {
const { machines } = rawData.data;
return machines.filter((m) =>
filters.every((f) => m[f.name] === f.value),
return machines.filter(
(m) =>
!filters.name ||
m.name.toLowerCase().includes(filters.name.toLowerCase()),
);
}
return [];
}, [isLoading, error, isValidating, rawData, filters]);
}, [isLoading, filters, rawData]);
return (
<MachineContext.Provider
@@ -85,7 +86,7 @@ export const MachineContextProvider = (props: MachineContextProviderProps) => {
data,
isLoading,
flakeName,
error,
isValidating,

View File

@@ -13,13 +13,10 @@ import { ReactNode } from "react";
import { tw } from "@/utils/tailwind";
import AppsIcon from "@mui/icons-material/Apps";
import BackupIcon from "@mui/icons-material/Backup";
import DashboardIcon from "@mui/icons-material/Dashboard";
import DesignServicesIcon from "@mui/icons-material/DesignServices";
import DevicesIcon from "@mui/icons-material/Devices";
import LanIcon from "@mui/icons-material/Lan";
import Link from "next/link";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import DashboardIcon from "@mui/icons-material/Dashboard";
import DevicesIcon from "@mui/icons-material/Devices";
import Link from "next/link";
type MenuEntry = {
icon: ReactNode;
@@ -49,24 +46,24 @@ const menuEntries: MenuEntry[] = [
to: "/applications",
disabled: true,
},
{
icon: <LanIcon />,
label: "Network",
to: "/network",
disabled: true,
},
{
icon: <DesignServicesIcon />,
label: "Templates",
to: "/templates",
disabled: false,
},
{
icon: <BackupIcon />,
label: "Backups",
to: "/backups",
disabled: true,
},
// {
// icon: <LanIcon />,
// label: "Network",
// to: "/network",
// disabled: true,
// },
// {
// icon: <DesignServicesIcon />,
// label: "Templates",
// to: "/templates",
// disabled: false,
// },
];
const hideSidebar = tw`-translate-x-14 lg:-translate-x-64`;
@@ -75,38 +72,35 @@ const showSidebar = tw`lg:translate-x-0`;
interface SidebarProps {
show: boolean;
onClose: () => void;
clanSelect: React.ReactNode;
}
export function Sidebar(props: SidebarProps) {
const { show, onClose, clanSelect } = props;
const { show, onClose } = props;
return (
<aside
className={tw`${
show ? showSidebar : hideSidebar
} z-9999 static left-0 top-0 flex h-screen w-14 flex-col overflow-x-hidden overflow-y-hidden bg-neutral-10 transition duration-150 ease-in-out dark:bg-neutral-2 lg:w-64`}
} z-9999 static left-0 top-0 flex h-screen w-14 flex-col overflow-x-hidden overflow-y-hidden bg-blue-3 transition duration-150 ease-in-out lg:w-64`}
>
<div className="flex items-center justify-between gap-2 overflow-hidden px-0 py-5 lg:p-6">
<div className="mt-8 hidden w-full text-center font-semibold text-white lg:block">
<div className="mt-8 flex flex-col py-6">
<div className="hidden w-full max-w-xs text-center shadow-sm lg:block">
<h3 className="m-0 w-full pb-2 font-semibold text-white">
Clan Dashboard
</h3>
</div>
<div className="flex items-center overflow-hidden">
<div className="hidden w-full text-center font-semibold text-white lg:block">
<Image
src="/logo.png"
src="/clan-white.png"
alt="Clan Logo"
width={75}
width={102}
height={75}
priority
/>
</div>
</div>
<div className="self-center">{clanSelect}</div>
<Divider
flexItem
className="mx-8 mb-4 mt-9 hidden bg-neutral-40 lg:block"
/>
<div className="flex w-full justify-center">
<IconButton size="large" className="text-white" onClick={onClose}>
<ChevronLeftIcon fontSize="inherit" />
</IconButton>
</div>
<Divider flexItem className="mx-8 my-4 hidden bg-blue-40 lg:block" />
<div className="flex flex-col overflow-hidden overflow-y-auto">
<List className="mb-14 px-0 pb-4 text-white lg:mt-1 lg:px-4">
{menuEntries.map((menuEntry, idx) => {
@@ -140,22 +134,11 @@ export function Sidebar(props: SidebarProps) {
);
})}
</List>
<Divider
flexItem
className="mx-8 my-10 hidden bg-neutral-40 lg:block"
/>
<div className="mx-auto mb-8 hidden w-full max-w-xs rounded-sm px-4 py-6 text-center align-bottom shadow-sm lg:block">
<h3 className="mb-2 w-full font-semibold text-white">
Clan.lol Admin
</h3>
<a
href=""
target="_blank"
rel="nofollow"
className="inline-block w-full rounded-md p-2 text-center text-white hover:text-purple-60/95"
>
Donate
</a>
<Divider flexItem className="mx-8 my-4 hidden bg-blue-40 lg:block" />
<div className="flex w-full justify-center py-2">
<IconButton size="large" className="text-white" onClick={onClose}>
<ChevronLeftIcon fontSize="inherit" />
</IconButton>
</div>
</div>
</aside>

View File

@@ -1,82 +0,0 @@
"use client";
import React, { useMemo } from "react";
import Box from "@mui/material/Box";
import Grid2 from "@mui/material/Unstable_Grid2";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material";
import { PieCards } from "./pieCards";
import { PieData, NodePieChart } from "./nodePieChart";
import { Machine } from "@/api/model/machine";
import { Status } from "@/api/model";
interface EnhancedTableToolbarProps {
tableData: readonly Machine[];
}
export function EnhancedTableToolbar(
props: React.PropsWithChildren<EnhancedTableToolbarProps>,
) {
const { tableData } = props;
const theme = useTheme();
const is_lg = useMediaQuery(theme.breakpoints.down("lg"));
const pieData: PieData[] = useMemo(() => {
const online = tableData.filter(
(row) => row.status === Status.online,
).length;
const offline = tableData.filter(
(row) => row.status === Status.offline,
).length;
const pending = tableData.filter(
(row) => row.status === Status.unknown,
).length;
return [
{ name: "Online", value: online, color: theme.palette.success.main },
{ name: "Offline", value: offline, color: theme.palette.error.main },
{ name: "Pending", value: pending, color: theme.palette.warning.main },
];
}, [tableData, theme]);
return (
<Grid2 container spacing={1}>
{/* Pie Chart Grid */}
<Grid2
key="PieChart"
md={6}
xs={12}
display="flex"
justifyContent="center"
alignItems="center"
>
<Box height={350} width={400}>
<NodePieChart data={pieData} showLabels={is_lg} />
</Box>
</Grid2>
{/* Card Stack Grid */}
<Grid2
key="CardStack"
lg={6}
display="flex"
sx={{ display: { lg: "flex", xs: "none", md: "flex" } }}
>
<PieCards pieData={pieData} />
</Grid2>
{/*Toolbar Grid */}
<Grid2
key="Toolbar"
xs={12}
container
justifyContent="center"
alignItems="center"
sx={{ pl: { sm: 2 }, pr: { xs: 1, sm: 1 }, pt: { xs: 1, sm: 3 } }}
>
{props.children}
</Grid2>
</Grid2>
);
}

View File

@@ -1,38 +1,21 @@
"use client";
import { CircularProgress, Grid, useTheme } from "@mui/material";
import { CircularProgress, Grid } from "@mui/material";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import TablePagination from "@mui/material/TablePagination";
import useMediaQuery from "@mui/material/useMediaQuery";
import { ChangeEvent, useMemo, useState } from "react";
import { ChangeEvent, useState } from "react";
import { Machine } from "@/api/model/machine";
import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
import { useMachines } from "../hooks/useMachines";
import { EnhancedTableToolbar } from "./enhancedTableToolbar";
import { NodeTableContainer } from "./nodeTableContainer";
import { SearchBar } from "./searchBar";
import { StickySpeedDial } from "./stickySpeedDial";
export function NodeTable() {
const { isLoading, data: machines } = useMachines();
const theme = useTheme();
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
const { isLoading, data: machines, rawData, setFilters } = useMachines();
const [selected, setSelected] = useState<string | undefined>(undefined);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [filteredList, setFilteredList] = useState<readonly Machine[]>([]);
const tableData = useMemo(() => {
const tableData = machines.map((machine) => {
return { name: machine.name, status: machine.status };
});
setFilteredList(tableData);
return tableData;
}, [machines]);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
@@ -60,19 +43,13 @@ export function NodeTable() {
return (
<Box sx={{ width: "100%" }}>
<Paper sx={{ width: "100%", mb: 2 }}>
<StickySpeedDial selected={selected} />
<EnhancedTableToolbar tableData={tableData}>
<Grid2 xs={12}>
<Paper sx={{ width: "100%", mb: 2, p: { xs: 0, lg: 2 } }} elevation={0}>
<SearchBar
tableData={tableData}
setFilteredList={setFilteredList}
allData={rawData?.data.machines || []}
setQuery={setFilters}
/>
</Grid2>
</EnhancedTableToolbar>
<NodeTableContainer
tableData={filteredList}
tableData={machines}
page={page}
rowsPerPage={rowsPerPage}
dense={false}
@@ -80,12 +57,10 @@ export function NodeTable() {
setSelected={setSelected}
/>
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
component="div"
count={filteredList.length}
count={machines.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}

View File

@@ -6,14 +6,15 @@ import { Autocomplete, InputAdornment, TextField } from "@mui/material";
import IconButton from "@mui/material/IconButton";
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { useDebounce } from "../hooks/useDebounce";
import { MachineFilter } from "../hooks/useMachines";
export interface SearchBarProps {
tableData: readonly Machine[];
setFilteredList: Dispatch<SetStateAction<readonly Machine[]>>;
allData: Machine[];
setQuery: Dispatch<SetStateAction<MachineFilter>>;
}
export function SearchBar(props: SearchBarProps) {
const { tableData, setFilteredList } = props;
const { allData, setQuery } = props;
const [search, setSearch] = useState<string>("");
const debouncedSearch = useDebounce(search, 250);
const [open, setOpen] = useState(false);
@@ -22,7 +23,6 @@ export function SearchBar(props: SearchBarProps) {
function handleEsc(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === "Escape") {
setSearch("");
setFilteredList(tableData);
}
// check if the key is Enter
@@ -32,32 +32,21 @@ export function SearchBar(props: SearchBarProps) {
}
useEffect(() => {
if (debouncedSearch) {
const filtered: Machine[] = tableData.filter((row) => {
return row.name.toLowerCase().includes(debouncedSearch.toLowerCase());
});
setFilteredList(filtered);
}
}, [debouncedSearch, tableData, setFilteredList]);
setQuery((filters) => ({ ...filters, name: debouncedSearch }));
}, [debouncedSearch, setQuery]);
const handleInputChange = (event: any, value: string) => {
if (value === "") {
setFilteredList(tableData);
}
console.log({ value });
setSearch(value);
};
const suggestions = useMemo(
() => tableData.map((row) => row.name),
[tableData],
);
const options = useMemo(() => allData.map((row) => row.name), [allData]);
return (
<Autocomplete
freeSolo
autoComplete
options={suggestions}
options={options}
renderOption={(props: any, option: any) => {
return (
<li {...props} key={option}>

View File

@@ -36,6 +36,7 @@ module.exports = {
white: common.white.value,
black: common.black.value,
neutral: getTailwindColors(palette)("neutral"),
blue: getTailwindColors(palette)("blue"),
purple: {
...getTailwindColors(palette)("purple"),
DEFAULT: palette.purple50.value,