Skip to content

Commit c90bff7

Browse files
Feature/multi query stores dashboard overview (#311)
* switch the default grid GroupBy from "None" to "Query Hash" and don't open the first line by default * The Query Stores Overview feature has been implemented. Here's a summary of what was created: New Files: 1. QueryStoreOverviewModels.cs — Models for: • QueryStoreState enum (Off, ReadOnly, ReadWrite) • DatabaseQueryStoreState — state per database • DatabaseMetrics — aggregated metrics (total + avg) per database • DatabaseTimeSlice — time slice data tagged by database • DatabaseWaitCategoryTimeSlice — wait stats tagged by database 2. QueryStoreOverviewService.cs — Parallel data fetching with: • SemaphoreSlim throttling (default DOP=8) • ConcurrentBag<T> for thread-safe result collection • Methods: FetchAllStatesAsync(string, int, CancellationToken), FetchAllMetricsAsync(string, List<string>, DateTime, DateTime, int, CancellationToken), FetchAllTimeSlicesAsync(string, List<string>, int, int, CancellationToken), FetchAllWaitStatsAsync(string, List<string>, DateTime, DateTime, int, CancellationToken) 3. QueryStoreOverviewControl.axaml — Layout with 3 rows: • Row 1: Donut chart + consolidated time slicer + consolidated wait stats ribbon • Row 2: 7 bar chart cards (Total metrics) • Row 3: 7 bar chart cards (Avg metrics) 4. QueryStoreOverviewControl.axaml.cs — Code-behind with: • Donut chart (RW=light blue, RO=dark blue, OFF=grey, center shows active/total) • Consolidated time slicer (30-day, 24h default selection) • Consolidated wait stats ribbon (sum across databases) • Top-N bar cards with consistent database colors, adaptive font color, tooltips, and right-click "Drill Down to DB Query Store" context menu Modified Files: 5. QuerySessionControl.axaml — Added "QS Overview" button 6. QuerySessionControl.axaml.cs — Added QueryStoreOverview_Click(object?, RoutedEventArgs) handler that opens the overview tab and wires drill-down to open single-DB Query Store tabs * 1. Drill-down with time range: DrillDownRequested now passes a DrillDownEventArgs containing Database, StartUtc, and EndUtc. The session control calls grid.SetInitialTimeRange() before the grid auto-fetches, so the drilled-down Query Store tab starts with the same time range selected in the overview. 2. Progress bar: Added an indeterminate ProgressBar at the top of the overview. It shows during LoadAsync() (all 3 phases) and during RefreshMetricsAndWaitStatsAsync(CancellationToken) (when the slicer range changes), and hides when complete via try/finally. * improve the dashboard * fix issue about waits stats on the QS overview * fix null on WTR in sql * add evg ref line in waitstats chart * 1. Dead code removed — Deleted FetchDatabaseWaitStatsAsync and DatabaseWaitCategoryTimeSlice (no callers). 2. Bare catch blocks fixed — All 4 parallel fetch methods now use when (ex is not OperationCanceledException) or when (!ct.IsCancellationRequested) so cancellation propagates correctly. 3. Permission errors surfaced — Added QueryStoreState.Error enum value and ErrorMessage property to DatabaseQueryStoreState. The donut now shows a red "Error" segment, and clicking it lists databases with their error messages. 4. _cts leak fixed — Added _cts?.Dispose() before every reassignment in LoadAsync() and OnSlicerRangeChanged(object?, TimeRangeChangedEventArgs), plus a DetachedFromVisualTree handler that cancels and disposes on control teardown. 5. SizeChanged race fixed — DrawWaitStatsChart() now returns early if _dbColorMap is empty, preventing all bars from being bucketed into "Others" when a resize fires before DrawBarCards() has run. 6. Misnamed field renamed — WaitRatio → WaitAmountHours on DatabaseWaitAmountTimeSlice and all references in the service and control. --------- Co-authored-by: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
1 parent 512ede4 commit c90bff7

7 files changed

Lines changed: 1491 additions & 0 deletions

src/PlanViewer.App/Controls/QuerySessionControl.axaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
Height="28" Padding="8,0" FontSize="12"
5858
Theme="{StaticResource AppButton}"
5959
ToolTip.Tip="Analyze top queries from Query Store"/>
60+
<Button x:Name="QueryStoreOverviewButton" Content="&#x1F4CA; QS Overview"
61+
Click="QueryStoreOverview_Click"
62+
Height="28" Padding="8,0" FontSize="12"
63+
Theme="{StaticResource AppButton}"
64+
ToolTip.Tip="Query Stores Overview across all databases"/>
6065
<TextBlock Text="|" VerticalAlignment="Center"
6166
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
6267
<Button x:Name="CopyReproButton" Content="&#x1F4CB; Copy Repro"

src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,161 @@ private bool HasQueryStoreTab()
13891389

13901390
public void TriggerQueryStore() => QueryStore_Click(null, new RoutedEventArgs());
13911391

1392+
private async void QueryStoreOverview_Click(object? sender, RoutedEventArgs e)
1393+
{
1394+
if (_serverConnection == null || _connectionString == null)
1395+
{
1396+
await ShowConnectionDialogAsync();
1397+
if (_serverConnection == null || _connectionString == null)
1398+
return;
1399+
}
1400+
1401+
SetStatus("Loading Query Store Overview...");
1402+
1403+
var supportsWaitStats = _serverMetadata?.SupportsQueryStoreWaitStats ?? false;
1404+
var overview = new QueryStoreOverviewControl(_serverConnection, _credentialService,
1405+
supportsWaitStats: supportsWaitStats);
1406+
overview.DrillDownRequested += async (_, args) =>
1407+
{
1408+
// Open a single-database Query Store tab directly (no connection dialog)
1409+
_selectedDatabase = args.Database;
1410+
_connectionString = _serverConnection!.GetConnectionString(_credentialService, args.Database);
1411+
await OpenQueryStoreForDatabaseAsync(args.Database, args.StartUtc, args.EndUtc);
1412+
};
1413+
1414+
var headerText = new TextBlock
1415+
{
1416+
Text = "QS Overview",
1417+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
1418+
FontSize = 12
1419+
};
1420+
1421+
var closeBtn = new Button
1422+
{
1423+
Content = "\u2715",
1424+
MinWidth = 22, MinHeight = 22, Width = 22, Height = 22,
1425+
Padding = new Avalonia.Thickness(0),
1426+
FontSize = 11,
1427+
Margin = new Avalonia.Thickness(6, 0, 0, 0),
1428+
Background = Brushes.Transparent,
1429+
BorderThickness = new Avalonia.Thickness(0),
1430+
Foreground = new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)),
1431+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
1432+
HorizontalContentAlignment = HorizontalAlignment.Center,
1433+
VerticalContentAlignment = VerticalAlignment.Center
1434+
};
1435+
1436+
var header = new StackPanel
1437+
{
1438+
Orientation = Avalonia.Layout.Orientation.Horizontal,
1439+
Children = { headerText, closeBtn }
1440+
};
1441+
1442+
var tab = new TabItem { Header = header, Content = overview };
1443+
closeBtn.Tag = tab;
1444+
closeBtn.Click += (s, _) =>
1445+
{
1446+
if (s is Button btn && btn.Tag is TabItem t)
1447+
SubTabControl.Items.Remove(t);
1448+
};
1449+
1450+
SubTabControl.Items.Add(tab);
1451+
SubTabControl.SelectedItem = tab;
1452+
1453+
try
1454+
{
1455+
await overview.LoadAsync();
1456+
SetStatus("");
1457+
}
1458+
catch (Exception ex)
1459+
{
1460+
SetStatus(ex.Message.Length > 80 ? ex.Message[..80] + "..." : ex.Message, autoClear: false);
1461+
}
1462+
}
1463+
1464+
private async Task OpenQueryStoreForDatabaseAsync(string database, DateTime? initialStartUtc = null, DateTime? initialEndUtc = null)
1465+
{
1466+
var connStr = _serverConnection!.GetConnectionString(_credentialService, database);
1467+
1468+
// Check if Query Store is enabled
1469+
SetStatus($"Checking Query Store on {database}...");
1470+
try
1471+
{
1472+
var (enabled, state) = await QueryStoreService.CheckEnabledAsync(connStr);
1473+
if (!enabled)
1474+
{
1475+
SetStatus($"Query Store not enabled on {database} ({state ?? "unknown"})");
1476+
return;
1477+
}
1478+
}
1479+
catch (Exception ex)
1480+
{
1481+
SetStatus(ex.Message.Length > 80 ? ex.Message[..80] + "..." : ex.Message, autoClear: false);
1482+
return;
1483+
}
1484+
1485+
SetStatus("");
1486+
1487+
// Check if wait stats are supported
1488+
var supportsWaitStats = _serverMetadata?.SupportsQueryStoreWaitStats ?? false;
1489+
if (supportsWaitStats)
1490+
{
1491+
try
1492+
{
1493+
supportsWaitStats = await QueryStoreService.IsWaitStatsCaptureEnabledAsync(connStr);
1494+
}
1495+
catch { supportsWaitStats = false; }
1496+
}
1497+
1498+
var databases = DatabaseBox.Items.OfType<string>().ToList();
1499+
1500+
var grid = new QueryStoreGridControl(_serverConnection!, _credentialService,
1501+
database, databases, supportsWaitStats);
1502+
if (initialStartUtc.HasValue && initialEndUtc.HasValue)
1503+
grid.SetInitialTimeRange(initialStartUtc.Value, initialEndUtc.Value);
1504+
grid.PlansSelected += OnQueryStorePlansSelected;
1505+
1506+
var headerText = new TextBlock
1507+
{
1508+
Text = $"Query Store — {database}",
1509+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
1510+
FontSize = 12
1511+
};
1512+
grid.DatabaseChanged += (_, db) => headerText.Text = $"Query Store — {db}";
1513+
1514+
var closeBtn = new Button
1515+
{
1516+
Content = "\u2715",
1517+
MinWidth = 22, MinHeight = 22, Width = 22, Height = 22,
1518+
Padding = new Avalonia.Thickness(0),
1519+
FontSize = 11,
1520+
Margin = new Avalonia.Thickness(6, 0, 0, 0),
1521+
Background = Brushes.Transparent,
1522+
BorderThickness = new Avalonia.Thickness(0),
1523+
Foreground = new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)),
1524+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
1525+
HorizontalContentAlignment = HorizontalAlignment.Center,
1526+
VerticalContentAlignment = VerticalAlignment.Center
1527+
};
1528+
1529+
var header = new StackPanel
1530+
{
1531+
Orientation = Avalonia.Layout.Orientation.Horizontal,
1532+
Children = { headerText, closeBtn }
1533+
};
1534+
1535+
var tab = new TabItem { Header = header, Content = grid };
1536+
closeBtn.Tag = tab;
1537+
closeBtn.Click += (s, _) =>
1538+
{
1539+
if (s is Button btn && btn.Tag is TabItem t)
1540+
SubTabControl.Items.Remove(t);
1541+
};
1542+
1543+
SubTabControl.Items.Add(tab);
1544+
SubTabControl.SelectedItem = tab;
1545+
}
1546+
13921547
private async void QueryStore_Click(object? sender, RoutedEventArgs e)
13931548
{
13941549
// If a QS tab already exists, always show connection dialog for a fresh tab

src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ public QueryStoreGridControl(ServerConnection serverConnection, ICredentialServi
9393
}, Avalonia.Threading.DispatcherPriority.Loaded);
9494
}
9595

