Add column filters to FinOps grids#614
Conversation
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>
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
Dashboard/Controls/FinOpsContent.xamlDashboard/Controls/FinOpsContent.xaml.csLite/Controls/FinOpsTab.xamlLite/Controls/FinOpsTab.xaml.cs
| // 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; | ||
| } |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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).
Summary
FinOpsFilter_Clickhandler that discovers the parent DataGrid via visual tree walk and maintains per-grid filter state dictionariesDataGridTemplateColumn(Query Preview) columns as intended — onlyDataGridTextColumncolumns get filter buttonsTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes