Skip to content

Commit 79a0234

Browse files
erikdarlingdataClaudioESSilvaclaude
committed
Merge PR #608: Add growth rate and VLF count columns (#567)
Resolves XAML conflict by taking dev's FinOpsContent.xaml and adding the two new Database Sizes columns (Auto Growth, VLF Count) from ClaudioESSilva's feature/GrowthRates-VLFCount branch. All other files auto-merged cleanly. Changes from PR #608: - Dashboard + Lite: new GrowthDisplay and VlfCountDisplay columns on Database Sizes grid - SQL collector: collect file growth settings and VLF counts - Upgrade script: 2.2.0-to-2.3.0/03_add_growth_vlf_columns.sql - Dashboard: DataGridFilterManager for Database Sizes column filtering - DuckDB schema: new growth/VLF columns in database_size_stats Co-Authored-By: Cláudio Silva <ClaudioESSilva@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents e438b9d + 2aced6b commit 79a0234

14 files changed

Lines changed: 474 additions & 18 deletions

Dashboard/Controls/FinOpsContent.xaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,27 @@
10571057
</StackPanel>
10581058
</DataGridTextColumn.Header>
10591059
</DataGridTextColumn>
1060+
<DataGridTextColumn Binding="{Binding GrowthDisplay}" Width="110" SortMemberPath="AutoGrowthSort">
1061+
<DataGridTextColumn.Header>
1062+
<StackPanel Orientation="Horizontal">
1063+
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="GrowthDisplay" Click="DatabaseSizesFilter_Click" Margin="0,0,4,0"/>
1064+
<TextBlock Text="Auto Growth" FontWeight="Bold" VerticalAlignment="Center"/>
1065+
</StackPanel>
1066+
</DataGridTextColumn.Header>
1067+
</DataGridTextColumn>
1068+
<DataGridTextColumn Binding="{Binding VlfCountDisplay}" Width="80" SortMemberPath="VlfCountSort">
1069+
<DataGridTextColumn.Header>
1070+
<StackPanel Orientation="Horizontal">
1071+
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="VlfCountDisplay" Click="DatabaseSizesFilter_Click" Margin="0,0,4,0"/>
1072+
<TextBlock Text="VLF Count" FontWeight="Bold" VerticalAlignment="Center"/>
1073+
</StackPanel>
1074+
</DataGridTextColumn.Header>
1075+
<DataGridTextColumn.ElementStyle>
1076+
<Style TargetType="TextBlock">
1077+
<Setter Property="HorizontalAlignment" Value="Right"/>
1078+
</Style>
1079+
</DataGridTextColumn.ElementStyle>
1080+
</DataGridTextColumn>
10601081
<DataGridTextColumn Binding="{Binding MonthlyCostShare, StringFormat='{}{0:N2}'}" Width="110">
10611082
<DataGridTextColumn.Header>
10621083
<StackPanel Orientation="Horizontal">

Dashboard/Controls/FinOpsContent.xaml.cs

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Threading.Tasks;
1515
using System.Windows;
1616
using System.Windows.Controls;
17+
using System.Windows.Controls.Primitives;
1718
using System.Windows.Data;
1819
using System.Windows.Controls.Primitives;
1920
using System.Windows.Media;
@@ -33,6 +34,10 @@ public partial class FinOpsContent : UserControl
3334
private DateTime _serverInventoryCacheTime;
3435
private decimal _currentServerMonthlyCost;
3536

37+
private DataGridFilterManager<FinOpsDatabaseSizeStats>? _dbSizesFilterMgr;
38+
private Popup? _dbSizeFilterPopup;
39+
private ColumnFilterPopup? _dbSizeFilterPopupContent;
40+
3641
public FinOpsContent()
3742
{
3843
InitializeComponent();
@@ -70,6 +75,8 @@ private void OnLoaded(object sender, RoutedEventArgs e)
7075
TabHelpers.FreezeColumns(ExpensiveQueriesDataGrid, 1);
7176
TabHelpers.FreezeColumns(IndexAnalysisDetailGrid, 1);
7277
TabHelpers.FreezeColumns(HighImpactDataGrid, 1);
78+
79+
_dbSizesFilterMgr = new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid);
7380
}
7481

7582
/// <summary>
@@ -624,16 +631,62 @@ private async Task LoadDatabaseSizesAsync()
624631
}
625632
}
626633

627-
DatabaseSizesDataGrid.ItemsSource = data;
628-
DatabaseSizesNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
629-
DbSizeCountIndicator.Text = data.Count > 0 ? $"{data.Count} file(s)" : "";
634+
_dbSizesFilterMgr!.UpdateData(data);
635+
UpdateDbSizeCountUI();
630636
}
631637
catch (Exception ex)
632638
{
633639
Logger.Error($"Error loading database sizes: {ex.Message}", ex);
634640
}
635641
}
636642

643+
private void UpdateDbSizeCountUI()
644+
{
645+
var list = DatabaseSizesDataGrid.ItemsSource as System.Collections.IList;
646+
int count = list?.Count ?? 0;
647+
DatabaseSizesNoDataMessage.Visibility = count == 0 ? Visibility.Visible : Visibility.Collapsed;
648+
DbSizeCountIndicator.Text = count > 0 ? $"{count} file(s)" : "";
649+
}
650+
651+
private void DatabaseSizesFilter_Click(object sender, RoutedEventArgs e)
652+
{
653+
if (sender is not Button button || button.Tag is not string columnName) return;
654+
655+
if (_dbSizeFilterPopup == null)
656+
{
657+
_dbSizeFilterPopupContent = new ColumnFilterPopup();
658+
_dbSizeFilterPopupContent.FilterApplied += FilterPopup_DbSizeFilterApplied;
659+
_dbSizeFilterPopupContent.FilterCleared += FilterPopup_DbSizeFilterCleared;
660+
_dbSizeFilterPopup = new Popup
661+
{
662+
Child = _dbSizeFilterPopupContent,
663+
StaysOpen = false,
664+
Placement = PlacementMode.Bottom,
665+
AllowsTransparency = true
666+
};
667+
}
668+
669+
_dbSizesFilterMgr!.Filters.TryGetValue(columnName, out var existingFilter);
670+
_dbSizeFilterPopupContent!.Initialize(columnName, existingFilter);
671+
_dbSizeFilterPopup.PlacementTarget = button;
672+
_dbSizeFilterPopup.IsOpen = true;
673+
}
674+
675+
private void FilterPopup_DbSizeFilterApplied(object? sender, FilterAppliedEventArgs e)
676+
{
677+
if (_dbSizeFilterPopup != null)
678+
_dbSizeFilterPopup.IsOpen = false;
679+
680+
_dbSizesFilterMgr!.SetFilter(e.FilterState);
681+
UpdateDbSizeCountUI();
682+
}
683+
684+
private void FilterPopup_DbSizeFilterCleared(object? sender, EventArgs e)
685+
{
686+
if (_dbSizeFilterPopup != null)
687+
_dbSizeFilterPopup.IsOpen = false;
688+
}
689+
637690
// ============================================
638691
// Application Connections Tab
639692
// ============================================
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Linq;
12+
using System.Windows;
13+
using System.Windows.Controls;
14+
using System.Windows.Media;
15+
using PerformanceMonitorDashboard.Models;
16+
17+
namespace PerformanceMonitorDashboard.Services;
18+
19+
/// <summary>
20+
/// Non-generic interface for looking up filter state from a shared dictionary.
21+
/// </summary>
22+
public interface IDataGridFilterManager
23+
{
24+
Dictionary<string, ColumnFilterState> Filters { get; }
25+
void SetFilter(ColumnFilterState filterState);
26+
void UpdateFilterButtonStyles();
27+
}
28+
29+
/// <summary>
30+
/// Manages column filter state, unfiltered data capture, and filter application
31+
/// for a single DataGrid. Eliminates per-grid boilerplate code.
32+
/// </summary>
33+
public class DataGridFilterManager<T> : IDataGridFilterManager
34+
{
35+
private readonly DataGrid _dataGrid;
36+
private readonly Dictionary<string, ColumnFilterState> _filters = new();
37+
private List<T>? _unfilteredData;
38+
39+
public DataGridFilterManager(DataGrid dataGrid)
40+
{
41+
_dataGrid = dataGrid;
42+
}
43+
44+
public Dictionary<string, ColumnFilterState> Filters => _filters;
45+
46+
/// <summary>
47+
/// Called when new data arrives (refresh cycle). Captures unfiltered data,
48+
/// then re-applies any active filters.
49+
/// </summary>
50+
public void UpdateData(List<T> newData)
51+
{
52+
_unfilteredData = newData;
53+
54+
if (!HasActiveFilters())
55+
{
56+
_dataGrid.ItemsSource = newData;
57+
return;
58+
}
59+
60+
ApplyFilters();
61+
}
62+
63+
/// <summary>
64+
/// Applies or removes a filter and re-filters the data.
65+
/// </summary>
66+
public void SetFilter(ColumnFilterState filterState)
67+
{
68+
if (filterState.IsActive)
69+
_filters[filterState.ColumnName] = filterState;
70+
else
71+
_filters.Remove(filterState.ColumnName);
72+
73+
ApplyFilters();
74+
UpdateFilterButtonStyles();
75+
}
76+
77+
private bool HasActiveFilters()
78+
{
79+
return _filters.Count > 0 && _filters.Values.Any(f => f.IsActive);
80+
}
81+
82+
private void ApplyFilters()
83+
{
84+
if (_unfilteredData == null) return;
85+
86+
if (!HasActiveFilters())
87+
{
88+
_dataGrid.ItemsSource = _unfilteredData;
89+
return;
90+
}
91+
92+
var filteredData = _unfilteredData.Where(item =>
93+
{
94+
foreach (var filter in _filters.Values)
95+
{
96+
if (filter.IsActive && !DataGridFilterService.MatchesFilter(item!, filter))
97+
return false;
98+
}
99+
return true;
100+
}).ToList();
101+
102+
_dataGrid.ItemsSource = filteredData;
103+
}
104+
105+
/// <summary>
106+
/// Updates filter icon colors (gold when active, dim when inactive).
107+
/// </summary>
108+
public void UpdateFilterButtonStyles()
109+
{
110+
foreach (var column in _dataGrid.Columns)
111+
{
112+
if (column.Header is StackPanel headerPanel)
113+
{
114+
var filterButton = headerPanel.Children.OfType<Button>().FirstOrDefault();
115+
if (filterButton != null && filterButton.Tag is string columnName)
116+
{
117+
bool hasActive = _filters.TryGetValue(columnName, out var filter) && filter.IsActive;
118+
119+
var textBlock = new TextBlock
120+
{
121+
Text = hasActive ? "\uE16E" : "\uE71C",
122+
FontFamily = new FontFamily("Segoe MDL2 Assets"),
123+
Foreground = hasActive
124+
? new SolidColorBrush(Color.FromRgb(0xFF, 0xD7, 0x00))
125+
: (Brush)Application.Current.FindResource("ForegroundDimBrush")
126+
};
127+
filterButton.Content = textBlock;
128+
129+
filterButton.ToolTip = hasActive && filter != null
130+
? $"Filter: {filter.DisplayText}\n(Click to modify)"
131+
: "Click to filter";
132+
}
133+
}
134+
}
135+
}
136+
}

Dashboard/Services/DatabaseService.FinOps.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,10 @@ public async Task<List<FinOpsDatabaseSizeStats>> GetFinOpsDatabaseSizeStatsAsync
322322
state_desc,
323323
volume_mount_point,
324324
volume_total_mb,
325-
volume_free_mb
325+
volume_free_mb,
326+
is_percent_growth,
327+
growth_pct,
328+
vlf_count
326329
FROM collect.database_size_stats
327330
WHERE collection_time =
328331
(
@@ -364,7 +367,10 @@ ORDER BY
364367
StateDesc = reader.IsDBNull(15) ? "" : reader.GetString(15),
365368
VolumeMountPoint = reader.IsDBNull(16) ? "" : reader.GetString(16),
366369
VolumeTotalMb = reader.IsDBNull(17) ? 0m : Convert.ToDecimal(reader.GetValue(17)),
367-
VolumeFreeMb = reader.IsDBNull(18) ? 0m : Convert.ToDecimal(reader.GetValue(18))
370+
VolumeFreeMb = reader.IsDBNull(18) ? 0m : Convert.ToDecimal(reader.GetValue(18)),
371+
IsPercentGrowth = reader.IsDBNull(19) ? null : (bool?)(Convert.ToInt32(reader.GetValue(19)) == 1),
372+
GrowthPct = reader.IsDBNull(20) ? null : Convert.ToInt32(reader.GetValue(20)),
373+
VlfCount = reader.IsDBNull(21) ? null : Convert.ToInt32(reader.GetValue(21))
368374
});
369375
}
370376
}
@@ -2540,9 +2546,32 @@ public class FinOpsDatabaseSizeStats
25402546
public string VolumeMountPoint { get; set; } = "";
25412547
public decimal VolumeTotalMb { get; set; }
25422548
public decimal VolumeFreeMb { get; set; }
2549+
public bool? IsPercentGrowth { get; set; }
2550+
public int? GrowthPct { get; set; }
2551+
public int? VlfCount { get; set; }
25432552

25442553
// FinOps cost — proportional share of server monthly budget
25452554
public decimal MonthlyCostShare { get; set; }
2555+
2556+
public string GrowthDisplay => IsPercentGrowth switch
2557+
{
2558+
null => "-",
2559+
true => GrowthPct.HasValue ? $"{GrowthPct}%" : "-",
2560+
false => AutoGrowthMb == 0 ? "Disabled" : $"{AutoGrowthMb:N0} MB"
2561+
};
2562+
2563+
public decimal AutoGrowthSort => IsPercentGrowth switch
2564+
{
2565+
null => -1m,
2566+
true => (decimal)(GrowthPct ?? -1),
2567+
false => AutoGrowthMb
2568+
};
2569+
2570+
public string VlfCountDisplay => string.Equals(FileTypeDesc, "LOG", StringComparison.OrdinalIgnoreCase)
2571+
? (VlfCount?.ToString() ?? "-") : "N/A";
2572+
2573+
public int VlfCountSort => string.Equals(FileTypeDesc, "LOG", StringComparison.OrdinalIgnoreCase)
2574+
? (VlfCount ?? 0) : -1;
25462575
}
25472576

25482577
public class FinOpsTopResourceConsumer

Lite/Controls/FinOpsTab.xaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,27 @@
10161016
</StackPanel>
10171017
</DataGridTextColumn.Header>
10181018
</DataGridTextColumn>
1019+
<DataGridTextColumn Binding="{Binding GrowthDisplay}" Width="110" SortMemberPath="AutoGrowthSort">
1020+
<DataGridTextColumn.Header>
1021+
<StackPanel Orientation="Horizontal">
1022+
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="GrowthDisplay" Click="FilterButton_Click" Margin="0,0,4,0"/>
1023+
<TextBlock Text="Auto Growth" FontWeight="Bold" VerticalAlignment="Center"/>
1024+
</StackPanel>
1025+
</DataGridTextColumn.Header>
1026+
</DataGridTextColumn>
1027+
<DataGridTextColumn Binding="{Binding VlfCountDisplay}" Width="80" SortMemberPath="VlfCountSort">
1028+
<DataGridTextColumn.Header>
1029+
<StackPanel Orientation="Horizontal">
1030+
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="VlfCountDisplay" Click="FilterButton_Click" Margin="0,0,4,0"/>
1031+
<TextBlock Text="VLF Count" FontWeight="Bold" VerticalAlignment="Center"/>
1032+
</StackPanel>
1033+
</DataGridTextColumn.Header>
1034+
<DataGridTextColumn.ElementStyle>
1035+
<Style TargetType="TextBlock">
1036+
<Setter Property="HorizontalAlignment" Value="Right"/>
1037+
</Style>
1038+
</DataGridTextColumn.ElementStyle>
1039+
</DataGridTextColumn>
10191040
<DataGridTextColumn Header="Monthly Cost ($)" Binding="{Binding MonthlyCostShare, StringFormat='{}{0:N2}'}" Width="110">
10201041
<DataGridTextColumn.ElementStyle>
10211042
<Style TargetType="TextBlock">

Lite/Database/DuckDbInitializer.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void Dispose()
8686
/// <summary>
8787
/// Current schema version. Increment this when schema changes require table rebuilds.
8888
/// </summary>
89-
internal const int CurrentSchemaVersion = 21;
89+
internal const int CurrentSchemaVersion = 22;
9090

9191
private readonly string _archivePath;
9292

@@ -583,6 +583,22 @@ New tables only — no existing table changes needed. Tables created by
583583
_logger?.LogWarning("Migration to v21 encountered an error (non-fatal): {Error}", ex.Message);
584584
}
585585
}
586+
587+
if (fromVersion < 22)
588+
{
589+
_logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats");
590+
try
591+
{
592+
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
593+
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
594+
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
595+
}
596+
catch (Exception ex)
597+
{
598+
_logger?.LogError(ex, "Migration to v22 failed");
599+
throw;
600+
}
601+
}
586602
}
587603

588604
/// <summary>

Lite/Database/Schema.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,10 @@ max_size_mb DECIMAL(19,2),
614614
state_desc VARCHAR,
615615
volume_mount_point VARCHAR,
616616
volume_total_mb DECIMAL(19,2),
617-
volume_free_mb DECIMAL(19,2)
617+
volume_free_mb DECIMAL(19,2),
618+
is_percent_growth BOOLEAN,
619+
growth_pct INTEGER,
620+
vlf_count INTEGER
618621
)";
619622

620623
public const string CreateDatabaseSizeStatsIndex = @"

0 commit comments

Comments
 (0)