Befor fixing linting problem

This commit is contained in:
2023-10-22 21:03:06 +02:00
parent 545d389df0
commit c7c47b6527
87 changed files with 703 additions and 3929 deletions

View File

@@ -6,7 +6,7 @@ import {
import { useAppState } from "./hooks/useAppContext";
export default function Background() {
const { data, isLoading } = useAppState();
const { isLoading } = useAppState();
return (
<div
@@ -14,7 +14,7 @@ export default function Background() {
"fixed -z-10 h-[100vh] w-[100vw] overflow-hidden opacity-10 blur-md dark:opacity-40"
}
>
{(isLoading || !data.isJoined) && (
{isLoading && (
<>
<Image
className="dark:hidden"

View File

@@ -1,176 +0,0 @@
"use client";
import { useGetMachineSchema } from "@/api/default/default";
import { Check, Error } from "@mui/icons-material";
import {
Box,
Button,
LinearProgress,
List,
ListItem,
ListItemIcon,
ListItemText,
Paper,
Typography,
} from "@mui/material";
import { IChangeEvent } from "@rjsf/core";
import { Form } from "@rjsf/mui";
import {
ErrorListProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
TranslatableString,
} from "@rjsf/utils";
import validator from "@rjsf/validator-ajv8";
import { JSONSchema7 } from "json-schema";
import { useMemo, useRef } from "react";
import toast from "react-hot-toast";
import { FormStepContentProps } from "./interfaces";
interface PureCustomConfigProps extends FormStepContentProps {
schema: JSONSchema7;
initialValues: any;
}
export function CustomConfig(props: FormStepContentProps) {
const { formHooks } = props;
const { data, isLoading, error } = useGetMachineSchema("mama");
// const { data, isLoading, error } = { data: {data:{schema: {
// title: 'Test form',
// type: 'object',
// properties: {
// name: {
// type: 'string',
// },
// age: {
// type: 'number',
// },
// },
// }}}, isLoading: false, error: undefined }
const schema = useMemo(() => {
if (!isLoading && !error?.message && data?.data) {
return data?.data.schema;
}
return {};
}, [data, isLoading, error]);
const initialValues = useMemo(
() =>
Object.entries(schema?.properties || {}).reduce((acc, [key, value]) => {
/*@ts-ignore*/
const init: any = value?.default;
if (init) {
return {
...acc,
[key]: init,
};
}
return acc;
}, {}),
[schema],
);
return isLoading ? (
<LinearProgress variant="indeterminate" />
) : error?.message ? (
<div>{error?.message}</div>
) : (
<PureCustomConfig
formHooks={formHooks}
initialValues={initialValues}
schema={schema}
/>
);
}
function ErrorList<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>({ errors, registry }: ErrorListProps<T, S, F>) {
const { translateString } = registry;
return (
<Paper elevation={0}>
<Box mb={2} p={2}>
<Typography variant="h6">
{translateString(TranslatableString.ErrorsLabel)}
</Typography>
<List dense={true}>
{errors.map((error, i: number) => {
return (
<ListItem key={i}>
<ListItemIcon>
<Error color="error" />
</ListItemIcon>
<ListItemText primary={error.stack} />
</ListItem>
);
})}
</List>
</Box>
</Paper>
);
}
function PureCustomConfig(props: PureCustomConfigProps) {
const { schema, formHooks } = props;
const { setValue, watch } = formHooks;
console.log({ schema });
const configData = watch("config") as IChangeEvent<any>;
console.log({ configData });
const setConfig = (data: IChangeEvent<any>) => {
console.log({ data });
setValue("config", data);
};
const formRef = useRef<any>();
const validate = () => {
const isValid: boolean = formRef?.current?.validateForm();
console.log({ isValid }, formRef.current);
if (!isValid) {
formHooks.setError("config", {
message: "invalid config",
});
toast.error(
"Configuration is invalid. Please check the highlighted fields for details.",
);
} else {
formHooks.clearErrors("config");
toast.success("Config seems valid");
}
};
return (
<Form
ref={formRef}
onChange={setConfig}
formData={configData.formData}
acceptcharset="utf-8"
schema={schema}
validator={validator}
liveValidate={true}
templates={{
// ObjectFieldTemplate:
ErrorListTemplate: ErrorList,
ButtonTemplates: {
SubmitButton: (props) => (
<div className="flex w-full items-center justify-center">
<Button
onClick={validate}
startIcon={<Check />}
variant="outlined"
color="secondary"
>
Validate
</Button>
</div>
),
},
}}
/>
);
}

View File

@@ -1,155 +0,0 @@
import {
Box,
Button,
MobileStepper,
Step,
StepLabel,
Stepper,
useMediaQuery,
useTheme,
} from "@mui/material";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { CustomConfig } from "./customConfig";
import { CreateMachineForm, FormStep } from "./interfaces";
export function CreateMachineForm() {
const formHooks = useForm<CreateMachineForm>({
defaultValues: {
name: "",
config: {},
},
});
const { handleSubmit, reset } = formHooks;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [activeStep, setActiveStep] = useState<number>(0);
const steps: FormStep[] = [
{
id: "template",
label: "Template",
content: <div></div>,
},
{
id: "modules",
label: "Modules",
content: <div></div>,
},
{
id: "config",
label: "Customize",
content: <CustomConfig formHooks={formHooks} />,
},
{
id: "save",
label: "Save",
content: <div></div>,
},
];
const handleNext = () => {
if (activeStep < steps.length - 1) {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
}
};
const handleBack = () => {
if (activeStep > 0) {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
}
};
const handleReset = () => {
setActiveStep(0);
reset();
};
const currentStep = steps.at(activeStep);
async function onSubmit(data: any) {
console.log({ data }, "Aggregated Data; creating machine from");
}
const BackButton = () => (
<Button
color="secondary"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
);
const NextButton = () => (
<>
{activeStep !== steps.length - 1 && (
<Button
disabled={!formHooks.formState.isValid}
onClick={handleNext}
color="secondary"
>
{activeStep <= steps.length - 1 && "Next"}
</Button>
)}
{activeStep === steps.length - 1 && (
<Button color="secondary" onClick={handleReset}>
Reset
</Button>
)}
</>
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ width: "100%" }}>
{isMobile && (
<MobileStepper
activeStep={activeStep}
color="secondary"
backButton={<BackButton />}
nextButton={<NextButton />}
steps={steps.length}
/>
)}
{!isMobile && (
<Stepper activeStep={activeStep} color="secondary">
{steps.map(({ label }, index) => {
const stepProps: { completed?: boolean } = {};
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step
sx={{
".MuiStepIcon-root.Mui-active": {
color: "secondary.main",
},
".MuiStepIcon-root.Mui-completed": {
color: "secondary.main",
},
}}
key={label}
{...stepProps}
>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
)}
{/* <CustomConfig formHooks={formHooks} /> */}
{/* The step Content */}
{currentStep && currentStep.content}
{/* Desktop step controls */}
{!isMobile && (
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<BackButton />
<Box sx={{ flex: "1 1 auto" }} />
<NextButton />
</Box>
)}
</Box>
</form>
);
}

View File

@@ -1,23 +0,0 @@
import { ReactElement } from "react";
import { UseFormReturn } from "react-hook-form";
export type StepId = "template" | "modules" | "config" | "save";
export type CreateMachineForm = {
name: string;
config: any;
};
export type FormHooks = UseFormReturn<CreateMachineForm>;
export type FormStep = {
id: StepId;
label: string;
content: FormStepContent;
};
export interface FormStepContentProps {
formHooks: FormHooks;
}
export type FormStepContent = ReactElement<FormStepContentProps>;

View File

@@ -1,73 +0,0 @@
import { DashboardCard } from "@/components/card";
import { NoDataOverlay } from "@/components/noDataOverlay";
import { status, Status, clanStatus } from "@/data/dashboardData";
import {
Chip,
Divider,
List,
ListItem,
ListItemIcon,
ListItemText,
} from "@mui/material";
import Link from "next/link";
import React from "react";
const statusColorMap: Record<
Status,
"default" | "primary" | "secondary" | "error" | "info" | "success" | "warning"
> = {
online: "info",
offline: "error",
pending: "default",
};
const MAX_OTHERS = 5;
export const NetworkOverview = () => {
const { self, other } = clanStatus;
const firstOthers = other.slice(0, MAX_OTHERS);
return (
<DashboardCard title="Clan Overview">
<List>
<ListItem>
<ListItemText primary={self.name} secondary={self.status} />
<ListItemIcon>
<Chip
label={status[self.status]}
color={statusColorMap[self.status]}
/>
</ListItemIcon>
</ListItem>
<Divider flexItem />
{!other.length && (
<div className="my-3 flex h-full w-full justify-center align-middle">
<NoDataOverlay
label={
<ListItemText
primary="No other nodes"
secondary={<Link href="/nodes">Add devices</Link>}
/>
}
/>
</div>
)}
{firstOthers.map((o) => (
<ListItem key={o.id}>
<ListItemText primary={o.name} secondary={o.status} />
<ListItemIcon>
<Chip label={status[o.status]} color={statusColorMap[o.status]} />
</ListItemIcon>
</ListItem>
))}
{other.length > MAX_OTHERS && (
<ListItem>
<ListItemText
secondary={` ${other.length - MAX_OTHERS} more ...`}
/>
</ListItem>
)}
</List>
</DashboardCard>
);
};

View File

@@ -1,91 +0,0 @@
import { DashboardCard } from "@/components/card";
import Image from "next/image";
interface AppCardProps {
name: string;
icon?: string;
}
const AppCard = (props: AppCardProps) => {
const { name, icon } = props;
const iconPath = icon
? `/app-icons/${icon}`
: "app-icons/app-placeholder.svg";
return (
<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
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"
>
<div className="flex w-full flex-col justify-center">
<div className="my-1 flex h-[22] w-[22] items-center justify-center self-center overflow-visible p-1 dark:invert">
<Image
src={iconPath}
alt={`${name}-app-icon`}
width={18 * 3}
height={18 * 3}
/>
</div>
<div className="flex w-full justify-center">{name}</div>
</div>
</div>
);
};
const apps = [
{
name: "Firefox",
icon: "firefox.svg",
},
{
name: "Discord",
icon: "discord.svg",
},
{
name: "Docs",
},
{
name: "Dochub",
icon: "dochub.svg",
},
{
name: "Chess",
icon: "chess.svg",
},
{
name: "Games",
icon: "games.svg",
},
{
name: "Mail",
icon: "mail.svg",
},
{
name: "Public transport",
icon: "public-transport.svg",
},
{
name: "Outlook",
icon: "mail.svg",
},
{
name: "Youtube",
icon: "youtube.svg",
},
];
export const AppOverview = () => {
return (
<DashboardCard title="Applications">
<div className="flex h-full w-full justify-center">
<div className="flex h-full w-fit justify-center">
<div className="grid w-full auto-cols-min auto-rows-min grid-cols-2 gap-8 py-8 sm:grid-cols-3 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 ">
{apps.map((app) => (
<AppCard key={app.name} name={app.name} icon={app.icon} />
))}
</div>
</div>
</div>
</DashboardCard>
);
};

View File

@@ -1,68 +0,0 @@
import { DashboardCard } from "@/components/card";
import { notificationData } from "@/data/dashboardData";
import {
Avatar,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from "@mui/material";
import CheckIcon from "@mui/icons-material/Check";
import InfoIcon from "@mui/icons-material/Info";
import PriorityHighIcon from "@mui/icons-material/PriorityHigh";
import CloseIcon from "@mui/icons-material/Close";
const severityMap = {
info: {
icon: <InfoIcon />,
color: "info",
},
success: {
icon: <CheckIcon />,
color: "success",
},
warning: {
icon: <PriorityHighIcon />,
color: "warning",
},
error: {
icon: <CloseIcon />,
color: "error",
},
};
export const Notifications = () => {
return (
<DashboardCard title="Notifications">
<List>
{notificationData.map((n, idx) => (
<ListItem key={idx}>
<ListItemAvatar>
<Avatar
sx={{
bgcolor: `${n.severity}.main`,
}}
>
{severityMap[n.severity].icon}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={n.msg}
secondary={n.date}
sx={{
width: "100px",
}}
/>
<ListItemText
primary={n.source}
sx={{
width: "100px",
}}
/>
</ListItem>
))}
</List>
</DashboardCard>
);
};

View File

@@ -1,64 +0,0 @@
"use client";
import { DashboardCard } from "@/components/card";
import { Fab, Typography } from "@mui/material";
import { MouseEventHandler, ReactNode } from "react";
import AppsIcon from "@mui/icons-material/Apps";
import DevicesIcon from "@mui/icons-material/Devices";
import LanIcon from "@mui/icons-material/Lan";
type Action = {
id: string;
icon: ReactNode;
label: ReactNode;
eventHandler: MouseEventHandler<HTMLButtonElement>;
};
export const QuickActions = () => {
const actions: Action[] = [
{
id: "network",
icon: <LanIcon sx={{ mr: 1 }} />,
label: "Network",
eventHandler: (event) => {
console.log({ event });
},
},
{
id: "apps",
icon: <AppsIcon sx={{ mr: 1 }} />,
label: "Apps",
eventHandler: (event) => {
console.log({ event });
},
},
{
id: "nodes",
icon: <DevicesIcon sx={{ mr: 1 }} />,
label: "Devices",
eventHandler: (event) => {
console.log({ event });
},
},
];
return (
<DashboardCard title="Quick Actions">
<div className="flex h-full w-full items-center justify-start pb-10 align-bottom">
<div className="flex w-full flex-col flex-wrap justify-evenly gap-2 sm:flex-row">
{actions.map(({ id, icon, label, eventHandler }) => (
<Fab
className="w-fit self-center shadow-none"
color="secondary"
key={id}
onClick={eventHandler}
variant="extended"
>
{icon}
<Typography>{label}</Typography>
</Fab>
))}
</div>
</div>
</DashboardCard>
);
};

View File

@@ -1,56 +0,0 @@
import { DashboardCard } from "@/components/card";
import SyncIcon from "@mui/icons-material/Sync";
import ScheduleIcon from "@mui/icons-material/Schedule";
import DoneIcon from "@mui/icons-material/Done";
import { ReactNode } from "react";
import { Chip } from "@mui/material";
const statusMap = {
running: <SyncIcon className="animate-bounce" />,
done: <DoneIcon />,
planned: <ScheduleIcon />,
};
interface TaskEntryProps {
status: ReactNode;
result: "default" | "error" | "info" | "success" | "warning";
task: string;
details?: string;
}
const TaskEntry = (props: TaskEntryProps) => {
const { result, task, status } = props;
return (
<>
<div className="col-span-1">{status}</div>
<div className="col-span-4">{task}</div>
<div className="col-span-1">
<Chip color={result} label={result} />
</div>
</>
);
};
export const TaskQueue = () => {
return (
<DashboardCard title="Task Queue">
<div className="grid grid-cols-6 gap-2 p-4">
<TaskEntry
result="success"
task="Update DevX"
status={statusMap.done}
/>
<TaskEntry
result="default"
task="Update XYZ"
status={statusMap.running}
/>
<TaskEntry
result="default"
task="Update ABC"
status={statusMap.planned}
/>
</div>
</DashboardCard>
);
};

View File

@@ -1,21 +0,0 @@
import { Chip } from "@mui/material";
interface FlakeBadgeProps {
flakeUrl: string;
flakeAttr: string;
}
export const FlakeBadge = (props: FlakeBadgeProps) => (
<Chip
color="secondary"
label={`${props.flakeUrl}#${props.flakeAttr}`}
sx={{
p: 2,
"&.MuiChip-root": {
maxWidth: "unset",
},
"&.MuiChip-label": {
overflow: "unset",
},
}}
/>
);

View File

@@ -11,36 +11,28 @@ import React, {
import { KeyedMutator } from "swr";
type AppContextType = {
// data: AxiosResponse<{}, any> | undefined;
data: AppState;
isLoading: boolean;
error: AxiosError<any> | undefined;
setAppState: Dispatch<SetStateAction<AppState>>;
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
swrKey: string | false | Record<any, any>;
};
// const initialState = {
// isLoading: true,
// } as const;
export const AppContext = createContext<AppContextType>({} as AppContextType);
type AppState = {
isJoined?: boolean;
clanName?: string;
};
type AppState = {};
interface AppContextProviderProps {
children: ReactNode;
}
export const WithAppState = (props: AppContextProviderProps) => {
const { children } = props;
const { isLoading, error, mutate, swrKey } = useListMachines();
const [data, setAppState] = useState<AppState>({ isJoined: false });
const isLoading = false;
const error = undefined;
const [data, setAppState] = useState<AppState>({});
return (
<AppContext.Provider
@@ -49,8 +41,6 @@ export const WithAppState = (props: AppContextProviderProps) => {
setAppState,
isLoading,
error,
swrKey,
mutate,
}}
>
{children}

View File

@@ -1,95 +0,0 @@
"use client";
import { useListMachines } from "@/api/default/default";
import { Machine, MachinesResponse } from "@/api/model";
import { AxiosError, AxiosResponse } from "axios";
import React, {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useMemo,
useState,
} from "react";
import { KeyedMutator } from "swr";
type Filter = {
name: keyof Machine;
value: Machine[keyof Machine];
};
type Filters = Filter[];
type MachineContextType =
| {
rawData: AxiosResponse<MachinesResponse, any> | undefined;
data: Machine[];
isLoading: boolean;
error: AxiosError<any> | undefined;
isValidating: boolean;
filters: Filters;
setFilters: Dispatch<SetStateAction<Filters>>;
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
swrKey: string | false | Record<any, any>;
}
| {
isLoading: true;
data: readonly [];
};
const initialState = {
isLoading: true,
data: [],
} as const;
export const MachineContext = createContext<MachineContextType>(initialState);
interface MachineContextProviderProps {
children: ReactNode;
}
export const MachineContextProvider = (props: MachineContextProviderProps) => {
const { children } = props;
const {
data: rawData,
isLoading,
error,
isValidating,
mutate,
swrKey,
} = useListMachines();
const [filters, setFilters] = useState<Filters>([]);
const data = useMemo(() => {
if (!isLoading && !error && !isValidating && rawData) {
const { machines } = rawData.data;
return machines.filter((m) =>
filters.every((f) => m[f.name] === f.value),
);
}
return [];
}, [isLoading, error, isValidating, rawData, filters]);
return (
<MachineContext.Provider
value={{
rawData,
data,
isLoading,
error,
isValidating,
filters,
setFilters,
swrKey,
mutate,
}}
>
{children}
</MachineContext.Provider>
);
};
export const useMachines = () => React.useContext(MachineContext);

View File

@@ -1,52 +0,0 @@
import { inspectVm } from "@/api/default/default";
import { HTTPValidationError, VmConfig } from "@/api/model";
import { AxiosError } from "axios";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
interface UseVmsOptions {
url: string;
attr: string;
}
export const useVms = (options: UseVmsOptions) => {
const { url, attr } = options;
const [isLoading, setIsLoading] = useState(true);
const [config, setConfig] = useState<VmConfig>();
const [error, setError] = useState<AxiosError<HTTPValidationError>>();
useEffect(() => {
const getVmInfo = async (url: string, attr: string) => {
if (url === "" || !url) {
toast.error("Flake url is missing", { id: "missing.flake.url" });
return undefined;
}
try {
const response = await inspectVm({
flake_attr: attr,
flake_url: url,
});
const {
data: { config },
} = response;
setError(undefined);
return config;
} catch (e) {
const err = e as AxiosError<HTTPValidationError>;
setError(err);
toast(
"Could not find default configuration. Please select a machine preset",
);
return undefined;
} finally {
setIsLoading(false);
}
};
getVmInfo(url, attr).then((c) => setConfig(c));
}, [url, attr]);
return {
error,
isLoading,
config,
};
};

View File

@@ -1,165 +0,0 @@
import {
Button,
InputAdornment,
LinearProgress,
ListSubheader,
MenuItem,
Select,
Switch,
TextField,
} from "@mui/material";
import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form";
import { FlakeBadge } from "../flakeBadge/flakeBadge";
import { createVm, useInspectFlakeAttrs } from "@/api/default/default";
import { VmConfig } from "@/api/model";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useAppState } from "../hooks/useAppContext";
interface VmPropLabelProps {
children: React.ReactNode;
}
const VmPropLabel = (props: VmPropLabelProps) => (
<div className="col-span-4 flex items-center sm:col-span-1">
{props.children}
</div>
);
interface VmPropContentProps {
children: React.ReactNode;
}
const VmPropContent = (props: VmPropContentProps) => (
<div className="col-span-4 sm:col-span-3">{props.children}</div>
);
interface VmDetailsProps {
formHooks: UseFormReturn<VmConfig, any, undefined>;
setVmUuid: Dispatch<SetStateAction<string | null>>;
}
export const ConfigureVM = (props: VmDetailsProps) => {
const { formHooks, setVmUuid } = props;
const { control, handleSubmit, watch, setValue } = formHooks;
const [isStarting, setStarting] = useState(false);
const { setAppState } = useAppState();
const { isLoading, data } = useInspectFlakeAttrs({ url: watch("flake_url") });
useEffect(() => {
if (!isLoading && data?.data) {
setValue("flake_attr", data.data.flake_attrs[0] || "");
}
}, [isLoading, setValue, data]);
const onSubmit: SubmitHandler<VmConfig> = async (data) => {
setStarting(true);
console.log(data);
const response = await createVm(data);
const { uuid } = response?.data || null;
setVmUuid(() => uuid);
setStarting(false);
if (response.statusText === "OK") {
toast.success(("Joined @ " + uuid) as string);
setAppState((s) => ({ ...s, isJoined: true }));
} else {
toast.error("Could not join");
}
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="grid grid-cols-4 gap-y-10"
>
<div className="col-span-4">
<ListSubheader sx={{ bgcolor: "inherit" }}>General</ListSubheader>
</div>
<VmPropLabel>Flake</VmPropLabel>
<VmPropContent>
<FlakeBadge
flakeAttr={watch("flake_attr")}
flakeUrl={watch("flake_url")}
/>
</VmPropContent>
<VmPropLabel>Machine</VmPropLabel>
<VmPropContent>
{!isLoading && (
<Controller
name="flake_attr"
control={control}
render={({ field }) => (
<Select
{...field}
required
variant="standard"
fullWidth
disabled={isLoading}
>
{!data?.data.flake_attrs.includes("default") && (
<MenuItem value={"default"}>default</MenuItem>
)}
{data?.data.flake_attrs.map((attr) => (
<MenuItem value={attr} key={attr}>
{attr}
</MenuItem>
))}
</Select>
)}
/>
)}
</VmPropContent>
<div className="col-span-4">
<ListSubheader sx={{ bgcolor: "inherit" }}>VM</ListSubheader>
</div>
<VmPropLabel>CPU Cores</VmPropLabel>
<VmPropContent>
<Controller
name="cores"
control={control}
render={({ field }) => <TextField type="number" {...field} />}
/>
</VmPropContent>
<VmPropLabel>Graphics</VmPropLabel>
<VmPropContent>
<Controller
name="graphics"
control={control}
render={({ field }) => (
<Switch {...field} defaultChecked={watch("graphics")} />
)}
/>
</VmPropContent>
<VmPropLabel>Memory Size</VmPropLabel>
<VmPropContent>
<Controller
name="memory_size"
control={control}
render={({ field }) => (
<TextField
type="number"
{...field}
InputProps={{
endAdornment: (
<InputAdornment position="end">MiB</InputAdornment>
),
}}
/>
)}
/>
</VmPropContent>
<div className="col-span-4 grid items-center">
{isStarting && <LinearProgress />}
<Button
autoFocus
type="submit"
disabled={isStarting}
variant="contained"
>
Join Clan
</Button>
</div>
</form>
);
};

View File

@@ -1,63 +0,0 @@
"use client";
import { useState } from "react";
import { LoadingOverlay } from "./loadingOverlay";
import { FlakeBadge } from "../flakeBadge/flakeBadge";
import { Typography, Button } from "@mui/material";
// import { FlakeResponse } from "@/api/model";
import { ConfirmVM } from "./confirmVM";
import { Log } from "./log";
import GppMaybeIcon from "@mui/icons-material/GppMaybe";
import { useInspectFlake } from "@/api/default/default";
interface ConfirmProps {
flakeUrl: string;
flakeAttr: string;
handleBack: () => void;
}
export const Confirm = (props: ConfirmProps) => {
const { flakeUrl, handleBack, flakeAttr } = props;
const [userConfirmed, setUserConfirmed] = useState(false);
const { data, isLoading } = useInspectFlake({
url: flakeUrl,
});
return userConfirmed ? (
<ConfirmVM
url={flakeUrl}
handleBack={handleBack}
defaultFlakeAttr={flakeAttr}
/>
) : (
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2 ">
{isLoading && (
<LoadingOverlay
title={"Loading Flake"}
subtitle={<FlakeBadge flakeUrl={flakeUrl} flakeAttr={flakeAttr} />}
/>
)}
{data && (
<>
<Typography variant="subtitle1">
To join the clan you must trust the Author
</Typography>
<GppMaybeIcon sx={{ height: "10rem", width: "10rem", mb: 5 }} />
<Button
autoFocus
size="large"
color="warning"
variant="contained"
onClick={() => setUserConfirmed(true)}
sx={{ mb: 10 }}
>
Trust Flake Author
</Button>
<Log
title="What's about to be built"
lines={data.data.content.split("\n")}
/>
</>
)}
</div>
);
};

View File

@@ -1,61 +0,0 @@
"use client";
import React, { useEffect, useState } from "react";
import { VmConfig } from "@/api/model";
import { useVms } from "@/components/hooks/useVms";
import { LoadingOverlay } from "./loadingOverlay";
import { useForm } from "react-hook-form";
import { ConfigureVM } from "./configureVM";
import { VmBuildLogs } from "./vmBuildLogs";
interface ConfirmVMProps {
url: string;
handleBack: () => void;
defaultFlakeAttr: string;
}
export function ConfirmVM(props: ConfirmVMProps) {
const { url, defaultFlakeAttr } = props;
const formHooks = useForm<VmConfig>({
defaultValues: {
flake_url: url,
flake_attr: defaultFlakeAttr,
cores: 4,
graphics: true,
memory_size: 2048,
},
});
const [vmUuid, setVmUuid] = useState<string | null>(null);
const { setValue, watch, formState } = formHooks;
const { config, isLoading } = useVms({
url,
attr: watch("flake_attr") || defaultFlakeAttr,
});
useEffect(() => {
if (config) {
setValue("cores", config?.cores);
setValue("memory_size", config?.memory_size);
setValue("graphics", config?.graphics);
}
}, [config, setValue]);
return (
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2">
{!formState.isSubmitted && (
<>
<div className="mb-2 w-full max-w-2xl">
{isLoading && (
<LoadingOverlay title={"Loading VM Configuration"} subtitle="" />
)}
<ConfigureVM formHooks={formHooks} setVmUuid={setVmUuid} />
</div>
</>
)}
{formState.isSubmitted && vmUuid && <VmBuildLogs vmUuid={vmUuid} />}
</div>
);
}

View File

@@ -1,20 +0,0 @@
"use client";
import { Typography } from "@mui/material";
import { ReactNode } from "react";
interface LayoutProps {
children: ReactNode;
}
export const Layout = (props: LayoutProps) => {
return (
<div className="grid h-[70vh] w-full grid-cols-1 justify-center gap-y-4">
<Typography variant="h4" className="w-full text-center">
Join{" "}
<Typography variant="h4" className="font-bold" component={"span"}>
Clan.lol
</Typography>
</Typography>
{props.children}
</div>
);
};

View File

@@ -11,14 +11,9 @@ import Image from "next/image";
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 AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import Link from "next/link";
import WysiwygIcon from "@mui/icons-material/Wysiwyg";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
type MenuEntry = {
@@ -32,39 +27,15 @@ type MenuEntry = {
const menuEntries: MenuEntry[] = [
{
icon: <DashboardIcon />,
label: "Dashoard",
icon: <AssignmentIndIcon />,
label: "Freelance",
to: "/",
disabled: false,
},
{
icon: <DevicesIcon />,
label: "Machines",
to: "/machines",
disabled: false,
},
{
icon: <AppsIcon />,
label: "Applications",
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",
icon: <WysiwygIcon />,
label: "Blog",
to: "/blog",
disabled: true,
},
];
@@ -138,23 +109,6 @@ 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>
</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 +0,0 @@
export { NodeTable } from "./nodeTable";

View File

@@ -1,57 +0,0 @@
import React from "react";
import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from "recharts";
import { useTheme } from "@mui/material/styles";
import { Box } from "@mui/material";
export interface PieData {
name: string;
value: number;
color: string;
}
interface Props {
data: PieData[];
showLabels?: boolean;
}
export 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}
legendType="square"
cx="50%"
cy="50%"
startAngle={0}
endAngle={360}
paddingAngle={0}
labelLine={true}
hide={false}
minAngle={0}
isAnimationActive={true}
animationBegin={0}
animationDuration={1000}
animationEasing="ease-in"
blendStroke={true}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Legend verticalAlign="bottom" />
</PieChart>
</ResponsiveContainer>
</Box>
);
}

View File

@@ -1,135 +0,0 @@
"use client";
import * as React from "react";
import Box from "@mui/material/Box";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import CircleIcon from "@mui/icons-material/Circle";
import Stack from "@mui/material/Stack/Stack";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2
import { Collapse } from "@mui/material";
import { Machine, Status } from "@/api/model";
function renderStatus(status: Status) {
switch (status) {
case Status.online:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="success" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Online
</Typography>
</Stack>
);
case Status.offline:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="error" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Offline
</Typography>
</Stack>
);
case Status.unknown:
return (
<Stack direction="row" alignItems="center" gap={1}>
<CircleIcon color="warning" style={{ fontSize: 15 }} />
<Typography component="div" align="left" variant="body1">
Pending
</Typography>
</Stack>
);
}
}
export function NodeRow(props: {
row: Machine;
selected: string | undefined;
setSelected: (a: string | undefined) => void;
}) {
const { row, selected, setSelected } = props;
const [open, setOpen] = React.useState(false);
// Speed optimization. We compare string pointers here instead of the string content.
const isSelected = selected == row.name;
const handleClick = (event: React.MouseEvent<unknown>, name: string) => {
if (isSelected) {
setSelected(undefined);
} else {
setSelected(name);
}
};
return (
<React.Fragment>
{/* Rendered Row */}
<TableRow
hover
role="checkbox"
aria-checked={isSelected}
tabIndex={-1}
key={row.name}
selected={isSelected}
sx={{ cursor: "pointer" }}
>
<TableCell padding="none">
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell
component="th"
scope="row"
onClick={(event) => handleClick(event, row.name)}
>
<Stack>
<Typography component="div" align="left" variant="body1">
{row.name}
</Typography>
</Stack>
</TableCell>
<TableCell
align="right"
onClick={(event) => handleClick(event, row.name)}
>
{renderStatus(row.status)}
</TableCell>
</TableRow>
{/* Row Expansion */}
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
Metadata
</Typography>
<Grid2 container spacing={2} paddingLeft={0}>
<Grid2
xs={6}
justifyContent="left"
display="flex"
paddingRight={3}
>
<Box>Hello1</Box>
</Grid2>
<Grid2 xs={6} paddingLeft={6}>
<Box>Hello2</Box>
</Grid2>
</Grid2>
</Box>
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
}

View File

@@ -1,96 +0,0 @@
"use client";
import { CircularProgress, Grid, useTheme } 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 { 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 machines = useMachines();
const theme = useTheme();
const is_xs = useMediaQuery(theme.breakpoints.only("xs"));
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.data.map((machine) => {
return { name: machine.name, status: machine.status };
});
setFilteredList(tableData);
return tableData;
}, [machines.data]);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
if (machines.isLoading) {
return (
<Grid
container
sx={{
h: "100vh",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress size={80} color="secondary" />
</Grid>
);
}
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>
<NodeTableContainer
tableData={filteredList}
page={page}
rowsPerPage={rowsPerPage}
dense={false}
selected={selected}
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}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
</Box>
);
}

View File

@@ -1,197 +0,0 @@
"use client";
import * as React from "react";
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 TableRow from "@mui/material/TableRow";
import TableSortLabel from "@mui/material/TableSortLabel";
import { visuallyHidden } from "@mui/utils";
import { NodeRow } from "./nodeRow";
import { Machine } from "@/api/model/machine";
interface HeadCell {
disablePadding: boolean;
id: keyof Machine;
label: string;
alignRight: boolean;
}
const headCells: readonly HeadCell[] = [
{
id: "name",
alignRight: false,
disablePadding: false,
label: "DOMAIN NAME",
},
{
id: "status",
alignRight: false,
disablePadding: false,
label: "STATUS",
},
];
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
export type NodeOrder = "asc" | "desc";
function getComparator<Key extends keyof any>(
order: NodeOrder,
orderBy: Key,
): (
a: { [key in Key]: number | string | boolean },
b: { [key in Key]: number | string | boolean },
) => number {
return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
// Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
// 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,
) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) {
return order;
}
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}
interface EnhancedTableProps {
onRequestSort: (
event: React.MouseEvent<unknown>,
property: keyof Machine,
) => void;
order: NodeOrder;
orderBy: string;
rowCount: number;
}
function EnhancedTableHead(props: EnhancedTableProps) {
const { order, orderBy, onRequestSort } = props;
const createSortHandler =
(property: keyof Machine) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
<TableCell id="dropdown" colSpan={1} />
{headCells.map((headCell) => (
<TableCell
key={headCell.id}
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"}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}
interface NodeTableContainerProps {
tableData: readonly Machine[];
page: number;
rowsPerPage: number;
dense: boolean;
selected: string | undefined;
setSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
}
export function NodeTableContainer(props: NodeTableContainerProps) {
const { tableData, page, rowsPerPage, dense, selected, setSelected } = props;
const [order, setOrder] = React.useState<NodeOrder>("asc");
const [orderBy, setOrderBy] = React.useState<keyof Machine>("status");
// Avoid a layout jump when reaching the last page with empty rows.
const emptyRows =
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - tableData.length) : 0;
const handleRequestSort = (
event: React.MouseEvent<unknown>,
property: keyof Machine,
) => {
const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? "desc" : "asc");
setOrderBy(property);
};
const visibleRows = React.useMemo(
() =>
stableSort(tableData, getComparator(order, orderBy)).slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage,
),
[order, orderBy, page, rowsPerPage, tableData],
);
return (
<TableContainer>
<Table aria-labelledby="tableTitle" size={dense ? "small" : "medium"}>
<EnhancedTableHead
order={order}
orderBy={orderBy}
onRequestSort={handleRequestSort}
rowCount={tableData.length}
/>
<TableBody>
{visibleRows.map((row, index) => {
return (
<NodeRow
key={index}
row={row}
selected={selected}
setSelected={setSelected}
/>
);
})}
{emptyRows > 0 && (
<TableRow
style={{
height: (dense ? 33 : 53) * emptyRows,
}}
>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
);
}

View File

@@ -1,73 +0,0 @@
import { Card, CardContent, Stack, Typography } from "@mui/material";
import hexRgb from "hex-rgb";
import { useMemo } from "react";
interface PieData {
name: string;
value: number;
color: string;
}
interface PieCardsProps {
pieData: PieData[];
}
export function PieCards(props: PieCardsProps) {
const { pieData } = props;
const cardData = useMemo(() => {
return pieData
.filter((pieItem) => pieItem.value > 0)
.concat({
name: "Total",
value: pieData.reduce((a, b) => a + b.value, 0),
color: "#000000",
});
}, [pieData]);
return (
<Stack
sx={{ paddingTop: 6 }}
height={350}
id="cardBox"
display="flex"
flexDirection="column"
justifyContent="flex-start"
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.25,
}),
}}
>
<CardContent>
<Typography
variant="h4"
component="div"
gutterBottom={true}
textAlign="center"
>
{pieItem.value}
</Typography>
<Typography
sx={{ mb: 1.5 }}
color="text.secondary"
textAlign="center"
>
{pieItem.name}
</Typography>
</CardContent>
</Card>
))}
</Stack>
);
}

View File

@@ -1,99 +0,0 @@
"use client";
import { SetStateAction, Dispatch, useState, useEffect, useMemo } from "react";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import { useDebounce } from "../hooks/useDebounce";
import { Autocomplete, InputAdornment, TextField } from "@mui/material";
import { Machine } from "@/api/model/machine";
export interface SearchBarProps {
tableData: readonly Machine[];
setFilteredList: Dispatch<SetStateAction<readonly Machine[]>>;
}
export function SearchBar(props: SearchBarProps) {
let { tableData, setFilteredList } = props;
const [search, setSearch] = useState<string>("");
const debouncedSearch = useDebounce(search, 250);
const [open, setOpen] = useState(false);
// Define a function to handle the Esc key press
function handleEsc(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === "Escape") {
setSearch("");
setFilteredList(tableData);
}
// check if the key is Enter
if (event.key === "Enter") {
setOpen(false);
}
}
useEffect(() => {
if (debouncedSearch) {
const filtered: Machine[] = tableData.filter((row) => {
return row.name.toLowerCase().includes(debouncedSearch.toLowerCase());
});
setFilteredList(filtered);
}
}, [debouncedSearch, tableData, setFilteredList]);
const handleInputChange = (event: any, value: string) => {
if (value === "") {
setFilteredList(tableData);
}
setSearch(value);
};
const suggestions = useMemo(
() => tableData.map((row) => row.name),
[tableData],
);
return (
<Autocomplete
freeSolo
autoComplete
options={suggestions}
renderOption={(props: any, option: any) => {
return (
<li {...props} key={option}>
{option}
</li>
);
}}
onKeyDown={handleEsc}
onInputChange={handleInputChange}
value={search}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
renderInput={(params) => (
<TextField
{...params}
fullWidth
label="Search"
variant="outlined"
autoComplete="nickname"
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<IconButton>
<SearchIcon />
</IconButton>
</InputAdornment>
),
}}
></TextField>
)}
/>
);
}

View File

@@ -1,85 +0,0 @@
"use client";
import * as React from "react";
import Box from "@mui/material/Box";
import DeleteIcon from "@mui/icons-material/Delete";
import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import EditIcon from "@mui/icons-material/ModeEdit";
import AddIcon from "@mui/icons-material/Add";
import Link from "next/link";
export function StickySpeedDial(props: { selected: string | undefined }) {
const { selected } = props;
const [open, setOpen] = React.useState(false);
function handleClose(event: any, reason: CloseReason) {
if (reason === "toggle" || reason === "escapeKeyDown") {
setOpen(false);
}
}
function handleOpen(event: any, reason: OpenReason) {
if (reason === "toggle") {
setOpen(true);
}
}
const isSomethingSelected = selected != undefined;
function editDial() {
if (isSomethingSelected) {
return (
<Link href={`/machines/edit/${selected}`} style={{ marginTop: 7.5 }}>
<EditIcon color="action" />
</Link>
);
} else {
return <EditIcon color="disabled" />;
}
}
return (
<Box
sx={{
transform: "translateZ(0px)",
flexGrow: 1,
position: "fixed",
right: 20,
top: 15,
margin: 0,
zIndex: 9000,
}}
>
<SpeedDial
color="secondary"
ariaLabel="SpeedDial basic example"
icon={<SpeedDialIcon />}
direction="down"
onClose={handleClose}
onOpen={handleOpen}
open={open}
>
<SpeedDialAction
key="Add"
icon={
<Link href="/machines/add" style={{ marginTop: 7.5 }}>
<AddIcon color="action" />
</Link>
}
tooltipTitle="Add"
/>
<SpeedDialAction
key="Delete"
icon={
<DeleteIcon color={isSomethingSelected ? "action" : "disabled"} />
}
tooltipTitle="Delete"
/>
<SpeedDialAction key="Edit" icon={editDial()} tooltipTitle="Edit" />
</SpeedDial>
</Box>
);
}