Clan-app: dynamic router concept

This commit is contained in:
Johannes Kirschbauer
2024-08-14 13:16:14 +02:00
parent 92dee5784f
commit 22d6d57e3a
17 changed files with 194 additions and 207 deletions

View File

@@ -12,6 +12,7 @@
"@floating-ui/dom": "^1.6.8",
"@modular-forms/solid": "^0.21.0",
"@solid-primitives/storage": "^3.7.1",
"@solidjs/router": "^0.14.2",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.51.2",
"material-icons": "^1.13.12",
@@ -1551,6 +1552,14 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solidjs/router": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.14.2.tgz",
"integrity": "sha512-JaJe7XJcZTyOfMOIVHmLO+3wP3akm5QQesrDU4XLn/JRMxozBzCaNXBsK7F8pBuDgxzRRxTV8RvXeS09HGXv6Q==",
"peerDependencies": {
"solid-js": "^1.8.6"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",

View File

@@ -41,6 +41,7 @@
"@floating-ui/dom": "^1.6.8",
"@modular-forms/solid": "^0.21.0",
"@solid-primitives/storage": "^3.7.1",
"@solidjs/router": "^0.14.2",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.51.2",
"material-icons": "^1.13.12",

View File

@@ -1,15 +1,6 @@
import { type Component, createEffect, createSignal } from "solid-js";
import { Layout } from "./layout/layout";
import { Route, Router } from "./Routes";
import { Toaster } from "solid-toast";
import { effect } from "solid-js/web";
import { createSignal } from "solid-js";
import { makePersisted } from "@solid-primitives/storage";
// Some global state
const [route, setRoute] = createSignal<Route>("machines");
export { route, setRoute };
const [activeURI, setActiveURI] = makePersisted(
createSignal<string | null>(null),
{
@@ -26,21 +17,3 @@ const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
});
export { clanList, setClanList };
const App: Component = () => {
effect(() => {
if (clanList().length === 0) {
setRoute("welcome");
}
});
return (
<div class="h-screen bg-gradient-to-b from-white to-base-100 p-4">
<Toaster position="top-right" />
<Layout>
<Router route={route} />
</Layout>
</div>
);
};
export default App;

View File

@@ -1,91 +0,0 @@
import { Accessor, For, Match, Switch } from "solid-js";
import { MachineListView } from "./routes/machines/view";
import { colors } from "./routes/colors/view";
import { CreateClan } from "./routes/clan/view";
import { HostList } from "./routes/hosts/view";
import { BlockDevicesView } from "./routes/blockdevices/view";
import { Flash } from "./routes/flash/view";
import { Settings } from "./routes/settings";
import { Welcome } from "./routes/welcome";
import { Deploy } from "./routes/deploy";
import { CreateMachine } from "./routes/machines/create";
import { DiskView } from "./routes/disk/view";
export type Route = keyof typeof routes;
export const routes = {
createClan: {
child: CreateClan,
label: "Create Clan",
icon: "groups",
},
machines: {
child: MachineListView,
label: "Machines",
icon: "devices_other",
},
"machines/add": {
child: CreateMachine,
label: "create Machine",
icon: "add",
},
hosts: {
child: HostList,
label: "hosts",
icon: "devices_other",
},
flash: {
child: Flash,
label: "create_flash_installer",
icon: "devices_other",
},
blockdevices: {
child: BlockDevicesView,
label: "blockdevices",
icon: "devices_other",
},
colors: {
child: colors,
label: "Colors",
icon: "color_lens",
},
settings: {
child: Settings,
label: "Settings",
icon: "settings",
},
welcome: {
child: Welcome,
label: "welcome",
icon: "settings",
},
deploy: {
child: Deploy,
label: "deploy",
icon: "content_copy",
},
diskConfig: {
child: DiskView,
label: "diskConfig",
icon: "disk",
},
"machines/edit": {
child: CreateMachine,
label: "Edit Machine",
icon: "edit",
},
};
interface RouterProps {
route: Accessor<Route>;
}
export const Router = (props: RouterProps) => {
const { route } = props;
return (
<Switch fallback={<p>route {route()} not found</p>}>
<For each={Object.entries(routes)}>
{([key, { child }]) => <Match when={route() === key}>{child}</Match>}
</For>
</Switch>
);
};

View File

@@ -1,14 +1,11 @@
import { Accessor, For, Setter } from "solid-js";
import { Route, routes } from "./Routes";
import { For, Show } from "solid-js";
import { activeURI } from "./App";
import { createQuery } from "@tanstack/solid-query";
import { callApi } from "./api";
import { A, RouteSectionProps } from "@solidjs/router";
import { AppRoute, routes } from "./index";
interface SidebarProps {
route: Accessor<Route>;
setRoute: Setter<Route>;
}
export const Sidebar = (props: SidebarProps) => {
export const Sidebar = (props: RouteSectionProps) => {
const query = createQuery(() => ({
queryKey: [activeURI(), "meta"],
queryFn: async () => {
@@ -20,29 +17,57 @@ export const Sidebar = (props: SidebarProps) => {
}
},
}));
const { route, setRoute } = props;
return (
<aside class="w-80 rounded-xl border border-slate-900 bg-slate-800 pb-10">
<div class="m-4 flex flex-col text-center capitalize text-white">
<span class="text-lg">{query.data?.name}</span>
<span class="text-sm">{query.data?.description}</span>
<RouteMenu class="menu px-4 py-2" routes={routes} />
</div>
<ul class="menu px-4 py-0">
<For each={Object.entries(routes)}>
{([key, { label, icon }]) => (
</aside>
);
};
const RouteMenu = (props: {
class?: string;
routes: AppRoute[];
prefix?: string;
}) => (
<ul class={props?.class}>
<For each={props.routes.filter((r) => !r.hidden)}>
{(route) => (
<li>
<button
onClick={() => setRoute(key as Route)}
class="group text-white"
classList={{ "!bg-primary !text-white": route() === key }}
>
<span class="material-icons">{icon}</span>
{label}
<Show
when={route.children}
fallback={
<A href={[props.prefix, route.path].filter(Boolean).join("")}>
<button class="text-white">
{route.icon && (
<span class="material-icons">{route.icon}</span>
)}
{route.label}
</button>
</A>
}
>
{(children) => (
<details id={`disclosure-${route.label}`} open={true}>
<summary class="group">
{route.icon && (
<span class="material-icons">{route.icon}</span>
)}
{route.label}
</summary>
<RouteMenu
routes={children()}
prefix={[props.prefix, route.path].filter(Boolean).join("")}
/>
</details>
)}
</Show>
</li>
)}
</For>
</ul>
</aside>
);
};
);

View File

@@ -1,7 +1,7 @@
import { Accessor, createSignal, Show } from "solid-js";
import { createSignal, Show } from "solid-js";
import { callApi, SuccessData } from "../api";
import { Menu } from "./Menu";
import { activeURI, setRoute } from "../App";
import { activeURI } from "../App";
import toast from "solid-toast";
type MachineDetails = SuccessData<"list_inventory_machines">["data"][string];
@@ -63,9 +63,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
<li>
<a
onClick={() => {
setRoute("machines/edit");
}}
// onClick={() => {
// setRoute("machines/edit");
// }}
>
Edit
</a>

View File

@@ -1,9 +1,19 @@
/* @refresh reload */
import { render } from "solid-js/web";
import { RouteDefinition, Router } from "@solidjs/router";
import "./index.css";
import App from "./App";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { MachineDetails } from "./routes/machines/[name]/view";
import { Layout } from "./layout/layout";
import { MachineListView } from "./routes/machines/view";
import { CreateClan } from "./routes/clan/view";
import { Settings } from "./routes/settings";
import { EditClanForm } from "./routes/clan/editClan";
import { Flash } from "./routes/flash/view";
import { CreateMachine } from "./routes/machines/create";
import { HostList } from "./routes/hosts/view";
import { Welcome } from "./routes/welcome";
const client = new QueryClient();
@@ -23,10 +33,88 @@ if (import.meta.env.DEV) {
await import("solid-devtools");
}
export type AppRoute = Omit<RouteDefinition, "children"> & {
label: string;
icon?: string;
children?: AppRoute[];
hidden?: boolean;
};
export const routes: AppRoute[] = [
{
path: "/machines",
label: "Machines",
icon: "devices_other",
children: [
{
path: "/",
label: "Overview",
component: () => <MachineListView />,
},
{
path: "/create",
label: "Create",
component: () => <CreateMachine />,
},
{
path: "/:id",
label: "Details",
hidden: true,
component: () => <MachineDetails />,
},
],
},
{
path: "/clan",
label: "Clans",
icon: "groups",
children: [
{
path: "/",
label: "Overview",
component: () => <Settings />,
},
{
path: "/create",
label: "Create",
component: () => <CreateClan />,
},
{
path: "/:id",
label: "Details",
hidden: true,
},
],
},
{
path: "/tools",
label: "Tools",
icon: "bolt",
children: [
{
path: "/flash",
label: "Clan Installer",
component: () => <Flash />,
},
{
path: "/hosts",
label: "Local Hosts",
component: () => <HostList />,
},
],
},
{
path: "/welcome",
label: "",
hidden: true,
component: () => <Welcome />,
},
];
render(
() => (
<QueryClientProvider client={client}>
<App />
<Router root={Layout}>{routes}</Router>
</QueryClientProvider>
),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View File

@@ -1,5 +1,5 @@
import { createQuery } from "@tanstack/solid-query";
import { activeURI, setRoute } from "../App";
import { activeURI } from "../App";
import { callApi } from "../api";
import { Accessor, Show } from "solid-js";
@@ -63,7 +63,7 @@ export const Header = (props: HeaderProps) => {
</div>
<div class="flex-none">
<span class="tooltip tooltip-bottom" data-tip="Settings">
<button class="link" onClick={() => setRoute("settings")}>
<button class="link">
<span class="material-icons">settings</span>
</button>
</span>

View File

@@ -1,15 +1,12 @@
import { Component, JSXElement, Show } from "solid-js";
import { Component, createEffect, Show } from "solid-js";
import { Header } from "./header";
import { Sidebar } from "../Sidebar";
import { activeURI, clanList, route, setRoute } from "../App";
import { activeURI, clanList } from "../App";
import { RouteSectionProps } from "@solidjs/router";
interface LayoutProps {
children: JSXElement;
}
export const Layout: Component<LayoutProps> = (props) => {
export const Layout: Component<RouteSectionProps> = (props) => {
return (
<>
<div class="h-screen bg-gradient-to-b from-white to-base-100 p-4">
<div class="drawer lg:drawer-open ">
<input
id="toplevel-drawer"
@@ -17,7 +14,7 @@ export const Layout: Component<LayoutProps> = (props) => {
class="drawer-toggle hidden"
/>
<div class="drawer-content">
<Show when={route() !== "welcome"}>
<Show when={props.location.pathname !== "welcome"}>
<Header clan_dir={activeURI} />
</Show>
{props.children}
@@ -25,7 +22,8 @@ export const Layout: Component<LayoutProps> = (props) => {
<div
class="drawer-side z-40 h-full"
classList={{
"!hidden": route() === "welcome" || clanList().length === 0,
"!hidden":
props.location.pathname === "welcome" || clanList().length === 0,
}}
>
<label
@@ -33,9 +31,9 @@ export const Layout: Component<LayoutProps> = (props) => {
aria-label="close sidebar"
class="drawer-overlay"
></label>
<Sidebar route={route} setRoute={setRoute} />
<Sidebar {...props} />
</div>
</div>
</div>
</>
);
};

View File

@@ -1,4 +1,4 @@
import { callApi, OperationResponse, pyApi } from "@/src/api";
import { callApi, OperationResponse } from "@/src/api";
import { Show } from "solid-js";
import {
createForm,
@@ -7,7 +7,7 @@ import {
SubmitHandler,
} from "@modular-forms/solid";
import toast from "solid-toast";
import { setActiveURI, setRoute } from "@/src/App";
import { setActiveURI } from "@/src/App";
type CreateForm = Meta & {
template_url: string;
@@ -53,7 +53,7 @@ export const ClanForm = () => {
},
});
setActiveURI(target_dir[0]);
setRoute("machines");
// setRoute("machines");
})(),
{
loading: "Creating clan...",

View File

@@ -2,7 +2,6 @@ import { callApi } from "@/src/api";
import { activeURI } from "@/src/App";
import { createQuery } from "@tanstack/solid-query";
import { createEffect } from "solid-js";
import toast from "solid-toast";
export function DiskView() {
const query = createQuery(() => ({

View File

@@ -1,11 +1,4 @@
import {
type Component,
createEffect,
createSignal,
For,
Show,
} from "solid-js";
import { route } from "@/src/App";
import { type Component, createSignal, For, Show } from "solid-js";
import { OperationResponse, pyApi } from "@/src/api";
type ServiceModel = Extract<
@@ -16,16 +9,6 @@ type ServiceModel = Extract<
export const HostList: Component = () => {
const [services, setServices] = createSignal<ServiceModel>();
// pyApi.show_mdns.receive((r) => {
// const { status } = r;
// if (status === "error") return console.error(r.errors);
// setServices(r.data.services);
// });
// createEffect(() => {
// if (route() === "hosts") pyApi.show_mdns.dispatch({});
// });
return (
<div>
<div class="tooltip tooltip-bottom" data-tip="Refresh install targets">

View File

@@ -0,0 +1,6 @@
import { useParams } from "@solidjs/router";
export const MachineDetails = () => {
const params = useParams();
return <div>Machine Details: {params.id}</div>;
};

View File

@@ -1,5 +1,5 @@
import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
import { activeURI, setRoute } from "@/src/App";
import { callApi, OperationArgs } from "@/src/api";
import { activeURI } from "@/src/App";
import { TextInput } from "@/src/components/TextInput";
import { createForm, required, reset } from "@modular-forms/solid";
import { createQuery, useQueryClient } from "@tanstack/solid-query";
@@ -49,7 +49,7 @@ export function CreateMachine() {
queryClient.invalidateQueries({
queryKey: [activeURI(), "list_machines"],
});
setRoute("machines");
// setRoute("machines");
} else {
toast.error(
`Error: ${response.errors[0].message}. Machine ${values.machine.name} could not be created`,

View File

@@ -1,5 +1,5 @@
import { type Component, createEffect, For, Match, Switch } from "solid-js";
import { activeURI, setRoute } from "@/src/App";
import { type Component, For, Match, Switch } from "solid-js";
import { activeURI } from "@/src/App";
import { callApi, OperationResponse } from "@/src/api";
import toast from "solid-toast";
import { MachineListItem } from "@/src/components/MachineListItem";
@@ -83,7 +83,10 @@ export const MachineListView: Component = () => {
</button>
</div>
<div class="tooltip tooltip-bottom" data-tip="Create machine">
<button class="btn btn-ghost" onClick={() => setRoute("machines/add")}>
<button
class="btn btn-ghost"
// onClick={() => setRoute("machines/add")}
>
<span class="material-icons ">add</span>
</button>
</div>

View File

@@ -1,11 +1,5 @@
import { callApi } from "@/src/api";
import {
activeURI,
clanList,
setActiveURI,
setClanList,
setRoute,
} from "@/src/App";
import { activeURI, clanList, setActiveURI, setClanList } from "@/src/App";
import { createSignal, For, Match, Setter, Show, Switch } from "solid-js";
import { createQuery } from "@tanstack/solid-query";
import { useFloating } from "@/src/floating";
@@ -17,7 +11,6 @@ export const registerClan = async () => {
const loc = await callApi("open_file", {
file_request: { mode: "select_folder" },
});
console.log({ loc }, loc.status);
if (loc.status === "success" && loc.data) {
const data = loc.data[0];
setClanList((s) => {
@@ -25,10 +18,10 @@ export const registerClan = async () => {
return Array.from(res);
});
setActiveURI(data);
setRoute((r) => {
if (r === "welcome") return "machines";
return r;
});
// setRoute((r) => {
// if (r === "welcome") return "machines";
// return r;
// });
return data;
}
} catch (e) {

View File

@@ -1,4 +1,4 @@
import { setActiveURI, setRoute } from "@/src/App";
import { setActiveURI } from "@/src/App";
import { registerClan } from "../settings";
export const Welcome = () => {
@@ -11,7 +11,7 @@ export const Welcome = () => {
<div class="flex flex-col items-start gap-2">
<button
class="btn btn-primary w-full"
onClick={() => setRoute("createClan")}
// onClick={() => setRoute("createClan")}
>
Build your own
</button>
@@ -27,7 +27,7 @@ export const Welcome = () => {
<button
class="link w-full text-right text-secondary"
onClick={async () => {
setRoute("machines");
// setRoute("machines");
}}
>
Skip (Debug)