feat(dashboard): add an HTML widget for author-supplied HTML, CSS, and JS - #2971
Merged
Conversation
…d JS
Adds a widget whose body is markup, styles, and script written by the
dashboard author. It renders inside an iframe with sandbox="allow-scripts"
and never allow-same-origin, so the document runs on an opaque origin with
no access to the parent DOM, cookies, localStorage, or API session.
That isolation is the whole security boundary, and it has to hold: the
public dashboard re-exports the same canvas and is served from the same
origin as /dashboard and /api, so every widget type becomes anonymously
viewable the moment it is registered.
The document builder is a pure module (Common/Utils/Dashboard/
HtmlWidgetDocument.ts) so the sandbox construction and the element-
containment escaping are testable without a browser.
Also supports {{variableName}} interpolation across all three fields, and
exposes the resolved variables plus the dashboard's time range to script
as window.ONEUPTIME. The widget reloads on dashboard refresh.
Contributor
Author
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
…ml-widget-cdfaf7 # Conflicts: # App/FeatureSet/Dashboard/src/Components/Dashboard/Toolbar/AddWidgetModal.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a widget you can paste arbitrary HTML, CSS, and JavaScript into.
How it renders
The author's content becomes the
srcdocof an iframe withsandbox="allow-scripts"and neverallow-same-origin. That puts the document on an opaque origin: it can build DOM, run timers, fetch, draw — and it cannot touch the dashboard's DOM, cookies,localStorage, or API session, nor navigate the tab.That boundary is the feature's whole security story, because a new widget type is public by construction —
App/FeatureSet/PublicDashboardre-exports the same canvas with no allow-list, and it is served from the same origin as/dashboardand/api.allow-same-origintogether withallow-scriptsis equivalent to no sandbox at all (the frame can strip its own attribute), sogetSandboxAttribute()has no input that emits it, and a test asserts that across every permission combination.Internal/Roadmap/SessionReplay.mdargues againstallow-scriptswithoutallow-same-origin. That conclusion is specific to replay, where the rrweb Replayer runs in the parent and writes intocontentDocument— it needs same-origin and no script execution. This widget is the exact inverse, and the code says so where a reviewer will look.What the author gets
FormFieldSchemaType.HTML/CSS/JavaScriptfields. Three newComponentInputTypemembers map to them; noCommon/UIchanges.{{variableName}}in any of the three fields, resolved through the sameDashboardVariableInterpolationthe query widgets use.window.ONEUPTIME— resolved variables plus the dashboard's time range as ISO strings.Notes for review
Common/Utils/Dashboard/HtmlWidgetDocument.tsis pure string transforms, so the sandbox construction and the</style>/</script>containment escaping are testable without a browser. The React component only hands the result to the iframe.useMemodeps are serialized keys, not thevariables/dashboardStartAndEndDateobject references. A relative range resolves against the clock, so rebuilding on an unrelated render would reload the frame and discard whatever the author's script had built.pointer-events: nonein edit mode — otherwise it swallows the clicks the canvas needs to select, drag, and resize the widget.Tests
233 assertions, all passing, plus the full existing dashboard suites (
Common/Tests/Utils/Dashboard143,App/Tests/Dashboard1875).Common/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts(62) — sandbox tokens across all 8 permission combinations (neverallow-same-origin, neverallow-top-navigation, only tokens asked for, always a string so React can't drop the attribute); variable resolution; interpolation edge cases (regex-metacharacter names,$&in values, no recursive expansion); element containment asserted by parsing the built document withDOMParserand checking no<img>appeared and the script count is still 2.Common/Tests/Utils/Dashboard/DashboardHtmlComponent.test.ts(16) — defaults, argument declarations, and registry dispatch.App/Tests/Dashboard/HtmlWidgetSandbox.test.ts(13) — source-level guards: the renderer derives its sandbox from the util, neither file mentionsallow-same-origin, no dashboard widget usesdangerouslySetInnerHTML, and the type is wired into all five registries. Each omission there fails differently and none fails at compile time — one of them renders a silently empty card.Verified in a real browser
Built the actual
srcdoc+ sandbox from the util and loaded it in headless Chrome:ONEUPTIME.variables.environment == "production";{{environment}}interpolated into HTML and JS; origin: opaque (null);parent.document,parent.localStorage,localStorage,document.cookie→ SecurityErrorsandbox=""; inline<script>andonerror=in the author's markup did not run; the JS field was omitted from the document entirely</style><img onerror>in CSS and</script><img onerror>in JS both stayed inert data; CSS still appliedDocs & i18n
dashboards/widgets.mdgets an## HTMLsection — what it can and cannot do, variables, permissions, and who-can-edit-vs-who-runs-it — in all 16 locales. Both new catalog strings added to all 16Locales/*.json;npm run i18n:validateandnpm run docs:check-anchorspass.