ui/service: rewire to allow external selection
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
useClanURI,
|
useClanURI,
|
||||||
useMachineName,
|
useMachineName,
|
||||||
} from "@/src/hooks/clan";
|
} from "@/src/hooks/clan";
|
||||||
import { CubeScene } from "@/src/scene/cubes";
|
import { CubeScene, setWorldMode, worldMode } from "@/src/scene/cubes";
|
||||||
import {
|
import {
|
||||||
ClanDetails,
|
ClanDetails,
|
||||||
MachinesQueryResult,
|
MachinesQueryResult,
|
||||||
@@ -38,10 +38,11 @@ import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
|||||||
import { UseQueryResult } from "@tanstack/solid-query";
|
import { UseQueryResult } from "@tanstack/solid-query";
|
||||||
import { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal";
|
import { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal";
|
||||||
import {
|
import {
|
||||||
InventoryInstance,
|
|
||||||
ServiceWorkflow,
|
ServiceWorkflow,
|
||||||
|
SubmitServiceHandler,
|
||||||
} from "@/src/workflows/Service/Service";
|
} from "@/src/workflows/Service/Service";
|
||||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
|
||||||
interface ClanContextProps {
|
interface ClanContextProps {
|
||||||
clanURI: string;
|
clanURI: string;
|
||||||
@@ -208,7 +209,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
|
|
||||||
const onAddService = async (): Promise<{ id: string }> => {
|
const onAddService = async (): Promise<{ id: string }> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setShowService(true);
|
setShowService((v) => !v);
|
||||||
console.log("setting current promise");
|
console.log("setting current promise");
|
||||||
setCurrentPromise({ resolve, reject });
|
setCurrentPromise({ resolve, reject });
|
||||||
});
|
});
|
||||||
@@ -287,8 +288,16 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const client = useApiClient();
|
const client = useApiClient();
|
||||||
const handleSubmitService = async (instance: InventoryInstance) => {
|
const handleSubmitService: SubmitServiceHandler = async (
|
||||||
console.log("Create Instance", instance);
|
instance,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
console.log(action, "Instance", instance);
|
||||||
|
|
||||||
|
if (action !== "create") {
|
||||||
|
toast.error("Only creating new services is supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const call = client.fetch("create_service_instance", {
|
const call = client.fetch("create_service_instance", {
|
||||||
flake: {
|
flake: {
|
||||||
identifier: ctx.clanURI,
|
identifier: ctx.clanURI,
|
||||||
@@ -299,13 +308,26 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
const result = await call.result;
|
const result = await call.result;
|
||||||
|
|
||||||
if (result.status === "error") {
|
if (result.status === "error") {
|
||||||
|
toast.error("Error creating service instance");
|
||||||
console.error("Error creating service instance", result.errors);
|
console.error("Error creating service instance", result.errors);
|
||||||
}
|
}
|
||||||
|
toast.success("Created");
|
||||||
//
|
//
|
||||||
currentPromise()?.resolve({ id: "0" });
|
currentPromise()?.resolve({ id: "0" });
|
||||||
setShowService(false);
|
setShowService(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(worldMode, (mode) => {
|
||||||
|
if (mode === "service") {
|
||||||
|
setShowService(true);
|
||||||
|
} else {
|
||||||
|
// todo: request close instead of force close
|
||||||
|
setShowService(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={loadingError()}>
|
<Show when={loadingError()}>
|
||||||
@@ -338,7 +360,6 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CubeScene
|
<CubeScene
|
||||||
onAddService={onAddService}
|
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onSelect={onMachineSelect}
|
onSelect={onMachineSelect}
|
||||||
isLoading={ctx.isLoading()}
|
isLoading={ctx.isLoading()}
|
||||||
@@ -349,6 +370,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
handleSubmit={handleSubmitService}
|
handleSubmit={handleSubmitService}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowService(false);
|
setShowService(false);
|
||||||
|
setWorldMode("default");
|
||||||
currentPromise()?.resolve({ id: "0" });
|
currentPromise()?.resolve({ id: "0" });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,32 +10,50 @@ import {
|
|||||||
ServiceModules,
|
ServiceModules,
|
||||||
TagsQuery,
|
TagsQuery,
|
||||||
useMachinesQuery,
|
useMachinesQuery,
|
||||||
|
useServiceInstances,
|
||||||
useServiceModules,
|
useServiceModules,
|
||||||
useTags,
|
useTags,
|
||||||
} from "@/src/hooks/queries";
|
} from "@/src/hooks/queries";
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
JSX,
|
||||||
|
Show,
|
||||||
|
on,
|
||||||
|
onMount,
|
||||||
|
} from "solid-js";
|
||||||
import { Search } from "@/src/components/Search/Search";
|
import { Search } from "@/src/components/Search/Search";
|
||||||
import Icon from "@/src/components/Icon/Icon";
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
import { Combobox } from "@kobalte/core/combobox";
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { TagSelect } from "@/src/components/Search/TagSelect";
|
import { TagSelect } from "@/src/components/Search/TagSelect";
|
||||||
import { Tag } from "@/src/components/Tag/Tag";
|
import { Tag } from "@/src/components/Tag/Tag";
|
||||||
import { createForm, FieldValues, setValue } from "@modular-forms/solid";
|
import { createForm, FieldValues } from "@modular-forms/solid";
|
||||||
import styles from "./Service.module.css";
|
import styles from "./Service.module.css";
|
||||||
import { TextInput } from "@/src/components/Form/TextInput";
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { BackButton } from "../Steps";
|
import { BackButton } from "../Steps";
|
||||||
import { SearchMultiple } from "@/src/components/Search/MultipleSearch";
|
import { SearchMultiple } from "@/src/components/Search/MultipleSearch";
|
||||||
|
import { useMachineClick } from "@/src/scene/cubes";
|
||||||
|
import {
|
||||||
|
clearAllHighlights,
|
||||||
|
highlightGroups,
|
||||||
|
setHighlightGroups,
|
||||||
|
} from "@/src/scene/highlightStore";
|
||||||
|
import { useClickOutside } from "@/src/hooks/useClickOutside";
|
||||||
|
|
||||||
type ModuleItem = ServiceModules[number];
|
type ModuleItem = ServiceModules[number];
|
||||||
|
|
||||||
interface Module {
|
interface Module {
|
||||||
value: string;
|
value: string;
|
||||||
input: string;
|
input?: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
raw: ModuleItem;
|
raw: ModuleItem;
|
||||||
|
instances: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectService = () => {
|
const SelectService = () => {
|
||||||
@@ -43,17 +61,27 @@ const SelectService = () => {
|
|||||||
const stepper = useStepper<ServiceSteps>();
|
const stepper = useStepper<ServiceSteps>();
|
||||||
|
|
||||||
const serviceModulesQuery = useServiceModules(clanURI);
|
const serviceModulesQuery = useServiceModules(clanURI);
|
||||||
|
const serviceInstancesQuery = useServiceInstances(clanURI);
|
||||||
|
const machinesQuery = useMachinesQuery(clanURI);
|
||||||
|
|
||||||
const [moduleOptions, setModuleOptions] = createSignal<Module[]>([]);
|
const [moduleOptions, setModuleOptions] = createSignal<Module[]>([]);
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (serviceModulesQuery.data) {
|
if (serviceModulesQuery.data && serviceInstancesQuery.data) {
|
||||||
setModuleOptions(
|
setModuleOptions(
|
||||||
serviceModulesQuery.data.map((m) => ({
|
serviceModulesQuery.data.map((m) => ({
|
||||||
value: `${m.module.name}:${m.module.input}`,
|
value: `${m.module.name}:${m.module.input}`,
|
||||||
label: m.module.name,
|
label: m.module.name,
|
||||||
description: m.info.manifest.description,
|
description: m.info.manifest.description,
|
||||||
input: m.module.input || "clan-core",
|
input: m.module.input,
|
||||||
raw: m,
|
raw: m,
|
||||||
|
// TODO: include the instances that use this module
|
||||||
|
instances: Object.entries(serviceInstancesQuery.data)
|
||||||
|
.filter(
|
||||||
|
([name, i]) =>
|
||||||
|
i.module?.name === m.module.name &&
|
||||||
|
(!i.module?.input || i.module?.input === m.module.input),
|
||||||
|
)
|
||||||
|
.map(([name, _]) => name),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,17 +100,77 @@ const SelectService = () => {
|
|||||||
input: module.raw.module.input,
|
input: module.raw.module.input,
|
||||||
raw: module.raw,
|
raw: module.raw,
|
||||||
});
|
});
|
||||||
|
// TODO: Ideally we need to ask
|
||||||
|
// - create new
|
||||||
|
// - update existing (and select which one)
|
||||||
|
|
||||||
|
// For now:
|
||||||
|
// Create a new instance, if there are no instances yet
|
||||||
|
// Update the first instance, if there is one
|
||||||
|
if (module.instances.length === 0) {
|
||||||
|
set("action", "create");
|
||||||
|
} else {
|
||||||
|
if (!serviceInstancesQuery.data) return;
|
||||||
|
if (!machinesQuery.data) return;
|
||||||
|
set("action", "update");
|
||||||
|
|
||||||
|
const instanceName = module.instances[0];
|
||||||
|
const instance = serviceInstancesQuery.data[instanceName];
|
||||||
|
console.log("Editing existing instance", module);
|
||||||
|
|
||||||
|
for (const role of Object.keys(instance.roles || {})) {
|
||||||
|
const tags = Object.keys(instance.roles?.[role].tags || {});
|
||||||
|
const machines = Object.keys(instance.roles?.[role].machines || {});
|
||||||
|
|
||||||
|
const machineTags = machines.map((m) => ({
|
||||||
|
value: "m_" + m,
|
||||||
|
label: m,
|
||||||
|
type: "machine" as const,
|
||||||
|
}));
|
||||||
|
const tagsTags = tags.map((t) => {
|
||||||
|
return {
|
||||||
|
value: "t_" + t,
|
||||||
|
label: t,
|
||||||
|
type: "tag" as const,
|
||||||
|
members: Object.entries(machinesQuery.data || {})
|
||||||
|
.filter(([_, m]) => m.tags?.includes(t))
|
||||||
|
.map(([k]) => k),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log("Members for role", role, [
|
||||||
|
...machineTags,
|
||||||
|
...tagsTags,
|
||||||
|
]);
|
||||||
|
if (!store.roles) {
|
||||||
|
set("roles", {});
|
||||||
|
}
|
||||||
|
const roleMembers = [...machineTags, ...tagsTags].sort((a, b) =>
|
||||||
|
a.label.localeCompare(b.label),
|
||||||
|
);
|
||||||
|
set("roles", role, roleMembers);
|
||||||
|
console.log("set", store.roles);
|
||||||
|
}
|
||||||
|
// Initialize the roles with the existing members
|
||||||
|
}
|
||||||
|
|
||||||
stepper.next();
|
stepper.next();
|
||||||
}}
|
}}
|
||||||
options={moduleOptions()}
|
options={moduleOptions()}
|
||||||
renderItem={(item) => {
|
renderItem={(item, opts) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center justify-between gap-2 overflow-hidden rounded-md px-2 py-1 pr-4">
|
<div class="flex items-center justify-between gap-2 overflow-hidden rounded-md px-2 py-1 pr-4">
|
||||||
<div class="flex size-8 items-center justify-center rounded-md bg-white">
|
<div class="flex size-8 shrink-0 items-center justify-center rounded-md bg-white">
|
||||||
<Icon icon="Code" />
|
<Icon icon="Code" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col">
|
<div class="flex w-full flex-col">
|
||||||
<Combobox.ItemLabel class="flex">
|
<Combobox.ItemLabel class="flex gap-1.5">
|
||||||
|
<Show when={item.instances.length > 0}>
|
||||||
|
<div class="flex items-center rounded bg-[#76FFA4] px-1 py-0.5">
|
||||||
|
<Typography hierarchy="label" weight="bold" size="xxs">
|
||||||
|
Added
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Typography hierarchy="body" size="s" weight="medium" inverted>
|
<Typography hierarchy="body" size="s" weight="medium" inverted>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -95,8 +183,12 @@ const SelectService = () => {
|
|||||||
inverted
|
inverted
|
||||||
class="flex justify-between"
|
class="flex justify-between"
|
||||||
>
|
>
|
||||||
<span>{item.description}</span>
|
<span class="inline-block max-w-32 truncate align-middle">
|
||||||
<span>by {item.input}</span>
|
{item.description}
|
||||||
|
</span>
|
||||||
|
<span class="inline-block max-w-8 truncate align-middle">
|
||||||
|
by {item.input}
|
||||||
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +236,8 @@ const ConfigureService = () => {
|
|||||||
|
|
||||||
const [formStore, { Form, Field }] = createForm<RolesForm>({
|
const [formStore, { Form, Field }] = createForm<RolesForm>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
instanceName: "backup-instance-1",
|
// Default to the module name, until we support multiple instances
|
||||||
|
instanceName: store.module.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,14 +261,17 @@ const ConfigureService = () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
store.handleSubmit({
|
store.handleSubmit(
|
||||||
name: values.instanceName,
|
{
|
||||||
module: {
|
name: values.instanceName,
|
||||||
name: store.module.name,
|
module: {
|
||||||
input: store.module.input,
|
name: store.module.name,
|
||||||
|
input: store.module.input,
|
||||||
|
},
|
||||||
|
roles,
|
||||||
},
|
},
|
||||||
roles,
|
store.action,
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -245,8 +341,11 @@ const ConfigureService = () => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<div class={cx(styles.footer, styles.backgroundAlt)}>
|
<div class={cx(styles.footer, styles.backgroundAlt)}>
|
||||||
|
<BackButton ghost hierarchy="primary" class="mr-auto" />
|
||||||
|
|
||||||
<Button hierarchy="secondary" type="submit">
|
<Button hierarchy="secondary" type="submit">
|
||||||
Add Service
|
<Show when={store.action === "create"}>Add Service</Show>
|
||||||
|
<Show when={store.action === "update"}>Save Changes</Show>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -266,116 +365,145 @@ type TagType =
|
|||||||
members: string[];
|
members: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RoleMembers extends FieldValues {
|
|
||||||
members: string[];
|
|
||||||
}
|
|
||||||
const ConfigureRole = () => {
|
const ConfigureRole = () => {
|
||||||
const stepper = useStepper<ServiceSteps>();
|
const stepper = useStepper<ServiceSteps>();
|
||||||
const [store, set] = getStepStore<ServiceStoreType>(stepper);
|
const [store, set] = getStepStore<ServiceStoreType>(stepper);
|
||||||
|
|
||||||
const [formStore, { Form, Field }] = createForm<RoleMembers>({
|
const [members, setMembers] = createSignal<TagType[]>(
|
||||||
initialValues: {
|
store.roles?.[store.currentRole || ""] || [],
|
||||||
members: [],
|
);
|
||||||
},
|
|
||||||
|
const lastClickedMachine = useMachineClick();
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("Current role", store.currentRole, members());
|
||||||
|
clearAllHighlights();
|
||||||
|
setHighlightGroups({
|
||||||
|
[store.currentRole as string]: new Set(
|
||||||
|
members().flatMap((m) => {
|
||||||
|
if (m.type === "machine") return m.label;
|
||||||
|
|
||||||
|
return m.members;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("now", highlightGroups);
|
||||||
});
|
});
|
||||||
|
onMount(() => setHighlightGroups(() => ({})));
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(lastClickedMachine, (machine) => {
|
||||||
|
// const machine = lastClickedMachine();
|
||||||
|
const currentMembers = members();
|
||||||
|
console.log("Clicked machine", machine, currentMembers);
|
||||||
|
if (!machine) return;
|
||||||
|
const machineTagName = "m_" + machine;
|
||||||
|
|
||||||
|
const existing = currentMembers.find((m) => m.value === machineTagName);
|
||||||
|
if (existing) {
|
||||||
|
// Remove
|
||||||
|
setMembers(currentMembers.filter((m) => m.value !== machineTagName));
|
||||||
|
} else {
|
||||||
|
// Add
|
||||||
|
setMembers([
|
||||||
|
...currentMembers,
|
||||||
|
{ value: machineTagName, label: machine, type: "machine" },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const machinesQuery = useMachinesQuery(useClanURI());
|
const machinesQuery = useMachinesQuery(useClanURI());
|
||||||
const tagsQuery = useTags(useClanURI());
|
const tagsQuery = useTags(useClanURI());
|
||||||
|
|
||||||
const options = useOptions(tagsQuery, machinesQuery);
|
const options = useOptions(tagsQuery, machinesQuery);
|
||||||
|
|
||||||
const handleSubmit = (values: RoleMembers) => {
|
const handleSubmit = () => {
|
||||||
if (!store.currentRole) return;
|
if (!store.currentRole) return;
|
||||||
|
|
||||||
const members: TagType[] = values.members.map(
|
|
||||||
(m) => options().find((o) => o.value === m)!,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!store.roles) {
|
if (!store.roles) {
|
||||||
set("roles", {});
|
set("roles", {});
|
||||||
}
|
}
|
||||||
set("roles", (r) => ({ ...r, [store.currentRole as string]: members }));
|
set("roles", (r) => ({ ...r, [store.currentRole as string]: members() }));
|
||||||
stepper.setActiveStep("view:members");
|
stepper.setActiveStep("view:members");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<form onSubmit={() => handleSubmit()}>
|
||||||
<div class={cx(styles.backgroundAlt, "rounded-md")}>
|
<div class={cx(styles.backgroundAlt, "rounded-md")}>
|
||||||
<div class="flex w-full flex-col ">
|
<div class="flex w-full flex-col ">
|
||||||
<Field name="members" type="string[]">
|
<SearchMultiple<TagType>
|
||||||
{(field, input) => (
|
values={members()}
|
||||||
<SearchMultiple<TagType>
|
options={options()}
|
||||||
initialValues={store.roles?.[store.currentRole || ""] || []}
|
headerClass={cx(styles.backgroundAlt, "flex flex-col gap-2.5")}
|
||||||
options={options()}
|
headerChildren={
|
||||||
headerClass={cx(styles.backgroundAlt, "flex flex-col gap-2.5")}
|
<div class="flex w-full gap-2.5">
|
||||||
headerChildren={
|
<BackButton
|
||||||
<div class="flex w-full gap-2.5">
|
ghost
|
||||||
<BackButton ghost size="xs" hierarchy="primary" />
|
size="xs"
|
||||||
<Typography
|
hierarchy="primary"
|
||||||
hierarchy="body"
|
// onClick={() => clearAllHighlights()}
|
||||||
size="s"
|
/>
|
||||||
weight="medium"
|
<Typography
|
||||||
inverted
|
hierarchy="body"
|
||||||
class="capitalize"
|
size="s"
|
||||||
>
|
weight="medium"
|
||||||
Select {store.currentRole}
|
inverted
|
||||||
</Typography>
|
class="capitalize"
|
||||||
</div>
|
>
|
||||||
}
|
Select {store.currentRole}
|
||||||
placeholder={"Search for Machine or Tags"}
|
</Typography>
|
||||||
renderItem={(item, opts) => (
|
</div>
|
||||||
<div class={cx("flex w-full items-center gap-2 px-3 py-2")}>
|
}
|
||||||
<Combobox.ItemIndicator>
|
placeholder={"Search for Machine or Tags"}
|
||||||
<Show
|
renderItem={(item, opts) => (
|
||||||
when={opts.selected}
|
<div class={cx("flex w-full items-center gap-2 px-3 py-2")}>
|
||||||
fallback={<Icon icon="Code" />}
|
<Combobox.ItemIndicator>
|
||||||
>
|
<Show when={opts.selected} fallback={<Icon icon="Code" />}>
|
||||||
<Icon icon="Checkmark" color="primary" inverted />
|
<Icon icon="Checkmark" color="primary" inverted />
|
||||||
</Show>
|
</Show>
|
||||||
</Combobox.ItemIndicator>
|
</Combobox.ItemIndicator>
|
||||||
<Combobox.ItemLabel class="flex items-center gap-2">
|
<Combobox.ItemLabel class="flex items-center gap-2">
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="s"
|
||||||
|
weight="medium"
|
||||||
|
inverted
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<Show when={item.type === "tag" && item}>
|
||||||
|
{(tag) => (
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="s"
|
size="xs"
|
||||||
weight="medium"
|
weight="medium"
|
||||||
inverted
|
inverted
|
||||||
|
color="secondary"
|
||||||
|
tag="div"
|
||||||
>
|
>
|
||||||
{item.label}
|
{tag().members.length}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Show when={item.type === "tag" && item}>
|
)}
|
||||||
{(tag) => (
|
</Show>
|
||||||
<Typography
|
</Combobox.ItemLabel>
|
||||||
hierarchy="body"
|
<Icon
|
||||||
size="xs"
|
class="ml-auto"
|
||||||
weight="medium"
|
icon={item.type === "machine" ? "Machine" : "Tag"}
|
||||||
inverted
|
color="quaternary"
|
||||||
color="secondary"
|
inverted
|
||||||
tag="div"
|
/>
|
||||||
>
|
</div>
|
||||||
{tag().members.length}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</Combobox.ItemLabel>
|
|
||||||
<Icon
|
|
||||||
class="ml-auto"
|
|
||||||
icon={item.type === "machine" ? "Machine" : "Tag"}
|
|
||||||
color="quaternary"
|
|
||||||
inverted
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
height="20rem"
|
|
||||||
virtualizerOptions={{
|
|
||||||
estimateSize: () => 38,
|
|
||||||
}}
|
|
||||||
onChange={(selection) => {
|
|
||||||
const newval = selection.map((s) => s.value);
|
|
||||||
setValue(formStore, field.name, newval);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Field>
|
height="20rem"
|
||||||
|
virtualizerOptions={{
|
||||||
|
estimateSize: () => 38,
|
||||||
|
}}
|
||||||
|
onChange={(selection) => {
|
||||||
|
setMembers(selection);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class={cx(styles.footer, styles.backgroundAlt)}>
|
<div class={cx(styles.footer, styles.backgroundAlt)}>
|
||||||
<Button hierarchy="secondary" type="submit">
|
<Button hierarchy="secondary" type="submit">
|
||||||
@@ -383,7 +511,7 @@ const ConfigureRole = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -410,7 +538,7 @@ export interface InventoryInstance {
|
|||||||
name: string;
|
name: string;
|
||||||
module: {
|
module: {
|
||||||
name: string;
|
name: string;
|
||||||
input: string;
|
input?: string;
|
||||||
};
|
};
|
||||||
roles: Record<string, RoleType>;
|
roles: Record<string, RoleType>;
|
||||||
}
|
}
|
||||||
@@ -429,14 +557,21 @@ export interface ServiceStoreType {
|
|||||||
roles: Record<string, TagType[]>;
|
roles: Record<string, TagType[]>;
|
||||||
currentRole?: string;
|
currentRole?: string;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
handleSubmit: (values: InventoryInstance) => void;
|
handleSubmit: SubmitServiceHandler;
|
||||||
|
action: "create" | "update";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SubmitServiceHandler = (
|
||||||
|
values: InventoryInstance,
|
||||||
|
action: "create" | "update",
|
||||||
|
) => void | Promise<void>;
|
||||||
|
|
||||||
interface ServiceWorkflowProps {
|
interface ServiceWorkflowProps {
|
||||||
initialStep?: ServiceSteps[number]["id"];
|
initialStep?: ServiceSteps[number]["id"];
|
||||||
initialStore?: Partial<ServiceStoreType>;
|
initialStore?: Partial<ServiceStoreType>;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
handleSubmit: (values: InventoryInstance) => void;
|
handleSubmit: SubmitServiceHandler;
|
||||||
|
rootProps?: JSX.HTMLAttributes<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceWorkflow = (props: ServiceWorkflowProps) => {
|
export const ServiceWorkflow = (props: ServiceWorkflowProps) => {
|
||||||
@@ -451,10 +586,25 @@ export const ServiceWorkflow = (props: ServiceWorkflowProps) => {
|
|||||||
} satisfies Partial<ServiceStoreType>,
|
} satisfies Partial<ServiceStoreType>,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
createEffect(() => {
|
||||||
|
if (stepper.currentStep().id !== "select:members") {
|
||||||
|
clearAllHighlights();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ref: HTMLDivElement;
|
||||||
|
useClickOutside(
|
||||||
|
() => ref,
|
||||||
|
() => {
|
||||||
|
if (stepper.currentStep().id === "select:service") props.onClose?.();
|
||||||
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={(e) => (ref = e)}
|
||||||
id="add-service"
|
id="add-service"
|
||||||
class="absolute bottom-full left-1/2 mb-2 -translate-x-1/2"
|
class="absolute bottom-full left-1/2 mb-2 -translate-x-1/2"
|
||||||
|
{...props.rootProps}
|
||||||
>
|
>
|
||||||
<StepperProvider stepper={stepper}>
|
<StepperProvider stepper={stepper}>
|
||||||
<div class="w-[30rem]">{stepper.currentStep().content()}</div>
|
<div class="w-[30rem]">{stepper.currentStep().content()}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user