Skip to content

feat(versioning): Version History UI for Explore and Dashboard#40334

Draft
kgabryje wants to merge 17 commits into
apache:sc-103156-versioningfrom
kgabryje:feat/version-history-frontend
Draft

feat(versioning): Version History UI for Explore and Dashboard#40334
kgabryje wants to merge 17 commits into
apache:sc-103156-versioningfrom
kgabryje:feat/version-history-frontend

Conversation

@kgabryje
Copy link
Copy Markdown
Member

@kgabryje kgabryje commented May 21, 2026

SUMMARY

Adds the production frontend for entity version history on top of the backend in #39603. Implements the "View version history" experience on Explore (chart edit page) and Dashboard view. Gated behind a new VERSION_HISTORY feature flag (default False).

🚧 Draft — stacks on #39603. PR base is currently sc-103156-versioning (a mirror of #39603's head branch on apache/superset, pushed to make the diff reviewable in isolation). Once #39603 merges to master, I'll re-base this branch onto master and re-target the PR base. The current diff is only the frontend changes (+4326 / -30 lines, ~54 files in superset-frontend/ plus the feature-flag entry in superset/config.py and a small Slice.data serialization patch).

What this PR adds:

  • Shared primitives under superset-frontend/src/features/versionHistory/:
    • VersionHistoryPanel — right-side Drawer with version list, search, date-grouped sections
    • PreviewBanner — top banner shown while previewing a historical version, with Close / Open as new / Restore actions
    • RestoreConfirmModal — destructive-action confirmation
    • Hooks: useVersionList, useVersionSnapshot, useRestoreVersion
    • Two React contexts: VersionHistoryContext (panel/preview state, URL roundtrip via ?version_uuid=) and ChartPreviewContext (chart shadow-render of form_data + slice scalars)
    • Utils: summarizeChange, formatChangeTitle, groupVersionsByDate, snapshotToFormData, forkActions — change-formatting logic ported from the debug dropdown in feat(versioning): capture and expose version history for charts, dashboards, and datasets #39603
  • Explore wiring:
    • "View version history" menu item in the chart actions menu, gated on the feature flag, disabled when hasUnsavedChanges
    • Chart shadow-renders snapshot form_data via ChartPreviewContext during preview — no Redux mutation. Slice-level scalar fields (description, name display) also shadow-rendered.
    • Save button disabled while previewing
    • "Open as new chart" hydrates the snapshot into form_data and POSTs a new chart with name pattern {original} (copy from {date})
    • Slice.data backend serialization now exposes uuid so the chart's frontend can address the versioning endpoints
  • Dashboard wiring:
    • "View version history" menu item at the bottom of the dashboard actions menu, gated on the flag, disabled in edit mode
    • Inline preview via the captured-original Redux pattern: ENTER_VERSION_PREVIEW / EXIT_VERSION_PREVIEW actions, four reducers extended (dashboardState, sliceEntities, dashboardLayout, dashboardInfo), undoableDashboardLayout.TRACKED_ACTIONS updated. Enter captures live sliceEntities + dashboardLayout + scalar dashboardInfo fields and swaps in the snapshot's values; exit restores the captured originals. No refetch.
    • DashboardPreviewBanner mounts above DashboardGrid in DashboardContainer.tsx
    • "Open as new dashboard" POSTs to /api/v1/dashboard/ with the snapshot's fields
  • Feature flag VERSION_HISTORY added to superset/config.py DEFAULT_FEATURE_FLAGS (default False), the frontend FeatureFlag enum, and docs/static/feature-flags.json.

What is NOT in this PR (deferred to follow-up work):

  • Datasets — the backend exposes the same endpoints for datasets, but the UI is scoped to Explore + Dashboard for V1.
  • AI attribution UI — the backend changed_by is rendered as the user name; a chatbot-author tag is deferred until a stable identity convention lands.
  • Tags / owners / roles in restore — backend leaves these at their live values; no UI surface needed.
  • Per-chart position inside a dashboardposition_json is versioned as an opaque blob (restored wholesale on dashboard restore); finer-grained layout versioning is a follow-up.
  • Diff-content search — search box filters version titles, not the full structured diff payload.

BEFORE/AFTER SCREENSHOTS

To attach after stack-base settles.

TESTING INSTRUCTIONS

Enable the flag and seed history against #39603's endpoints.

  1. Enable the feature flag in superset_config.py:
FEATURE_FLAGS = {"VERSION_HISTORY": True}
  1. Chart preview + restore:

    • Open any chart in Explore. Make 3 edits and Save 3 times to seed version history.
    • Click the three-dot menu → View version history. Right-side panel opens listing versions newest-first, with author + timestamp.
    • Click a version row → preview banner appears at the top, chart re-renders the historical form_data (including title and description in the header), Save button is disabled.
    • Click Restore this version → confirm modal → success toast → panel refreshes with the restore as the new latest version.
    • With preview open, copy the URL, paste in new tab → preview reopens on the same version (URL roundtrip).
  2. Chart fork (Open as new chart):

    • Open the panel, click the three-dot menu on any version row → Open as new chart.
    • New chart created with name {original} (copy from {YYYY-MM-DD}). Success toast. Navigates to the new chart's explore URL.
  3. Dashboard preview + restore:

    • Open any dashboard. Make changes via edit mode, save, repeat to seed.
    • Three-dot menu → View version history. Panel opens. (In edit mode the menu item is disabled with a tooltip.)
    • Click a version row → grid re-renders the historical layout + slice data + scalar dashboard fields (title, description) via the captured-original swap; banner appears above the grid.
    • Close preview → grid + scalars restore to live state.
    • Restore this version → confirm modal → success → page reflects the restored state.
  4. Dashboard fork (Open as new dashboard):

    • Panel → three-dot menu on a version → Open as new dashboard. New dashboard created with snapshot's layout. Navigate.
  5. Feature flag off: disable VERSION_HISTORY → menu items vanish, panels do not mount, no console errors.

  6. Tests:

cd superset-frontend
npm run type
npm test -- src/features/versionHistory
npm test -- src/dashboard/reducers

ADDITIONAL INFORMATION

Depends on: #39603 (backend versioning endpoints). Base will be re-pointed to master after #39603 merges.

kgabryje and others added 11 commits May 21, 2026 13:24
Adds the cross-entity primitives (types, hooks, context, components)
used to build chart and dashboard version history UI on top of the
backend versioning endpoints introduced in apache#39603.

Gated behind a new VERSION_HISTORY feature flag (default off).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps the Explore subtree in VersionHistoryProvider when the
VERSION_HISTORY feature flag is on and the chart has a uuid, mounting
the side panel + preview banner. Adds a "View version history" item to
the additional-actions menu; the item is disabled with a tooltip when
the chart has unsaved form-control changes. While previewing, the Save
button is disabled and the chart panel shadow-renders snapshot
form_data via a dedicated context — no Redux mutation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mounts VersionHistoryProvider + side panel on the dashboard Header when
the VERSION_HISTORY feature flag is on and the dashboard has a uuid.
Adds a "View version history" item to the bottom of the dashboard
options menu; disabled with a tooltip while in edit mode.

The panel lists versions and the restore flow goes through the backend
endpoint. Inline preview rendering of historical layouts on the
dashboard grid is deferred — that needs a Redux capture-and-replay of
sliceEntities + dashboardLayout, and is tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers summarizeChange across the layout/field/fallback branches that
Mike's demo handles, groupVersionsByDate edge cases, the snapshot →
form_data adapter, and the useVersionList hook's API-shape handling
(oldest-first → newest-first reversal, error path).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-define

oxlint flagged the wrapping components for using their inner helpers
before declaration. Moves inner helpers above their outer wrappers; no
behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Includes one type-check fix (Change[] cast went through unknown rather
than directly) and a prettier autofix on the snapshotToFormData spread.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The username-sniffing heuristic guessed too aggressively. AI attribution
will land properly once the backend exposes an authored_by-flavored
field; until then, all versions render with their plain user name and
the search input drives the only filter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds ENTER_VERSION_PREVIEW / EXIT_VERSION_PREVIEW actions wired through
three reducers (dashboardState, sliceEntities, dashboardLayout). The
enter thunk captures the live sliceEntities + dashboardLayout.present
into dashboardState.versionPreview, then dispatches the snapshot's
slices + parsed position_json. The exit thunk reads back the captured
originals and dispatches them as the new values; no backend re-fetch,
no editMode toggle.

DashboardPreviewBridge subscribes to VersionHistoryContext's
previewVersionUuid, fetches the snapshot, and drives the swap. Cleanup
on unmount calls exitVersionPreview. DashboardPreviewBanner mounts
above DashboardGrid and offers Close preview + Restore (with the
existing RestoreConfirmModal).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds forkChartFromSnapshot + forkDashboardFromSnapshot that POST the
snapshot's fields to the resource's create endpoint with a copy-name
("{original} (copy from {YYYY-MM-DD})") and return the new id. The
Explore and dashboard mount components fetch the snapshot for the
selected version, fork it, toast on success, and navigate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Exercises ENTER/EXIT_VERSION_PREVIEW across all three reducers (state,
slices, layout), the captured-original roundtrip, and the chart +
dashboard fork POST bodies built from a snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 21, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 8ea195a
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a0f29107478dd00084dbd29
😎 Deploy Preview https://deploy-preview-40334--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

kgabryje and others added 4 commits May 21, 2026 16:27
Correctness:
- Preserve the original live-state capture when switching between two
  version previews (A → B without exit no longer corrupts the exit
  target).
- Clear redux-undo history at the boundaries of preview enter/exit so
  Ctrl+Z in a later edit-mode session can't walk back into a historical
  layout.
- Reload after a successful restore so the in-memory entity reflects the
  new backend state (Redux slices were silently stale otherwise).
- Validate the ?version_uuid= URL param against a canonical UUID regex
  before it reaches SupersetClient endpoints.
- Cancel stale useVersionList responses via a request-id token so a
  late reply can't clobber a newer one.
- Clean up the ?version_uuid= URL param on DashboardPreviewBridge
  unmount so reloading the page doesn't silently re-enter preview.

Type safety:
- Extend Slice.uuid, DashboardInfo.uuid, DashboardState.versionPreview
  and VersionSnapshot.changes so the previously-cast call sites
  type-check without `as unknown as` ladders.

Resilience / UX:
- useRestoreVersion now logs and surfaces the server error detail
  instead of swallowing the rejection.
- Fork-action helpers accept an optional ownerId so the new resource
  starts with the current user as an owner.
- Defensive fallback for formatVersionUser when name + username are
  both empty.
- PreviewBanner buttons are both disabled while a restore is in flight.

A11y / API:
- Replace the row-level <button> in VersionItem with a div role=button
  + keyboard handler so the dropdown trigger isn't nested inside
  another native button.
- Add useRequiredVersionHistory variant that throws when no provider is
  mounted (useVersionHistory continues to return a no-op stub for the
  legitimate "feature flag off" case).

Tests:
- Add a thunk-level A → B preview switch test that pins the new
  preserve-original behavior.
- Verify exitVersionPreview is a no-op when no preview is active.
- Add an owners-included case to the fork-action tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The chart-side menu item was rendering but inert because
state.explore.slice.uuid was never populated — Slice.data didn't
serialize it, so the bootstrap payload arrived without uuid, the
ExploreVersionHistoryRoot never wrapped the tree, and the menu
fell back to the no-op stub from useVersionHistory.

Backend:
- Include uuid in Slice.data so the Explore bootstrap payload carries
  it through hydrateExplore into state.explore.slice. SliceSchema gets
  the matching field for OpenAPI parity.

Frontend:
- Switch ExploreChartHeader to useOptionalVersionHistory so the menu
  item gate (slice && onOpenVersionHistory && featureEnabled) is only
  truthy when a provider has actually mounted. The previous stub
  fallback rendered the item but did nothing on click.
- Tag both Explore and Dashboard provider mounts with a
  data-test="version-history-provider-mount" marker for Playwright
  smoke tests to detect mount.

Tests:
- Backend: Slice.data now returns uuid (covered with + without uuid).
- Frontend: hydrateExplore preserves slice.uuid through to
  state.explore.slice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dashboard preview crash:
The snapshot endpoint for dashboards returns only scalar columns plus
a ``_version`` metadata blob — there is no ``slices`` field today.
enterVersionPreview was replacing sliceEntities.slices with the
normalized (and therefore empty) snapshot value, wiping every chart
entry. DragDroppable then looked up each CHART- component's chartId
in an empty map, got undefined, and crashed on ``.type``.

Fix: merge the snapshot's slices on top of the live ones rather than
replacing. The live map wins for ids the snapshot doesn't carry; the
snapshot wins for ids it does (matching the eventual contract). The
``slices`` field is still optional in the payload — once the backend
emits per-version slice form_data, callers get the right behavior
without any further frontend change.

Guard: enterVersionPreview now bails (returns false) when the parsed
position_json lacks ROOT_ID / GRID_ID structure. The Dashboard mount
shows ``Snapshot is missing layout structure, cannot preview`` and
backs out of preview mode instead of dispatching a malformed swap.

Banner Invalid Date:
ExplorePreviewBanner read issued_at off the snapshot root, but the
single-version endpoint nests version metadata under ``_version`` and
omits root-level ``issued_at``. The banner now prefers the matching
list-row's issued_at (already loaded via useVersionList), falls back
to ``snapshot._version.issued_at``, and renders an empty string only
if both are missing — no more "Invalid Date" rendering.

Tests:
- enterVersionPreview merges live slices when the snapshot omits them.
- enterVersionPreview bails on missing / empty position_json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SupersetClient.get returns ``{ json: { result: ... } }`` but the
snapshot hook was setting the envelope itself as the snapshot value.
Every downstream consumer then saw ``snapshot.position_json`` /
``snapshot.issued_at`` / ``snapshot.params`` as undefined.

The dashboard guard added in the previous commit caught the
position_json absence and bailed into the danger toast, which masked
the real bug — healthy snapshots were being rejected. The chart
preview's "Invalid Date" was the same root cause for issued_at, masked
by the list-row fallback.

useVersionList already unwraps the envelope; useVersionSnapshot now
matches that contract.

Test: new __tests__/useVersionSnapshot.test.ts pins the unwrap with a
realistic dashboard payload and asserts position_json + issued_at land
at the root of the returned snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions github-actions Bot added i18n Namespace | Anything related to localization risk:db-migration PRs that require a DB migration i18n:french Translation related to French language api Related to the REST API doc Namespace | Anything related to documentation packages risk:ci-script PR modifies scripts that execute in CI (supply chain risk) labels May 21, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 42.86667% with 857 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.61%. Comparing base (1230b90) to head (dd53841).
⚠️ Report is 62 commits behind head on master.

Files with missing lines Patch % Lines
superset/versioning/baseline.py 42.39% 98 Missing and 8 partials ⚠️
superset/versioning/diff.py 61.75% 79 Missing and 17 partials ⚠️
superset/versioning/queries.py 21.55% 91 Missing ⚠️
superset/tasks/version_history_retention.py 0.00% 77 Missing ⚠️
superset/charts/api.py 22.50% 62 Missing ⚠️
superset/datasets/api.py 22.50% 62 Missing ⚠️
superset/dashboards/api.py 22.78% 61 Missing ⚠️
superset/versioning/changes.py 76.15% 38 Missing and 19 partials ⚠️
superset/daos/dataset.py 12.00% 44 Missing ⚠️
superset/commands/version_restore.py 0.00% 33 Missing ⚠️
... and 17 more
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #40334       +/-   ##
===========================================
- Coverage   64.16%   41.61%   -22.55%     
===========================================
  Files        2591      769     -1822     
  Lines      138293    65065    -73228     
  Branches    32084     7929    -24155     
===========================================
- Hits        88737    27080    -61657     
+ Misses      48026    37269    -10757     
+ Partials     1530      716      -814     
Flag Coverage Δ
hive 38.96% <22.60%> (-0.49%) ⬇️
javascript ?
mysql ?
postgres ?
presto 41.07% <42.86%> (-0.06%) ⬇️
python 41.14% <42.86%> (-19.51%) ⬇️
sqlite ?
superset-extensions-cli 90.82% <ø> (?)
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

kgabryje and others added 2 commits May 21, 2026 18:19
#1 Fork handlers unwrap json.result. ExploreVersionHistoryMount and
DashboardVersionHistoryMount were passing the SupersetClient envelope
straight to the fork helpers, so every fork POSTed an empty body and
the new resource ended up without datasource / params / layout.

#2 useRestoreVersion returns { ok, error } directly. The previous
lastError-via-state approach hit a stale-closure bug: the danger toast
read the pre-restore state value instead of the current error detail,
silently dropping the formatted message.

#3 Dashboard menu gates on useOptionalVersionHistory. The strict
useVersionHistory stub fallback returns a truthy openPanel, so the
menu item rendered as an inert click target when the provider wasn't
mounted (uuid not yet loaded). Symmetric with the chart-side fix in
40fb5ac.

#4 DashboardPreviewBanner surfaces the restore error detail. Mirrors
the side panel's behavior now that #2 exposes the error directly.

#5 Drop _version.issued_at / _version.changes fallback ladders. These
were workarounds for the envelope unwrap bug fixed in 98e2217;
with the unwrap in place the root-level issued_at is populated and
the fallback chain only created noise.

#13 Consistent fork id reading. forkDashboardFromSnapshot stops
double-checking json.result.id; both fork endpoints return the id at
the root.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ss 3

Selecting a historical version of a dashboard now updates the visible
title, description, slug, CSS, json_metadata, and published flag — not
only the slice grid. The Header reads its H1 from
``layout[DASHBOARD_HEADER_ID].meta.text`` and the rest is read from
``state.dashboardInfo``; neither lives in ``dashboardLayout.present``
alone, so the previous swap left them showing live values during
preview. The thunk now captures these scalars into
``dashboardState.versionPreview.capturedDashboardInfo`` on enter,
applies the snapshot's values via new ``ENTER_VERSION_PREVIEW`` /
``EXIT_VERSION_PREVIEW`` handlers in ``dashboardInfo.ts``, and injects
the snapshot's ``dashboard_title`` into ``DASHBOARD_HEADER_ID`` as a
defense (some snapshots' ``position_json`` lacks the header block).
A → B switches preserve the original captured live state so exit
always restores the user's pre-preview values.

For charts, a new ``ChartPreviewSliceOverrides`` shape on
``ChartPreviewContext`` carries snapshot ``slice_name``, ``description``,
``certified_by`` and ``certification_details``; ``ExploreChartHeader``
prefers those during preview without mutating ``state.explore.slice``,
preserving the "chart preview = no Redux mutation" invariant. Title
editing and Save are also disabled while previewing.

Also rolls in the second-pass review follow-ups:
- useVersionList: cancellation race test (issued vs. winning request).
- dashboardPreview: snapshot-wins merge direction + dashboardInfo
  capture/restore coverage.
- DashboardPreviewBridge: adopt strict useRequiredVersionHistory; bridge
  is the sole owner of exitVersionPreview cleanup (banner stops
  double-dispatching).
- VersionItem: actions dropdown moved out of the clickable row so
  role=button no longer nests inside role=button.
- formatVersionDate: empty-string guard returns '' instead of
  "Invalid Date".
- useVersionSnapshot: chart-shape unwrap test pinned alongside the
  existing dashboard-shape one.
- VersionSnapshot: index signature replaced with a tight field set
  (scalars, slices, position_json, _version, etc.); migrated the
  few ``as unknown as`` ladders this enabled.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kgabryje kgabryje changed the base branch from master to sc-103156-versioning May 22, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API doc Namespace | Anything related to documentation i18n:french Translation related to French language i18n Namespace | Anything related to localization packages risk:ci-script PR modifies scripts that execute in CI (supply chain risk) risk:db-migration PRs that require a DB migration size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant