From adea270b27b961dfffb0a3400aa3ca0046f69ad2 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 28 Aug 2025 14:13:05 +0200 Subject: [PATCH 1/3] ui/tagSelect: remove left over console.log --- pkgs/clan-app/ui/src/components/Search/TagSelect.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx b/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx index 590577fd8..4fb798fba 100644 --- a/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx +++ b/pkgs/clan-app/ui/src/components/Search/TagSelect.tsx @@ -55,7 +55,6 @@ export function TagSelect( > aria-label="Fruits"> {(state) => { - console.log("combobox state selected", state.selectedOptions()); return ( Date: Thu, 28 Aug 2025 14:13:26 +0200 Subject: [PATCH 2/3] ui/services: add submit handler to create the instance --- .../src/workflows/Service/Service.stories.tsx | 3 + .../ui/src/workflows/Service/Service.tsx | 94 ++++++++++++------- 2 files changed, 65 insertions(+), 32 deletions(-) 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 0e4e132e1..ef0b5c04e 100644 --- a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx +++ b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx @@ -166,6 +166,9 @@ export const Default: Story = { export const SelectRoleMembers: Story = { render: () => ( { + console.log("Submitted instance:", instance); + }} initialStep="select:members" initialStore={{ currentRole: "peer", diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.tsx b/pkgs/clan-app/ui/src/workflows/Service/Service.tsx index 944dd1e2b..b4a4c0277 100644 --- a/pkgs/clan-app/ui/src/workflows/Service/Service.tsx +++ b/pkgs/clan-app/ui/src/workflows/Service/Service.tsx @@ -18,8 +18,6 @@ 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"; @@ -145,7 +143,6 @@ const ConfigureService = () => { const [store, set] = getStepStore(stepper); const [formStore, { Form, Field }] = createForm({ - // initialValues: props.initialValues, initialValues: { instanceName: "backup-instance-1", }, @@ -157,7 +154,28 @@ const ConfigureService = () => { const options = useOptions(tagsQuery, machinesQuery); const handleSubmit = (values: RolesForm) => { - console.log("Create service submitted with values:", values); + const roles: Record = 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 ( @@ -185,13 +203,19 @@ const ConfigureService = () => { )} - + ); @@ -269,8 +295,6 @@ const ConfigureRole = () => { set("roles", {}); } set("roles", (r) => ({ ...r, [store.currentRole as string]: members })); - console.log("Roles form submitted ", members); - stepper.setActiveStep("view:members"); }; @@ -381,6 +405,21 @@ const 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; +} + +interface RoleType { + machines: Record; + tags: Record; +} + export interface ServiceStoreType { module: { name: string; @@ -390,45 +429,36 @@ export interface ServiceStoreType { roles: Record; currentRole?: string; close: () => void; + handleSubmit: (values: InventoryInstance) => void; } interface ServiceWorkflowProps { initialStep?: ServiceSteps[number]["id"]; initialStore?: Partial; + onClose?: () => void; + handleSubmit: (values: InventoryInstance) => void; } + 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), + close: () => props.onClose?.(), + handleSubmit: props.handleSubmit, } satisfies Partial, }, ); return ( - <> -
- -
- -
{stepper.currentStep().content()}
-
-
-
-
- - setShow(!show())} - description="Add new Service" - name="modules" - icon="Modules" - /> - -
-
- +
+ +
{stepper.currentStep().content()}
+
+
); }; From a92a1a7dd1c12aec6eef1955ad64311bf5191d1f Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 28 Aug 2025 14:13:39 +0200 Subject: [PATCH 3/3] ui/clan: wire up service create --- pkgs/clan-app/ui/src/routes/Clan/Clan.tsx | 60 ++++++++++++++++++++--- pkgs/clan-app/ui/src/scene/cubes.css | 2 + pkgs/clan-app/ui/src/scene/cubes.tsx | 19 +++++-- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index 10116caf8..e2a3d15d7 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -36,6 +36,11 @@ import { createForm, FieldValues, reset } from "@modular-forms/solid"; import { Sidebar } from "@/src/components/Sidebar/Sidebar"; import { UseQueryResult } from "@tanstack/solid-query"; import { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal"; +import { + InventoryInstance, + ServiceWorkflow, +} from "@/src/workflows/Service/Service"; +import { useApiClient } from "@/src/hooks/ApiClient"; interface ClanContextProps { clanURI: string; @@ -179,7 +184,10 @@ const ClanSceneController = (props: RouteSectionProps) => { 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; reject: (err: unknown) => void; } | null>(null); @@ -187,7 +195,15 @@ const ClanSceneController = (props: RouteSectionProps) => { const onCreate = async (): Promise<{ id: string }> => { return new Promise((resolve, reject) => { 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 }; }; - const [showModal, setShowModal] = createSignal(false); - const [loadingError, setLoadingError] = createSignal< { 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 ( <> @@ -274,15 +308,15 @@ const ClanSceneController = (props: RouteSectionProps) => { { setShowModal(false); - dialogHandlers()?.reject(new Error("User cancelled")); + currentPromise()?.reject(new Error("User cancelled")); }} onSubmit={async (values) => { try { const result = await sendCreate(values); - dialogHandlers()?.resolve(result); + currentPromise()?.resolve(result); setShowModal(false); } catch (err) { - dialogHandlers()?.reject(err); + currentPromise()?.reject(err); setShowModal(false); } }} @@ -297,10 +331,22 @@ const ClanSceneController = (props: RouteSectionProps) => { + { + setShowService(false); + currentPromise()?.resolve({ id: "0" }); + }} + /> + + } onCreate={onCreate} clanURI={ctx.clanURI} sceneStore={() => store.sceneData?.[ctx.clanURI]} diff --git a/pkgs/clan-app/ui/src/scene/cubes.css b/pkgs/clan-app/ui/src/scene/cubes.css index 9be825105..063fe6ad8 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.css +++ b/pkgs/clan-app/ui/src/scene/cubes.css @@ -4,6 +4,8 @@ cursor: pointer; } +/*
+ */ .toolbar-container { @apply absolute bottom-10 z-10 w-full; @apply flex justify-center items-center; diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index ef3e322f8..b20024c2a 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -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 * as THREE from "three"; @@ -34,12 +41,14 @@ function garbageCollectGroup(group: THREE.Group) { export function CubeScene(props: { cubesQuery: MachinesQueryResult; onCreate: () => Promise<{ id: string }>; + onAddService: () => Promise<{ id: string }>; selectedIds: Accessor>; onSelect: (v: Set) => void; sceneStore: Accessor; setMachinePos: (machineId: string, pos: [number, number] | null) => void; isLoading: boolean; clanURI: string; + toolbarPopup?: JSX.Element; }) { let container: HTMLDivElement; let scene: THREE.Scene; @@ -213,7 +222,7 @@ export function CubeScene(props: { controls = new MapControls(camera, renderer.domElement); controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled controls.mouseButtons.RIGHT = null; - controls.enableRotate = false; + // controls.enableRotate = false; controls.minZoom = 1.2; controls.maxZoom = 3.5; controls.addEventListener("change", () => { @@ -543,6 +552,9 @@ export function CubeScene(props: { <>
(container = el)} />
+
+ {props.toolbarPopup} +