diff --git a/pkgs/ui/src/app/machines/add/page.tsx b/pkgs/ui/src/app/machines/add/page.tsx index 6413c3c88..3581d0006 100644 --- a/pkgs/ui/src/app/machines/add/page.tsx +++ b/pkgs/ui/src/app/machines/add/page.tsx @@ -1,64 +1,7 @@ "use client"; -import { useForm } from "react-hook-form"; - -import { JSONSchema7 } from "json-schema"; -import { useMemo, useState } from "react"; -import { schema } from "./schema"; -import Form from "@rjsf/mui"; -import validator from "@rjsf/validator-ajv8"; -import { Button } from "@mui/material"; - -interface CreateMachineFormProps { - schema: JSONSchema7; - initialValues: any; -} - -const defaultValues = Object.entries(schema.properties || {}).reduce( - (acc, [key, value]) => { - /*@ts-ignore*/ - const init: any = value?.default; - if (init) { - return { - ...acc, - [key]: init, - }; - } - return acc; - }, - {}, -); - -function CreateMachineForm(props: CreateMachineFormProps) { - const { schema, initialValues } = props; - - return ( -
( - - ), - }, - }} - /> - ); -} +import { CreateMachineForm } from "@/components/createMachineForm"; export default function CreateMachine() { - return ; + return ; } diff --git a/pkgs/ui/src/components/createMachineForm/customConfig.tsx b/pkgs/ui/src/components/createMachineForm/customConfig.tsx new file mode 100644 index 000000000..7b9e76f23 --- /dev/null +++ b/pkgs/ui/src/components/createMachineForm/customConfig.tsx @@ -0,0 +1,165 @@ +"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, FormProps } from "@rjsf/core"; +import { Form } from "@rjsf/mui"; +import validator from "@rjsf/validator-ajv8"; +import toast from "react-hot-toast"; +import { JSONSchema7 } from "json-schema"; +import { useMemo, useRef } from "react"; +import { FormStepContentProps } from "./interfaces"; +import { + ErrorListProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + TranslatableString, +} from "@rjsf/utils"; + +interface PureCustomConfigProps extends FormStepContentProps { + schema: JSONSchema7; + initialValues: any; +} +export function CustomConfig(props: FormStepContentProps) { + const { formHooks, handleNext } = props; + const { data, isLoading, error } = useGetMachineSchema("mama"); + 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 ? ( + + ) : error?.message ? ( +
{error?.message}
+ ) : ( + + ); +} + +function ErrorList< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>({ errors, registry }: ErrorListProps) { + const { translateString } = registry; + return ( + + + + {translateString(TranslatableString.ErrorsLabel)} + + + {errors.map((error, i: number) => { + return ( + + + + + + + ); + })} + + + + ); +} + +function PureCustomConfig(props: PureCustomConfigProps) { + const { schema, initialValues, formHooks, handleNext } = props; + const { setValue, watch } = formHooks; + + console.log({ schema }); + + const configData = watch("config") as IChangeEvent; + + console.log({ configData }); + + const setConfig = (data: IChangeEvent) => { + console.log({ data }); + setValue("config", data); + }; + + const formRef = useRef(); + + 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 ( + ( +
+ +
+ ), + }, + }} + /> + ); +} diff --git a/pkgs/ui/src/components/createMachineForm/index.tsx b/pkgs/ui/src/components/createMachineForm/index.tsx new file mode 100644 index 000000000..3c5186a63 --- /dev/null +++ b/pkgs/ui/src/components/createMachineForm/index.tsx @@ -0,0 +1,160 @@ +import { + Box, + Button, + MobileStepper, + Step, + StepLabel, + Stepper, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import React, { ReactNode, useState } from "react"; +import { useForm, UseFormReturn } from "react-hook-form"; +import { CustomConfig } from "./customConfig"; +import { CreateMachineForm, FormStep } from "./interfaces"; + +const SC = (props: { children: ReactNode }) => { + return <>{props.children}; +}; + +export function CreateMachineForm() { + const formHooks = useForm({ + defaultValues: { + name: "", + config: {}, + }, + }); + const { handleSubmit, control, watch, reset, formState } = formHooks; + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [activeStep, setActiveStep] = useState(0); + + const steps: FormStep[] = [ + { + id: "template", + label: "Template", + content:
, + }, + { + id: "modules", + label: "Modules", + content:
, + }, + { + id: "config", + label: "Customize", + content: , + }, + { + id: "save", + label: "Save", + content:
, + }, + ]; + + 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 = () => ( + + ); + + const NextButton = () => ( + <> + {activeStep !== steps.length - 1 && ( + + )} + {activeStep === steps.length - 1 && ( + + )} + + ); + return ( + + + {isMobile && ( + } + nextButton={} + steps={steps.length} + /> + )} + {!isMobile && ( + + {steps.map(({ label }, index) => { + const stepProps: { completed?: boolean } = {}; + const labelProps: { + optional?: React.ReactNode; + } = {}; + return ( + + {label} + + ); + })} + + )} + {/* */} + {/* The step Content */} + {currentStep && currentStep.content} + + {/* Desktop step controls */} + {!isMobile && ( + + + + + + )} + + + ); +} diff --git a/pkgs/ui/src/components/createMachineForm/interfaces.ts b/pkgs/ui/src/components/createMachineForm/interfaces.ts new file mode 100644 index 000000000..93d730fcb --- /dev/null +++ b/pkgs/ui/src/components/createMachineForm/interfaces.ts @@ -0,0 +1,24 @@ +import { ReactElement, ReactNode } 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; + +export type FormStep = { + id: StepId; + label: string; + content: FormStepContent; +}; + +export interface FormStepContentProps { + formHooks: FormHooks; + handleNext: () => void; +} + +export type FormStepContent = ReactElement; diff --git a/pkgs/ui/src/components/createMachineForm/saveConfig.tsx b/pkgs/ui/src/components/createMachineForm/saveConfig.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/ui/src/components/createMachineForm/selectModules.tsx b/pkgs/ui/src/components/createMachineForm/selectModules.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/ui/src/components/createMachineForm/selectTemplate.tsx b/pkgs/ui/src/components/createMachineForm/selectTemplate.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/ui/src/data/_schema.ts b/pkgs/ui/src/data/_schema.ts new file mode 100644 index 000000000..7a3b8938d --- /dev/null +++ b/pkgs/ui/src/data/_schema.ts @@ -0,0 +1,88 @@ +import { RJSFSchema } from "@rjsf/utils"; +export const schema: RJSFSchema = { + properties: { + bloatware: { + properties: { + age: { + default: 42, + description: "The age of the user", + type: "integer", + }, + isAdmin: { + default: false, + description: "Is the user an admin?", + type: "boolean", + }, + kernelModules: { + default: ["nvme", "xhci_pci", "ahci"], + description: "A list of enabled kernel modules", + items: { + type: "string", + }, + type: "array", + }, + name: { + default: "John Doe", + description: "The name of the user", + type: "string", + }, + services: { + properties: { + opt: { + default: "foo", + description: "A submodule option", + type: "string", + }, + }, + type: "object", + }, + userIds: { + additionalProperties: { + type: "integer", + }, + default: { + albrecht: 3, + horst: 1, + peter: 2, + }, + description: "Some attributes", + type: "object", + }, + }, + type: "object", + }, + networking: { + properties: { + zerotier: { + properties: { + controller: { + properties: { + enable: { + default: false, + description: + "Whether to enable turn this machine into the networkcontroller.", + type: "boolean", + }, + public: { + default: false, + description: + "everyone can join a public network without having the administrator to accept\n", + type: "boolean", + }, + }, + type: "object", + }, + networkId: { + description: "zerotier networking id\n", + type: "string", + }, + }, + required: ["networkId"], + type: "object", + }, + }, + type: "object", + }, + }, + type: "object", +}; diff --git a/pkgs/ui/src/app/machines/add/schema.ts b/pkgs/ui/src/data/_schema2.ts similarity index 100% rename from pkgs/ui/src/app/machines/add/schema.ts rename to pkgs/ui/src/data/_schema2.ts