mikebridge commented on PR #41550:
URL: https://github.com/apache/superset/pull/41550#issuecomment-5166719110

   # Frontend self-review pass at `1816052493`
   
   An AI-assisted frontend-only pass over `preset/master...HEAD` (49 commits, 
85 files, +17,348 −5,829 — the 24 `.ts`/`.tsx` files of it), run against our 
React/Redux review checklist. Every `file:line` below was re-read at that SHA 
before posting, and anything that didn't survive the check was dropped rather 
than hedged.
   
   Prior threads from @kgabryje and codeant were read first; the five 
substantive frontend findings are all genuinely resolved in the tree, so they 
aren't repeated here. **No Critical issues.** Two type-contract weaknesses 
follow — neither is reachable as a user-visible bug today, both are one-line 
fixes.
   
   ## Warning 1 — bulk delete discriminates on the optional `source_type`, 
while every other semantic-view branch uses the required `kind`
   
   `superset-frontend/src/pages/DatasetList/index.tsx:1278`, `:1281`, `:1590`
   
   `Dataset` declares `kind: 'physical' | 'virtual' | 'semantic_view'` 
(required) but `source_type?: 'database' | 'semantic_layer'` (**optional**, 
`:153`). The new bulk-delete routing and the semantic-view count driving the 
modal's recoverability promise both key off the optional one:
   
   ```ts
   d => d.source_type === 'semantic_layer'        // :1281, and :1590 for the 
count
   ```
   
   whereas row rendering (`:844`), the row-action branch, and export (`:615`) 
all use `kind === 'semantic_view'`.
   
   Were `source_type` ever absent, `undefined` classifies a semantic view as a 
regular dataset, so `pendingBulkSemanticCount === 0` → `recoverable={softDelete 
&& true}` at `:1549` → the modal drops the type-DELETE gate and promises *"will 
be moved to Recently Archived. You can recover them there"* — then sends a 
semantic-view id to `DELETE /api/v1/dataset/`. This file's own export comment 
at `:607-613` spells out what follows: that endpoint *"looks rows up by bare 
numeric id against `tables` only — passing a semantic-view id silently returns 
whatever SqlaTable happens to share that id."*
   
   **Not reachable today** — `superset/datasource/schemas.py:45` and `:96` both 
emit `source_type` via `fields.Constant`, so it is always present. That is what 
keeps this a Warning rather than a Critical. But the TS type says otherwise, so 
nothing in the compiler or the tests binds that guarantee, and the failure mode 
is deleting an unrelated object under an "Archive" promise.
   
   **Fix:** use `kind === 'semantic_view'` at all three sites, matching the 
path that already documents the hazard. `kind` is the stronger discriminator — 
`SemanticViewListSchema.kind` is a `fields.Constant("semantic_view")` 
(`schemas.py:95`), so a dataset can never produce that value, while 
`DatasetListSchema.kind` is a plain `fields.String()`. Making `source_type` 
required in the TS type would also close it, but leaves two fields encoding one 
fact.
   
   ## Warning 2 — `ArchivedItem` doesn't declare the field the Archived column 
renders
   
   `superset-frontend/src/pages/ArchivedList/types.ts:84-95`, rendered at 
`superset-frontend/src/pages/ArchivedList/index.tsx:325`
   
   The type declares `deleted_at?: string | null` (`:88`), used only as a sort 
id. The value actually rendered is `deleted_at_delta_humanized`, which is **not 
declared** — it resolves through the `[key: string]: unknown` index signature 
at `:95`, so `String(original.deleted_at_delta_humanized ?? '')` compiles 
regardless.
   
   The server attaches it post-`dump` in `superset/views/filters.py:437` 
(`_inject_deleted_at`), and it is genuinely nullable — 
`deleted_at_map.get(row_id, (None, None))` yields `None` for any row missing 
from the projection. A rename or a null gives a silently blank Archived column 
with no compile error, and the unit fixtures supply the field 
(`ArchivedList.test.tsx:73`), so they would keep passing against a renamed API.
   
   The sibling convention already handles this: `src/views/CRUD/types.ts` 
declares `changed_on_delta_humanized` in three places.
   
   **Fix:** declare `deleted_at_delta_humanized?: string | null` on 
`ArchivedItem`. Worth considering narrowing the index signature to just the 
three per-type name fields, since that signature is what let this through.
   
   ## Suggestion — `config.deletedRecencyOperator as FilterOperator`
   
   `superset-frontend/src/pages/ArchivedList/index.tsx:414`. Both operators are 
typed as bare `string` in the config (`types.ts:44,46`), so this cast is 
load-bearing rather than cosmetic. Typing those fields as `FilterOperator` (or 
a union including the soft-delete operators) removes the cast and turns an 
operator typo into a compile error instead of a rison request the API rejects.
   
   ## What looks good
   
   - **The `inFlight` guard is the right shape.** `inFlightRef` as the 
authority with `inFlight` state mirroring it for rendering 
(`ArchivedList/index.tsx:183-198`) handles the "two fast clicks both pass a 
state check" race, and `await refreshData()` before `finally` keeps the guard 
alive past the stale row. Both paths clear in `finally` — no way to enter and 
not exit.
   - **`useAppSelector` is used** (`:21`, `:450`) — the typed hook from 
`views/store.ts` that only ~23 files have adopted. The selector reads `roles` 
without constructing a new object, so it stays referentially stable.
   - **Type selection is derived, not stored** (`:489-493`), with the reasoning 
for why a `useState` initializer would strand the Select on a value its options 
no longer contain.
   - **The recoverability promise matches the effect on the bulk path** — 
`pendingBulkSemanticCount` and `handleBulkDatasetDelete` apply the identical 
predicate to the identical array, and React 18 batches the two `setState`s in 
`onSelect` (`:1589-1593`), so the count can't be read stale.
   - **The flag-off path is genuinely unchanged** in `CRUD/utils.tsx` (each 
ternary preserves the original msgid verbatim), and the route is gated in 
`routes.tsx` behind a `lazy()` import, so flag-off deployments don't even fetch 
the chunk.
   
   ## Checked and clean
   
   Recording these so the next pass doesn't re-spend the effort.
   
   - **Retracted mid-review:** I suspected the single-row dataset delete would 
promise "Archive" for a semantic view, since `recoverable={softDelete}` at 
`:1379` has no semantic-view guard where the bulk path at `:1549` does. It 
doesn't — the Cell branches on `kind === 'semantic_view'` at `:844` and routes 
semantic views to `handleSemanticViewDelete` → `/api/v1/semantic_view/{id}`, 
whose modal at `:1503` passes no `recoverable` (full danger treatment) and 
toasts "Deleted:", not "Archived:". The mirror is covered.
   - **The type-to-confirm gate is armed by default.** 
`DeleteModal/index.tsx:42-49` defaults `requireConfirmationText = true` / 
`recoverable = false`, so the row purge (`ArchivedList:119-137`, passing 
neither) gets the gate; `hide()` and `confirm()` both reset `disableChange` 
(`:63-73`).
   - No unstable `useSelector` results, no reducer mutation (the branch adds no 
reducers), no dispatch-during-render.
   
   **Not verified:** no browser walkthrough, no Playwright or Jest run, no 
profiling, no flag-on/flag-off visual diff. The three Playwright specs and five 
test files were read from the diff, not executed. Backend behaviour was 
inferred from the Marshmallow schemas and `views/filters.py`, not observed 
against a running instance. The Python portion of the diff 
(`commands/purge.py`, `purge_cascade.py`, the migration) was out of scope for 
this pass.
   


-- 
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]

Reply via email to