Merge pull request 'ui/install: hook up stepper store and api' (#4626) from install-ui into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4626
This commit is contained in:
@@ -11,6 +11,10 @@
|
|||||||
@apply outline outline-1 outline-inv-2
|
@apply outline outline-1 outline-inv-2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-loading] {
|
||||||
|
@apply cursor-wait;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-def-2;
|
@apply bg-def-2;
|
||||||
|
|
||||||
@@ -38,6 +42,9 @@
|
|||||||
&[data-disabled] {
|
&[data-disabled] {
|
||||||
@apply cursor-not-allowed;
|
@apply cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
&[data-loading] {
|
||||||
|
@apply bg-inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.options_content {
|
.options_content {
|
||||||
|
|||||||
@@ -47,25 +47,28 @@ export const Default: Story = {
|
|||||||
placeholder: "Select your pet",
|
placeholder: "Select your pet",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export const Async: Story = {
|
||||||
// <Field name="language">
|
args: {
|
||||||
// {(field, input) => (
|
required: true,
|
||||||
// <Select
|
label: {
|
||||||
// required
|
label: "Select your pet",
|
||||||
// label={{
|
description: "Choose your favorite pet from the list",
|
||||||
// label: "Language",
|
},
|
||||||
// description: "Select your preferred language",
|
getOptions: async () => {
|
||||||
// }}
|
return new Promise((resolve) => {
|
||||||
// options={[
|
setTimeout(() => {
|
||||||
// { value: "en", label: "English" },
|
resolve([
|
||||||
// { value: "fr", label: "Français" },
|
{ value: "dog", label: "Doggy" },
|
||||||
// ]}
|
{ value: "cat", label: "Catty" },
|
||||||
// placeholder="Language"
|
{ value: "fish", label: "Fishy" },
|
||||||
// onChange={(opt) => {
|
{ value: "bird", label: "Birdy" },
|
||||||
// setValue(formStore, "language", opt?.value || "");
|
{ value: "hamster", label: "Hammy" },
|
||||||
// }}
|
{ value: "snake", label: "Snakey" },
|
||||||
// name={field.name}
|
{ value: "turtle", label: "Turtly" },
|
||||||
// validationState={field.error ? "invalid" : "valid"}
|
]);
|
||||||
// />
|
}, 3000);
|
||||||
// )}
|
});
|
||||||
// </Field>
|
},
|
||||||
|
placeholder: "Select your pet",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import { Select as KSelect } from "@kobalte/core/select";
|
|||||||
import Icon from "../Icon/Icon";
|
import Icon from "../Icon/Icon";
|
||||||
import { Orienter } from "../Form/Orienter";
|
import { Orienter } from "../Form/Orienter";
|
||||||
import { Label, LabelProps } from "../Form/Label";
|
import { Label, LabelProps } from "../Form/Label";
|
||||||
import { createEffect, createSignal, JSX, splitProps } from "solid-js";
|
import { createEffect, createSignal, JSX, Show, splitProps } from "solid-js";
|
||||||
import styles from "./Select.module.css";
|
import styles from "./Select.module.css";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
export interface Option { value: string; label: string; disabled?: boolean }
|
export interface Option {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SelectProps {
|
export type SelectProps = {
|
||||||
// Kobalte Select props, for modular forms
|
// Kobalte Select props, for modular forms
|
||||||
name: string;
|
name: string;
|
||||||
placeholder?: string | undefined;
|
placeholder?: string | undefined;
|
||||||
options: Option[];
|
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
error: string;
|
error: string;
|
||||||
required?: boolean | undefined;
|
required?: boolean | undefined;
|
||||||
@@ -25,24 +28,57 @@ export interface SelectProps {
|
|||||||
// Custom props
|
// Custom props
|
||||||
orientation?: "horizontal" | "vertical";
|
orientation?: "horizontal" | "vertical";
|
||||||
label?: Omit<LabelProps, "labelComponent" | "descriptionComponent">;
|
label?: Omit<LabelProps, "labelComponent" | "descriptionComponent">;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
// Sync options
|
||||||
|
options: Option[];
|
||||||
|
getOptions?: never;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
// Async options
|
||||||
|
getOptions: () => Promise<Option[]>;
|
||||||
|
options?: never;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const Select = (props: SelectProps) => {
|
export const Select = (props: SelectProps) => {
|
||||||
const [root, selectProps] = splitProps(
|
const [root, selectProps] = splitProps(
|
||||||
props,
|
props,
|
||||||
["name", "placeholder", "options", "required", "disabled"],
|
["name", "placeholder", "required", "disabled"],
|
||||||
["placeholder", "ref", "onInput", "onChange", "onBlur"],
|
["placeholder", "ref", "onInput", "onChange", "onBlur"],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [getValue, setValue] = createSignal<Option>();
|
const [getValue, setValue] = createSignal<Option>();
|
||||||
|
|
||||||
|
const [resolvedOptions, setResolvedOptions] = createSignal<Option[]>([]);
|
||||||
|
|
||||||
|
// Internal loading state for async options
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
createEffect(async () => {
|
||||||
|
if (props.getOptions) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options = await props.getOptions();
|
||||||
|
setResolvedOptions(options);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} else if (props.options) {
|
||||||
|
setResolvedOptions(props.options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = () => props.options ?? resolvedOptions();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setValue(props.options.find((option) => props.value === option.value));
|
console.log("options,", options());
|
||||||
|
setValue(options().find((option) => props.value === option.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KSelect
|
<KSelect
|
||||||
{...root}
|
{...root}
|
||||||
|
options={options()}
|
||||||
sameWidth={true}
|
sameWidth={true}
|
||||||
gutter={0}
|
gutter={0}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
@@ -70,6 +106,20 @@ export const Select = (props: SelectProps) => {
|
|||||||
</KSelect.Item>
|
</KSelect.Item>
|
||||||
)}
|
)}
|
||||||
placeholder={
|
placeholder={
|
||||||
|
<Show
|
||||||
|
when={!loading()}
|
||||||
|
fallback={
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="xs"
|
||||||
|
weight="bold"
|
||||||
|
class="flex w-full items-center"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -78,6 +128,7 @@ export const Select = (props: SelectProps) => {
|
|||||||
>
|
>
|
||||||
{props.placeholder}
|
{props.placeholder}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Orienter orientation={props.orientation || "horizontal"}>
|
<Orienter orientation={props.orientation || "horizontal"}>
|
||||||
@@ -88,7 +139,10 @@ export const Select = (props: SelectProps) => {
|
|||||||
validationState={props.error ? "invalid" : "valid"}
|
validationState={props.error ? "invalid" : "valid"}
|
||||||
/>
|
/>
|
||||||
<KSelect.HiddenSelect {...selectProps} />
|
<KSelect.HiddenSelect {...selectProps} />
|
||||||
<KSelect.Trigger class={cx(styles.trigger)}>
|
<KSelect.Trigger
|
||||||
|
class={cx(styles.trigger)}
|
||||||
|
data-loading={loading() || undefined}
|
||||||
|
>
|
||||||
<KSelect.Value<Option>>
|
<KSelect.Value<Option>>
|
||||||
{(state) => (
|
{(state) => (
|
||||||
<Typography
|
<Typography
|
||||||
@@ -101,7 +155,11 @@ export const Select = (props: SelectProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</KSelect.Value>
|
</KSelect.Value>
|
||||||
<KSelect.Icon as="button" class={styles.icon}>
|
<KSelect.Icon
|
||||||
|
as="button"
|
||||||
|
class={styles.icon}
|
||||||
|
data-loading={loading() || undefined}
|
||||||
|
>
|
||||||
<Icon icon="Expand" color="inherit" />
|
<Icon icon="Expand" color="inherit" />
|
||||||
</KSelect.Icon>
|
</KSelect.Icon>
|
||||||
</KSelect.Trigger>
|
</KSelect.Trigger>
|
||||||
|
|||||||
33
pkgs/clan-app/ui/src/hooks/ApiClient.tsx
Normal file
33
pkgs/clan-app/ui/src/hooks/ApiClient.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createContext, JSX, useContext } from "solid-js";
|
||||||
|
import { ApiCall, OperationArgs, OperationNames } from "./api";
|
||||||
|
|
||||||
|
export interface ApiClient {
|
||||||
|
fetch: Fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Fetcher = <K extends OperationNames>(
|
||||||
|
method: K,
|
||||||
|
args: OperationArgs<K>,
|
||||||
|
) => ApiCall<K>;
|
||||||
|
|
||||||
|
const ApiClientContext = createContext<ApiClient>();
|
||||||
|
|
||||||
|
interface ApiClientProviderProps {
|
||||||
|
client: ApiClient;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
export const ApiClientProvider = (props: ApiClientProviderProps) => {
|
||||||
|
return (
|
||||||
|
<ApiClientContext.Provider value={props.client}>
|
||||||
|
{props.children}
|
||||||
|
</ApiClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useApiClient = () => {
|
||||||
|
const client = useContext(ApiClientContext);
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("useApiClient must be used within an ApiClientProvider");
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
};
|
||||||
@@ -49,6 +49,11 @@ export interface ApiCall<K extends OperationNames> {
|
|||||||
cancel: () => Promise<void>;
|
cancel: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not use this function directly, use `useApiClient` function instead.
|
||||||
|
* This allows mocking the result in tests.
|
||||||
|
* Or switch to different client implementations.
|
||||||
|
*/
|
||||||
export const callApi = <K extends OperationNames>(
|
export const callApi = <K extends OperationNames>(
|
||||||
method: K,
|
method: K,
|
||||||
args: OperationArgs<K>,
|
args: OperationArgs<K>,
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ export const navigateToMachine = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const clanURIParam = (params: Params) => {
|
export const clanURIParam = (params: Params) => {
|
||||||
|
try {
|
||||||
return decodeBase64(params.clanURI);
|
return decodeBase64(params.clanURI);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decode clan URI:", params.clanURI, e);
|
||||||
|
throw new Error("Invalid clan URI");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useClanURI = () => clanURIParam(useParams());
|
export const useClanURI = () => clanURIParam(useParams());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useQueries, useQuery, UseQueryResult } from "@tanstack/solid-query";
|
import { useQueries, useQuery, UseQueryResult } from "@tanstack/solid-query";
|
||||||
import { callApi, SuccessData } from "../hooks/api";
|
import { SuccessData } from "../hooks/api";
|
||||||
import { encodeBase64 } from "@/src/hooks/clan";
|
import { encodeBase64 } from "@/src/hooks/clan";
|
||||||
|
import { useApiClient } from "./ApiClient";
|
||||||
|
|
||||||
export type ClanDetails = SuccessData<"get_clan_details">;
|
export type ClanDetails = SuccessData<"get_clan_details">;
|
||||||
export type ClanDetailsWithURI = ClanDetails & { uri: string };
|
export type ClanDetailsWithURI = ClanDetails & { uri: string };
|
||||||
@@ -12,11 +13,12 @@ export type MachineDetails = SuccessData<"get_machine_details">;
|
|||||||
export type MachinesQueryResult = UseQueryResult<ListMachines>;
|
export type MachinesQueryResult = UseQueryResult<ListMachines>;
|
||||||
export type ClanListQueryResult = UseQueryResult<ClanDetailsWithURI>[];
|
export type ClanListQueryResult = UseQueryResult<ClanDetailsWithURI>[];
|
||||||
|
|
||||||
export const useMachinesQuery = (clanURI: string) =>
|
export const useMachinesQuery = (clanURI: string) => {
|
||||||
useQuery<ListMachines>(() => ({
|
const client = useApiClient();
|
||||||
|
return useQuery<ListMachines>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "machines"],
|
queryKey: ["clans", encodeBase64(clanURI), "machines"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const api = callApi("list_machines", {
|
const api = client.fetch("list_machines", {
|
||||||
flake: {
|
flake: {
|
||||||
identifier: clanURI,
|
identifier: clanURI,
|
||||||
},
|
},
|
||||||
@@ -29,12 +31,14 @@ export const useMachinesQuery = (clanURI: string) =>
|
|||||||
return result.data;
|
return result.data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const useMachineQuery = (clanURI: string, machineName: string) =>
|
export const useMachineQuery = (clanURI: string, machineName: string) => {
|
||||||
useQuery<Machine>(() => ({
|
const client = useApiClient();
|
||||||
|
return useQuery<Machine>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName],
|
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const call = callApi("get_machine", {
|
const call = client.fetch("get_machine", {
|
||||||
name: machineName,
|
name: machineName,
|
||||||
flake: {
|
flake: {
|
||||||
identifier: clanURI,
|
identifier: clanURI,
|
||||||
@@ -49,12 +53,17 @@ export const useMachineQuery = (clanURI: string, machineName: string) =>
|
|||||||
return result.data;
|
return result.data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const useMachineDetailsQuery = (clanURI: string, machineName: string) =>
|
export const useMachineDetailsQuery = (
|
||||||
useQuery<MachineDetails>(() => ({
|
clanURI: string,
|
||||||
|
machineName: string,
|
||||||
|
) => {
|
||||||
|
const client = useApiClient();
|
||||||
|
return useQuery<MachineDetails>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "machine_detail", machineName],
|
queryKey: ["clans", encodeBase64(clanURI), "machine_detail", machineName],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const call = callApi("get_machine_details", {
|
const call = client.fetch("get_machine_details", {
|
||||||
machine: {
|
machine: {
|
||||||
name: machineName,
|
name: machineName,
|
||||||
flake: {
|
flake: {
|
||||||
@@ -73,12 +82,14 @@ export const useMachineDetailsQuery = (clanURI: string, machineName: string) =>
|
|||||||
return result.data;
|
return result.data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const useClanDetailsQuery = (clanURI: string) =>
|
export const useClanDetailsQuery = (clanURI: string) => {
|
||||||
useQuery<ClanDetails>(() => ({
|
const client = useApiClient();
|
||||||
|
return useQuery<ClanDetails>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const call = callApi("get_clan_details", {
|
const call = client.fetch("get_clan_details", {
|
||||||
flake: {
|
flake: {
|
||||||
identifier: clanURI,
|
identifier: clanURI,
|
||||||
},
|
},
|
||||||
@@ -97,14 +108,16 @@ export const useClanDetailsQuery = (clanURI: string) =>
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const useClanListQuery = (clanURIs: string[]): ClanListQueryResult =>
|
export const useClanListQuery = (clanURIs: string[]): ClanListQueryResult => {
|
||||||
useQueries(() => ({
|
const client = useApiClient();
|
||||||
|
return useQueries(() => ({
|
||||||
queries: clanURIs.map((clanURI) => ({
|
queries: clanURIs.map((clanURI) => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
||||||
enabled: !!clanURI,
|
enabled: !!clanURI,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const call = callApi("get_clan_details", {
|
const call = client.fetch("get_clan_details", {
|
||||||
flake: {
|
flake: {
|
||||||
identifier: clanURI,
|
identifier: clanURI,
|
||||||
},
|
},
|
||||||
@@ -124,3 +137,54 @@ export const useClanListQuery = (clanURIs: string[]): ClanListQueryResult =>
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MachineFlashOptions = SuccessData<"get_machine_flash_options">;
|
||||||
|
export type MachineFlashOptionsQuery = UseQueryResult<MachineFlashOptions>;
|
||||||
|
|
||||||
|
export const useMachineFlashOptions = (
|
||||||
|
clanURI: string,
|
||||||
|
): MachineFlashOptionsQuery => {
|
||||||
|
const client = useApiClient();
|
||||||
|
return useQuery<MachineFlashOptions>(() => ({
|
||||||
|
queryKey: ["clans", encodeBase64(clanURI), "machine_flash_options"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const call = client.fetch("get_machine_flash_options", {
|
||||||
|
flake: {
|
||||||
|
identifier: clanURI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await call.result;
|
||||||
|
|
||||||
|
if (result.status === "error") {
|
||||||
|
// todo should we create some specific error types?
|
||||||
|
console.error("Error fetching clan details:", result.errors);
|
||||||
|
throw new Error(result.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SystemStorageOptions = SuccessData<"list_system_storage_devices">;
|
||||||
|
export type SystemStorageOptionsQuery = UseQueryResult<SystemStorageOptions>;
|
||||||
|
|
||||||
|
export const useSystemStorageOptions = (): SystemStorageOptionsQuery => {
|
||||||
|
const client = useApiClient();
|
||||||
|
return useQuery<SystemStorageOptions>(() => ({
|
||||||
|
queryKey: ["system", "storage_devices"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const call = client.fetch("list_system_storage_devices", {});
|
||||||
|
const result = await call.result;
|
||||||
|
|
||||||
|
if (result.status === "error") {
|
||||||
|
// todo should we create some specific error types?
|
||||||
|
console.error("Error fetching clan details:", result.errors);
|
||||||
|
throw new Error(result.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Setter,
|
Setter,
|
||||||
useContext,
|
useContext,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
import { createStore, SetStoreFunction, Store } from "solid-js/store";
|
||||||
|
|
||||||
export interface StepBase {
|
export interface StepBase {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,24 +14,39 @@ export interface StepBase {
|
|||||||
|
|
||||||
export type Step<ExtraFields = unknown> = StepBase & ExtraFields;
|
export type Step<ExtraFields = unknown> = StepBase & ExtraFields;
|
||||||
|
|
||||||
export interface StepOptions<Id> {
|
export interface StepOptions<Id, StoreType> {
|
||||||
initialStep: Id;
|
initialStep: Id;
|
||||||
|
initialStoreData?: StoreType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStepper<
|
export function createStepper<
|
||||||
T extends readonly Step<Extra>[],
|
T extends readonly Step<Extra>[],
|
||||||
StepId extends T[number]["id"],
|
StepId extends T[number]["id"],
|
||||||
Extra = unknown,
|
Extra = unknown,
|
||||||
>(s: { steps: T }, stepOpts: StepOptions<StepId>): StepperReturn<T> {
|
StoreType extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
>(
|
||||||
|
s: { steps: T },
|
||||||
|
stepOpts: StepOptions<StepId, StoreType>,
|
||||||
|
): StepperReturn<T, T[number]["id"]> {
|
||||||
const [activeStep, setActiveStep] = createSignal<T[number]["id"]>(
|
const [activeStep, setActiveStep] = createSignal<T[number]["id"]>(
|
||||||
stepOpts.initialStep,
|
stepOpts.initialStep,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const store: StoreTuple<StoreType> = createStore<StoreType>(
|
||||||
|
stepOpts.initialStoreData ?? ({} as StoreType),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks to manage the current step in the workflow.
|
* Hooks to manage the current step in the workflow.
|
||||||
* It provides the active step and a function to set the active step.
|
* It provides the active step and a function to set the active step.
|
||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
* Usage store = getStepStore<MyStoreType>(stepper);
|
||||||
|
*
|
||||||
|
* TODO: Getting type inference working is tricky. Might fix this later.
|
||||||
|
*/
|
||||||
|
_store: store as unknown as never,
|
||||||
activeStep,
|
activeStep,
|
||||||
setActiveStep,
|
setActiveStep,
|
||||||
currentStep: () => {
|
currentStep: () => {
|
||||||
@@ -73,10 +89,12 @@ export function createStepper<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StoreTuple<T> = [get: Store<T>, set: SetStoreFunction<T>];
|
||||||
export interface StepperReturn<
|
export interface StepperReturn<
|
||||||
T extends readonly Step[],
|
T extends readonly Step[],
|
||||||
StepId = T[number]["id"],
|
StepId = T[number]["id"],
|
||||||
> {
|
> {
|
||||||
|
_store: never;
|
||||||
activeStep: Accessor<StepId>;
|
activeStep: Accessor<StepId>;
|
||||||
setActiveStep: Setter<StepId>;
|
setActiveStep: Setter<StepId>;
|
||||||
currentStep: () => T[number];
|
currentStep: () => T[number];
|
||||||
@@ -125,3 +143,10 @@ export function StepperProvider<
|
|||||||
export function defineSteps<T extends readonly StepBase[]>(steps: T) {
|
export function defineSteps<T extends readonly StepBase[]>(steps: T) {
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface getStepStoreArg {
|
||||||
|
_store: never;
|
||||||
|
}
|
||||||
|
export function getStepStore<StoreType>(stepper: getStepStoreArg) {
|
||||||
|
return stepper._store as StoreTuple<StoreType>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { Routes } from "@/src/routes";
|
|||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
import { Layout } from "@/src/routes/Layout";
|
import { Layout } from "@/src/routes/Layout";
|
||||||
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
||||||
|
import { ApiClientProvider } from "./hooks/ApiClient";
|
||||||
|
import { callApi } from "./hooks/api";
|
||||||
|
|
||||||
export const client = new QueryClient();
|
export const client = new QueryClient();
|
||||||
|
|
||||||
@@ -23,10 +25,12 @@ if (import.meta.env.DEV) {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
|
<ApiClientProvider client={{ fetch: callApi }}>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
|
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
|
||||||
<Router root={Layout}>{Routes}</Router>
|
<Router root={Layout}>{Routes}</Router>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</ApiClientProvider>
|
||||||
),
|
),
|
||||||
root!,
|
root!,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,105 @@
|
|||||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||||
import { InstallModal } from "./install";
|
import { InstallModal } from "./install";
|
||||||
|
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";
|
||||||
|
|
||||||
const meta: Meta = {
|
type ResultDataMap = {
|
||||||
|
[K in OperationNames]: SuccessQuery<K>["data"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockFetcher: Fetcher = <K extends OperationNames>(
|
||||||
|
name: K,
|
||||||
|
_args: unknown,
|
||||||
|
): ApiCall<K> => {
|
||||||
|
// TODO: Make this configurable for every story
|
||||||
|
const resultData: Partial<ResultDataMap> = {
|
||||||
|
get_machine_flash_options: {
|
||||||
|
keymaps: ["DE_de", "US_en"],
|
||||||
|
languages: ["en", "de"],
|
||||||
|
},
|
||||||
|
get_system_file: ["id_rsa.pub"],
|
||||||
|
list_system_storage_devices: {
|
||||||
|
blockdevices: [
|
||||||
|
{
|
||||||
|
name: "sda_bla_bla",
|
||||||
|
path: "/dev/sda",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sdb_foo_foo",
|
||||||
|
path: "/dev/sdb",
|
||||||
|
},
|
||||||
|
] as SuccessQuery<"list_system_storage_devices">["data"]["blockdevices"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 InstallModal> = {
|
||||||
title: "workflows/install",
|
title: "workflows/install",
|
||||||
component: InstallModal,
|
component: InstallModal,
|
||||||
|
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;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj;
|
type Story = StoryObj<typeof InstallModal>;
|
||||||
|
|
||||||
export const Init: Story = {
|
export const Init: Story = {
|
||||||
description: "Welcome step for the installation workflow",
|
description: "Welcome step for the installation workflow",
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ const steps = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type InstallSteps = typeof steps;
|
export type InstallSteps = typeof steps;
|
||||||
|
export interface InstallStoreType {
|
||||||
|
flash: {
|
||||||
|
language: string;
|
||||||
|
keymap: string;
|
||||||
|
ssh_file: string;
|
||||||
|
device: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const InstallModal = (props: InstallModalProps) => {
|
export const InstallModal = (props: InstallModalProps) => {
|
||||||
const stepper = createStepper(
|
const stepper = createStepper(
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { defineSteps, Step, StepBase, useStepper } from "@/src/hooks/stepper";
|
import { defineSteps, useStepper } from "@/src/hooks/stepper";
|
||||||
import { InstallSteps } from "../install";
|
import { InstallSteps } from "../install";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import { Divider } from "@/src/components/Divider/Divider";
|
|
||||||
import { StepLayout } from "../../Steps";
|
import { StepLayout } from "../../Steps";
|
||||||
|
|
||||||
const ChoiceLocalOrRemote = () => {
|
const ChoiceLocalOrRemote = () => {
|
||||||
@@ -10,8 +9,8 @@ const ChoiceLocalOrRemote = () => {
|
|||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-3">
|
<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 flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||||
<div class="flex gap-2 justify-between">
|
<div class="flex justify-between gap-2">
|
||||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
<div class="flex flex-col justify-center gap-1 px-1">
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -31,8 +30,8 @@ const ChoiceLocalOrRemote = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
<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 justify-between">
|
<div class="flex justify-between gap-2">
|
||||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
<div class="flex flex-col justify-center gap-1 px-1">
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -62,8 +61,8 @@ const ChoiceLocalInstaller = () => {
|
|||||||
body={
|
body={
|
||||||
<div class="flex flex-col gap-3">
|
<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 flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||||
<div class="flex gap-2 justify-between">
|
<div class="flex justify-between gap-2">
|
||||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
<div class="flex flex-col justify-center gap-1 px-1">
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -83,8 +82,8 @@ const ChoiceLocalInstaller = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
<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 justify-between">
|
<div class="flex justify-between gap-2">
|
||||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
<div class="flex flex-col justify-center gap-1 px-1">
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useStepper } from "@/src/hooks/stepper";
|
import { getStepStore, useStepper } from "@/src/hooks/stepper";
|
||||||
import {
|
import {
|
||||||
createForm,
|
createForm,
|
||||||
getError,
|
getError,
|
||||||
@@ -6,8 +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 } from "../install";
|
import { InstallSteps, InstallStoreType } from "../install";
|
||||||
import { callApi } from "@/src/hooks/api";
|
|
||||||
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";
|
||||||
@@ -17,6 +16,12 @@ import { Alert } from "@/src/components/Alert/Alert";
|
|||||||
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import Icon from "@/src/components/Icon/Icon";
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
|
import {
|
||||||
|
useMachineFlashOptions,
|
||||||
|
useSystemStorageOptions,
|
||||||
|
} from "@/src/hooks/queries";
|
||||||
|
import { useClanURI } from "@/src/hooks/clan";
|
||||||
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
|
|
||||||
const Prose = () => (
|
const Prose = () => (
|
||||||
<StepLayout
|
<StepLayout
|
||||||
@@ -47,7 +52,7 @@ const Prose = () => (
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col px-4 gap-4">
|
<div class="flex flex-col gap-4 px-4">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<Typography hierarchy="body" size="default" weight="bold">
|
<Typography hierarchy="body" size="default" weight="bold">
|
||||||
Let's walk through it.
|
Let's walk through it.
|
||||||
@@ -101,15 +106,23 @@ const ConfigureImage = () => {
|
|||||||
});
|
});
|
||||||
const stepSignal = useStepper<InstallSteps>();
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
|
|
||||||
// TODO: push values to the parent form Store
|
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
const handleSubmit: SubmitHandler<ConfigureImageForm> = (values, event) => {
|
const handleSubmit: SubmitHandler<ConfigureImageForm> = (values, event) => {
|
||||||
console.log("ISO creation submitted", values);
|
// Push values to the store
|
||||||
// Here you would typically trigger the ISO creation process
|
set("flash", (s) => ({
|
||||||
|
...s,
|
||||||
|
language: values.language,
|
||||||
|
keymap: values.keymap,
|
||||||
|
ssh_file: values.ssh_key,
|
||||||
|
}));
|
||||||
|
|
||||||
stepSignal.next();
|
stepSignal.next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const client = useApiClient();
|
||||||
const onSelectFile = async () => {
|
const onSelectFile = async () => {
|
||||||
const req = callApi("get_system_file", {
|
const req = client.fetch("get_system_file", {
|
||||||
file_request: {
|
file_request: {
|
||||||
mode: "select_folder",
|
mode: "select_folder",
|
||||||
title: "Select a folder for you new Clan",
|
title: "Select a folder for you new Clan",
|
||||||
@@ -131,6 +144,9 @@ const ConfigureImage = () => {
|
|||||||
throw new Error("No data returned from api call");
|
throw new Error("No data returned from api call");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currClan = useClanURI();
|
||||||
|
const optionsQuery = useMachineFlashOptions(currClan);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<StepLayout
|
<StepLayout
|
||||||
@@ -168,10 +184,19 @@ const ConfigureImage = () => {
|
|||||||
label: "Language",
|
label: "Language",
|
||||||
description: "Select your preferred language",
|
description: "Select your preferred language",
|
||||||
}}
|
}}
|
||||||
options={[
|
getOptions={async () => {
|
||||||
{ value: "en", label: "English" },
|
if (!optionsQuery.data) {
|
||||||
{ value: "fr", label: "Français" },
|
await optionsQuery.refetch();
|
||||||
]}
|
}
|
||||||
|
|
||||||
|
return (optionsQuery.data?.languages ?? []).map(
|
||||||
|
(lang) => ({
|
||||||
|
// TODO: Pretty label ?
|
||||||
|
value: lang,
|
||||||
|
label: lang,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
placeholder="Language"
|
placeholder="Language"
|
||||||
name={field.name}
|
name={field.name}
|
||||||
/>
|
/>
|
||||||
@@ -188,10 +213,19 @@ const ConfigureImage = () => {
|
|||||||
label: "Keymap",
|
label: "Keymap",
|
||||||
description: "Select your keyboard layout",
|
description: "Select your keyboard layout",
|
||||||
}}
|
}}
|
||||||
options={[
|
getOptions={async () => {
|
||||||
{ value: "EN_US", label: "QWERTY" },
|
if (!optionsQuery.data) {
|
||||||
{ value: "DE_DE", label: "QWERTZ" },
|
await optionsQuery.refetch();
|
||||||
]}
|
}
|
||||||
|
|
||||||
|
return (optionsQuery.data?.keymaps ?? []).map(
|
||||||
|
(keymap) => ({
|
||||||
|
// TODO: Pretty label ?
|
||||||
|
value: keymap,
|
||||||
|
label: keymap,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
placeholder="Keymap"
|
placeholder="Keymap"
|
||||||
name={field.name}
|
name={field.name}
|
||||||
/>
|
/>
|
||||||
@@ -226,9 +260,34 @@ const ChooseDisk = () => {
|
|||||||
const [formStore, { Form, Field }] = createForm<ChooseDiskForm>({
|
const [formStore, { Form, Field }] = createForm<ChooseDiskForm>({
|
||||||
validate: valiForm(ChooseDiskSchema),
|
validate: valiForm(ChooseDiskSchema),
|
||||||
});
|
});
|
||||||
|
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
|
const client = useApiClient();
|
||||||
|
const systemStorageQuery = useSystemStorageOptions();
|
||||||
const handleSubmit: SubmitHandler<ChooseDiskForm> = (values, event) => {
|
const handleSubmit: SubmitHandler<ChooseDiskForm> = (values, event) => {
|
||||||
console.log("Disk selected", values);
|
// Just for completeness, set the disk in the store
|
||||||
|
console.log("Flashing", store.flash);
|
||||||
|
set("flash", (s) => ({
|
||||||
|
...s,
|
||||||
|
device: values.disk,
|
||||||
|
}));
|
||||||
|
const call = client.fetch("run_machine_flash", {
|
||||||
|
system_config: {
|
||||||
|
keymap: store.flash.keymap,
|
||||||
|
language: store.flash.language,
|
||||||
|
ssh_keys_path: [store.flash.ssh_file],
|
||||||
|
},
|
||||||
|
disks: [
|
||||||
|
{
|
||||||
|
name: "main",
|
||||||
|
device: values.disk,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// TOOD: Pass the "call" Promise to the "progress step"
|
||||||
|
|
||||||
|
console.log("Flashing", store.flash);
|
||||||
|
|
||||||
// Here you would typically trigger the disk selection process
|
// Here you would typically trigger the disk selection process
|
||||||
stepSignal.next();
|
stepSignal.next();
|
||||||
};
|
};
|
||||||
@@ -249,10 +308,18 @@ const ChooseDisk = () => {
|
|||||||
label: "USB Stick",
|
label: "USB Stick",
|
||||||
description: "Select the usb stick",
|
description: "Select the usb stick",
|
||||||
}}
|
}}
|
||||||
options={[
|
getOptions={async () => {
|
||||||
{ value: "1", label: "sda1" },
|
if (!systemStorageQuery.data) {
|
||||||
{ value: "2", label: "sdb2" },
|
await systemStorageQuery.refetch();
|
||||||
]}
|
}
|
||||||
|
|
||||||
|
return (systemStorageQuery.data?.blockdevices ?? []).map(
|
||||||
|
(dev) => ({
|
||||||
|
value: dev.path,
|
||||||
|
label: dev.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
placeholder="Choose Device"
|
placeholder="Choose Device"
|
||||||
name={field.name}
|
name={field.name}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ def flash_command(args: argparse.Namespace) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
run_machine_flash(
|
run_machine_flash(
|
||||||
machine,
|
machine=machine,
|
||||||
mode=opts.mode,
|
mode=opts.mode, # type: ignore
|
||||||
disks=opts.disks,
|
disks=opts.disks,
|
||||||
system_config=opts.system_config,
|
system_config=opts.system_config,
|
||||||
dry_run=opts.dry_run,
|
dry_run=opts.dry_run,
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ def find_toplevel(top_level_files: list[str]) -> Path | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def clan_core_flake() -> Path:
|
||||||
|
"""
|
||||||
|
Returns the path to the clan core flake.
|
||||||
|
"""
|
||||||
|
return module_root().parent.parent.parent
|
||||||
|
|
||||||
|
|
||||||
class TemplateType(Enum):
|
class TemplateType(Enum):
|
||||||
CLAN = "clan"
|
CLAN = "clan"
|
||||||
DISK = "disk"
|
DISK = "disk"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
from clan_cli.facts.generate import generate_facts
|
from clan_cli.facts.generate import generate_facts
|
||||||
from clan_cli.vars.generate import generate_vars
|
from clan_cli.vars.generate import generate_vars
|
||||||
@@ -12,7 +12,9 @@ from clan_cli.vars.upload import populate_secret_vars
|
|||||||
|
|
||||||
from clan_lib.api import API
|
from clan_lib.api import API
|
||||||
from clan_lib.cmd import Log, RunOpts, cmd_with_root, run
|
from clan_lib.cmd import Log, RunOpts, cmd_with_root, run
|
||||||
|
from clan_lib.dirs import clan_core_flake
|
||||||
from clan_lib.errors import ClanError
|
from clan_lib.errors import ClanError
|
||||||
|
from clan_lib.flake.flake import Flake
|
||||||
from clan_lib.machines.machines import Machine
|
from clan_lib.machines.machines import Machine
|
||||||
from clan_lib.nix import nix_shell
|
from clan_lib.nix import nix_shell
|
||||||
|
|
||||||
@@ -35,17 +37,20 @@ class Disk:
|
|||||||
device: str
|
device: str
|
||||||
|
|
||||||
|
|
||||||
|
installer_machine = Machine(name="flash-installer", flake=Flake(str(clan_core_flake())))
|
||||||
|
|
||||||
|
|
||||||
# TODO: unify this with machine install
|
# TODO: unify this with machine install
|
||||||
@API.register
|
@API.register
|
||||||
def run_machine_flash(
|
def run_machine_flash(
|
||||||
machine: Machine,
|
|
||||||
*,
|
|
||||||
mode: str,
|
|
||||||
disks: list[Disk],
|
disks: list[Disk],
|
||||||
system_config: SystemConfig,
|
system_config: SystemConfig,
|
||||||
dry_run: bool,
|
# Optional parameters
|
||||||
write_efi_boot_entries: bool,
|
machine: Machine = installer_machine,
|
||||||
debug: bool,
|
mode: Literal["format", "mount"] = "format",
|
||||||
|
dry_run: bool = False,
|
||||||
|
write_efi_boot_entries: bool = False,
|
||||||
|
debug: bool = False,
|
||||||
extra_args: list[str] | None = None,
|
extra_args: list[str] | None = None,
|
||||||
graphical: bool = False,
|
graphical: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user