Skip to content

Commit f7cc5a6

Browse files
Add copy commands to Query Store grid context menu (#91)
- Copy Query ID, Plan ID, Query Hash, Plan Hash, Module Name, Query Text - Copy Row (tab-delimited, all columns) - Items disabled when no row selected or value is empty Closes #91 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a9abb99 commit f7cc5a6

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

src/PlanViewer.App/Controls/QueryStoreGridControl.axaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,17 @@
100100
BorderThickness="0"
101101
ScrollViewer.HorizontalScrollBarVisibility="Auto">
102102
<DataGrid.ContextMenu>
103-
<ContextMenu>
103+
<ContextMenu Opening="ContextMenu_Opening">
104104
<MenuItem Header="Load Plan" Click="LoadHighlightedPlan_Click"/>
105+
<Separator/>
106+
<MenuItem x:Name="CopyQueryIdItem" Header="Copy Query ID"/>
107+
<MenuItem x:Name="CopyPlanIdItem" Header="Copy Plan ID"/>
108+
<MenuItem x:Name="CopyQueryHashItem" Header="Copy Query Hash"/>
109+
<MenuItem x:Name="CopyPlanHashItem" Header="Copy Plan Hash"/>
110+
<MenuItem x:Name="CopyModuleItem" Header="Copy Module Name"/>
111+
<Separator/>
112+
<MenuItem x:Name="CopyQueryTextItem" Header="Copy Query Text"/>
113+
<MenuItem x:Name="CopyRowItem" Header="Copy Row"/>
105114
</ContextMenu>
106115
</DataGrid.ContextMenu>
107116
<DataGrid.Columns>

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,62 @@ private void LoadHighlightedPlan_Click(object? sender, RoutedEventArgs e)
209209
PlansSelected?.Invoke(this, new List<QueryStorePlan> { row.Plan });
210210
}
211211

212+
// ── Context menu ────────────────────────────────────────────────────────
213+
214+
private void ContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e)
215+
{
216+
var row = ResultsGrid.SelectedItem as QueryStoreRow;
217+
var hasRow = row != null;
218+
219+
CopyQueryIdItem.IsEnabled = hasRow;
220+
CopyPlanIdItem.IsEnabled = hasRow;
221+
CopyQueryHashItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.QueryHash);
222+
CopyPlanHashItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.QueryPlanHash);
223+
CopyModuleItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.ModuleName);
224+
CopyQueryTextItem.IsEnabled = hasRow;
225+
CopyRowItem.IsEnabled = hasRow;
226+
227+
// Wire click handlers (clear first to avoid stacking)
228+
CopyQueryIdItem.Click -= CopyMenuItem_Click;
229+
CopyPlanIdItem.Click -= CopyMenuItem_Click;
230+
CopyQueryHashItem.Click -= CopyMenuItem_Click;
231+
CopyPlanHashItem.Click -= CopyMenuItem_Click;
232+
CopyModuleItem.Click -= CopyMenuItem_Click;
233+
CopyQueryTextItem.Click -= CopyMenuItem_Click;
234+
CopyRowItem.Click -= CopyMenuItem_Click;
235+
236+
if (!hasRow) return;
237+
238+
CopyQueryIdItem.Tag = row!.QueryId.ToString();
239+
CopyPlanIdItem.Tag = row.PlanId.ToString();
240+
CopyQueryHashItem.Tag = row.QueryHash;
241+
CopyPlanHashItem.Tag = row.QueryPlanHash;
242+
CopyModuleItem.Tag = row.ModuleName;
243+
CopyQueryTextItem.Tag = row.FullQueryText;
244+
CopyRowItem.Tag = $"{row.QueryId}\t{row.PlanId}\t{row.QueryHash}\t{row.QueryPlanHash}\t{row.ModuleName}\t{row.LastExecutedLocal}\t{row.ExecsDisplay}\t{row.TotalCpuDisplay}\t{row.AvgCpuDisplay}\t{row.TotalDurDisplay}\t{row.AvgDurDisplay}\t{row.TotalReadsDisplay}\t{row.AvgReadsDisplay}\t{row.TotalWritesDisplay}\t{row.AvgWritesDisplay}\t{row.TotalPhysReadsDisplay}\t{row.AvgPhysReadsDisplay}\t{row.TotalMemDisplay}\t{row.AvgMemDisplay}\t{row.FullQueryText}";
245+
246+
CopyQueryIdItem.Click += CopyMenuItem_Click;
247+
CopyPlanIdItem.Click += CopyMenuItem_Click;
248+
CopyQueryHashItem.Click += CopyMenuItem_Click;
249+
CopyPlanHashItem.Click += CopyMenuItem_Click;
250+
CopyModuleItem.Click += CopyMenuItem_Click;
251+
CopyQueryTextItem.Click += CopyMenuItem_Click;
252+
CopyRowItem.Click += CopyMenuItem_Click;
253+
}
254+
255+
private async void CopyMenuItem_Click(object? sender, RoutedEventArgs e)
256+
{
257+
if (sender is MenuItem item && item.Tag is string text)
258+
await SetClipboardTextAsync(text);
259+
}
260+
261+
private async System.Threading.Tasks.Task SetClipboardTextAsync(string text)
262+
{
263+
var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(this);
264+
if (topLevel?.Clipboard != null)
265+
await topLevel.Clipboard.SetTextAsync(text);
266+
}
267+
212268
// ── Column filter infrastructure ───────────────────────────────────────
213269

214270
private static readonly Dictionary<string, Func<QueryStoreRow, string>> TextAccessors = new()

0 commit comments

Comments
 (0)