96+
/// <summary>
97+
/// Sets the initial slicer time range (e.g. from overview drill-down).
98+
/// Must be called before the control is loaded to take effect on the first fetch.
99+
/// </summary>
100+
public void SetInitialTimeRange(DateTime startUtc, DateTime endUtc)
101+
{
102+
_slicerStartUtc = startUtc;
103+
_slicerEndUtc = endUtc;
104+
}
105+
96106
private void PopulateDatabaseBox(List<string> databases, string selectedDatabase)
97107
{
98108
QsDatabaseBox.ItemsSource = databases;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:controls="using:PlanViewer.App.Controls"
4+
x:Class="PlanViewer.App.Controls.QueryStoreOverviewControl">
5+
<Grid x:Name="RootGrid" Background="{DynamicResource BackgroundBrush}">
6+
<Grid.RowDefinitions>
7+
<RowDefinition Height="Auto"/>
8+
<RowDefinition Height="1*"/>
9+
<RowDefinition Height="2*"/>
10+
<RowDefinition Height="2*"/>
11+
</Grid.RowDefinitions>
12+
13+
<!-- Row 0: Progress bar (always visible to reserve space, toggle IsIndeterminate) -->
14+
<ProgressBar x:Name="LoadingBar" Grid.Row="0" IsIndeterminate="False"
15+
Height="3" Margin="0"
16+
Foreground="{DynamicResource AccentBrush}"/>
17+
18+
<!-- Row 1: Donut + TimeSlicer + WaitStats -->
19+
<Grid Grid.Row="1" Margin="10">
20+
<Grid.ColumnDefinitions>
21+
<ColumnDefinition Width="0.6*"/>
22+
<ColumnDefinition Width="2*"/>
23+
<ColumnDefinition Width="2*"/>
24+
</Grid.ColumnDefinitions>
25+
26+
<!-- Donut chart area -->
27+
<Border Grid.Column="0" Background="{DynamicResource BackgroundLightBrush}"
28+
CornerRadius="4" Margin="0,0,5,0" ClipToBounds="True">
29+
<Grid RowDefinitions="Auto,*">
30+
<TextBlock Grid.Row="0" Text="QS States" FontSize="11" FontWeight="SemiBold"
31+
Foreground="{DynamicResource ForegroundBrush}"
32+
HorizontalAlignment="Center" Margin="0,4,0,0"/>
33+
<Canvas x:Name="DonutCanvas" Grid.Row="1"/>
34+
</Grid>
35+
</Border>
36+
37+
<!-- Time slicer (consolidated) - reuses the full TimeRangeSlicerControl -->
38+
<controls:TimeRangeSlicerControl x:Name="OverviewTimeSlicer"
39+
Grid.Column="1" Margin="5,0,5,0"
40+
VerticalAlignment="Stretch"/>
41+
42+
<!-- Wait stats ribbon (consolidated by database) -->
43+
<Border Grid.Column="2" Background="{DynamicResource BackgroundLightBrush}"
44+
CornerRadius="4" Margin="5,0,0,0" ClipToBounds="True">
45+
<Grid RowDefinitions="Auto,*">
46+
<TextBlock Grid.Row="0" Text="Wait Stats by Database" FontSize="11" FontWeight="SemiBold"
47+
Foreground="{DynamicResource ForegroundBrush}"
48+
HorizontalAlignment="Center" Margin="0,4,0,0"/>
49+
<Border x:Name="WaitStatsBorder" Grid.Row="1" ClipToBounds="True">
50+
<Canvas x:Name="WaitStatsCanvas" Background="Transparent"/>
51+
</Border>
52+
</Grid>
53+
</Border>
54+
</Grid>
55+
56+
<!-- Row 1: Total metrics bar cards -->
57+
<Grid x:Name="TotalMetricsGrid" Grid.Row="2" Margin="10"/>
58+
59+
<!-- Row 2: Avg metrics bar cards -->
60+
<Grid x:Name="AvgMetricsGrid" Grid.Row="3" Margin="10"/>
61+
</Grid>
62+
</UserControl>

0 commit comments

Comments
 (0)