Skip to content

Commit 0389209

Browse files
committed
chore: fixed dashboard extension
1 parent 4fef943 commit 0389209

11 files changed

Lines changed: 67 additions & 7885 deletions

File tree

extensions/dashboard/contract/shell/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The React/TypeScript runtime that consumes the Forge dashboard contract. It fetc
88
- **UI primitives:** shadcn/ui (vendored) + lucide-react icons. Light / dark / system theme baked in.
99
- **Bundle size:** ~120KB gzipped JS + ~5KB CSS for the v1 vocabulary; well within the 350KB budget.
1010
- **Built-in intent vocabulary (v1):** `page.shell`, `metric.counter`, `action.button`, `action.menu`, `action.divider`, `form.edit`, `form.field`, `resource.list`, `resource.detail`, `dashboard.grid`, `audit.tail`. Unknown intents render a graceful fallback.
11-
- **Embedded into the Go binary:** `pnpm build` emits `dist/`, which the dashboard extension serves under `/dashboard/contract/static/*` and `/dashboard/contract/app/*` (SPA fallback).
11+
- **Embedded into the Go binary:** `pnpm build` emits `dist/`, which the dashboard extension serves under `/dashboard/ui/static/*` and `/dashboard/ui/*` (SPA fallback).
1212

1313
For the architecture deep-dive (how the renderer + registry work, how to author new intents), see [ARCHITECTURE.md](./ARCHITECTURE.md). For the design rationale across slices, see [SLICE_D_DESIGN.md](../SLICE_D_DESIGN.md) and [SLICE_E_DESIGN.md](../SLICE_E_DESIGN.md).
1414

@@ -25,7 +25,7 @@ The dev server expects the dashboard binary running on `localhost:8080`. Start i
2525
go run ./cmd/forge ... # whatever your dashboard entrypoint is
2626
```
2727

28-
Then browse to `http://localhost:5173/dashboard/contract/app/extensions` (or any other pilot route).
28+
Then browse to `http://localhost:5173/dashboard/ui/extensions` (or any other pilot route).
2929

3030
## Build
3131

