From 9926d1d0c27a879586abde6520d7cfaca7f262be Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:18:00 -0500 Subject: [PATCH 1/3] Add loading overlay and contextual empty states (#106, PR 1) Reusable LoadingOverlay control with spinning animation and randomized loading messages. Wired into DailySummary, Memory, and QueryPerformance tabs. Extracted shared LoadingMessages helper from ServerTab, added EmptyStateMessage style for context-specific empty states. Lite gets the LoadingMessages helper (UI wiring in PR 2). Co-Authored-By: Claude Opus 4.6 --- Dashboard/Controls/DailySummaryContent.xaml | 5 +- .../Controls/DailySummaryContent.xaml.cs | 7 +++ Dashboard/Controls/LoadingOverlay.xaml | 48 +++++++++++++++++++ Dashboard/Controls/LoadingOverlay.xaml.cs | 41 ++++++++++++++++ Dashboard/Controls/MemoryContent.xaml | 9 +++- Dashboard/Controls/MemoryContent.xaml.cs | 14 ++++++ .../Controls/QueryPerformanceContent.xaml | 8 +++- .../Controls/QueryPerformanceContent.xaml.cs | 14 ++++++ Dashboard/Helpers/LoadingMessages.cs | 46 ++++++++++++++++++ Dashboard/ServerTab.xaml.cs | 31 +----------- Dashboard/Themes/DarkTheme.xaml | 9 ++++ Lite/Helpers/LoadingMessages.cs | 43 +++++++++++++++++ 12 files changed, 240 insertions(+), 35 deletions(-) create mode 100644 Dashboard/Controls/LoadingOverlay.xaml create mode 100644 Dashboard/Controls/LoadingOverlay.xaml.cs create mode 100644 Dashboard/Helpers/LoadingMessages.cs create mode 100644 Lite/Helpers/LoadingMessages.cs diff --git a/Dashboard/Controls/DailySummaryContent.xaml b/Dashboard/Controls/DailySummaryContent.xaml index e6d2ab0c..652a8b8f 100644 --- a/Dashboard/Controls/DailySummaryContent.xaml +++ b/Dashboard/Controls/DailySummaryContent.xaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:controls="clr-namespace:PerformanceMonitorDashboard.Controls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> @@ -144,7 +145,9 @@ - + + diff --git a/Dashboard/Controls/DailySummaryContent.xaml.cs b/Dashboard/Controls/DailySummaryContent.xaml.cs index 802c1a47..eda60644 100644 --- a/Dashboard/Controls/DailySummaryContent.xaml.cs +++ b/Dashboard/Controls/DailySummaryContent.xaml.cs @@ -79,6 +79,9 @@ private async System.Threading.Tasks.Task LoadDailySummaryAsync() try { + DailySummaryLoading.IsLoading = true; + DailySummaryNoDataMessage.Visibility = Visibility.Collapsed; + var data = await _databaseService.GetDailySummaryAsync(_dailySummaryDate); // Store unfiltered data and reset filters when new data is loaded @@ -100,6 +103,10 @@ private async System.Threading.Tasks.Task LoadDailySummaryAsync() { Logger.Error($"Error loading daily summary: {ex.Message}"); } + finally + { + DailySummaryLoading.IsLoading = false; + } } #region Event Handlers diff --git a/Dashboard/Controls/LoadingOverlay.xaml b/Dashboard/Controls/LoadingOverlay.xaml new file mode 100644 index 00000000..4fbbeaa6 --- /dev/null +++ b/Dashboard/Controls/LoadingOverlay.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dashboard/Controls/LoadingOverlay.xaml.cs b/Dashboard/Controls/LoadingOverlay.xaml.cs new file mode 100644 index 00000000..b9b6810f --- /dev/null +++ b/Dashboard/Controls/LoadingOverlay.xaml.cs @@ -0,0 +1,41 @@ +using System.Windows; +using System.Windows.Controls; +using PerformanceMonitorDashboard.Helpers; + +namespace PerformanceMonitorDashboard.Controls +{ + public partial class LoadingOverlay : UserControl + { + public static readonly DependencyProperty IsLoadingProperty = + DependencyProperty.Register( + nameof(IsLoading), + typeof(bool), + typeof(LoadingOverlay), + new PropertyMetadata(false, OnIsLoadingChanged)); + + public bool IsLoading + { + get => (bool)GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public LoadingOverlay() + { + InitializeComponent(); + } + + private static void OnIsLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is LoadingOverlay overlay) + { + var isLoading = (bool)e.NewValue; + overlay.Visibility = isLoading ? Visibility.Visible : Visibility.Collapsed; + + if (isLoading) + { + overlay.MessageText.Text = LoadingMessages.GetRandom(); + } + } + } + } +} diff --git a/Dashboard/Controls/MemoryContent.xaml b/Dashboard/Controls/MemoryContent.xaml index 298686c1..bae6cf3b 100644 --- a/Dashboard/Controls/MemoryContent.xaml +++ b/Dashboard/Controls/MemoryContent.xaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF" + xmlns:controls="clr-namespace:PerformanceMonitorDashboard.Controls" mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="1200"> @@ -90,7 +91,9 @@ - + + @@ -116,7 +119,9 @@ - + + diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs index b9c7c9e6..f2b35d6f 100644 --- a/Dashboard/Controls/MemoryContent.xaml.cs +++ b/Dashboard/Controls/MemoryContent.xaml.cs @@ -383,6 +383,9 @@ private async System.Threading.Tasks.Task RefreshMemoryGrantsAsync() try { + MemoryGrantsLoading.IsLoading = true; + MemoryGrantsNoDataMessage.Visibility = Visibility.Collapsed; + var data = await _databaseService.GetMemoryGrantStatsAsync(_memoryGrantsHoursBack, _memoryGrantsFromDate, _memoryGrantsToDate); var dataList = data.ToList(); MemoryGrantsNoDataMessage.Visibility = dataList.Count == 0 ? Visibility.Visible : Visibility.Collapsed; @@ -392,6 +395,10 @@ private async System.Threading.Tasks.Task RefreshMemoryGrantsAsync() { Logger.Error($"Error loading memory grants: {ex.Message}"); } + finally + { + MemoryGrantsLoading.IsLoading = false; + } } private void LoadMemoryGrantsChart(IEnumerable data, int hoursBack, DateTime? fromDate, DateTime? toDate) @@ -485,6 +492,9 @@ private async System.Threading.Tasks.Task RefreshMemoryClerksAsync() try { + MemoryClerksLoading.IsLoading = true; + MemoryClerksNoDataMessage.Visibility = Visibility.Collapsed; + var data = await _databaseService.GetMemoryClerksTopNAsync(5, _memoryClerksHoursBack, _memoryClerksFromDate, _memoryClerksToDate); var dataList = data.ToList(); MemoryClerksNoDataMessage.Visibility = dataList.Count == 0 ? Visibility.Visible : Visibility.Collapsed; @@ -494,6 +504,10 @@ private async System.Threading.Tasks.Task RefreshMemoryClerksAsync() { Logger.Error($"Error loading memory clerks: {ex.Message}"); } + finally + { + MemoryClerksLoading.IsLoading = false; + } } private void LoadMemoryClerksChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate) diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml b/Dashboard/Controls/QueryPerformanceContent.xaml index 12bf9709..ca8c9dd3 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml +++ b/Dashboard/Controls/QueryPerformanceContent.xaml @@ -314,7 +314,9 @@ - + + @@ -585,7 +587,9 @@ - + + diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index e903040d..b93a0805 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -217,6 +217,9 @@ public async Task RefreshAllDataAsync() if (_databaseService == null) return; + QueryStatsLoading.IsLoading = true; + QueryStatsNoDataMessage.Visibility = Visibility.Collapsed; + // Fetch grid data (summary views aggregated per query/procedure) var queryStatsTask = _databaseService.GetQueryStatsAsync(_queryStatsHoursBack, _queryStatsFromDate, _queryStatsToDate); var procStatsTask = _databaseService.GetProcedureStatsAsync(_procStatsHoursBack, _procStatsFromDate, _procStatsToDate); @@ -263,6 +266,10 @@ await Task.WhenAll( { Logger.Error($"Error refreshing QueryPerformance data: {ex.Message}", ex); } + finally + { + QueryStatsLoading.IsLoading = false; + } } private void SetStatus(string message) @@ -367,7 +374,10 @@ private async Task RefreshActiveQueriesAsync() try { + ActiveQueriesLoading.IsLoading = true; + ActiveQueriesNoDataMessage.Visibility = Visibility.Collapsed; SetStatus("Loading active queries..."); + var data = await _databaseService.GetQuerySnapshotsAsync(_activeQueriesHoursBack, _activeQueriesFromDate, _activeQueriesToDate); ActiveQueriesDataGrid.ItemsSource = data; ActiveQueriesNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; @@ -378,6 +388,10 @@ private async Task RefreshActiveQueriesAsync() Logger.Error($"Error loading active queries: {ex.Message}"); SetStatus("Error loading active queries"); } + finally + { + ActiveQueriesLoading.IsLoading = false; + } } private void ActiveQueriesFilter_Click(object sender, RoutedEventArgs e) diff --git a/Dashboard/Helpers/LoadingMessages.cs b/Dashboard/Helpers/LoadingMessages.cs new file mode 100644 index 00000000..0f4f9102 --- /dev/null +++ b/Dashboard/Helpers/LoadingMessages.cs @@ -0,0 +1,46 @@ +using System; + +namespace PerformanceMonitorDashboard.Helpers +{ + /// + /// Randomized loading messages displayed while data is being fetched. + /// + public static class LoadingMessages + { + private static readonly string[] Messages = + [ + "Reticulating splines...", + "Consulting the oracle...", + "Asking the database nicely...", + "Crunching numbers...", + "Thinking really hard...", + "Mulling it over...", + "Communing with SQL Server...", + "Summoning data spirits...", + "Decoding the matrix...", + "Interrogating indexes...", + "Parsing the void...", + "Calibrating flux capacitors...", + "Negotiating with stored procedures...", + "Convincing queries to run faster...", + "Herding cursors...", + "Massaging execution plans...", + "Whispering to wait stats...", + "Befriending buffer pools...", + "Coaxing data from disk...", + "Pondering performance...", + "Untangling spaghetti queries...", + "Poking the query optimizer...", + "Warming up the plan cache...", + "Dusting off statistics...", + "Reasoning with RESOURCE_SEMAPHORE...", + "Apologizing to tempdb...", + "Flattering the cardinality estimator...", + "Bribing the lock manager...", + "Decrypting wait stats hieroglyphics...", + "Teaching cursors to fly...", + ]; + + public static string GetRandom() => Messages[Random.Shared.Next(Messages.Length)]; + } +} diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 51c17f69..b1d47686 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -35,36 +35,7 @@ public partial class ServerTab : UserControl public int UtcOffsetMinutes { get; } public DatabaseService DatabaseService => _databaseService; - // Fun AI-powered loading messages - private static readonly string[] _loadingMessages = new[] - { - "Reticulating splines...", - "Consulting the oracle...", - "Asking the database nicely...", - "Crunching numbers...", - "Thinking really hard...", - "Mulling it over...", - "Communing with SQL Server...", - "Summoning data spirits...", - "Decoding the matrix...", - "Interrogating indexes...", - "Parsing the void...", - "Calibrating flux capacitors...", - "Negotiating with stored procedures...", - "Convincing queries to run faster...", - "Herding cursors...", - "Massaging execution plans...", - "Whispering to wait stats...", - "Befriending buffer pools...", - "Coaxing data from disk...", - "Pondering performance..." - }; - private static readonly Random _loadingRandom = Random.Shared; - - private string GetLoadingMessage() - { - return _loadingMessages[_loadingRandom.Next(_loadingMessages.Length)]; - } + private static string GetLoadingMessage() => LoadingMessages.GetRandom(); private readonly UserPreferencesService _preferencesService; diff --git a/Dashboard/Themes/DarkTheme.xaml b/Dashboard/Themes/DarkTheme.xaml index 686dcef6..7390435c 100644 --- a/Dashboard/Themes/DarkTheme.xaml +++ b/Dashboard/Themes/DarkTheme.xaml @@ -157,6 +157,15 @@ + + +