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
301 changes: 13 additions & 288 deletions Dashboard/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,17 +457,21 @@ private async void ServerTab_KeyDown(object sender, System.Windows.Input.KeyEven
private void SetupChartContextMenus()
{
// Resource Overview charts
SetupChartSaveMenu(ResourceOverviewCpuChart, "CPU_Utilization", "collect.cpu_utilization_stats");
SetupChartSaveMenu(ResourceOverviewMemoryChart, "Memory_Utilization", "collect.memory_stats");
SetupChartSaveMenu(ResourceOverviewIoChart, "IO_Latency", "collect.file_io_stats");
SetupChartSaveMenu(ResourceOverviewWaitChart, "Wait_Stats", "collect.wait_stats");
Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewCpuChart, "CPU_Utilization", "collect.cpu_utilization_stats");
Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewMemoryChart, "Memory_Utilization", "collect.memory_stats");
Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewIoChart, "IO_Latency", "collect.file_io_stats");
Helpers.TabHelpers.SetupChartContextMenu(ResourceOverviewWaitChart, "Wait_Stats", "collect.wait_stats");

// Blocking Stats charts
SetupChartSaveMenu(LockWaitStatsChart, "Lock_Wait_Stats", "collect.wait_stats");
SetupChartSaveMenu(BlockingStatsBlockingEventsChart, "Blocking_Events", "collect.blocking_deadlock_stats");
SetupChartSaveMenu(BlockingStatsDurationChart, "Blocking_Duration", "collect.blocking_deadlock_stats");
SetupChartSaveMenu(BlockingStatsDeadlocksChart, "Deadlocks", "collect.blocking_deadlock_stats");
SetupChartSaveMenu(BlockingStatsDeadlockWaitTimeChart, "Deadlock_Wait_Time", "collect.blocking_deadlock_stats");
Helpers.TabHelpers.SetupChartContextMenu(LockWaitStatsChart, "Lock_Wait_Stats", "collect.wait_stats");
Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsBlockingEventsChart, "Blocking_Events", "collect.blocking_deadlock_stats");
Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsDurationChart, "Blocking_Duration", "collect.blocking_deadlock_stats");
Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsDeadlocksChart, "Deadlocks", "collect.blocking_deadlock_stats");
Helpers.TabHelpers.SetupChartContextMenu(BlockingStatsDeadlockWaitTimeChart, "Deadlock_Wait_Time", "collect.blocking_deadlock_stats");

// Current Waits charts
Helpers.TabHelpers.SetupChartContextMenu(CurrentWaitsDurationChart, "Current_Waits_Duration", "collect.waiting_tasks");
Helpers.TabHelpers.SetupChartContextMenu(CurrentWaitsBlockedChart, "Current_Waits_Blocked", "collect.waiting_tasks");

// Query Performance Trends charts now handled by QueryPerformanceContent UserControl

Expand All @@ -477,279 +481,6 @@ private void SetupChartContextMenus()
// Memory Analysis charts now handled by MemoryContent UserControl
}

private void SetupChartSaveMenu(WpfPlot chart, string chartName, string? dataSource = null)
{
// Create native WPF context menu (simpler - no zoom items since double-click handles reset)
var contextMenu = new ContextMenu();

var copyItem = new MenuItem { Header = "Copy Image", Icon = new TextBlock { Text = "📋" } };
copyItem.Click += (s, e) =>
{
var tempFile = Path.Combine(Path.GetTempPath(), $"chart_copy_{Guid.NewGuid()}.png");
try
{
chart.Plot.SavePng(tempFile, (int)chart.ActualWidth, (int)chart.ActualHeight);
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bitmap.UriSource = new Uri(tempFile);
bitmap.EndInit();
bitmap.Freeze();
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
Clipboard.SetDataObject(new System.Windows.DataObject(System.Windows.DataFormats.Bitmap, bitmap), false);
}
finally
{
if (File.Exists(tempFile)) File.Delete(tempFile);
}
};
contextMenu.Items.Add(copyItem);

var saveItem = new MenuItem { Header = "Save Image As...", Icon = new TextBlock { Text = "💾" } };
saveItem.Click += (s, e) =>
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
var defaultFileName = $"{chartName}_{timestamp}.png";
var saveDialog = new SaveFileDialog
{
Filter = "PNG Image|*.png|JPEG Image|*.jpg|BMP Image|*.bmp",
FileName = defaultFileName,
DefaultExt = ".png"
};
if (saveDialog.ShowDialog() == true)
{
chart.Plot.SavePng(saveDialog.FileName, (int)chart.ActualWidth, (int)chart.ActualHeight);
}
};
contextMenu.Items.Add(saveItem);

var openWindowItem = new MenuItem { Header = "Open in New Window", Icon = new TextBlock { Text = "🗗" } };
openWindowItem.Click += (s, e) =>
{
var newWindow = new Window
{
Title = chartName.Replace("_", " ", StringComparison.Ordinal),
Width = 800,
Height = 600
};
var tempFile = Path.Combine(Path.GetTempPath(), $"chart_temp_{Guid.NewGuid()}.png");
try
{
chart.Plot.SavePng(tempFile, 800, 600);
var image = new System.Windows.Controls.Image();
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bitmap.UriSource = new Uri(tempFile);
bitmap.EndInit();
bitmap.Freeze();
image.Source = bitmap;
newWindow.Content = image;
}
finally
{
if (File.Exists(tempFile)) File.Delete(tempFile);
}
newWindow.Show();
};
contextMenu.Items.Add(openWindowItem);

contextMenu.Items.Add(new Separator());

var autoscaleItem = new MenuItem { Header = "Revert (or double-click)", Icon = new TextBlock { Text = "↩" } };
autoscaleItem.Click += (s, e) =>
{
chart.Plot.Axes.AutoScale();
chart.Refresh();
};
contextMenu.Items.Add(autoscaleItem);

contextMenu.Items.Add(new Separator());

var exportCsvItem = new MenuItem { Header = "Export Data to CSV...", Icon = new TextBlock { Text = "📊" } };
exportCsvItem.Click += (s, e) =>
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
var defaultFileName = $"{chartName}_data_{timestamp}.csv";
var saveDialog = new SaveFileDialog
{
Filter = "CSV Files|*.csv|All Files|*.*",
FileName = defaultFileName,
DefaultExt = ".csv"
};
if (saveDialog.ShowDialog() == true)
{
try
{
var sb = new StringBuilder();
sb.AppendLine("DateTime,Series,Value");

var plottables = chart.Plot.GetPlottables();
int seriesIndex = 1;
foreach (var plottable in plottables)
{
if (plottable is ScottPlot.Plottables.Scatter scatter)
{
var seriesName = scatter.LegendText ?? $"Series{seriesIndex}";
var points = scatter.Data.GetScatterPoints();

foreach (var point in points)
{
var dateTime = DateTime.FromOADate(point.X);
sb.AppendLine(CultureInfo.InvariantCulture, $"{dateTime:yyyy-MM-dd HH:mm:ss},{TabHelpers.EscapeCsvField(seriesName)},{point.Y}");
}
seriesIndex++;
}
}

File.WriteAllText(saveDialog.FileName, sb.ToString());
MessageBox.Show($"Data exported to:\n{saveDialog.FileName}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Error exporting data:\n\n{ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
};
contextMenu.Items.Add(exportCsvItem);

// Show Data Source (if provided)
if (!string.IsNullOrEmpty(dataSource))
{
contextMenu.Items.Add(new Separator());

var dataSourceItem = new MenuItem { Header = "Show Data Source", Icon = new TextBlock { Text = "ℹ" } };
dataSourceItem.Click += (s, e) =>
{
MessageBox.Show(
$"Data Source:\n\n{dataSource}",
"Chart Data Source",
MessageBoxButton.OK,
MessageBoxImage.Information);
};
contextMenu.Items.Add(dataSourceItem);
}

// Disable ScottPlot's default right-click context menu handling
chart.UserInputProcessor.UserActionResponses.RemoveAll(r =>
r.GetType().Name.Contains("Context", StringComparison.Ordinal) ||
r.GetType().Name.Contains("RightClick", StringComparison.Ordinal) ||
r.GetType().Name.Contains("Menu", StringComparison.Ordinal));

// Use PreviewMouseRightButtonDown to show context menu before ScottPlot handles it
chart.PreviewMouseRightButtonDown += (s, e) =>
{
e.Handled = true; // Prevent ScottPlot from handling
contextMenu.PlacementTarget = chart;
contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
contextMenu.IsOpen = true;
};

// Disable ALL of ScottPlot's default double-click behaviors
chart.UserInputProcessor.UserActionResponses.RemoveAll(r =>
r.GetType().Name.Contains("DoubleClick", StringComparison.Ordinal));

// Use PreviewMouseDoubleClick to intercept before ScottPlot
chart.PreviewMouseDoubleClick += (s, e) =>
{
e.Handled = true;
_isAutoScaling = true;

Dispatcher.BeginInvoke(new Action(async () =>
{
try
{
if (_isZoomed)
{
await ResetToOriginalRange();
}
else
{
chart.Plot.Axes.AutoScale();
chart.Refresh();
}
}
finally
{
await Task.Delay(200);
_isAutoScaling = false;
}
}), System.Windows.Threading.DispatcherPriority.Background);
};

// Add AxisLimitsChanged handler for auto-zoom-out detection
chart.Plot.RenderManager.AxisLimitsChanged += (s, e) =>
{
if (_isApplyingZoom || _isAutoScaling) return; // Avoid recursion

// Debounce - reset timer on each change
_lastZoomedChart = chart;

if (_chartZoomDebounceTimer == null)
{
_chartZoomDebounceTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(800)
};
_chartZoomDebounceTimer.Tick += ChartZoomDebounce_Tick;
}

_chartZoomDebounceTimer.Stop();
_chartZoomDebounceTimer.Start();
};
}

private async void ChartZoomDebounce_Tick(object? sender, EventArgs e)
{
_chartZoomDebounceTimer?.Stop();

if (_lastZoomedChart == null || _isApplyingZoom) return;

try
{
var limits = _lastZoomedChart.Plot.Axes.GetLimits();
var fromDate = DateTime.FromOADate(limits.Left);
var toDate = DateTime.FromOADate(limits.Right);

// Validate dates
if (fromDate >= toDate || fromDate.Year < 2000 || toDate.Year > 2100)
return;

// Get current data range
DateTime currentFrom, currentTo;
if (_globalFromDate.HasValue && _globalToDate.HasValue)
{
currentFrom = _globalFromDate.Value;
currentTo = _globalToDate.Value;
}
else
{
currentTo = Helpers.ServerTimeHelper.ServerNow;
currentFrom = currentTo.AddHours(-_globalHoursBack);
}

// Check if zoomed significantly beyond current data range (more than 10% wider)
var currentSpan = (currentTo - currentFrom).TotalMinutes;
var newSpan = (toDate - fromDate).TotalMinutes;

bool zoomedOut = fromDate < currentFrom.AddMinutes(-currentSpan * 0.1) ||
toDate > currentTo.AddMinutes(currentSpan * 0.1);

if (zoomedOut && newSpan > currentSpan * 1.1)
{
// Auto-apply the zoomed-out range to fetch more data
_isApplyingZoom = true;
await ZoomToTimeRange(fromDate, toDate);
_isApplyingZoom = false;
}
}
catch (Exception ex)
{
// Log but don't show error for zoom operations - they're non-critical
Logger.Warning($"Chart zoom error: {ex.Message}");
}
}

private async void ServerTab_Loaded(object sender, RoutedEventArgs e)
{
try
Expand Down Expand Up @@ -814,12 +545,6 @@ private async void RefreshButton_Click(object sender, RoutedEventArgs e)
private DateTime? _originalToDate = null;
private bool _isZoomed = false;

// Debounce timer for auto-applying chart zoom
private DispatcherTimer? _chartZoomDebounceTimer;
private WpfPlot? _lastZoomedChart;
private bool _isApplyingZoom = false;
private bool _isAutoScaling = false;

private async void GlobalTimeRange_Click(object sender, RoutedEventArgs e)
{
try
Expand Down
23 changes: 23 additions & 0 deletions Lite/Controls/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe
_currentWaitsDurationHover = new Helpers.ChartHoverHelper(CurrentWaitsDurationChart, "ms");
_currentWaitsBlockedHover = new Helpers.ChartHoverHelper(CurrentWaitsBlockedChart, "sessions");

/* Chart context menus (right-click save/export) */
Helpers.ContextMenuHelper.SetupChartContextMenu(WaitStatsChart, "Wait_Stats");
Helpers.ContextMenuHelper.SetupChartContextMenu(QueryDurationTrendChart, "Query_Duration_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(ProcDurationTrendChart, "Procedure_Duration_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(QueryStoreDurationTrendChart, "QueryStore_Duration_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(ExecutionCountTrendChart, "Execution_Count_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(CpuChart, "CPU_Usage");
Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryChart, "Memory_Usage");
Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryClerksChart, "Memory_Clerks");
Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryGrantSizingChart, "Memory_Grant_Sizing");
Helpers.ContextMenuHelper.SetupChartContextMenu(MemoryGrantActivityChart, "Memory_Grant_Activity");
Helpers.ContextMenuHelper.SetupChartContextMenu(FileIoReadChart, "File_IO_Read_Latency");
Helpers.ContextMenuHelper.SetupChartContextMenu(FileIoWriteChart, "File_IO_Write_Latency");
Helpers.ContextMenuHelper.SetupChartContextMenu(TempDbChart, "TempDB_Stats");
Helpers.ContextMenuHelper.SetupChartContextMenu(TempDbFileIoChart, "TempDB_File_IO");
Helpers.ContextMenuHelper.SetupChartContextMenu(LockWaitTrendChart, "Lock_Wait_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(BlockingTrendChart, "Blocking_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(DeadlockTrendChart, "Deadlock_Trends");
Helpers.ContextMenuHelper.SetupChartContextMenu(CurrentWaitsDurationChart, "Current_Waits_Duration");
Helpers.ContextMenuHelper.SetupChartContextMenu(CurrentWaitsBlockedChart, "Current_Waits_Blocked");
Helpers.ContextMenuHelper.SetupChartContextMenu(PerfmonChart, "Perfmon_Counters");
Helpers.ContextMenuHelper.SetupChartContextMenu(CollectorDurationChart, "Collector_Duration");

/* Initial load is triggered by MainWindow.ConnectToServer calling RefreshData()
after collectors finish - no Loaded handler needed */
}
Expand Down
Loading
Loading