@@ -124,8 +124,8 @@ The Go side serves the built shell from two route groups (registered in `extensi
124124

125125
| URL pattern | Purpose | Cache |
126126
|---|---|---|
127-
| `/dashboard/contract/static/*` | Hashed assets (JS, CSS, fonts) from `dist/` | Immutable for `/assets/*`, no-cache otherwise |
128-
| `/dashboard/contract/app[/*]` | SPA index.html — React Router handles client-side routing | no-cache |
127+
| `/dashboard/ui/static/*` | Hashed assets (JS, CSS, fonts) from `dist/` | Immutable for `/assets/*`, no-cache otherwise |
128+
| `/dashboard/ui[/*]` | SPA index.html — React Router handles client-side routing | no-cache |
129129

130130
`pnpm build` is required before `go build` so `embed.FS` picks up the latest assets.
131131

extensions/dashboard/contract/shell/dist/assets/index-BFB0RZ8m.js

Lines changed: 0 additions & 7846 deletions
This file was deleted.

extensions/dashboard/contract/shell/dist/assets/index-BFB0RZ8m.js.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

extensions/dashboard/contract/shell/dist/index.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>Forge Dashboard</title>
7-
<script type="module" crossorigin src="/dashboard/contract/static/assets/index-BFB0RZ8m.js"></script>
8-
<link rel="modulepreload" crossorigin href="/dashboard/contract/static/assets/query-vendor-ksos0O3Y.js">
9-
<link rel="modulepreload" crossorigin href="/dashboard/contract/static/assets/react-vendor-B3zIxWVv.js">
10-
<link rel="modulepreload" crossorigin href="/dashboard/contract/static/assets/recharts-vendor-0WIoiWbO.js">
11-
<link rel="modulepreload" crossorigin href="/dashboard/contract/static/assets/dnd-vendor-Bp3SBVy5.js">
12-
<link rel="stylesheet" crossorigin href="/dashboard/contract/static/assets/index-DNgq8ixA.css">
7+
<script type="module" crossorigin src="/dashboard/ui/static/assets/index-Dk-DkQ1p.js"></script>
8+
<link rel="modulepreload" crossorigin href="/dashboard/ui/static/assets/query-vendor-ksos0O3Y.js">
9+
<link rel="modulepreload" crossorigin href="/dashboard/ui/static/assets/react-vendor-B3zIxWVv.js">
10+
<link rel="modulepreload" crossorigin href="/dashboard/ui/static/assets/recharts-vendor-0WIoiWbO.js">
11+
<link rel="modulepreload" crossorigin href="/dashboard/ui/static/assets/dnd-vendor-Bp3SBVy5.js">
12+
<link rel="stylesheet" crossorigin href="/dashboard/ui/static/assets/index-DNgq8ixA.css">
1313
</head>
1414
<body class="bg-gray-50 text-gray-900">
1515
<div id="root"></div>

extensions/dashboard/contract/shell/src/runtime/apps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interface AppStore {
4949
function loadInitial(): string | null {
5050
if (typeof window === "undefined") return null;
5151
// URL beats localStorage on first load so deep-linking into a different
52-
// app (e.g. /dashboard/contract/app/users) wins over a stale selection.
52+
// app (e.g. /dashboard/ui/users) wins over a stale selection.
5353
const params = new URLSearchParams(window.location.search);
5454
const fromURL = params.get("app");
5555
if (fromURL) return fromURL;

extensions/dashboard/contract/shell/src/runtime/config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ function readInjected(): InjectedConfig {
4545
const injected = readInjected();
4646

4747
export const basePath: string = injected.basePath ?? FALLBACK_BASE;
48-
export const contractBase: string = injected.contractBase ?? `${FALLBACK_BASE}/api/dashboard/v1`;
49-
export const shellBase: string = injected.shellBase ?? `${FALLBACK_BASE}/contract/app`;
48+
export const contractBase: string =
49+
injected.contractBase ?? `${FALLBACK_BASE}/api/dashboard/v1`;
50+
export const shellBase: string = injected.shellBase ?? `${FALLBACK_BASE}/ui`;
5051
export const authEnabled: boolean = injected.authEnabled ?? false;
5152
export const loginPath: string = injected.loginPath ?? `${FALLBACK_BASE}/login`;
5253
export const loginOp: string = injected.loginOp ?? "auth.login";

extensions/dashboard/contract/shell/test/setup.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { cleanup } from "@testing-library/react";
66
// (matching the existing MSW handlers) instead of inheriting the production
77
// fallback /dashboard/api/dashboard/v1 from runtime/config.ts. Must run before
88
// any module under test imports runtime/config; setupFiles guarantees that.
9-
(window as unknown as { __FORGE_DASHBOARD__: Record<string, string> }).__FORGE_DASHBOARD__ = {
9+
(
10+
window as unknown as { __FORGE_DASHBOARD__: Record<string, string> }
11+
).__FORGE_DASHBOARD__ = {
1012
basePath: "",
1113
contractBase: "/api/dashboard/v1",
12-
shellBase: "/dashboard/contract/app",
14+
shellBase: "/dashboard/ui",
1315
};
1416

1517
// jsdom polyfills required by Radix UI primitives.
@@ -19,8 +21,9 @@ class ResizeObserverStub {
1921
disconnect() {}
2022
}
2123
if (typeof globalThis.ResizeObserver === "undefined") {
22-
(globalThis as unknown as { ResizeObserver: typeof ResizeObserverStub }).ResizeObserver =
23-
ResizeObserverStub;
24+
(
25+
globalThis as unknown as { ResizeObserver: typeof ResizeObserverStub }
26+
).ResizeObserver = ResizeObserverStub;
2427
}
2528

2629
if (typeof Element !== "undefined" && !Element.prototype.hasPointerCapture) {
@@ -35,9 +38,9 @@ if (typeof Element !== "undefined" && !Element.prototype.hasPointerCapture) {
3538
// jsdom doesn't ship matchMedia; the slice (l) Sidebar uses it for the
3639
// mobile breakpoint check. Static "false" is fine — tests never resize.
3740
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
38-
(window as unknown as { matchMedia: (q: string) => MediaQueryList }).matchMedia = (
39-
query: string,
40-
) =>
41+
(
42+
window as unknown as { matchMedia: (q: string) => MediaQueryList }
43+
).matchMedia = (query: string) =>
4144
({
4245
matches: false,
4346
media: query,
@@ -66,8 +69,9 @@ class EventSourceStub {
6669
close() {}
6770
}
6871
if (typeof globalThis.EventSource === "undefined") {
69-
(globalThis as unknown as { EventSource: typeof EventSourceStub }).EventSource =
70-
EventSourceStub;
72+
(
73+
globalThis as unknown as { EventSource: typeof EventSourceStub }
74+
).EventSource = EventSourceStub;
7175
}
7276

7377
afterEach(() => {

extensions/dashboard/contract/shell/test/smoke.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ beforeAll(() => {
4848
onopen: (() => void) | null = null;
4949
onerror: (() => void) | null = null;
5050
};
51-
history.pushState({}, "", "/dashboard/contract/app/metrics/live");
51+
history.pushState({}, "", "/dashboard/ui/metrics/live");
5252
server.listen({ onUnhandledRequest: "error" });
5353
});
5454
afterEach(() => server.resetHandlers());

extensions/dashboard/contract/shell/vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineConfig({
1212
"@": path.resolve(__dirname, "./src"),
1313
},
1414
},
15-
base: "/dashboard/contract/static/",
15+
base: "/dashboard/ui/static/",
1616
build: {
1717
outDir: "dist",
1818
emptyOutDir: true,
@@ -39,7 +39,7 @@ export default defineConfig({
3939
target: "http://localhost:8080",
4040
changeOrigin: false,
4141
},
42-
"/dashboard/contract/static": {
42+
"/dashboard/ui/static": {
4343
target: "http://localhost:5173",
4444
bypass: () => "/index.html",
4545
},

extensions/dashboard/extension.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ func (e *Extension) Register(app forge.App) error {
287287

288288
// Slice (i): the legacy CoreContributor is retired. The contract pilot
289289
// (registered below) now serves Overview / Health / Metrics / Traces /
290-
// Extensions / Services through the React shell at /contract/app/*. Old
290+
// Extensions / Services through the React shell at /ui/*. Old
291291
// templ paths 302-redirect to the shell; see pages.RegisterPages.
292292

293293
// Rebuild the search index against any contributors registered later.
@@ -1537,15 +1537,24 @@ func (e *Extension) registerRoutes() {
15371537
}), routeOpts...))
15381538

15391539
// Slice (d) Phase 7: static + SPA serving for the embedded React shell.
1540-
// Static assets at /dashboard/contract/static/* are served from the
1541-
// embedded dist/. Any path under /dashboard/contract/app/* serves
1542-
// index.html; React Router handles client-side routing.
1540+
// Static assets at /dashboard/ui/static/* are served from the embedded
1541+
// dist/. Any other path under /dashboard/ui/* serves index.html; React
1542+
// Router handles client-side routing. The concrete "static" segment
1543+
// takes precedence over the SPA catch-all in the router trie.
15431544
shellFS, shellErr := contractshell.FS()
15441545
if shellErr == nil {
1545-
staticPrefix := base + "/contract/static"
1546+
staticPrefix := base + "/ui/static"
15461547
must(router.GET(staticPrefix+"/*filepath", e.makeShellStaticHandler(shellFS, staticPrefix)))
1547-
must(router.GET(base+"/contract/app", e.makeShellSPAHandler(shellFS)))
1548-
must(router.GET(base+"/contract/app/*filepath", e.makeShellSPAHandler(shellFS)))
1548+
must(router.GET(base+"/ui", e.makeShellSPAHandler(shellFS)))
1549+
must(router.GET(base+"/ui/*filepath", e.makeShellSPAHandler(shellFS)))
1550+
1551+
// Backward-compat: the shell used to live at /{base}/contract/app.
1552+
// 302 the old entry + any deep link to the new /{base}/ui path so
1553+
// existing bookmarks keep working.
1554+
legacyPrefix := base + "/contract/app"
1555+
legacyRedirect := e.makeShellRedirectHandler(legacyPrefix, base+"/ui")
1556+
must(router.GET(legacyPrefix, legacyRedirect))
1557+
must(router.GET(legacyPrefix+"/*filepath", legacyRedirect))
15491558
}
15501559
}
15511560

@@ -1816,7 +1825,7 @@ func (a *idempotencyAdapter) Store(ctx context.Context, key, identity string, c
18161825
}
18171826

18181827
// makeShellStaticHandler serves files from the embedded React shell at
1819-
// /{base}/contract/static/*. Hashed asset paths (under /assets/) are cached
1828+
// /{base}/ui/static/*. Hashed asset paths (under /assets/) are cached
18201829
// aggressively; everything else uses a no-cache header so deploys land
18211830
// immediately. The stripPrefix is the URL prefix the static handler is
18221831
// mounted at — request paths beneath it are resolved against the embedded FS.
@@ -1838,8 +1847,23 @@ func (e *Extension) makeShellStaticHandler(shellFS fs.FS, stripPrefix string) ht
18381847
}
18391848
}
18401849

1850+
// makeShellRedirectHandler 302s requests from a legacy shell prefix to the
1851+
// current one, preserving the trailing subpath and query string. Used to keep
1852+
// old /{base}/contract/app bookmarks working after the shell moved to
1853+
// /{base}/ui.
1854+
func (e *Extension) makeShellRedirectHandler(oldPrefix, newPrefix string) http.HandlerFunc {
1855+
return func(w http.ResponseWriter, r *http.Request) {
1856+
target := newPrefix + strings.TrimPrefix(r.URL.Path, oldPrefix)
1857+
if r.URL.RawQuery != "" {
1858+
target += "?" + r.URL.RawQuery
1859+
}
1860+
1861+
http.Redirect(w, r, target, http.StatusFound)
1862+
}
1863+
}
1864+
18411865
// makeShellSPAHandler returns the SPA index.html for any path under
1842-
// /{base}/contract/app/*. React Router handles the client-side routing.
1866+
// /{base}/ui/*. React Router handles the client-side routing.
18431867
//
18441868
// The handler injects a small bootstrap script just before </head> that
18451869
// surfaces the configured BasePath to the shell. This lets the React shell
@@ -1858,7 +1882,7 @@ func (e *Extension) makeShellSPAHandler(shellFS fs.FS) http.HandlerFunc {
18581882
cfg := map[string]any{
18591883
"basePath": e.config.BasePath,
18601884
"contractBase": e.config.BasePath + "/api/dashboard/v1",
1861-
"shellBase": e.config.BasePath + "/contract/app",
1885+
"shellBase": e.config.BasePath + "/ui",
18621886
"authEnabled": e.config.EnableAuth,
18631887
"loginPath": e.config.BasePath + e.config.LoginPath,
18641888
// Slice (l): the contributor that owns the contract /login graph

0 commit comments

Comments
 (0)