Skip to content

Add column filters to FinOps grids#614

Merged
erikdarlingdata merged 1 commit intodevfrom
feature/finops-grid-filters
Mar 17, 2026
Merged

Add column filters to FinOps grids#614
erikdarlingdata merged 1 commit intodevfrom
feature/finops-grid-filters

Conversation

@erikdarlingdata
Copy link
Owner

@erikdarlingdata erikdarlingdata commented Mar 17, 2026

Summary

  • Lite: Add column-level filtering to all 5 Optimization sub-tab grids (Idle Databases, Tempdb Pressure, Wait Stats Summary, Expensive Queries, Memory Grant Efficiency) using the existing shared filter infrastructure
  • Dashboard: Add column-level filtering to all 10 FinOps DataGrids (Recommendations, Database Resources, Storage Growth, Database Sizes, Wait Category Summary, Expensive Queries, Memory Grant Efficiency, Application Connections, Server Inventory, High Impact) with a single FinOpsFilter_Click handler that discovers the parent DataGrid via visual tree walk and maintains per-grid filter state dictionaries
  • Skips DataGridTemplateColumn (Query Preview) columns as intended — only DataGridTextColumn columns get filter buttons

Test plan

  • Open Dashboard FinOps tab, verify filter buttons appear on all column headers across all 10 grids
  • Click a filter button, enter text, apply — verify rows are filtered
  • Verify filter button turns gold when active, white when cleared
  • Apply filters on multiple columns simultaneously — verify AND logic
  • Switch between sub-tabs and verify filters persist per-grid
  • Open Lite Optimization tab, verify filter buttons on all 5 sub-grids
  • Build both Dashboard and Lite with 0 errors

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added column filtering to FinOps data grids—users can filter data directly from column headers using dedicated filter buttons.
    • Filter status is visually indicated: gold highlighting shows active filters, white shows inactive columns; tooltips display applied filter details.
    • Filters persist per grid and can be cleared to view all unfiltered data.

Lite: Add column-level filtering to all 5 Optimization sub-tab grids
(IdleDatabases, TempdbPressure, WaitCategorySummary, ExpensiveQueries,
MemoryGrantEfficiency) using the existing shared filter infrastructure.

Dashboard: Add column-level filtering to all 10 FinOps DataGrids
(Recommendations, DatabaseResources, StorageGrowth, DatabaseSizes,
WaitCategorySummary, ExpensiveQueries, MemoryGrantEfficiency,
ApplicationConnections, ServerInventory, HighImpact). Uses a single
FinOpsFilter_Click handler that finds the parent DataGrid via visual
tree walk, with per-grid filter state dictionaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

This change introduces a column-based filtering system for FinOps DataGrids. It adds filtering popup UI, state management, filter button styling, and header modifications across multiple XAML controls, enabling dynamic per-column data filtering with persistent state and visual feedback.

Changes

Cohort / File(s) Summary
Filter Implementation & Popup Management
Dashboard/Controls/FinOpsContent.xaml.cs
Introduces ColumnFilterPopup-based filtering with lazy initialization, click handlers to open/close the popup, and state tracking via _gridFilters and _gridUnfilteredData dictionaries. Implements filter application via reflection-based MatchesFilter, visual feedback through UpdateFinOpsFilterButtonStyles, and event wiring to persist filter state per grid/column.
XAML Header Structure
Lite/Controls/FinOpsTab.xaml
Replaces static DataGridTextColumn headers with dynamic StackPanel headers containing filter Button and bold TextBlock labels across multiple DataGrids (IdleDatabases, TempdbPressure, WaitCategorySummary, ExpensiveQueries, MemoryGrantEfficiency, etc.). Preserves existing bindings and styling while restructuring header rendering.
Data Flow & Filter Manager Integration
Lite/Controls/FinOpsTab.xaml.cs
Adds private filter manager fields for five key row types (IdleDatabaseRow, TempdbSummaryRow, WaitCategorySummaryRow, ExpensiveQueryRow, MemoryGrantEfficiencyRow). Replaces direct ItemsSource assignments in Load\*Async methods with UpdateData() calls on respective filter managers, routing data through the filtering layer.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add column filters to FinOps grids' directly and clearly summarizes the main change: implementing column-level filtering UI across FinOps DataGrids in both Dashboard and Lite views.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/finops-grid-filters
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Dashboard/Controls/FinOpsContent.xaml.cs`:
- Around line 1059-1090: _gridUnfilteredData is cached once and becomes stale
when a grid's ItemsSource is later refreshed; introduce a single setter method
(e.g., SetFinOpsGridData(dataGrid, newData)) that assigns the new ItemsSource,
updates _gridUnfilteredData[dataGrid] = newData, and then reapplies any active
filters from _gridFilters (use DataGridFilterService.MatchesFilter to produce
filtered view or set ItemsSource = newData if no filters), and replace all
direct assignments to dataGrid.ItemsSource in your Load*Async methods with calls
to SetFinOpsGridData so the cache stays in sync with reloads.

In `@Lite/Controls/FinOpsTab.xaml`:
- Around line 1538-1575: The export/copy code is calling col.Header?.ToString()
and returns "System.Windows.Controls.StackPanel" when headers use StackPanel
templates; update CopyAllRows_Click and ExportToCsv_Click in FinOpsTab.xaml.cs
to call the existing GetHeaderText(col) helper instead of col.Header?.ToString()
so the real column labels are used (replace both occurrences referenced around
the current handlers and any similar export/copy logic for the other column
groups).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 642951f7-a0c9-4bb7-8520-cc2729ead348

📥 Commits

Reviewing files that changed from the base of the PR and between 713f840 and 619b003.

📒 Files selected for processing (4)
  • Dashboard/Controls/FinOpsContent.xaml
  • Dashboard/Controls/FinOpsContent.xaml.cs
  • Lite/Controls/FinOpsTab.xaml
  • Lite/Controls/FinOpsTab.xaml.cs

Comment on lines +1059 to +1090
// Capture unfiltered data on first filter application
if (!_gridUnfilteredData.TryGetValue(dataGrid, out var cached) || cached == null)
{
cached = dataGrid.ItemsSource;
_gridUnfilteredData[dataGrid] = cached;
}

var unfilteredData = cached;
if (unfilteredData == null) return;

if (!_gridFilters.TryGetValue(dataGrid, out var filters) || filters.Count == 0)
{
dataGrid.ItemsSource = unfilteredData;
return;
}

// Generic filtering: cast to IEnumerable, filter each item using reflection-based MatchesFilter
var sourceList = unfilteredData.Cast<object>().ToList();
var filteredData = sourceList.Where(item =>
{
foreach (var filter in filters.Values)
{
if (filter.IsActive && !DataGridFilterService.MatchesFilter(item, filter))
{
return false;
}
}
return true;
}).ToList();

dataGrid.ItemsSource = filteredData;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Filter cache can go stale after grid refreshes

_gridUnfilteredData is captured once and then reused indefinitely. After a later data reload, active filters can be evaluated against stale rows instead of the latest ItemsSource.

💡 Proposed fix (introduce a filter-aware grid data setter)
+private void SetFinOpsGridData(DataGrid dataGrid, System.Collections.IEnumerable data)
+{
+    _gridUnfilteredData[dataGrid] = data;
+
+    if (_gridFilters.TryGetValue(dataGrid, out var filters) && filters.Count > 0)
+    {
+        ApplyFinOpsFilters(dataGrid);
+        UpdateFinOpsFilterButtonStyles(dataGrid);
+        return;
+    }
+
+    dataGrid.ItemsSource = data;
+    UpdateFinOpsFilterButtonStyles(dataGrid);
+}

Then call SetFinOpsGridData(...) from each Load*Async method instead of assigning ItemsSource directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dashboard/Controls/FinOpsContent.xaml.cs` around lines 1059 - 1090,
_gridUnfilteredData is cached once and becomes stale when a grid's ItemsSource
is later refreshed; introduce a single setter method (e.g.,
SetFinOpsGridData(dataGrid, newData)) that assigns the new ItemsSource, updates
_gridUnfilteredData[dataGrid] = newData, and then reapplies any active filters
from _gridFilters (use DataGridFilterService.MatchesFilter to produce filtered
view or set ItemsSource = newData if no filters), and replace all direct
assignments to dataGrid.ItemsSource in your Load*Async methods with calls to
SetFinOpsGridData so the cache stays in sync with reloads.

