Skip to content

Add configurable UI extension slots#3622

Open
Alex-Tideman wants to merge 6 commits into
mainfrom
extensions-exploration
Open

Add configurable UI extension slots#3622
Alex-Tideman wants to merge 6 commits into
mainfrom
extensions-exploration

Conversation

@Alex-Tideman

@Alex-Tideman Alex-Tideman commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Description & motivation 💭

Adds configurable iframe-based UI extensions so operators can mount customer-owned UI into stable Temporal UI slots without forking the main UI bundle.

This PR:

  • Adds customUi server configuration for enabling iframe extensions, including extension IDs, slot names, source URLs, allowed origins, route filters, sandbox options, sizing bounds, and permissions.
  • Exposes the configured extensions through the settings API and maps them into client-side settings.customUi.
  • Adds reusable ExtensionSlot and IframeExtension components that render configured extensions only when the slot, route pattern, iframe URL, and allowed origin all match.
  • Adds extension host utilities for route matching, origin validation, sandbox token generation, bounded iframe sizing, scoped context permissions, and same-origin navigation requests.
  • Adds a small versioned postMessage protocol:
    • Host sends temporal-ui/context and temporal-ui/theme.
    • Extensions can send temporal-extension/ready, temporal-extension/resize, and permitted temporal-extension/navigate.
  • Mounts extension slots in the top navigation, a full-width row below the top navigation, and the workflow header after workflow details.
  • Documents the extension configuration, available slots, route matching, URL/origin rules, sandboxing, permissions, messaging, and local development workflow in src/lib/extensions/README.md.

Screenshots (if applicable) 📸

Examples

Screenshot 2026-07-01 at 10 15 13 AM Screenshot 2026-07-01 at 10 14 58 AM

Design Considerations 🎨

  • Extension slots are intentionally narrow and stable so external UI can appear near existing Temporal context without changing the default UI when customUi.enabled is false.
  • Iframes are sandboxed by default with only allow-scripts; additional capabilities are opt-in per extension.
  • Workflow and user context are permission-gated so extensions only receive sensitive context when explicitly configured.
  • Extension navigation requests are limited to same-origin paths and require navigation:write.

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Local test commands run:

pnpm exec vitest run src/lib/extensions/iframe-extensions.test.ts src/lib/extensions/messages.test.ts src/lib/services/settings-service.test.ts
go test ./server/api/...

Results:

  • Vitest: 3 files passed, 30 tests passed.
  • Go: github.com/temporalio/ui-server/v2/server/api passed.

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Configure customUi.enabled: true with one or more iframeExtensions in the UI server config.
  2. Point an extension at a relative src with allowedOrigin: self, or at an HTTPS origin with a matching explicit allowedOrigin.
  3. Use one of the mounted slots:
    • app.top-nav.actions.before
    • app.top-nav.actions.after
    • app.top-nav.sub-nav
    • workflow.header.after-details
  4. Load a matching route and verify the extension iframe renders only when its slot and routePatterns match.
  5. From the extension iframe, send ready and resize messages and verify the host responds with context/theme messages and clamps dimensions to configured bounds.
  6. For a workflow header extension with permissions: [context:workflow], verify workflow context is delivered.
  7. With permissions: [navigation:write], send a same-origin navigate message and verify the host navigates; verify off-origin URLs are ignored.

Checklists

Draft Checklist

N/A

Merge Checklist

N/A

Issue(s) closed

N/A

Docs

Any docs updates needed?

Docs for the new extension framework are included in src/lib/extensions/README.md.

@Alex-Tideman Alex-Tideman requested a review from a team as a code owner July 1, 2026 18:28
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment Jul 1, 2026 10:48pm

Request Review

taskQueue: workflow?.taskQueue,
workflowType: workflow?.name,
},
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Type 'WorkflowStatus | undefined' is not assignable to type 'string | undefined'.

@temporal-cicd

temporal-cicd Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

📊 Strict Mode: 4 errors in 3 files (0.4% of 921 total)

src/lib/services/settings-service.ts (1)
  • L119:2: Type '{ auth: { enabled: boolean; options: string[] | null; redirectToProvider: boolean; }; baseUrl: string | null; codec: { endpoint: string; passAccessToken: boolean | undefined; includeCredentials: boolean | undefined; customErrorMessage: { ...; }; }; ... 19 more ...; version: string; }' is not assignable to type 'Settings'.
src/lib/layouts/workflow-header.svelte (1)
  • L199:8: Type 'WorkflowStatus | undefined' is not assignable to type 'string | undefined'.
src/routes/(app)/+layout.svelte (2)
  • L83:21: 'namespace.namespaceInfo' is possibly 'null' or 'undefined'.
  • L86:6: Argument of type 'ICapabilities | null | undefined' is not assignable to parameter of type 'NamespaceCapabilities | undefined'.

Generated by 🚫 dangerJS against 69598c6

buildIframeSandbox({
...extension().sandbox,
allowForms: true,
allowPopupsToEscapeSandbox: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowPopupsToEscapeSandbox This seems sus I feel like we shouldn't allow sandbox escaping?

).toBeNull();
expect(safeNavigationPath('javascript:alert(1)', currentOrigin)).toBeNull();
});
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some tests around iframe allow headers on origin and remote and maybe an iFrame script to reach to the parent frame and a script from the parent frame trying to reach into the child frame?

const minWidth = positiveOrUndefined(sizing.minWidth) ?? MIN_WIDTH;
const maxWidth = positiveOrUndefined(sizing.maxWidth) ?? MAX_WIDTH;
return clamp(defaultWidth, minWidth, maxWidth);
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be cool to have an @Attach on the parent html container with a ResizeObserver debounced by RAF that sends the height/width of the container to the iframe so you could have the iframe reactive to the size of the parent html contianer


CustomUI struct {
Enabled bool `yaml:"enabled"`
IframeExtensions []IframeExtension `yaml:"iframeExtensions"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think static YAML is fine for now, but wondering if it might it be worth it to support getting extensions via a manifest URL?

Something that wouldn't require a redeploy every time something needed to be changed e.g.

  extensionManifests:
    - url: https://example.com/manifest.json  
      allowedOrigin: https://example.com       
      permissions: [context:workflow]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think that's a great idea. Then that would also support Nexus endpoints - you could point the url to a Nexus endpoint and have it return a custom UI

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants