Skip to content

Feat 4143 data table filter row#6058

Open
yuejt14 wants to merge 10 commits intomage-ai:masterfrom
yuejt14:feat-4143-data-table-filter-row
Open

Feat 4143 data table filter row#6058
yuejt14 wants to merge 10 commits intomage-ai:masterfrom
yuejt14:feat-4143-data-table-filter-row

Conversation

@yuejt14
Copy link
Copy Markdown

@yuejt14 yuejt14 commented Mar 19, 2026

Description

Add inline column filtering to the DataTable component, as requested in #4143. Users can filter rows directly from text inputs rendered below the column headers.

Key changes:

  • New FilterRow component that renders a text input beneath each column header
  • New filterUtils.ts module with filter expression parsing and matching logic supporting:
    • Text search (substring match, case-insensitive)
    • Comparison operators: >, <, >=, <=, =, !=
    • Numeric and date-aware comparisons with string fallback
    • Special keywords: is blank, is not blank
  • DataTable (index.tsx) integrates filtering with debounced input, preserves original row indices for correct index display/invalid-value highlighting/row-removal previews, and
    shows a "No rows match" message when the filtered result is empty
  • TableOutput displays "Showing X of Y rows" when a filter is active via the onFilteredRowCount callback
  • Filtering is enabled in both TableOutput (code block output) and Sidekick data preview

How Has This Been Tested?

  • Typed text into filter inputs and verified substring matching works across columns
  • Tested comparison operators (>100, <=50, !=0) on numeric columns
  • Verified is blank / is not blank filters correctly identify null/empty cells
  • Confirmed row indices remain correct after filtering (original index shown, not filtered index)
  • Confirmed invalid value highlighting and row removal previews reference original row indices
  • Verified "No rows match the current filters" message appears for empty results
  • Confirmed horizontal scrollbar still works when filtered result is empty
  • Verified filters reset when underlying data changes
  • Tested debounce behavior — rapid typing doesn't cause excessive re-renders

Checklist

  • [ *] The PR is tagged with proper labels (bug, enhancement, feature, documentation)
  • [ *] I have performed a self-review of my own code
  • I have added unit tests that prove my fix is effective or that my feature works
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation

cc:
@wangxiaoyou1993

@wangxiaoyou1993
Copy link
Copy Markdown
Member

Could you add screenshots or a short video showing your testing results?

@yuejt14
Copy link
Copy Markdown
Author

yuejt14 commented Mar 19, 2026

https://github.com/user-attachments/assets/cb4ba639-7e55-4033-9be9-47ab2e431d27
here's a short video using the filter row in data table
@wangxiaoyou1993

…taTable

Filters now use declared intent instead of heuristic type detection:
 - >, <, >=, <= are always numeric (no string fallback)
 - "date" prefix required for date comparisons (no auto-detection)
 - = / != auto-detect numeric vs string exact match
 - Bare text remains case-insensitive substring match
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds inline per-column filtering to the frontend DataTable so users can filter table rows directly in the UI (used in Sidekick data preview and code block table outputs).

Changes:

  • Introduces a FilterRow UI rendered under column headers when enableFiltering is enabled.
  • Adds filterUtils.ts for parsing filter expressions and matching rows (substring, comparison ops, date prefix, blank/not blank).
  • Integrates filtering into DataTable with debounced inputs, original row index preservation, and filtered row count reporting to TableOutput.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mage_ai/frontend/components/Sidekick/index.tsx Enables filtering in the Sidekick data preview table.
mage_ai/frontend/components/DataTable/index.tsx Implements filtering state, debouncing, empty-filter UI, original index mapping, and the onFilteredRowCount callback.
mage_ai/frontend/components/DataTable/filterUtils.ts New filter parsing/matching logic used by DataTable.
mage_ai/frontend/components/DataTable/FilterRow.tsx New component rendering per-column filter inputs beneath headers.
mage_ai/frontend/components/CodeBlock/CodeOutput/TableOutput.tsx Enables filtering in table output and shows “Showing X of Y rows” when filtered.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +419 to +426
const [prevData, setPrevData] = useState(data);
if (data !== prevData) {
setPrevData(data);
setFilters({});
setDebouncedFilters({});
cancelDebounce();
}

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

State is being updated during render (if (data !== prevData) { setPrevData(...); setFilters(...); ... }). This can trigger React warnings and extra renders (and cancelDebounce() is also a side effect during render). Move this reset logic into a useEffect (or use a useRef to track previous data) keyed on data so updates happen after render.

Suggested change
const [prevData, setPrevData] = useState(data);
if (data !== prevData) {
setPrevData(data);
setFilters({});
setDebouncedFilters({});
cancelDebounce();
}
const prevDataRef = useRef(data);
useEffect(() => {
if (data !== prevDataRef.current) {
prevDataRef.current = data;
setFilters({});
setDebouncedFilters({});
cancelDebounce();
}
}, [data, cancelDebounce]);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is a supported React pattern documented here: https://react.dev/reference/react/useState#storing-information-from-previous-renders

React considers it a last resort — they recommend first considering computed values, key-based resets, or event handlers. But for this case:

  • Filters are user-entered state, not derivable from data
  • A key reset would blow away all component state (scroll position, etc.)
  • data arrives as a prop, not from a user event in this component

So the during-render pattern is the appropriate choice here. Critically, the docs say: "However, it's better than updating state in an effect."
Moving this to a useEffect would cause the component to render one frame with new data but stale filters (a visible flash of incorrect state), then fire the effect and re-render with cleared filters. The during-render approach avoids that by clearing filters before the browser paints.
cancelDebounce() is just clearing a timeout — a trivial synchronous operation, so it's fine here.

indexColumnValue = indexColumnValue[idx];
}
} else if (isFiltering) {
indexColumnValue = String(displayIndex);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

When filtering and shouldUseIndexProp is false, the index column uses String(displayIndex) which is always zero-based. This bypasses the disableZeroIndexRowNumber behavior implemented in buildIndexColumns (and can change displayed row numbers when filtering is enabled). Consider reusing the same index calculation as the index column accessor (e.g., add the +1 offset when disableZeroIndexRowNumber is true) rather than hardcoding String(displayIndex).

Suggested change
indexColumnValue = String(displayIndex);
const rowNumber = disableZeroIndexRowNumber ? displayIndex + 1 : displayIndex;
indexColumnValue = String(rowNumber);

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +61
{indexColumnWidths.map((w, idx) => (
<div
className="td td-index-column"
key={`filter-index-${idx}`}
style={{
left: 0,
minWidth: w,
position: 'sticky',
width: w,
}}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

All sticky index placeholders in the filter row are positioned with left: 0. If there are multiple index columns (indexColumnWidths.length > 1), these sticky cells will overlap each other. Compute left as the cumulative sum of previous index column widths (or reuse the same sticky positioning approach as the header/body cells) so each sticky column is offset correctly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this is a pre-existing pattern across the entire DataTable — the header cells (line 784), body cells (line 575), and this filter row all hardcode left: 0 for every index column. Fixing it only here would create an inconsistency with the header and body rows. I'd prefer to address this as a separate follow-up that fixes all three locations together. In practice, numberOfIndexes is typically 1 (just the row number), so the overlap doesn't manifest in normal usage.

Comment on lines 41 to +42
const { data, resource_usage: resourceUsage, sample_data: sampleData } = output;
const [filteredRowCount, setFilteredRowCount] = useState<number | null>(null);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

filteredRowCount state can persist across output changes and briefly show a stale "Showing X of Y rows" until the new DataTable calls onFilteredRowCount. Reset filteredRowCount to null in an effect when the underlying table content changes (e.g., when order, uuid, or output changes) to avoid flashing incorrect counts.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +153
const cellNumeric = parseFloat(displayValue);
if (isNaN(cellNumeric)) {
return false;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Numeric comparisons currently parse the cell value with parseFloat(displayValue). This will treat strings like "100abc" as numeric 100, which can produce incorrect matches. Consider validating the cell value with the same strict numeric regex used for filter parsing (or otherwise requiring a fully numeric string) before comparing.

Suggested change
const cellNumeric = parseFloat(displayValue);
if (isNaN(cellNumeric)) {
return false;
}
const normalizedDisplay = displayValue.trim();
if (!STRICT_NUMBER_REGEX.test(normalizedDisplay)) {
return false;
}
const cellNumeric = parseFloat(normalizedDisplay);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants