ui/update: integrate with api
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||
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";
|
||||
import { UpdateModal } from "./UpdateMachine";
|
||||
|
||||
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> = {
|
||||
get_generators: [
|
||||
{
|
||||
name: "funny.gritty",
|
||||
prompts: [
|
||||
{
|
||||
name: "gritty.name",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(1) Name",
|
||||
group: "User",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gritty.foo",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(2) Password",
|
||||
group: "Root",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gritty.bar",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(3) Gritty",
|
||||
group: "Root",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "funny.dodo",
|
||||
prompts: [
|
||||
{
|
||||
name: "gritty.name",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(4) Name",
|
||||
group: "User",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gritty.foo",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(5) Password",
|
||||
group: "Lonely",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gritty.bar",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(6) Batty",
|
||||
group: "Root",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
run_generators: null,
|
||||
run_machine_update: null,
|
||||
};
|
||||
|
||||
return {
|
||||
uuid: "mock",
|
||||
cancel: () => Promise.resolve(),
|
||||
result: new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const status = name === "run_machine_update" ? "error" : "success";
|
||||
|
||||
resolve({
|
||||
op_key: "1",
|
||||
status: status,
|
||||
errors: [
|
||||
{
|
||||
message: "Mock error message",
|
||||
description:
|
||||
"This is a more detailed description of the mock error.",
|
||||
},
|
||||
],
|
||||
data: resultData[name],
|
||||
} as OperationResponse<K>);
|
||||
}, 1500);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const meta: Meta<typeof UpdateModal> = {
|
||||
title: "workflows/update",
|
||||
component: UpdateModal,
|
||||
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 UpdateModal>;
|
||||
|
||||
export const Init: Story = {
|
||||
description: "Welcome step for the update workflow",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Jon",
|
||||
},
|
||||
};
|
||||
export const Address: Story = {
|
||||
description: "Welcome step for the update workflow",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Jon",
|
||||
initialStep: "update:address",
|
||||
},
|
||||
};
|
||||
export const UpdateProgress: Story = {
|
||||
description: "Welcome step for the update workflow",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Jon",
|
||||
initialStep: "update:progress",
|
||||
},
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
StepperProvider,
|
||||
useStepper,
|
||||
} from "@/src/hooks/stepper";
|
||||
import { Show } from "solid-js";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import { ConfigureAddress, ConfigureData } from "./steps/installSteps";
|
||||
|
||||
@@ -16,6 +16,9 @@ import { Button } from "@/src/components/Button/Button";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { useClanURI } from "@/src/hooks/clan";
|
||||
import { AlertProps } from "@/src/components/Alert/Alert";
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface UpdateStepperProps {
|
||||
@@ -24,21 +27,77 @@ interface UpdateStepperProps {
|
||||
const UpdateStepper = (props: UpdateStepperProps) => {
|
||||
const stepSignal = useStepper<UpdateSteps>();
|
||||
|
||||
const [store, _set] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
const [alert, setAlert] = createSignal<AlertProps>();
|
||||
|
||||
const clanURI = useClanURI();
|
||||
|
||||
const client = useApiClient();
|
||||
const handleUpdate = async () => {
|
||||
console.log("Starting update for", store.install.machineName);
|
||||
|
||||
if (!store.install.targetHost) {
|
||||
console.error("No target host specified, API requires it");
|
||||
return;
|
||||
}
|
||||
|
||||
const port = store.install.port
|
||||
? parseInt(store.install.port, 10)
|
||||
: undefined;
|
||||
|
||||
const call = client.fetch("run_machine_update", {
|
||||
machine: {
|
||||
flake: { identifier: clanURI },
|
||||
name: store.install.machineName,
|
||||
},
|
||||
build_host: null,
|
||||
target_host: {
|
||||
address: store.install.targetHost,
|
||||
port,
|
||||
password: store.install.password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
UserKnownHostsFile: "/dev/null",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await call.result;
|
||||
|
||||
if (result.status === "error") {
|
||||
console.error("Update failed", result.errors);
|
||||
setAlert(() => ({
|
||||
type: "error",
|
||||
title: "Update failed",
|
||||
description: result.errors[0].message,
|
||||
}));
|
||||
stepSignal.previous();
|
||||
return;
|
||||
}
|
||||
if (result.status === "success") {
|
||||
stepSignal.next();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
component={stepSignal.currentStep().content}
|
||||
onDone={props.onDone}
|
||||
next="update"
|
||||
stepFinished={handleUpdate}
|
||||
alert={alert()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface UpdateModalProps {
|
||||
machineName: string;
|
||||
open: boolean;
|
||||
initialStep?: UpdateSteps[number]["id"];
|
||||
mount?: Node;
|
||||
onClose?: () => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export const UpdateHeader = (props: { machineName: string }) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
PromptValues,
|
||||
} from "../InstallMachine";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
import { Alert, AlertProps } from "@/src/components/Alert/Alert";
|
||||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Orienter } from "@/src/components/Form/Orienter";
|
||||
@@ -60,7 +60,11 @@ const ConfigureAdressSchema = v.object({
|
||||
|
||||
type ConfigureAdressForm = v.InferInput<typeof ConfigureAdressSchema>;
|
||||
|
||||
export const ConfigureAddress = (props: { next?: string }) => {
|
||||
export const ConfigureAddress = (props: {
|
||||
next?: string;
|
||||
stepFinished: () => void;
|
||||
alert?: AlertProps;
|
||||
}) => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
@@ -89,8 +93,8 @@ export const ConfigureAddress = (props: { next?: string }) => {
|
||||
password: values.password,
|
||||
}));
|
||||
|
||||
// Here you would typically trigger the ISO creation process
|
||||
stepSignal.next();
|
||||
props.stepFinished?.();
|
||||
};
|
||||
|
||||
const tryReachable = async () => {
|
||||
@@ -129,6 +133,7 @@ export const ConfigureAddress = (props: { next?: string }) => {
|
||||
<StepLayout
|
||||
body={
|
||||
<div class="flex flex-col gap-2">
|
||||
<Show when={props.alert}>{(alert) => <Alert {...alert()} />}</Show>
|
||||
<Fieldset>
|
||||
<Field name="targetHost">
|
||||
{(field, props) => (
|
||||
@@ -256,7 +261,7 @@ const CheckHardware = () => {
|
||||
const call = client.fetch("run_machine_hardware_info", {
|
||||
target_host: {
|
||||
address: store.install.targetHost,
|
||||
...(port && { port }),
|
||||
port,
|
||||
password: store.install.password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
@@ -706,7 +711,7 @@ const InstallSummary = () => {
|
||||
},
|
||||
target_host: {
|
||||
address: store.install.targetHost,
|
||||
...(port && { port }),
|
||||
port,
|
||||
password: store.install.password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
|
||||
Reference in New Issue
Block a user