Skip to content
5 changes: 5 additions & 0 deletions src/PlanViewer.App/Controls/QuerySessionControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
Height="28" Padding="8,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Analyze top queries from Query Store"/>
<Button x:Name="QueryStoreOverviewButton" Content="&#x1F4CA; QS Overview"
Click="QueryStoreOverview_Click"
Height="28" Padding="8,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Query Stores Overview across all databases"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button x:Name="CopyReproButton" Content="&#x1F4CB; Copy Repro"
Expand Down
155 changes: 155 additions & 0 deletions src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,161 @@ private bool HasQueryStoreTab()

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

private async void QueryStoreOverview_Click(object? sender, RoutedEventArgs e)
{
if (_serverConnection == null || _connectionString == null)
{
await ShowConnectionDialogAsync();
if (_serverConnection == null || _connectionString == null)
return;
}

SetStatus("Loading Query Store Overview...");

var supportsWaitStats = _serverMetadata?.SupportsQueryStoreWaitStats ?? false;
var overview = new QueryStoreOverviewControl(_serverConnection, _credentialService,
supportsWaitStats: supportsWaitStats);
overview.DrillDownRequested += async (_, args) =>
{
// Open a single-database Query Store tab directly (no connection dialog)
_selectedDatabase = args.Database;
_connectionString = _serverConnection!.GetConnectionString(_credentialService, args.Database);
await OpenQueryStoreForDatabaseAsync(args.Database, args.StartUtc, args.EndUtc);
};

var headerText = new TextBlock
{
Text = "QS Overview",
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
FontSize = 12
};

var closeBtn = new Button
{
Content = "\u2715",
MinWidth = 22, MinHeight = 22, Width = 22, Height = 22,
Padding = new Avalonia.Thickness(0),
FontSize = 11,
Margin = new Avalonia.Thickness(6, 0, 0, 0),
Background = Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(0),
Foreground = new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
};

var header = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { headerText, closeBtn }
};

var tab = new TabItem { Header = header, Content = overview };
closeBtn.Tag = tab;
closeBtn.Click += (s, _) =>
{
if (s is Button btn && btn.Tag is TabItem t)
SubTabControl.Items.Remove(t);
};

SubTabControl.Items.Add(tab);
SubTabControl.SelectedItem = tab;

try
{
await overview.LoadAsync();
SetStatus("");
}
catch (Exception ex)
{
SetStatus(ex.Message.Length > 80 ? ex.Message[..80] + "..." : ex.Message, autoClear: false);
}
}

private async Task OpenQueryStoreForDatabaseAsync(string database, DateTime? initialStartUtc = null, DateTime? initialEndUtc = null)
{
var connStr = _serverConnection!.GetConnectionString(_credentialService, database);

// Check if Query Store is enabled
SetStatus($"Checking Query Store on {database}...");
try
{
var (enabled, state) = await QueryStoreService.CheckEnabledAsync(connStr);
if (!enabled)
{
SetStatus($"Query Store not enabled on {database} ({state ?? "unknown"})");
return;
}
}
catch (Exception ex)
{
SetStatus(ex.Message.Length > 80 ? ex.Message[..80] + "..." : ex.Message, autoClear: false);
return;
}

SetStatus("");

// Check if wait stats are supported
var supportsWaitStats = _serverMetadata?.SupportsQueryStoreWaitStats ?? false;
if (supportsWaitStats)
{
try
{
supportsWaitStats = await QueryStoreService.IsWaitStatsCaptureEnabledAsync(connStr);
}
catch { supportsWaitStats = false; }
}

var databases = DatabaseBox.Items.OfType<string>().ToList();

var grid = new QueryStoreGridControl(_serverConnection!, _credentialService,
database, databases, supportsWaitStats);
if (initialStartUtc.HasValue && initialEndUtc.HasValue)
grid.SetInitialTimeRange(initialStartUtc.Value, initialEndUtc.Value);
grid.PlansSelected += OnQueryStorePlansSelected;

var headerText = new TextBlock
{
Text = $"Query Store — {database}",
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
FontSize = 12
};
grid.DatabaseChanged += (_, db) => headerText.Text = $"Query Store — {db}";

var closeBtn = new Button
{
Content = "\u2715",
MinWidth = 22, MinHeight = 22, Width = 22, Height = 22,
Padding = new Avalonia.Thickness(0),
FontSize = 11,
Margin = new Avalonia.Thickness(6, 0, 0, 0),
Background = Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(0),
Foreground = new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
};

var header = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { headerText, closeBtn }
};

var tab = new TabItem { Header = header, Content = grid };
closeBtn.Tag = tab;
closeBtn.Click += (s, _) =>
{
if (s is Button btn && btn.Tag is TabItem t)
SubTabControl.Items.Remove(t);
};

SubTabControl.Items.Add(tab);
SubTabControl.SelectedItem = tab;
}

private async void QueryStore_Click(object? sender, RoutedEventArgs e)
{
// If a QS tab already exists, always show connection dialog for a fresh tab
Expand Down
10 changes: 10 additions & 0 deletions src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public QueryStoreGridControl(ServerConnection serverConnection, ICredentialServi
}, Avalonia.Threading.DispatcherPriority.Loaded);
}

/// <summary>
/// Sets the initial slicer time range (e.g. from overview drill-down).
/// Must be called before the control is loaded to take effect on the first fetch.
/// </summary>
public void SetInitialTimeRange(DateTime startUtc, DateTime endUtc)
{
_slicerStartUtc = startUtc;
_slicerEndUtc = endUtc;
}

private void PopulateDatabaseBox(List<string> databases, string selectedDatabase)
{
QsDatabaseBox.ItemsSource = databases;
Expand Down
62 changes: 62 additions & 0 deletions src/PlanViewer.App/Controls/QueryStoreOverviewControl.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PlanViewer.App.Controls"
x:Class="PlanViewer.App.Controls.QueryStoreOverviewControl">
<Grid x:Name="RootGrid" Background="{DynamicResource BackgroundBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="2*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>

<!-- Row 0: Progress bar (always visible to reserve space, toggle IsIndeterminate) -->
<ProgressBar x:Name="LoadingBar" Grid.Row="0" IsIndeterminate="False"
Height="3" Margin="0"
Foreground="{DynamicResource AccentBrush}"/>

<!-- Row 1: Donut + TimeSlicer + WaitStats -->
<Grid Grid.Row="1" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.6*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>

<!-- Donut chart area -->
<Border Grid.Column="0" Background="{DynamicResource BackgroundLightBrush}"
CornerRadius="4" Margin="0,0,5,0" ClipToBounds="True">
<Grid RowDefinitions="Auto,*">
<TextBlock Grid.Row="0" Text="QS States" FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource ForegroundBrush}"
HorizontalAlignment="Center" Margin="0,4,0,0"/>
<Canvas x:Name="DonutCanvas" Grid.Row="1"/>
</Grid>
</Border>

<!-- Time slicer (consolidated) - reuses the full TimeRangeSlicerControl -->
<controls:TimeRangeSlicerControl x:Name="OverviewTimeSlicer"
Grid.Column="1" Margin="5,0,5,0"
VerticalAlignment="Stretch"/>

<!-- Wait stats ribbon (consolidated by database) -->
<Border Grid.Column="2" Background="{DynamicResource BackgroundLightBrush}"
CornerRadius="4" Margin="5,0,0,0" ClipToBounds="True">
<Grid RowDefinitions="Auto,*">
<TextBlock Grid.Row="0" Text="Wait Stats by Database" FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource ForegroundBrush}"
HorizontalAlignment="Center" Margin="0,4,0,0"/>
<Border x:Name="WaitStatsBorder" Grid.Row="1" ClipToBounds="True">
<Canvas x:Name="WaitStatsCanvas" Background="Transparent"/>
</Border>
</Grid>
</Border>
</Grid>

<!-- Row 1: Total metrics bar cards -->
<Grid x:Name="TotalMetricsGrid" Grid.Row="2" Margin="10"/>

<!-- Row 2: Avg metrics bar cards -->
<Grid x:Name="AvgMetricsGrid" Grid.Row="3" Margin="10"/>
</Grid>
</UserControl>
Loading
Loading