1import { Route, Router, type RouteSectionProps, useLocation } from "@solidjs/router";
2import Eye from "lucide-solid/icons/eye";
3import { createResource, Show } from "solid-js";
4import { render } from "solid-js/web";
5import { parseResponse } from "hono/client";
6import { api, queries } from "./api.ts";
7import { Loaded } from "./components/Loaded.tsx";
8import { PAGES, Sidebar, viewAs } from "./components/Sidebar.tsx";
9import { Toasts } from "./components/Toast.tsx";
10import { Tooltips } from "./components/Tooltip.tsx";
11import { Overview } from "./pages/Overview.tsx";
12import { Service } from "./pages/Service.tsx";
13import { Storage } from "./pages/Storage.tsx";
14import { Media } from "./pages/Media.tsx";
15import { Seedbox } from "./pages/Seedbox.tsx";
16import { YouTube } from "./pages/YouTube.tsx";
17import { PaperClover } from "./pages/PaperClover.tsx";
18import { directory, Users } from "./pages/Users.tsx";
19import { Deploys } from "./pages/Deploys.tsx";
20import { Deploy } from "./pages/Deploy.tsx";
21import { VMs } from "./pages/VMs.tsx";
22import { MCP } from "./pages/MCP.tsx";
23import { MCPConsent } from "./pages/MCPConsent.tsx";
24import { SignIn } from "./pages/SignIn.tsx";
25import { Account } from "./pages/Account.tsx";
26import "./styles.css";
27
28const [me, { refetch }] = createResource(() => parseResponse(api.me.$get()));
29
30/** A route preload, which the router also runs on link hover, starting each query's first load. */
31const preload = (...list: { preload(): void }[]) => () => list.forEach((query) => query.preload());
32
33function Shell(props: RouteSectionProps) {
34 const location = useLocation();
35 const consent = () => location.pathname.startsWith("/connect/") || location.pathname === "/mcp" && new URLSearchParams(location.search).has("request");
36 const section = () => location.pathname.startsWith("/services/") ? "admin"
37 : PAGES.find((page) => page.href !== "/" && location.pathname.startsWith(page.href))?.section;
38 return (
39 <>
40 <Show when={location.pathname !== "/sign-in" && !consent()} fallback={<Show when={consent()} fallback={props.children}><MCPConsent /></Show>}>
41 <Loaded data={me} what="your account" retry={refetch} skeleton={<div class="shell"><div class="sidebar" /><main class="main" /></div>}>
42 {(user) => (
43 <div class="shell">
44 <Show when={user().viewing}>
45 <div class="viewing-as" role="status">
46 <Eye size={15} />
47 <span>previewing as <b>{user().groups.length ? `someone in ${user().groups.join(", ")}` : "a friend in no groups"}</b></span>
48 <button class="button small" onClick={() => viewAs(null)}>back to admin</button>
49 </div>
50 </Show>
51 <Sidebar me={user()} />
52 <main class="main">
53 <Show when={!section() || user().sections.includes(section()!)} fallback={
54 <div class="page empty">Your account can't open this page. Ask an admin to add you to its group.</div>
55 }>
56 {props.children}
57 </Show>
58 </main>
59 </div>
60 )}
61 </Loaded>
62 </Show>
63 <Tooltips />
64 <Toasts />
65 </>
66 );
67}
68
69render(() => (
70 <Router root={Shell}>
71 <Route path="/sign-in" component={SignIn} />
72 <Route path="/" component={() => <Overview me={me.latest!} />} preload={() => {
73 queries.launcher.preload();
74 if (me.latest?.sections.includes("metrics")) preload(queries.host, queries.storage, queries.services)();
75 }} />
76 <Route path="/services/:id/:tab?" component={Service} preload={({ params }) => {
77 queries.service.preload(params.id!);
78 queries.deploys.preload();
79 }} />
80 <Route path="/storage" component={Storage} preload={preload(queries.storage, queries.launcher)} />
81 <Route path="/media" component={Media} preload={preload(queries.launcher)} />
82 <Route path="/seedbox" component={Seedbox} preload={preload(queries.seedbox, queries.seedboxHistory, queries.launcher)} />
83 <Route path="/youtube" component={YouTube} />
84 <Route path="/paper-clover" component={PaperClover} preload={preload(queries.paperClover, queries.paperCloverActivity)} />
85 <Route path="/users/:name?" component={Users} preload={preload(directory)} />
86 <Route path="/deploys" component={Deploys} preload={preload(queries.deploys, queries.services)} />
87 <Route path="/deploys/:id/:tab?" component={Deploy} preload={preload(queries.deploys, queries.services)} />
88 <Route path="/vms" component={VMs} preload={preload(queries.vms)} />
89 <Route path="/mcp" component={() => <MCP me={me()!} />} />
90 <Route path="/mcp/settings/:catalog" component={() => <MCP me={me()!} />} />
91 <Route path="/connect/:id" component={MCPConsent} />
92 <Route path="/account" component={Account} preload={preload(queries.launcher)} />
93 <Route path="*" component={() => <div class="empty">No page here</div>} />
94 </Router>
95), document.getElementById("root")!);