diff --git a/src/Aspire.Dashboard/Components/Controls/Grid/AspireTemplateColumn.cs b/src/Aspire.Dashboard/Components/Controls/Grid/AspireTemplateColumn.cs index 95781857f0f..ec5adb9e82d 100644 --- a/src/Aspire.Dashboard/Components/Controls/Grid/AspireTemplateColumn.cs +++ b/src/Aspire.Dashboard/Components/Controls/Grid/AspireTemplateColumn.cs @@ -17,6 +17,11 @@ public class AspireTemplateColumn : TemplateColumn, IAspir protected override void OnInitialized() { Tooltip = true; + + if (ColumnManager is not null && ColumnId is not null) + { + Width = ColumnManager.GetColumnWidth(ColumnId); + } } protected override bool ShouldRender() diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor b/src/Aspire.Dashboard/Components/Controls/GridValue.razor index ebbaf904ed6..fda87aa4714 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor @@ -3,7 +3,7 @@ @inject IStringLocalizer Loc @inject IStringLocalizer DialogsLoc -
+
@* Value area *@ diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index fa6c77155b6..4875352de47 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -127,18 +127,18 @@ Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" - OverscanCount="100" + OverscanCount="@DashboardUIHelpers.DefaultDataGridOverscanCount" ItemsProvider="@GetData" ResizableColumns="true" ResizeColumnOnAllRows="false" - GridTemplateColumns="@_manager.GetGridTemplateColumns()" RowClass="@(r => GetRowClass(r.Resource))" Loading="!_loadingTcs.Task.IsCompleted" ShowHover="true" TGridItem="ResourceGridViewModel" ItemKey="@(r => r.Resource.Name)" OnRowClick="@(r => r.ExecuteOnDefault(d => ShowResourceDetailsAsync(d.Resource, buttonId: null)))" - Class="main-grid enable-row-click"> + Class="main-grid enable-row-click" + DisplayMode="DataGridDisplayMode.Table"> @{ diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 5b3b0f24626..87b9e143a93 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -34,7 +34,7 @@ public partial class Resources : ComponentBase, IComponentWithTelemetry, IAsyncD private const string ActionsColumn = nameof(ActionsColumn); private Subscription? _logsSubscription; - private IList? _gridColumns; + private List? _gridColumns; private EventCallback _onToggleCollapseAllCallback; private EventCallback _onToggleResourceTypeCallback; private bool _hideResourceGraph; @@ -175,13 +175,13 @@ protected override async Task OnInitializedAsync() (_resizeLabels, _sortLabels) = DashboardUIHelpers.CreateGridLabels(ControlsStringsLoc); _gridColumns = [ - new GridColumn(Name: NameColumn, DesktopWidth: "1.5fr", MobileWidth: "1.5fr"), - new GridColumn(Name: StateColumn, DesktopWidth: "1.25fr", MobileWidth: "1.25fr"), - new GridColumn(Name: StartTimeColumn, DesktopWidth: "1fr"), - new GridColumn(Name: TypeColumn, DesktopWidth: "1fr", IsVisible: () => _showResourceTypeColumn), - new GridColumn(Name: SourceColumn, DesktopWidth: "2.25fr"), - new GridColumn(Name: UrlsColumn, DesktopWidth: "2.25fr", MobileWidth: "2fr"), - new GridColumn(Name: ActionsColumn, DesktopWidth: "minmax(150px, 1.5fr)", MobileWidth: "1fr") + new GridColumn(Name: NameColumn, DesktopWidth: Width.Fraction(1.5m), MobileWidth: Width.Fraction(1.5m)), + new GridColumn(Name: StateColumn, DesktopWidth: Width.Fraction(1.25m), MobileWidth: Width.Fraction(1.25m)), + new GridColumn(Name: StartTimeColumn, DesktopWidth: Width.Fraction(1)), + new GridColumn(Name: TypeColumn, DesktopWidth: Width.Fraction(1), IsVisible: () => _showResourceTypeColumn), + new GridColumn(Name: SourceColumn, DesktopWidth: Width.Fraction(2.25m)), + new GridColumn(Name: UrlsColumn, DesktopWidth: Width.Fraction(2.25m), MobileWidth: Width.Fraction(2)), + new GridColumn(Name: ActionsColumn, DesktopWidth: Width.Fraction(1.5m), MobileWidth: Width.Fraction(1)) ]; _onToggleCollapseAllCallback = EventCallback.Factory.Create(this, OnToggleCollapseAll); diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor index 044d9c6b9bd..c1303f4f56f 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor @@ -122,16 +122,16 @@ RowClass="@GetRowClass" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" - OverscanCount="100" + OverscanCount="@DashboardUIHelpers.DefaultDataGridOverscanCount" ResizableColumns="true" ResizeColumnOnAllRows="false" ItemsProvider="@GetData" TGridItem="OtlpLogEntry" - GridTemplateColumns="@_manager.GetGridTemplateColumns()" ShowHover="true" ItemKey="@(r => r.InternalId)" OnRowClick="@(r => r.ExecuteOnDefault(d => OnShowPropertiesAsync(d, buttonId: null)))" - Class="main-grid enable-row-click"> + Class="main-grid enable-row-click" + DisplayMode="DataGridDisplayMode.Table"> diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs index 1d34d9453a3..7f4c0258607 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs @@ -44,7 +44,7 @@ public partial class StructuredLogs : IComponentWithTelemetry, IPageWithSessionA private string _filter = string.Empty; private FluentDataGrid _dataGrid = null!; private GridColumnManager _manager = null!; - private IList _gridColumns = null!; + private List _gridColumns = null!; private ColumnResizeLabels _resizeLabels = ColumnResizeLabels.Default; private ColumnSortLabels _sortLabels = ColumnSortLabels.Default; @@ -152,12 +152,12 @@ protected override void OnInitialized() (_resizeLabels, _sortLabels) = DashboardUIHelpers.CreateGridLabels(ControlsStringsLoc); _gridColumns = [ - new GridColumn(Name: ResourceColumn, DesktopWidth: "2fr", MobileWidth: "1fr"), - new GridColumn(Name: LogLevelColumn, DesktopWidth: "1fr"), - new GridColumn(Name: TimestampColumn, DesktopWidth: "1.5fr"), - new GridColumn(Name: MessageColumn, DesktopWidth: "5fr", "2.5fr"), - new GridColumn(Name: TraceColumn, DesktopWidth: "1fr"), - new GridColumn(Name: ActionsColumn, DesktopWidth: "1fr", MobileWidth: "0.8fr") + new GridColumn(Name: ResourceColumn, DesktopWidth: Width.Fraction(2), MobileWidth: Width.Fraction(1)), + new GridColumn(Name: LogLevelColumn, DesktopWidth: Width.Fraction(1)), + new GridColumn(Name: TimestampColumn, DesktopWidth: Width.Fraction(1.5m)), + new GridColumn(Name: MessageColumn, DesktopWidth: Width.Fraction(5), MobileWidth: Width.Fraction(2.5m)), + new GridColumn(Name: TraceColumn, DesktopWidth: Width.Fraction(1)), + new GridColumn(Name: ActionsColumn, DesktopWidth: Width.Fraction(1), MobileWidth: Width.Fraction(0.8m)) ]; if (!string.IsNullOrEmpty(TraceId)) diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor index 524bbe2ca13..37e34567abd 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor @@ -107,12 +107,12 @@ ItemsProvider="@GetData" TGridItem="SpanWaterfallViewModel" RowClass="@GetRowClass" - GridTemplateColumns="@_manager.GetGridTemplateColumns()" RowSize="DataGridRowSize.Small" - OverscanCount="100" + OverscanCount="@DashboardUIHelpers.DefaultDataGridOverscanCount" ShowHover="true" ItemKey="@(r => r.Span.SpanId)" - OnRowClick="@(r => r.ExecuteOnDefault(d => OnShowPropertiesAsync(d, buttonId: null)))"> + OnRowClick="@(r => r.ExecuteOnDefault(d => OnShowPropertiesAsync(d, buttonId: null)))" + DisplayMode="DataGridDisplayMode.Table"> @{ var isServerOrConsumer = context.Span.Kind == OtlpSpanKind.Server || context.Span.Kind == OtlpSpanKind.Consumer; diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs index f78674e2e9f..eeca09c4c0f 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs @@ -35,7 +35,7 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp private string? _elementIdBeforeDetailsViewOpened; private FluentDataGrid _dataGrid = null!; private GridColumnManager _manager = null!; - private IList _gridColumns = null!; + private List _gridColumns = null!; private string _filter = string.Empty; [Parameter] @@ -83,9 +83,9 @@ protected override void OnInitialized() TelemetryContextProvider.Initialize(TelemetryContext); _gridColumns = [ - new GridColumn(Name: NameColumn, DesktopWidth: "6fr", MobileWidth: "6fr"), - new GridColumn(Name: TicksColumn, DesktopWidth: "12fr", MobileWidth: "12fr"), - new GridColumn(Name: ActionsColumn, DesktopWidth: "100px", MobileWidth: null) + new GridColumn(Name: NameColumn, DesktopWidth: Width.Fraction(6), MobileWidth: Width.Fraction(6)), + new GridColumn(Name: TicksColumn, DesktopWidth: Width.Fraction(12), MobileWidth: Width.Fraction(12)), + new GridColumn(Name: ActionsColumn, DesktopWidth: Width.Pixels(100), MobileWidth: null) ]; foreach (var resolver in OutgoingPeerResolvers) diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor b/src/Aspire.Dashboard/Components/Pages/Traces.razor index f86ae4a4da3..dee92d63375 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor @@ -92,16 +92,16 @@ RowClass="@GetRowClass" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" - OverscanCount="100" + OverscanCount="@DashboardUIHelpers.DefaultDataGridOverscanCount" ResizableColumns="true" ResizeColumnOnAllRows="false" ItemsProvider="@GetData" TGridItem="OtlpTrace" - GridTemplateColumns="@_manager.GetGridTemplateColumns()" ShowHover="true" ItemKey="@(r => r.TraceId)" OnRowClick="@(r => r.ExecuteOnDefault(d => NavigationManager.NavigateTo(DashboardUrls.TraceDetailUrl(d.TraceId))))" - Class="main-grid enable-row-click"> + Class="main-grid enable-row-click" + DisplayMode="DataGridDisplayMode.Table"> @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.FirstSpan.StartTime, MillisecondsDisplay.Truncated) diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs b/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs index 9e3d1936d91..40406deb1b1 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs @@ -27,7 +27,7 @@ public partial class Traces : IComponentWithTelemetry, IPageWithSessionAndUrlSta private const string SpansColumn = nameof(SpansColumn); private const string DurationColumn = nameof(DurationColumn); private const string ActionsColumn = nameof(ActionsColumn); - private IList _gridColumns = null!; + private List _gridColumns = null!; private SelectViewModel _allApplication = null!; private TotalItemsFooter _totalItemsFooter = default!; @@ -154,11 +154,11 @@ protected override void OnInitialized() (_resizeLabels, _sortLabels) = DashboardUIHelpers.CreateGridLabels(ControlsStringsLoc); _gridColumns = [ - new GridColumn(Name: TimestampColumn, DesktopWidth: "0.8fr", MobileWidth: "0.8fr"), - new GridColumn(Name: NameColumn, DesktopWidth: "2fr", MobileWidth: "2fr"), - new GridColumn(Name: SpansColumn, DesktopWidth: "3fr"), - new GridColumn(Name: DurationColumn, DesktopWidth: "0.8fr"), - new GridColumn(Name: ActionsColumn, DesktopWidth: "0.5fr", MobileWidth: "1fr") + new GridColumn(Name: TimestampColumn, DesktopWidth: Width.Fraction(0.8m), MobileWidth: Width.Fraction(0.8m)), + new GridColumn(Name: NameColumn, DesktopWidth: Width.Fraction(2), MobileWidth: Width.Fraction(2)), + new GridColumn(Name: SpansColumn, DesktopWidth: Width.Fraction(3)), + new GridColumn(Name: DurationColumn, DesktopWidth: Width.Fraction(0.8m)), + new GridColumn(Name: ActionsColumn, DesktopWidth: Width.Fraction(0.5m), MobileWidth: Width.Fraction(1)) ]; _allApplication = new SelectViewModel { Id = null, Name = ControlsStringsLoc[name: nameof(ControlsStrings.LabelAll)] }; diff --git a/src/Aspire.Dashboard/Components/Resize/GridColumnManager.razor.cs b/src/Aspire.Dashboard/Components/Resize/GridColumnManager.razor.cs index 6a5a1247432..fc1a375f025 100644 --- a/src/Aspire.Dashboard/Components/Resize/GridColumnManager.razor.cs +++ b/src/Aspire.Dashboard/Components/Resize/GridColumnManager.razor.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; using Aspire.Dashboard.Model; using Microsoft.AspNetCore.Components; @@ -9,7 +8,8 @@ namespace Aspire.Dashboard.Components.Resize; public partial class GridColumnManager : ComponentBase, IDisposable { - private Dictionary _columnById = null!; + private Dictionary _columnDesktopById = null!; + private Dictionary _columnMobileById = null!; private float _availableFraction = 1; private ViewportInformation? _gridViewportInformation; @@ -17,7 +17,7 @@ public partial class GridColumnManager : ComponentBase, IDisposable public required DimensionManager DimensionManager { get; init; } [Parameter] - public required IList Columns { get; set; } + public required List Columns { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } @@ -27,7 +27,60 @@ public partial class GridColumnManager : ComponentBase, IDisposable protected override void OnInitialized() { DimensionManager.OnViewportSizeChanged += OnViewportSizeChanged; - _columnById = Columns.ToDictionary(c => c.Name, StringComparers.GridColumn); + } + + protected override void OnParametersSet() + { + _columnDesktopById = Columns.Where(c => c.DesktopWidth is not null) + .Select(c => new GridColumnView(c.Name, c.DesktopWidth!.Value, c.IsVisible)) + .ToDictionary(c => c.Name, StringComparers.GridColumn); + _columnMobileById = Columns.Where(c => c.MobileWidth is not null) + .Select(c => new GridColumnView(c.Name, c.MobileWidth!.Value, c.IsVisible)) + .ToDictionary(c => c.Name, StringComparers.GridColumn); + + if (ViewportInformation.IsDesktop) + { + SetWidths(_columnDesktopById); + } + else + { + SetWidths(_columnMobileById); + } + } + + private static void SetWidths(Dictionary columnById) + { + var visibleColumns = columnById.Values.Where(c => c.IsVisible?.Invoke() is null or true).ToList(); + var lastPercentageColumn = columnById.Values.Where(c => c.IsVisible?.Invoke() is null or true).LastOrDefault(); + var fractionTotal = visibleColumns.Where(c => c.Width is { Unit: WidthUnit.Fraction }).Sum(c => c.Width.Value); + + // We want percentages to add up to exactly 100% on the browser. This can be a problem with rounding. + // The fix is to use the remaining percentage value for the value percentage column. + var remainingPercentage = 100m; + for (var i = 0; i < visibleColumns.Count; i++) + { + var column = visibleColumns[i]; + + if (column.Width.Unit == WidthUnit.Pixels) + { + column.ResolvedBrowserWidth = $"{column.Width.Value}px"; + } + else + { + var isLast = column == lastPercentageColumn; + if (isLast) + { + column.ResolvedBrowserWidth = $"{remainingPercentage}%"; + } + else + { + var percentage = Math.Round(column.Width.Value / fractionTotal * 100, 1); + column.ResolvedBrowserWidth = $"{percentage}%"; + + remainingPercentage -= percentage; + } + } + } } private void OnViewportSizeChanged(object sender, ViewportSizeChangedEventArgs e) @@ -67,44 +120,36 @@ public void SetWidthFraction(float fraction) /// public bool IsColumnVisible(string columnName) { - return _columnById.TryGetValue(columnName, out var column) // Is a known column. - && GetColumnWidth(column) is not null // Has width for current viewport. - && column.IsVisible?.Invoke() is null or true; // Is visible. + if (GetColumnView(columnName) is not { } column) + { + return false; + } + + return column.IsVisible?.Invoke() is null or true; } - /// - /// Gets a string that can be used as the value for the grid-template-columns CSS property. - /// For example, 1fr 2fr 1fr. - /// - /// - public string GetGridTemplateColumns() + public string? GetColumnWidth(string columnName) { - var sb = new StringBuilder(); - - foreach (var (_, column) in _columnById) + if (GetColumnView(columnName) is not { } column) { - if (column.IsVisible?.Invoke() is null or true && - GetColumnWidth(column) is string width) - { - if (sb.Length > 0) - { - sb.Append(' '); - } - - sb.Append(width); - } + return null; } - return sb.ToString(); + return column.ResolvedBrowserWidth; } - private string? GetColumnWidth(GridColumn column) + private GridColumnView? GetColumnView(string columnName) { - var viewportInformation = _gridViewportInformation ?? DimensionManager.ViewportInformation; + var columnsById = ViewportInformation.IsDesktop + ? _columnDesktopById + : _columnMobileById; + + if (!columnsById.TryGetValue(columnName, out var column)) // Is a known column. + { + return null; + } - return viewportInformation.IsDesktop - ? column.DesktopWidth - : column.MobileWidth; + return column; } public void Dispose() diff --git a/src/Aspire.Dashboard/Model/GridColumn.cs b/src/Aspire.Dashboard/Model/GridColumn.cs index 76126685fc4..c7d95e28398 100644 --- a/src/Aspire.Dashboard/Model/GridColumn.cs +++ b/src/Aspire.Dashboard/Model/GridColumn.cs @@ -3,4 +3,21 @@ namespace Aspire.Dashboard.Model; -public record GridColumn(string Name, string? DesktopWidth, string? MobileWidth = null, Func? IsVisible = null); +public record GridColumn(string Name, Width? DesktopWidth, Width? MobileWidth = null, Func? IsVisible = null); + +public record GridColumnView(string Name, Width Width, Func? IsVisible = null) +{ + public string? ResolvedBrowserWidth { get; set; } +} + +public enum WidthUnit +{ + Pixels, + Fraction +} + +public record struct Width(decimal Value, WidthUnit Unit) +{ + public static Width Pixels(decimal value) => new(value, WidthUnit.Pixels); + public static Width Fraction(decimal value) => new(value, WidthUnit.Fraction); +} diff --git a/src/Aspire.Dashboard/Utils/DashboardUIHelpers.cs b/src/Aspire.Dashboard/Utils/DashboardUIHelpers.cs index bcd533ce063..b6b862077ab 100644 --- a/src/Aspire.Dashboard/Utils/DashboardUIHelpers.cs +++ b/src/Aspire.Dashboard/Utils/DashboardUIHelpers.cs @@ -14,6 +14,8 @@ internal static class DashboardUIHelpers { public const string MessageBarSection = "MessagesTop"; + public const int DefaultDataGridOverscanCount = 20; + // The initial data fetch for a FluentDataGrid doesn't include a count of items to return. // The data grid doesn't specify a count because it doesn't know how many items fit in the UI. // Once it knows the height of items and the height of the grid then it specifies the desired item count