Merge pull request 'ui/clan: wire up service create' (#5016) from search into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5016
This commit is contained in:
@@ -55,7 +55,6 @@ export function TagSelect<T extends { value: unknown }>(
|
|||||||
>
|
>
|
||||||
<Combobox.Control<T> aria-label="Fruits">
|
<Combobox.Control<T> aria-label="Fruits">
|
||||||
{(state) => {
|
{(state) => {
|
||||||
console.log("combobox state selected", state.selectedOptions());
|
|
||||||
return (
|
return (
|
||||||
<Combobox.Trigger
|
<Combobox.Trigger
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
|||||||
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
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 {
|
||||||
|
InventoryInstance,
|
||||||
|
ServiceWorkflow,
|
||||||
|
} from "@/src/workflows/Service/Service";
|
||||||
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
|
|
||||||
interface ClanContextProps {
|
interface ClanContextProps {
|
||||||
clanURI: string;
|
clanURI: string;
|
||||||
@@ -179,7 +184,10 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
const [showService, setShowService] = createSignal(false);
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = createSignal(false);
|
||||||
|
const [currentPromise, setCurrentPromise] = createSignal<{
|
||||||
resolve: ({ id }: { id: string }) => void;
|
resolve: ({ id }: { id: string }) => void;
|
||||||
reject: (err: unknown) => void;
|
reject: (err: unknown) => void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
@@ -187,7 +195,15 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
const onCreate = async (): Promise<{ id: string }> => {
|
const onCreate = async (): Promise<{ id: string }> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
setDialogHandlers({ resolve, reject });
|
setCurrentPromise({ resolve, reject });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddService = async (): Promise<{ id: string }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setShowService(true);
|
||||||
|
console.log("setting current promise");
|
||||||
|
setCurrentPromise({ resolve, reject });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -217,8 +233,6 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
return { id: values.name };
|
return { id: values.name };
|
||||||
};
|
};
|
||||||
|
|
||||||
const [showModal, setShowModal] = createSignal(false);
|
|
||||||
|
|
||||||
const [loadingError, setLoadingError] = createSignal<
|
const [loadingError, setLoadingError] = createSignal<
|
||||||
{ title: string; description: string } | undefined
|
{ title: string; description: string } | undefined
|
||||||
>();
|
>();
|
||||||
@@ -265,6 +279,26 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const client = useApiClient();
|
||||||
|
const handleSubmitService = async (instance: InventoryInstance) => {
|
||||||
|
console.log("Create Instance", instance);
|
||||||
|
const call = client.fetch("create_service_instance", {
|
||||||
|
flake: {
|
||||||
|
identifier: ctx.clanURI,
|
||||||
|
},
|
||||||
|
module_ref: instance.module,
|
||||||
|
roles: instance.roles,
|
||||||
|
});
|
||||||
|
const result = await call.result;
|
||||||
|
|
||||||
|
if (result.status === "error") {
|
||||||
|
console.error("Error creating service instance", result.errors);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
currentPromise()?.resolve({ id: "0" });
|
||||||
|
setShowService(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={loadingError()}>
|
<Show when={loadingError()}>
|
||||||
@@ -274,15 +308,15 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
<MockCreateMachine
|
<MockCreateMachine
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
dialogHandlers()?.reject(new Error("User cancelled"));
|
currentPromise()?.reject(new Error("User cancelled"));
|
||||||
}}
|
}}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const result = await sendCreate(values);
|
const result = await sendCreate(values);
|
||||||
dialogHandlers()?.resolve(result);
|
currentPromise()?.resolve(result);
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dialogHandlers()?.reject(err);
|
currentPromise()?.reject(err);
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -297,10 +331,22 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CubeScene
|
<CubeScene
|
||||||
|
onAddService={onAddService}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onSelect={onMachineSelect}
|
onSelect={onMachineSelect}
|
||||||
isLoading={ctx.isLoading()}
|
isLoading={ctx.isLoading()}
|
||||||
cubesQuery={ctx.machinesQuery}
|
cubesQuery={ctx.machinesQuery}
|
||||||
|
toolbarPopup={
|
||||||
|
<Show when={showService()}>
|
||||||
|
<ServiceWorkflow
|
||||||
|
handleSubmit={handleSubmitService}
|
||||||
|
onClose={() => {
|
||||||
|
setShowService(false);
|
||||||
|
currentPromise()?.resolve({ id: "0" });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
clanURI={ctx.clanURI}
|
clanURI={ctx.clanURI}
|
||||||
sceneStore={() => store.sceneData?.[ctx.clanURI]}
|
sceneStore={() => store.sceneData?.[ctx.clanURI]}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* <div class="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col items-center">
|
||||||
|
<Show when={show()}> */
|
||||||
.toolbar-container {
|
.toolbar-container {
|
||||||
@apply absolute bottom-10 z-10 w-full;
|
@apply absolute bottom-10 z-10 w-full;
|
||||||
@apply flex justify-center items-center;
|
@apply flex justify-center items-center;
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { createSignal, createEffect, onCleanup, onMount, on } from "solid-js";
|
import {
|
||||||
|
createSignal,
|
||||||
|
createEffect,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
on,
|
||||||
|
JSX,
|
||||||
|
} from "solid-js";
|
||||||
import "./cubes.css";
|
import "./cubes.css";
|
||||||
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
@@ -34,12 +41,14 @@ function garbageCollectGroup(group: THREE.Group) {
|
|||||||
export function CubeScene(props: {
|
export function CubeScene(props: {
|
||||||
cubesQuery: MachinesQueryResult;
|
cubesQuery: MachinesQueryResult;
|
||||||
onCreate: () => Promise<{ id: string }>;
|
onCreate: () => Promise<{ id: string }>;
|
||||||
|
onAddService: () => Promise<{ id: string }>;
|
||||||
selectedIds: Accessor<Set<string>>;
|
selectedIds: Accessor<Set<string>>;
|
||||||
onSelect: (v: Set<string>) => void;
|
onSelect: (v: Set<string>) => void;
|
||||||
sceneStore: Accessor<SceneData>;
|
sceneStore: Accessor<SceneData>;
|
||||||
setMachinePos: (machineId: string, pos: [number, number] | null) => void;
|
setMachinePos: (machineId: string, pos: [number, number] | null) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
clanURI: string;
|
clanURI: string;
|
||||||
|
toolbarPopup?: JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let scene: THREE.Scene;
|
let scene: THREE.Scene;
|
||||||
@@ -213,7 +222,7 @@ export function CubeScene(props: {
|
|||||||
controls = new MapControls(camera, renderer.domElement);
|
controls = new MapControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
||||||
controls.mouseButtons.RIGHT = null;
|
controls.mouseButtons.RIGHT = null;
|
||||||
controls.enableRotate = false;
|
// controls.enableRotate = false;
|
||||||
controls.minZoom = 1.2;
|
controls.minZoom = 1.2;
|
||||||
controls.maxZoom = 3.5;
|
controls.maxZoom = 3.5;
|
||||||
controls.addEventListener("change", () => {
|
controls.addEventListener("change", () => {
|
||||||
@@ -543,6 +552,9 @@ export function CubeScene(props: {
|
|||||||
<>
|
<>
|
||||||
<div class="cubes-scene-container" ref={(el) => (container = el)} />
|
<div class="cubes-scene-container" ref={(el) => (container = el)} />
|
||||||
<div class="toolbar-container">
|
<div class="toolbar-container">
|
||||||
|
<div class="absolute bottom-full left-1/2 mb-2 -translate-x-1/2">
|
||||||
|
{props.toolbarPopup}
|
||||||
|
</div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
description="Select machine"
|
description="Select machine"
|
||||||
@@ -563,7 +575,8 @@ export function CubeScene(props: {
|
|||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
description="Add new Service"
|
description="Add new Service"
|
||||||
name="modules"
|
name="modules"
|
||||||
icon="Modules"
|
icon="Services"
|
||||||
|
onClick={props.onAddService}
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon="Reload"
|
icon="Reload"
|
||||||
|
|||||||
@@ -166,6 +166,9 @@ export const Default: Story = {
|
|||||||
export const SelectRoleMembers: Story = {
|
export const SelectRoleMembers: Story = {
|
||||||
render: () => (
|
render: () => (
|
||||||
<ServiceWorkflow
|
<ServiceWorkflow
|
||||||
|
handleSubmit={(instance) => {
|
||||||
|
console.log("Submitted instance:", instance);
|
||||||
|
}}
|
||||||
initialStep="select:members"
|
initialStep="select:members"
|
||||||
initialStore={{
|
initialStore={{
|
||||||
currentRole: "peer",
|
currentRole: "peer",
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ 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 { Toolbar } from "@/src/components/Toolbar/Toolbar";
|
|
||||||
import { ToolbarButton } from "@/src/components/Toolbar/ToolbarButton";
|
|
||||||
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, setValue } from "@modular-forms/solid";
|
||||||
@@ -145,7 +143,6 @@ const ConfigureService = () => {
|
|||||||
const [store, set] = getStepStore<ServiceStoreType>(stepper);
|
const [store, set] = getStepStore<ServiceStoreType>(stepper);
|
||||||
|
|
||||||
const [formStore, { Form, Field }] = createForm<RolesForm>({
|
const [formStore, { Form, Field }] = createForm<RolesForm>({
|
||||||
// initialValues: props.initialValues,
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
instanceName: "backup-instance-1",
|
instanceName: "backup-instance-1",
|
||||||
},
|
},
|
||||||
@@ -157,7 +154,28 @@ const ConfigureService = () => {
|
|||||||
const options = useOptions(tagsQuery, machinesQuery);
|
const options = useOptions(tagsQuery, machinesQuery);
|
||||||
|
|
||||||
const handleSubmit = (values: RolesForm) => {
|
const handleSubmit = (values: RolesForm) => {
|
||||||
console.log("Create service submitted with values:", values);
|
const roles: Record<string, RoleType> = Object.fromEntries(
|
||||||
|
Object.entries(store.roles).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
machines: Object.fromEntries(
|
||||||
|
value.filter((v) => v.type === "machine").map((v) => [v.label, {}]),
|
||||||
|
),
|
||||||
|
tags: Object.fromEntries(
|
||||||
|
value.filter((v) => v.type === "tag").map((v) => [v.label, {}]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
store.handleSubmit({
|
||||||
|
name: values.instanceName,
|
||||||
|
module: {
|
||||||
|
name: store.module.name,
|
||||||
|
input: store.module.input,
|
||||||
|
},
|
||||||
|
roles,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -185,13 +203,19 @@ const ConfigureService = () => {
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<Button icon="Close" color="primary" ghost size="s" class="ml-auto" />
|
<Button
|
||||||
|
icon="Close"
|
||||||
|
color="primary"
|
||||||
|
ghost
|
||||||
|
size="s"
|
||||||
|
class="ml-auto"
|
||||||
|
onClick={store.close}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.content}>
|
<div class={styles.content}>
|
||||||
<For each={Object.keys(store.module.raw?.info.roles || {})}>
|
<For each={Object.keys(store.module.raw?.info.roles || {})}>
|
||||||
{(role) => {
|
{(role) => {
|
||||||
const values = store.roles?.[role] || [];
|
const values = store.roles?.[role] || [];
|
||||||
console.log("Role members:", role, values, "from", options());
|
|
||||||
return (
|
return (
|
||||||
<TagSelect<TagType>
|
<TagSelect<TagType>
|
||||||
label={role}
|
label={role}
|
||||||
@@ -221,7 +245,9 @@ const ConfigureService = () => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<div class={cx(styles.footer, styles.backgroundAlt)}>
|
<div class={cx(styles.footer, styles.backgroundAlt)}>
|
||||||
<Button hierarchy="secondary">Add Service</Button>
|
<Button hierarchy="secondary" type="submit">
|
||||||
|
Add Service
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
@@ -269,8 +295,6 @@ const ConfigureRole = () => {
|
|||||||
set("roles", {});
|
set("roles", {});
|
||||||
}
|
}
|
||||||
set("roles", (r) => ({ ...r, [store.currentRole as string]: members }));
|
set("roles", (r) => ({ ...r, [store.currentRole as string]: members }));
|
||||||
console.log("Roles form submitted ", members);
|
|
||||||
|
|
||||||
stepper.setActiveStep("view:members");
|
stepper.setActiveStep("view:members");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -381,6 +405,21 @@ const steps = [
|
|||||||
|
|
||||||
export type ServiceSteps = typeof steps;
|
export type ServiceSteps = typeof steps;
|
||||||
|
|
||||||
|
// TODO: Ideally we would impot this from a backend model package
|
||||||
|
export interface InventoryInstance {
|
||||||
|
name: string;
|
||||||
|
module: {
|
||||||
|
name: string;
|
||||||
|
input: string;
|
||||||
|
};
|
||||||
|
roles: Record<string, RoleType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleType {
|
||||||
|
machines: Record<string, { settings?: unknown }>;
|
||||||
|
tags: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServiceStoreType {
|
export interface ServiceStoreType {
|
||||||
module: {
|
module: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -390,45 +429,36 @@ export interface ServiceStoreType {
|
|||||||
roles: Record<string, TagType[]>;
|
roles: Record<string, TagType[]>;
|
||||||
currentRole?: string;
|
currentRole?: string;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
|
handleSubmit: (values: InventoryInstance) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceWorkflowProps {
|
interface ServiceWorkflowProps {
|
||||||
initialStep?: ServiceSteps[number]["id"];
|
initialStep?: ServiceSteps[number]["id"];
|
||||||
initialStore?: Partial<ServiceStoreType>;
|
initialStore?: Partial<ServiceStoreType>;
|
||||||
|
onClose?: () => void;
|
||||||
|
handleSubmit: (values: InventoryInstance) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceWorkflow = (props: ServiceWorkflowProps) => {
|
export const ServiceWorkflow = (props: ServiceWorkflowProps) => {
|
||||||
const [show, setShow] = createSignal(false);
|
|
||||||
const stepper = createStepper(
|
const stepper = createStepper(
|
||||||
{ steps },
|
{ steps },
|
||||||
{
|
{
|
||||||
initialStep: props.initialStep || "select:service",
|
initialStep: props.initialStep || "select:service",
|
||||||
initialStoreData: {
|
initialStoreData: {
|
||||||
...props.initialStore,
|
...props.initialStore,
|
||||||
close: () => setShow(false),
|
close: () => props.onClose?.(),
|
||||||
|
handleSubmit: props.handleSubmit,
|
||||||
} satisfies Partial<ServiceStoreType>,
|
} satisfies Partial<ServiceStoreType>,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div class="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col items-center">
|
id="add-service"
|
||||||
<Show when={show()}>
|
class="absolute bottom-full left-1/2 mb-2 -translate-x-1/2"
|
||||||
<div class="absolute bottom-full left-1/2 mb-2 -translate-x-1/2">
|
>
|
||||||
<StepperProvider stepper={stepper}>
|
<StepperProvider stepper={stepper}>
|
||||||
<div class="w-[30rem]">{stepper.currentStep().content()}</div>
|
<div class="w-[30rem]">{stepper.currentStep().content()}</div>
|
||||||
</StepperProvider>
|
</StepperProvider>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
<div class="flex justify-center space-x-4">
|
|
||||||
<Toolbar>
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => setShow(!show())}
|
|
||||||
description="Add new Service"
|
|
||||||
name="modules"
|
|
||||||
icon="Modules"
|
|
||||||
/>
|
|
||||||
</Toolbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user