diff --git a/.github/workflows/check-pr-branch.yml b/.github/workflows/check-pr-branch.yml new file mode 100644 index 00000000..88fa6fae --- /dev/null +++ b/.github/workflows/check-pr-branch.yml @@ -0,0 +1,21 @@ +name: Check pull request target branch +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - edited +jobs: + check-branches: + runs-on: ubuntu-latest + steps: + - name: Check branches + env: + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} + run: | + if [ "$HEAD_REF" != "dev" ] && [ "$BASE_REF" == "main" ]; then + echo "::error::Pull requests to main are only allowed from dev. Please target the dev branch instead." + exit 1 + fi diff --git a/Dashboard/AboutWindow.xaml b/Dashboard/AboutWindow.xaml index 432be912..b8760fa4 100644 --- a/Dashboard/AboutWindow.xaml +++ b/Dashboard/AboutWindow.xaml @@ -43,13 +43,12 @@ Check for Updates + - + - - - + www.erikdarling.com diff --git a/Dashboard/AboutWindow.xaml.cs b/Dashboard/AboutWindow.xaml.cs index e661dfd5..0e61086e 100644 --- a/Dashboard/AboutWindow.xaml.cs +++ b/Dashboard/AboutWindow.xaml.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Reflection; using System.Windows; +using PerformanceMonitorDashboard.Services; namespace PerformanceMonitorDashboard { @@ -17,6 +18,8 @@ public partial class AboutWindow : Window private const string ReleasesUrl = "https://github.com/erikdarlingdata/PerformanceMonitor/releases"; private const string DarlingDataUrl = "https://www.erikdarling.com"; + private string? _updateReleaseUrl; + public AboutWindow() { InitializeComponent(); @@ -39,9 +42,37 @@ private void ReportIssueLink_Click(object sender, RoutedEventArgs e) OpenUrl(IssuesUrl); } - private void CheckUpdatesLink_Click(object sender, RoutedEventArgs e) + private async void CheckUpdatesLink_Click(object sender, RoutedEventArgs e) + { + UpdateStatusText.Text = "Checking for updates..."; + UpdateStatusText.Visibility = Visibility.Visible; + + var result = await UpdateCheckService.CheckForUpdateAsync(bypassCache: true); + + if (result == null) + { + UpdateStatusText.Text = "Unable to check for updates. Please try again later."; + } + else if (result.IsUpdateAvailable) + { + _updateReleaseUrl = result.ReleaseUrl; + UpdateStatusText.Text = $"Update available: {result.LatestVersion} (you have {result.CurrentVersion})"; + UpdateStatusText.Cursor = System.Windows.Input.Cursors.Hand; + UpdateStatusText.MouseLeftButtonUp += UpdateStatusText_Click; + UpdateStatusText.TextDecorations = System.Windows.TextDecorations.Underline; + UpdateStatusText.Foreground = FindResource("AccentBrush") as System.Windows.Media.Brush + ?? System.Windows.Media.Brushes.DodgerBlue; + } + else + { + UpdateStatusText.Text = $"You're up to date ({result.CurrentVersion})"; + } + } + + private void UpdateStatusText_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { - OpenUrl(ReleasesUrl); + if (!string.IsNullOrEmpty(_updateReleaseUrl)) + OpenUrl(_updateReleaseUrl); } private void DarlingDataLink_Click(object sender, RoutedEventArgs e) diff --git a/Dashboard/App.xaml.cs b/Dashboard/App.xaml.cs index 3e257435..1a17661f 100644 --- a/Dashboard/App.xaml.cs +++ b/Dashboard/App.xaml.cs @@ -65,6 +65,7 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) { var exception = e.ExceptionObject as Exception; diff --git a/Dashboard/Controls/LandingPage.xaml b/Dashboard/Controls/LandingPage.xaml index 250f2a10..7e99b049 100644 --- a/Dashboard/Controls/LandingPage.xaml +++ b/Dashboard/Controls/LandingPage.xaml @@ -113,15 +113,15 @@ Padding="16,8"> - + - + - + diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs index 0b7e662d..56ffdec8 100644 --- a/Dashboard/Controls/MemoryContent.xaml.cs +++ b/Dashboard/Controls/MemoryContent.xaml.cs @@ -213,26 +213,26 @@ private void LoadMemoryStatsOverviewChart(List memoryData, int var totalScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(totalXs, totalYs); totalScatter.LineWidth = 2; - totalScatter.MarkerSize = 0; - totalScatter.Color = ScottPlot.Colors.Gray; + totalScatter.MarkerSize = 5; + totalScatter.Color = TabHelpers.ChartColors[9]; totalScatter.LegendText = "Total Memory"; var bufferScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(bufferXs, bufferYs); bufferScatter.LineWidth = 2; - bufferScatter.MarkerSize = 0; - bufferScatter.Color = ScottPlot.Colors.Blue; + bufferScatter.MarkerSize = 5; + bufferScatter.Color = TabHelpers.ChartColors[0]; bufferScatter.LegendText = "Buffer Pool"; var cacheScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(cacheXs, cacheYs); cacheScatter.LineWidth = 2; - cacheScatter.MarkerSize = 0; - cacheScatter.Color = ScottPlot.Colors.Green; + cacheScatter.MarkerSize = 5; + cacheScatter.Color = TabHelpers.ChartColors[1]; cacheScatter.LegendText = "Plan Cache"; var availScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(availXs, availYs); availScatter.LineWidth = 2; - availScatter.MarkerSize = 0; - availScatter.Color = ScottPlot.Colors.Orange; + availScatter.MarkerSize = 5; + availScatter.Color = TabHelpers.ChartColors[2]; availScatter.LegendText = "Available Physical"; _legendPanels[MemoryStatsOverviewChart] = MemoryStatsOverviewChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -282,7 +282,7 @@ private void AddPressureWarningSpans(List dataList) if (item.BufferPoolPressureWarning && item.PlanCachePressureWarning) { - vline.Color = ScottPlot.Colors.Red.WithAlpha(0.5); + vline.Color = TabHelpers.ChartColors[3].WithAlpha(0.5); // Add legend entry for BP pressure (covers "both" case too) if (!bpLegendAdded) { @@ -292,7 +292,7 @@ private void AddPressureWarningSpans(List dataList) } else if (item.BufferPoolPressureWarning) { - vline.Color = ScottPlot.Colors.Red.WithAlpha(0.3); + vline.Color = TabHelpers.ChartColors[3].WithAlpha(0.3); if (!bpLegendAdded) { vline.LegendText = "BP Pressure"; @@ -301,7 +301,7 @@ private void AddPressureWarningSpans(List dataList) } else { - vline.Color = ScottPlot.Colors.Orange.WithAlpha(0.3); + vline.Color = TabHelpers.ChartColors[2].WithAlpha(0.3); if (!pcLegendAdded) { vline.LegendText = "PC Pressure"; @@ -419,14 +419,14 @@ private void LoadMemoryGrantsChart(IEnumerable data, int h { var grantedScatter = MemoryGrantsChart.Plot.Add.Scatter(grantedXs, grantedYs); grantedScatter.LineWidth = 2; - grantedScatter.MarkerSize = 0; - grantedScatter.Color = ScottPlot.Colors.Blue; + grantedScatter.MarkerSize = 5; + grantedScatter.Color = TabHelpers.ChartColors[0]; grantedScatter.LegendText = "Granted MB"; var targetScatter = MemoryGrantsChart.Plot.Add.Scatter(targetXs, targetYs); targetScatter.LineWidth = 2; - targetScatter.MarkerSize = 0; - targetScatter.Color = ScottPlot.Colors.Orange; + targetScatter.MarkerSize = 5; + targetScatter.Color = TabHelpers.ChartColors[2]; targetScatter.LegendText = "Target MB"; _legendPanels[MemoryGrantsChart] = MemoryGrantsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -502,7 +502,7 @@ private void LoadMemoryClerksChart(List data, int hoursBack, D .Select(x => x.ClerkType) .ToList(); - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, ScottPlot.Colors.Purple }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var clerkType in topClerks) @@ -519,7 +519,7 @@ private void LoadMemoryClerksChart(List data, int hoursBack, D var scatter = MemoryClerksChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = clerkType.Length > 20 ? clerkType.Substring(0, 20) + "..." : clerkType; colorIndex++; @@ -647,8 +647,8 @@ private void LoadPlanCacheChart(IEnumerable data, int hoursB var singleScatter = PlanCacheChart.Plot.Add.Scatter(singleXs, singleYs); singleScatter.LineWidth = 2; - singleScatter.MarkerSize = 0; - singleScatter.Color = ScottPlot.Colors.Red; + singleScatter.MarkerSize = 5; + singleScatter.Color = TabHelpers.ChartColors[3]; singleScatter.LegendText = "Single-Use"; // Multi-Use series with gap filling @@ -658,8 +658,8 @@ private void LoadPlanCacheChart(IEnumerable data, int hoursB var multiScatter = PlanCacheChart.Plot.Add.Scatter(multiXs, multiYs); multiScatter.LineWidth = 2; - multiScatter.MarkerSize = 0; - multiScatter.Color = ScottPlot.Colors.Green; + multiScatter.MarkerSize = 5; + multiScatter.Color = TabHelpers.ChartColors[1]; multiScatter.LegendText = "Multi-Use"; _legendPanels[PlanCacheChart] = PlanCacheChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -786,7 +786,7 @@ private void LoadMemoryPressureEventsChart(IEnumerable var highScatter = MemoryPressureEventsChart.Plot.Add.Scatter(xs, ys); highScatter.LineWidth = 2; highScatter.MarkerSize = 5; - highScatter.Color = ScottPlot.Colors.Red; + highScatter.Color = TabHelpers.ChartColors[3]; highScatter.LegendText = "High Pressure Events"; _legendPanels[MemoryPressureEventsChart] = MemoryPressureEventsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index c4396979..34509c4e 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -243,9 +243,9 @@ await Task.WhenAll( QueryStoreNoDataMessage.Visibility = queryStore.Count == 0 ? Visibility.Visible : Visibility.Collapsed; // Populate charts from time-series data - LoadDurationChart(QueryPerfTrendsQueryChart, await queryDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", ScottPlot.Colors.Blue); - LoadDurationChart(QueryPerfTrendsProcChart, await procDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", ScottPlot.Colors.Green); - LoadDurationChart(QueryPerfTrendsQsChart, await qsDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", ScottPlot.Colors.Purple); + LoadDurationChart(QueryPerfTrendsQueryChart, await queryDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", TabHelpers.ChartColors[0]); + LoadDurationChart(QueryPerfTrendsProcChart, await procDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", TabHelpers.ChartColors[1]); + LoadDurationChart(QueryPerfTrendsQsChart, await qsDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", TabHelpers.ChartColors[4]); LoadExecChart(await execTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate); } catch (Exception ex) @@ -915,18 +915,13 @@ private void LoadDurationChart(WpfPlot chart, IEnumerable tre dataList.Select(d => d.CollectionTime), dataList.Select(d => d.AvgDurationMs)); - if (xs.Length > 0) - { - var scatter = chart.Plot.Add.Scatter(xs, ys); - scatter.LineWidth = 2; - scatter.MarkerSize = 5; - scatter.Color = color; - scatter.LegendText = legendText; - - _legendPanels[chart] = chart.Plot.ShowLegend(ScottPlot.Edge.Bottom); - chart.Plot.Legend.FontSize = 12; - } - else + var scatter = chart.Plot.Add.Scatter(xs, ys); + scatter.LineWidth = 2; + scatter.MarkerSize = 5; + scatter.Color = color; + scatter.LegendText = legendText; + + if (xs.Length == 0) { double xCenter = xMin + (xMax - xMin) / 2; var noDataText = chart.Plot.Add.Text("No data for selected time range", xCenter, 0.5); @@ -935,6 +930,9 @@ private void LoadDurationChart(WpfPlot chart, IEnumerable tre noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } + _legendPanels[chart] = chart.Plot.ShowLegend(ScottPlot.Edge.Bottom); + chart.Plot.Legend.FontSize = 12; + chart.Plot.Axes.DateTimeTicksBottom(); chart.Plot.Axes.SetLimitsX(xMin, xMax); chart.Plot.YLabel("Duration (ms/sec)"); @@ -971,18 +969,13 @@ private void LoadExecChart(IEnumerable execTrends, int hours dataList.Select(d => d.CollectionTime), dataList.Select(d => (double)d.ExecutionsPerSecond)); - if (xs.Length > 0) - { - var scatter = QueryPerfTrendsExecChart.Plot.Add.Scatter(xs, ys); - scatter.LineWidth = 2; - scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Blue; - scatter.LegendText = "Executions/sec"; + var scatter = QueryPerfTrendsExecChart.Plot.Add.Scatter(xs, ys); + scatter.LineWidth = 2; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[0]; + scatter.LegendText = "Executions/sec"; - _legendPanels[QueryPerfTrendsExecChart] = QueryPerfTrendsExecChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); - QueryPerfTrendsExecChart.Plot.Legend.FontSize = 12; - } - else + if (xs.Length == 0) { double xCenter = xMin + (xMax - xMin) / 2; var noDataText = QueryPerfTrendsExecChart.Plot.Add.Text("No data for selected time range", xCenter, 0.5); @@ -991,6 +984,9 @@ private void LoadExecChart(IEnumerable execTrends, int hours noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } + _legendPanels[QueryPerfTrendsExecChart] = QueryPerfTrendsExecChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); + QueryPerfTrendsExecChart.Plot.Legend.FontSize = 12; + QueryPerfTrendsExecChart.Plot.Axes.DateTimeTicksBottom(); QueryPerfTrendsExecChart.Plot.Axes.SetLimitsX(xMin, xMax); QueryPerfTrendsExecChart.Plot.YLabel("Executions/sec"); diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml b/Dashboard/Controls/ResourceMetricsContent.xaml index 52b06e2c..9ea43c3e 100644 --- a/Dashboard/Controls/ResourceMetricsContent.xaml +++ b/Dashboard/Controls/ResourceMetricsContent.xaml @@ -110,8 +110,9 @@