mikebridge commented on PR #41551:
URL: https://github.com/apache/superset/pull/41551#issuecomment-5132088867
## Consolidated review — version-history UI
Two independent review panels were run over `master...HEAD` (57 files,
+8,459/−98) at `0d2e568fc6`: a Codex panel (6 lenses) and a second panel (react
/ preset / committer). Findings below are merged and de-duplicated; **⚑ marks
items two or more independent lenses found separately**, which is the strongest
signal in here.
Worth stating plainly: the two panels disagreed sharply in places. The Codex
run's `preset-review` returned *no findings and approved*, while the second run
of the same lens returned 20 findings including 4 High. Treat neither panel's
silence as evidence.
### Already resolved — please disregard from the Codex report
Two of that report's three Blockers no longer apply:
- **"PR description omits testing instructions / checklist"** — the
description has been rewritten and now carries testing steps and a completed
checklist. *(The BEFORE/AFTER screenshot gap does still stand.)*
- **"Required frontend validation has not run"** — CI on this revision is
**63 passing / 0 failing**.
---
### High — worth fixing before a committer pass
**1. ⚑ The extracted `canOverwriteSlice` silently reverts the chart-owner
overwrite path — and it is the one change in this PR not behind the feature
flag**
`superset-frontend/src/explore/exploreUtils/canOverwriteSlice.ts:46` ·
`SaveModal.tsx:228`
SaveModal's previous local helper was `can_overwrite || isUserAdmin ||
canEditSlice || isCurrentUserOwner()`. The shared helper has no `owners`
handling, so a chart **owner** who is not an editor and not an Admin now gets
"Save (Overwrite)" disabled and the default flips to Save-As. That owner branch
was added deliberately in #41352 ("let chart owners overwrite when owners come
through as objects").
Two aggravating factors:
- Every other change here is gated by `VERSION_HISTORY`; **this one alters
the Save-modal gate for all deployments on merge**.
- The existing regression test `SaveModal.test.tsx:361` still passes
**vacuously** — its fixture slice carries `editors: [{id: 1}]`, so it now
succeeds via the editors branch and no longer exercises owners at all.
The direction may well be correct (the server's `is_editor` does not consult
`owners` either, so the old client gate was over-permissive and owners would
have hit a 403). But it should be a stated decision, not a side effect: either
restore the owner route, or note the revert in the PR description and rewrite
that test to assert the new behaviour.
**2. ⚑ Dashboard restore gate omits `is_managed_externally`, so the URL
param bypasses what the menu hides**
`DashboardVersionHistory.tsx:57` · `DashboardBuilder.tsx:438,770`
`canOverwriteSlice.ts:55` deliberately refuses externally-managed slices.
The dashboard side does not mirror it: the header menu correctly ANDs
`!is_managed_externally`, but `canRestore` uses `dash_edit_perm` alone. So on
an externally-managed dashboard the menu entry is hidden while
`?version_history=true` opens the panel **with Restore enabled**, and the
preview banner's Restore button renders. The server's restore endpoint has no
`is_managed_externally` check, so the UI is the only guard. This is the same
class of gate divergence the `canOverwriteSlice` extraction set out to
eliminate.
**3. `window.open` is called after `await`, so "Open as new" is
popup-blocked outside Chrome**
`useVersionActions.tsx:157` · `openRelated.ts:49`
`openAsNew` awaits the snapshot fetch and the create call before opening the
tab. Safari blocks any `window.open` outside the synchronous gesture task;
Firefox/Chrome block once transient activation lapses, which two sequential
round-trips will routinely exceed. It fails **silently** — the success toast
fires and the entity *is* created — so users see no tab, click again, and fork
duplicates. Open a blank tab synchronously on click and set its location, or
replace it with a toast containing a real link.
Related, and found independently by both panels: the in-flight guard reads
render-time `isCreating` before `setIsCreating(true)`, so two fast activations
both observe `false`. Use a ref-based lock cleared in `finally`, and propagate
the disabled/loading state to the kebab triggers, not just the banner button.
**4. ⚑ The restore path discards both the server error and the server's
partial-success message**
`useVersionActions.tsx:106,120`
The `restoreVersion` return value is dropped and the catch emits a fixed
string with no `getClientErrorObject`, so a 403 and a 422 carrying a real
reason are indistinguishable to the user. More seriously, the endpoint returns
`"OK; N chart(s) referenced by the snapshot no longer exist and were not
reattached"` on partial success — the frontend never reads it, so **a dashboard
restore that silently drops chart associations reports an unqualified
success**. `useVersionActivity.ts:131` already uses `getClientErrorObject`, so
this is an inconsistency within the feature.
**5. ⚑ The restore orchestration and the chart preview have no tests at all**
`useVersionActions.tsx` · `ChartVersionPreview.tsx`
Neither file has a test. Untested: no-op detection, the re-entrancy guard,
both failure toasts, and the whole `openAsNew` flow. Both container suites mock
`useVersionActions` to a stub, so the kebab restore path is unexercised
end-to-end, and `ChartVersionPreview` (236 lines, one of the four
`canOverwriteSlice` call sites) has no coverage of snapshot fetch, datasource
switching, stale-response protection, or error rendering.
**6. Preview lifecycle is one ~145-line effect**
`useDashboardVersionPreview.ts:178`
Fetch orchestration, snapshot resolution, store hydration and teardown are
encoded in a single effect, which is where the concurrency bugs in this feature
will keep coming from. Related: `DashboardVersionHistory` and
`ExploreVersionHistory` independently reimplement URL-triggered opening,
cleanup, debounced fetching and refresh detection — fixes to one will drift
from the other.
---
### Medium
- **Preview gate does not actually disable descendants.** `aria-disabled` is
descriptive only, and `pointer-events: none` plus keydown suppression do not
block programmatic or AT activation — focusable chart menus and links stay live
during historical preview. Consider `inert`. Separately, the key handler passes
through only Tab/Escape, so it also swallows Space, PageUp/Down, Home/End and
arrows — a keyboard user cannot scroll a long previewed dashboard (WCAG 2.1.1).
- **Two rollout switches with hidden coordination.** `VERSION_HISTORY` can
be on while `ENABLE_VERSIONING_CAPTURE` is off, giving a visible panel that
misleadingly reports "No history yet."
- **Flag-off behaviour is untested for four of seven gated surfaces**
(dashboard header menu, DashboardBuilder panel, PreviewBanner,
ExploreViewContainer). The two new DashboardBuilder tests never set
`window.featureFlags`, so they assert the preview gate with the flag *off* —
the opposite of the configuration they model.
- **Some layout DOM/CSS ships unconditionally**, outside the flag: the
dashboard grid template changes for everyone, and four new wrapper `<div>`s are
inserted around FilterBar / DataSourcePanel / control panels in deployments
that have the flag off.
- **`canRestore` is an authorization prop that defaults to `true`** in both
`VersionHistoryPanel` and `PreviewBanner`, and no test pins it — removing the
gate term leaves the suite green.
- **The URL-param authorization gate has no test at either call site** —
dropping `&& canRestore` from both leaves everything green. This is the
security-relevant fix in the PR and the least covered.
- **A missing effect cleanup** in `useDashboardVersionPreview.ts:313` lets
an in-flight `apply()` hydrate the global store after unmount, clobbering
whatever page mounted next. Every other async site in this feature invalidates
on teardown.
- **Restore fires two activity refetches** — the "sync so it does not
refetch" line runs before the value it means to absorb changes.
- **Dedupe key omits `from_value`/`to_value`**, so genuinely distinct
records sharing kind/operation/path within one transaction are dropped on page
merge — the collision condition the code documents elsewhere.
- **Groups with no visible records can be restored but not previewed**
(restores, imports, clones, scaffolding-only saves), which inverts the desired
risk profile: preview matters most *before* a destructive restore.
- **i18n**: sentences assembled from independently translated fragments, a
hardcoded English plural (`'%s charts updated'`) two functions below a correct
`tn()` usage, and an entity link produced by `lastIndexOf` string surgery on a
translated headline — which silently degrades or links the wrong substring
under translation.
- **No user or operator documentation** for a new panel on two of the
most-used pages: the two-flag setup, retention, who may view or restore, and
the preview/restore/open-as-new model are all undocumented.
- **`describeRecord` is a ~95-line conditional dispatch table** mixing
classification, value extraction, templates and fallback at one level.
### Low
Nested interactive controls under `role="button"`; previewed row signalled
by colour alone with no `aria-pressed`; no focus management or Escape-to-close
on the panel; session-log middleware dispatching on every explore control
keystroke whether or not the panel is ever opened; unguarded `JSON.parse` of
snapshot `params` surfacing as a generic failure; two timestamp-parsing
conventions in one feature plus lexicographic ordering of `issued_at`;
hardcoded en-US date formats; `editors: []` fabricated for preview charts when
the real value is in hand; deprecated antd `Alert message` prop; search input
missing `maxLength` against a documented 1024-char server cap;
`window.featureFlags` mutated without restore; unrelated `package-lock.json`
churn (two `@noble/hashes` entries).
### Open process question
The Codex committer lens raised that no approved SIP is linked for this
feature. That one is a judgement call for a committer rather than something to
fix in code — worth resolving explicitly, either by linking the accepted
versioning SIP as covering this UI and restore behaviour, or establishing that
on `dev@`.
---
### Worth saying — what is good here
The fetch-invalidation discipline is the strongest thing in this diff.
`useVersionActivity` bumps its fetch id *before* the `!uuid` guard specifically
so clearing the uuid invalidates an in-flight response, and
`useDashboardVersionPreview` bumps unconditionally on preview-close so
historical data cannot land after the interaction gates disappear — that is the
failure mode a preview feature actually dies of, and both are commented with
the *reason* rather than the mechanism. The session-log middleware inlines its
action constants to keep explore out of every page's bundle and then exports
them so a test can pin them against the real constants, turning a silent
death-by-rename into a failing test. And the preview key gate cites WCAG 2.1.2
for the Tab/Escape carve-out, which normally only surfaces in an audit.
_This review was generated by Claude (AI) on behalf of @mikebridge._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]