Skip to content

Commit ae901ae

Browse files
authored
Add unified Settings window (#351)
Adds a non-modal Settings window (ALT+S) consolidating Query Store, Multi QS Overview, Query History, and Script Options into a single sidebar UI. Settings persist to appsettings.json; one-time migration from the legacy perfstudio_format_settings.json.
1 parent 06dc723 commit ae901ae

16 files changed

Lines changed: 1122 additions & 571 deletions

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ private async void Format_Click(object? sender, RoutedEventArgs e)
8686

8787
try
8888
{
89-
var settings = SqlFormatSettingsService.Load(out var loadError);
90-
if (loadError != null)
91-
SetStatus("Warning: using default format settings (load failed)");
89+
var settings = AppSettingsService.Load().FormatOptions ?? new SqlFormatSettings();
9290

9391
var (formatted, errors) = await Task.Run(() => SqlFormattingService.Format(sql, settings));
9492

@@ -150,10 +148,4 @@ private async void Format_Click(object? sender, RoutedEventArgs e)
150148
FormatButton.IsEnabled = true;
151149
}
152150
}
153-
154-
private void FormatOptions_Click(object? sender, RoutedEventArgs e)
155-
{
156-
var dialog = new Dialogs.FormatOptionsWindow();
157-
dialog.ShowDialog(GetParentWindow());
158-
}
159151
}

src/PlanViewer.App/Controls/QuerySessionControl.axaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,6 @@
9292
Height="28" Padding="10,0" FontSize="12"
9393
Theme="{StaticResource AppButton}"
9494
ToolTip.Tip="Format the SQL query"/>
95-
<Button x:Name="FormatOptionsButton" Content="&#x2699; Format Options" Click="FormatOptions_Click"
96-
Height="28" Padding="10,0" FontSize="12"
97-
Theme="{StaticResource AppButton}"
98-
ToolTip.Tip="Configure SQL formatting options"/>
9995
</WrapPanel>
10096
</DockPanel>
10197
</Border>

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,23 @@ public QueryStoreGridControl(ServerConnection serverConnection, ICredentialServi
6161
_database = initialDatabase;
6262
_connectionString = serverConnection.GetConnectionString(credentialService, initialDatabase);
6363
_waitStatsSupported = supportsWaitStats;
64-
_slicerDaysBack = AppSettingsService.Load().QueryStoreSlicerDays;
64+
65+
var userSettings = AppSettingsService.Load();
66+
_slicerDaysBack = userSettings.QueryStoreSlicerDays;
67+
6568
InitializeComponent();
69+
70+
// Apply user defaults to UI controls
71+
TopNBox.Value = userSettings.QueryStoreTopLimit;
72+
SelectComboByTag(OrderByBox, userSettings.QueryStoreDefaultMetric);
73+
SelectComboByTag(GroupByBox, userSettings.QueryStoreDefaultGroupBy switch
74+
{
75+
"QueryHash" => "query-hash",
76+
"Module" => "module",
77+
"None" => "none",
78+
_ => "query-hash"
79+
});
80+
6681
ResultsGrid.ItemsSource = _filteredRows;
6782
Helpers.DataGridBehaviors.Attach(ResultsGrid);
6883
EnsureFilterPopup();
@@ -227,6 +242,21 @@ private static readonly (string ColumnId, Func<QueryStoreRow, double> Accessor)[
227242
["AvgMemSort"] = "AvgMemory",
228243
};
229244

245+
private static void SelectComboByTag(ComboBox box, string tag)
246+
{
247+
for (int i = 0; i < box.Items.Count; i++)
248+
{
249+
if (box.Items[i] is ComboBoxItem item && item.Tag?.ToString() == tag)
250+
{
251+
box.SelectedIndex = i;
252+
return;
253+
}
254+
}
255+
// Unknown tag — fall back to the first item so the combo is never empty
256+
if (box.Items.Count > 0)
257+
box.SelectedIndex = 0;
258+
}
259+
230260

231261
}
232262

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,14 @@ private async System.Threading.Tasks.Task LoadHistoryAsync()
296296
private void BuildColorMap()
297297
{
298298
_planHashColorMap.Clear();
299-
var hashes = _historyData.Select(r => r.QueryPlanHash).Distinct().OrderBy(h => h).ToList();
299+
var maxPlans = Services.AppSettingsService.Load().QueryHistoryMaxPlans;
300+
var hashes = _historyData
301+
.GroupBy(r => r.QueryPlanHash)
302+
.OrderByDescending(g => g.Sum(r => r.CountExecutions))
303+
.Take(maxPlans)
304+
.Select(g => g.Key)
305+
.OrderBy(h => h)
306+
.ToList();
300307
for (int i = 0; i < hashes.Count; i++)
301308
_planHashColorMap[hashes[i]] = PlanColors[i % PlanColors.Length];
302309
}

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

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Avalonia.Interactivity;
1313
using Avalonia.Media;
1414
using Avalonia.Threading;
15+
using PlanViewer.App.Services;
1516
using PlanViewer.Core.Interfaces;
1617
using PlanViewer.Core.Models;
1718
using PlanViewer.Core.Services;
@@ -38,18 +39,8 @@ public partial class QueryStoreOverviewControl : UserControl
3839
private DateTime _slicerEndUtc;
3940
private int _daysBack = 30;
4041

41-
// Color palette for databases (minimizes color dispersion)
42-
private static readonly Color[] Palette = new[]
43-
{
44-
Color.Parse("#2EAEF1"), // blue
45-
Color.Parse("#F2994A"), // orange
46-
Color.Parse("#27AE60"), // green
47-
Color.Parse("#9B51E0"), // purple
48-
Color.Parse("#EB5757"), // red
49-
Color.Parse("#F2C94C"), // yellow
50-
Color.Parse("#56CCF2"), // light blue
51-
Color.Parse("#BB6BD9"), // violet
52-
};
42+
// Color palette for databases — loaded from user settings
43+
private readonly Color[] _palette;
5344

5445
private static readonly Color OthersColor = Color.Parse("#555555");
5546

@@ -69,13 +60,21 @@ public class DrillDownEventArgs(string database, DateTime startUtc, DateTime end
6960
public event EventHandler<DrillDownEventArgs>? DrillDownRequested;
7061

7162
public QueryStoreOverviewControl(ServerConnection serverConnection,
72-
ICredentialService credentialService, int maxDop = 8, int topN = 4, bool supportsWaitStats = true)
63+
ICredentialService credentialService, int maxDop = 8, int? topN = null, bool supportsWaitStats = true)
7364
{
7465
_serverConnection = serverConnection;
7566
_credentialService = credentialService;
7667
_masterConnectionString = serverConnection.GetConnectionString(credentialService, "master");
7768
_maxDop = maxDop;
78-
_topN = topN;
69+
70+
var userSettings = AppSettingsService.Load();
71+
_topN = topN ?? userSettings.MultiQsTopDbCount;
72+
_palette = userSettings.MultiQsTopDbColors
73+
.Select(hex => { try { return Color.Parse(hex); } catch { return Color.Parse("#555555"); } })
74+
.ToArray();
75+
if (_palette.Length == 0)
76+
_palette = AppSettingsService.DefaultTopDbColors.Select(hex => Color.Parse(hex)).ToArray();
77+
7978
_supportsWaitStats = supportsWaitStats;
8079
_slicerEndUtc = DateTime.UtcNow;
8180
_slicerStartUtc = _slicerEndUtc.AddHours(-24);
@@ -668,8 +667,8 @@ private void DrawBarCards()
668667
.Select(m => m.DatabaseName)
669668
.ToList();
670669
var topDbs = ranked.Take(_topN).ToList();
671-
for (int i = 0; i < topDbs.Count && i < Palette.Length; i++)
672-
_dbColorMap[topDbs[i]] = Palette[i];
670+
for (int i = 0; i < topDbs.Count && i < _palette.Length; i++)
671+
_dbColorMap[topDbs[i]] = _palette[i];
673672

674673
DrawMetricRow(TotalMetricsGrid, isTotal: true, topDbs);
675674
DrawMetricRow(AvgMetricsGrid, isTotal: false, topDbs);

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Avalonia.Interactivity;
99
using Avalonia.Media;
1010
using Avalonia.Threading;
11+
using PlanViewer.App.Services;
1112
using PlanViewer.Core.Models;
1213
using PlanViewer.Core.Services;
1314

@@ -52,13 +53,14 @@ private enum DragMode { None, MoveRange, DragStart, DragEnd, SelectRect }
5253
private double _selectRectOriginX; // canvas-x where drag-select started
5354
private double _selectRectCurrentX; // canvas-x of current pointer during drag-select
5455

55-
private string _activeFilterTag = "24"; // tag of the currently active quick-filter button
56+
private string _activeFilterTag; // tag from user settings
5657
private DispatcherTimer? _rangeChangedDebounce;
5758

5859
public event EventHandler<TimeRangeChangedEventArgs>? RangeChanged;
5960

6061
public TimeRangeSlicerControl()
6162
{
63+
_activeFilterTag = AppSettingsService.Load().QueryStoreDefaultTimeRange;
6264
InitializeComponent();
6365
SlicerBorder.SizeChanged += (_, _) => Redraw();
6466
SlicerCanvas.Focusable = true;
@@ -80,13 +82,13 @@ public void LoadData(List<QueryStoreTimeSlice> data, string metric,
8082
}
8183
else
8284
{
83-
// Default selection: last 24 hours
85+
// Default selection from user settings
8486
_rangeEnd = 1.0;
85-
_activeFilterTag = "24";
87+
var defaultHours = int.TryParse(_activeFilterTag, out var h) ? h : 24;
8688
if (_data.Count >= 2)
8789
{
8890
var last = _data[^1].IntervalStartUtc.AddHours(1);
89-
var start24h = last.AddHours(-24);
91+
var start24h = last.AddHours(-defaultHours);
9092
_rangeStart = GetNormFromDateTime(start24h);
9193
}
9294
else
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.ComponentModel;
2+
using System.Reflection;
3+
4+
namespace PlanViewer.App.Dialogs;
5+
6+
internal class FormatOptionRow : INotifyPropertyChanged
7+
{
8+
private string _currentValue = "";
9+
private bool _boolValue;
10+
11+
public string Name { get; set; } = "";
12+
13+
public bool IsBool { get; set; }
14+
15+
public bool BoolValue
16+
{
17+
get => _boolValue;
18+
set
19+
{
20+
if (_boolValue == value) return;
21+
_boolValue = value;
22+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BoolValue)));
23+
// Keep CurrentValue in sync for serialization
24+
if (IsBool)
25+
{
26+
_currentValue = value.ToString();
27+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentValue)));
28+
}
29+
}
30+
}
31+
32+
public bool DefaultBoolValue { get; set; }
33+
34+
public string[]? ChoiceOptions { get; set; }
35+
36+
public bool IsChoice => ChoiceOptions != null;
37+
38+
public bool IsText => !IsBool && !IsChoice;
39+
40+
public string CurrentValue
41+
{
42+
get => _currentValue;
43+
set
44+
{
45+
if (_currentValue == value) return;
46+
_currentValue = value;
47+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentValue)));
48+
}
49+
}
50+
51+
public string DefaultValue { get; set; } = "";
52+
53+
internal PropertyInfo PropertyInfo { get; set; } = null!;
54+
55+
public event PropertyChangedEventHandler? PropertyChanged;
56+
}

src/PlanViewer.App/Dialogs/FormatOptionsWindow.axaml

Lines changed: 0 additions & 121 deletions
This file was deleted.

0 commit comments

Comments
 (0)