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: {
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,7 +3,6 @@ import { Sidebar } from "@/components/sidebar";
import { tw } from "@/utils/tailwind";
import MenuIcon from "@mui/icons-material/Menu";
import {
Button,
CssBaseline,
IconButton,
MenuItem,
@ -71,7 +70,7 @@ 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}
@ -133,21 +132,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 =
| {
rawData: AxiosResponse<MachinesResponse, any> | undefined;
data: Machine[];
isLoading: boolean;
flakeName: string;
error: AxiosError<any> | undefined;
isValidating: boolean;
export type MachineFilter = PartialRecord<
keyof Machine,
Machine[keyof Machine]
>;
filters: Filters;
setFilters: Dispatch<SetStateAction<Filters>>;
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
swrKey: string | false | Record<any, any>;
}
| {
isLoading: true;
data: readonly [];
};
type MachineContextType = {
rawData: AxiosResponse<MachinesResponse, any> | undefined;
data: Machine[];
isLoading: boolean;
error: AxiosError<any> | undefined;
isValidating: boolean;
const initialState = {
isLoading: true,
data: [],
} as const;
filters: MachineFilter;
setFilters: Dispatch<SetStateAction<MachineFilter>>;
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
swrKey: string | false | Record<any, any>;
};
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">
<Image
src="/logo.png"
alt="Clan Logo"
width={75}
height={75}
priority
/>
<div className="flex flex-col py-6 mt-8">
<div className="hidden w-full max-w-xs text-center shadow-sm lg:block">
<h3 className="m-0 pb-2 w-full 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="/clan-white.png"
alt="Clan Logo"
width={102}
height={75}
priority
/>
</div>
</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}>
<SearchBar
tableData={tableData}
setFilteredList={setFilteredList}
/>
</Grid2>
</EnhancedTableToolbar>
<Paper sx={{ width: "100%", mb: 2, p: { xs: 0, lg: 2 } }} elevation={0}>
<SearchBar
allData={rawData?.data.machines || []}
setQuery={setFilters}
/>
<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,