Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace data grid column headers with custom template #5266

Merged
17 changes: 11 additions & 6 deletions src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@namespace Aspire.Dashboard.Components

@using Aspire.Dashboard.Components.Controls.Grid
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Model
Expand All @@ -10,10 +11,14 @@
{
<div class="metrics-filters-section">
<h5>@Loc[nameof(ControlsStrings.ChartContainerFiltersHeader)]</h5>
<FluentDataGrid Items="@Queryable.AsQueryable(DimensionFilters)" GridTemplateColumns="200px 1fr auto" GenerateHeader="GenerateHeaderOption.None">
<FluentDataGrid ResizeLabel="@AspireFluentDataGridHeaderCell.GetResizeLabel(Loc)"
ResizeType="DataGridResizeType.Discrete"
Items="@Queryable.AsQueryable(DimensionFilters)"
GridTemplateColumns="200px 1fr auto"
GenerateHeader="GenerateHeaderOption.None">
<ChildContent>
<PropertyColumn Tooltip="true" TooltipText="@(c => c.Name)" Property="@(c => c.Name)"/>
<TemplateColumn Tooltip="true" TooltipText="@(c => c.SelectedValues.Count == 0 ? Loc[nameof(ControlsStrings.None)] : string.Join(", ", c.SelectedValues.Select(v => v.Name)))">
<AspirePropertyColumn Tooltip="true" TooltipText="@(c => c.Name)" Property="@(c => c.Name)"/>
<AspireTemplateColumn Tooltip="true" TooltipText="@(c => c.SelectedValues.Count == 0 ? Loc[nameof(ControlsStrings.None)] : string.Join(", ", c.SelectedValues.Select(v => v.Name)))">
<FluentOverflow Class="dimension-overflow">
<ChildContent>
@if (context.SelectedValues.Count == 0)
Expand Down Expand Up @@ -43,8 +48,8 @@
@* Intentionally empty. Don't display an overflow template here. *@
</OverflowTemplate>
</FluentOverflow>
</TemplateColumn>
<TemplateColumn>
</AspireTemplateColumn>
<AspireTemplateColumn>
@{
var id = $"typeFilterButton-{context.SanitizedHtmlId}-{Guid.NewGuid()}";
}
Expand Down Expand Up @@ -74,7 +79,7 @@
</FluentStack>
</Body>
</FluentPopover>
</TemplateColumn>
</AspireTemplateColumn>
</ChildContent>
</FluentDataGrid>
</div>
Expand Down
18 changes: 11 additions & 7 deletions src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
@namespace Aspire.Dashboard.Components.Controls

@using Aspire.Dashboard.Extensions
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@using System.Globalization
@using Aspire.Dashboard.Components.Controls.Grid

@inherits Aspire.Dashboard.Components.Controls.Chart.ChartBase
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc

@{
// these colors line up with P50/P90/P99 colors for the plotly graph
Expand All @@ -19,6 +21,8 @@
<div id="metric-table-container" style="height: 40vh; overflow-y: auto; margin-bottom: 20px; max-width:1200px;">
@* ItemKey is to preserve row focus by associating rows with their associated time *@
<FluentDataGrid
ResizeLabel="@AspireFluentDataGridHeaderCell.GetResizeLabel(ControlsStringsLoc)"
ResizeType="DataGridResizeType.Discrete"
Items="@_metricsView"
ItemSize="46"
Virtualize="true"
Expand All @@ -33,7 +37,7 @@
{
foreach (var (percentile, underlineColor) in percentileColumns)
{
<TemplateColumn Title="@((_metricsView.FirstOrDefault() as HistogramMetricView)?.Percentiles[percentile].Name ?? (_instrument is not null ? $"P{percentile} {InstrumentUnitResolver.ResolveDisplayedUnit(_instrument, titleCase: true, pluralize: true)}" : $"P{percentile}"))">
<AspireTemplateColumn Title="@((_metricsView.FirstOrDefault() as HistogramMetricView)?.Percentiles[percentile].Name ?? (_instrument is not null ? $"P{percentile} {InstrumentUnitResolver.ResolveDisplayedUnit(_instrument, titleCase: true, pluralize: true)}" : $"P{percentile}"))">
@if (context is HistogramMetricView histogramMetric)
{
var percentileData = histogramMetric.Percentiles[percentile];
Expand All @@ -47,13 +51,13 @@
<FluentIcon Style="vertical-align: text-bottom" Value="@icon" Title="@title"/>
}
}
</TemplateColumn>
</AspireTemplateColumn>
}
}
else if (_metrics.Values.All(value => value is MetricValueView))
{
<!-- if we're switching between grid types, this could be false -->
<TemplateColumn Title="@_unitColumnHeader">
<AspireTemplateColumn Title="@_unitColumnHeader">
@{
var metricValueView = context as MetricValueView;
}
Expand All @@ -74,11 +78,11 @@
<FluentIcon Style="vertical-align: text-bottom" Value="@icon" Title="@title"/>
}
}
</TemplateColumn>
</AspireTemplateColumn>
}
@if (_exemplars.Count > 0)
{
<TemplateColumn Title="@Loc[nameof(ControlsStrings.MetricTableExemplarsColumnHeader)]">
<AspireTemplateColumn Title="@Loc[nameof(ControlsStrings.MetricTableExemplarsColumnHeader)]">
@if (context.Exemplars.Count > 0)
{
@* min-width ensures a consistent button width up to 999 metrics *@
Expand All @@ -91,7 +95,7 @@
{
<span>0</span>
}
</TemplateColumn>
</AspireTemplateColumn>
}
</ChildContent>
<EmptyContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@using Aspire.Dashboard.Resources
@typeparam T

<FluentKeyCode Only="@(new[] { KeyCode.Ctrl, KeyCode.Enter })" OnKeyDown="HandleKeyDown" class="keycapture">
<span class="col-sort-container" @oncontextmenu="@(() => Grid.RemoveSortByColumnAsync(Column))" @oncontextmenu:preventDefault>
<FluentButton Disabled="@(!AnyColumnActionEnabled)" Id="@_columnId" Appearance="Appearance.Stealth" class="col-sort-button" @onclick="@HandleColumnHeaderClickedAsync" aria-label="@Tooltip" title="@Tooltip">
<div class="col-title-text" title="@Tooltip">@Column.Title</div>

@if (Grid.SortByAscending.HasValue && Column.ShowSortIcon)
{
if (Grid.SortByAscending == true)
{
<FluentIcon Value="@(new Icons.Regular.Size16.ArrowSortUp())" Slot="@(Column.Align == Align.End ? "start" : "end")" Style="opacity: 0.5; margin-left: -5px;"/>
}
else
{
<FluentIcon Value="@(new Icons.Regular.Size16.ArrowSortDown())" Slot="@(Column.Align == Align.End ? "start" : "end")" Style="opacity: 0.5; margin-left: -5px;"/>
}
}
@if (Grid.ResizeType is not null && Column.ColumnOptions is not null)
{
@if (Column.Filtered.GetValueOrDefault())
{
<FluentIcon Value="@(new Icons.Regular.Size16.Filter())" Slot="@(Column.Align == Align.End ? "start" : "end")" Style="opacity: 0.5; margin-left: -5px;"/>
}
}
</FluentButton>

<FluentMenu Anchor="@_columnId" @bind-Open="@_isMenuOpen">
<FluentMenuItem OnClick="@(async () => await Grid.SortByColumnAsync(Column))">
@GetSortOptionText()
</FluentMenuItem>
<FluentMenuItem OnClick="@(async () => await Grid.ShowColumnOptionsAsync(Column))">@Loc[nameof(ControlsStrings.FluentDataGridHeaderCellResizeButtonText)]</FluentMenuItem>
</FluentMenu>
</span>
</FluentKeyCode>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using Aspire.Dashboard.Resources;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Controls.Grid;

public partial class AspireFluentDataGridHeaderCell<T> : ComponentBase
{
[Parameter]
public required ColumnBase<T> Column { get; set; }

[Parameter]
public required FluentDataGrid<T> Grid { get; set; }

[Inject]
public required IStringLocalizer<ControlsStrings> Loc { get; init; }

private bool _isMenuOpen;
private readonly string _columnId = $"column-header{Guid.NewGuid():N}";

private string? Tooltip => Column.Tooltip ? Column.Title : null;

private void HandleKeyDown(FluentKeyCodeEventArgs e)
{
if (e.CtrlKey && e.Key == KeyCode.Enter)
{
Grid.RemoveSortByColumnAsync(Column);
}
}

public bool AnyColumnActionEnabled => Column.Sortable is true || Grid.ResizableColumns;

private async Task HandleColumnHeaderClickedAsync()
{
if (Column.Sortable is true && Grid.ResizableColumns)
{
_isMenuOpen = !_isMenuOpen;
}
else if (Column.Sortable is true && !Grid.ResizableColumns)
{
await Grid.SortByColumnAsync(Column);
}
else if (Column.Sortable is not true && Grid.ResizableColumns)
{
await Grid.ShowColumnOptionsAsync(Column);
}
}

private string GetSortOptionText()
{
if (Grid.SortByAscending.HasValue && Column.ShowSortIcon)
{
if (Grid.SortByAscending is true)
{
return Loc[nameof(ControlsStrings.FluentDataGridHeaderCellSortDescendingButtonText)];
}
else
{
return Loc[nameof(ControlsStrings.FluentDataGridHeaderCellSortAscendingButtonText)];
}
}

return Loc[nameof(ControlsStrings.FluentDataGridHeaderCellSortButtonText)];
}
}

internal static class AspireFluentDataGridHeaderCell
{
public static RenderFragment<ColumnBase<T>> RenderHeaderContent<T>(FluentDataGrid<T>? grid)
{
Debug.Assert(grid is not null);

return GetHeaderContent;

RenderFragment GetHeaderContent(ColumnBase<T> value) => builder =>
{
builder.OpenComponent<AspireFluentDataGridHeaderCell<T>>(0);
builder.AddAttribute(1, nameof(AspireFluentDataGridHeaderCell<T>.Column), value);
builder.AddAttribute(2, nameof(AspireFluentDataGridHeaderCell<T>.Grid), grid);
builder.CloseComponent();
};
}

public static string GetResizeLabel(IStringLocalizer<ControlsStrings> loc)
{
return loc[nameof(ControlsStrings.FluentDataGridHeaderCellResizeLabel)];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Controls.Grid;

public class AspirePropertyColumn<TGridItem, TProp> : PropertyColumn<TGridItem, TProp>
{
protected override void OnParametersSet()
{
HeaderCellItemTemplate = AspireFluentDataGridHeaderCell.RenderHeaderContent(Grid);
base.OnParametersSet();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Controls.Grid;

public class AspireTemplateColumn<TGridItem> : TemplateColumn<TGridItem>
{
protected override void OnParametersSet()
{
HeaderCellItemTemplate = AspireFluentDataGridHeaderCell.RenderHeaderContent(Grid);
base.OnParametersSet();
}
}
15 changes: 9 additions & 6 deletions src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Components.Controls.Grid
@using Aspire.Dashboard.Resources
@typeparam TItem
@inject IStringLocalizer<ControlsStrings> Loc

<FluentDataGrid Items="@Items"
<FluentDataGrid ResizeLabel="@AspireFluentDataGridHeaderCell.GetResizeLabel(Loc)"
ResizeType="DataGridResizeType.Discrete"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think there might be the opportunity to reduce the amount of change by having a custom FluentDataGrid type instead of custom columns. The custom grid type could loop through the columns and set HeaderCellItemTemplate = AspireFluentDataGridHeaderCell.RenderHeaderContent(Grid); on all the columns.

However, that might create problems of its own if there are situations where that is wrong behavior. Custom columns that set the header is probably ok for now. It's verbose, but it gives us control.

Items="@Items"
ResizableColumns="true"
Style="width:100%"
GenerateHeader="@GenerateHeader"
GridTemplateColumns="@GridTemplateColumns"
ShowHover="true">
<TemplateColumn Title="@(NameColumnTitle ?? Loc[nameof(ControlsStrings.NameColumnHeader)])" Class="nameColumn" SortBy="@NameSort" Sortable="@IsNameSortable">
<AspireTemplateColumn Title="@(NameColumnTitle ?? Loc[nameof(ControlsStrings.NameColumnHeader)])" Class="nameColumn" SortBy="@NameSort" Sortable="@IsNameSortable">
<GridValue
ValueDescription="@(NameColumnTitle ?? Loc[nameof(ControlsStrings.NameColumnHeader)])"
Value="@NameColumnValue(context)"
HighlightText="@HighlightText" />
</TemplateColumn>
<TemplateColumn Title="@(ValueColumnTitle ?? Loc[nameof(ControlsStrings.PropertyGridValueColumnHeader)])" Class="valueColumn" SortBy="@ValueSort" Sortable="@IsValueSortable">
</AspireTemplateColumn>
<AspireTemplateColumn Title="@(ValueColumnTitle ?? Loc[nameof(ControlsStrings.PropertyGridValueColumnHeader)])" Class="valueColumn" SortBy="@ValueSort" Sortable="@IsValueSortable">
<GridValue
ValueDescription="@(ValueColumnTitle ?? Loc[nameof(ControlsStrings.PropertyGridValueColumnHeader)])"
Value="@ValueColumnValue(context)" HighlightText="@HighlightText"
EnableMasking="@EnableValueMasking" IsMasked="@GetIsItemMasked(context)"
IsMaskedChanged="(newValue) => OnIsMaskedChanged(context, newValue)"
TextVisualizerTitle="@NameColumnValue(context)"/>
@ExtraValueContent(context)
</TemplateColumn>
</AspireTemplateColumn>
</FluentDataGrid>

27 changes: 16 additions & 11 deletions src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Components.Controls.Grid
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@inject IStringLocalizer<ControlsStrings> ControlStringsLoc
Expand Down Expand Up @@ -43,24 +44,26 @@
@FilteredResourceValues.Count()
</FluentBadge>
</div>
<FluentDataGrid Items="@FilteredResourceValues"
<FluentDataGrid ResizeLabel="@AspireFluentDataGridHeaderCell.GetResizeLabel(ControlStringsLoc)"
ResizeType="DataGridResizeType.Discrete"
Items="@FilteredResourceValues"
ResizableColumns="true"
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="1fr 1.5fr"
ShowHover="true">
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
<AspireTemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
<GridValue
Value="@(context.KnownProperty?.DisplayName ?? context.Key)"
ValueDescription="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]"
ToolTip="@context.Key" />
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]" Class="valueColumn">
</AspireTemplateColumn>
<AspireTemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]" Class="valueColumn">
<GridValue
Value="@GetDisplayedValue(TimeProvider, context)"
ValueDescription="@(context.KnownProperty?.DisplayName ?? context.Key)"
ToolTip="@context.Tooltip" />
</TemplateColumn>
</AspireTemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
<FluentAccordionItem Heading="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsEndpointsHeader)]" Expanded="true">
Expand All @@ -69,16 +72,18 @@
@FilteredEndpoints.Count()
</FluentBadge>
</div>
<FluentDataGrid Items="@FilteredEndpoints"
<FluentDataGrid ResizeLabel="@AspireFluentDataGridHeaderCell.GetResizeLabel(ControlStringsLoc)"
ResizeType="DataGridResizeType.Discrete"
Items="@FilteredEndpoints"
ResizableColumns="true"
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="1fr 1.5fr"
ShowHover="true">
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
<AspireTemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
<GridValue Value="@context.Name" ValueDescription="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" />
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]" Class="valueColumn">
</AspireTemplateColumn>
<AspireTemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]" Class="valueColumn">
<GridValue
Value="@context.Text"
ValueDescription="@context.Name"
Expand All @@ -94,7 +99,7 @@
}
</ContentAfterValue>
</GridValue>
</TemplateColumn>
</AspireTemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
<FluentAccordionItem Heading="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsEnvironmentVariablesHeader)]" Expanded="true">
Expand Down
Loading
Loading