Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/PlanViewer.App/Controls/QueryStoreGridControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
xmlns:local="using:PlanViewer.App.Controls"
x:Class="PlanViewer.App.Controls.QueryStoreGridControl"
Background="{DynamicResource BackgroundBrush}">
<Grid RowDefinitions="Auto,*">
<Grid RowDefinitions="Auto,Auto,*">
<!-- Time Range Slicer -->
<local:TimeRangeSlicerControl x:Name="TimeRangeSlicer" Grid.Row="0"/>

<!-- Toolbar -->
<Border Grid.Row="0" Background="{DynamicResource BackgroundDarkBrush}" Padding="8,6"
<Border Grid.Row="1" Background="{DynamicResource BackgroundDarkBrush}" Padding="8,6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" Spacing="12">
<StackPanel Spacing="4">
Expand All @@ -27,16 +30,10 @@
Width="120" Height="36" FontSize="14" FormatString="0"
HorizontalContentAlignment="Center"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Hours back" Foreground="{DynamicResource ForegroundBrush}" FontSize="11"/>
<NumericUpDown x:Name="HoursBackBox" Value="24" Minimum="1" Maximum="168"
Width="120" Height="36" FontSize="14" FormatString="0"
HorizontalContentAlignment="Center"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Order by" Foreground="{DynamicResource ForegroundBrush}" FontSize="11"/>
<ComboBox x:Name="OrderByBox" Width="160" Height="36" FontSize="13"
SelectedIndex="0">
SelectedIndex="0" SelectionChanged="OrderBy_SelectionChanged">
<ComboBoxItem Content="Total CPU" Tag="cpu"/>
<ComboBoxItem Content="Avg CPU" Tag="avg-cpu"/>
<ComboBoxItem Content="Total Duration" Tag="duration"/>
Expand Down Expand Up @@ -86,7 +83,7 @@
</Border>

<!-- DataGrid -->
<DataGrid Grid.Row="1" x:Name="ResultsGrid"
<DataGrid Grid.Row="2" x:Name="ResultsGrid"
AutoGenerateColumns="False"
CanUserSortColumns="True"
CanUserReorderColumns="True"
Expand Down
148 changes: 138 additions & 10 deletions src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using PlanViewer.Core.Interfaces;
using PlanViewer.Core.Models;
using PlanViewer.App.Dialogs;
using PlanViewer.App.Services;
using PlanViewer.Core.Services;

namespace PlanViewer.App.Controls;
Expand All @@ -32,6 +33,12 @@ public partial class QueryStoreGridControl : UserControl
private ColumnFilterPopup? _filterPopupContent;
private string? _sortedColumnTag;
private bool _sortAscending;
private DateTime? _slicerStartUtc;
private DateTime? _slicerEndUtc;
private int _slicerDaysBack = 30;
private string _lastFetchedOrderBy = "cpu";
private bool _initialOrderByLoaded;
private bool _suppressRangeChanged;

public event EventHandler<List<QueryStorePlan>>? PlansSelected;
public event EventHandler<string>? DatabaseChanged;
Expand All @@ -45,11 +52,21 @@ public QueryStoreGridControl(ServerConnection serverConnection, ICredentialServi
_credentialService = credentialService;
_database = initialDatabase;
_connectionString = serverConnection.GetConnectionString(credentialService, initialDatabase);
_slicerDaysBack = AppSettingsService.Load().QueryStoreSlicerDays;
InitializeComponent();
ResultsGrid.ItemsSource = _filteredRows;
EnsureFilterPopup();
SetupColumnHeaders();
PopulateDatabaseBox(databases, initialDatabase);
TimeRangeSlicer.RangeChanged += OnTimeRangeChanged;
TimeRangeSlicer.IsExpanded = true;

// Auto-fetch with default settings on connect
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
Fetch_Click(null, new RoutedEventArgs());
_initialOrderByLoaded = true;
}, Avalonia.Threading.DispatcherPriority.Loaded);
}

private void PopulateDatabaseBox(List<string> databases, string selectedDatabase)
Expand Down Expand Up @@ -101,25 +118,61 @@ private async void Fetch_Click(object? sender, RoutedEventArgs e)
_fetchCts = new CancellationTokenSource();
var ct = _fetchCts.Token;

var topN = (int)(TopNBox.Value ?? 25);
var hoursBack = (int)(HoursBackBox.Value ?? 24);
var orderBy = (OrderByBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "cpu";
_lastFetchedOrderBy = orderBy;

FetchButton.IsEnabled = false;
LoadButton.IsEnabled = false;
StatusText.Text = "Loading time slicer...";
_rows.Clear();
_filteredRows.Clear();

try
{
// Load slicer data first — LoadData sets a default 24h selection and
// fires RangeChanged which triggers FetchPlansForRangeAsync.
await LoadTimeSlicerDataAsync(orderBy, ct);
}
catch (OperationCanceledException)
{
StatusText.Text = "Cancelled.";
}
catch (Exception ex)
{
StatusText.Text = ex.Message.Length > 80 ? ex.Message[..80] + "..." : ex.Message;
}
finally
{
FetchButton.IsEnabled = true;
}
}

