Skip to content

[feature] Migrate DevTools UI to TypeScript #47

Description

@rian-be

Summary

Migrate the DevTools plugin frontend (src/Plugins/Solutions/DevTools/UI) from single embedded HTML file with inline JavaScript to typed, modular TypeScript implementation compiled into single embedded HTML resource.

Goal

The DevTools plugin (Swagger UI for REST + web UI for browsing and invoking gRPC methods) serves its frontend as one embedded resource. The migration should produce maintainable, type safe frontend with clean separation between rendering, domain logic, state, and the HTTP boundary, while preserving the single file embedded resource serving model and the runtime {{pathBase}} substitution performed by middleware.

Problem

The current UI/ui.html is ~330 line file mixing inline JavaScript, HTML, and CSS. The logic is monolith (state, string built DOM via innerHTML, manual esc() injection, inline fetch calls). There is no toolchain: no type checking, no bundling, and no way to reuse or unit test pieces of the frontend. It also works only accidentally prettyPrint receives an object instead of string and renders [object Object] in the Request JSON field.

Scope

  • TypeScript frontend for the DevTools plugin UI

Architecture

flowchart TD
    TS["UI/src/*.ts (TS modules)"] -->|esbuild bundle| JS["dist/bundle.js"]
    TEMPLATE["UI/template.html (HTML+CSS)"] -->|build.mjs inlines bundle| UI["dist/ui.html"]
    UI -->|EmbeddedResource| CSPROJ["DevTools.csproj"]
    CSPROJ -->|"LogicalName DevTools.UI.ui.html"| DLL["DevTools.dll"]
    DLL -->|"LoadEmbeddedPage + Replace {{pathBase}}"| MID["DevToolsMiddleware"]
    MID -->|serves| BROWSER["Browser"]
    classDef ts fill:#ffebee,stroke:#c62839,color:#7b1f2e
    classDef build fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
    classDef cs fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
    classDef serve fill:#fff3e0,stroke:#e65100,color:#bf360c
    class TS,TEMPLATE ts
    class JS,UI build
    class CSPROJ,DLL cs
    class MID,BROWSER serve
Loading

Frontend module boundaries:

flowchart TD
    APP["app.ts (composition root)"] -->|GrpcApi interface| API["api.ts HttpGrpcApi"]
    APP --> CTRL["controller.ts createInvokeAction"]
    CTRL --> RENDER["render.ts renderServiceList / renderMethodDetail"]
    RENDER -->|pure functions| SCHEMA["schema.ts domain logic"]
    CTRL --> SCHEMA
    APP --> STORE["store.ts InMemoryDevToolsStore"]
    APP --> STORAGE["storage.ts sessionStorage headers"]
    RENDER --> DOM["dom.ts el / getById"]
    API -->|fetch| HTTP["/api/services /api/invoke"]
    classDef root fill:#ede7f6,stroke:#4527a0,color:#311b92
    classDef infra fill:#e1f5fe,stroke:#01579b,color:#0d47a1
    classDef domain fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
    class APP root
    class API,DOM,STORAGE,HTTP infra
    class CTRL,RENDER,SCHEMA,STORE domain
Loading

Proposed Contract

The frontend boundary must be typed gRPC client the composition root (app.ts) depends on the GrpcApi interface and never touches fetch directly:

export interface InvokeGrpcRequest {
  service: string;
  method: string;
  requestJson: string;
  headers: Record<string, string>;
}

export interface GrpcApi {
  fetchServices(): Promise<GrpcCatalogResponse>;
  invoke(request: InvokeGrpcRequest): Promise<InvocationResult>;
}

The embedded resource must keep its runtime placeholder so the middleware can inject the serving path without rebuild:

<script>
window.__GRPC_UI_BASE__ = "{{pathBase}}";
</script>

DevToolsMiddleware.LoadEmbeddedPage() locates the resource by DevTools.UI.ui.html and replaces {{pathBase}} with the configured GrpcUiPathBase.

