Skip to content

Commit

Permalink
Re-focus view button when closing details panel (#3368)
Browse files Browse the repository at this point in the history
* Re-focus button when closing details panel

* Add internal id for log entries, make sure to null element id on detail view closed

* Make sure view button is selected only after a user action

---------

Co-authored-by: Adam Ratzman <adamratzman@microsoft.com>
  • Loading branch information
adamint and Adam Ratzman authored Apr 5, 2024
1 parent 5a6856d commit e784c91
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 35 deletions.
12 changes: 8 additions & 4 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<FluentCheckbox
Label="@resourceType"
@bind-Value:get="isChecked"
@bind-Value:set="c => OnResourceTypeVisibilityChanged(resourceType, c)"
@bind-Value:set="c => OnResourceTypeVisibilityChangedAsync(resourceType, c)"
/>
}
</FluentStack>
Expand All @@ -44,11 +44,11 @@
Immediate="true"
@bind-Value="_filter"
slot="end"
@bind-Value:after="HandleSearchFilterChanged" />
@bind-Value:after="HandleSearchFilterChangedAsync" />
</FluentToolbar>
<SummaryDetailsView DetailsTitle="@(SelectedResource != null ? $"{SelectedResource.ResourceType}: {GetResourceName(SelectedResource)}" : null)"
ShowDetails="@(SelectedResource is not null)"
OnDismiss="() => ClearSelectedResource()"
OnDismiss="@(() => ClearSelectedResourceAsync(causedByUserAction: true))"
SelectedValue="@SelectedResource"
ViewKey="ResourcesList">
<Summary>
Expand Down Expand Up @@ -77,9 +77,13 @@
<FluentAnchor Appearance="Appearance.Lightweight" Href="@DashboardUrls.ConsoleLogsUrl(resource: context.Name)">@ControlsStringsLoc[ControlsStrings.ViewAction]</FluentAnchor>
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesDetailsColumnHeader)]" Sortable="false" Class="no-ellipsis">
@{
var id = $"details-button-{context.Uid}";
}
<FluentButton Appearance="Appearance.Lightweight"
Id="@id"
Title="@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]"
OnClick="() => ShowResourceDetails(context)">@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
OnClick="@(() => ShowResourceDetailsAsync(context, id))">@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
@if (HasResourcesWithCommands)
{
Expand Down
27 changes: 20 additions & 7 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Components.Pages;

Expand All @@ -30,6 +31,8 @@ public partial class Resources : ComponentBase, IAsyncDisposable
public required IToastService ToastService { get; init; }
[Inject]
public required BrowserTimeProvider TimeProvider { get; init; }
[Inject]
public required IJSRuntime JS { get; init; }

private ResourceViewModel? SelectedResource { get; set; }

Expand All @@ -41,6 +44,7 @@ public partial class Resources : ComponentBase, IAsyncDisposable
private bool _isTypeFilterVisible;
private Task? _resourceSubscriptionTask;
private bool _isLoading = true;
private string? _elementIdBeforeDetailsViewOpened;

public Resources()
{
Expand All @@ -49,7 +53,7 @@ public Resources()

private bool Filter(ResourceViewModel resource) => _visibleResourceTypes.ContainsKey(resource.ResourceType) && (_filter.Length == 0 || resource.MatchesFilter(_filter)) && resource.State != ResourceStates.HiddenState;

protected void OnResourceTypeVisibilityChanged(string resourceType, bool isVisible)
protected Task OnResourceTypeVisibilityChangedAsync(string resourceType, bool isVisible)
{
if (isVisible)
{
Expand All @@ -60,12 +64,12 @@ protected void OnResourceTypeVisibilityChanged(string resourceType, bool isVisib
_visibleResourceTypes.TryRemove(resourceType, out _);
}

ClearSelectedResource();
return ClearSelectedResourceAsync();
}

private void HandleSearchFilterChanged()
private Task HandleSearchFilterChangedAsync()
{
ClearSelectedResource();
return ClearSelectedResourceAsync();
}

private bool? AreAllTypesVisible
Expand Down Expand Up @@ -202,21 +206,30 @@ private bool ApplicationErrorCountsChanged(Dictionary<OtlpApplication, int> newA
return false;
}

private void ShowResourceDetails(ResourceViewModel resource)
private async Task ShowResourceDetailsAsync(ResourceViewModel resource, string buttonId)
{
_elementIdBeforeDetailsViewOpened = buttonId;

if (SelectedResource == resource)
{
ClearSelectedResource();
await ClearSelectedResourceAsync();
}
else
{
SelectedResource = resource;
}
}

private void ClearSelectedResource()
private async Task ClearSelectedResourceAsync(bool causedByUserAction = false)
{
SelectedResource = null;

if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
{
await JS.InvokeVoidAsync("focusElement", _elementIdBeforeDetailsViewOpened);
}

_elementIdBeforeDetailsViewOpened = null;
}

private string GetResourceName(ResourceViewModel resource) => ResourceViewModel.GetResourceName(resource, _resourceByName);
Expand Down
9 changes: 6 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
@bind-SelectedResource:after="HandleSelectedApplicationChangedAsync" />
<FluentSearch @bind-Value="PageViewModel.Filter"
@oninput="HandleFilter"
@bind-Value:after="HandleClear"
@bind-Value:after="HandleClearAsync"
Placeholder="@ControlsStringsLoc[nameof(ControlsStrings.FilterPlaceholder)]"
title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsMessageFilter)]"
slot="end" />
Expand Down Expand Up @@ -60,7 +60,7 @@
</FluentToolbar>
<SummaryDetailsView
ShowDetails="SelectedLogEntry is not null"
OnDismiss="() => ClearSelectedLogEntry()"
OnDismiss="@(() => ClearSelectedLogEntryAsync(causedByUserAction: true))"
ViewKey="StructuredLogsList"
SelectedValue="@SelectedLogEntry">
<DetailsTitleTemplate>
Expand Down Expand Up @@ -103,7 +103,10 @@
}
</TemplateColumn>
<TemplateColumn Title="@ControlsStringsLoc[nameof(ControlsStrings.DetailsColumnHeader)]" Class="no-ellipsis">
<FluentButton Appearance="Appearance.Lightweight" OnClick="() => OnShowProperties(context)">@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
@{
var id = $"details-button-{context.InternalId}";
}
<FluentButton Id="@id" Appearance="Appearance.Lightweight" OnClick="@(() => OnShowPropertiesAsync(context, id))">@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
</ChildContent>
<EmptyContent>
Expand Down
42 changes: 28 additions & 14 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public partial class StructuredLogs : IPageWithSessionAndUrlState<StructuredLogs
private Subscription? _logsSubscription;
private bool _applicationChanged;
private CancellationTokenSource? _filterCts;
private string? _elementIdBeforeDetailsViewOpened;

