Skip to content

feat(dashboards): add read-only kiosk mode#2703

Open
MikeShi42 wants to merge 7 commits into
mainfrom
cursor/dashboard-kiosk-mode-3998
Open

feat(dashboards): add read-only kiosk mode#2703
MikeShi42 wants to merge 7 commits into
mainfrom
cursor/dashboard-kiosk-mode-3998

Conversation

@MikeShi42

@MikeShi42 MikeShi42 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a shareable, URL-backed kiosk mode for dashboards intended for static displays. Kiosk mode reduces the app to the dashboard name and live/read-only status, locks all dashboard authoring controls, and forces automatic refresh including live behavior for trace/search tiles.

Kiosk mode deliberately does not control browser fullscreen. Viewers can choose browser or operating-system fullscreen independently without changing kiosk state. The saved dashboard is never changed; tab and group expansion remain viewer-only URL state.

Screenshots or video

dashboard_kiosk_url_only_demo.mp4

How to test on Vercel preview

Preview routes: /dashboards

Steps:

  1. Open /dashboards, select Create New Saved Dashboard, and add a chart tile.
  2. Open the dashboard overflow menu and select Enter kiosk mode.
  3. Verify the URL includes kiosk=true and only the dashboard name, Live · Read-only status, exit control, and dashboard tiles remain visible.
  4. Verify browser chrome remains unchanged; kiosk mode must not request or exit native fullscreen.
  5. Reload the page and verify kiosk mode remains active with navigation, query controls, tile actions, resizing, and Add controls hidden.
  6. Select Exit kiosk mode and verify the standard editable dashboard and navigation return.

Validation

  • App lint, TypeScript, and style checks pass.
  • Focused kiosk regression suites pass: 30 tests.
  • Manual browser validation confirms kiosk mode enters/exits without changing native fullscreen state.
  • Full CI passes, including unit/integration checks, all four Playwright shards, and the aggregate E2E check.

References

  • Linear Issue: N/A
  • Related PRs: N/A

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Co-authored-by: Mike Shi <mike@hyperdx.io>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 24, 2026 10:04pm
hyperdx-storybook Ready Ready Preview, Comment Jul 24, 2026 10:04pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a58a3c8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a URL-backed read-only kiosk mode for dashboards. The main changes are:

  • A kiosk query-state hook with enter and exit actions.
  • A minimal kiosk header with live/read-only status.
  • Hidden dashboard navigation, filters, authoring controls, drag handles, and layout editing in kiosk mode.
  • Forced live refresh while kiosk mode is active.
  • Unit and E2E coverage for kiosk entry, reload persistence, read-only chrome, and exit.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
packages/app/src/hooks/useDashboardKioskMode.ts Adds URL query-state handling for entering and exiting kiosk mode.
packages/app/src/DBDashboardPage.tsx Wires kiosk mode into dashboard chrome, read-only tile behavior, layout locking, and live refresh.
packages/app/src/components/DashboardContainer.tsx Adds read-only gating for group and tab authoring controls.
packages/app/src/layout.tsx Hides global app navigation and banner chrome for dashboard kiosk URLs.
packages/app/tests/e2e/features/dashboard.spec.ts Adds E2E coverage for entering, persisting, and exiting kiosk mode.

Reviews (6): Last reviewed commit: "Merge branch 'main' into cursor/dashboar..." | Re-trigger Greptile

Comment thread packages/app/src/hooks/useDashboardKioskMode.ts Outdated
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 243 passed • 1 skipped • 1005s

Status Count
✅ Passed 243
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

cursoragent and others added 2 commits July 22, 2026 03:04
Co-authored-by: Mike Shi <mike@hyperdx.io>
Co-authored-by: Mike Shi <mike@hyperdx.io>
Comment thread packages/app/src/hooks/useDashboardKioskMode.ts
cursoragent and others added 2 commits July 24, 2026 16:12
react-grid-layout retains a .react-resizable-handle element in the DOM
when isResizable={false}, making it CSS-hidden rather than removing it.
The previous toHaveCount(0) assertion failed because 1 element was still
present in DOM.

