Merge pull request 'ui/implement-add-machine-workflow' (#5021) from ui/implement-add-machine-workflow into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5021
This commit is contained in:
hsjobeki
2025-08-29 08:42:31 +00:00
17 changed files with 677 additions and 116 deletions

View File

@@ -1,6 +1,6 @@
import { createSignal, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
import { Button } from "@/src/components/Button/Button"; import { Button } from "@/src/components/Button/Button";
import { InstallModal } from "@/src/workflows/Install/install"; import { InstallModal } from "@/src/workflows/InstallMachine/InstallMachine";
import { useMachineName } from "@/src/hooks/clan"; import { useMachineName } from "@/src/hooks/clan";
import { useMachineStateQuery } from "@/src/hooks/queries"; import { useMachineStateQuery } from "@/src/hooks/queries";
import styles from "./SidebarSectionInstall.module.css"; import styles from "./SidebarSectionInstall.module.css";

View File

@@ -24,16 +24,11 @@ import {
useClanListQuery, useClanListQuery,
useMachinesQuery, useMachinesQuery,
} from "@/src/hooks/queries"; } from "@/src/hooks/queries";
import { callApi } from "@/src/hooks/api";
import { clanURIs, setStore, store } from "@/src/stores/clan"; import { clanURIs, setStore, store } from "@/src/stores/clan";
import { produce } from "solid-js/store"; import { produce } from "solid-js/store";
import { Button } from "@/src/components/Button/Button";
import { Splash } from "@/src/scene/splash"; import { Splash } from "@/src/scene/splash";
import cx from "classnames"; import cx from "classnames";
import styles from "./Clan.module.css"; import styles from "./Clan.module.css";
import { Modal } from "@/src/components/Modal/Modal";
import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid";
import { Sidebar } from "@/src/components/Sidebar/Sidebar"; import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { UseQueryResult } from "@tanstack/solid-query"; import { UseQueryResult } from "@tanstack/solid-query";
import { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal"; import { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal";
@@ -43,6 +38,7 @@ import {
} from "@/src/workflows/Service/Service"; } from "@/src/workflows/Service/Service";
import { useApiClient } from "@/src/hooks/ApiClient"; import { useApiClient } from "@/src/hooks/ApiClient";
import toast from "solid-toast"; import toast from "solid-toast";
import { AddMachine } from "@/src/workflows/AddMachine/AddMachine";
interface ClanContextProps { interface ClanContextProps {
clanURI: string; clanURI: string;
@@ -134,56 +130,6 @@ export const Clan: Component<RouteSectionProps> = (props) => {
); );
}; };
interface CreateFormValues extends FieldValues {
name: string;
}
interface MockProps {
onClose: () => void;
onSubmit: (formValues: CreateFormValues) => void;
}
const MockCreateMachine = (props: MockProps) => {
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
return (
<Modal
open={true}
onClose={() => {
reset(form);
props.onClose();
}}
class={cx(styles.createModal)}
title="Create Machine"
>
<Form class="flex flex-col" onSubmit={props.onSubmit}>
<Field name="name">
{(field, props) => (
<>
<TextInput
{...field}
label="Name"
size="s"
required={true}
input={{ ...props, placeholder: "name", autofocus: true }}
/>
</>
)}
</Field>
<div class="mt-4 flex w-full items-center justify-end gap-4">
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
Cancel
</Button>
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
Create
</Button>
</div>
</Form>
</Modal>
);
};
const ClanSceneController = (props: RouteSectionProps) => { const ClanSceneController = (props: RouteSectionProps) => {
const ctx = useContext(ClanContext); const ctx = useContext(ClanContext);
if (!ctx) { if (!ctx) {
@@ -194,7 +140,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
const [showService, setShowService] = createSignal(false); const [showService, setShowService] = createSignal(false);
const [showModal, setShowModal] = createSignal(false); const [showCreate, setShowCreate] = createSignal(false);
const [currentPromise, setCurrentPromise] = createSignal<{ const [currentPromise, setCurrentPromise] = createSignal<{
resolve: ({ id }: { id: string }) => void; resolve: ({ id }: { id: string }) => void;
reject: (err: unknown) => void; reject: (err: unknown) => void;
@@ -202,45 +148,11 @@ const ClanSceneController = (props: RouteSectionProps) => {
const onCreate = async (): Promise<{ id: string }> => { const onCreate = async (): Promise<{ id: string }> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setShowModal(true); setShowCreate(true);
setCurrentPromise({ resolve, reject }); setCurrentPromise({ resolve, reject });
}); });
}; };
const onAddService = async (): Promise<{ id: string }> => {
return new Promise((resolve, reject) => {
setShowService((v) => !v);
console.log("setting current promise");
setCurrentPromise({ resolve, reject });
});
};
const sendCreate = async (values: CreateFormValues) => {
const api = callApi("create_machine", {
opts: {
clan_dir: {
identifier: ctx.clanURI,
},
machine: {
name: values.name,
},
},
});
const res = await api.result;
if (res.status === "error") {
// TODO: Handle displaying errors
console.error("Error creating machine:");
// Important: rejects the promise
throw new Error(res.errors[0].message);
}
// trigger a refetch of the machines query
ctx.machinesQuery.refetch();
return { id: values.name };
};
const [loadingError, setLoadingError] = createSignal< const [loadingError, setLoadingError] = createSignal<
{ title: string; description: string } | undefined { title: string; description: string } | undefined
>(); >();
@@ -312,8 +224,6 @@ const ClanSceneController = (props: RouteSectionProps) => {
console.error("Error creating service instance", result.errors); console.error("Error creating service instance", result.errors);
} }
toast.success("Created"); toast.success("Created");
//
currentPromise()?.resolve({ id: "0" });
setShowService(false); setShowService(false);
}; };
@@ -322,7 +232,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
if (mode === "service") { if (mode === "service") {
setShowService(true); setShowService(true);
} else { } else {
// todo: request close instead of force close // TODO: request soft close instead of forced close
setShowService(false); setShowService(false);
} }
}), }),
@@ -333,22 +243,19 @@ const ClanSceneController = (props: RouteSectionProps) => {
<Show when={loadingError()}> <Show when={loadingError()}>
<ListClansModal error={loadingError()} /> <ListClansModal error={loadingError()} />
</Show> </Show>
<Show when={showModal()}> <Show when={showCreate()}>
<MockCreateMachine <AddMachine
onClose={() => { onCreated={async (id) => {
setShowModal(false); const promise = currentPromise();
currentPromise()?.reject(new Error("User cancelled")); if (promise) {
}} await ctx.machinesQuery.refetch();
onSubmit={async (values) => { promise.resolve({ id });
try { setCurrentPromise(null);
const result = await sendCreate(values);
currentPromise()?.resolve(result);
setShowModal(false);
} catch (err) {
currentPromise()?.reject(err);
setShowModal(false);
} }
}} }}
onClose={() => {
setShowCreate(false);
}}
/> />
</Show> </Show>
<div <div

View File

@@ -39,10 +39,10 @@ export class MachineManager {
const actualIds = Object.keys(machinesQueryResult.data); const actualIds = Object.keys(machinesQueryResult.data);
const machinePositions = machinePositionsSignal(); const machinePositions = machinePositionsSignal();
// Remove stale // Remove stale
for (const id of Object.keys(machinePositions)) { for (const id of Object.keys(machinePositions)) {
if (!actualIds.includes(id)) { if (!actualIds.includes(id)) {
console.log("Removing stale machine", id);
setMachinePos(id, null); setMachinePos(id, null);
} }
} }
@@ -61,10 +61,11 @@ export class MachineManager {
// //
createEffect(() => { createEffect(() => {
const positions = machinePositionsSignal(); const positions = machinePositionsSignal();
if (!positions) return;
// Remove machines from scene // Remove machines from scene
for (const [id, repr] of this.machines) { for (const [id, repr] of this.machines) {
if (!(id in positions)) { if (!Object.keys(positions).includes(id)) {
repr.dispose(scene); repr.dispose(scene);
this.machines.delete(id); this.machines.delete(id);
} }

View File

@@ -0,0 +1,119 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { AddMachine } from "@/src/workflows/AddMachine/AddMachine";
import {
createMemoryHistory,
MemoryRouter,
RouteDefinition,
} from "@solidjs/router";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { ApiClientProvider, Fetcher } from "@/src/hooks/ApiClient";
import {
ApiCall,
OperationNames,
OperationResponse,
SuccessQuery,
} from "@/src/hooks/api";
type ResultDataMap = {
[K in OperationNames]: SuccessQuery<K>["data"];
};
const mockFetcher: Fetcher = <K extends OperationNames>(
name: K,
_args: unknown,
): ApiCall<K> => {
// TODO: Make this configurable for every story
const resultData: Partial<ResultDataMap> = {
list_machines: {
pandora: {
name: "pandora",
},
enceladus: {
name: "enceladus",
},
dione: {
name: "dione",
},
},
};
return {
uuid: "mock",
cancel: () => Promise.resolve(),
result: new Promise((resolve) => {
setTimeout(() => {
resolve({
op_key: "1",
status: "success",
data: resultData[name],
} as OperationResponse<K>);
}, 1500);
}),
};
};
const meta: Meta<typeof AddMachine> = {
title: "workflows/add-machine",
component: AddMachine,
decorators: [
(Story: StoryObj, context: StoryContext) => {
const Routes: RouteDefinition[] = [
{
path: "/clans/:clanURI",
component: () => (
<div class="w-[600px]">
<Story />
</div>
),
},
];
const history = createMemoryHistory();
history.set({ value: "/clans/dGVzdA==", replace: true });
const queryClient = new QueryClient();
return (
<ApiClientProvider client={{ fetch: mockFetcher }}>
<QueryClientProvider client={queryClient}>
<MemoryRouter
root={(props) => {
console.debug("Rendering MemoryRouter root with props:", props);
return props.children;
}}
history={history}
>
{Routes}
</MemoryRouter>
</QueryClientProvider>
</ApiClientProvider>
);
},
],
};
export default meta;
type Story = StoryObj<typeof AddMachine>;
export const General: Story = {
args: {},
};
export const Host: Story = {
args: {
initialStep: "host",
},
};
export const Tags: Story = {
args: {
initialStep: "tags",
},
};
export const Progress: Story = {
args: {
initialStep: "progress",
},
};

View File

@@ -0,0 +1,136 @@
import {
createStepper,
defineSteps,
StepperProvider,
useStepper,
} from "@/src/hooks/stepper";
import {
GeneralForm,
StepGeneral,
} from "@/src/workflows/AddMachine/StepGeneral";
import { Modal } from "@/src/components/Modal/Modal";
import cx from "classnames";
import { Dynamic } from "solid-js/web";
import { Show } from "solid-js";
import { Typography } from "@/src/components/Typography/Typography";
import { StepHost } from "@/src/workflows/AddMachine/StepHost";
import { StepTags } from "@/src/workflows/AddMachine/StepTags";
import { StepProgress } from "./StepProgress";
interface AddMachineStepperProps {
onDone: () => void;
}
const AddMachineStepper = (props: AddMachineStepperProps) => {
const stepSignal = useStepper<AddMachineSteps>();
return (
<Dynamic
component={stepSignal.currentStep().content}
onDone={props.onDone}
/>
);
};
export interface AddMachineProps {
onClose: () => void;
onCreated: (id: string) => void;
initialStep?: AddMachineSteps[number]["id"];
}
export interface AddMachineStoreType {
general: GeneralForm;
deploy: {
targetHost: string;
};
tags: {
tags: string[];
};
onCreated: (id: string) => void;
error?: string;
}
const steps = defineSteps([
{
id: "general",
title: "General",
content: StepGeneral,
},
{
id: "host",
title: "Host",
content: StepHost,
},
{
id: "tags",
title: "Tags",
content: StepTags,
},
{
id: "progress",
title: "Creating...",
content: StepProgress,
isSplash: true,
},
] as const);
export type AddMachineSteps = typeof steps;
export const AddMachine = (props: AddMachineProps) => {
const stepper = createStepper(
{
steps,
},
{
initialStep: props.initialStep || "general",
initialStoreData: { onCreated: props.onCreated },
},
);
const MetaHeader = () => {
const title = stepper.currentStep().title;
return (
<Show when={title}>
<Typography
hierarchy="label"
family="mono"
size="default"
weight="medium"
>
{title}
</Typography>
</Show>
);
};
const sizeClasses = () => {
const defaultClass = "max-w-3xl h-fit";
const currentStep = stepper.currentStep();
if (!currentStep) {
return defaultClass;
}
switch (currentStep.id) {
default:
return defaultClass;
}
};
return (
<StepperProvider stepper={stepper}>
<Modal
class={cx("w-screen", sizeClasses())}
title="Add Machine"
onClose={props.onClose}
open={true}
// @ts-expect-error some steps might not have
metaHeader={stepper.currentStep()?.title ? <MetaHeader /> : undefined}
// @ts-expect-error some steps might not have
disablePadding={stepper.currentStep()?.isSplash}
>
<AddMachineStepper onDone={() => props.onClose()} />
</Modal>
</StepperProvider>
);
};

View File

@@ -0,0 +1,176 @@
import { NextButton, StepLayout } from "@/src/workflows/Steps";
import * as v from "valibot";
import { getStepStore, useStepper } from "@/src/hooks/stepper";
import {
clearError,
createForm,
FieldValues,
getError,
getErrors,
setError,
SubmitHandler,
valiForm,
} from "@modular-forms/solid";
import {
AddMachineSteps,
AddMachineStoreType,
} from "@/src/workflows/AddMachine/AddMachine";
import { Fieldset } from "@/src/components/Form/Fieldset";
import { TextInput } from "@/src/components/Form/TextInput";
import { Divider } from "@/src/components/Divider/Divider";
import { TextArea } from "@/src/components/Form/TextArea";
import { Select } from "@/src/components/Select/Select";
import { Show } from "solid-js";
import { Alert } from "@/src/components/Alert/Alert";
import { useMachinesQuery } from "@/src/hooks/queries";
import { useClanURI } from "@/src/hooks/clan";
const PlatformOptions = [
{ label: "NixOS", value: "nixos" },
{ label: "Darwin", value: "darwin" },
];
const GeneralSchema = v.object({
name: v.pipe(
v.string("Name must be a string"),
v.nonEmpty("Please enter a machine name"),
v.regex(
new RegExp(/^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$/),
"Name must be a valid hostname e.g. alphanumeric characters and - only",
),
),
description: v.optional(v.string("Description must be a string")),
machineClass: v.pipe(v.string(), v.nonEmpty()),
});
export interface GeneralForm extends FieldValues {
machineClass: "nixos" | "darwin";
name: string;
description?: string;
}
export const StepGeneral = () => {
const stepSignal = useStepper<AddMachineSteps>();
const [store, set] = getStepStore<AddMachineStoreType>(stepSignal);
const clanURI = useClanURI();
const machines = useMachinesQuery(clanURI);
const machineNames = () => {
if (!machines.isSuccess) {
return [];
}
return Object.keys(machines.data || {});
};
const [formStore, { Form, Field }] = createForm<GeneralForm>({
validate: valiForm(GeneralSchema),
initialValues: { ...store.general, machineClass: "nixos" },
});
const handleSubmit: SubmitHandler<GeneralForm> = (values, event) => {
if (machineNames().includes(values.name)) {
setError(
formStore,
"name",
`A machine named '${values.name}' already exists. Please choose a different one.`,
);
return;
}
clearError(formStore, "name");
set("general", (s) => ({
...s,
...values,
}));
stepSignal.next();
};
const formError = () => {
const errors = getErrors(formStore);
return errors.name || errors.description || errors.machineClass;
};
return (
<Form onSubmit={handleSubmit} class="h-full">
<StepLayout
body={
<div class="flex flex-col gap-2">
<Show when={formError()}>
<Alert
type="error"
icon="WarningFilled"
title="Error"
description={formError()}
/>
</Show>
<Fieldset>
<Field name="name">
{(field, input) => (
<TextInput
{...field}
value={field.value}
label="Name"
required
orientation="horizontal"
input={{
...input,
placeholder: "A unique machine name.",
}}
validationState={
getError(formStore, "name") ? "invalid" : "valid"
}
/>
)}
</Field>
<Divider />
<Field name="description">
{(field, input) => (
<TextArea
{...field}
value={field.value}
label="Description"
orientation="horizontal"
input={{
...input,
placeholder: "A short description of the machine.",
}}
validationState={
getError(formStore, "description") ? "invalid" : "valid"
}
/>
)}
</Field>
</Fieldset>
<Fieldset>
<Field name="machineClass">
{(field, props) => (
<Select
zIndex={100}
{...props}
value={field.value}
error={field.error}
required
label={{
label: "Platform",
}}
options={PlatformOptions}
name={field.name}
/>
)}
</Field>
</Fieldset>
</div>
}
footer={
<div class="flex justify-end">
<NextButton type="submit" />
</div>
}
/>
</Form>
);
};

View File

@@ -0,0 +1,76 @@
import { BackButton, NextButton, StepLayout } from "@/src/workflows/Steps";
import * as v from "valibot";
import { getStepStore, useStepper } from "@/src/hooks/stepper";
import {
createForm,
getError,
SubmitHandler,
valiForm,
} from "@modular-forms/solid";
import {
AddMachineSteps,
AddMachineStoreType,
} from "@/src/workflows/AddMachine/AddMachine";
import { Fieldset } from "@/src/components/Form/Fieldset";
import { TextInput } from "@/src/components/Form/TextInput";
const HostSchema = v.object({
targetHost: v.pipe(v.string("Name must be a string")),
});
type HostForm = v.InferInput<typeof HostSchema>;
export const StepHost = () => {
const stepSignal = useStepper<AddMachineSteps>();
const [store, set] = getStepStore<AddMachineStoreType>(stepSignal);
const [formStore, { Form, Field }] = createForm<HostForm>({
validate: valiForm(HostSchema),
initialValues: store.deploy,
});
const handleSubmit: SubmitHandler<HostForm> = (values, event) => {
set("deploy", (s) => ({
...s,
...values,
}));
stepSignal.next();
};
return (
<Form onSubmit={handleSubmit} class="h-full">
<StepLayout
body={
<div class="flex flex-col gap-2">
<Fieldset>
<Field name="targetHost">
{(field, input) => (
<TextInput
{...field}
value={field.value}
label="Target"
orientation="horizontal"
input={{
...input,
placeholder: "root@flashinstaller.local",
}}
validationState={
getError(formStore, "targetHost") ? "invalid" : "valid"
}
/>
)}
</Field>
</Fieldset>
</div>
}
footer={
<div class="flex justify-between">
<BackButton />
<NextButton type="submit" />
</div>
}
/>
</Form>
);
};

View File

@@ -0,0 +1,40 @@
import { getStepStore, useStepper } from "@/src/hooks/stepper";
import {
AddMachineSteps,
AddMachineStoreType,
} from "@/src/workflows/AddMachine/AddMachine";
import { Loader } from "@/src/components/Loader/Loader";
import { Typography } from "@/src/components/Typography/Typography";
import { Show } from "solid-js";
import { Alert } from "@/src/components/Alert/Alert";
export interface StepProgressProps {
onDone: () => void;
}
export const StepProgress = (props: StepProgressProps) => {
const stepSignal = useStepper<AddMachineSteps>();
const [store, set] = getStepStore<AddMachineStoreType>(stepSignal);
return (
<div class="flex flex-col items-center justify-center gap-2.5 px-6 pb-7 pt-4">
<Show
when={store.error}
fallback={
<>
<Loader class="size-8" />
<Typography hierarchy="body" size="s" weight="medium" family="mono">
{store.general?.name} is being created
</Typography>
</>
}
>
<Alert
type="error"
title="There was an error"
description={store.error}
/>
</Show>
</div>
);
};

View File

@@ -0,0 +1,102 @@
import { BackButton, StepLayout } from "@/src/workflows/Steps";
import * as v from "valibot";
import { getStepStore, useStepper } from "@/src/hooks/stepper";
import { createForm, SubmitHandler, valiForm } from "@modular-forms/solid";
import {
AddMachineSteps,
AddMachineStoreType,
} from "@/src/workflows/AddMachine/AddMachine";
import { Fieldset } from "@/src/components/Form/Fieldset";
import { MachineTags } from "@/src/components/Form/MachineTags";
import { Button } from "@/src/components/Button/Button";
import { useApiClient } from "@/src/hooks/ApiClient";
import { useClanURI } from "@/src/hooks/clan";
const TagsSchema = v.object({
tags: v.array(v.string()),
});
type TagsForm = v.InferInput<typeof TagsSchema>;
export const StepTags = (props: { onDone: () => void }) => {
const stepSignal = useStepper<AddMachineSteps>();
const [store, set] = getStepStore<AddMachineStoreType>(stepSignal);
const [formStore, { Form, Field }] = createForm<TagsForm>({
validate: valiForm(TagsSchema),
initialValues: store.tags,
});
const apiClient = useApiClient();
const clanURI = useClanURI();
const handleSubmit: SubmitHandler<TagsForm> = async (values, event) => {
set("tags", (s) => ({
...s,
...values,
}));
const call = apiClient.fetch("create_machine", {
opts: {
clan_dir: {
identifier: clanURI,
},
machine: {
...store.general,
...store.tags,
deploy: store.deploy,
},
},
});
stepSignal.next();
const result = await call.result;
if (result.status == "error") {
// setError(result.errors[0].message);
}
if (result.status == "success") {
console.log("Machine creation was successful");
if (store.general) {
store.onCreated(store.general.name);
}
}
console.log("Done creating machine");
props.onDone();
};
return (
<Form onSubmit={handleSubmit} class="h-full">
<StepLayout
body={
<div class="flex flex-col gap-2">
<Fieldset>
<Field name="tags" type="string[]">
{(field, input) => (
<MachineTags
{...field}
required
orientation="horizontal"
defaultValue={field.value}
defaultOptions={[]}
input={input}
/>
)}
</Field>
</Fieldset>
</div>
}
footer={
<div class="flex justify-between">
<BackButton />
<Button hierarchy="primary" type="submit" endIcon="Flash">
Create Machine
</Button>
</div>
}
/>
</Form>
);
};

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { InstallModal } from "./install"; import { InstallModal } from "./InstallMachine";
import { import {
createMemoryHistory, createMemoryHistory,
MemoryRouter, MemoryRouter,

View File

@@ -1,5 +1,5 @@
import { defineSteps, useStepper } from "@/src/hooks/stepper"; import { defineSteps, useStepper } from "@/src/hooks/stepper";
import { InstallSteps } from "../install"; import { InstallSteps } from "../InstallMachine";
import { Button } from "@/src/components/Button/Button"; import { Button } from "@/src/components/Button/Button";
import { StepLayout } from "../../Steps"; import { StepLayout } from "../../Steps";
import { NavSection } from "@/src/components/NavSection/NavSection"; import { NavSection } from "@/src/components/NavSection/NavSection";

View File

@@ -6,7 +6,7 @@ import {
valiForm, valiForm,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import * as v from "valibot"; import * as v from "valibot";
import { InstallSteps, InstallStoreType } from "../install"; import { InstallSteps, InstallStoreType } from "../InstallMachine";
import { Fieldset } from "@/src/components/Form/Fieldset"; import { Fieldset } from "@/src/components/Form/Fieldset";
import { HostFileInput } from "@/src/components/Form/HostFileInput"; import { HostFileInput } from "@/src/components/Form/HostFileInput";
import { Select } from "@/src/components/Select/Select"; import { Select } from "@/src/components/Select/Select";

View File

@@ -11,7 +11,11 @@ import {
import { Fieldset } from "@/src/components/Form/Fieldset"; import { Fieldset } from "@/src/components/Form/Fieldset";
import * as v from "valibot"; import * as v from "valibot";
import { getStepStore, useStepper } from "@/src/hooks/stepper"; import { getStepStore, useStepper } from "@/src/hooks/stepper";
import { InstallSteps, InstallStoreType, PromptValues } from "../install"; import {
InstallSteps,
InstallStoreType,
PromptValues,
} from "../InstallMachine";
import { TextInput } from "@/src/components/Form/TextInput"; import { TextInput } from "@/src/components/Form/TextInput";
import { Alert } from "@/src/components/Alert/Alert"; import { Alert } from "@/src/components/Alert/Alert";
import { createSignal, For, Match, Show, Switch } from "solid-js"; import { createSignal, For, Match, Show, Switch } from "solid-js";

View File

@@ -1,7 +1,7 @@
import { JSX } from "solid-js"; import { JSX } from "solid-js";
import { useStepper } from "../hooks/stepper"; import { useStepper } from "../hooks/stepper";
import { Button, ButtonProps } from "../components/Button/Button"; import { Button, ButtonProps } from "../components/Button/Button";
import { InstallSteps } from "./Install/install"; import { InstallSteps } from "@/src/workflows/InstallMachine/InstallMachine";
import styles from "./Steps.module.css"; import styles from "./Steps.module.css";
interface StepLayoutProps { interface StepLayoutProps {