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
2 changes: 2 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
<MicrosoftAspNetCoreTestHostNet11Version>11.0.0-rc.1.26425.128</MicrosoftAspNetCoreTestHostNet11Version>
<MicrosoftExtensionsCachingMemoryNet11Version>11.0.0-rc.1.26425.128</MicrosoftExtensionsCachingMemoryNet11Version>
<MicrosoftExtensionsConfigurationBinderNet11Version>11.0.0-rc.1.26425.128</MicrosoftExtensionsConfigurationBinderNet11Version>
<MicrosoftExtensionsConfigurationEnvironmentVariablesNet11Version>11.0.0-rc.1.26425.128</MicrosoftExtensionsConfigurationEnvironmentVariablesNet11Version>
<MicrosoftExtensionsFileSystemGlobbingNet11Version>11.0.0-rc.1.26425.128</MicrosoftExtensionsFileSystemGlobbingNet11Version>
<MicrosoftExtensionsHttpNet11Version>11.0.0-rc.1.26425.128</MicrosoftExtensionsHttpNet11Version>
<MicrosoftExtensionsLoggingNet11Version>11.0.0-rc.1.26425.128</MicrosoftExtensionsLoggingNet11Version>
</PropertyGroup>
Expand Down
24 changes: 22 additions & 2 deletions src/Aspire.Dashboard/Components/Controls/AspireMenu.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public AspireMenu(LibraryConfiguration configuration)
private FluentMenu? _menu;
private IReadOnlyList<MenuButtonItem>? _renderedItems;
private bool _refreshMenuAfterRender;
private bool _reopenInProgress;
private bool? _appliedOpen;
private int _cursorLeft;
private int _cursorTop;
Expand Down Expand Up @@ -98,10 +99,15 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Trigger identifies either the button anchor or the cursor anchor. The parameterless
// path leaves placement to Fluent's CSS anchor positioning and viewport fallbacks.
if (_reopenInProgress)
{
await _menu.CloseMenuAsync();
}
await _menu.OpenMenuAsync();
}
else
{
_reopenInProgress = false;
await _menu.CloseMenuAsync();
}

Expand All @@ -127,8 +133,10 @@ public async Task OpenAsync(int clientX, int clientY)
.AddStyle("min-width", "64px")
.Build();

// Escape and light-dismiss can close the browser popover without raising OpenedChanged.
// Treat every cursor request as a new open/position request even when Open is still true.
// Escape and light-dismiss update the browser popover before their asynchronous
// OpenedChanged notification reaches this component. Treat every cursor request as a
// new open/position request even when Open is still true.
_reopenInProgress = Open;
_refreshMenuAfterRender = true;
await SetOpenAsync(true);

Expand Down Expand Up @@ -186,6 +194,18 @@ private async Task HandleSecondaryActionClicked(MenuButtonItem item)

private async Task OnOpenChanged(bool open)
{
// Fluent reports native popover toggles through asynchronous JS callbacks. During a deliberate
// close-and-reopen, ignore the reset's close notification until the following open is confirmed.
if (_reopenInProgress)
{
if (!open)
{
return;
}

_reopenInProgress = false;
}

_appliedOpen = open;
await SetOpenAsync(open);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
};
}

<div>
<div class="aspire-menu-button">
<FluentButton Id="@MenuButtonId" Title="@Title" Appearance="@ButtonAppearance" IconOnly="@iconOnly" @onclick="ToggleMenu" Disabled="@_disabled" AdditionalAttributes="@additionalButtonAttributes" Class="@ButtonClass">
@if (IconStart is not null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ else
IconStart="@(new Icons.Regular.Size24.DataArea())">
<div class="metrics-chart-container metric-tab">
<PlotlyChart InstrumentViewModel="_instrumentViewModel" Duration="Duration" Resources="Resources" DataEndTime="_dataEndTime" />
<ChartFilters InstrumentType="_instrument.Summary.Type" ShowCount="_instrumentViewModel.ShowCount" ShowCountChanged="ShowCountChangedAsync" DimensionFilters="@DimensionFilters" OnDimensionValuesChanged="DimensionValuesChangedAsync" />
<ChartFilters @key="(MeterName, InstrumentName)" InstrumentType="_instrument.Summary.Type" ShowCount="_instrumentViewModel.ShowCount" ShowCountChanged="ShowCountChangedAsync" DimensionFilters="@DimensionFilters" OnDimensionValuesChanged="DimensionValuesChangedAsync" />
</div>
</FluentTab>
<FluentTab HeaderClass="tab-label"
Expand All @@ -49,7 +49,7 @@ else
IconStart="@(new Icons.Regular.Size24.Table())">
<div class="metrics-chart-container metric-tab">
<MetricTable InstrumentViewModel="_instrumentViewModel" Duration="Duration" Resources="Resources" DataEndTime="_dataEndTime" />
<ChartFilters InstrumentType="_instrument.Summary.Type" ShowCount="_instrumentViewModel.ShowCount" ShowCountChanged="ShowCountChangedAsync" DimensionFilters="@DimensionFilters" OnDimensionValuesChanged="DimensionValuesChangedAsync" />
<ChartFilters @key="(MeterName, InstrumentName)" InstrumentType="_instrument.Summary.Type" ShowCount="_instrumentViewModel.ShowCount" ShowCountChanged="ShowCountChangedAsync" DimensionFilters="@DimensionFilters" OnDimensionValuesChanged="DimensionValuesChangedAsync" />
</div>
</FluentTab>
</FluentTabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,35 @@
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<ControlsStrings> Loc

@{
var id = $"typeFilterButton-{Filter.SanitizedHtmlId}-{_idSuffix}";
}
<span class="chart-filter-button-container">
<FluentButton id="@id"
Class="chart-filter-button"
IconOnly="true"
Appearance="@(Filter.AreAllValuesSelected is true ? ButtonAppearance.Transparent : ButtonAppearance.Primary)"
@onclick="() => Filter.PopupVisible = !Filter.PopupVisible"
aria-label="@(Filter.AreAllValuesSelected is true ? Loc[nameof(ControlsStrings.ChartContainerAllTags)] : Loc[nameof(ControlsStrings.ChartContainerFilteredTags)])">
<FluentIcon Value="@(new Icons.Regular.Size20.Filter())" Color="Color.Custom" CustomColor="@(Filter.AreAllValuesSelected is true ? "var(--colorBrandForeground1)" : "currentColor")" />
</FluentButton>
</span>
@* Each item is approximately 36px tall, plus 48px for the header and container padding. Include the always-rendered "All" checkbox row. The popover body has a CSS max-height of 300px. *@
<FluentPopover AnchorId="@id" @bind-Opened="Filter.PopupVisible" Class="content-list-popover chart-filter-popover">
<strong>@Filter.Name</strong>
<div class="dimension-popup-container">
<FluentStack Orientation="Orientation.Vertical" Class="content-list-popover-items dimension-popup">
<FluentCheckbox Label="@Loc[nameof(ControlsStrings.LabelAll)]"
Class="aspire-checkbox"
ThreeState="true"
ShowIndeterminate="false"
ThreeStateOrderUncheckToIntermediate="true"
Value="@(Filter.AreAllValuesSelected is true)"
@bind-CheckState:get="Filter.AreAllValuesSelected"
@bind-CheckState:set="@(v => OnAllValuesSelectionChangedAsync(v))"/>
@foreach (var tag in ChartFilterTags.GetOrderedValues(Filter.Values))
{
var isChecked = Filter.SelectedValues.Contains(tag);
<FluentCheckbox Label="@tag.Text"
<span class="chart-filter-popover-container">
<FluentPopover AnchorId="@AnchorId"
Opened="@Opened"
OpenedChanged="@OpenedChanged"
Class="content-list-popover chart-filter-popover"
Style="@($"--chart-filter-anchor: --{AnchorId};")">
<strong>@Filter.Name</strong>
<div class="dimension-popup-container">
<FluentStack Orientation="Orientation.Vertical" Class="content-list-popover-items dimension-popup">
<FluentCheckbox Label="@Loc[nameof(ControlsStrings.LabelAll)]"
Class="aspire-checkbox"
title="@tag.Text"
@key=tag
@bind-Value:get="isChecked"
@bind-Value:set="c => OnTagSelectionChangedAsync(tag, c)"/>
}
</FluentStack>
</div>
</FluentPopover>
ThreeState="true"
ShowIndeterminate="false"
ThreeStateOrderUncheckToIntermediate="true"
Value="@(Filter.AreAllValuesSelected is true)"
@bind-CheckState:get="Filter.AreAllValuesSelected"
@bind-CheckState:set="@(v => OnAllValuesSelectionChangedAsync(v))"/>
@foreach (var tag in ChartFilterTags.GetOrderedValues(Filter.Values))
{
var isChecked = Filter.SelectedValues.Contains(tag);
<FluentCheckbox Label="@tag.Text"
Class="aspire-checkbox"
title="@tag.Text"
@key=tag
@bind-Value:get="isChecked"
@bind-Value:set="c => OnTagSelectionChangedAsync(tag, c)"/>
}
</FluentStack>
</div>
</FluentPopover>
</span>
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,33 @@ namespace Aspire.Dashboard.Components;

public partial class ChartFilterPopover : IDisposable
{
private readonly Guid _idSuffix = Guid.NewGuid();
private DimensionFilterViewModel? _subscribedFilter;

[Parameter, EditorRequired]
public required string AnchorId { get; set; }

[Parameter, EditorRequired]
public required DimensionFilterViewModel Filter { get; set; }

[Parameter, EditorRequired]
public required bool Opened { get; set; }

[Parameter, EditorRequired]
public required EventCallback<bool> OpenedChanged { get; set; }

[Parameter, EditorRequired]
public required EventCallback<DimensionFilterViewModel> OnSelectionChanged { get; set; }

protected override void OnInitialized()
protected override void OnParametersSet()
{
if (ReferenceEquals(_subscribedFilter, Filter))
{
return;
}

_subscribedFilter?.NotifyStateChanged -= OnFilterStateChanged;
Filter.NotifyStateChanged += OnFilterStateChanged;
_subscribedFilter = Filter;
}

private void OnFilterStateChanged()
Expand All @@ -42,6 +58,6 @@ private async Task OnAllValuesSelectionChangedAsync(bool? isChecked)

public void Dispose()
{
Filter.NotifyStateChanged -= OnFilterStateChanged;
_subscribedFilter?.NotifyStateChanged -= OnFilterStateChanged;
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
.chart-filter-button-container {
display: inline-flex;
width: 32px;
height: 32px;
}

.chart-filter-button-container ::deep .chart-filter-button {
width: 32px;
min-width: 32px;
max-width: 32px;
height: 32px;
flex: 0 0 32px;
font-weight: var(--fontWeightRegular);
}

.chart-filter-button-container ::deep .chart-filter-button::part(control) {
width: 32px;
min-width: 32px;
height: 32px;
padding: 0;
font-weight: var(--fontWeightRegular);
}

.chart-filter-button-container + ::deep .chart-filter-popover::part(dialog) {
transform: translateX(calc(-100% + 32px));
.chart-filter-popover-container ::deep .chart-filter-popover::part(dialog) {
position-anchor: var(--chart-filter-anchor);
/* Fluent sets the dialog insets inline; the anchor position must override them. */
top: anchor(bottom) !important;
right: anchor(right) !important;
bottom: auto !important;
left: auto !important;
position-try-fallbacks: flip-block;
box-sizing: border-box;
max-width: 400px;
width: max-content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<button type="button"
class="dimension-tag filter-value-tag @isIncludedInFiltersClass"
style="display:inline-block;"
@onclick="@ShowPopover">
@onclick="@ShowPopoverAsync">
@($"+{overflow.OverflowCount}")
</button>
</MoreTemplate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public partial class ChartFilterTags : IDisposable
[Parameter, EditorRequired]
public required EventCallback<DimensionFilterViewModel> OnSelectionChanged { get; set; }

[Parameter, EditorRequired]
public required EventCallback<DimensionFilterViewModel> OnShowPopover { get; set; }

private const int MaxRenderedOverflowItems = 20;

protected override void OnInitialized()
Expand Down Expand Up @@ -54,11 +57,7 @@ private async Task OnTagActivatedAsync(DimensionValueViewModel tag, bool toggleS
await OnSelectionChanged.InvokeAsync(Filter);
}

private void ShowPopover()
{
Filter.PopupVisible = true;
Filter.NotifyStateChanged?.Invoke();
}
private Task ShowPopoverAsync() => OnShowPopover.InvokeAsync(Filter);

/// <summary>
/// Orders dimension values numerically if all values are parsable as doubles;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,42 @@
<div class="metrics-filters-section">
<h5>@Loc[nameof(ControlsStrings.ChartContainerFiltersHeader)]</h5>
<FluentDataGrid Class="aspire-spaced-grid" ItemsProvider="@EnumerableGridItemsProvider.Create(() => DimensionFilters)"
ItemKey="@(filter => filter.Name)"
GridTemplateColumns="200px 1fr auto"
GenerateHeader="DataGridGeneratedHeaderType.None"
RowSize="DataGridRowSize.Medium">
<ChildContent>
<AspirePropertyColumn Tooltip="true" TooltipText="@(c => c.Name)" Property="@(c => c.Name)"/>
<AspireTemplateColumn>
<ChartFilterTags @key="context" Filter="context" OnSelectionChanged="OnDimensionValuesChanged" />
<ChartFilterTags @key="context"
Filter="context"
OnSelectionChanged="OnDimensionValuesChanged"
OnShowPopover="ShowPopup" />
</AspireTemplateColumn>
<AspireTemplateColumn>
<ChartFilterPopover @key="context" Filter="context" OnSelectionChanged="OnDimensionValuesChanged" />
<span class="chart-filter-button-container">
<FluentButton id="@GetFilterButtonId(context)"
Class="chart-filter-button"
Style="@($"anchor-name: --{GetFilterButtonId(context)}")"
IconOnly="true"
Appearance="@(context.AreAllValuesSelected is true ? ButtonAppearance.Transparent : ButtonAppearance.Primary)"
@onclick="() => TogglePopup(context)"
aria-label="@(context.AreAllValuesSelected is true ? Loc[nameof(ControlsStrings.ChartContainerAllTags)] : Loc[nameof(ControlsStrings.ChartContainerFilteredTags)])">
<FluentIcon Value="@(new Icons.Regular.Size20.Filter())" Color="Color.Custom" CustomColor="@(context.AreAllValuesSelected is true ? "var(--colorBrandForeground1)" : "currentColor")" />
</FluentButton>
</span>
</AspireTemplateColumn>
</ChildContent>
</FluentDataGrid>
@foreach (var filter in DimensionFilters)
{
<ChartFilterPopover @key="filter.Name"
AnchorId="@GetFilterButtonId(filter)"
Filter="filter"
Opened="@IsPopupOpen(filter)"
OpenedChanged="@(opened => SetPopupOpen(filter, opened))"
OnSelectionChanged="OnFilterSelectionChangedAsync" />
}
</div>
}
@if (InstrumentType == OtlpInstrumentType.Histogram)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace Aspire.Dashboard.Components;

public partial class ChartFilters
{
private readonly Guid _idSuffix = Guid.NewGuid();
private readonly HashSet<string> _openFilterNames = [];

[Parameter, EditorRequired]
public required OtlpInstrumentType InstrumentType { get; set; }

Expand All @@ -25,5 +28,27 @@ public partial class ChartFilters
[Parameter]
public EventCallback<DimensionFilterViewModel> OnDimensionValuesChanged { get; set; }

private string GetFilterButtonId(DimensionFilterViewModel filter) => $"typeFilterButton-{filter.SanitizedHtmlId}-{filter.NameHash}-{_idSuffix}";

private bool IsPopupOpen(DimensionFilterViewModel filter) => _openFilterNames.Contains(filter.Name);

private void TogglePopup(DimensionFilterViewModel filter) => SetPopupOpen(filter, !IsPopupOpen(filter));

private void ShowPopup(DimensionFilterViewModel filter) => SetPopupOpen(filter, opened: true);

private void SetPopupOpen(DimensionFilterViewModel filter, bool opened)
{
if (opened)
{
_openFilterNames.Add(filter.Name);
}
else
{
_openFilterNames.Remove(filter.Name);
}
}

private Task OnFilterSelectionChangedAsync(DimensionFilterViewModel filter) => OnDimensionValuesChanged.InvokeAsync(filter);

private Task OnShowCountChangedAsync(bool value) => ShowCountChanged.InvokeAsync(value);
}
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
/* Tag-related styles are in ChartFilterTags.razor.css */

.chart-filter-button-container {
display: inline-flex;
width: 32px;
height: 32px;
}

.chart-filter-button-container ::deep .chart-filter-button {
width: 32px;
min-width: 32px;
max-width: 32px;
height: 32px;
flex: 0 0 32px;
font-weight: var(--fontWeightRegular);
}

.chart-filter-button-container ::deep .chart-filter-button::part(control) {
width: 32px;
min-width: 32px;
height: 32px;
padding: 0;
font-weight: var(--fontWeightRegular);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</FluentButton>
</div>

<FluentDivider Class="action-divider" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<FluentDivider Class="action-divider" Orientation="Orientation.Vertical" />
}

<div class="resource-action-slot">
Expand Down
Loading
Loading