Install: split steps into files
This commit is contained in:
@@ -1,58 +1,15 @@
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||
import { Modal } from "@/src/components/Modal/Modal";
|
||||
import { Select } from "@/src/components/Select/Select";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import {
|
||||
createStepper,
|
||||
StepperProvider,
|
||||
useStepper,
|
||||
} from "@/src/hooks/stepper";
|
||||
import {
|
||||
createForm,
|
||||
FieldValues,
|
||||
getError,
|
||||
SubmitHandler,
|
||||
valiForm,
|
||||
} from "@modular-forms/solid";
|
||||
import { JSX, Show } from "solid-js";
|
||||
import { createForm, FieldValues, SubmitHandler } from "@modular-forms/solid";
|
||||
import { Show } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import * as v from "valibot";
|
||||
|
||||
const CreateFlashSchema = v.object({
|
||||
ssh_key: v.pipe(
|
||||
v.string("Please select a key."),
|
||||
v.nonEmpty("Please select a key."),
|
||||
),
|
||||
language: v.pipe(v.string(), v.nonEmpty("Please choose a language.")),
|
||||
keymap: v.pipe(v.string(), v.nonEmpty("Please select a keyboard layout.")),
|
||||
});
|
||||
|
||||
export const InstallHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
<Typography hierarchy="label" size="default">
|
||||
Installing: {props.machineName}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
<div class="px-6 py-2">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="default"
|
||||
family="mono"
|
||||
weight="medium"
|
||||
>
|
||||
Create installer
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { InitialStep } from "./steps/Initial";
|
||||
import { createInstallerSteps } from "./steps/createInstaller";
|
||||
import { installSteps } from "./steps/installSteps";
|
||||
|
||||
interface InstallForm extends FieldValues {
|
||||
data_from_step_1: string;
|
||||
@@ -60,23 +17,6 @@ interface InstallForm extends FieldValues {
|
||||
data_from_step_3?: string;
|
||||
}
|
||||
|
||||
type NextButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & {};
|
||||
|
||||
const NextButton = (props: NextButtonProps) => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
hierarchy="primary"
|
||||
disabled={!stepSignal.hasNext()}
|
||||
endIcon="ArrowRight"
|
||||
{...props}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const InstallStepper = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
|
||||
@@ -98,304 +38,12 @@ const InstallStepper = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const BackButton = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
disabled={!stepSignal.hasPrevious()}
|
||||
startIcon="ArrowLeft"
|
||||
onClick={() => {
|
||||
stepSignal.previous();
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface InstallModalProps {
|
||||
machineName: string;
|
||||
initialStep?: string;
|
||||
}
|
||||
|
||||
const InitialChoice = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col gap-1 px-1">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
Remote setup
|
||||
</Typography>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xxs"
|
||||
weight="normal"
|
||||
color="secondary"
|
||||
>
|
||||
Is your machine currently online? Does it have an IP-address, can
|
||||
you SSH into it? And does it support Kexec?
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
icon="CaretRight"
|
||||
onClick={() => stepSignal.setActiveStep("install:machine-0")}
|
||||
></Button>
|
||||
</div>
|
||||
<Divider orientation="horizontal" class="bg-def-3" />
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Typography hierarchy="label" size="xs" weight="bold">
|
||||
I don't have an installer, yet
|
||||
</Typography>
|
||||
<Button
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
endIcon="Flash"
|
||||
type="button"
|
||||
onClick={() => stepSignal.setActiveStep("create:iso-0")}
|
||||
>
|
||||
Create USB Installer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type FlashFormType = v.InferInput<typeof CreateFlashSchema>;
|
||||
|
||||
const CreateIso = () => {
|
||||
const [formStore, { Form, Field }] = createForm<FlashFormType>({
|
||||
validate: valiForm(CreateFlashSchema),
|
||||
});
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
|
||||
// TODO: push values to the parent form Store
|
||||
const handleSubmit: SubmitHandler<FlashFormType> = (values, event) => {
|
||||
console.log("ISO creation submitted", values);
|
||||
// Here you would typically trigger the ISO creation process
|
||||
stepSignal.next();
|
||||
};
|
||||
|
||||
const onSelectFile = async () => {
|
||||
const req = callApi("get_system_file", {
|
||||
file_request: {
|
||||
mode: "select_folder",
|
||||
title: "Select a folder for you new Clan",
|
||||
},
|
||||
});
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
// just throw the first error, I can't imagine why there would be multiple
|
||||
// errors for this call
|
||||
throw new Error(resp.errors[0].message);
|
||||
}
|
||||
|
||||
if (resp.status === "success" && resp.data) {
|
||||
return resp.data[0];
|
||||
}
|
||||
|
||||
throw new Error("No data returned from api call");
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<StepLayout
|
||||
body={
|
||||
<div class="flex flex-col gap-2">
|
||||
<Fieldset>
|
||||
<Field name="ssh_key">
|
||||
{(field, input) => (
|
||||
<HostFileInput
|
||||
description="Public Key for connecting to the machine"
|
||||
onSelectFile={onSelectFile}
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Select directory"
|
||||
orientation="horizontal"
|
||||
placeholder="Select SSH Key"
|
||||
required={true}
|
||||
validationState={
|
||||
getError(formStore, "ssh_key") ? "invalid" : "valid"
|
||||
}
|
||||
input={input}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
<Fieldset>
|
||||
<Field name="language">
|
||||
{(field, props) => (
|
||||
<Select
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
required
|
||||
label={{
|
||||
label: "Language",
|
||||
description: "Select your preferred language",
|
||||
}}
|
||||
options={[
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "fr", label: "Français" },
|
||||
]}
|
||||
placeholder="Language"
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="keymap">
|
||||
{(field, props) => (
|
||||
<Select
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
required
|
||||
label={{
|
||||
label: "Keymap",
|
||||
description: "Select your keyboard layout",
|
||||
}}
|
||||
options={[
|
||||
{ value: "EN_US", label: "QWERTY" },
|
||||
{ value: "DE_DE", label: "QWERTZ" },
|
||||
]}
|
||||
placeholder="Keymap"
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div class="flex justify-between">
|
||||
<BackButton />
|
||||
<NextButton type="submit" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
interface StepLayoutProps {
|
||||
body: JSX.Element;
|
||||
footer: JSX.Element;
|
||||
}
|
||||
const StepLayout = (props: StepLayoutProps) => {
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
{props.body}
|
||||
{props.footer}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "init",
|
||||
content: InitialChoice,
|
||||
},
|
||||
{
|
||||
id: "create:iso-0",
|
||||
content: () => (
|
||||
<StepLayout
|
||||
body={
|
||||
<>
|
||||
<div class="flex h-36 w-full flex-col justify-center gap-3 rounded-md px-4 py-6 text-fg-inv-1 outline-2 outline-bg-def-acc-3 bg-inv-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
color="inherit"
|
||||
>
|
||||
Create a portable installer
|
||||
</Typography>
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
size="default"
|
||||
weight="bold"
|
||||
color="inherit"
|
||||
>
|
||||
Grab a disposable USB stick and plug it in
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Typography hierarchy="body" size="default" weight="bold">
|
||||
We will erase everything on it during this process
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="xs">
|
||||
Create a portable installer tool that can turn any machine into
|
||||
a fully configured Clan machine.
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={<RegularFooter />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "create:iso-1",
|
||||
title: CreateHeader,
|
||||
content: CreateIso,
|
||||
},
|
||||
{
|
||||
id: "install:machine-0",
|
||||
title: InstallHeader,
|
||||
content: () => (
|
||||
<div>
|
||||
Enter the targetHost
|
||||
<NextButton />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "install:confirm",
|
||||
title: InstallHeader,
|
||||
content: (props: { machineName: string }) => (
|
||||
<div>
|
||||
Confirm the installation of {props.machineName}
|
||||
<NextButton />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "install:progress",
|
||||
title: InstallHeader,
|
||||
content: () => (
|
||||
<div>
|
||||
<p>Installation in progress...</p>
|
||||
<p>Please wait while we set up your machine.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as const;
|
||||
|
||||
const RegularFooter = () => {
|
||||
const stepper = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex justify-between">
|
||||
<BackButton />
|
||||
<NextButton type="button" onClick={() => stepper.next()} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const steps = [InitialStep, ...createInstallerSteps, ...installSteps] as const;
|
||||
|
||||
export type InstallSteps = typeof steps;
|
||||
|
||||
|
||||
64
pkgs/clan-app/ui/src/workflows/Install/steps/Initial.tsx
Normal file
64
pkgs/clan-app/ui/src/workflows/Install/steps/Initial.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useStepper } from "@/src/hooks/stepper";
|
||||
import { InstallSteps } from "../install";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
|
||||
const InitialChoice = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col gap-1 px-1">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
Remote setup
|
||||
</Typography>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xxs"
|
||||
weight="normal"
|
||||
color="secondary"
|
||||
>
|
||||
Is your machine currently online? Does it have an IP-address, can
|
||||
you SSH into it? And does it support Kexec?
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
icon="CaretRight"
|
||||
onClick={() => stepSignal.setActiveStep("install:machine-0")}
|
||||
></Button>
|
||||
</div>
|
||||
<Divider orientation="horizontal" class="bg-def-3" />
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Typography hierarchy="label" size="xs" weight="bold">
|
||||
I don't have an installer, yet
|
||||
</Typography>
|
||||
<Button
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
endIcon="Flash"
|
||||
type="button"
|
||||
onClick={() => stepSignal.setActiveStep("create:iso-0")}
|
||||
>
|
||||
Create USB Installer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InitialStep = {
|
||||
id: "init",
|
||||
content: InitialChoice,
|
||||
};
|
||||
206
pkgs/clan-app/ui/src/workflows/Install/steps/createInstaller.tsx
Normal file
206
pkgs/clan-app/ui/src/workflows/Install/steps/createInstaller.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { useStepper } from "@/src/hooks/stepper";
|
||||
import {
|
||||
createForm,
|
||||
getError,
|
||||
SubmitHandler,
|
||||
valiForm,
|
||||
} from "@modular-forms/solid";
|
||||
import * as v from "valibot";
|
||||
import { InstallSteps } from "../install";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||
import { Select } from "@/src/components/Select/Select";
|
||||
import { BackButton, NextButton, StepFooter, StepLayout } from "../../Steps";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
const CreateHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
<div class="px-6 py-2">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="default"
|
||||
family="mono"
|
||||
weight="medium"
|
||||
>
|
||||
Create installer
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateFlashSchema = v.object({
|
||||
ssh_key: v.pipe(
|
||||
v.string("Please select a key."),
|
||||
v.nonEmpty("Please select a key."),
|
||||
),
|
||||
language: v.pipe(v.string(), v.nonEmpty("Please choose a language.")),
|
||||
keymap: v.pipe(v.string(), v.nonEmpty("Please select a keyboard layout.")),
|
||||
});
|
||||
|
||||
type FlashFormType = v.InferInput<typeof CreateFlashSchema>;
|
||||
|
||||
const CreateIso = () => {
|
||||
const [formStore, { Form, Field }] = createForm<FlashFormType>({
|
||||
validate: valiForm(CreateFlashSchema),
|
||||
});
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
|
||||
// TODO: push values to the parent form Store
|
||||
const handleSubmit: SubmitHandler<FlashFormType> = (values, event) => {
|
||||
console.log("ISO creation submitted", values);
|
||||
// Here you would typically trigger the ISO creation process
|
||||
stepSignal.next();
|
||||
};
|
||||
|
||||
const onSelectFile = async () => {
|
||||
const req = callApi("get_system_file", {
|
||||
file_request: {
|
||||
mode: "select_folder",
|
||||
title: "Select a folder for you new Clan",
|
||||
},
|
||||
});
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
// just throw the first error, I can't imagine why there would be multiple
|
||||
// errors for this call
|
||||
throw new Error(resp.errors[0].message);
|
||||
}
|
||||
|
||||
if (resp.status === "success" && resp.data) {
|
||||
return resp.data[0];
|
||||
}
|
||||
|
||||
throw new Error("No data returned from api call");
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<StepLayout
|
||||
body={
|
||||
<div class="flex flex-col gap-2">
|
||||
<Fieldset>
|
||||
<Field name="ssh_key">
|
||||
{(field, input) => (
|
||||
<HostFileInput
|
||||
description="Public Key for connecting to the machine"
|
||||
onSelectFile={onSelectFile}
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Select directory"
|
||||
orientation="horizontal"
|
||||
placeholder="Select SSH Key"
|
||||
required={true}
|
||||
validationState={
|
||||
getError(formStore, "ssh_key") ? "invalid" : "valid"
|
||||
}
|
||||
input={input}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
<Fieldset>
|
||||
<Field name="language">
|
||||
{(field, props) => (
|
||||
<Select
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
required
|
||||
label={{
|
||||
label: "Language",
|
||||
description: "Select your preferred language",
|
||||
}}
|
||||
options={[
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "fr", label: "Français" },
|
||||
]}
|
||||
placeholder="Language"
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="keymap">
|
||||
{(field, props) => (
|
||||
<Select
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
required
|
||||
label={{
|
||||
label: "Keymap",
|
||||
description: "Select your keyboard layout",
|
||||
}}
|
||||
options={[
|
||||
{ value: "EN_US", label: "QWERTY" },
|
||||
{ value: "DE_DE", label: "QWERTZ" },
|
||||
]}
|
||||
placeholder="Keymap"
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div class="flex justify-between">
|
||||
<BackButton />
|
||||
<NextButton type="submit" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const createInstallerSteps = [
|
||||
{
|
||||
id: "create:iso-0",
|
||||
content: () => (
|
||||
<StepLayout
|
||||
body={
|
||||
<>
|
||||
<div class="flex h-36 w-full flex-col justify-center gap-3 rounded-md px-4 py-6 text-fg-inv-1 outline-2 outline-bg-def-acc-3 bg-inv-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
color="inherit"
|
||||
>
|
||||
Create a portable installer
|
||||
</Typography>
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
size="default"
|
||||
weight="bold"
|
||||
color="inherit"
|
||||
>
|
||||
Grab a disposable USB stick and plug it in
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Typography hierarchy="body" size="default" weight="bold">
|
||||
We will erase everything on it during this process
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="xs">
|
||||
Create a portable installer tool that can turn any machine into
|
||||
a fully configured Clan machine.
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={<StepFooter />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "create:iso-1",
|
||||
title: CreateHeader,
|
||||
content: CreateIso,
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { NextButton } from "../../Steps";
|
||||
|
||||
export const InstallHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
<Typography hierarchy="label" size="default">
|
||||
Installing: {props.machineName}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const installSteps = [
|
||||
{
|
||||
id: "install:machine-0",
|
||||
title: InstallHeader,
|
||||
content: () => (
|
||||
<div>
|
||||
Enter the targetHost
|
||||
<NextButton />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "install:confirm",
|
||||
title: InstallHeader,
|
||||
content: (props: { machineName: string }) => (
|
||||
<div>
|
||||
Confirm the installation of {props.machineName}
|
||||
<NextButton />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "install:progress",
|
||||
title: InstallHeader,
|
||||
content: () => (
|
||||
<div>
|
||||
<p>Installation in progress...</p>
|
||||
<p>Please wait while we set up your machine.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as const;
|
||||
70
pkgs/clan-app/ui/src/workflows/Steps.tsx
Normal file
70
pkgs/clan-app/ui/src/workflows/Steps.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { JSX } from "solid-js";
|
||||
import { useStepper } from "../hooks/stepper";
|
||||
import { Button } from "../components/Button/Button";
|
||||
import { InstallSteps } from "./Install/install";
|
||||
|
||||
interface StepLayoutProps {
|
||||
body: JSX.Element;
|
||||
footer: JSX.Element;
|
||||
}
|
||||
export const StepLayout = (props: StepLayoutProps) => {
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
{props.body}
|
||||
{props.footer}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type NextButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & {};
|
||||
|
||||
export const NextButton = (props: NextButtonProps) => {
|
||||
// TODO: Make this type generic
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
hierarchy="primary"
|
||||
disabled={!stepSignal.hasNext()}
|
||||
endIcon="ArrowRight"
|
||||
{...props}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const BackButton = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
disabled={!stepSignal.hasPrevious()}
|
||||
startIcon="ArrowLeft"
|
||||
onClick={() => {
|
||||
stepSignal.previous();
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a footer with Back and Next buttons.
|
||||
* The Next button will trigger the next step in the stepper.
|
||||
* The Back button will go to the previous step.
|
||||
*
|
||||
* Does not trigger submission on any form
|
||||
*
|
||||
* Use this for overview steps where no form submission is required.
|
||||
*/
|
||||
export const StepFooter = () => {
|
||||
const stepper = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex justify-between">
|
||||
<BackButton />
|
||||
<NextButton type="button" onClick={() => stepper.next()} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user