Change the assertion to toBeHidden(), which passes for both CSS-hidden
elements and elements that do not exist at all — matching the actual
user-visible contract (handles non-interactive in kiosk mode).

The page object JSDoc already stated 'absent or hidden', so only the
spec assertion needed updating.

Co-authored-by: Mike Shi <mike@hyperdx.io>
Co-authored-by: Mike Shi <mike@hyperdx.io>
Co-authored-by: Mike Shi <mike@hyperdx.io>
@cursor cursor Bot changed the title feat(dashboards): add full-screen kiosk mode feat(dashboards): add read-only kiosk mode Jul 24, 2026
Comment thread packages/app/src/hooks/useDashboardKioskMode.ts
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jul 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 474 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 7
  • Production lines changed: 474 (+ 248 in test files, excluded from tier calculation)
  • Branch: cursor/dashboard-kiosk-mode-3998
  • Author: MikeShi42

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

🔴 P0/P1 — must fix

  • packages/app/src/DBDashboardPage.tsx:1834isLive: isLive || isKioskMode forces the refresh interval on, and refresh() rewrites the URL's from/to to a now-relative window, so a pinned absolute range is silently destroyed, is not restored on kiosk exit, and cannot be stopped because the Live toggle lives in the hidden query toolbar.
    • Fix: Force live only when the current range is already now-anchored, or snapshot from/to on kiosk enter and restore them on exit.
    • correctness, testing, performance