Comment on lines +1538 to +1575
<DataGridTextColumn Binding="{Binding DatabaseName}" Width="180">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="DatabaseName" Click="FilterButton_Click" Margin="0,0,4,0"/>
<TextBlock Text="Database" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TotalSizeMb, StringFormat='{}{0:N2}'}" Width="120">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="TotalSizeMb" Click="FilterButton_Click" Margin="0,0,4,0"/>
<TextBlock Text="Total Size MB" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="TextBlock"><Setter Property="HorizontalAlignment" Value="Right"/></Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Files" Binding="{Binding FileCount}" Width="60">
<DataGridTextColumn Binding="{Binding FileCount}" Width="60">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="FileCount" Click="FilterButton_Click" Margin="0,0,4,0"/>
<TextBlock Text="Files" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="TextBlock"><Setter Property="HorizontalAlignment" Value="Right"/></Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Last Execution" Binding="{Binding LastExecutionTime, StringFormat='{}{0:yyyy-MM-dd HH:mm}'}" Width="160"/>
<DataGridTextColumn Binding="{Binding LastExecutionTime, StringFormat='{}{0:yyyy-MM-dd HH:mm}'}" Width="160">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="LastExecutionTime" Click="FilterButton_Click" Margin="0,0,4,0"/>
<TextBlock Text="Last Execution" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Header templates break exported/copied column names

These headers are now StackPanel objects, but export/copy still uses col.Header?.ToString() (Lite/Controls/FinOpsTab.xaml.cs, Line 887 and Line 933 paths). That produces System.Windows.Controls.StackPanel instead of real column labels.

💡 Proposed fix (Lite/Controls/FinOpsTab.xaml.cs helper)
+private static string GetHeaderText(DataGridColumn col)
+{
+    return col.Header switch
+    {
+        string s => s,
+        TextBlock tb => tb.Text,
+        StackPanel sp => sp.Children.OfType<TextBlock>().FirstOrDefault()?.Text
+                         ?? col.SortMemberPath
+                         ?? string.Empty,
+        _ => col.Header?.ToString() ?? string.Empty
+    };
+}

Use GetHeaderText(col) in CopyAllRows_Click and ExportToCsv_Click instead of col.Header?.ToString().

Also applies to: 1600-1637, 1676-1702, 1745-1770, 1803-1841

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Lite/Controls/FinOpsTab.xaml` around lines 1538 - 1575, The export/copy code
is calling col.Header?.ToString() and returns
"System.Windows.Controls.StackPanel" when headers use StackPanel templates;
update CopyAllRows_Click and ExportToCsv_Click in FinOpsTab.xaml.cs to call the
existing GetHeaderText(col) helper instead of col.Header?.ToString() so the real
column labels are used (replace both occurrences referenced around the current
handlers and any similar export/copy logic for the other column groups).

@erikdarlingdata erikdarlingdata merged commit ba9b20c into dev Mar 17, 2026
5 checks passed
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.

1 participant