diff --git a/pkgs/clan-app/ui/src/components/Search/MultipleSearch.tsx b/pkgs/clan-app/ui/src/components/Search/MultipleSearch.tsx index b7945962f..b1698c435 100644 --- a/pkgs/clan-app/ui/src/components/Search/MultipleSearch.tsx +++ b/pkgs/clan-app/ui/src/components/Search/MultipleSearch.tsx @@ -2,9 +2,11 @@ import Icon from "../Icon/Icon"; import { Button } from "../Button/Button"; import styles from "./Search.module.css"; import { Combobox } from "@kobalte/core/combobox"; -import { createMemo, createSignal, For, JSX } from "solid-js"; +import { createMemo, createSignal, For, JSX, Match, Switch } from "solid-js"; import { createVirtualizer, VirtualizerOptions } from "@tanstack/solid-virtual"; import { CollectionNode } from "@kobalte/core/*"; +import cx from "classnames"; +import { Loader } from "../Loader/Loader"; export interface Option { value: string; @@ -23,6 +25,10 @@ export interface SearchMultipleProps { placeholder?: string; virtualizerOptions?: Partial>; height: string; // e.g. '14.5rem' + headerClass?: string; + headerChildren?: JSX.Element; + loading?: boolean; + loadingComponent?: JSX.Element; } export function SearchMultiple( props: SearchMultipleProps, @@ -72,7 +78,6 @@ export function SearchMultiple( props.onChange(values); }} class={styles.searchContainer} - style={{ "--container-height": props.height }} placement="bottom-start" options={props.options} optionValue="value" @@ -89,69 +94,78 @@ export function SearchMultiple( triggerMode="manual" noResetInputOnBlur={true} > - class={styles.searchHeader}> + + class={cx(styles.searchHeader, props.headerClass || "bg-inv-3")} + > {(state) => ( -
- - { - inputEl = el; - }} - class={styles.searchInput} - placeholder={props.placeholder} - value={inputValue()} - onChange={(e) => { - setInputValue(e.currentTarget.value); - }} - /> -
+ // Dispatch an input event to notify combobox listeners + inputEl.dispatchEvent( + new Event("input", { bubbles: true, cancelable: true }), + ); + }} + /> + + )} - - - - ref={(el) => { - listboxRef = el; - }} - style={{ - height: "100%", - width: "100%", - overflow: "auto", - "overflow-y": "auto", - }} - scrollToItem={(key) => { - const idx = comboboxItems().findIndex( - (option) => option.rawValue.value === key, - ); - virtualizer().scrollToIndex(idx); - }} - > - {(items) => { - // Update the virtualizer with the filtered items - const arr = Array.from(items()); - setComboboxItems(arr); + + ref={(el) => { + listboxRef = el; + }} + style={{ + height: props.height, + width: "100%", + overflow: "auto", + "overflow-y": "auto", + }} + scrollToItem={(key) => { + const idx = comboboxItems().findIndex( + (option) => option.rawValue.value === key, + ); + virtualizer().scrollToIndex(idx); + }} + class={styles.listbox} + > + {(items) => { + // Update the virtualizer with the filtered items + const arr = Array.from(items()); + setComboboxItems(arr); - return ( + return ( + + + {props.loadingComponent ?? ( +
+ +
+ )} +
+
( }}
- ); - }} - -
-
+ + + ); + }} + + {/* */} ); } diff --git a/pkgs/clan-app/ui/src/components/Search/Search.module.css b/pkgs/clan-app/ui/src/components/Search/Search.module.css index 08a0c392b..d52dedebe 100644 --- a/pkgs/clan-app/ui/src/components/Search/Search.module.css +++ b/pkgs/clan-app/ui/src/components/Search/Search.module.css @@ -29,7 +29,7 @@ } .searchHeader { - @apply bg-inv-3 flex gap-2 items-center p-2 rounded-md z-50; + @apply flex gap-2 items-center p-2 rounded-t-md z-50; @apply px-3 pt-3 pb-2; } @@ -42,18 +42,21 @@ } .searchItem { + @apply flex flex-col justify-center overflow-hidden; + box-shadow: 0 1px 0 0 theme(colors.border.inv.2); + &[data-highlighted], &:focus, &:focus-visible, &:hover { - @apply bg-inv-acc-2; + @apply bg-inv-acc-2 rounded-md; + box-shadow: unset; } &:active { - @apply bg-inv-acc-3; + @apply bg-inv-acc-3 rounded-md; + box-shadow: unset; } - - @apply flex flex-col justify-center; } .searchContainer { @@ -61,8 +64,6 @@ @apply rounded-lg; - height: var(--container-height, 14.5rem); - border: 1px solid #2b4647; background: @@ -78,9 +79,8 @@ 0 4px 6px -2px rgba(0, 0, 0, 0.05); } -.searchContent { - @apply px-3; - height: calc(var(--container-height, 14.5rem) - 3.5rem); +.listbox { + @apply px-3 pt-3.5; } @keyframes contentHide { diff --git a/pkgs/clan-app/ui/src/components/Search/Search.stories.tsx b/pkgs/clan-app/ui/src/components/Search/Search.stories.tsx index c0d9e9b2c..27da6034a 100644 --- a/pkgs/clan-app/ui/src/components/Search/Search.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Search/Search.stories.tsx @@ -9,7 +9,7 @@ import { SearchMultiple, SearchMultipleProps, } from "./MultipleSearch"; -import { JSX, Show } from "solid-js"; +import { Show } from "solid-js"; const meta = { title: "Components/Search", @@ -72,6 +72,7 @@ export interface Module { export const Default: Story = { args: { + height: "14.5rem", // Test with lots of modules options: generateModules(1000), renderItem: (item: Module) => { @@ -119,6 +120,7 @@ export const Default: Story = { export const Loading: Story = { args: { + height: "14.5rem", // Test with lots of modules loading: true, options: [], @@ -151,19 +153,6 @@ type MachineOrTag = type: "tag"; }; -interface WrapIfProps { - condition: boolean; - wrapper: (children: JSX.Element) => JSX.Element; - children: JSX.Element; -} -const WrapIf = (props: WrapIfProps) => { - if (props.condition) { - return props.wrapper(props.children); - } else { - return props.children; - } -}; - const machinesAndTags: MachineOrTag[] = [ { value: "machine-1", label: "Machine 1", type: "machine" }, { value: "machine-2", label: "Machine 2", type: "machine" }, diff --git a/pkgs/clan-app/ui/src/components/Search/Search.tsx b/pkgs/clan-app/ui/src/components/Search/Search.tsx index 4e002d96c..5dcb69fa9 100644 --- a/pkgs/clan-app/ui/src/components/Search/Search.tsx +++ b/pkgs/clan-app/ui/src/components/Search/Search.tsx @@ -6,6 +6,7 @@ import { createMemo, createSignal, For, JSX, Match, Switch } from "solid-js"; import { createVirtualizer } from "@tanstack/solid-virtual"; import { CollectionNode } from "@kobalte/core/*"; import { Loader } from "../Loader/Loader"; +import cx from "classnames"; export interface Option { value: string; @@ -18,6 +19,8 @@ export interface SearchProps { renderItem: (item: T) => JSX.Element; loading?: boolean; loadingComponent?: JSX.Element; + headerClass?: string; + height: string; // e.g. '14.5rem' } export function Search(props: SearchProps) { // Controlled input value, to allow resetting the input itself @@ -80,7 +83,9 @@ export function Search(props: SearchProps) { triggerMode="manual" noResetInputOnBlur={true} > - class={styles.searchHeader}> + + class={cx(styles.searchHeader, props.headerClass || "bg-inv-3")} + > {(state) => (
@@ -114,85 +119,79 @@ export function Search(props: SearchProps) {
)} - - - - ref={(el) => { - listboxRef = el; - }} - style={{ - height: "100%", - width: "100%", - overflow: "auto", - "overflow-y": "auto", - }} - scrollToItem={(key) => { - const idx = comboboxItems().findIndex( - (option) => option.rawValue.value === key, - ); - virtualizer().scrollToIndex(idx); - }} - > - {(items) => { - // Update the virtualizer with the filtered items - const arr = Array.from(items()); - setComboboxItems(arr); + + ref={(el) => { + listboxRef = el; + }} + style={{ + height: props.height, + width: "100%", + overflow: "auto", + "overflow-y": "auto", + }} + class={styles.listbox} + scrollToItem={(key) => { + const idx = comboboxItems().findIndex( + (option) => option.rawValue.value === key, + ); + virtualizer().scrollToIndex(idx); + }} + > + {(items) => { + // Update the virtualizer with the filtered items + const arr = Array.from(items()); + setComboboxItems(arr); - return ( - - - {props.loadingComponent ?? ( -
- -
- )} -
- -
- - {(virtualRow) => { - const item: CollectionNode | undefined = - items().getItem(virtualRow.key as string); + return ( + + + {props.loadingComponent ?? ( +
+ +
+ )} +
+ +
+ + {(virtualRow) => { + const item: CollectionNode | undefined = + items().getItem(virtualRow.key as string); - if (!item) { - console.warn( - "Item not found for key:", - virtualRow.key, - ); - return null; - } - return ( - - {props.renderItem(item.rawValue)} - - ); - }} - -
-
-
- ); - }} - - - + if (!item) { + console.warn("Item not found for key:", virtualRow.key); + return null; + } + return ( + + {props.renderItem(item.rawValue)} + + ); + }} +
+
+
+
+ ); + }} + ); } diff --git a/pkgs/clan-app/ui/src/components/Search/TagSelect.module.css b/pkgs/clan-app/ui/src/components/Search/TagSelect.module.css index 9c80c90c0..d3fcc1c7c 100644 --- a/pkgs/clan-app/ui/src/components/Search/TagSelect.module.css +++ b/pkgs/clan-app/ui/src/components/Search/TagSelect.module.css @@ -1,22 +1,3 @@ -.dummybg { - padding: 1rem; - width: 20rem; - min-height: 10rem; - border-radius: 8px; - border: 1px solid #2e4a4b; - background: - linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), - linear-gradient( - 180deg, - theme(colors.bg.inv.2) 0%, - theme(colors.bg.inv.3) 100% - ); - - box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); -} - .trigger { @apply rounded-md bg-inv-4 w-full min-h-11; diff --git a/pkgs/clan-app/ui/src/components/Search/TagSelect.stories.tsx b/pkgs/clan-app/ui/src/components/Search/TagSelect.stories.tsx index ef70a71e0..379345e24 100644 --- a/pkgs/clan-app/ui/src/components/Search/TagSelect.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Search/TagSelect.stories.tsx @@ -3,6 +3,7 @@ import { Meta, StoryObj } from "@kachurun/storybook-solid"; import { TagSelect, TagSelectProps } from "./TagSelect"; import { Tag } from "../Tag/Tag"; import Icon from "../Icon/Icon"; +import { createSignal } from "solid-js"; const meta = { title: "Components/Custom/SelectStepper", @@ -11,28 +12,51 @@ const meta = { export default meta; -type Story = StoryObj>; +interface Item { + value: string; + label: string; +} -const Item = (item: string) => ( +type Story = StoryObj>; + +const Item = (item: Item) => ( ( )} > - {item} + {item.label} ); export const Default: Story = { args: { renderItem: Item, - values: ["foo", "bar"], - options: ["foo", "bar", "baz", "qux", "quux"], - onChange: (values: string[]) => { - console.log("Selected values:", values); - }, - onClick: () => { - console.log("Combobox clicked"); - }, + label: "Peer", + options: [ + { value: "foo", label: "Foo" }, + { value: "bar", label: "Bar" }, + { value: "baz", label: "Baz" }, + { value: "qux", label: "Qux" }, + { value: "quux", label: "Quux" }, + { value: "corge", label: "Corge" }, + { value: "grault", label: "Grault" }, + ], + } satisfies Partial>, + render: (args: TagSelectProps) => { + const [state, setState] = createSignal([]); + return ( + + {...args} + values={state()} + onClick={() => { + console.log("Clicked, current values:"); + setState(() => [ + { value: "baz", label: "Baz" }, + { value: "qux", label: "Qux" }, + ]); + }} + /> + ); }, }; diff --git a/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx b/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx index 43a541134..590577fd8 100644 --- a/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx +++ b/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx @@ -5,43 +5,58 @@ import styles from "./TagSelect.module.css"; import { Combobox } from "@kobalte/core/combobox"; import { Button } from "../Button/Button"; +// Base props common to both modes export interface TagSelectProps { - // Define any props needed for the SelectStepper component + onClick: () => void; + label: string; values: T[]; options: T[]; - onChange: (values: T[]) => void; - onClick: () => void; renderItem: (item: T) => JSX.Element; } -export function TagSelect(props: TagSelectProps) { +/** + * Shallowly interactive field for selecting multiple tags / machines. + * It does only handle click and focus interactions + * Displays the selected items as tags + */ +export function TagSelect( + props: TagSelectProps, +) { + const optionValue = "value"; return ( -
-
-
- - Servers - - -
- - multiple - value={props.values} - onChange={props.onChange} - options={props.options} - allowsEmptyCollection - class="w-full" +
+
+ - aria-label="Fruits"> - {(state) => ( + {props.label} + + +
+ + multiple + optionValue={optionValue} + value={props.values} + options={props.options} + allowsEmptyCollection + class="w-full" + > + aria-label="Fruits"> + {(state) => { + console.log("combobox state selected", state.selectedOptions()); + return ( (props: TagSelectProps) { {props.renderItem}
- )} - - -
+ ); + }} + +
); } diff --git a/pkgs/clan-app/ui/src/hooks/queries.ts b/pkgs/clan-app/ui/src/hooks/queries.ts index 085335716..e678517b3 100644 --- a/pkgs/clan-app/ui/src/hooks/queries.ts +++ b/pkgs/clan-app/ui/src/hooks/queries.ts @@ -42,6 +42,7 @@ export const DefaultQueryClient = new QueryClient({ }, }); +export type MachinesQuery = ReturnType; export const useMachinesQuery = (clanURI: string) => { const client = useApiClient(); @@ -117,6 +118,27 @@ export const useMachineQuery = (clanURI: string, machineName: string) => { })); }; +export type TagsQuery = ReturnType; +export const useTags = (clanURI: string) => { + const client = useApiClient(); + return useQuery(() => ({ + queryKey: ["clans", encodeBase64(clanURI), "tags"], + queryFn: async () => { + const apiCall = client.fetch("list_tags", { + flake: { + identifier: clanURI, + }, + }); + + const result = await apiCall.result; + if (result.status === "error") { + throw new Error("Error fetching tags: " + result.errors[0].message); + } + return result.data; + }, + })); +}; + export const useMachineStateQuery = (clanURI: string, machineName: string) => { const client = useApiClient(); return useQuery(() => ({ diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.module.css b/pkgs/clan-app/ui/src/workflows/Service/Service.module.css new file mode 100644 index 000000000..9478b954d --- /dev/null +++ b/pkgs/clan-app/ui/src/workflows/Service/Service.module.css @@ -0,0 +1,35 @@ +.content { + @apply px-3 flex flex-col gap-5 py-6; + border: 1px solid #2e4a4b; + background: + linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), + linear-gradient( + 180deg, + theme(colors.bg.inv.2) 0%, + theme(colors.bg.inv.3) 100% + ); + + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +.header { + @apply py-2 pl-3 pr-2 flex gap-2.5 w-full items-center; + border-radius: 8px 8px 0 0; + border-bottom: 1px solid #2e4a4b; +} + +.footer { + @apply py-3 px-4 flex justify-end w-full; + border-radius: 0 0 8px 8px; + border-top: 1px solid #2e4a4b; +} + +.backgroundAlt { + background: linear-gradient( + 90deg, + theme(colors.bg.inv.3) 0%, + theme(colors.bg.inv.4) 100% + ); +} diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx index ec534547f..0e4e132e1 100644 --- a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx +++ b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx @@ -26,23 +26,37 @@ const mockFetcher: Fetcher = ( const resultData: Partial = { list_service_modules: [ { - module: { name: "Module A", input: "Input A" }, + module: { name: "Borgbackup", input: "clan-core" }, info: { manifest: { - name: "Module A", + name: "Borgbackup", description: "This is module A", }, roles: { - peer: null, + client: null, server: null, }, }, }, { - module: { name: "Module B", input: "Input B" }, + module: { name: "Zerotier", input: "clan-core" }, info: { manifest: { - name: "Module B", + name: "Zerotier", + description: "This is module B", + }, + roles: { + peer: null, + moon: null, + controller: null, + }, + }, + }, + { + module: { name: "Admin", input: "clan-core" }, + info: { + manifest: { + name: "Admin", description: "This is module B", }, roles: { @@ -51,22 +65,10 @@ const mockFetcher: Fetcher = ( }, }, { - module: { name: "Module C", input: "Input B" }, + module: { name: "Garage", input: "lo-l" }, 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", + name: "Garage", description: "This is module B", }, roles: { @@ -75,6 +77,28 @@ const mockFetcher: Fetcher = ( }, }, ], + list_machines: { + jon: { + name: "jon", + tags: ["all", "nixos", "tag1"], + }, + sara: { + name: "sara", + tags: ["all", "darwin", "tag2"], + }, + kyra: { + name: "kyra", + tags: ["all", "darwin", "tag2"], + }, + leila: { + name: "leila", + tags: ["all", "darwin", "tag2"], + }, + }, + list_tags: { + options: ["desktop", "server", "full", "only", "streaming", "backup"], + special: ["all", "nixos", "darwin"], + }, }; return { @@ -138,3 +162,14 @@ type Story = StoryObj; export const Default: Story = { args: {}, }; + +export const SelectRoleMembers: Story = { + render: () => ( + + ), +}; diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.tsx b/pkgs/clan-app/ui/src/workflows/Service/Service.tsx index 0360f6b1d..944dd1e2b 100644 --- a/pkgs/clan-app/ui/src/workflows/Service/Service.tsx +++ b/pkgs/clan-app/ui/src/workflows/Service/Service.tsx @@ -4,14 +4,31 @@ import { 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 { + MachinesQuery, + ServiceModules, + TagsQuery, + useMachinesQuery, + useServiceModules, + useTags, +} from "@/src/hooks/queries"; +import { createEffect, createMemo, createSignal, For, Show } 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"; +import { Toolbar } from "@/src/components/Toolbar/Toolbar"; +import { ToolbarButton } from "@/src/components/Toolbar/ToolbarButton"; +import { TagSelect } from "@/src/components/Search/TagSelect"; +import { Tag } from "@/src/components/Tag/Tag"; +import { createForm, FieldValues, setValue } from "@modular-forms/solid"; +import styles from "./Service.module.css"; +import { TextInput } from "@/src/components/Form/TextInput"; +import { Button } from "@/src/components/Button/Button"; +import cx from "classnames"; +import { BackButton } from "../Steps"; +import { SearchMultiple } from "@/src/components/Search/MultipleSearch"; type ModuleItem = ServiceModules[number]; @@ -48,20 +65,21 @@ const SelectService = () => { return ( loading={serviceModulesQuery.isLoading} + height="13rem" onChange={(module) => { if (!module) return; - console.log("Module selected"); set("module", { name: module.raw.module.name, input: module.raw.module.input, + raw: module.raw, }); stepper.next(); }} options={moduleOptions()} renderItem={(item) => { return ( -
+
@@ -90,14 +108,273 @@ const SelectService = () => { ); }; +const useOptions = (tagsQuery: TagsQuery, machinesQuery: MachinesQuery) => + createMemo(() => { + const tags = tagsQuery.data; + const machines = machinesQuery.data; + if (!tags || !machines) { + return []; + } + + const machineOptions = Object.keys(machines).map((m) => ({ + label: m, + value: "m_" + m, + type: "machine" as const, + })); + + const tagOptions = [...tags.options, ...tags.special].map((tag) => ({ + type: "tag" as const, + label: tag, + value: "t_" + tag, + members: Object.entries(machines) + .filter(([_, v]) => v.tags?.includes(tag)) + .map(([k]) => k), + })); + + return [...machineOptions, ...tagOptions].sort((a, b) => + a.label.localeCompare(b.label), + ); + }); + +interface RolesForm extends FieldValues { + roles: Record; + instanceName: string; +} +const ConfigureService = () => { + const stepper = useStepper(); + const [store, set] = getStepStore(stepper); + + const [formStore, { Form, Field }] = createForm({ + // initialValues: props.initialValues, + initialValues: { + instanceName: "backup-instance-1", + }, + }); + + const machinesQuery = useMachinesQuery(useClanURI()); + const tagsQuery = useTags(useClanURI()); + + const options = useOptions(tagsQuery, machinesQuery); + + const handleSubmit = (values: RolesForm) => { + console.log("Create service submitted with values:", values); + }; + + return ( +
+
+
+ +
+
+ + {store.module.name} + + + {(field, input) => ( + + )} + +
+
+
+ + {(role) => { + const values = store.roles?.[role] || []; + console.log("Role members:", role, values, "from", options()); + return ( + + label={role} + renderItem={(item: TagType) => ( + ( + + )} + > + {item.label} + + )} + values={values} + options={options()} + onClick={() => { + set("currentRole", role); + stepper.next(); + }} + /> + ); + }} + +
+
+ +
+
+ ); +}; + +type TagType = + | { + value: string; + label: string; + type: "machine"; + } + | { + value: string; + label: string; + type: "tag"; + members: string[]; + }; + +interface RoleMembers extends FieldValues { + members: string[]; +} +const ConfigureRole = () => { + const stepper = useStepper(); + const [store, set] = getStepStore(stepper); + + const [formStore, { Form, Field }] = createForm({ + initialValues: { + members: [], + }, + }); + + const machinesQuery = useMachinesQuery(useClanURI()); + const tagsQuery = useTags(useClanURI()); + + const options = useOptions(tagsQuery, machinesQuery); + + const handleSubmit = (values: RoleMembers) => { + if (!store.currentRole) return; + + const members: TagType[] = values.members.map( + (m) => options().find((o) => o.value === m)!, + ); + + if (!store.roles) { + set("roles", {}); + } + set("roles", (r) => ({ ...r, [store.currentRole as string]: members })); + console.log("Roles form submitted ", members); + + stepper.setActiveStep("view:members"); + }; + + return ( +
+
+
+ + {(field, input) => ( + + initialValues={store.roles?.[store.currentRole || ""] || []} + options={options()} + headerClass={cx(styles.backgroundAlt, "flex flex-col gap-2.5")} + headerChildren={ +
+ + + Select {store.currentRole} + +
+ } + placeholder={"Search for Machine or Tags"} + renderItem={(item, opts) => ( +
+ + } + > + + + + + + {item.label} + + + {(tag) => ( + + {tag().members.length} + + )} + + + +
+ )} + height="20rem" + virtualizerOptions={{ + estimateSize: () => 38, + }} + onChange={(selection) => { + const newval = selection.map((s) => s.value); + setValue(formStore, field.name, newval); + }} + /> + )} +
+
+
+ +
+
+
+ ); +}; + const steps = [ { id: "select:service", content: SelectService, }, + { + id: "view:members", + content: ConfigureService, + }, { id: "select:members", - content: () =>
Configure your service here.
, + content: ConfigureRole, }, { id: "settings", content: () =>
Adjust settings here.
}, ] as const; @@ -108,17 +385,50 @@ export interface ServiceStoreType { module: { name: string; input: string; + raw?: ModuleItem; }; + roles: Record; + currentRole?: string; + close: () => void; } -export const ServiceWorkflow = () => { - const stepper = createStepper({ steps }, { initialStep: "select:service" }); - +interface ServiceWorkflowProps { + initialStep?: ServiceSteps[number]["id"]; + initialStore?: Partial; +} +export const ServiceWorkflow = (props: ServiceWorkflowProps) => { + const [show, setShow] = createSignal(false); + const stepper = createStepper( + { steps }, + { + initialStep: props.initialStep || "select:service", + initialStoreData: { + ...props.initialStore, + close: () => setShow(false), + } satisfies Partial, + }, + ); return ( - - - {stepper.currentStep().content()} - stepper.next()} /> - + <> +
+ +
+ +
{stepper.currentStep().content()}
+
+
+
+
+ + setShow(!show())} + description="Add new Service" + name="modules" + icon="Modules" + /> + +
+
+ ); }; diff --git a/pkgs/clan-app/ui/src/workflows/Steps.tsx b/pkgs/clan-app/ui/src/workflows/Steps.tsx index c0ff851be..ba25778fd 100644 --- a/pkgs/clan-app/ui/src/workflows/Steps.tsx +++ b/pkgs/clan-app/ui/src/workflows/Steps.tsx @@ -35,7 +35,8 @@ export const NextButton = (props: NextButtonProps) => { ); }; -export const BackButton = () => { +type BackButtonProps = ButtonProps & {}; +export const BackButton = (props: BackButtonProps) => { const stepSignal = useStepper(); return ( ); };