Skip to content

Delete marker checkbox selects extra versions due to stale selection state #1063

@eve-scality

Description

@eve-scality

Identified by automated analysis of ARTESCA-8467
Confidence: high

What needs to change

File: src/lib/components/tablev2/MultiSelectableContent.tsx
Fix the MultiSelectableContent.tsx in core-ui to properly separate single-row-click selection (for viewing details) from checkbox-based multi-selection (for actions like delete):

  1. Clear single-selection state before checkbox multi-selection: In the row body onClick handler, instead of using toggleRowSelected(true) which writes to selectedRowIds, track the "viewed" row separately without polluting the multi-selection state. One approach:

    onClick: onSingleRowSelected
      ? () => {
          toggleAllRowsSelected(false); // clear multi-selection
          onSingleRowSelected(row);
          // Don't call row.toggleRowSelected(true) — this leaks into selectedRowIds
        }
      : ...
  2. Fix the double-toggle on checkbox click: Prevent the checkbox's onChange from firing toggleRowSelected independently. Either:

    • Add event.stopPropagation() on the checkbox to prevent the onChange from bubbling, OR
    • Remove the toggleRowSelected call from handleMultipleSelectedRows (line 114) since the checkbox onChange already handles it, OR
    • Override the checkbox's onChange to prevent the automatic toggle, letting handleMultipleSelectedRows be the sole controller
  3. Alternative approach in data-browser: If the core-ui fix is too risky, the data-browser could work around it by providing a getRowId prop to the Table that uniquely identifies each row (using Key + VersionId + type), and by clearing the single-selection state when switching to multi-select mode.

Root Cause

The root cause is in core-ui's MultiSelectableContent.tsx component, where single-row-click selection state leaks into checkbox-based multi-selection.

When onSingleRowSelected is provided (as it is in data-browser's ObjectList), two selection modes coexist:

  1. Row body click (lines 135-140): Calls toggleAllRowsSelected(false) to clear all, then row.toggleRowSelected(true) to highlight the clicked row. This puts the row into react-table's internal selectedRowIds map.

  2. Checkbox click (lines 169-176): Calls handleMultipleSelectedRows which reads selectedRowIds to determine which rows are already selected, then adds/removes the checkbox's row.

The bug occurs because the row-body click in step 1 writes to selectedRowIds (via toggleRowSelected(true)), and the checkbox click in step 2 reads from the same selectedRowIds. When a user clicks a version row to view its details, then clicks the delete marker's checkbox to select it for deletion, handleMultipleSelectedRows sees the version row in selectedRowIds and includes it in the multi-selection callback — even though the user only intended to select the delete marker.

Specifically in handleMultipleSelectedRows (lines 89-115):

const keys = Object.keys(selectedRowIds); // includes the previously-clicked version row!
onMultiSelectionChanged([
    ...rows.filter((row) => keys.includes(row.id)), // picks up the version row
    rows[currentRowIndex], // adds the delete marker
]);

This results in both the delete marker AND the version appearing in the delete confirmation dialog, even though only the delete marker's checkbox was checked.

Additionally, there is a secondary double-toggle issue: clicking the checkbox fires both the checkbox's onChange (from getToggleRowSelectedProps) AND the container div's onClick (which calls handleMultipleSelectedRowstoggleRowSelected), causing toggleRowSelected to be called twice per click.

Evidence

src/lib/components/tablev2/MultiSelectableContent.tsx
├── L135: onClick: onSingleRowSelected ...  — When the user clicks a row body (not the checkbox), the row is added to react-table's selectedRowIds via toggleRowSelected(true). This single-selection state persists and contaminates the next checkbox-based multi-selection.
├── L89: const handleMultipleSelectedRows = ( ...  — handleMultipleSelectedRows reads selectedRowIds to determine existing selections. Since the previously single-clicked row is in selectedRowIds, it gets included in the multi-selection result via `rows.filter((row) => keys.includes(row.id))`, causing an unexpected extra row to appear as selected.
└── L163: if (cell.column.id === 'selection') { ...  — The checkbox cell's div onClick calls handleMultipleSelectedRows. But the checkbox inside (rendered by useCheckbox) already has getToggleRowSelectedProps() onChange handler. Clicking the checkbox fires BOTH: the checkbox onChange (toggles row in react-table) and the div onClick (calls handleMultipleSelectedRows which also calls toggleRowSelected), causing a double-toggle of the selection state.
src/lib/components/tablev2/useCheckbox.tsx
└── L45: Cell: ({ row }) => { ...  — The checkbox uses getToggleRowSelectedProps() which provides an onChange handler that calls toggleRowSelected. This onChange fires before the parent div's onClick handler, creating the double-toggle scenario.

Upstream Impact

Medium severity. When users browse versioned objects in the Data Browser and interact with delete markers, selecting a single delete marker for deletion incorrectly shows additional object versions in the delete confirmation dialog. This can lead to accidental deletion of object versions that the user did not intend to delete, especially problematic for objects under compliance retention lock. The issue affects all Data Browser users working with versioned S3 buckets containing delete markers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cerebro-analyzedIssue created by Cerebro automated analysis

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions