Skip to content

Feature/query store timeslicer#114

Merged
erikdarlingdata merged 5 commits intoerikdarlingdata:devfrom
rferraton:feature/query-store-timeslicer
Mar 19, 2026
Merged

Feature/query store timeslicer#114
erikdarlingdata merged 5 commits intoerikdarlingdata:devfrom
rferraton:feature/query-store-timeslicer

Conversation

@rferraton
Copy link
Contributor

@rferraton rferraton commented Mar 19, 2026

What does this PR do?

  • QueryStore : Add a time range filter in the query store with the selected sorting metric as background of the time range graphic selector
    • Filtering first on last 24h (on opening grid)
    • filtering using interval start and end with the lower and upper bound of the periode selected
  • QueryStore : Autoload grid on openning
  • QueryStore : re-Autoload grid and RangeTimeSlicer on changing sort metric

Which component(s) does this affect?

  • Desktop App (PlanViewer.App)
  • Core Library (PlanViewer.Core)
  • CLI Tool (PlanViewer.Cli)
  • SSMS Extension (PlanViewer.Ssms)
  • Tests
  • Documentation

How was this tested?

2026-03-19_22h42_55

I have give a look on generated query and add filter that (i hope) would limit impact on QS with long history

Checklist

  • I have read the contributing guide
  • My code builds with zero warnings (dotnet build -c Debug)
  • All tests pass (dotnet test)
  • I have not introduced any hardcoded credentials or server names

Summary by CodeRabbit

  • New Features
    • Added interactive time-range slicer with visual chart-based date range selection, replacing the previous "hours back" input.
    • Slicer supports pointer-based dragging and zoom controls for precise time range adjustments.
    • Chart metric data updates dynamically when changing order-by selections.
    • Configurable default slicer period in settings (default: 30 days).

…tric display in background of the range is base on the sorting metric in the drop down selector

Auto fetch when opening the query store
- Fix selection bugs : fetch is relaunch when moving
- Selection default selection on top 25 query limited to 24h using Rangetime which display the time selection
- number of hours selected in the Range selector is displayed in the top left of the Rangetime selector
Add SelectionChanged handler for "Order by" ComboBox to allow users to switch metrics and refresh data without losing time range selection.
Preserve slicer selection across metric changes and suppress redundant fetches.
Remove row limit from time-slice SQL query to ensure all data is available for the slicer : using where filtering only
… instead of query_store_runtime_stats.last_execution_time only
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

This PR introduces an interactive time range slicer component to the Query Store view, allowing users to visualize and select time ranges from aggregated metrics. It refactors the plan-fetching logic to use date-range parameters, adds hourly metric aggregation queries to the service layer, and integrates the new slicer into the existing QueryStoreGridControl.

Changes

Cohort / File(s) Summary
Time Range Slicer Control
src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml, src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml.cs
New interactive user control featuring metric chart visualization, pointer-based range selection with drag/zoom interactions, and public RangeChanged event emitting UTC start/end boundaries.
Query Store Grid Integration
src/PlanViewer.App/Controls/QueryStoreGridControl.axaml, src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
Restructured grid layout to embed TimeRangeSlicerControl, removed legacy hours-back input, added order-by selection event handling, and refactored fetch logic to orchestrate slicer state with date-range-based queries and cached metric metrics.
Core Models & Services
src/PlanViewer.Core/Models/QueryStoreTimeSlice.cs, src/PlanViewer.Core/Services/QueryStoreService.cs
Added QueryStoreTimeSlice model for hourly aggregated metrics; extended FetchTopPlansAsync with optional date-range filtering via startUtc/endUtc parameters and added new FetchTimeSliceDataAsync method for hourly metric bucket retrieval.
Theme & Settings
src/PlanViewer.App/Themes/DarkTheme.axaml, src/PlanViewer.App/Services/AppSettingsService.cs
Added ten slicer-specific brush resources for theming (background, chart, overlay, handle states, border, labels); extended AppSettings with QueryStoreSlicerDays configuration property (default 30 days).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #98: Modifies QueryStoreGridControl's ViewHistory_Click method and integrates time-slicer interactions with Query Store history fetching behavior.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/query store timeslicer' is directly related to the main change—adding a time range slicer/filter control to the Query Store UI with metric visualization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@rferraton
Copy link
Contributor Author

Nota : No effort on UTC conversion for now. all is in UTC (Range linechart, data, filter...).

I would propose to create tools settings to adjust that later with user preferences but i would like to avoid headache this evening

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs`:
- Around line 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.
- Around line 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.

In `@src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml.cs`:
- Around line 138-165: The avg-* branches are incorrectly returning Total*
values and labels; add per-bucket average properties to the QueryStoreTimeSlice
model (e.g., AvgCpu, AvgDuration, AvgReads, AvgWrites, AvgPhysicalReads,
AvgMemory) and populate them in QueryStoreService.FetchTimeSliceDataAsync so
each time-slice contains both Total* and Avg* fields, then update
TimeRangeSlicerControl.GetMetricValues to return the corresponding Avg*
properties for "avg-*" cases (e.g., "avg-cpu" => _data.Select(d =>
d.AvgCpu).ToArray()) and update GetMetricLabel to use "Avg ..." labels for those
branches (e.g., "avg-cpu" => "Avg CPU (ms)"); ensure numeric types match (cast
ints to double where needed) and leave non-avg branches using Total* as before.
- Around line 189-198: The chart currently spaces points by array index which
collapses gaps because FetchTimeSliceDataAsync(...) returns only hours with
data; instead compute each point's X from its actual timestamp (use the bucket's
IntervalStartUtc) mapped into the control's time span (same mapping
GetDateTimeAtNorm uses) so plotted points align with wall-clock time; in
TimeRangeSlicerControl.axaml.cs modify the loop that builds linePoints to read
the timestamps corresponding to values (or the returned bucket objects) and
calculate x = (bucket.IntervalStartUtc - rangeStart).TotalSeconds /
rangeDuration.TotalSeconds * w (or equivalent normalized mapping) before adding
new Point(x,y), or alternatively pre-fill zero-valued hourly buckets in the data
returned by FetchTimeSliceDataAsync to ensure dense hourly spacing—choose the
timestamp-based X computation for correctness.

In `@src/PlanViewer.Core/Services/QueryStoreService.cs`:
- Line 325: The filter using rs.first_execution_time with DATEADD(HOUR,
-@hoursBack, GETUTCDATE()) wrongly excludes query-store rows that had an earlier
first_execution_time but later executions inside the window; modify the
predicate in the QueryStoreService SQL (references to rs.first_execution_time,
`@hoursBack`, DATEADD/GETUTCDATE()) to use an overlap-safe column such as
rs.last_execution_time or remove the runtime_stats-side filter after measuring;
update the same pattern at the other occurrences noted (the similar predicates
around lines referenced as 392-393) so the history view and slicer are not
undercounted.
- Around line 134-138: The UTC window currently excludes buckets that end
exactly on the boundary because QueryStoreService builds timeWhereClause with
"rsi.end_time < `@rangeEnd`"; change the predicate so the upper boundary is
inclusive (e.g., use rsi.end_time <= `@rangeEnd`) or alternatively compare the
bucket start to the end boundary (e.g., use rsi.start_time < `@rangeEnd`) to match
TimeRangeSlicerControl semantics; update the timeWhereClause string accordingly
where it is set and leave the `@rangeStart/`@rangeEnd SqlParameter additions
intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1afa25e6-53b9-4b20-a76a-4e26a01f7e65

📥 Commits

Reviewing files that changed from the base of the PR and between 78e3e98 and 102b5c4.

📒 Files selected for processing (8)
  • src/PlanViewer.App/Controls/QueryStoreGridControl.axaml
  • src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
  • src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml
  • src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml.cs
  • src/PlanViewer.App/Services/AppSettingsService.cs
  • src/PlanViewer.App/Themes/DarkTheme.axaml
  • src/PlanViewer.Core/Models/QueryStoreTimeSlice.cs
  • src/PlanViewer.Core/Services/QueryStoreService.cs

Comment on lines +157 to +171
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);
Copy link

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.

Comment on lines +308 to +316
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);
Copy link

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.

Comment on lines +138 to +165
private double[] GetMetricValues()
{
return _metric switch
{
"cpu" or "avg-cpu" => _data.Select(d => d.TotalCpu).ToArray(),
"duration" or "avg-duration" => _data.Select(d => d.TotalDuration).ToArray(),
"reads" or "avg-reads" => _data.Select(d => d.TotalReads).ToArray(),
"writes" or "avg-writes" => _data.Select(d => d.TotalWrites).ToArray(),
"physical-reads" or "avg-physical-reads" => _data.Select(d => d.TotalPhysicalReads).ToArray(),
"memory" or "avg-memory" => _data.Select(d => d.TotalMemory).ToArray(),
"executions" => _data.Select(d => (double)d.TotalExecutions).ToArray(),
_ => _data.Select(d => d.TotalCpu).ToArray(),
};
}

private string GetMetricLabel()
{
return _metric switch
{
"cpu" or "avg-cpu" => "Total CPU (ms)",
"duration" or "avg-duration" => "Total Duration (ms)",
"reads" or "avg-reads" => "Total Reads",
"writes" or "avg-writes" => "Total Writes",
"physical-reads" or "avg-physical-reads" => "Total Physical Reads",
"memory" or "avg-memory" => "Total Memory (MB)",
"executions" => "Executions",
_ => "Total CPU (ms)",
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

avg-* modes are still rendering total-series data.

All avg-* branches read the same Total* fields, and the labels still say Total .... When the grid switches to Avg CPU, Avg Duration, and the other average modes, the slicer background stops matching the selected metric. Add per-bucket averages to src/PlanViewer.Core/Models/QueryStoreTimeSlice.cs / src/PlanViewer.Core/Services/QueryStoreService.cs::FetchTimeSliceDataAsync(...) and switch these cases to those values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml.cs` around lines 138
- 165, The avg-* branches are incorrectly returning Total* values and labels;
add per-bucket average properties to the QueryStoreTimeSlice model (e.g.,
AvgCpu, AvgDuration, AvgReads, AvgWrites, AvgPhysicalReads, AvgMemory) and
populate them in QueryStoreService.FetchTimeSliceDataAsync so each time-slice
contains both Total* and Avg* fields, then update
TimeRangeSlicerControl.GetMetricValues to return the corresponding Avg*
properties for "avg-*" cases (e.g., "avg-cpu" => _data.Select(d =>
d.AvgCpu).ToArray()) and update GetMetricLabel to use "Avg ..." labels for those
branches (e.g., "avg-cpu" => "Avg CPU (ms)"); ensure numeric types match (cast
ints to double where needed) and leave non-avg branches using Total* as before.

Comment on lines +189 to +198
var n = values.Length;
var stepX = w / n;

// Draw filled area + line for the chart
var linePoints = new List<Point>(n);
for (int i = 0; i < n; i++)
{
var x = i * stepX + stepX / 2;
var y = chartBottom - (values[i] / max) * chartHeight;
linePoints.Add(new Point(x, y));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Plot buckets by timestamp, not by array index.

src/PlanViewer.Core/Services/QueryStoreService.cs::FetchTimeSliceDataAsync(...) only returns hours that have runtime stats, so quiet hours are absent. Spacing every point evenly here collapses those gaps, while GetDateTimeAtNorm() still maps the handles across real wall-clock time. On sparse data the shaded range will no longer line up with the plotted hours. Either insert zero-value hourly buckets before drawing or compute x from IntervalStartUtc.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PlanViewer.App/Controls/TimeRangeSlicerControl.axaml.cs` around lines 189
- 198, The chart currently spaces points by array index which collapses gaps
because FetchTimeSliceDataAsync(...) returns only hours with data; instead
compute each point's X from its actual timestamp (use the bucket's
IntervalStartUtc) mapped into the control's time span (same mapping
GetDateTimeAtNorm uses) so plotted points align with wall-clock time; in
TimeRangeSlicerControl.axaml.cs modify the loop that builds linePoints to read
the timestamps corresponding to values (or the returned bucket objects) and
calculate x = (bucket.IntervalStartUtc - rangeStart).TotalSeconds /
rangeDuration.TotalSeconds * w (or equivalent normalized mapping) before adding
new Point(x,y), or alternatively pre-fill zero-valued hourly buckets in the data
returned by FetchTimeSliceDataAsync to ensure dense hourly spacing—choose the
timestamp-based X computation for correctness.

Comment on lines +134 to +138
if (startUtc.HasValue && endUtc.HasValue)
{
timeWhereClause = "WHERE rsi.start_time >= @rangeStart AND rsi.end_time < @rangeEnd";
parameters.Add(new SqlParameter("@rangeStart", startUtc.Value));
parameters.Add(new SqlParameter("@rangeEnd", endUtc.Value));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Include the boundary bucket in the explicit UTC window.

TimeRangeSlicerControl computes the selected end at the end of the last bucket, not its start. With rsi.end_time < @rangeEnd``, a bucket that ends exactly on that boundary is excluded, so the default last-24-hours selection can miss its newest hour. Make the upper bound inclusive or compare rsi.start_time to `@rangeEnd` instead.

Suggested fix
-            timeWhereClause = "WHERE rsi.start_time >= `@rangeStart` AND rsi.end_time < `@rangeEnd`";
+            timeWhereClause = "WHERE rsi.start_time >= `@rangeStart` AND rsi.start_time < `@rangeEnd`";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (startUtc.HasValue && endUtc.HasValue)
{
timeWhereClause = "WHERE rsi.start_time >= @rangeStart AND rsi.end_time < @rangeEnd";
parameters.Add(new SqlParameter("@rangeStart", startUtc.Value));
parameters.Add(new SqlParameter("@rangeEnd", endUtc.Value));
if (startUtc.HasValue && endUtc.HasValue)
{
timeWhereClause = "WHERE rsi.start_time >= `@rangeStart` AND rsi.start_time < `@rangeEnd`";
parameters.Add(new SqlParameter("@rangeStart", startUtc.Value));
parameters.Add(new SqlParameter("@rangeEnd", endUtc.Value));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PlanViewer.Core/Services/QueryStoreService.cs` around lines 134 - 138,
The UTC window currently excludes buckets that end exactly on the boundary
because QueryStoreService builds timeWhereClause with "rsi.end_time <
`@rangeEnd`"; change the predicate so the upper boundary is inclusive (e.g., use
rsi.end_time <= `@rangeEnd`) or alternatively compare the bucket start to the end
boundary (e.g., use rsi.start_time < `@rangeEnd`) to match TimeRangeSlicerControl
semantics; update the timeWhereClause string accordingly where it is set and
leave the `@rangeStart/`@rangeEnd SqlParameter additions intact.

ON rs.plan_id = p.plan_id
WHERE p.query_id = @queryId
AND rsi.start_time >= DATEADD(HOUR, -@hoursBack, GETUTCDATE())
AND rs.first_execution_time >= DATEADD(HOUR, -@hoursBack, GETUTCDATE()) --performance: filter runtime_stats by time directly
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

first_execution_time is trimming valid stats rows.

A Query Store stats row can have first_execution_time before the cutoff and later executions still inside the selected period. These predicates undercount both the history view and the slicer around the window boundary. If you need a runtime-stats-side filter for scan reduction, make it overlap-safe, such as last_execution_time, or drop it after measuring.

Also applies to: 392-393

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/PlanViewer.Core/Services/QueryStoreService.cs` at line 325, The filter
using rs.first_execution_time with DATEADD(HOUR, -@hoursBack, GETUTCDATE())
wrongly excludes query-store rows that had an earlier first_execution_time but
later executions inside the window; modify the predicate in the
QueryStoreService SQL (references to rs.first_execution_time, `@hoursBack`,
DATEADD/GETUTCDATE()) to use an overlap-safe column such as
rs.last_execution_time or remove the runtime_stats-side filter after measuring;
update the same pattern at the other occurrences noted (the similar predicates
around lines referenced as 392-393) so the history view and slicer are not
undercounted.

Copy link
Owner

@erikdarlingdata erikdarlingdata left a comment

Choose a reason for hiding this comment

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

Looks good — nice iteration from #113. The interval-based time filtering, metric-refresh on sort change, and selection preservation are solid improvements.

A couple of minor suggestions for future consideration (not blocking):

  • A short debounce (e.g., 300ms) on OnTimeRangeChanged before triggering FetchPlansForRangeAsync would reduce rapid cancel-fetch cycles when the user is dragging the slicer handle
  • The daysBack interpolation in the SQL could be parameterized for consistency, though it's not a security risk since it comes from settings

We'll handle both of those + a time display picker (UTC/server/local) in a follow-up.

@erikdarlingdata erikdarlingdata merged commit 94b36cc into erikdarlingdata:dev Mar 19, 2026
4 checks passed
@erikdarlingdata
Copy link
Owner

@rferraton this is so cool, I can't stop messing with it. I'm gonna port this idea over to the monitoring tools too. Seriously killer stuff! I added in the local/server/utc time picker stuff just to round it out.

erikdarlingdata added a commit that referenced this pull request Mar 26, 2026
Two regressions introduced in the time slicer PR (#114):

1. Search filter (module, query_id, etc.) was built via BuildSearchFilter()
   but never passed to FetchTopPlansAsync — the filter parameter was
   dropped when the call site switched to named parameters for startUtc/
   endUtc. Add filter: back to the call.

2. Clicking Fetch reloads the slicer via LoadTimeSlicerDataAsync, which
   calls LoadData without preserving the current selection. LoadData
   defaults to last 24h when no selection is provided, discarding the
   user's custom range. Pass the current _slicerStartUtc/_slicerEndUtc
   through to LoadData's selectionStart/selectionEnd parameters.

Fixes #147

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants