diff --git a/pkgs/clan-app/ui/src/components/Sidebar/SidebarNav.stories.tsx b/pkgs/clan-app/ui/src/components/Sidebar/SidebarNav.stories.tsx index e67d966c8..ba0a3c12a 100644 --- a/pkgs/clan-app/ui/src/components/Sidebar/SidebarNav.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Sidebar/SidebarNav.stories.tsx @@ -14,35 +14,35 @@ import { StoryContext } from "@kachurun/storybook-solid-vite"; const sidebarNavProps: SidebarNavProps = { clanLinks: [ - { label: "Brian's Clan", path: "/clan/1" }, - { label: "Dave's Clan", path: "/clan/2" }, - { label: "Mic92's Clan", path: "/clan/3" }, + { label: "Brian's Clan", path: "/clans/1" }, + { label: "Dave's Clan", path: "/clans/2" }, + { label: "Mic92's Clan", path: "/clans/3" }, ], clanDetail: { label: "Brian's Clan", - settingsPath: "/clan/1/settings", + settingsPath: "/clans/1/settings", machines: [ { label: "Backup & Home", - path: "/clan/1/machine/backup", + path: "/clans/1/machine/backup", serviceCount: 3, status: "Online", }, { label: "Raspberry Pi", - path: "/clan/1/machine/pi", + path: "/clans/1/machine/pi", serviceCount: 1, status: "Offline", }, { label: "Mom's Laptop", - path: "/clan/1/machine/moms-laptop", + path: "/clans/1/machine/moms-laptop", serviceCount: 2, status: "Installed", }, { label: "Dad's Laptop", - path: "/clan/1/machine/dads-laptop", + path: "/clans/1/machine/dads-laptop", serviceCount: 4, status: "Not Installed", }, @@ -52,10 +52,10 @@ const sidebarNavProps: SidebarNavProps = { { label: "Tools", links: [ - { label: "Borgbackup", path: "/clan/1/service/borgbackup" }, - { label: "Syncthing", path: "/clan/1/service/syncthing" }, - { label: "Mumble", path: "/clan/1/service/mumble" }, - { label: "Minecraft", path: "/clan/1/service/minecraft" }, + { label: "Borgbackup", path: "/clans/1/service/borgbackup" }, + { label: "Syncthing", path: "/clans/1/service/syncthing" }, + { label: "Mumble", path: "/clans/1/service/mumble" }, + { label: "Minecraft", path: "/clans/1/service/minecraft" }, ], }, { @@ -81,7 +81,7 @@ const meta: Meta = { component: SidebarNav, render: (_: never, context: StoryContext) => { const history = createMemoryHistory(); - history.set({ value: "/clan/1/machine/backup" }); + history.set({ value: "/clans/1/machine/backup" }); return (
@@ -93,7 +93,7 @@ const meta: Meta = { )} > - <>} /> + <>} />
); diff --git a/pkgs/clan-app/ui/src/hooks/clan.ts b/pkgs/clan-app/ui/src/hooks/clan.ts index faf1953fa..1f8b13d65 100644 --- a/pkgs/clan-app/ui/src/hooks/clan.ts +++ b/pkgs/clan-app/ui/src/hooks/clan.ts @@ -21,7 +21,7 @@ export const selectClanFolder = async () => { }; export const navigateToClan = (navigate: Navigator, uri: string) => { - navigate("/clan/" + window.btoa(uri)); + navigate("/clans/" + window.btoa(uri)); }; export const clanURIParam = (params: Params) => { diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index c9af5cf85..719b510f5 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -1,10 +1,63 @@ -import { RouteSectionProps, useParams } from "@solidjs/router"; -import { Component } from "solid-js"; -import { clanURIParam } from "@/src/hooks/clan"; +import { RouteSectionProps } from "@solidjs/router"; +import { Component, JSX } from "solid-js"; +import { useMaybeClanURI } from "@/src/hooks/clan"; import { CubeScene } from "@/src/scene/cubes"; +import { useQuery, UseQueryResult } from "@tanstack/solid-query"; +import { callApi, SuccessData } from "@/src/hooks/api"; -export const Clan: Component = (props) => { - const params = useParams(); - const clanURI = clanURIParam(params); - return ; +export const Clans: Component = (props) => { + return ( + <> +
+ {props.children} +
+ + + ); +}; + +const ClanSwitchDog = () => { + const maybeClanURI = useMaybeClanURI(); + + return ( + + {({ query }) => } + + ); +}; + +export type ListMachines = SuccessData<"list_machines">; + +const SceneDataProvider = (props: { + clanURI: string | null; + children: (sceneData: { query: UseQueryResult }) => JSX.Element; +}) => { + const machinesQuery = useQuery(() => ({ + queryKey: ["machines"], + enabled: !!props.clanURI, + queryFn: async () => { + if (!props.clanURI) { + return {}; + } + const api = callApi("list_machines", { + flake: { + identifier: props.clanURI, + }, + }); + const result = await api.result; + if (result.status === "error") { + console.error("Error fetching machines:", result.errors); + return {}; + } + return result.data; + }, + })); + + // This component can be used to provide scene data or context if needed + return props.children({ query: machinesQuery }); }; diff --git a/pkgs/clan-app/ui/src/routes/Layout.tsx b/pkgs/clan-app/ui/src/routes/Layout.tsx index 4d18b1e7d..697c52c69 100644 --- a/pkgs/clan-app/ui/src/routes/Layout.tsx +++ b/pkgs/clan-app/ui/src/routes/Layout.tsx @@ -8,7 +8,7 @@ export const Layout: Component = (props) => { // check for an active clan uri and redirect to it on first load const activeURI = activeClanURI(); - if (!props.location.pathname.startsWith("/clan/") && activeURI) { + if (!props.location.pathname.startsWith("/clans/") && activeURI) { navigateToClan(navigate, activeURI); } else { navigate("/"); diff --git a/pkgs/clan-app/ui/src/routes/index.tsx b/pkgs/clan-app/ui/src/routes/index.tsx index b2588ac0d..d7030621c 100644 --- a/pkgs/clan-app/ui/src/routes/index.tsx +++ b/pkgs/clan-app/ui/src/routes/index.tsx @@ -1,6 +1,6 @@ import type { RouteDefinition } from "@solidjs/router/dist/types"; import { Onboarding } from "@/src/routes/Onboarding/Onboarding"; -import { Clan } from "@/src/routes/Clan/Clan"; +import { Clans } from "@/src/routes/Clan/Clan"; export const Routes: RouteDefinition[] = [ { @@ -8,7 +8,42 @@ export const Routes: RouteDefinition[] = [ component: Onboarding, }, { - path: "/clan/:clanURI", - component: Clan, + path: "/clans", + component: Clans, + children: [ + { + path: "/", + component: () => ( +

+ Clans (index) - (Doesnt really exist, just to keep the scene + mounted) +

+ ), + }, + { + path: "/:clanURI", + children: [ + { + path: "/", + component: (props) =>

ClanID: {props.params.clanURI}

, + }, + { + path: "/machines", + children: [ + { + path: "/", + component: () =>

Machines (Index)

, + }, + { + path: "/:machineID", + component: (props) => ( +

Machine ID: {props.params.machineID}

+ ), + }, + ], + }, + ], + }, + ], }, ]; diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index 5fbab30cd..c12a6ba00 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -13,6 +13,9 @@ import * as THREE from "three"; import { Toolbar } from "../components/Toolbar/Toolbar"; import { ToolbarButton } from "../components/Toolbar/ToolbarButton"; import { Divider } from "../components/Divider/Divider"; +import { UseQueryResult } from "@tanstack/solid-query"; +import { ListMachines } from "../routes/Clan/Clan"; +import { callApi } from "../hooks/api"; function garbageCollectGroup(group: THREE.Group) { for (const child of group.children) { @@ -53,7 +56,8 @@ function getFloorPosition( return intersection.toArray() as [number, number, number]; } -export function CubeScene() { +export function CubeScene(props: { cubesQuery: UseQueryResult }) { + // sceneData.cubesQuer let container: HTMLDivElement; let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; @@ -131,6 +135,10 @@ export function CubeScene() { const CREATE_BASE_COLOR = 0x636363; const CREATE_BASE_EMISSIVE = 0xc5fad7; + function getDefaultPosition(): [number, number, number] { + return [0, 0, 0]; + } + function getGridPosition( id: string, index: number, @@ -348,8 +356,10 @@ export function CubeScene() { } // === Add/Delete Cube API === - function addCube() { - const id = crypto.randomUUID(); + function addCube(id: string | undefined = undefined) { + if (!id) { + id = crypto.randomUUID(); + } // Add to creating set first setCreatingIds((prev) => new Set([...prev, id])); @@ -505,6 +515,16 @@ export function CubeScene() { const grid = new THREE.GridHelper(1000, 1000 / 1, 0xe1edef, 0xe1edef); + createEffect(() => { + if (props.cubesQuery.data) { + for (const machineId of Object.keys(props.cubesQuery.data)) { + console.log("Received: ", machineId); + setNextPosition(new THREE.Vector3(...getDefaultPosition())); + addCube(machineId); + } + } + }); + onMount(() => { // Scene setup scene = new THREE.Scene(); @@ -687,19 +707,19 @@ export function CubeScene() { // Snap to grid const snapped = new THREE.Vector3( Math.round(point.x / GRID_SIZE) * GRID_SIZE, - BASE_HEIGHT / 2, + 0, Math.round(point.z / GRID_SIZE) * GRID_SIZE, ); if (!initBase) { // Create initial base mesh if it doesn't exist initBase = createCubeBase( - [snapped.x, BASE_HEIGHT / 2, snapped.z], + [snapped.x, 0, snapped.z], 1, CREATE_BASE_COLOR, CREATE_BASE_EMISSIVE, // Emissive color ); } else { - initBase.position.set(snapped.x, BASE_HEIGHT / 2, snapped.z); + initBase.position.set(snapped.x, 0, snapped.z); } scene.remove(initBase); // Remove any existing base mesh scene.add(initBase); @@ -786,7 +806,28 @@ export function CubeScene() { if (initBase) { scene.remove(initBase); // Remove the base mesh after adding cube setWorldMode("view"); - addCube(); + const res = callApi("create_machine", { + opts: { + clan_dir: { + identifier: "/home/johannes/git/tmp/my-clan", + }, + machine: { + name: "sara", + }, + }, + }); + res.result.then(() => { + props.cubesQuery.refetch(); + const pos = nextBasePosition(); + + if (!pos) { + console.error("No next position set for new cube"); + return; + } + + positionMap.set("sara", pos); + addCube("sara"); + }); } return; } diff --git a/pkgs/clan-app/ui/src/stores/clan.ts b/pkgs/clan-app/ui/src/stores/clan.ts index 74fcd4742..21ca87e0c 100644 --- a/pkgs/clan-app/ui/src/stores/clan.ts +++ b/pkgs/clan-app/ui/src/stores/clan.ts @@ -4,6 +4,11 @@ import { makePersisted } from "@solid-primitives/storage"; interface ClanStoreType { clanURIs: string[]; activeClanURI?: string; + sceneData?: { + [clanURI: string]: { + [machineId: string]: { position: [number, number] }; + }; + }; } const [store, setStore] = makePersisted(