forked from clan/clan-core
start machine list cleanup
This commit is contained in:
parent
778349b72d
commit
808bd3defd
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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"
|
||||||
|
@ -34,7 +34,7 @@ interface AppContextProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
const mock = {
|
const mock = {
|
||||||
data: { flakes: [] },
|
data: { flakes: ["example_clan"] },
|
||||||
};
|
};
|
||||||
|
|
||||||
// list_clans
|
// list_clans
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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}
|
||||||
|
@ -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}>
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user