feat: configurable control ui base path
This commit is contained in:
@@ -4,7 +4,14 @@ import { customElement, state } from "lit/decorators.js";
|
||||
import { GatewayBrowserClient, type GatewayEventFrame, type GatewayHelloOk } from "./gateway";
|
||||
import { loadSettings, saveSettings, type UiSettings } from "./storage";
|
||||
import { renderApp } from "./app-render";
|
||||
import { normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation";
|
||||
import {
|
||||
inferBasePathFromPathname,
|
||||
normalizeBasePath,
|
||||
normalizePath,
|
||||
pathForTab,
|
||||
tabFromPath,
|
||||
type Tab,
|
||||
} from "./navigation";
|
||||
import {
|
||||
resolveTheme,
|
||||
type ResolvedTheme,
|
||||
@@ -74,6 +81,12 @@ type EventLogEntry = {
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__CLAWDIS_CONTROL_UI_BASE_PATH__?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CRON_FORM: CronFormState = {
|
||||
name: "",
|
||||
description: "",
|
||||
@@ -468,9 +481,11 @@ export class ClawdisApp extends LitElement {
|
||||
|
||||
private inferBasePath() {
|
||||
if (typeof window === "undefined") return "";
|
||||
const path = window.location.pathname;
|
||||
if (path === "/ui" || path.startsWith("/ui/")) return "/ui";
|
||||
return "";
|
||||
const configured = window.__CLAWDIS_CONTROL_UI_BASE_PATH__;
|
||||
if (typeof configured === "string" && configured.trim()) {
|
||||
return normalizeBasePath(configured);
|
||||
}
|
||||
return inferBasePathFromPathname(window.location.pathname);
|
||||
}
|
||||
|
||||
private syncThemeWithSettings() {
|
||||
|
||||
@@ -21,11 +21,13 @@ beforeEach(() => {
|
||||
ClawdisApp.prototype.connect = () => {
|
||||
// no-op: avoid real gateway WS connections in browser tests
|
||||
};
|
||||
window.__CLAWDIS_CONTROL_UI_BASE_PATH__ = undefined;
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ClawdisApp.prototype.connect = originalConnect;
|
||||
window.__CLAWDIS_CONTROL_UI_BASE_PATH__ = undefined;
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
@@ -47,6 +49,25 @@ describe("control UI routing", () => {
|
||||
expect(window.location.pathname).toBe("/ui/cron");
|
||||
});
|
||||
|
||||
it("infers nested base paths", async () => {
|
||||
const app = mountApp("/apps/clawdis/cron");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.basePath).toBe("/apps/clawdis");
|
||||
expect(app.tab).toBe("cron");
|
||||
expect(window.location.pathname).toBe("/apps/clawdis/cron");
|
||||
});
|
||||
|
||||
it("honors explicit base path overrides", async () => {
|
||||
window.__CLAWDIS_CONTROL_UI_BASE_PATH__ = "/clawdis";
|
||||
const app = mountApp("/clawdis/sessions");
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.basePath).toBe("/clawdis");
|
||||
expect(app.tab).toBe("sessions");
|
||||
expect(window.location.pathname).toBe("/clawdis/sessions");
|
||||
});
|
||||
|
||||
it("updates the URL when clicking nav items", async () => {
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
|
||||
@@ -37,7 +37,7 @@ const PATH_TO_TAB = new Map(
|
||||
Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]),
|
||||
);
|
||||
|
||||
function normalizeBasePath(basePath: string): string {
|
||||
export function normalizeBasePath(basePath: string): string {
|
||||
if (!basePath) return "";
|
||||
let base = basePath.trim();
|
||||
if (!base.startsWith("/")) base = `/${base}`;
|
||||
@@ -78,6 +78,24 @@ export function tabFromPath(pathname: string, basePath = ""): Tab | null {
|
||||
return PATH_TO_TAB.get(normalized) ?? null;
|
||||
}
|
||||
|
||||
export function inferBasePathFromPathname(pathname: string): string {
|
||||
let normalized = normalizePath(pathname);
|
||||
if (normalized.endsWith("/index.html")) {
|
||||
normalized = normalizePath(normalized.slice(0, -"/index.html".length));
|
||||
}
|
||||
if (normalized === "/") return "";
|
||||
const segments = normalized.split("/").filter(Boolean);
|
||||
if (segments.length === 0) return "";
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const candidate = `/${segments.slice(i).join("/")}`.toLowerCase();
|
||||
if (PATH_TO_TAB.has(candidate)) {
|
||||
const prefix = segments.slice(0, i);
|
||||
return prefix.length ? `/${prefix.join("/")}` : "";
|
||||
}
|
||||
}
|
||||
return `/${segments.join("/")}`;
|
||||
}
|
||||
|
||||
export function titleForTab(tab: Tab) {
|
||||
switch (tab) {
|
||||
case "overview":
|
||||
|
||||
Reference in New Issue
Block a user