🟡 P2 — recommended

  • packages/app/src/layout.tsx:30isDashboardKioskMode is derived from router.query.kiosk, which lags nuqs' optimistic state and is {} before router.isReady, so entering kiosk and cold-loading ?kiosk=true both paint an intermediate frame with AppNav still mounted beside the minimal kiosk header, then reflow.
    • Fix: Read kiosk state in PageWrapper from the same nuqs parser the page uses, or gate the AppNav decision on router.isReady the way the banner is already gated on hasMounted at line 28.
    • adversarial, julik-frontend-races
  • packages/app/src/layout.tsx:34 — The kiosk predicate is implemented twice, as a hand-rolled === 'true' / Array.isArray(...).includes('true') check here and as parseAsBoolean in the hook, with the param name 'kiosk' a bare literal in both places and no shared constant.
    • Fix: Export the query key and predicate from useDashboardKioskMode.ts and consume them in PageWrapper so one parser governs both consumers.
    • maintainability, correctness, adversarial, agent-native
  • packages/app/src/hooks/useDashboardKioskMode.ts:20useHotkeys([['Escape', exitKioskMode]]) is registered unconditionally, so a single Escape that dismisses a Mantine overlay also drops the display out of kiosk mode, and every Escape on a normal dashboard calls setIsKioskMode(null) for a param that is absent.
    • Fix: Guard the handler on isKioskMode and ignore the key when an overlay has already consumed the event.
    • correctness, maintainability, julik-frontend-races, testing
  • packages/app/src/hooks/useDashboardKioskMode.ts:7 — Kiosk enter uses history: 'replace' while each forced refresh tick pushes a history entry through the time-range state, so an unattended display accumulates roughly one entry per interval and the Back button can never leave kiosk mode.
    • Fix: Use replace-history semantics for the refresh-driven time-range writes while kiosk is active, and push a history entry when entering kiosk so Back is a natural exit.
    • correctness
  • packages/app/src/components/DashboardKioskHeader.tsx:25 — The header renders a green dot and the literal Live · Read-only unconditionally, but its entire prop surface is { dashboardName, onExit }, so it cannot reflect that isRefreshEnabled is gated on useDocumentVisibility() and refresh has stopped.
    • Fix: Pass isRefreshEnabled and a last-successful-refresh timestamp into the header and render an idle state plus a relative "updated" time when refresh is not running.
    • adversarial
  • packages/app/src/DBDashboardPage.tsx:3026 — When dashboard is undefined from a deleted id or a failed fetch, the entire grid subtree including its ErrorBoundary is skipped, and every banner is gated on !isKioskMode, so a wall display shows a titled header with a green Live dot over an empty page indefinitely.
    • Fix: Render an explicit unavailable state in kiosk mode once fetching settles with no dashboard, and keep at least one visible error affordance for query failures.
    • adversarial, agent-native
  • packages/app/src/DBDashboardPage.tsx:1128getRowSearchLink is passed to table tiles with no readOnly guard while sibling props such as showDisplaySwitcher={!readOnly} are gated, so one stray click navigates the display off the dashboard, possibly to an arbitrary external URL, with AppNav hidden and no return path.
    • Fix: Thread readOnly into the row-navigation paths and suppress row onClick actions and the heatmap drill-down link when read-only.
    • adversarial
  • packages/app/src/DBDashboardPage.tsx:1834 — Because the interval period is derived from auto-granularity, any range up to 15 minutes yields a 15-second refresh, so a kiosk URL with a short range re-queries every tile every 15 seconds with no floor, backoff, or reachable throttle.
    • Fix: Clamp the kiosk refresh interval to a minimum floor independent of auto-granularity, or expose the interval as a query param.
    • performance
  • packages/app/src/DBDashboardPage.tsx:1374CollapsedToolbarProvider is rendered unconditionally, so tiles narrower than the 200px container-query breakpoint still show a kebab button in kiosk mode whose dropdown is empty because collapsedMenuItems is null when read-only.
    • Fix: Render the collapsed menu only when it has items, or skip the provider entirely when readOnly.
    • correctness
  • packages/app/src/hooks/__tests__/useDashboardKioskMode.test.tsx:5 — The suite mocks useQueryState to a constant [false, mock] and mocks useHotkeys entirely, so isKioskMode is never observed as true, the parser and history: 'replace' option are never exercised, and the fullscreen test asserts the absence of code the hook never contained.
    • Fix: Delete the fullscreen scaffolding and its test, exercise the real parser against ?kiosk=true, and assert the Escape binding is registered and is inert when kiosk is off.
    • testing, maintainability, adversarial, julik-frontend-races
  • packages/app/src/DBDashboardPage.tsx:2167 — Nothing tests the feature's central read-only guarantee: onEditClick, onDuplicateClick, and onDeleteClick are still passed to Tile unconditionally and only readOnly={isKioskMode} suppresses the affordances, so a regression in that handling would re-enable mutation with no failing test, and there is no e2e spec for the enter/reload/exit flow.
    • Fix: Add a render test asserting no tile mutation affordance and no EditTileModal in kiosk mode, plus an e2e spec covering enter, reload persistence, and exit.
    • testing, maintainability, adversarial, julik-frontend-races
