ui/services: workflow select service

This commit is contained in:
Johannes Kirschbauer
2025-08-26 17:14:21 +02:00
parent 53e16242b9
commit 602879c9e4
2 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { ServiceWorkflow } from "./Service";
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_service_modules: [
{
module: { name: "Module A", input: "Input A" },
info: {
manifest: {
name: "Module A",
description: "This is module A",
},
roles: {
peer: null,
server: null,
},
},
},
{
module: { name: "Module B", input: "Input B" },
info: {
manifest: {
name: "Module B",
description: "This is module B",
},
roles: {
default: null,
},
},
},
{
module: { name: "Module C", input: "Input B" },
info: {
manifest: {
name: "Module B",
description: "This is module B",
},
roles: {
default: null,
},
},
},
{
module: { name: "Module B", input: "Input A" },
info: {
manifest: {
name: "Module B",
description: "This is module B",
},
roles: {
default: null,
},
},
},
],
};
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 ServiceWorkflow> = {
title: "workflows/service",
component: ServiceWorkflow,
decorators: [
(Story: StoryObj, context: StoryContext) => {
const Routes: RouteDefinition[] = [
{
path: "/clans/:clanURI",
component: () => (
<div>
<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 ServiceWorkflow>;
export const Default: Story = {
args: {},
};

View File

@@ -0,0 +1,124 @@
import {
createStepper,
getStepStore,
StepperProvider,
useStepper,
} from "@/src/hooks/stepper";
import { BackButton, NextButton } from "../Steps";
import { useClanURI } from "@/src/hooks/clan";
import { ServiceModules, useServiceModules } from "@/src/hooks/queries";
import { createEffect, createSignal } from "solid-js";
import { Search } from "@/src/components/Search/Search";
import Icon from "@/src/components/Icon/Icon";
import { Combobox } from "@kobalte/core/combobox";
import { Typography } from "@/src/components/Typography/Typography";
type ModuleItem = ServiceModules[number];
interface Module {
value: string;
input: string;
label: string;
description: string;
raw: ModuleItem;
}
const SelectService = () => {
const clanURI = useClanURI();
const stepper = useStepper<ServiceSteps>();
const serviceModulesQuery = useServiceModules(clanURI);
const [moduleOptions, setModuleOptions] = createSignal<Module[]>([]);
createEffect(() => {
if (serviceModulesQuery.data) {
setModuleOptions(
serviceModulesQuery.data.map((m) => ({
value: `${m.module.name}:${m.module.input}`,
label: m.module.name,
description: m.info.manifest.description,
input: m.module.input || "clan-core",
raw: m,
})),
);
}
});
const [store, set] = getStepStore<ServiceStoreType>(stepper);
return (
<Search<Module>
loading={serviceModulesQuery.isLoading}
onChange={(module) => {
if (!module) return;
console.log("Module selected");
set("module", {
name: module.raw.module.name,
input: module.raw.module.input,
});
stepper.next();
}}
options={moduleOptions()}
renderItem={(item) => {
return (
<div class="flex items-center justify-between gap-2 rounded-md px-2 py-1 pr-4">
<div class="flex size-8 items-center justify-center rounded-md bg-white">
<Icon icon="Code" />
</div>
<div class="flex w-full flex-col">
<Combobox.ItemLabel class="flex">
<Typography hierarchy="body" size="s" weight="medium" inverted>
{item.label}
</Typography>
</Combobox.ItemLabel>
<Typography
hierarchy="body"
size="xxs"
weight="normal"
color="quaternary"
inverted
class="flex justify-between"
>
<span>{item.description}</span>
<span>by {item.input}</span>
</Typography>
</div>
</div>
);
}}
/>
);
};
const steps = [
{
id: "select:service",
content: SelectService,
},
{
id: "select:members",
content: () => <div>Configure your service here.</div>,
},
{ id: "settings", content: () => <div>Adjust settings here.</div> },
] as const;
export type ServiceSteps = typeof steps;
export interface ServiceStoreType {
module: {
name: string;
input: string;
};
}
export const ServiceWorkflow = () => {
const stepper = createStepper({ steps }, { initialStep: "select:service" });
return (
<StepperProvider stepper={stepper}>
<BackButton />
{stepper.currentStep().content()}
<NextButton onClick={() => stepper.next()} />
</StepperProvider>
);
};