Skip to content

feat(ui): close the TableV2 parity gaps, add a shared parity suite - #31954

Open
harsh-vador wants to merge 5 commits into
feat/core-table-compact-radiofrom
feat/tablev2-parity-and-gaps
Open

feat(ui): close the TableV2 parity gaps, add a shared parity suite#31954
harsh-vador wants to merge 5 commits into
feat/core-table-compact-radiofrom
feat/tablev2-parity-and-gaps

Conversation

@harsh-vador

@harsh-vador harsh-vador commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Stacked on #31953 — review that first. Base will retarget to main once it merges.

TableV2 is documented as a drop-in replacement for the legacy Table, but silently dropped a number of props. This adds a suite that drives both wrappers through the same specs, and fixes everything it found red.

The suite is the point

Every spec runs against legacy Table first. A spec that cannot go green there is a wrong spec, not a legacy bug — three specs were corrected that way during the run, and one "bug" turned out not to exist at all (the customize dropdown does re-show a hidden column; a React Aria popover marks the page aria-hidden and the flag outlived the menu in jsdom).

DOM differences that are genuine design choices — which element the pager is, how a control is activated — live in a per-wrapper adapter. The assertions stay shared.

Fixed

Prop Was
onRow onClick never fired. React Aria strips a row's click handler unless the row is interactive; an empty onAction marks it interactive so the handler gets a real MouseEvent, with no second activation path
column filter dropdown could not open at allDialogTrigger reaches its child through a PressResponder, and the child was a core Button rather than a React Aria pressable
className accepted by the props type, dropped at render
controlled sortOrder ignored
rowSelection.getCheckboxProps ignored — rows meant to be unselectable were selectable. Mapped to disabledKeys + disabledBehavior="selection"
sortDirections, indentSize, footer, expandedRowRender unimplemented
page-size changer never appeared — showSizeChanger / pageSizeOptions / onShowSizeChange now reach the internal pager, and changing size resets to page one

Deliberately not implemented

summary and components are removed from TableV2Props, so passing them fails to compile rather than rendering a table that quietly lost a feature. React Aria's collection builder discards any table child that is not a Header or Body, so a tfoot never reaches the DOM — and a summary drawn outside the table would not line up with the columns, which is worse than not drawing it. TeamHierarchy uses components and will fail to compile when it migrates; that is intended.

customPaginationProps now requires pagination={false} in the type, since it means the parent already fetched exactly this page. A runtime short-circuit backstops untyped call sites.

Verification

  • 144 tests in src/components/common/Table (includes the pre-existing Table.test.tsx and DraggableMenu suites)
  • adopter suites (Glossary, Metrics, LineageTable, ListView) — 110 tests
  • tsc --noEmit: 573 errors, identical to main — zero delta (note: main is not clean; zero is unreachable)
  • eslint: 0 errors

🤖 Generated with Claude Code

Greptile Summary

TableV2 gains broader compatibility with the legacy Table API and a shared parity suite that runs equivalent behavior checks against both wrappers.

  • Adds controlled sorting, page-size controls, footer and expanded-row rendering, indentation, row interactions, filter activation, class forwarding, and per-row selection disabling.
  • Introduces shared parity tests with implementation-specific adapters for Ant Design and React Aria DOM behavior.

Confidence Score: 4/5

The PR is not yet safe to merge because expanded child rows marked disabled by getCheckboxProps can still be selected.

The disabled-key set is built from top-level filteredDataSource records, while expanded descendants are introduced only in flatRows; consequently, disabled child keys never reach the table’s disabledKeys selection guard.

Files Needing Attention: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx

Important Files Changed

Filename Overview
openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx Expands TableV2’s legacy compatibility across sorting, pagination, selection, expansion, filtering, and row interaction behavior.
openmetadata-ui/src/main/resources/ui/src/components/common/Table/tests/tableParity.shared.tsx Defines the shared behavioral contract exercised against both table wrappers.
openmetadata-ui/src/main/resources/ui/src/components/common/Table/tests/Table.parity.test.tsx Supplies the Ant Design adapter for the shared parity suite.
openmetadata-ui/src/main/resources/ui/src/components/common/Table/tests/TableV2.parity.test.tsx Supplies the React Aria adapter and parent-owned pagination coverage for TableV2.

Reviews (2): Last reviewed commit: "fix(ui): keep TableV2 working when two c..." | Re-trigger Greptile

harsh-vador and others added 4 commits August 24, 2026 15:58
TableV2 was documented as a drop-in replacement for the legacy Table but
silently dropped a number of props. This adds a parity suite that drives both
wrappers through the same specs, and fixes everything it found red.

The suite is the interesting part: every spec runs against legacy Table first,
so a spec that cannot go green there is a wrong spec rather than a legacy bug.
Three specs were corrected that way during the run. DOM differences that are
genuine design choices (which element the pager is, how a control is activated)
live in a per-wrapper adapter; the assertions stay shared.

Fixed:
- onRow onClick never fired. React Aria strips a row's click handler unless the
  row is interactive; an empty onAction marks it interactive so the call site's
  handler receives a real MouseEvent, with no second activation path.
- The column filter dropdown could not open at all — DialogTrigger reaches its
  child through a PressResponder, and the child was a core Button rather than a
  React Aria pressable.
- className was accepted by the props type and dropped at render.
- Controlled sortOrder was ignored; a column declaring it now drives the sort.
- rowSelection.getCheckboxProps was ignored, so rows meant to be unselectable
  were selectable. Mapped to disabledKeys + disabledBehavior="selection".
- sortDirections, indentSize, footer and expandedRowRender were unimplemented.
- The page-size changer never appeared: showSizeChanger, pageSizeOptions and
  onShowSizeChange now reach the internal pager, and changing size resets to
  page one.

Two props stay unsupported and are now omitted from TableV2Props, so passing
them fails to compile instead of rendering a table that quietly lost a feature:
summary (React Aria discards any table child that is not a Header or Body, and
a summary drawn outside the table would not line up with the columns) and
components (no equivalent — use dragAndDropHooks or a column render).

customPaginationProps now requires pagination={false} in the type, since it
means the parent already fetched exactly this page; slicing again would drop
rows. A runtime short-circuit backstops untyped call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 6405dbd4b9a7b357c4219f2d18fb2d43b22650ce in Playwright run 32730203330, attempt 1.

✅ 552 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 51m 25s

⏱️ Max setup 4m 16s · max shard execution 16m 47s · max shard-job elapsed before upload 20m 27s · reporting 4s