🔵 P3 nitpicks (12)
  • packages/app/src/layout.tsx:34 — For a duplicated param such as ?kiosk=false&kiosk=true, the layout's .includes('true') returns true while parseAsBoolean reads the first value, hiding AppNav while the page renders the fully editable header with no exit button.
    • Fix: Narrow arrays to their first element rather than matching any occurrence, or share the parser.
  • packages/app/src/DBDashboardPage.tsx:497 — The f hotkey and isFocused hover tracking are not gated on readOnly, so hovering a tile in kiosk mode and pressing f opens a modal containing a TimePicker and GranularityPicker.
    • Fix: Gate the f hotkey and focus tracking on !readOnly.
  • packages/app/src/DBDashboardPage.tsx:3191fillViewport applies height: 100vh where the root otherwise uses min-height: 100%, capping the sticky header's containing block to one viewport so the only exit control scrolls away on a dashboard taller than the screen.
    • Fix: Use min-height: 100vh for .fillViewport, or drop the prop since the root already fills its container.
  • packages/app/src/layout.tsx:76 — Unmounting AppNav contradicts the file's own docblock about preserving nav state and re-runs its mount-only hdx-login-redirect-url consumer on every kiosk exit, which can Router.push away from the dashboard being viewed.
    • Fix: Hide AppNav with CSS instead of unmounting it, or move the redirect consumer up into the layout.
  • packages/app/src/layout.tsx:32 — The pathname allowlist includes the no-id /dashboards route, where an unsaved local dashboard in kiosk mode renders nothing but a header because the temporary-dashboard banner, Add menu, and empty grid are all suppressed.
    • Fix: Restrict the kiosk predicate to the saved-dashboard route, or render an explicit empty state.
  • packages/app/src/DBDashboardPage.tsx:2167 — The diff adds roughly 21 independent !isKioskMode render guards and prop ternaries across a 3206-line component with no type, lint rule, or test that would catch a newly added authoring control that forgets one.
    • Fix: Consolidate the authoring-only subtree behind a single branch so new controls inherit the gate.
  • packages/app/src/layout.tsx:32 — The two dashboard pathnames are hard-coded, so a renamed or added dashboard route silently keeps AppNav visible while the page still enters kiosk mode.
    • Fix: Derive the check from a shared route helper rather than string equality.
  • packages/app/src/components/DashboardKioskHeader.tsx:16 — The fallback 'Untitled Dashboard' diverges from the canonical default name 'My New Dashboard' in dashboard.ts, and the required dashboardName: string forces the caller to pass '' as a sentinel it then re-derives with a falsy check.
    • Fix: Type the prop as optional and reuse the existing default-name string.
  • packages/app/src/hooks/useDashboardKioskMode.ts:17exitKioskMode passes null against a parser declared .withDefault(false), which correctly clears the param but reads as an inconsistency that a future edit could "fix" into persisting kiosk=false in shared URLs.
    • Fix: Add a one-line comment stating that null removes the param rather than writing kiosk=false.
  • packages/app/src/components/DashboardKioskHeader.tsx:30style={{ borderRadius: '50%', flexShrink: 0 }} is the exact raw-style anti-pattern that agent_docs/code_style.md:34 forbids in favour of individual Mantine style props.
    • Fix: Replace the raw style object with Mantine style props.
  • packages/app/src/components/DashboardKioskHeader.tsx:19aria-label is set on a Mantine Group, which renders a plain div with no role, so the live/read-only status is not reliably exposed to assistive technology.
    • Fix: Add role="status" to the group or move the label onto an element with a role.
  • packages/app/src/layout.tsx:42 — The synthetic resize effect has no mount guard, so it fires on the first commit of every page using withAppNav, not just dashboards.
    • Fix: Dispatch only on an actual transition of the kiosk flag.

Reviewers (9): correctness, adversarial, testing, maintainability, kieran-typescript, julik-frontend-races, performance, project-standards, agent-native.

Scope caveat: bash, grep, and glob were unavailable in this environment, so the diff could not be computed with git; the change surface was reconstructed by reading .git/index and the files themselves, and reviewers were constrained to Read. Line attributions were verified against file contents, but an unrelated changed file could have been missed.

Testing gaps:

  • No test ever observes isKioskMode === trueuseQueryState is mocked to a constant, so the parser, default, and history: 'replace' option are entirely unexercised.
  • No test asserts the read-only guarantee: tile edit/duplicate/delete/move suppression, EditTileModal absence, grid drag/resize disabled, AppNav hidden.
  • No e2e spec for the enter → reload-persists → exit flow, despite existing dashboard*.spec.ts suites.
  • Nothing verifies that layout.tsx's hand-rolled predicate and the hook's parser agree for the same URL, including the duplicated-param case.
  • No coverage of kiosk mode with a deleted or unfetchable dashboard, or of forced live refresh over a pinned absolute range.
  • The document.fullscreenElement / requestFullscreen / exitFullscreen property overrides in beforeEach have no afterEach restoring the original descriptors.

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

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants