From 85c147abaa76c80604101b82a5cf004bb1b41d7c Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:13:01 -0500 Subject: [PATCH] Add current configuration view to Dashboard Overview (#143) New "Current Configuration" tab in Overview shows the latest state of server settings, database settings, and trace flags using ROW_NUMBER windowing over the change history tables. Three sub-tabs with inline column filters for quick searching. Co-Authored-By: Claude Opus 4.6 --- Dashboard/Controls/CurrentConfigContent.xaml | 117 ++++++++++++ .../Controls/CurrentConfigContent.xaml.cs | 122 ++++++++++++ Dashboard/Models/CurrentServerConfigItem.cs | 43 +++++ Dashboard/ServerTab.xaml | 5 + Dashboard/ServerTab.xaml.cs | 5 +- .../Services/DatabaseService.SystemEvents.cs | 174 ++++++++++++++++++ 6 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 Dashboard/Controls/CurrentConfigContent.xaml create mode 100644 Dashboard/Controls/CurrentConfigContent.xaml.cs create mode 100644 Dashboard/Models/CurrentServerConfigItem.cs diff --git a/Dashboard/Controls/CurrentConfigContent.xaml b/Dashboard/Controls/CurrentConfigContent.xaml new file mode 100644 index 00000000..9108c2f6 --- /dev/null +++ b/Dashboard/Controls/CurrentConfigContent.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dashboard/Controls/CurrentConfigContent.xaml.cs b/Dashboard/Controls/CurrentConfigContent.xaml.cs new file mode 100644 index 00000000..4700a14d --- /dev/null +++ b/Dashboard/Controls/CurrentConfigContent.xaml.cs @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using PerformanceMonitorDashboard.Helpers; +using PerformanceMonitorDashboard.Services; + +namespace PerformanceMonitorDashboard.Controls +{ + public partial class CurrentConfigContent : UserControl + { + private DatabaseService? _databaseService; + + public CurrentConfigContent() + { + InitializeComponent(); + } + + public void Initialize(DatabaseService databaseService) + { + _databaseService = databaseService; + } + + public async Task RefreshAllDataAsync() + { + if (_databaseService == null) return; + + await Task.WhenAll( + RefreshServerConfigAsync(), + RefreshDatabaseConfigAsync(), + RefreshTraceFlagsAsync() + ); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + TabHelpers.AutoSizeColumnMinWidths(ServerConfigDataGrid); + TabHelpers.AutoSizeColumnMinWidths(DatabaseConfigDataGrid); + TabHelpers.AutoSizeColumnMinWidths(TraceFlagsDataGrid); + TabHelpers.FreezeColumns(ServerConfigDataGrid, 1); + TabHelpers.FreezeColumns(DatabaseConfigDataGrid, 1); + } + + #region Server Configuration + + private async Task RefreshServerConfigAsync() + { + if (_databaseService == null) return; + + try + { + var data = await _databaseService.GetCurrentServerConfigAsync(); + ServerConfigDataGrid.ItemsSource = data; + ServerConfigNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; + } + catch (Exception ex) + { + Logger.Error($"Error loading server configuration: {ex.Message}"); + } + } + + private void ServerConfigFilterTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + DataGridFilterService.ApplyFilter(ServerConfigDataGrid, sender as TextBox); + } + + #endregion + + #region Database Configuration + + private async Task RefreshDatabaseConfigAsync() + { + if (_databaseService == null) return; + + try + { + var data = await _databaseService.GetCurrentDatabaseConfigAsync(); + DatabaseConfigDataGrid.ItemsSource = data; + DatabaseConfigNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; + } + catch (Exception ex) + { + Logger.Error($"Error loading database configuration: {ex.Message}"); + } + } + + private void DatabaseConfigFilterTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + DataGridFilterService.ApplyFilter(DatabaseConfigDataGrid, sender as TextBox); + } + + #endregion + + #region Trace Flags + + private async Task RefreshTraceFlagsAsync() + { + if (_databaseService == null) return; + + try + { + var data = await _databaseService.GetCurrentTraceFlagsAsync(); + TraceFlagsDataGrid.ItemsSource = data; + TraceFlagsNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; + } + catch (Exception ex) + { + Logger.Error($"Error loading trace flags: {ex.Message}"); + } + } + + #endregion + } +} diff --git a/Dashboard/Models/CurrentServerConfigItem.cs b/Dashboard/Models/CurrentServerConfigItem.cs new file mode 100644 index 00000000..50ccc375 --- /dev/null +++ b/Dashboard/Models/CurrentServerConfigItem.cs @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; + +namespace PerformanceMonitorDashboard.Models +{ + public class CurrentServerConfigItem + { + public string ConfigurationName { get; set; } = string.Empty; + public string ValueConfigured { get; set; } = string.Empty; + public string ValueInUse { get; set; } = string.Empty; + public string ValueMinimum { get; set; } = string.Empty; + public string ValueMaximum { get; set; } = string.Empty; + public bool IsDynamic { get; set; } + public bool IsAdvanced { get; set; } + public string Description { get; set; } = string.Empty; + public DateTime LastChanged { get; set; } + } + + public class CurrentDatabaseConfigItem + { + public string DatabaseName { get; set; } = string.Empty; + public string SettingType { get; set; } = string.Empty; + public string SettingName { get; set; } = string.Empty; + public string SettingValue { get; set; } = string.Empty; + public DateTime LastChanged { get; set; } + } + + public class CurrentTraceFlagItem + { + public int TraceFlag { get; set; } + public bool Status { get; set; } + public bool IsGlobal { get; set; } + public bool IsSession { get; set; } + public DateTime LastChanged { get; set; } + } +} diff --git a/Dashboard/ServerTab.xaml b/Dashboard/ServerTab.xaml index fa7f83b4..526137c9 100644 --- a/Dashboard/ServerTab.xaml +++ b/Dashboard/ServerTab.xaml @@ -229,6 +229,11 @@ + + + + + diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 58acbcf6..9ee1de2d 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -115,6 +115,7 @@ public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0) DailySummaryTab.Initialize(_databaseService); CriticalIssuesTab.Initialize(_databaseService); DefaultTraceTab.Initialize(_databaseService); + CurrentConfigTab.Initialize(_databaseService); MemoryTab.Initialize(_databaseService); PerformanceTab.Initialize(_databaseService, s => StatusText.Text = s); SystemEventsContent.Initialize(_databaseService); @@ -1169,6 +1170,7 @@ private async Task ApplyAndRefreshCurrentTabAsync() CollectionHealth_Refresh_Click(null, new RoutedEventArgs()); await CriticalIssuesTab.RefreshDataAsync(); await DefaultTraceTab.RefreshAllDataAsync(); + await CurrentConfigTab.RefreshAllDataAsync(); await RefreshResourceOverviewAsync(); break; @@ -1278,13 +1280,14 @@ private async Task LoadDataAsync() var dailySummaryTask = DailySummaryTab.RefreshDataAsync(); var criticalIssuesTask = CriticalIssuesTab.RefreshDataAsync(); var defaultTraceTask = DefaultTraceTab.RefreshAllDataAsync(); + var currentConfigTask = CurrentConfigTab.RefreshAllDataAsync(); var systemEventsTask = SystemEventsContent.RefreshAllDataAsync(); // Wait for everything to complete before _isRefreshing resets await Task.WhenAll( healthTask, blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask, performanceTask, memoryTask, resourceOverviewTask, runningJobsTask, - resourceMetricsTask, dailySummaryTask, criticalIssuesTask, defaultTraceTask, systemEventsTask); + resourceMetricsTask, dailySummaryTask, criticalIssuesTask, defaultTraceTask, currentConfigTask, systemEventsTask); // Populate grids with fetched data var healthData = await healthTask; diff --git a/Dashboard/Services/DatabaseService.SystemEvents.cs b/Dashboard/Services/DatabaseService.SystemEvents.cs index 1e6b21ab..114dfe1b 100644 --- a/Dashboard/Services/DatabaseService.SystemEvents.cs +++ b/Dashboard/Services/DatabaseService.SystemEvents.cs @@ -1175,5 +1175,179 @@ FROM collect.HealthParser_MemoryNodeOOM AS hpmn return items; } + + public async Task> GetCurrentServerConfigAsync() + { + var items = new List(); + + await using var tc = await OpenThrottledConnectionAsync(); + var connection = tc.Connection; + + string query = @" + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + WITH + latest AS + ( + SELECT + h.configuration_id, + h.configuration_name, + h.value_configured, + h.value_in_use, + h.value_minimum, + h.value_maximum, + h.is_dynamic, + h.is_advanced, + h.description, + h.collection_time, + rn = ROW_NUMBER() OVER (PARTITION BY h.configuration_id ORDER BY h.collection_time DESC) + FROM config.server_configuration_history AS h + ) + SELECT + l.configuration_name, + value_configured = CONVERT(nvarchar(100), l.value_configured), + value_in_use = CONVERT(nvarchar(100), l.value_in_use), + value_minimum = CONVERT(nvarchar(100), l.value_minimum), + value_maximum = CONVERT(nvarchar(100), l.value_maximum), + l.is_dynamic, + l.is_advanced, + l.description, + last_changed = l.collection_time + FROM latest AS l + WHERE l.rn = 1 + ORDER BY + l.configuration_name;"; + + using var command = new SqlCommand(query, connection); + command.CommandTimeout = 30; + + using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + items.Add(new CurrentServerConfigItem + { + ConfigurationName = reader.GetString(0), + ValueConfigured = reader.IsDBNull(1) ? string.Empty : reader.GetString(1), + ValueInUse = reader.IsDBNull(2) ? string.Empty : reader.GetString(2), + ValueMinimum = reader.IsDBNull(3) ? string.Empty : reader.GetString(3), + ValueMaximum = reader.IsDBNull(4) ? string.Empty : reader.GetString(4), + IsDynamic = reader.GetBoolean(5), + IsAdvanced = reader.GetBoolean(6), + Description = reader.IsDBNull(7) ? string.Empty : reader.GetString(7), + LastChanged = reader.GetDateTime(8) + }); + } + + return items; + } + + public async Task> GetCurrentDatabaseConfigAsync() + { + var items = new List(); + + await using var tc = await OpenThrottledConnectionAsync(); + var connection = tc.Connection; + + string query = @" + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + WITH + latest AS + ( + SELECT + h.database_name, + h.setting_type, + h.setting_name, + h.setting_value, + h.collection_time, + rn = ROW_NUMBER() OVER ( + PARTITION BY h.database_name, h.setting_type, h.setting_name + ORDER BY h.collection_time DESC + ) + FROM config.database_configuration_history AS h + ) + SELECT + l.database_name, + l.setting_type, + l.setting_name, + setting_value = CONVERT(nvarchar(500), l.setting_value), + last_changed = l.collection_time + FROM latest AS l + WHERE l.rn = 1 + ORDER BY + l.database_name, + l.setting_type, + l.setting_name;"; + + using var command = new SqlCommand(query, connection); + command.CommandTimeout = 30; + + using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + items.Add(new CurrentDatabaseConfigItem + { + DatabaseName = reader.GetString(0), + SettingType = reader.GetString(1), + SettingName = reader.GetString(2), + SettingValue = reader.IsDBNull(3) ? string.Empty : reader.GetString(3), + LastChanged = reader.GetDateTime(4) + }); + } + + return items; + } + + public async Task> GetCurrentTraceFlagsAsync() + { + var items = new List(); + + await using var tc = await OpenThrottledConnectionAsync(); + var connection = tc.Connection; + + string query = @" + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + WITH + latest AS + ( + SELECT + h.trace_flag, + h.status, + h.is_global, + h.is_session, + h.collection_time, + rn = ROW_NUMBER() OVER (PARTITION BY h.trace_flag ORDER BY h.collection_time DESC) + FROM config.trace_flags_history AS h + ) + SELECT + l.trace_flag, + l.status, + l.is_global, + l.is_session, + last_changed = l.collection_time + FROM latest AS l + WHERE l.rn = 1 + ORDER BY + l.trace_flag;"; + + using var command = new SqlCommand(query, connection); + command.CommandTimeout = 30; + + using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + items.Add(new CurrentTraceFlagItem + { + TraceFlag = reader.GetInt32(0), + Status = reader.GetBoolean(1), + IsGlobal = reader.GetBoolean(2), + IsSession = reader.GetBoolean(3), + LastChanged = reader.GetDateTime(4) + }); + } + + return items; + } } }