Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 215 additions & 22 deletions src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ private void OnKeyDown(object? sender, KeyEventArgs e)
ExecuteEstimated_Click(this, new RoutedEventArgs());
e.Handled = true;
}
// Escape → Cancel running query
else if (e.Key == Key.Escape && _executionCts != null && !_executionCts.IsCancellationRequested)
{
_executionCts.Cancel();
e.Handled = true;
}
}

private void OnEditorPointerWheel(object? sender, PointerWheelEventArgs e)
Expand Down Expand Up @@ -448,10 +454,96 @@ private async Task CaptureAndShowPlan(bool estimated)
_executionCts = new CancellationTokenSource();
var ct = _executionCts.Token;

var planType = estimated ? "estimated" : "actual";
SetStatus($"Capturing {planType} plan...");
ExecuteButton.IsEnabled = false;
ExecuteEstButton.IsEnabled = false;
var planType = estimated ? "Estimated" : "Actual";

// Create loading tab with cancel button
var loadingPanel = new StackPanel
{
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
Width = 300
};

var progressBar = new ProgressBar
{
IsIndeterminate = true,
Height = 4,
Margin = new Avalonia.Thickness(0, 0, 0, 12)
};

var statusLabel = new TextBlock
{
Text = $"Capturing {planType.ToLower()} plan...",
FontSize = 14,
Foreground = new SolidColorBrush(Color.Parse("#B0B6C0")),
HorizontalAlignment = HorizontalAlignment.Center
};

var cancelBtn = new Button
{
Content = "\u25A0 Cancel",
Height = 32,
Width = 120,
Padding = new Avalonia.Thickness(16, 0),
FontSize = 13,
Margin = new Avalonia.Thickness(0, 16, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Theme = (Avalonia.Styling.ControlTheme)this.FindResource("AppButton")!
};
cancelBtn.Click += (_, _) => _executionCts?.Cancel();

loadingPanel.Children.Add(progressBar);
loadingPanel.Children.Add(statusLabel);
loadingPanel.Children.Add(cancelBtn);

var loadingContainer = new Grid
{
Background = new SolidColorBrush(Color.Parse("#1A1D23")),
Focusable = true,
Children = { loadingPanel }
};
loadingContainer.KeyDown += (_, ke) =>
{
if (ke.Key == Key.Escape) { _executionCts?.Cancel(); ke.Handled = true; }
};

// Add loading tab and switch to it
_planCounter++;
var tabLabel = estimated ? $"Est Plan {_planCounter}" : $"Plan {_planCounter}";
var headerText = new TextBlock
{
Text = tabLabel,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 12
};
var closeBtn = new Button
{
Content = "\u2715",
MinWidth = 22, MinHeight = 22, Width = 22, Height = 22,
Padding = new Avalonia.Thickness(0),
FontSize = 11,
Margin = new Avalonia.Thickness(6, 0, 0, 0),
Background = Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(0),
Foreground = new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)),
VerticalAlignment = VerticalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
};
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = { headerText, closeBtn }
};
var loadingTab = new TabItem { Header = header, Content = loadingContainer };
closeBtn.Tag = loadingTab;
closeBtn.Click += ClosePlanTab_Click;

SubTabControl.Items.Add(loadingTab);
SubTabControl.SelectedItem = loadingTab;
loadingContainer.Focus();

try
{
Expand All @@ -466,45 +558,51 @@ private async Task CaptureAndShowPlan(bool estimated)
if (estimated)
{
planXml = await EstimatedPlanExecutor.GetEstimatedPlanAsync(
_connectionString, _selectedDatabase, queryText, timeoutSeconds: 60, ct);
_connectionString, _selectedDatabase, queryText, timeoutSeconds: 0, ct);
}
else
{
planXml = await ActualPlanExecutor.ExecuteForActualPlanAsync(
_connectionString, _selectedDatabase, queryText,
planXml: null, isolationLevel: null,
isAzureSqlDb: isAzure, timeoutSeconds: 60, ct);
isAzureSqlDb: isAzure, timeoutSeconds: 0, ct);
}

sw.Stop();