Requirements

  • Frontend must be authored in strict TypeScript (strict: true, tsc --noEmit clean).
  • A build step must compile the UI with esbuild and inline it into single dist/ui.html (no separate JS bundle).
  • dist/ui.html must be the only frontend artifact DevTools.csproj embeds it as EmbeddedResource with LogicalName="DevTools.UI.ui.html".
  • Rendering must use DOM APIs (createElement, textContent, classList) no innerHTML interpolation, no manual HTML escaping.
  • The {{pathBase}} placeholder must remain in the HTML (not in the bundle) and be the middleware's only insertion point.
  • Rendering, domain logic, and state must be separated so pure parts (schema.ts) are testable without browser.
  • The composition root must consume the GrpcApi interface API transport must be hidden behind HttpGrpcApi.
  • The Docker build must compile the UI in node:22 stage before publishing the plugin.
  • Taskfile.yml build must run build-ui before dotnet build a watch-ui task must rebuild on changes.

Backward Compatibility

  • Resource name DevTools.UI.ui.html must stay unchanged DevToolsMiddleware.cs load path requires no changes.
  • {{pathBase}} substitution semantics must stay unchanged.
  • DOM structure and CSS classes must be preserved so the UI looks identical.
  • Plugin loading, manifest (authkit.devtools), and serving paths (/grpc-ui, /devtools) must stay unchanged.

Validation

  • npm run typecheck 0 errors.
  • npm run build produces dist/ui.html with inlined bundle, no leftover /*__BUNDLE__*/ marker, {{pathBase}} intact.
  • dotnet build DevTools.csproj succeeds DevTools.UI.ui.html present in the plugin DLL.
  • Runtime check loads the published DLL and reads the embedded resource (placeholder + bundle present).
  • docker build succeeds end to end (UI stage -> publish stage).

Acceptance Criteria

Functional

  • /grpc-ui renders the catalog: each discovered service listed with package name and method count first service expanded by default.
  • Selecting method highlights it in the side panel and renders Request schema, Response schema, Metadata headers, and Request JSON cards.
  • Fill defaults populates the Request JSON from the schema (correct defaults per field type: "", 0, false, [], {}, first enum value) recursive/nested and repeated fields handled.
  • Format JSON pretty prints the textarea invalid JSON is left untouched.
  • Invoke sends unary request to /api/invoke, shows status code + elapsed ms, renders response body (pretty printed), errors, and trailers; streaming methods get disabled Invoke button.
  • Headers typed in the Metadata card persist across method switches via sessionStorage.
  • Errors (catalog fetch failure, HTTP error, non JSON response) are surfaced in the UI, never as blank page or unhandled console crash.

Build & packaging

  • npm run typecheck exits 0 (strict TypeScript).
  • npm run build produces single dist/ui.html whose only script entries are the {{pathBase}} init snippet and the inlined IIFE bundle no /*__BUNDLE__*/ marker, no inline <script src>.
  • dotnet build succeeds and the published DevTools.dll contains exactly one frontend resource: DevTools.UI.ui.html.
  • Loading the published DLL at runtime yields the embedded HTML with the {{pathBase}} placeholder intact.
  • From a clean clone (npm ci && npm run build && dotnet publish) the plugin publishes without committing node_modules/ or dist/.
  • docker build completes through the ui and publish stages and serves on startup.

Structure & boundaries

  • The composition root references HttpGrpcApi only once and otherwise consumes the GrpcApi interface no other module calls fetch.
  • No innerHTML assignments and no string built DOM anywhere (only textContent/classList/createElement).
  • No HTTP/transport concern lives in schema.ts, store.ts, or render.ts.
  • No runtime type casts outside dom.ts getById.

Non regression

  • Swagger UI and the landing page (/devtools) still serve unchanged.
  • Resource name and {{pathBase}} behavior unchanged; DevToolsMiddleware.cs load path untouched.

Non Goals

  • No framework (React/Vue/Svelte) and no bundler beyond esbuild.
  • No CSS refactor existing theme and classes retained.
  • No change to the .NET plugin contract or middleware router.
  • No frontend test framework type safety enforced via tsc.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Advanced extensionsarea/devtoolsDevTools pluginenhancementNew feature or requestepicParent/umbrella issue

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions