feat(ui): services in sidebar and sidebar pane
This commit is contained in:
committed by
Johannes Kirschbauer
parent
f1de0e28ff
commit
6d8ea1f2c5
@@ -5,7 +5,11 @@ import Icon from "../Icon/Icon";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { For, Show } from "solid-js";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
||||
import {
|
||||
buildMachinePath,
|
||||
buildServicePath,
|
||||
useClanURI,
|
||||
} from "@/src/hooks/clan";
|
||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { SidebarProps } from "./Sidebar";
|
||||
import { Button } from "../Button/Button";
|
||||
@@ -33,19 +37,19 @@ const MachineRoute = (props: MachineProps) => {
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
inverted
|
||||
>
|
||||
{props.name}
|
||||
</Typography>
|
||||
<MachineStatus status={status()} />
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center gap-1">
|
||||
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
|
||||
<Icon icon="Flash" size="0.75rem" inverted color="tertiary" />
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="s"
|
||||
inverted={true}
|
||||
inverted
|
||||
color="primary"
|
||||
>
|
||||
{props.serviceCount}
|
||||
@@ -56,18 +60,13 @@ const MachineRoute = (props: MachineProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarBody = (props: SidebarProps) => {
|
||||
const clanURI = useClanURI();
|
||||
|
||||
const Machines = () => {
|
||||
const ctx = useClanContext();
|
||||
if (!ctx) {
|
||||
throw new Error("ClanContext not found");
|
||||
}
|
||||
|
||||
const sectionLabels = (props.staticSections || []).map(
|
||||
(section) => section.title,
|
||||
);
|
||||
|
||||
// controls which sections are open by default
|
||||
// we want them all to be open by default
|
||||
const defaultAccordionValues = ["your-machines", ...sectionLabels];
|
||||
const clanURI = ctx.clanURI;
|
||||
|
||||
const machines = () => {
|
||||
if (!ctx.machinesQuery.isSuccess) {
|
||||
@@ -79,13 +78,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="sidebar-body">
|
||||
<Accordion
|
||||
class="accordion"
|
||||
multiple
|
||||
defaultValue={defaultAccordionValues}
|
||||
>
|
||||
<Accordion.Item class="item" value="your-machines">
|
||||
<Accordion.Item class="item" value="machines">
|
||||
<Accordion.Header class="header">
|
||||
<Accordion.Trigger class="trigger">
|
||||
<Typography
|
||||
@@ -93,17 +86,12 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
inverted={true}
|
||||
inverted
|
||||
color="tertiary"
|
||||
>
|
||||
Your Machines
|
||||
</Typography>
|
||||
<Icon
|
||||
icon="CaretDown"
|
||||
color="tertiary"
|
||||
inverted={true}
|
||||
size="0.75rem"
|
||||
/>
|
||||
<Icon icon="CaretDown" color="tertiary" inverted size="0.75rem" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="content">
|
||||
@@ -111,12 +99,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
when={machines()}
|
||||
fallback={
|
||||
<div class="flex w-full flex-col items-center justify-center gap-2.5">
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="medium"
|
||||
inverted
|
||||
>
|
||||
<Typography hierarchy="body" size="s" weight="medium" inverted>
|
||||
No machines yet
|
||||
</Typography>
|
||||
<Button
|
||||
@@ -137,7 +120,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
clanURI={clanURI}
|
||||
machineID={id}
|
||||
name={machine.data.name || id}
|
||||
serviceCount={0}
|
||||
serviceCount={machine?.instance_refs?.length ?? 0}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -145,6 +128,103 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
</Show>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServiceRoute = (props: {
|
||||
clanURI: string;
|
||||
machineName?: string;
|
||||
label: string;
|
||||
id: string;
|
||||
module: { input?: string | null | undefined; name: string };
|
||||
}) => (
|
||||
<A href={buildServicePath(props)} replace={true}>
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="s"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
inverted
|
||||
>
|
||||
{props.label}
|
||||
</Typography>
|
||||
</div>
|
||||
</A>
|
||||
);
|
||||
|
||||
const Services = () => {
|
||||
const ctx = useClanContext();
|
||||
if (!ctx) {
|
||||
throw new Error("ClanContext not found");
|
||||
}
|
||||
|
||||
const serviceInstances = () => {
|
||||
if (!ctx.serviceInstancesQuery.isSuccess) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(ctx.serviceInstancesQuery.data).map(
|
||||
([id, instance]) => {
|
||||
const moduleName = instance.module.name;
|
||||
|
||||
const label = moduleName == id ? moduleName : `${moduleName} (${id})`;
|
||||
|
||||
return { id, label, module: instance.module };
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion.Item class="item" value="services">
|
||||
<Accordion.Header class="header">
|
||||
<Accordion.Trigger class="trigger">
|
||||
<Typography
|
||||
class="section-title"
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
inverted
|
||||
color="tertiary"
|
||||
>
|
||||
Services
|
||||
</Typography>
|
||||
<Icon icon="CaretDown" color="tertiary" inverted size="0.75rem" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="content">
|
||||
<nav>
|
||||
<For each={serviceInstances()}>
|
||||
{(instance) => <ServiceRoute clanURI={ctx.clanURI} {...instance} />}
|
||||
</For>
|
||||
</nav>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarBody = (props: SidebarProps) => {
|
||||
const clanURI = useClanURI();
|
||||
|
||||
const ctx = useClanContext();
|
||||
|
||||
const sectionLabels = (props.staticSections || []).map(
|
||||
(section) => section.title,
|
||||
);
|
||||
|
||||
// controls which sections are open by default
|
||||
// we want them all to be open by default
|
||||
const defaultAccordionValues = ["machines", "services", ...sectionLabels];
|
||||
|
||||
return (
|
||||
<div class="sidebar-body">
|
||||
<Accordion
|
||||
class="accordion"
|
||||
multiple
|
||||
defaultValue={defaultAccordionValues}
|
||||
>
|
||||
<Machines />
|
||||
<Services />
|
||||
|
||||
<For each={props.staticSections}>
|
||||
{(section) => (
|
||||
@@ -156,7 +236,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
inverted={true}
|
||||
inverted
|
||||
color="tertiary"
|
||||
>
|
||||
{section.title}
|
||||
@@ -164,7 +244,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
<Icon
|
||||
icon="CaretDown"
|
||||
color="tertiary"
|
||||
inverted={true}
|
||||
inverted
|
||||
size="0.75rem"
|
||||
/>
|
||||
</Accordion.Trigger>
|
||||
@@ -179,7 +259,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
inverted
|
||||
>
|
||||
{link.label}
|
||||
</Typography>
|
||||
|
||||
@@ -30,6 +30,24 @@ export const buildClanPath = (clanURI: string) => {
|
||||
export const buildMachinePath = (clanURI: string, name: string) =>
|
||||
buildClanPath(clanURI) + "/machines/" + name;
|
||||
|
||||
export const buildServicePath = (props: {
|
||||
clanURI: string;
|
||||
machineName?: string;
|
||||
id: string;
|
||||
module: {
|
||||
input?: string | null | undefined;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
const { clanURI, machineName, id, module } = props;
|
||||
const result =
|
||||
(machineName
|
||||
? buildMachinePath(clanURI, machineName)
|
||||
: buildClanPath(clanURI)) +
|
||||
`/services/${module.input ?? "clan"}/${module.name}`;
|
||||
return id == module.name ? result : result + "/" + id;
|
||||
};
|
||||
|
||||
export const navigateToClan = (navigate: Navigator, clanURI: string) => {
|
||||
const path = buildClanPath(clanURI);
|
||||
console.log("Navigating to clan", clanURI, path);
|
||||
@@ -64,7 +82,21 @@ export const machineNameParam = (params: Params) => {
|
||||
return params.machineName;
|
||||
};
|
||||
|
||||
export const inputParam = (params: Params) => params.input;
|
||||
export const nameParam = (params: Params) => params.name;
|
||||
export const idParam = (params: Params) => params.id;
|
||||
|
||||
export const useMachineName = (): string => machineNameParam(useParams());
|
||||
export const useInputParam = (): string => inputParam(useParams());
|
||||
export const useNameParam = (): string => nameParam(useParams());
|
||||
|
||||
export const maybeUseIdParam = (): string | null => {
|
||||
const params = useParams();
|
||||
if (params.id === undefined) {
|
||||
return null;
|
||||
}
|
||||
return idParam(params);
|
||||
};
|
||||
|
||||
export const maybeUseMachineName = (): string | null => {
|
||||
const params = useParams();
|
||||
|
||||
@@ -25,6 +25,9 @@ export type MachineStatus = MachineState["status"];
|
||||
export type ListMachines = SuccessData<"list_machines">;
|
||||
export type MachineDetails = SuccessData<"get_machine_details">;
|
||||
|
||||
export type ListServiceModules = SuccessData<"list_service_modules">;
|
||||
export type ListServiceInstances = SuccessData<"list_service_instances">;
|
||||
|
||||
export interface MachineDetail {
|
||||
tags: Tags;
|
||||
machine: Machine;
|
||||
@@ -166,6 +169,54 @@ export const useMachineStateQuery = (clanURI: string, machineName: string) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const useServiceModulesQuery = (clanURI: string) => {
|
||||
const client = useApiClient();
|
||||
|
||||
return useQuery<ListServiceModules>(() => ({
|
||||
queryKey: ["clans", encodeBase64(clanURI), "service_modules"],
|
||||
queryFn: async () => {
|
||||
const call = client.fetch("list_service_modules", {
|
||||
flake: {
|
||||
identifier: clanURI,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await call.result;
|
||||
if (result.status === "error") {
|
||||
throw new Error(
|
||||
"Error fetching service modules: " + result.errors[0].message,
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const useServiceInstancesQuery = (clanURI: string) => {
|
||||
const client = useApiClient();
|
||||
|
||||
return useQuery<ListServiceInstances>(() => ({
|
||||
queryKey: ["clans", encodeBase64(clanURI), "service_instances"],
|
||||
queryFn: async () => {
|
||||
const call = client.fetch("list_service_instances", {
|
||||
flake: {
|
||||
identifier: clanURI,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await call.result;
|
||||
if (result.status === "error") {
|
||||
throw new Error(
|
||||
"Error fetching service instances: " + result.errors[0].message,
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const useMachineDetailsQuery = (
|
||||
clanURI: string,
|
||||
machineName: string,
|
||||
|
||||
@@ -17,13 +17,15 @@ import {
|
||||
useClanURI,
|
||||
useMachineName,
|
||||
} from "@/src/hooks/clan";
|
||||
import { CubeScene, setWorldMode, worldMode } from "@/src/scene/cubes";
|
||||
import { CubeScene } from "@/src/scene/cubes";
|
||||
import {
|
||||
ClanDetails,
|
||||
ListServiceInstances,
|
||||
MachinesQueryResult,
|
||||
useClanDetailsQuery,
|
||||
useClanListQuery,
|
||||
useMachinesQuery,
|
||||
useServiceInstancesQuery,
|
||||
} from "@/src/hooks/queries";
|
||||
import { clanURIs, setStore, store } from "@/src/stores/clan";
|
||||
import { produce } from "solid-js/store";
|
||||
@@ -41,18 +43,24 @@ import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import toast from "solid-toast";
|
||||
import { AddMachine } from "@/src/workflows/AddMachine/AddMachine";
|
||||
|
||||
export type WorldMode = "default" | "select" | "service" | "create" | "move";
|
||||
|
||||
interface ClanContextProps {
|
||||
clanURI: string;
|
||||
machinesQuery: MachinesQueryResult;
|
||||
activeClanQuery: UseQueryResult<ClanDetails>;
|
||||
otherClanQueries: UseQueryResult<ClanDetails>[];
|
||||
allClansQueries: UseQueryResult<ClanDetails>[];
|
||||
serviceInstancesQuery: UseQueryResult<ListServiceInstances>;
|
||||
|
||||
isLoading(): boolean;
|
||||
isError(): boolean;
|
||||
|
||||
showAddMachine(): boolean;
|
||||
setShowAddMachine(value: boolean): void;
|
||||
|
||||
worldMode(): WorldMode;
|
||||
setWorldMode(mode: WorldMode): void;
|
||||
}
|
||||
|
||||
function createClanContext(
|
||||
@@ -60,10 +68,13 @@ function createClanContext(
|
||||
machinesQuery: MachinesQueryResult,
|
||||
activeClanQuery: UseQueryResult<ClanDetails>,
|
||||
otherClanQueries: UseQueryResult<ClanDetails>[],
|
||||
serviceInstancesQuery: UseQueryResult<ListServiceInstances>,
|
||||
) {
|
||||
const [worldMode, setWorldMode] = createSignal<WorldMode>("select");
|
||||
const [showAddMachine, setShowAddMachine] = createSignal(false);
|
||||
|
||||
const allClansQueries = [activeClanQuery, ...otherClanQueries];
|
||||
const allQueries = [machinesQuery, ...allClansQueries];
|
||||
const allQueries = [machinesQuery, ...allClansQueries, serviceInstancesQuery];
|
||||
|
||||
return {
|
||||
clanURI,
|
||||
@@ -71,10 +82,13 @@ function createClanContext(
|
||||
activeClanQuery,
|
||||
otherClanQueries,
|
||||
allClansQueries,
|
||||
serviceInstancesQuery,
|
||||
isLoading: () => allQueries.some((q) => q.isLoading),
|
||||
isError: () => activeClanQuery.isError,
|
||||
showAddMachine,
|
||||
setShowAddMachine,
|
||||
setWorldMode,
|
||||
worldMode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,12 +118,14 @@ export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
);
|
||||
|
||||
const machinesQuery = useMachinesQuery(clanURI);
|
||||
const serviceInstancesQuery = useServiceInstancesQuery(clanURI);
|
||||
|
||||
const ctx = createClanContext(
|
||||
clanURI,
|
||||
machinesQuery,
|
||||
activeClanQuery,
|
||||
otherClanQueries,
|
||||
serviceInstancesQuery,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -217,7 +233,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
||||
console.error("Error creating service instance", result.errors);
|
||||
}
|
||||
toast.success("Created");
|
||||
setWorldMode("select");
|
||||
ctx.setWorldMode("select");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -254,11 +270,11 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
||||
isLoading={ctx.isLoading()}
|
||||
cubesQuery={ctx.machinesQuery}
|
||||
toolbarPopup={
|
||||
<Show when={worldMode() === "service"}>
|
||||
<Show when={ctx.worldMode() === "service"}>
|
||||
<ServiceWorkflow
|
||||
handleSubmit={handleSubmitService}
|
||||
onClose={() => {
|
||||
setWorldMode("select");
|
||||
ctx.setWorldMode("select");
|
||||
currentPromise()?.resolve({ id: "0" });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SidebarMachineStatus } from "@/src/components/Sidebar/SidebarMachineSta
|
||||
import { SidebarSectionInstall } from "@/src/components/Sidebar/SidebarSectionInstall";
|
||||
|
||||
import styles from "./Machine.module.css";
|
||||
import { SectionServices } from "@/src/routes/Machine/SectionServices";
|
||||
|
||||
export const Machine = (props: RouteSectionProps) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -62,6 +63,7 @@ export const Machine = (props: RouteSectionProps) => {
|
||||
/>
|
||||
<SectionGeneral {...sectionProps} />
|
||||
<SectionTags {...sectionProps} />
|
||||
<SectionServices />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.sectionServices {
|
||||
@apply overflow-hidden flex flex-col;
|
||||
@apply bg-inv-4 rounded-md;
|
||||
|
||||
nav * {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
nav > a {
|
||||
@apply block w-full px-2 py-1.5 min-h-7 my-2 rounded-md;
|
||||
|
||||
&:first-child {
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
theme(colors.secondary.900),
|
||||
60%,
|
||||
theme(colors.secondary.600) 100%
|
||||
);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-inv-acc-2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-inv-acc-3;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-inv-acc-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
pkgs/clan-app/ui/src/routes/Machine/SectionServices.tsx
Normal file
56
pkgs/clan-app/ui/src/routes/Machine/SectionServices.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { SidebarSection } from "@/src/components/Sidebar/SidebarSection";
|
||||
import { useClanContext } from "@/src/routes/Clan/Clan";
|
||||
import { For, Show } from "solid-js";
|
||||
import { useMachineName } from "@/src/hooks/clan";
|
||||
import { ServiceRoute } from "@/src/components/Sidebar/SidebarBody";
|
||||
import styles from "./SectionServices.module.css";
|
||||
|
||||
export const SectionServices = () => {
|
||||
const ctx = useClanContext();
|
||||
|
||||
const services = () => {
|
||||
if (!(ctx.machinesQuery.isSuccess && ctx.serviceInstancesQuery.isSuccess)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const machineName = useMachineName();
|
||||
if (!ctx.machinesQuery.data[machineName]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (ctx.machinesQuery.data[machineName].instance_refs ?? []).map(
|
||||
(id) => {
|
||||
const module = ctx.serviceInstancesQuery.data?.[id].module;
|
||||
if (!module) {
|
||||
throw new Error(`Service instance ${id} has no module`);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
module,
|
||||
label: module.name == id ? module.name : `${module.name} (${id})`,
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={ctx.serviceInstancesQuery.isSuccess}>
|
||||
<SidebarSection title="Services">
|
||||
<div class={styles.sectionServices}>
|
||||
<nav>
|
||||
<For each={services()}>
|
||||
{(instance) => (
|
||||
<ServiceRoute
|
||||
clanURI={ctx.clanURI}
|
||||
machineName={useMachineName()}
|
||||
{...instance}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</nav>
|
||||
</div>
|
||||
</SidebarSection>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
22
pkgs/clan-app/ui/src/routes/Service/Service.tsx
Normal file
22
pkgs/clan-app/ui/src/routes/Service/Service.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { maybeUseIdParam, useInputParam, useNameParam } from "@/src/hooks/clan";
|
||||
import { createEffect } from "solid-js";
|
||||
import { useClanContext } from "@/src/routes/Clan/Clan";
|
||||
|
||||
export const Service = (props: RouteSectionProps) => {
|
||||
const ctx = useClanContext();
|
||||
|
||||
console.log("service route");
|
||||
|
||||
createEffect(() => {
|
||||
const input = useInputParam();
|
||||
const name = useNameParam();
|
||||
const id = maybeUseIdParam();
|
||||
|
||||
ctx.setWorldMode("service");
|
||||
|
||||
console.log("service", input, name, id);
|
||||
});
|
||||
|
||||
return <>h1</>;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import type { RouteDefinition } from "@solidjs/router/dist/types";
|
||||
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
|
||||
import { Clan } from "@/src/routes/Clan/Clan";
|
||||
import { Machine } from "@/src/routes/Machine/Machine";
|
||||
import { Service } from "@/src/routes/Service/Service";
|
||||
|
||||
export const Routes: RouteDefinition[] = [
|
||||
{
|
||||
@@ -30,6 +31,19 @@ export const Routes: RouteDefinition[] = [
|
||||
{
|
||||
path: "/machines/:machineName",
|
||||
component: Machine,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
path: "/services/:input/:name/:id?",
|
||||
component: Service,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/services/:input/:name/:id?",
|
||||
component: Service,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
setHighlightGroups,
|
||||
} from "./highlightStore";
|
||||
import { createMachineMesh } from "./MachineRepr";
|
||||
import { useClanContext } from "@/src/routes/Clan/Clan";
|
||||
|
||||
function intersectMachines(
|
||||
event: MouseEvent,
|
||||
@@ -94,12 +95,6 @@ export function useMachineClick() {
|
||||
return lastClickedMachine;
|
||||
}
|
||||
|
||||
/*Gloabl signal*/
|
||||
const [worldMode, setWorldMode] = createSignal<
|
||||
"default" | "select" | "service" | "create" | "move"
|
||||
>("select");
|
||||
export { worldMode, setWorldMode };
|
||||
|
||||
export function CubeScene(props: {
|
||||
cubesQuery: MachinesQueryResult;
|
||||
onCreate: () => Promise<{ id: string }>;
|
||||
@@ -111,6 +106,8 @@ export function CubeScene(props: {
|
||||
clanURI: string;
|
||||
toolbarPopup?: JSX.Element;
|
||||
}) {
|
||||
const ctx = useClanContext();
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let scene: THREE.Scene;
|
||||
let camera: THREE.OrthographicCamera;
|
||||
@@ -440,7 +437,7 @@ export function CubeScene(props: {
|
||||
updateCameraInfo();
|
||||
|
||||
createEffect(
|
||||
on(worldMode, (mode) => {
|
||||
on(ctx.worldMode, (mode) => {
|
||||
if (mode === "create") {
|
||||
actionBase!.visible = true;
|
||||
} else {
|
||||
@@ -466,7 +463,7 @@ export function CubeScene(props: {
|
||||
// - Select/deselects a cube in mode
|
||||
// - Creates a new cube in "create" mode
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (worldMode() === "create") {
|
||||
if (ctx.worldMode() === "create") {
|
||||
props
|
||||
.onCreate()
|
||||
.then(({ id }) => {
|
||||
@@ -484,16 +481,16 @@ export function CubeScene(props: {
|
||||
.finally(() => {
|
||||
if (actionBase) actionBase.visible = false;
|
||||
|
||||
setWorldMode("select");
|
||||
ctx.setWorldMode("select");
|
||||
});
|
||||
}
|
||||
if (worldMode() === "move") {
|
||||
if (ctx.worldMode() === "move") {
|
||||
const currId = menuIntersection().at(0);
|
||||
const pos = cursorPosition();
|
||||
if (!currId || !pos) return;
|
||||
|
||||
props.setMachinePos(currId, pos);
|
||||
setWorldMode("select");
|
||||
ctx.setWorldMode("select");
|
||||
clearHighlight("move");
|
||||
}
|
||||
|
||||
@@ -513,13 +510,13 @@ export function CubeScene(props: {
|
||||
|
||||
if (!id) return;
|
||||
|
||||
if (worldMode() === "select") props.onSelect(new Set<string>([id]));
|
||||
if (ctx.worldMode() === "select") props.onSelect(new Set<string>([id]));
|
||||
|
||||
emitMachineClick(id); // notify subscribers
|
||||
} else {
|
||||
emitMachineClick(null);
|
||||
|
||||
if (worldMode() === "select") props.onSelect(new Set<string>());
|
||||
if (ctx.worldMode() === "select") props.onSelect(new Set<string>());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -561,7 +558,7 @@ export function CubeScene(props: {
|
||||
if (e.button === 0) {
|
||||
// Left button
|
||||
|
||||
if (worldMode() === "select" && machines.length) {
|
||||
if (ctx.worldMode() === "select" && machines.length) {
|
||||
// Disable controls to avoid conflict
|
||||
controls.enabled = false;
|
||||
|
||||
@@ -571,7 +568,7 @@ export function CubeScene(props: {
|
||||
// Set machine as flying
|
||||
setHighlightGroups({ move: new Set(machines) });
|
||||
|
||||
setWorldMode("move");
|
||||
ctx.setWorldMode("move");
|
||||
renderLoop.requestRender();
|
||||
}, 500);
|
||||
setCancelMove(cancelMove);
|
||||
@@ -597,14 +594,14 @@ export function CubeScene(props: {
|
||||
// Always re-enable controls
|
||||
controls.enabled = true;
|
||||
|
||||
if (worldMode() === "move") {
|
||||
if (ctx.worldMode() === "move") {
|
||||
// Set machine as not flying
|
||||
props.setMachinePos(
|
||||
highlightGroups["move"].values().next().value!,
|
||||
cursorPosition() || null,
|
||||
);
|
||||
clearHighlight("move");
|
||||
setWorldMode("select");
|
||||
ctx.setWorldMode("select");
|
||||
renderLoop.requestRender();
|
||||
}
|
||||
}
|
||||
@@ -691,13 +688,14 @@ export function CubeScene(props: {
|
||||
|
||||
const onAddClick = (event: MouseEvent) => {
|
||||
setPositionMode("grid");
|
||||
setWorldMode("create");
|
||||
ctx.setWorldMode("create");
|
||||
renderLoop.requestRender();
|
||||
};
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!(worldMode() === "create" || worldMode() === "move")) return;
|
||||
if (!(ctx.worldMode() === "create" || ctx.worldMode() === "move")) return;
|
||||
|
||||
const actionRepr = worldMode() === "create" ? actionBase : actionMachine;
|
||||
const actionRepr =
|
||||
ctx.worldMode() === "create" ? actionBase : actionMachine;
|
||||
if (!actionRepr) return;
|
||||
|
||||
actionRepr.visible = true;
|
||||
@@ -732,7 +730,7 @@ export function CubeScene(props: {
|
||||
}
|
||||
};
|
||||
const handleMenuSelect = (mode: "move") => {
|
||||
setWorldMode(mode);
|
||||
ctx.setWorldMode(mode);
|
||||
setHighlightGroups({ move: new Set(menuIntersection()) });
|
||||
|
||||
// Find the position of the first selected machine
|
||||
@@ -752,7 +750,7 @@ export function CubeScene(props: {
|
||||
};
|
||||
|
||||
createEffect(
|
||||
on(worldMode, (mode) => {
|
||||
on(ctx.worldMode, (mode) => {
|
||||
console.log("World mode changed to", mode);
|
||||
}),
|
||||
);
|
||||
@@ -775,10 +773,10 @@ export function CubeScene(props: {
|
||||
<div
|
||||
class={cx(
|
||||
"cubes-scene-container",
|
||||
worldMode() === "default" && "cursor-no-drop",
|
||||
worldMode() === "select" && "cursor-pointer",
|
||||
worldMode() === "service" && "cursor-pointer",
|
||||
worldMode() === "create" && "cursor-cell",
|
||||
ctx.worldMode() === "default" && "cursor-no-drop",
|
||||
ctx.worldMode() === "select" && "cursor-pointer",
|
||||
ctx.worldMode() === "service" && "cursor-pointer",
|
||||
ctx.worldMode() === "create" && "cursor-cell",
|
||||
isDragging() && "!cursor-grabbing",
|
||||
)}
|
||||
ref={(el) => (container = el)}
|
||||
@@ -792,24 +790,24 @@ export function CubeScene(props: {
|
||||
description="Select machine"
|
||||
name="Select"
|
||||
icon="Cursor"
|
||||
onClick={() => setWorldMode("select")}
|
||||
selected={worldMode() === "select"}
|
||||
onClick={() => ctx.setWorldMode("select")}
|
||||
selected={ctx.worldMode() === "select"}
|
||||
/>
|
||||
<ToolbarButton
|
||||
description="Create new machine"
|
||||
name="new-machine"
|
||||
icon="NewMachine"
|
||||
onClick={onAddClick}
|
||||
selected={worldMode() === "create"}
|
||||
selected={ctx.worldMode() === "create"}
|
||||
/>
|
||||
<Divider orientation="vertical" />
|
||||
<ToolbarButton
|
||||
description="Add new Service"
|
||||
name="modules"
|
||||
icon="Services"
|
||||
selected={worldMode() === "service"}
|
||||
selected={ctx.worldMode() === "service"}
|
||||
onClick={() => {
|
||||
setWorldMode("service");
|
||||
ctx.setWorldMode("service");
|
||||
}}
|
||||
/>
|
||||
<ToolbarButton
|
||||
|
||||
Reference in New Issue
Block a user