🌐 215.54 requests/attempt · 2.83 app boots/UI scenario · 20.06% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 20.06% (convergence target: at most 15%).
  • Browser traffic was 215.54 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.83 per UI scenario (1626 boots / 575 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 133 0 0 0 0 0
✅ Shard chromium-02 140 0 0 0 0 0
🟡 Shard chromium-03 125 0 1 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 16 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard ingestion-01 1 0 0 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 11 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 1 flaky test(s) (passed on retry)
  • Pages/Entity.spec.tsUpdate description (shard chromium-03, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@harsh-vador harsh-vador self-assigned this Aug 24, 2026
Comment on lines +213 to +227
const buildExpandedDetailRow = <T extends object>(
expandable: TableComponentProps<T>['expandable'],
flatRow: FlatRow<T>,
isExpanded: boolean,
columnCount: number
) => {
const renderDetail = expandable?.expandedRowRender;

if (!renderDetail || !isExpanded || !flatRow.hasChildren) {
return null;
}

return (
<UntitledTable.Row
id={`${flatRow.rowKey}-expanded`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: expandedRowRender detail row colSpan ignores selection/expander cols

buildExpandedDetailRow sets the detail cell's colSpan to propsColumns.length only (TableV2.tsx:1086-1091, 229-233). When rowSelection is active (react-aria injects a selection column) or an expander column is present, the detail panel spans one column too few and will not stretch across the full table width — and a react-aria Row that omits the required selection cell may not align correctly. Compute the colSpan from the actual rendered column count (including selection/expander columns) rather than just propsColumns.length.

Was this helpful? React with 👍 / 👎

Comment on lines +154 to +168
const resolveClientPagination = <T,>(
pagination: TableComponentProps<T>['pagination'],
pageSizeOverride: number | null,
hasParentPagination: boolean
) => {
if (pagination === false || hasParentPagination) {
return null;
}
const cfg = (pagination ?? {}) as TablePaginationConfig;

return {
pageSize: pageSizeOverride ?? (cfg.pageSize as number) ?? DEFAULT_PAGE_SIZE,
hideOnSinglePage: cfg.hideOnSinglePage ?? false,
showSizeChanger: cfg.showSizeChanger ?? false,
pageSizeOptions: (cfg.pageSizeOptions ?? []).map(Number),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: User pageSize override shadows a parent-controlled pagination.pageSize

Once the user picks a size, pageSizeOverride is set and resolveClientPagination returns pageSizeOverride ?? cfg.pageSize (TableV2.tsx:165,322,377). If the parent later re-renders with a different pagination.pageSize, that controlled value is permanently ignored because the override always wins and is never reset when the incoming pagination prop changes. Reset pageSizeOverride to null when rest.pagination (or its pageSize) changes, or prefer the prop value when it differs from the last override.

Was this helpful? React with 👍 / 👎

Comment on lines +190 to +204
/**
* React Aria always opens a fresh sort on 'ascending'. AntD lets a column say
* which way the first click should go via `sortDirections`, so honour the head
* of that list when the sort moves to a different column.
*/
const resolveSortDirection = <T,>(
column: ColumnType<T> | undefined,
isFirstClickOnColumn: boolean,
fallback: 'ascending' | 'descending' | null
) => {
const preferred = column?.sortDirections?.[0];

if (!isFirstClickOnColumn || !preferred) {
return fallback;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: sortDirections only honored when switching columns, not on re-click

resolveSortDirection applies the column's preferred first direction only when isFirstClickOnColumn is true, i.e. when moving to a different column (TableV2.tsx:195-207,653-657). AntD's sortDirections also governs the cycle on the same column (e.g. ['descend','ascend'] cycles descend→ascend→none); on repeat clicks TableV2 falls back to react-aria's ascending/descending toggle, so same-column cycling can diverge from AntD. This is a partial-parity gap worth documenting or handling if full parity is expected.

Was this helpful? React with 👍 / 👎

Comment on lines +598 to +601
filteredDataSource
.map((record, index) => ({ key: getRowKey(record, index), record }))
.filter(({ record }) => getCheckboxProps(record).disabled)
.map(({ key }) => key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Disabled child rows remain selectable

When an expanded tree uses getCheckboxProps to disable nested records, disabledRowKeys evaluates only the top-level filteredDataSource, so child keys are omitted from disabledKeys. Those child rows remain selectable, allowing nested schema columns intended to be disabled to be added to a data contract.

@harsh-vador harsh-vador added the safe to test Add this label to run secure Github workflows on PRs label Aug 24, 2026
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 67%
67.2% (80845/120299) 51.69% (49508/95773) 52.69% (14836/28152)

AntD tolerates duplicate column keys and renders both. React Aria uses the id
as a collection key, so the duplicate collapsed the column while the row still
rendered a cell for it — 'Cell count must match column count. Found 5 cells and
4 columns'. VersionTable has two columns keyed 'tags', and it is unlikely to be
the only one.

Column ids are now derived once and de-duplicated by suffixing repeats, so the
header and the body agree. Parity spec added: legacy already rendered both
columns, and TableV2 now matches.
@harsh-vador
harsh-vador force-pushed the feat/tablev2-parity-and-gaps branch from 84343ea to 6405dbd Compare August 24, 2026 13:00
@gitar-bot

gitar-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 6 findings

Closes TableV2 parity gaps with a shared behavioral suite covering sorting, expansion, row interactions, filtering, pagination, and selection. However, changes are requested due to two important issues: disabledRowKeys omits nested child rows leaving them selectable, and select-all reports getCheckboxProps-disabled rows as selected.

⚠️ Bug: Select-all reports getCheckboxProps-disabled rows as selected

📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:625-639

When the header select-all control is used, React Aria fires onSelectionChange with the sentinel 'all'. handleSelectionChange then maps every record in filteredDataSource to selected keys/rows without excluding disabledRowKeys, so rows the caller marked disabled via getCheckboxProps are reported as selected in onChange. This breaks parity with AntD (which excludes disabled rows from select-all) and the very contract getCheckboxProps is meant to enforce. The shared select-all spec doesn't catch it because that case uses no disabled rows. Filter out disabled keys in the 'all' branch.

Exclude disabled rows when select-all is used.
const selectedKeys =
  keys === 'all'
    ? dataSource
        .map((r, i) => getRowKey(r, i))
        .filter((k) => !disabledRowKeys?.has(k))
    : [...keys].map(String);
⚠️ Bug: disabledRowKeys omits nested/child rows, leaving them selectable

📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:611-623 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:630-637

disabledRowKeys is derived only from the top-level filteredDataSource, but expanded tree rows are produced by flattenTreeRows which recurses into record.children. Child records that getCheckboxProps marks disabled never get their key into disabledRowKeys, so nested rows meant to be unselectable remain selectable — the opposite of the intended behavior. (The same top-level-only assumption in handleSelectionChange's selectedRows filter also drops selected child records.) Derive disabled keys by walking children recursively, mirroring flattenTreeRows.

Recurse into children so nested disabled rows are included.
const collectDisabled = (rows: T[], acc: Set<string>, base = 0): number => {
  let i = base;
  for (const record of rows) {
    const idx = i++;
    if (getCheckboxProps(record).disabled) {
      acc.add(getRowKey(record, idx));
    }
    const children = (record as Record<string, unknown>).children as T[] | undefined;
    if (children?.length) {
      i += collectDisabled(children, acc, i);
    }
  }
  return i - base;
};
const keys = new Set<string>();
collectDisabled(filteredDataSource, keys);
return keys;
💡 Edge Case: expandedRowRender detail row colSpan ignores selection/expander cols

📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:213-227 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:1086-1091

buildExpandedDetailRow sets the detail cell's colSpan to propsColumns.length only (TableV2.tsx:1086-1091, 229-233). When rowSelection is active (react-aria injects a selection column) or an expander column is present, the detail panel spans one column too few and will not stretch across the full table width — and a react-aria Row that omits the required selection cell may not align correctly. Compute the colSpan from the actual rendered column count (including selection/expander columns) rather than just propsColumns.length.

💡 Bug: User pageSize override shadows a parent-controlled pagination.pageSize

📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:154-168 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:322 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:375-382

Once the user picks a size, pageSizeOverride is set and resolveClientPagination returns pageSizeOverride ?? cfg.pageSize (TableV2.tsx:165,322,377). If the parent later re-renders with a different pagination.pageSize, that controlled value is permanently ignored because the override always wins and is never reset when the incoming pagination prop changes. Reset pageSizeOverride to null when rest.pagination (or its pageSize) changes, or prefer the prop value when it differs from the last override.

💡 Quality: sortDirections only honored when switching columns, not on re-click

📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:190-204 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:644-658

resolveSortDirection applies the column's preferred first direction only when isFirstClickOnColumn is true, i.e. when moving to a different column (TableV2.tsx:195-207,653-657). AntD's sortDirections also governs the cycle on the same column (e.g. ['descend','ascend'] cycles descend→ascend→none); on repeat clicks TableV2 falls back to react-aria's ascending/descending toggle, so same-column cycling can diverge from AntD. This is a partial-parity gap worth documenting or handling if full parity is expected.

💡 Bug: Sort lookup uses raw column key, not deduplicated columnIds

📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:664-678 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:997 📄 openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:1008

This commit makes each React Aria column id unique via getColumnIds (e.g. the second column sharing key name becomes name-1), but handleSortChange and the sort/filter matching still resolve columns with the raw String(c.key ?? c.dataIndex ?? idx). When a duplicate-keyed column is sortable, React Aria fires onSortChange with descriptor.column = 'name-1', which matches no column, so clickedColumn/matchedCol are undefined and the click sorts nothing (and onChange gets column: undefined). Derive the sort key the same way — build a columnId -> column map from columnIds (or compare against columnIds[idx]) in both handleSortChange and the filteredDataSource sort predicate so header ids and sort lookups agree.

🤖 Prompt for agents
Code Review: Closes TableV2 parity gaps with a shared behavioral suite covering sorting, expansion, row interactions, filtering, pagination, and selection. However, changes are requested due to two important issues: disabledRowKeys omits nested child rows leaving them selectable, and select-all reports getCheckboxProps-disabled rows as selected.

1. 💡 Edge Case: expandedRowRender detail row colSpan ignores selection/expander cols
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:213-227, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:1086-1091

   buildExpandedDetailRow sets the detail cell's colSpan to `propsColumns.length` only (TableV2.tsx:1086-1091, 229-233). When `rowSelection` is active (react-aria injects a selection column) or an expander column is present, the detail panel spans one column too few and will not stretch across the full table width — and a react-aria Row that omits the required selection cell may not align correctly. Compute the colSpan from the actual rendered column count (including selection/expander columns) rather than just `propsColumns.length`.

2. 💡 Bug: User pageSize override shadows a parent-controlled pagination.pageSize
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:154-168, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:322, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:375-382

   Once the user picks a size, `pageSizeOverride` is set and `resolveClientPagination` returns `pageSizeOverride ?? cfg.pageSize` (TableV2.tsx:165,322,377). If the parent later re-renders with a different `pagination.pageSize`, that controlled value is permanently ignored because the override always wins and is never reset when the incoming `pagination` prop changes. Reset `pageSizeOverride` to null when `rest.pagination` (or its pageSize) changes, or prefer the prop value when it differs from the last override.

3. 💡 Quality: sortDirections only honored when switching columns, not on re-click
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:190-204, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:644-658

   resolveSortDirection applies the column's preferred first direction only when `isFirstClickOnColumn` is true, i.e. when moving to a different column (TableV2.tsx:195-207,653-657). AntD's `sortDirections` also governs the cycle on the same column (e.g. `['descend','ascend']` cycles descend→ascend→none); on repeat clicks TableV2 falls back to react-aria's ascending/descending toggle, so same-column cycling can diverge from AntD. This is a partial-parity gap worth documenting or handling if full parity is expected.

4. 💡 Bug: Sort lookup uses raw column key, not deduplicated columnIds
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:664-678, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:997, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:1008

   This commit makes each React Aria column `id` unique via `getColumnIds` (e.g. the second column sharing key `name` becomes `name-1`), but `handleSortChange` and the sort/filter matching still resolve columns with the raw `String(c.key ?? c.dataIndex ?? idx)`. When a duplicate-keyed column is sortable, React Aria fires `onSortChange` with `descriptor.column = 'name-1'`, which matches no column, so `clickedColumn`/`matchedCol` are undefined and the click sorts nothing (and `onChange` gets `column: undefined`). Derive the sort key the same way — build a `columnId -> column` map from `columnIds` (or compare against `columnIds[idx]`) in both `handleSortChange` and the `filteredDataSource` sort predicate so header ids and sort lookups agree.

5. ⚠️ Bug: Select-all reports getCheckboxProps-disabled rows as selected
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:625-639

   When the header select-all control is used, React Aria fires onSelectionChange with the sentinel 'all'. handleSelectionChange then maps every record in filteredDataSource to selected keys/rows without excluding disabledRowKeys, so rows the caller marked `disabled` via getCheckboxProps are reported as selected in onChange. This breaks parity with AntD (which excludes disabled rows from select-all) and the very contract getCheckboxProps is meant to enforce. The shared select-all spec doesn't catch it because that case uses no disabled rows. Filter out disabled keys in the 'all' branch.

   Fix (Exclude disabled rows when select-all is used.):
   const selectedKeys =
     keys === 'all'
       ? dataSource
           .map((r, i) => getRowKey(r, i))
           .filter((k) => !disabledRowKeys?.has(k))
       : [...keys].map(String);

6. ⚠️ Bug: disabledRowKeys omits nested/child rows, leaving them selectable
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:611-623, openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx:630-637

   disabledRowKeys is derived only from the top-level filteredDataSource, but expanded tree rows are produced by flattenTreeRows which recurses into record.children. Child records that getCheckboxProps marks disabled never get their key into disabledRowKeys, so nested rows meant to be unselectable remain selectable — the opposite of the intended behavior. (The same top-level-only assumption in handleSelectionChange's selectedRows filter also drops selected child records.) Derive disabled keys by walking children recursively, mirroring flattenTreeRows.

   Fix (Recurse into children so nested disabled rows are included.):
   const collectDisabled = (rows: T[], acc: Set<string>, base = 0): number => {
     let i = base;
     for (const record of rows) {
       const idx = i++;
       if (getCheckboxProps(record).disabled) {
         acc.add(getRowKey(record, idx));
       }
       const children = (record as Record<string, unknown>).children as T[] | undefined;
       if (children?.length) {
         i += collectDisabled(children, acc, i);
       }
     }
     return i - base;
   };
   const keys = new Set<string>();
   collectDisabled(filteredDataSource, keys);
   return keys;

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@github-actions

Copy link
Copy Markdown
Contributor

❌ UI Checkstyle Failed

❌ ESLint + Prettier + Organise Imports (src)

One or more source files have linting or formatting issues.

Affected files
  • openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx
    • openmetadata-ui/src/main/resources/ui/src/components/common/Table/__tests__/Table.parity.test.tsx
    • openmetadata-ui/src/main/resources/ui/src/components/common/Table/__tests__/TableV2.parity.test.tsx
    • openmetadata-ui/src/main/resources/ui/src/components/common/Table/__tests__/tableParity.shared.tsx

❌ Antd + Less Deprecation Guard

A new antd import or new .less file was added. Use UntitledUI + Tailwind for new work.

Affected files

openmetadata-ui/src/main/resources/ui/src/components/common/Table/tests/tableParity.shared.tsx: import { ColumnsType } from 'antd/lib/table'

🔍 ESLint findings in this PR's files — 0 error(s), 25 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 25 warning(s) across 2 changed file(s).

Count Rule
7 sonarjs/no-nested-functions
4 react-hooks/exhaustive-deps
4 sonarjs/no-nested-conditional
3 sonarjs/cyclomatic-complexity
3 sonarjs/no-duplicate-string
2 i18next/no-literal-string
1 @typescript-eslint/no-non-null-assertion
1 sonarjs/expression-complexity
All findings
Location Rule Message
🟡 src/components/common/Table/TableV2.tsx:336:3 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 18 which is greater than 10 authorized.","cost":8,"secondaryLocations":[{"line":336,"column":2,"endLine":336,"endColumn
🟡 src/components/common/Table/TableV2.tsx:456:20 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:461:37 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:462:15 @typescript-eslint/no-non-null-assertion Forbidden non-null assertion.
🟡 src/components/common/Table/TableV2.tsx:502:6 react-hooks/exhaustive-deps React Hook useMemo has a missing dependency: 'scroll'. Either include it or remove the dependency array.
🟡 src/components/common/Table/TableV2.tsx:529:5 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'setPreference'. Either include it or remove the dependency array.
🟡 src/components/common/Table/TableV2.tsx:551:6 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'setPreference'. Either include it or remove the dependency array.
🟡 src/components/common/Table/TableV2.tsx:705:17 sonarjs/no-nested-conditional Extract this nested ternary operation into an independent statement.
🟡 src/components/common/Table/TableV2.tsx:715:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'filteredDataSource.length' and 'rest'. Either include them or remove the dependency array.
🟡 src/components/common/Table/TableV2.tsx:992:49 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 13 which is greater than 10 authorized.","cost":3,"secondaryLocations":[{"line":992,"column":48,"endLine":992,"endColum
🟡 src/components/common/Table/TableV2.tsx:1054:65 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:1061:65 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:1131:55 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 17 which is greater than 10 authorized.","cost":7,"secondaryLocations":[{"line":1131,"column":54,"endLine":1131,"endCol
🟡 src/components/common/Table/TableV2.tsx:1191:37 sonarjs/no-nested-conditional Extract this nested ternary operation into an independent statement.
🟡 src/components/common/Table/TableV2.tsx:1197:60 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:1207:54 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:1218:39 sonarjs/no-nested-conditional Extract this nested ternary operation into an independent statement.
🟡 src/components/common/Table/TableV2.tsx:1224:60 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/common/Table/TableV2.tsx:1272:8 sonarjs/expression-complexity Reduce the number of conditional operators (5) used in the expression (maximum allowed 3).
🟡 src/components/common/Table/TableV2.tsx:1276:11 sonarjs/no-nested-conditional Extract this nested ternary operation into an independent statement.
🟡 src/components/common/Table/__tests__/tableParity.shared.tsx:76:11 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 8 times.
🟡 src/components/common/Table/__tests__/tableParity.shared.tsx:78:21 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/components/common/Table/__tests__/tableParity.shared.tsx:623:72 i18next/no-literal-string disallow literal string: <button data-testid="filter-apply" onClick={() => confirm()}> apply
🟡 src/components/common/Table/__tests__/tableParity.shared.tsx:627:59 i18next/no-literal-string disallow literal string: f
🟡 src/components/common/Table/__tests__/tableParity.shared.tsx:658:33 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

Comment on lines 664 to 678
const handleSortChange = useCallback(
(descriptor: AriaSortDescriptor) => {
const newKey = descriptor.column ? String(descriptor.column) : null;
const newDirection = descriptor.direction ?? null;
const clickedColumn = propsColumns.find((c, idx) => {
const key = String(c.key ?? (c as ColumnType<T>).dataIndex ?? idx);

return key === newKey;
}) as ColumnType<T> | undefined;

const newDirection = resolveSortDirection(
clickedColumn,
sortState.columnKey !== newKey,
descriptor.direction ?? null
);
setSortState({ columnKey: newKey, direction: newDirection });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: Sort lookup uses raw column key, not deduplicated columnIds

This commit makes each React Aria column id unique via getColumnIds (e.g. the second column sharing key name becomes name-1), but handleSortChange and the sort/filter matching still resolve columns with the raw String(c.key ?? c.dataIndex ?? idx). When a duplicate-keyed column is sortable, React Aria fires onSortChange with descriptor.column = 'name-1', which matches no column, so clickedColumn/matchedCol are undefined and the click sorts nothing (and onChange gets column: undefined). Derive the sort key the same way — build a columnId -> column map from columnIds (or compare against columnIds[idx]) in both handleSortChange and the filteredDataSource sort predicate so header ids and sort lookups agree.

Was this helpful? React with 👍 / 👎

);
}, [rest.rowSelection, filteredDataSource, getRowKey]);

const handleSelectionChange = useCallback(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Select-all reports getCheckboxProps-disabled rows as selected

When the header select-all control is used, React Aria fires onSelectionChange with the sentinel 'all'. handleSelectionChange then maps every record in filteredDataSource to selected keys/rows without excluding disabledRowKeys, so rows the caller marked disabled via getCheckboxProps are reported as selected in onChange. This breaks parity with AntD (which excludes disabled rows from select-all) and the very contract getCheckboxProps is meant to enforce. The shared select-all spec doesn't catch it because that case uses no disabled rows. Filter out disabled keys in the 'all' branch.

Exclude disabled rows when select-all is used.:

const selectedKeys =
  keys === 'all'
    ? dataSource
        .map((r, i) => getRowKey(r, i))
        .filter((k) => !disabledRowKeys?.has(k))
    : [...keys].map(String);
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +611 to +623
const disabledRowKeys = useMemo((): Set<string> | undefined => {
const getCheckboxProps = rest.rowSelection?.getCheckboxProps;
if (!getCheckboxProps) {
return undefined;
}

return new Set(
filteredDataSource
.map((record, index) => ({ key: getRowKey(record, index), record }))
.filter(({ record }) => getCheckboxProps(record).disabled)
.map(({ key }) => key)
);
}, [rest.rowSelection, filteredDataSource, getRowKey]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: disabledRowKeys omits nested/child rows, leaving them selectable

disabledRowKeys is derived only from the top-level filteredDataSource, but expanded tree rows are produced by flattenTreeRows which recurses into record.children. Child records that getCheckboxProps marks disabled never get their key into disabledRowKeys, so nested rows meant to be unselectable remain selectable — the opposite of the intended behavior. (The same top-level-only assumption in handleSelectionChange's selectedRows filter also drops selected child records.) Derive disabled keys by walking children recursively, mirroring flattenTreeRows.

Recurse into children so nested disabled rows are included.:

const collectDisabled = (rows: T[], acc: Set<string>, base = 0): number => {
  let i = base;
  for (const record of rows) {
    const idx = i++;
    if (getCheckboxProps(record).disabled) {
      acc.add(getRowKey(record, idx));
    }
    const children = (record as Record<string, unknown>).children as T[] | undefined;
    if (children?.length) {
      i += collectDisabled(children, acc, i);
    }
  }
  return i - base;
};
const keys = new Set<string>();
collectDisabled(filteredDataSource, keys);
return keys;
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant