From f7cc5a670ea336c29995afedaf98611bcadb3747 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:12:43 -0400 Subject: [PATCH] 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) --- .../Controls/QueryStoreGridControl.axaml | 11 +++- .../Controls/QueryStoreGridControl.axaml.cs | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml index d0b241c..1841365 100644 --- a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml +++ b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml @@ -100,8 +100,17 @@ BorderThickness="0" ScrollViewer.HorizontalScrollBarVisibility="Auto"> - + + + + + + + + + + diff --git a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs index 16f4a37..d6fc363 100644 --- a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs @@ -209,6 +209,62 @@ private void LoadHighlightedPlan_Click(object? sender, RoutedEventArgs e) PlansSelected?.Invoke(this, new List { row.Plan }); } + // ── Context menu ──────────────────────────────────────────────────────── + + private void ContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e) + { + var row = ResultsGrid.SelectedItem as QueryStoreRow; + var hasRow = row != null; + + CopyQueryIdItem.IsEnabled = hasRow; + CopyPlanIdItem.IsEnabled = hasRow; + CopyQueryHashItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.QueryHash); + CopyPlanHashItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.QueryPlanHash); + CopyModuleItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.ModuleName); + CopyQueryTextItem.IsEnabled = hasRow; + CopyRowItem.IsEnabled = hasRow; + + // Wire click handlers (clear first to avoid stacking) + CopyQueryIdItem.Click -= CopyMenuItem_Click; + CopyPlanIdItem.Click -= CopyMenuItem_Click; + CopyQueryHashItem.Click -= CopyMenuItem_Click; + CopyPlanHashItem.Click -= CopyMenuItem_Click; + CopyModuleItem.Click -= CopyMenuItem_Click; + CopyQueryTextItem.Click -= CopyMenuItem_Click; + CopyRowItem.Click -= CopyMenuItem_Click; + + if (!hasRow) return; + + CopyQueryIdItem.Tag = row!.QueryId.ToString(); + CopyPlanIdItem.Tag = row.PlanId.ToString(); + CopyQueryHashItem.Tag = row.QueryHash; + CopyPlanHashItem.Tag = row.QueryPlanHash; + CopyModuleItem.Tag = row.ModuleName; + CopyQueryTextItem.Tag = row.FullQueryText; + 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}"; + + CopyQueryIdItem.Click += CopyMenuItem_Click; + CopyPlanIdItem.Click += CopyMenuItem_Click; + CopyQueryHashItem.Click += CopyMenuItem_Click; + CopyPlanHashItem.Click += CopyMenuItem_Click; + CopyModuleItem.Click += CopyMenuItem_Click; + CopyQueryTextItem.Click += CopyMenuItem_Click; + CopyRowItem.Click += CopyMenuItem_Click; + } + + private async void CopyMenuItem_Click(object? sender, RoutedEventArgs e) + { + if (sender is MenuItem item && item.Tag is string text) + await SetClipboardTextAsync(text); + } + + private async System.Threading.Tasks.Task SetClipboardTextAsync(string text) + { + var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(this); + if (topLevel?.Clipboard != null) + await topLevel.Clipboard.SetTextAsync(text); + } + // ── Column filter infrastructure ─────────────────────────────────────── private static readonly Dictionary> TextAccessors = new()