ui/clan: rework routing concept
This commit is contained in:
@@ -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<RouteSectionProps> = {
|
||||
component: SidebarNav,
|
||||
render: (_: never, context: StoryContext<SidebarNavProps>) => {
|
||||
const history = createMemoryHistory();
|
||||
history.set({ value: "/clan/1/machine/backup" });
|
||||
history.set({ value: "/clans/1/machine/backup" });
|
||||
|
||||
return (
|
||||
<div style="height: 670px;">
|
||||
@@ -93,7 +93,7 @@ const meta: Meta<RouteSectionProps> = {
|
||||
</Suspense>
|
||||
)}
|
||||
>
|
||||
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
|
||||
<Route path="/clans/1/machine/backup" component={(props) => <></>} />
|
||||
</MemoryRouter>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<RouteSectionProps> = (props) => {
|
||||
const params = useParams();
|
||||
const clanURI = clanURIParam(params);
|
||||
return <CubeScene />;
|
||||
export const Clans: Component<RouteSectionProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
<ClanSwitchDog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ClanSwitchDog = () => {
|
||||
const maybeClanURI = useMaybeClanURI();
|
||||
|
||||
return (
|
||||
<SceneDataProvider clanURI={maybeClanURI}>
|
||||
{({ query }) => <CubeScene cubesQuery={query} />}
|
||||
</SceneDataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export type ListMachines = SuccessData<"list_machines">;
|
||||
|
||||
const SceneDataProvider = (props: {
|
||||
clanURI: string | null;
|
||||
children: (sceneData: { query: UseQueryResult<ListMachines> }) => JSX.Element;
|
||||
}) => {
|
||||
const machinesQuery = useQuery<ListMachines>(() => ({
|
||||
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 });
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export const Layout: Component<RouteSectionProps> = (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("/");
|
||||
|
||||
@@ -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: () => (
|
||||
<h1>
|
||||
Clans (index) - (Doesnt really exist, just to keep the scene
|
||||
mounted)
|
||||
</h1>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/:clanURI",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: (props) => <h1>ClanID: {props.params.clanURI}</h1>,
|
||||
},
|
||||
{
|
||||
path: "/machines",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => <h1>Machines (Index)</h1>,
|
||||
},
|
||||
{
|
||||
path: "/:machineID",
|
||||
component: (props) => (
|
||||
<h1>Machine ID: {props.params.machineID}</h1>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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<ListMachines> }) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user