generated from Luis/nextjs-python-web-template
Befor fixing linting problem
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export { NodeTable } from "./nodeTable";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user