public string BasePath => DashboardUrls.StructuredLogsBasePath;
public string SessionStorageKey => "StructuredLogs_PageState";
Expand Down Expand Up @@ -147,13 +148,12 @@ private Task HandleSelectedApplicationChangedAsync()
return this.AfterViewModelChangedAsync();
}

private Task HandleSelectedLogLevelChangedAsync()
private async Task HandleSelectedLogLevelChangedAsync()
{
_applicationChanged = true;

ClearSelectedLogEntry();

return this.AfterViewModelChangedAsync();
await ClearSelectedLogEntryAsync();
await this.AfterViewModelChangedAsync();
}

private void UpdateSubscription()
Expand All @@ -170,11 +170,13 @@ private void UpdateSubscription()
}
}

private void OnShowProperties(OtlpLogEntry entry)
private async Task OnShowPropertiesAsync(OtlpLogEntry entry, string buttonId)
{
_elementIdBeforeDetailsViewOpened = buttonId;

if (SelectedLogEntry?.LogEntry == entry)
{
ClearSelectedLogEntry();
await ClearSelectedLogEntryAsync();
}
else
{
Expand All @@ -187,9 +189,16 @@ private void OnShowProperties(OtlpLogEntry entry)
}
}

private void ClearSelectedLogEntry()
private async Task ClearSelectedLogEntryAsync(bool causedByUserAction = false)
{
SelectedLogEntry = null;

if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
{
await JS.InvokeVoidAsync("focusElement", _elementIdBeforeDetailsViewOpened);
}

_elementIdBeforeDetailsViewOpened = null;
}

private async Task OpenFilterAsync(LogFilter? entry)
Expand All @@ -213,7 +222,7 @@ private async Task OpenFilterAsync(LogFilter? entry)
await DialogService.ShowPanelAsync<FilterDialog>(data, parameters);
}

private Task HandleFilterDialog(DialogResult result)
private async Task HandleFilterDialog(DialogResult result)
{
if (result.Data is FilterDialogResult filterResult && filterResult.Filter is LogFilter filter)
{
Expand All @@ -226,36 +235,41 @@ private Task HandleFilterDialog(DialogResult result)
ViewModel.AddFilter(filter);
}

ClearSelectedLogEntry();
await ClearSelectedLogEntryAsync();
}

return this.AfterViewModelChangedAsync();
await this.AfterViewModelChangedAsync();
}

private void HandleFilter(ChangeEventArgs args)
{
if (args.Value is string newFilter)
{
PageViewModel.Filter = newFilter;
ClearSelectedLogEntry();
_filterCts?.Cancel();

// Debouncing logic. Apply the filter after a delay.
var cts = _filterCts = new CancellationTokenSource();
_ = Task.Run(async () =>
{
await ClearSelectedLogEntryAsync();

await Task.Delay(400, cts.Token);
ViewModel.FilterText = newFilter;
await InvokeAsync(StateHasChanged);
});
}
}

private void HandleClear()
private async Task HandleClearAsync()
{
_filterCts?.Cancel();
if (_filterCts is not null)
{
await _filterCts.CancelAsync();
}

ViewModel.FilterText = string.Empty;
ClearSelectedLogEntry();
await ClearSelectedLogEntryAsync();
StateHasChanged();
}

Expand Down
13 changes: 9 additions & 4 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

<SummaryDetailsView
ShowDetails="SelectedSpan is not null"
OnDismiss="() => ClearSelectedSpan()"
OnDismiss="@(() => ClearSelectedSpanAsync(causedByUserAction: true))"
ViewKey="TraceDetail"
SelectedValue="@SelectedSpan">
<DetailsTitleTemplate>
Expand All @@ -55,7 +55,7 @@
<Summary>
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" Class="trace-view-grid" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="SpanWaterfallViewModel" RowClass="@GetRowClass" GridTemplateColumns="4fr 12fr 85px">
<TemplateColumn Title="Name">
<div class="col-long-content" title="@context.GetTooltip()" @onclick="() => OnShowProperties(context)">
<div class="col-long-content" title="@context.GetTooltip()" @onclick="() => OnShowPropertiesAsync(context, null)">
@{
var isServerOrConsumer = context.Span.Kind == OtlpSpanKind.Server || context.Span.Kind == OtlpSpanKind.Consumer;
// Indent the span name based on the depth of the span.
Expand Down Expand Up @@ -152,7 +152,7 @@
</div>
</HeaderCellItemTemplate>
<ChildContent>
<div class="ticks" @onclick="() => OnShowProperties(context)">
<div class="ticks" @onclick="() => OnShowPropertiesAsync(context, null)">
<div class="span-container" style="grid-template-columns: @context.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)% @context.Width.ToString("F2", CultureInfo.InvariantCulture)% min-content;">
<div class="span-bar" style="grid-column: 2; background: @ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Span.Source));"></div>
<div class="span-bar-label @(context.LabelIsRight ? "span-bar-label-right" : "span-bar-label-left")">
Expand All @@ -169,10 +169,15 @@
</ChildContent>
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.DetailsColumnHeader)]" Class="no-ellipsis">
@{
var id = context.Span.SpanId;
}

<FluentButton
Id="@id"
Style="margin-left: 7px;"
Title="@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]"
Appearance="Appearance.Lightweight" OnClick="() => OnShowProperties(context)">@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
Appearance="Appearance.Lightweight" OnClick="@(() => OnShowPropertiesAsync(context, id))">@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
</FluentDataGrid>
</Summary>
Expand Down
20 changes: 17 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Aspire.Dashboard.Otlp.Storage;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Components.Pages;

Expand All @@ -20,6 +21,7 @@ public partial class TraceDetail : ComponentBase
private int _maxDepth;
private List<OtlpApplication> _applications = default!;
private readonly List<string> _collapsedSpanIds = [];
private string? _elementIdBeforeDetailsViewOpened;

[Parameter]
public required string TraceId { get; set; }
Expand All @@ -33,6 +35,9 @@ public partial class TraceDetail : ComponentBase
[Inject]
public required BrowserTimeProvider TimeProvider { get; set; }

[Inject]
public required IJSRuntime JS { get; set; }

protected override void OnInitialized()
{
foreach (var resolver in OutgoingPeerResolvers)
Expand Down Expand Up @@ -239,11 +244,13 @@ private void OnToggleCollapse(SpanWaterfallViewModel viewModel)
}
}

private void OnShowProperties(SpanWaterfallViewModel viewModel)
private async Task OnShowPropertiesAsync(SpanWaterfallViewModel viewModel, string? buttonId)
{
_elementIdBeforeDetailsViewOpened = buttonId;

if (SelectedSpan?.Span == viewModel.Span)
{
ClearSelectedSpan();
await ClearSelectedSpanAsync();
}
else
{
Expand All @@ -262,9 +269,16 @@ private void OnShowProperties(SpanWaterfallViewModel viewModel)
}
}

private void ClearSelectedSpan()
private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)
{
SelectedSpan = null;

if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
{
await JS.InvokeVoidAsync("focusElement", _elementIdBeforeDetailsViewOpened);
}

_elementIdBeforeDetailsViewOpened = null;
}

private string GetResourceName(OtlpApplication app) => OtlpApplication.GetResourceName(app, _applications);
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class OtlpLogEntry
public string? OriginalFormat { get; }
public OtlpApplication Application { get; }
public OtlpScope Scope { get; }
public Guid InternalId { get; }

public OtlpLogEntry(LogRecord record, OtlpApplication logApp, OtlpScope scope, TelemetryLimitOptions options)
{
Expand Down Expand Up @@ -56,6 +57,7 @@ public OtlpLogEntry(LogRecord record, OtlpApplication logApp, OtlpScope scope, T
ParentId = parentId ?? string.Empty;
Application = logApp;
Scope = scope;
InternalId = Guid.NewGuid();
}

private static LogLevel MapSeverity(SeverityNumber severityNumber) => severityNumber switch
Expand Down
7 changes: 7 additions & 0 deletions src/Aspire.Dashboard/wwwroot/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,10 @@ window.getBrowserTimeZone = function () {

return options.timeZone;
}

window.focusElement = function(selector) {
const element = document.getElementById(selector);
if (element) {
element.focus();
}
}

0 comments on commit e784c91

Please sign in to comment.