1
0
forked from clan/clan-core

start machine list cleanup

This commit is contained in:
Johannes Kirschbauer 2023-11-17 16:09:30 +01:00
parent 778349b72d
commit 808bd3defd
Signed by: hsjobeki
SSH Key Fingerprint: SHA256:vX3utDqig7Ph5L0JPv87ZTPb/w7cMzREKVZzzLFg9qU
16 changed files with 1264 additions and 1107 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: { baseColors: {
neutral: { neutral: {
keyColor: "#92898a", keyColor: "#808080",
tones: [2, 5, 8, 92, 95, 98], tones: [2, 5, 8, 92, 95, 98],
}, },
green: { green: {
@ -43,7 +43,7 @@ export const config: PaletteConfig = {
}, },
blue: { blue: {
keyColor: "#1B7AC5", keyColor: "#1B7AC5",
tones: [5, 95], tones: [1, 2, 3, 5, 95, 98],
}, },
}, },

View File

@ -3,7 +3,6 @@ import { Sidebar } from "@/components/sidebar";
import { tw } from "@/utils/tailwind"; import { tw } from "@/utils/tailwind";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import { import {
Button,
CssBaseline, CssBaseline,
IconButton, IconButton,
MenuItem, MenuItem,
@ -71,7 +70,7 @@ export default function RootLayout({
return ( return (
<> <>
<Background /> <Background />
<div className="flex h-screen overflow-hidden"> <div className="flex h-screen overflow-hidden bg-neutral-95">
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
<Sidebar <Sidebar
show={showSidebarDerived} show={showSidebarDerived}
@ -133,21 +132,7 @@ export default function RootLayout({
<div className="px-1"> <div className="px-1">
<div className="relative flex h-full flex-1 flex-col"> <div className="relative flex h-full flex-1 flex-col">
<main> <main>{children}</main>
<Button
fullWidth
onClick={() => {
appState.setAppState((s) => ({
...s,
isJoined: !s.isJoined,
}));
}}
>
Toggle Joined
</Button>
{children}
</main>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -8,13 +8,9 @@ interface DashboardCardProps {
const DashboardCard = (props: DashboardCardProps) => { const DashboardCard = (props: DashboardCardProps) => {
const { children, title } = props; const { children, title } = props;
return ( return (
<div <div className="h-full w-full bg-white dark:bg-neutral-5">
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 px-3 py-2"> <div className="h-full w-full px-3 py-2">
<Typography variant="h6" color={"secondary"}> <Typography variant="h6" color={"primary"}>
{title} {title}
</Typography> </Typography>
{children} {children}

View File

@ -13,7 +13,7 @@ const AppCard = (props: AppCardProps) => {
<div <div
role="button" role="button"
className="flex h-40 w-40 cursor-pointer items-center justify-center rounded-3xl p-2 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 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" 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 }) => ( {actions.map(({ id, icon, label, eventHandler }) => (
<Fab <Fab
className="w-fit self-center shadow-none" className="w-fit self-center shadow-none"
color="secondary" color="primary"
key={id} key={id}
onClick={eventHandler} onClick={eventHandler}
variant="extended" variant="extended"

View File

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

View File

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

View File

@ -13,13 +13,10 @@ import { ReactNode } from "react";
import { tw } from "@/utils/tailwind"; import { tw } from "@/utils/tailwind";
import AppsIcon from "@mui/icons-material/Apps"; import AppsIcon from "@mui/icons-material/Apps";
import BackupIcon from "@mui/icons-material/Backup"; 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 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 = { type MenuEntry = {
icon: ReactNode; icon: ReactNode;
@ -49,24 +46,24 @@ const menuEntries: MenuEntry[] = [
to: "/applications", to: "/applications",
disabled: true, disabled: true,
}, },
{
icon: <LanIcon />,
label: "Network",
to: "/network",
disabled: true,
},
{
icon: <DesignServicesIcon />,
label: "Templates",
to: "/templates",
disabled: false,
},
{ {
icon: <BackupIcon />, icon: <BackupIcon />,
label: "Backups", label: "Backups",
to: "/backups", to: "/backups",
disabled: true, 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`; const hideSidebar = tw`-translate-x-14 lg:-translate-x-64`;
@ -75,38 +72,35 @@ const showSidebar = tw`lg:translate-x-0`;
interface SidebarProps { interface SidebarProps {
show: boolean; show: boolean;
onClose: () => void; onClose: () => void;
clanSelect: React.ReactNode;
} }
export function Sidebar(props: SidebarProps) { export function Sidebar(props: SidebarProps) {
const { show, onClose, clanSelect } = props; const { show, onClose } = props;
return ( return (
<aside <aside
className={tw`${ className={tw`${
show ? showSidebar : hideSidebar 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="flex flex-col py-6 mt-8">
<div className="mt-8 hidden w-full text-center font-semibold text-white lg:block"> <div className="hidden w-full max-w-xs text-center shadow-sm lg:block">
<Image <h3 className="m-0 pb-2 w-full font-semibold text-white">
src="/logo.png" Clan Dashboard
alt="Clan Logo" </h3>
width={75} </div>
height={75} <div className="flex items-center overflow-hidden">
priority <div className="hidden w-full text-center font-semibold text-white lg:block">
/> <Image
src="/clan-white.png"
alt="Clan Logo"
width={102}
height={75}
priority
/>
</div>
</div> </div>
</div> </div>
<div className="self-center">{clanSelect}</div> <Divider flexItem className="mx-8 my-4 hidden bg-blue-40 lg:block" />
<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>
<div className="flex flex-col overflow-hidden overflow-y-auto"> <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"> <List className="mb-14 px-0 pb-4 text-white lg:mt-1 lg:px-4">
{menuEntries.map((menuEntry, idx) => { {menuEntries.map((menuEntry, idx) => {
@ -140,22 +134,11 @@ export function Sidebar(props: SidebarProps) {
); );
})} })}
</List> </List>
<Divider <Divider flexItem className="mx-8 my-4 hidden bg-blue-40 lg:block" />
flexItem <div className="flex w-full justify-center py-2">
className="mx-8 my-10 hidden bg-neutral-40 lg:block" <IconButton size="large" className="text-white" onClick={onClose}>
/> <ChevronLeftIcon fontSize="inherit" />
<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"> </IconButton>
<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>
</div> </div>
</div> </div>
</aside> </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"; "use client";
import { CircularProgress, Grid, useTheme } from "@mui/material"; import { CircularProgress, Grid } from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import TablePagination from "@mui/material/TablePagination"; import TablePagination from "@mui/material/TablePagination";
import useMediaQuery from "@mui/material/useMediaQuery"; import { ChangeEvent, useState } from "react";
import { ChangeEvent, useMemo, useState } from "react";
import { Machine } from "@/api/model/machine";
import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
import { useMachines } from "../hooks/useMachines"; import { useMachines } from "../hooks/useMachines";
import { EnhancedTableToolbar } from "./enhancedTableToolbar";
import { NodeTableContainer } from "./nodeTableContainer"; import { NodeTableContainer } from "./nodeTableContainer";
import { SearchBar } from "./searchBar"; import { SearchBar } from "./searchBar";
import { StickySpeedDial } from "./stickySpeedDial";
export function NodeTable() { export function NodeTable() {
const { isLoading, data: machines } = useMachines(); const { isLoading, data: machines, rawData, setFilters } = useMachines();
const theme = useTheme();
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
const [selected, setSelected] = useState<string | undefined>(undefined); const [selected, setSelected] = useState<string | undefined>(undefined);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5); 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) => { const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage); setPage(newPage);
@ -60,19 +43,13 @@ export function NodeTable() {
return ( return (
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
<Paper sx={{ width: "100%", mb: 2 }}> <Paper sx={{ width: "100%", mb: 2, p: { xs: 0, lg: 2 } }} elevation={0}>
<StickySpeedDial selected={selected} /> <SearchBar
<EnhancedTableToolbar tableData={tableData}> allData={rawData?.data.machines || []}
<Grid2 xs={12}> setQuery={setFilters}
<SearchBar />
tableData={tableData}
setFilteredList={setFilteredList}
/>
</Grid2>
</EnhancedTableToolbar>
<NodeTableContainer <NodeTableContainer
tableData={filteredList} tableData={machines}
page={page} page={page}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
dense={false} dense={false}
@ -80,12 +57,10 @@ export function NodeTable() {
setSelected={setSelected} setSelected={setSelected}
/> />
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
<TablePagination <TablePagination
rowsPerPageOptions={[5, 10, 25]} rowsPerPageOptions={[5, 10, 25]}
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
component="div" component="div"
count={filteredList.length} count={machines.length}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
page={page} page={page}
onPageChange={handleChangePage} onPageChange={handleChangePage}

View File

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

View File

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