private async System.Threading.Tasks.Task FetchPlansForRangeAsync()
{
_fetchCts?.Cancel();
_fetchCts?.Dispose();
_fetchCts = new CancellationTokenSource();
var ct = _fetchCts.Token;

var topN = (int)(TopNBox.Value ?? 25);
var orderBy = _lastFetchedOrderBy;
var filter = BuildSearchFilter();

FetchButton.IsEnabled = false;
LoadButton.IsEnabled = false;
StatusText.Text = "Fetching...";
StatusText.Text = "Fetching plans...";
_rows.Clear();
_filteredRows.Clear();

try
{
var plans = await QueryStoreService.FetchTopPlansAsync(
_connectionString, topN, orderBy, hoursBack, filter, ct);
_connectionString, topN, orderBy, ct: ct,
startUtc: _slicerStartUtc, endUtc: _slicerEndUtc);
Comment on lines +157 to +171
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pass the built search filter into the range fetch.

BuildSearchFilter() still runs on Line 159, but FetchTopPlansAsync is called without filter: on Lines 169-171. After this refactor, the Search by controls no longer affect the fetched results.

Suggested fix
             var plans = await QueryStoreService.FetchTopPlansAsync(
-                _connectionString, topN, orderBy, ct: ct,
+                _connectionString, topN, orderBy, filter: filter, ct: ct,
                 startUtc: _slicerStartUtc, endUtc: _slicerEndUtc);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs` around lines 157
- 171, The code builds a search filter via BuildSearchFilter() but never passes
it into QueryStoreService.FetchTopPlansAsync, so search controls don't affect
results; update the FetchTopPlansAsync call in QueryStoreGridControl.axaml.cs to
include the filter parameter (e.g., pass filter: filter) alongside the existing
args (connectionString/_connectionString, topN, orderBy/_lastFetchedOrderBy, ct,
startUtc/_slicerStartUtc, endUtc/_slicerEndUtc) so the built filter is applied
when fetching plans.


if (plans.Count == 0)
{
StatusText.Text = "No Query Store data found.";
StatusText.Text = "No Query Store data found for the selected range.";
return;
}

Expand Down Expand Up @@ -194,12 +247,91 @@ private void SearchValue_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e)
}
}

private async void OrderBy_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (!_initialOrderByLoaded) return;
var newOrderBy = (OrderByBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "cpu";
if (newOrderBy == _lastFetchedOrderBy) return;

_lastFetchedOrderBy = newOrderBy;

_fetchCts?.Cancel();
_fetchCts?.Dispose();
_fetchCts = new CancellationTokenSource();
var ct = _fetchCts.Token;

// Capture the current slicer selection so it survives the reload
var selStart = TimeRangeSlicer.SelectionStart;
var selEnd = TimeRangeSlicer.SelectionEnd;

FetchButton.IsEnabled = false;
StatusText.Text = "Refreshing metric...";

try
{
var sliceData = await QueryStoreService.FetchTimeSliceDataAsync(
_connectionString, newOrderBy, _slicerDaysBack, ct);
if (ct.IsCancellationRequested) return;

if (sliceData.Count > 0)
{
// Suppress the implicit RangeChanged fetch — we will refresh the grid explicitly below
_suppressRangeChanged = true;
try { TimeRangeSlicer.LoadData(sliceData, newOrderBy, selStart, selEnd); }
finally { _suppressRangeChanged = false; }

// Explicitly refresh the grid with the new metric and current time range
await FetchPlansForRangeAsync();
}
else
{
StatusText.Text = "No time-slicer data available.";
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
StatusText.Text = ex.Message.Length > 80 ? ex.Message[..80] + "..." : ex.Message;
}
finally
{
FetchButton.IsEnabled = true;
}
}

private void ClearSearch_Click(object? sender, RoutedEventArgs e)
{
SearchTypeBox.SelectedIndex = 0;
SearchValueBox.Text = "";
}

private async System.Threading.Tasks.Task LoadTimeSlicerDataAsync(string metric, CancellationToken ct)
{
try
{
var sliceData = await QueryStoreService.FetchTimeSliceDataAsync(
_connectionString, metric, _slicerDaysBack, ct);
if (ct.IsCancellationRequested) return;
if (sliceData.Count > 0)
TimeRangeSlicer.LoadData(sliceData, metric);
Comment on lines +308 to +316
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the current range when refreshing slicer data.

This path always calls TimeRangeSlicer.LoadData(sliceData, metric) with no selection bounds, so clicking Fetch after narrowing the range jumps back to the default last 24 hours. Refreshing the metric data should preserve TimeRangeSlicer.SelectionStart / TimeRangeSlicer.SelectionEnd.

Suggested fix
     private async System.Threading.Tasks.Task LoadTimeSlicerDataAsync(string metric, CancellationToken ct)
     {
         try
         {
+            var selectionStart = TimeRangeSlicer.SelectionStart;
+            var selectionEnd = TimeRangeSlicer.SelectionEnd;
             var sliceData = await QueryStoreService.FetchTimeSliceDataAsync(
                 _connectionString, metric, _slicerDaysBack, ct);
             if (ct.IsCancellationRequested) return;
             if (sliceData.Count > 0)
-                TimeRangeSlicer.LoadData(sliceData, metric);
+                TimeRangeSlicer.LoadData(sliceData, metric, selectionStart, selectionEnd);
             else
                 StatusText.Text = "No time-slicer data available.";
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs` around lines 308
- 316, LoadTimeSlicerDataAsync currently calls
TimeRangeSlicer.LoadData(sliceData, metric) and thus resets the slicer to
defaults; before calling LoadData capture the current
TimeRangeSlicer.SelectionStart and SelectionEnd (or a flag whether a custom
selection exists), then call LoadData(sliceData, metric) and restore the
captured SelectionStart/SelectionEnd back onto TimeRangeSlicer (or call an
overload of LoadData that accepts start/end if available). Ensure you still
honor CancellationToken ct (check ct.IsCancellationRequested before restoring)
and use the existing method names LoadTimeSlicerDataAsync,
TimeRangeSlicer.LoadData, TimeRangeSlicer.SelectionStart, and
TimeRangeSlicer.SelectionEnd to locate where to add the capture/restore logic.

else
StatusText.Text = "No time-slicer data available.";
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
StatusText.Text = $"Slicer: {(ex.Message.Length > 60 ? ex.Message[..60] + "..." : ex.Message)}";
}
}

private async void OnTimeRangeChanged(object? sender, TimeRangeChangedEventArgs e)
{
_slicerStartUtc = e.StartUtc;
_slicerEndUtc = e.EndUtc;
if (_suppressRangeChanged) return;
await FetchPlansForRangeAsync();
}

private void SelectToggle_Click(object? sender, RoutedEventArgs e)
{
var allSelected = _filteredRows.Count > 0 && _filteredRows.All(r => r.IsSelected);
Expand All @@ -225,14 +357,11 @@ private async void ViewHistory_Click(object? sender, RoutedEventArgs e)
{
if (ResultsGrid.SelectedItem is not QueryStoreRow row) return;

var hoursBack = (int)(HoursBackBox.Value ?? 24);

var window = new QueryStoreHistoryWindow(
_connectionString,
row.QueryId,
row.FullQueryText,
_database,
hoursBack);
_database);

var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(this);
if (topLevel is Window parentWindow)
Expand Down Expand Up @@ -588,7 +717,6 @@ private void ApplySortAndFilters()
: source.OrderByDescending(r => GetSortKey(_sortedColumnTag, r));
}


_filteredRows.Clear();
foreach (var row in source)
_filteredRows.Add(row);
Expand Down
41 changes: 41 additions & 0 deletions src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PlanViewer.App.Controls.TimeRangeSlicerControl">
<Border Background="{DynamicResource SlicerBackgroundBrush}"
BorderBrush="{DynamicResource SlicerBorderBrush}"
BorderThickness="0,0,0,1">
<Grid RowDefinitions="Auto,Auto">
<!-- Toggle header -->
<Button x:Name="ToggleButton" Grid.Row="0"
Background="Transparent" BorderThickness="0"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="8,4" Cursor="Hand"
Click="Toggle_Click">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Name="ToggleIcon" Text="▾" FontSize="12"
Foreground="{DynamicResource SlicerToggleBrush}"
VerticalAlignment="Center"/>
<TextBlock x:Name="ToggleLabel" Text="Time Range"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource SlicerToggleBrush}"
VerticalAlignment="Center"/>
<TextBlock x:Name="RangeLabel" Text=""
FontSize="11"
Foreground="{DynamicResource SlicerLabelBrush}"
VerticalAlignment="Center" Margin="12,0,0,0"/>
</StackPanel>
</Button>
<!-- Slicer canvas area -->
<Border x:Name="SlicerBorder" Grid.Row="1" Height="120" Margin="8,0,8,6"
Background="{DynamicResource SlicerBackgroundBrush}"
CornerRadius="4" ClipToBounds="True">
<Canvas x:Name="SlicerCanvas"
Background="Transparent"
PointerPressed="Canvas_PointerPressed"
PointerMoved="Canvas_PointerMoved"
PointerReleased="Canvas_PointerReleased"
PointerWheelChanged="Canvas_PointerWheelChanged"/>
</Border>
</Grid>
</Border>
</UserControl>
Loading
Loading