Skip to content

Commit

Permalink
Improve summary details view / toolbars / navbar experience on deskto…
Browse files Browse the repository at this point in the history
…p for low-vision users, as well as mobile users (dotnet#4245)

* Temporarily add nuget.org feed

* Refactor current layout into DesktopLayout component

* wip prototype

* add mobile nav menu

* add rest of nav menu to mobile, increase reconnection modal size on mobile

* Add common page layout, use it for Resources

* Convert Console Logs toolbar

* Use new layout for structured logs

* fix scroll bars, place toolbar on mobile within footer

* Rename page sections, make title section more generic, do trace detail page

* use new layout for metrics page

* Add context parameter to details views, make them full page height on mobile

* wip custom browser resize logic

* Get initial viewport parameters for render (do not use scoped js because it is ~20ms slower)

* fix nested h1, increase mobile toolbar button size, add resize listener

* clean up views

* remove extra class

* remove redundant code, clean up

* remove redundant code, add a few comments

* fix accidental logic bug

* Increase mobile toolbar height, make entire page scrollable at very low resolution

* Add translation, comment

* move comment

* remove redundant if

* Move toolbar mobile button to top

* remove padding bottom for page header on mobile

* some requested changes

* try to avoid closing dialog on mobile if a toolbar setting has been changed, remove extraneous css, add Filter to url

* Persist filter into URL, as well as visible types for resources.

* Remove unnecessary button on detail view, invoke listeners after filters opened

* Move MobileLayout and DesktopLayout logic into MainLayout so that page is not re-initialized when layout is changed. Remove filter state added to individual pages. Add a view model to the log viewer so that logs appear quickly after switching layouts

* Open aspire repo link in new tab on mobile navigation

* Add divs around page content layout to fix scoped css not being applied

* fix extra div not taking up total height

* fix metric scrollbar, change debounced resize event to throttled

* Fix scrollbar erroneously appearing on console logs

* Consolidate two selects into one

* Refactor mobile nav menu to its own component, rename desktop nav menu component to DesktopNavMenu, and show page as active in mobile nav menu if its url matches current uri

* Show icon as active in mobile navigation menu for the current page

* Close mobile filter/nav menus when changing to desktop layout

* Use --accent-foreground-active for active page in mobile nav

* Make detail view close button background transparent in mobile

* hide duration progress circle on traces mobile

* fix continuous scroll on traces/console logs/structured logs

* add console log application to mobile toolbar

* re-add mobile filter footer button

* fix incorrect merge conflicts

* Avoid invoking redundant SetStateAndNavigateAsync calls, cleanup

* fix chart/table state being lost on layout change

* fix toolbar with newline appearance on mobile

* add file headers, fix merge

* fix console logs not updating after layout change

---------

Co-authored-by: Adam Ratzman <[email protected]>
  • Loading branch information
adamint and Adam Ratzman authored Jul 16, 2024
1 parent 4374062 commit 602932b
Show file tree
Hide file tree
Showing 87 changed files with 2,285 additions and 1,172 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
@namespace Aspire.Dashboard.Components

@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Otlp.Model.MetricValues
@using Aspire.Dashboard.Resources
@using Metrics = Aspire.Dashboard.Components.Pages.Metrics
@inject IStringLocalizer<ControlsStrings> Loc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@ namespace Aspire.Dashboard.Components;

public partial class ChartContainer : ComponentBase, IAsyncDisposable
{
private readonly CounterChartViewModel _viewModel = new();

private OtlpInstrument? _instrument;
private PeriodicTimer? _tickTimer;
private Task? _tickTask;
private IDisposable? _themeChangedSubscription;
private int _renderedDimensionsCount;
private string? _previousMeterName;
private string? _previousInstrumentName;
private readonly InstrumentViewModel _instrumentViewModel = new InstrumentViewModel();

[Parameter, EditorRequired]
Expand All @@ -45,6 +41,9 @@ public partial class ChartContainer : ComponentBase, IAsyncDisposable
[Inject]
public required ThemeManager ThemeManager { get; init; }

[Inject]
public required CurrentChartViewModel ChartViewModel { get; init; }

protected override void OnInitialized()
{
// Update the graph every 200ms. This displays the latest data and moves time forward.
Expand Down Expand Up @@ -113,7 +112,7 @@ private async Task UpdateInstrumentDataAsync(OtlpInstrument instrument)

private bool MatchDimension(DimensionScope dimension)
{
foreach (var dimensionFilter in _viewModel.DimensionFilters)
foreach (var dimensionFilter in ChartViewModel.DimensionFilters)
{
if (!MatchFilter(dimension.Attributes, dimensionFilter))
{
Expand Down Expand Up @@ -156,14 +155,14 @@ protected override async Task OnParametersSetAsync()
return;
}

var hasInstrumentChanged = _previousMeterName != MeterName || _previousInstrumentName != InstrumentName;
_previousMeterName = MeterName;
_previousInstrumentName = InstrumentName;
var hasInstrumentChanged = ChartViewModel.PreviousMeterName != MeterName || ChartViewModel.PreviousInstrumentName != InstrumentName;
ChartViewModel.PreviousMeterName = MeterName;
ChartViewModel.PreviousInstrumentName = InstrumentName;

var filters = CreateUpdatedFilters(hasInstrumentChanged);

_viewModel.DimensionFilters.Clear();
_viewModel.DimensionFilters.AddRange(filters);
ChartViewModel.DimensionFilters.Clear();
ChartViewModel.DimensionFilters.AddRange(filters);

await UpdateInstrumentDataAsync(_instrument);
}
Expand Down Expand Up @@ -235,7 +234,7 @@ private List<DimensionFilterViewModel> CreateUpdatedFilters(bool hasInstrumentCh
}
else
{
var existing = _viewModel.DimensionFilters.SingleOrDefault(m => m.Name == item.Name);
var existing = ChartViewModel.DimensionFilters.SingleOrDefault(m => m.Name == item.Name);
if (existing != null)
{
// Select previously selected.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
@inject IStringLocalizer<ControlsStrings> Loc

<div class="metrics-filters-container">
@if (ViewModel.DimensionFilters.Count > 0)
@if (ChartViewModel.DimensionFilters.Count > 0)
{
<div class="metrics-filters-section">
<h5>@Loc[nameof(ControlsStrings.ChartContainerFiltersHeader)]</h5>
<FluentDataGrid Items="@Queryable.AsQueryable(ViewModel.DimensionFilters)" GridTemplateColumns="200px 1fr auto" GenerateHeader="GenerateHeaderOption.None">
<FluentDataGrid Items="@Queryable.AsQueryable(ChartViewModel.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.ChartContainerNoneSelected)] : string.Join(", ", c.SelectedValues.Select(v => v.Name)))">
Expand Down Expand Up @@ -79,7 +79,7 @@
<div>
<FluentSwitch Class="table-switch"
Label="@Loc[nameof(ControlsStrings.ChartContainerShowCountLabel)]"
@bind-Value="_showCount"
@bind-Value="ChartViewModel.ShowCounts"
@bind-Value:after="ShowCountChanged"/>
</div>
</div>
Expand All @@ -90,9 +90,6 @@
[Parameter, EditorRequired]
public required OtlpInstrument Instrument { get; set; }

[Parameter, EditorRequired]
public required CounterChartViewModel ViewModel { get; set; }

[Parameter, EditorRequired]
public required InstrumentViewModel InstrumentViewModel { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
using Microsoft.AspNetCore.Components;

namespace Aspire.Dashboard.Components;

public partial class ChartFilters
{
private bool _showCount;
[Inject]
public required CurrentChartViewModel ChartViewModel { get; init; }

protected override void OnInitialized()
{
InstrumentViewModel.DataUpdateSubscriptions.Add(() =>
{
_showCount = InstrumentViewModel.ShowCount;
ChartViewModel.ShowCounts = InstrumentViewModel.ShowCount;
return Task.CompletedTask;
});
}

private void ShowCountChanged()
{
InstrumentViewModel.ShowCount = _showCount;
InstrumentViewModel.ShowCount = ChartViewModel.ShowCounts;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@

<FluentStack Orientation="Orientation.Vertical" Style="margin-bottom: 20px;">
<FluentSwitch Class="table-switch"
@bind-Value="@_onlyShowValueChanges"
@bind-Value="@ChartViewModel.OnlyShowValueChangesInTable"
@bind-Value:after="SettingsChangedAsync"
Label="@Loc[nameof(ControlsStrings.MetricTableShowOnlyValueChanges)]"/>
</FluentStack>
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using System.Diagnostics;
using System.Globalization;
using Aspire.Dashboard.Components.Controls.Chart;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Utils;
Expand All @@ -24,7 +24,6 @@ public partial class MetricTable : ChartBase

private OtlpInstrument? _instrument;
private bool _showCount;
private bool _onlyShowValueChanges = true;
private DateTimeOffset? _lastUpdate;

private readonly CancellationTokenSource _waitTaskCancellationTokenSource = new();
Expand All @@ -34,6 +33,9 @@ public partial class MetricTable : ChartBase
[Inject]
public required IJSRuntime JS { get; init; }

[Inject]
public required CurrentChartViewModel ChartViewModel { get; init; }

[Inject]
public required IDialogService DialogService { get; init; }

Expand Down Expand Up @@ -139,7 +141,7 @@ private SortedList<DateTimeOffset, MetricViewBase> UpdateMetrics(out ISet<DateTi
continue;
}

if (_onlyShowValueChanges && valueDiffs.All(diff => DoubleEquals(diff, 0)))
if (ChartViewModel.OnlyShowValueChangesInTable && valueDiffs.All(diff => DoubleEquals(diff, 0)))
{
continue;
}
Expand Down Expand Up @@ -174,7 +176,7 @@ MetricViewBase CreateHistogramMetricView()
continue;
}

if (_onlyShowValueChanges && DoubleEquals(valueDiff, 0d))
if (ChartViewModel.OnlyShowValueChangesInTable && DoubleEquals(valueDiff, 0d))
{
continue;
}
Expand Down
56 changes: 56 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/DetailView.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@using Aspire.Dashboard.Components.Resize
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<ControlsStrings> Loc

<div class="details-container">
<header style="height: auto;">
@if (DetailsTitle is not null)
{
<div class="details-header-title" title="@DetailsTitle">@DetailsTitle</div>
}
else if (DetailsTitleTemplate is not null)
{
<div class="details-header-title">@DetailsTitleTemplate</div>
}
<div class="header-actions">
@if (ViewportInformation.IsDesktop)
{
<FluentButton Appearance="Appearance.Stealth"
IconEnd="@(Orientation == Orientation.Horizontal ? _splitHorizontalIcon : _splitVerticalIcon)"
OnClick="HandleToggleOrientation"
Title="@(Orientation == Orientation.Horizontal ? Loc[nameof(ControlsStrings.SummaryDetailsViewSplitHorizontal)] : Loc[nameof(ControlsStrings.SummaryDetailsViewSplitVertical)])"
aria-label="@(Orientation == Orientation.Horizontal ? Loc[nameof(ControlsStrings.SummaryDetailsViewSplitHorizontal)] : Loc[nameof(ControlsStrings.SummaryDetailsViewSplitVertical)])"/>
}

<FluentButton Appearance="Appearance.Stealth" BackgroundColor="@(ViewportInformation.IsDesktop ? null : "rgba(0, 0, 0, 0)")" IconEnd="@(new Icons.Regular.Size16.Dismiss())"
OnClick="HandleDismissAsync" Title="@Loc[nameof(ControlsStrings.SummaryDetailsViewCloseView)]" aria-label="@Loc[nameof(ControlsStrings.SummaryDetailsViewCloseView)]"/>
</div>
</header>
@Details
</div>

@code {
private readonly Icon _splitHorizontalIcon = new Icons.Regular.Size16.SplitHorizontal();
private readonly Icon _splitVerticalIcon = new Icons.Regular.Size16.SplitVertical();

[Parameter]
public string? DetailsTitle { get; set; }

[Parameter]
public RenderFragment? Details { get; set; }

[Parameter]
public RenderFragment? DetailsTitleTemplate { get; set; }

[Parameter]
public required Func<Task> HandleToggleOrientation { get; set; }

[Parameter]
public required Func<Task> HandleDismissAsync { get; set; }

[Parameter]
public required Orientation Orientation { get; set; }

[CascadingParameter]
public required ViewportInformation ViewportInformation { get; set; }
}
30 changes: 30 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@using Aspire.Dashboard.Model.Otlp
@inject IStringLocalizer<Resources.StructuredLogs> Loc

<FluentSelect TOption="SelectViewModel<LogLevel?>"
Items="@LogLevels"
Position="SelectPosition.Below"
OptionText="@(c => c.Name)"
Label="@(IncludeLabel ? Loc[nameof(Resources.StructuredLogs.StructuredLogsLevels)].Value : null)"
@bind-SelectedOption="@LogLevel"
@bind-SelectedOption:after="@HandleSelectedLogLevelChangedInternalAsync"
Width="120px"
Style="min-width: auto;"
AriaLabel="@Loc[nameof(Resources.StructuredLogs.StructuredLogsSelectMinimumLogLevel)]"/>

@code {
[Parameter]
public bool IncludeLabel { get; set; }

[Parameter, EditorRequired]
public required List<SelectViewModel<LogLevel?>> LogLevels { get; set; }

[Parameter, EditorRequired]
public required SelectViewModel<LogLevel?> LogLevel { get; set; }

[Parameter]
public EventCallback<SelectViewModel<LogLevel?>> LogLevelChanged { get; set; }

[Parameter, EditorRequired]
public required Func<Task> HandleSelectedLogLevelChangedAsync { get; set; }
}
16 changes: 16 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Controls;

public partial class LogLevelSelect : ComponentBase
{
private async Task HandleSelectedLogLevelChangedInternalAsync()
{
await LogLevelChanged.InvokeAsync(LogLevel);
await HandleSelectedLogLevelChangedAsync();
}
}

2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Controls/LogViewer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<div class="log-overflow continuous-scroll-overflow">
<div class="log-container" id="logContainer">
<Virtualize Items="_logEntries.GetEntries()" ItemSize="20" OverscanCount="100" TItem="LogEntry">
<Virtualize Items="@ViewModel.LogEntries.GetEntries()" ItemSize="20" OverscanCount="100" TItem="LogEntry">
<div class="line-row-container">
<div class="line-row">
<span class="line-area" role="log">
Expand Down
29 changes: 23 additions & 6 deletions src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.ConsoleLogs;
using Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
Expand All @@ -23,6 +24,12 @@ public sealed partial class LogViewer
[Inject]
public required BrowserTimeProvider TimeProvider { get; init; }

[Inject]
public required LogViewerViewModel ViewModel { get; init; }

[Inject]
public required DimensionManager DimensionManager { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_applicationChanged)
Expand All @@ -33,13 +40,22 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (firstRender)
{
await JS.InvokeVoidAsync("initializeContinuousScroll");
DimensionManager.OnBrowserDimensionsChanged += OnBrowserResize;
}
}

private readonly LogEntries _logEntries = new();
private void OnBrowserResize(object? o, EventArgs args)
{
InvokeAsync(async () =>
{
await JS.InvokeVoidAsync("resetContinuousScrollPosition");
await JS.InvokeVoidAsync("initializeContinuousScroll");
});
}

internal async Task SetLogSourceAsync(IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> batches, bool convertTimestampsFromUtc)
internal async Task SetLogSourceAsync(string resourceName, IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> batches, bool convertTimestampsFromUtc)
{
ViewModel.ResourceName = resourceName;
_convertTimestampsFromUtc = convertTimestampsFromUtc;

var cancellationToken = await _cancellationSeries.NextAsync();
Expand All @@ -57,12 +73,12 @@ internal async Task SetLogSourceAsync(IAsyncEnumerable<IReadOnlyList<ResourceLog
{
// Keep track of the base line number to ensure that we can calculate the line number of each log entry.
// This becomes important when the total number of log entries exceeds the limit and is truncated.
if (_logEntries.BaseLineNumber is null)
if (ViewModel.LogEntries.BaseLineNumber is null)
{
_logEntries.BaseLineNumber = lineNumber;
ViewModel.LogEntries.BaseLineNumber = lineNumber;
}

_logEntries.InsertSorted(logParser.CreateLogEntry(content, isErrorOutput));
ViewModel.LogEntries.InsertSorted(logParser.CreateLogEntry(content, isErrorOutput));
}

StateHasChanged();
Expand All @@ -84,12 +100,13 @@ internal async Task ClearLogsAsync()
await _cancellationSeries.ClearAsync();

_applicationChanged = true;
_logEntries.Clear();
ViewModel.LogEntries.Clear();
StateHasChanged();
}

public async ValueTask DisposeAsync()
{
await _cancellationSeries.ClearAsync();
DimensionManager.OnBrowserDimensionsChanged -= OnBrowserResize;
}
}
Loading

0 comments on commit 602932b

Please sign in to comment.