if (string.IsNullOrEmpty(planXml))
{
SetStatus($"No plan returned ({sw.Elapsed.TotalSeconds:F1}s)");
statusLabel.Text = $"No plan returned ({sw.Elapsed.TotalSeconds:F1}s)";
progressBar.IsVisible = false;
cancelBtn.IsVisible = false;
return;
}

// Replace loading content with the plan viewer
SetStatus($"{planType} plan captured ({sw.Elapsed.TotalSeconds:F1}s)");
AddPlanTab(planXml, queryText, estimated);
var viewer = new PlanViewerControl();
viewer.Metadata = _serverMetadata;
viewer.LoadPlan(planXml, tabLabel, queryText);
loadingTab.Content = viewer;
HumanAdviceButton.IsEnabled = true;
RobotAdviceButton.IsEnabled = true;
}
catch (OperationCanceledException)
{
SetStatus("Cancelled");
SubTabControl.Items.Remove(loadingTab);
}
catch (SqlException ex)
{
SetStatus(ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message, autoClear: false);
statusLabel.Text = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
progressBar.IsVisible = false;
cancelBtn.IsVisible = false;
}
catch (Exception ex)
{
SetStatus(ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message, autoClear: false);
}
finally
{
ExecuteButton.IsEnabled = true;
ExecuteEstButton.IsEnabled = true;
statusLabel.Text = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
progressBar.IsVisible = false;
cancelBtn.IsVisible = false;
}
}

Expand Down Expand Up @@ -1174,8 +1272,93 @@ private async void GetActualPlan_Click(object? sender, RoutedEventArgs e)
_executionCts = new CancellationTokenSource();
var ct = _executionCts.Token;

SetStatus("Capturing actual plan...");
GetActualPlanButton.IsEnabled = false;
// Create loading tab with cancel button
var loadingPanel = new StackPanel
{
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
Width = 300
};

var progressBar = new ProgressBar
{
IsIndeterminate = true,
Height = 4,
Margin = new Avalonia.Thickness(0, 0, 0, 12)
};

var statusLabel = new TextBlock
{
Text = "Capturing actual plan...",
FontSize = 14,
Foreground = new SolidColorBrush(Color.Parse("#B0B6C0")),
HorizontalAlignment = HorizontalAlignment.Center
};

var cancelBtn = new Button
{
Content = "\u25A0 Cancel",
Height = 32,
Width = 120,
Padding = new Avalonia.Thickness(16, 0),
FontSize = 13,
Margin = new Avalonia.Thickness(0, 16, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Theme = (Avalonia.Styling.ControlTheme)this.FindResource("AppButton")!
};
cancelBtn.Click += (_, _) => _executionCts?.Cancel();

loadingPanel.Children.Add(progressBar);
loadingPanel.Children.Add(statusLabel);
loadingPanel.Children.Add(cancelBtn);

var loadingContainer = new Grid
{
Background = new SolidColorBrush(Color.Parse("#1A1D23")),
Focusable = true,
Children = { loadingPanel }
};
loadingContainer.KeyDown += (_, ke) =>
{
if (ke.Key == Key.Escape) { _executionCts?.Cancel(); ke.Handled = true; }
};

_planCounter++;
var tabLabel = $"Plan {_planCounter}";
var headerText = new TextBlock
{
Text = tabLabel,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 12
};
var closeBtn = new Button
{
Content = "\u2715",
MinWidth = 22, MinHeight = 22, Width = 22, Height = 22,
Padding = new Avalonia.Thickness(0),
FontSize = 11,
Margin = new Avalonia.Thickness(6, 0, 0, 0),
Background = Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(0),
Foreground = new SolidColorBrush(Color.FromRgb(0xE4, 0xE6, 0xEB)),
VerticalAlignment = VerticalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
};
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = { headerText, closeBtn }
};
var loadingTab = new TabItem { Header = header, Content = loadingContainer };
closeBtn.Tag = loadingTab;
closeBtn.Click += ClosePlanTab_Click;

SubTabControl.Items.Add(loadingTab);
SubTabControl.SelectedItem = loadingTab;
loadingContainer.Focus();

try
{
Expand All @@ -1185,30 +1368,40 @@ private async void GetActualPlan_Click(object? sender, RoutedEventArgs e)
var actualPlanXml = await ActualPlanExecutor.ExecuteForActualPlanAsync(
_connectionString, _selectedDatabase, queryText,
planXml, isolationLevel: null,
isAzureSqlDb: isAzure, timeoutSeconds: 60, ct);
isAzureSqlDb: isAzure, timeoutSeconds: 0, ct);

sw.Stop();

if (string.IsNullOrEmpty(actualPlanXml))
{
SetStatus($"No actual plan returned ({sw.Elapsed.TotalSeconds:F1}s)");
statusLabel.Text = $"No actual plan returned ({sw.Elapsed.TotalSeconds:F1}s)";
progressBar.IsVisible = false;
cancelBtn.IsVisible = false;
return;
}

SetStatus($"Actual plan captured ({sw.Elapsed.TotalSeconds:F1}s)");
AddPlanTab(actualPlanXml, queryText, estimated: false);
var actualViewer = new PlanViewerControl();
actualViewer.Metadata = _serverMetadata;
actualViewer.LoadPlan(actualPlanXml, tabLabel, queryText);
loadingTab.Content = actualViewer;
}
catch (OperationCanceledException)
{
SetStatus("Cancelled");
SubTabControl.Items.Remove(loadingTab);
}
catch (SqlException ex)
{
SetStatus(ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message, autoClear: false);
statusLabel.Text = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
progressBar.IsVisible = false;
cancelBtn.IsVisible = false;
}
catch (Exception ex)
{
SetStatus(ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message, autoClear: false);
statusLabel.Text = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
progressBar.IsVisible = false;
cancelBtn.IsVisible = false;
}
finally
{
Expand Down
27 changes: 25 additions & 2 deletions src/PlanViewer.App/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1035,19 +1035,43 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer)
HorizontalAlignment = HorizontalAlignment.Center
};

var cancelBtn = new Button
{
Content = "\u25A0 Cancel",
Height = 32,
Width = 120,
Padding = new Avalonia.Thickness(16, 0),
FontSize = 13,
Margin = new Avalonia.Thickness(0, 16, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Theme = (Avalonia.Styling.ControlTheme)this.FindResource("AppButton")!
};

loadingPanel.Children.Add(progressBar);
loadingPanel.Children.Add(statusText);
loadingPanel.Children.Add(cancelBtn);

var cts = new System.Threading.CancellationTokenSource();
cancelBtn.Click += (_, _) => cts.Cancel();

var loadingContainer = new Grid
{
Background = new SolidColorBrush(Color.Parse("#1A1D23")),
Focusable = true,
Children = { loadingPanel }
};
loadingContainer.KeyDown += (_, ke) =>
{
if (ke.Key == Avalonia.Input.Key.Escape) { cts.Cancel(); ke.Handled = true; }
};

var tab = CreateTab("Actual Plan", loadingContainer);
MainTabControl.Items.Add(tab);
MainTabControl.SelectedItem = tab;
UpdateEmptyOverlay();
loadingContainer.Focus();

try
{
Expand All @@ -1064,13 +1088,12 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer)

statusText.Text = "Capturing actual plan...";

var cts = new System.Threading.CancellationTokenSource();
var sw = System.Diagnostics.Stopwatch.StartNew();

var actualPlanXml = await ActualPlanExecutor.ExecuteForActualPlanAsync(
connectionString, database, queryText,
viewer.RawXml, isolationLevel: null,
isAzureSqlDb: isAzure, timeoutSeconds: 60, cts.Token);
isAzureSqlDb: isAzure, timeoutSeconds: 0, cts.Token);

sw.Stop();

Expand Down
5 changes: 2 additions & 3 deletions src/PlanViewer.Core/Services/CredentialServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ public static ICredentialService Create()
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return new KeychainCredentialService();

throw new PlatformNotSupportedException(
"Credential storage is not yet supported on this platform. " +
"Windows and macOS are supported.");
// Linux and other platforms: use in-memory storage (credentials not persisted across sessions)
return new InMemoryCredentialService();
}
}
Loading
Loading