diff --git a/pkgs/ui/public/favicon.ico b/pkgs/ui/public/favicon.ico index 05acd55ee..94364cd20 100644 Binary files a/pkgs/ui/public/favicon.ico and b/pkgs/ui/public/favicon.ico differ diff --git a/pkgs/ui/src/app/favicon.ico b/pkgs/ui/src/app/favicon.ico deleted file mode 100644 index 718d6fea4..000000000 Binary files a/pkgs/ui/src/app/favicon.ico and /dev/null differ diff --git a/pkgs/ui/src/app/layout.tsx b/pkgs/ui/src/app/layout.tsx index b9d3fa532..0dec9b405 100644 --- a/pkgs/ui/src/app/layout.tsx +++ b/pkgs/ui/src/app/layout.tsx @@ -53,7 +53,7 @@ export default function RootLayout({ Clan.lol - + diff --git a/pkgs/ui/src/components/createMachineForm/clanModules.tsx b/pkgs/ui/src/components/createMachineForm/clanModules.tsx index e069beafb..045bbc198 100644 --- a/pkgs/ui/src/components/createMachineForm/clanModules.tsx +++ b/pkgs/ui/src/components/createMachineForm/clanModules.tsx @@ -1,6 +1,13 @@ import { getMachineSchema } from "@/api/machine/machine"; import { useListClanModules } from "@/api/modules/modules"; -import { Alert, AlertTitle, FormHelperText, Typography } from "@mui/material"; +import { + Alert, + AlertTitle, + Divider, + FormHelperText, + Input, + Typography, +} from "@mui/material"; import Box from "@mui/material/Box"; import Chip from "@mui/material/Chip"; import FormControl from "@mui/material/FormControl"; @@ -8,7 +15,8 @@ import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; import OutlinedInput from "@mui/material/OutlinedInput"; import Select, { SelectChangeEvent } from "@mui/material/Select"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { Controller } from "react-hook-form"; import { toast } from "react-hot-toast"; import { CreateMachineForm, FormStepContentProps } from "./interfaces"; @@ -23,26 +31,85 @@ const MenuProps = { }, }; +// interface IupdateSchema { +// clanName: string; +// modules: string[]; +// formHooks: FormHooks; +// setSchemaError: Dispatch>; +// } + +// const updateSchema = ({ +// clanName, +// modules, +// formHooks, +// setSchemaError, +// }: IupdateSchema) => { +// formHooks.setValue("isSchemaLoading", true); +// getMachineSchema(clanName, { +// clanImports: modules, +// }) +// .then((response) => { +// if (response.statusText == "OK") { +// formHooks.setValue("schema", response.data.schema); +// setSchemaError(null); +// } +// }) +// .catch((error) => { +// formHooks.setValue("schema", {}); +// console.error({ error }); +// setSchemaError(error.message); +// toast.error(`${error.message}`); +// }) +// .finally(() => { +// formHooks.setValue("isSchemaLoading", false); +// }); +// }; + type ClanModulesProps = FormStepContentProps; +const SchemaSuccessMsg = () => ( + + Success + + Machine configuration schema successfully created. + + +); + +interface SchemaErrorMsgProps { + msg: string | null; +} + +const SchemaErrorMsg = (props: SchemaErrorMsgProps) => ( + + Error + + Machine configuration schema could not be created. + + + {props.msg} + + +); + export default function ClanModules(props: ClanModulesProps) { const { clanName, formHooks } = props; const { data, isLoading } = useListClanModules(clanName); - + const [schemaError] = useState(null); const selectedModules = formHooks.watch("modules"); - useEffect(() => { getMachineSchema(clanName, { - imports: [], + clanImports: [], }).then((response) => { if (response.statusText == "OK") { formHooks.setValue("schema", response.data.schema); } }); - formHooks.setValue("modules", []); + // Only re-run if global clanName has changed // eslint-disable-next-line react-hooks/exhaustive-deps }, [clanName]); + const isSchemaLoading = formHooks.watch("isSchemaLoading"); const handleChange = ( event: SelectChangeEvent, @@ -53,7 +120,7 @@ export default function ClanModules(props: ClanModulesProps) { const newValue = typeof value === "string" ? value.split(",") : value; formHooks.setValue("modules", newValue); getMachineSchema(clanName, { - imports: newValue, + clanImports: newValue, }) .then((response) => { if (response.statusText == "OK") { @@ -66,8 +133,19 @@ export default function ClanModules(props: ClanModulesProps) { toast.error(`${error.message}`); }); }; + return (
+ + Machine name + } + /> + Choose a unique name for the machine. + + Info Optionally select some modules —{" "} @@ -106,6 +184,14 @@ export default function ClanModules(props: ClanModulesProps) { (Optional) Select clan modules to be added. + + {!isSchemaLoading && } + {!isSchemaLoading && + (!schemaError ? ( + + ) : ( + + ))}
); } diff --git a/pkgs/ui/src/components/createMachineForm/customConfig.tsx b/pkgs/ui/src/components/createMachineForm/customConfig.tsx index bccf78369..c9086e604 100644 --- a/pkgs/ui/src/components/createMachineForm/customConfig.tsx +++ b/pkgs/ui/src/components/createMachineForm/customConfig.tsx @@ -114,7 +114,7 @@ function PureCustomConfig(props: PureCustomConfigProps) { ); } else { formHooks.clearErrors("config"); - toast.success("Config seems valid"); + toast.success("Configuration is valid"); } }; @@ -139,7 +139,7 @@ function PureCustomConfig(props: PureCustomConfigProps) { variant="outlined" color="secondary" > - Validate + Validate configuration ), diff --git a/pkgs/ui/src/components/createMachineForm/index.tsx b/pkgs/ui/src/components/createMachineForm/index.tsx index b672cb03b..65b11e00d 100644 --- a/pkgs/ui/src/components/createMachineForm/index.tsx +++ b/pkgs/ui/src/components/createMachineForm/index.tsx @@ -1,6 +1,8 @@ +import { createMachine, setMachineConfig } from "@/api/machine/machine"; import { Box, Button, + CircularProgress, LinearProgress, MobileStepper, Step, @@ -11,6 +13,7 @@ import { } from "@mui/material"; import React, { useState } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; import { useAppState } from "../hooks/useAppContext"; import ClanModules from "./clanModules"; import { CustomConfig } from "./customConfig"; @@ -22,22 +25,19 @@ export function CreateMachineForm() { } = useAppState(); const formHooks = useForm({ defaultValues: { + isSchemaLoading: false, name: "", config: {}, modules: [], }, }); - const { handleSubmit, reset } = formHooks; + + const { handleSubmit, watch } = 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", @@ -56,11 +56,6 @@ export function CreateMachineForm() { ), }, - { - id: "save", - label: "Save", - content:
, - }, ]; const handleNext = () => { @@ -75,14 +70,23 @@ export function CreateMachineForm() { } }; - const handleReset = () => { - setActiveStep(0); - reset(); - }; const currentStep = steps.at(activeStep); - async function onSubmit(data: any) { + async function onSubmit(data: CreateMachineForm) { console.log({ data }, "Aggregated Data; creating machine from"); + if (clanName) { + if (!data.name) { + toast.error("Machine name should not be empty"); + return; + } + await createMachine(clanName, { + name: data.name, + }); + await setMachineConfig(clanName, data.name, { + clan: data.config.formData, + clanImports: data.modules, + }); + } } const BackButton = () => ( @@ -102,17 +106,21 @@ export function CreateMachineForm() { )} {activeStep === steps.length - 1 && ( - )} diff --git a/pkgs/ui/src/components/createMachineForm/interfaces.ts b/pkgs/ui/src/components/createMachineForm/interfaces.ts index 3cac40b18..77c2ed752 100644 --- a/pkgs/ui/src/components/createMachineForm/interfaces.ts +++ b/pkgs/ui/src/components/createMachineForm/interfaces.ts @@ -9,6 +9,7 @@ export type CreateMachineForm = { config: any; modules: string[]; schema: JSONSchema7; + isSchemaLoading: boolean; }; export type FormHooks = UseFormReturn; diff --git a/pkgs/ui/src/components/createMachineForm/saveConfig.tsx b/pkgs/ui/src/components/createMachineForm/saveConfig.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/ui/src/components/createMachineForm/selectModules.tsx b/pkgs/ui/src/components/createMachineForm/selectModules.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/ui/src/components/createMachineForm/selectTemplate.tsx b/pkgs/ui/src/components/createMachineForm/selectTemplate.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/ui/src/components/hooks/useVms.tsx b/pkgs/ui/src/components/hooks/useVms.tsx index b385a72d5..6eda02897 100644 --- a/pkgs/ui/src/components/hooks/useVms.tsx +++ b/pkgs/ui/src/components/hooks/useVms.tsx @@ -1,5 +1,5 @@ -import { inspectVm } from "@/api/vm/vm"; import { HTTPValidationError, VmConfig } from "@/api/model"; +import { inspectVm } from "@/api/vm/vm"; import { AxiosError } from "axios"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; diff --git a/pkgs/ui/src/components/join/configureVM.tsx b/pkgs/ui/src/components/join/configureVM.tsx index 28b56fc62..27ab0526e 100644 --- a/pkgs/ui/src/components/join/configureVM.tsx +++ b/pkgs/ui/src/components/join/configureVM.tsx @@ -1,3 +1,5 @@ +import { useInspectFlakeAttrs } from "@/api/flake/flake"; +import { FormValues } from "@/views/joinPrequel"; import { Button, InputAdornment, @@ -8,14 +10,10 @@ import { Switch, TextField, } from "@mui/material"; -import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form"; -import { FlakeBadge } from "../flakeBadge/flakeBadge"; -import { createVm } from "@/api/vm/vm"; -import { useInspectFlakeAttrs } from "@/api/flake/flake"; -import { VmConfig } from "@/api/model"; -import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { useEffect } from "react"; +import { Controller, UseFormReturn } from "react-hook-form"; import { toast } from "react-hot-toast"; -import { useAppState } from "../hooks/useAppContext"; +import { FlakeBadge } from "../flakeBadge/flakeBadge"; interface VmPropLabelProps { children: React.ReactNode; @@ -34,44 +32,41 @@ const VmPropContent = (props: VmPropContentProps) => ( ); interface VmDetailsProps { - formHooks: UseFormReturn; - setVmUuid: Dispatch>; + formHooks: UseFormReturn; } +type ClanError = { + detail: { + msg: string; + loc: []; + }[]; +}; + 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") }); + const { formHooks } = props; + const { control, watch, setValue, formState } = formHooks; + + const { isLoading, data, error } = useInspectFlakeAttrs({ + url: watch("flakeUrl"), + }); useEffect(() => { if (!isLoading && data?.data) { setValue("flake_attr", data.data.flake_attrs[0] || ""); } }, [isLoading, setValue, data]); + if (error) { + const msg = + (error?.response?.data as unknown as ClanError)?.detail?.[0]?.msg || + error.message; - const onSubmit: SubmitHandler = 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"); - } - }; - + toast.error(msg, { + id: error.name, + }); + return
{msg}
; + } return ( -
+
General
@@ -79,7 +74,7 @@ export const ConfigureVM = (props: VmDetailsProps) => { Machine @@ -88,6 +83,7 @@ export const ConfigureVM = (props: VmDetailsProps) => { ( Clan + } + /> + )} + /> + ( + Name + } + endAdornment={confirmAdornment} + /> + )} + /> + {isSubmitting && } +
+ ); +}; diff --git a/pkgs/ui/src/views/joinForm.tsx b/pkgs/ui/src/views/joinForm.tsx new file mode 100644 index 000000000..83f3edef8 --- /dev/null +++ b/pkgs/ui/src/views/joinForm.tsx @@ -0,0 +1,51 @@ +import { Confirm } from "@/components/join/confirm"; +import { Input, InputAdornment } from "@mui/material"; +import { Controller, useFormContext } from "react-hook-form"; + +interface JoinFormProps { + confirmAdornment?: React.ReactNode; + initialParams: { + flakeUrl: string; + flakeAttr: string; + }; +} +export const JoinForm = (props: JoinFormProps) => { + const { initialParams, confirmAdornment } = props; + const { control, formState, reset, getValues, watch } = useFormContext(); + + return ( +
+ {watch("flakeUrl") || initialParams.flakeUrl ? ( + reset()} + flakeUrl={ + formState.isSubmitted + ? getValues("flakeUrl") + : initialParams.flakeUrl + } + flakeAttr={initialParams.flakeAttr} + /> + ) : ( + ( + Clan + } + endAdornment={confirmAdornment} + /> + )} + /> + )} +
+ ); +}; diff --git a/pkgs/ui/src/views/joinPrequel.tsx b/pkgs/ui/src/views/joinPrequel.tsx index eb039a667..294a6d506 100644 --- a/pkgs/ui/src/views/joinPrequel.tsx +++ b/pkgs/ui/src/views/joinPrequel.tsx @@ -1,23 +1,28 @@ "use client"; import { IconButton, - Input, InputAdornment, - LinearProgress, MenuItem, Select, + Typography, } from "@mui/material"; import { useSearchParams } from "next/navigation"; import { Suspense, useState } from "react"; import { createFlake } from "@/api/flake/flake"; +import { VmConfig } from "@/api/model"; +import { createVm } from "@/api/vm/vm"; import { useAppState } from "@/components/hooks/useAppContext"; -import { Confirm } from "@/components/join/confirm"; import { Layout } from "@/components/join/layout"; +import { VmBuildLogs } from "@/components/join/vmBuildLogs"; import { ChevronRight } from "@mui/icons-material"; -import { Controller, useForm } from "react-hook-form"; +import { AxiosError } from "axios"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; +import { CreateForm } from "./createForm"; +import { JoinForm } from "./joinForm"; -type FormValues = { +export type FormValues = VmConfig & { workflow: "join" | "create"; flakeUrl: string; dest?: string; @@ -27,13 +32,25 @@ export default function JoinPrequel() { const queryParams = useSearchParams(); const flakeUrl = queryParams.get("flake") || ""; const flakeAttr = queryParams.get("attr") || "default"; - const [, setForkInProgress] = useState(false); + const initialParams = { flakeUrl, flakeAttr }; + const { setAppState } = useAppState(); - const { control, formState, getValues, reset, watch, handleSubmit } = - useForm({ - defaultValues: { flakeUrl: "", dest: undefined, workflow: "join" }, - }); + const methods = useForm({ + defaultValues: { + flakeUrl: "", + dest: undefined, + workflow: "join", + cores: 4, + graphics: true, + memory_size: 2048, + }, + }); + + const { control, watch, handleSubmit } = methods; + + const [vmUuid, setVmUuid] = useState(null); + const [showLogs, setShowLogs] = useState(false); const workflow = watch("workflow"); @@ -54,88 +71,85 @@ export default function JoinPrequel() { )} /> - + ); return ( - + + {workflow}{" "} + + Clan.lol + + + } + > - {!formState.isSubmitted && !flakeUrl && ( - { - console.log("submitted", { values }); - if (workflow === "create") { - setForkInProgress(true); - createFlake({ - flake_name: values.dest || "default", - url: values.flakeUrl, - }).then(() => { - setForkInProgress(false); - setAppState((s) => ({ ...s, isJoined: true })); - }); - } - })} - className="w-full max-w-2xl justify-self-center" - > - ( - Clan + {vmUuid && showLogs ? ( + setShowLogs(false)} /> + ) : ( + + { + if (workflow === "create") { + try { + await createFlake({ + flake_name: values.dest || "default", + url: values.flakeUrl, + }); + setAppState((s) => ({ ...s, isJoined: true })); + } catch (error) { + toast.error( + `Error: ${(error as AxiosError).message || ""}`, + ); } - endAdornment={ - workflow == "join" ? WorkflowAdornment : undefined + } + if (workflow === "join") { + console.log("JOINING"); + console.log(values); + try { + const response = await createVm({ + cores: values.cores, + flake_attr: values.flake_attr, + flake_url: values.flakeUrl, + graphics: values.graphics, + memory_size: values.memory_size, + }); + const { uuid } = response?.data || null; + setShowLogs(true); + setVmUuid(() => uuid); + if (response.statusText === "OK") { + toast.success(("Joined @ " + uuid) as string); + } else { + toast.error("Could not join"); + } + } catch (error) { + toast.error( + `Error: ${(error as AxiosError).message || ""}`, + ); } + } + })} + className="w-full max-w-2xl justify-self-center" + > + {workflow == "join" && ( + )} - /> - {workflow === "create" && ( - ( - Name - } - endAdornment={ - workflow == "create" ? WorkflowAdornment : undefined - } - /> - )} - /> - )} - - )} - {formState.isSubmitted && workflow == "create" && ( -
- -
- )} - {(formState.isSubmitted || flakeUrl) && workflow == "join" && ( - reset()} - flakeUrl={formState.isSubmitted ? getValues("flakeUrl") : flakeUrl} - flakeAttr={flakeAttr} - /> + {workflow == "create" && ( + + )} + +
)}