-
Notifications
You must be signed in to change notification settings - Fork 30
Feature/query store timeslicer #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7030575
9ae1a02
d68a546
85853e8
102b5c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
@@ -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) | ||
|
|
@@ -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); | ||
|
|
||
| if (plans.Count == 0) | ||
| { | ||
| StatusText.Text = "No Query Store data found."; | ||
| StatusText.Text = "No Query Store data found for the selected range."; | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the current range when refreshing slicer data. This path always calls 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 |
||
| 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); | ||
|
|
@@ -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) | ||
|
|
@@ -588,7 +717,6 @@ private void ApplySortAndFilters() | |
| : source.OrderByDescending(r => GetSortKey(_sortedColumnTag, r)); | ||
| } | ||
|
|
||
|
|
||
| _filteredRows.Clear(); | ||
| foreach (var row in source) | ||
| _filteredRows.Add(row); | ||
|
|
||
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass the built search filter into the range fetch.
BuildSearchFilter()still runs on Line 159, butFetchTopPlansAsyncis called withoutfilter:on Lines 169-171. After this refactor, theSearch bycontrols no longer affect the fetched results.Suggested fix
🤖 Prompt for AI Agents