Skip to content
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b57fc7
Initial plan for issue
Copilot May 22, 2025
0fb3044
Add localized strings for Collapse All and Expand All buttons
Copilot May 22, 2025
deb0de6
Add trace actions dropdown menu with Collapse All and Expand All buttons
Copilot May 22, 2025
18c03a6
Add tests for Collapse All and Expand All buttons
Copilot May 22, 2025
332b6b1
Merge branch 'copilot/fix-9434' of https://github.com/dotnet/aspire i…
danmoseley May 29, 2025
d5b5c90
more instructions
danmoseley May 29, 2025
34b388e
Uncomment and fix Collapse All and Expand All tests
Copilot May 29, 2025
a14c166
Update tests/Aspire.Dashboard.Components.Tests/Pages/TraceDetailsTest…
danmoseley May 29, 2025
4aa3b14
Replace hardcoded "Collapse All" and "Expand All" strings with locali…
Copilot May 29, 2025
91db72a
Address code review feedback: remove unused IDs, make methods private…
Copilot May 29, 2025
90020cf
Extract common span collapse/expand logic into helper functions
Copilot May 29, 2025
ea558a2
Resolve merge conflicts with main branch
Copilot Jul 30, 2025
7d92864
Changes before error encountered
Copilot Jul 30, 2025
85950ca
Merge branch 'main' into copilot/fix-9434
Jul 31, 2025
c3ee60c
didn't show in desktop toolbar, needs to close mobile toolbar after u…
Jul 31, 2025
1158e94
Reorganize trace menu structure with "View structured logs" at top
Copilot Aug 22, 2025
8f27460
Update text to lowercase and add ViewStructuredLogsText resource, exc…
Copilot Aug 22, 2025
a386742
Fix
JamesNK Aug 22, 2025
abe9726
Merge remote-tracking branch 'origin/main' into copilot/fix-9434
JamesNK Aug 22, 2025
fd2a98d
Fix test
JamesNK Aug 22, 2025
4929653
Fix test
JamesNK Aug 22, 2025
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
3 changes: 3 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
* Make only high confidence suggestions when reviewing code changes.
* Always use the latest version C#, currently C# 13 features.
* Never change global.json unless explicitly asked to.
* Never change package.json or package-lock.json files unless explicitly asked to.
* Never change NuGet.config files unless explicitly asked to.
* Don't update files under `*/api/*.cs` (e.g. src/Aspire.Hosting/api/Aspire.Hosting.cs) as they are generated.

## Formatting
@@ -37,6 +39,7 @@ In building and testing, never use `dotnet` without extension. Use `./dotnet.sh`
* Do not emit "Act", "Arrange" or "Assert" comments.
* We do not use any mocking framework at the moment.
* Copy existing style in nearby files for test method names and capitalization.
* Do not leave newly-added tests commented out. All added tests should be building and passing.
* Do not use Directory.SetCurrentDirectory in tests as it can cause side effects when tests execute concurrently.

## Running tests
30 changes: 17 additions & 13 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor
Original file line number Diff line number Diff line change
@@ -21,7 +21,8 @@
<AspirePageContentLayout
AddNewlineOnToolbar="true"
MobileToolbarButtonText="@Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailMobileToolbarButtonText)]"
IsSummaryDetailsViewOpen="@(SelectedData is not null)">
IsSummaryDetailsViewOpen="@(SelectedData is not null)"
@ref="@_layout">
<PageTitleSection>
<div class="page-header">
<h1>
@@ -42,23 +43,26 @@
ImmediateDelay="@FluentUIExtensions.InputDelay"
Placeholder="@ControlStringsLoc[nameof(ControlsStrings.FilterPlaceholder)]"
title="@Loc[nameof(Dashboard.Resources.Traces.TracesNameFilter)]"/>
<FluentAnchor Appearance="Appearance.Lightweight"
Href="@DashboardUrls.StructuredLogsUrl(traceId: trace.TraceId)">@ControlStringsLoc[nameof(ControlsStrings.ViewLogsLink)]</FluentAnchor>
<AspireMenuButton ButtonAppearance="Appearance.Lightweight"
Icon="@(new Icons.Regular.Size20.Options())"
Items="@_traceActionsMenuItems"
Title="@ControlStringsLoc[nameof(ControlsStrings.ActionsButtonText)]" />
</div>
</div>
}
else
{
<FluentToolbar>
<FluentSearch @bind-Value="_filter"
@bind-Value:after="HandleAfterFilterBindAsync"
Immediate="true"
ImmediateDelay="@FluentUIExtensions.InputDelay"
Placeholder="@ControlStringsLoc[nameof(ControlsStrings.FilterPlaceholder)]"
title="@Loc[nameof(Dashboard.Resources.Traces.TracesNameFilter)]"/>
<FluentAnchor Appearance="Appearance.Lightweight"
Href="@DashboardUrls.StructuredLogsUrl(traceId: trace.TraceId)">@ControlStringsLoc[nameof(ControlsStrings.ViewLogsLink)]</FluentAnchor>
</FluentToolbar>
<FluentSearch @bind-Value="_filter"
@bind-Value:after="HandleAfterFilterBindAsync"
Immediate="true"
ImmediateDelay="@FluentUIExtensions.InputDelay"
Placeholder="@ControlStringsLoc[nameof(ControlsStrings.FilterPlaceholder)]"
title="@Loc[nameof(Dashboard.Resources.Traces.TracesNameFilter)]"/>
<AspireMenuButton ButtonAppearance="Appearance.Lightweight"
Icon="@(new Icons.Regular.Size20.Options())"
Items="@_traceActionsMenuItems"
Title="@ControlStringsLoc[nameof(ControlsStrings.ActionsButtonText)]"
Text="@ControlStringsLoc[nameof(ControlsStrings.ActionsButtonText)]"/>
}
</ToolbarSection>
<MainSection>
139 changes: 131 additions & 8 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Runtime.InteropServices;
using Aspire.Dashboard.Components.Layout;
using Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Otlp;
@@ -15,6 +16,7 @@
using Microsoft.Extensions.Localization;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;

namespace Aspire.Dashboard.Components.Pages;

@@ -23,6 +25,7 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
private const string NameColumn = nameof(NameColumn);
private const string TicksColumn = nameof(TicksColumn);
private const string ActionsColumn = nameof(ActionsColumn);
private const int RootSpanDepth = 1;

private readonly List<IDisposable> _peerChangesSubscriptions = new();
private OtlpTrace? _trace;
@@ -37,6 +40,8 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
private GridColumnManager _manager = null!;
private IList<GridColumn> _gridColumns = null!;
private string _filter = string.Empty;
private readonly List<MenuButtonItem> _traceActionsMenuItems = [];
private AspirePageContentLayout? _layout;

[Parameter]
public required string TraceId { get; set; }
@@ -63,9 +68,6 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
[Inject]
public required NavigationManager NavigationManager { get; init; }

[Inject]
public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; }

[Inject]
public required IStringLocalizer<Dashboard.Resources.TraceDetail> Loc { get; init; }

@@ -75,6 +77,9 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
[Inject]
public required IStringLocalizer<ControlsStrings> ControlStringsLoc { get; init; }

[Inject]
public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; }

[CascadingParameter]
public required ViewportInformation ViewportInformation { get; set; }

@@ -97,6 +102,48 @@ protected override void OnInitialized()
await InvokeAsync(_dataGrid.SafeRefreshDataAsync);
}));
}

UpdateTraceActionsMenu();
}

private void UpdateTraceActionsMenu()
{
_traceActionsMenuItems.Clear();

// Add "View structured logs" at the top
_traceActionsMenuItems.Add(new MenuButtonItem
{
Text = ControlStringsLoc[nameof(ControlsStrings.ViewStructuredLogsText)],
Icon = new Icons.Regular.Size16.SlideTextSparkle(),
OnClick = () =>
{
NavigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(traceId: _trace?.TraceId));
return Task.CompletedTask;
}
});

// Add divider
_traceActionsMenuItems.Add(new MenuButtonItem
{
IsDivider = true
});

// Add expand/collapse options
_traceActionsMenuItems.Add(new MenuButtonItem
Copy link
Member

Choose a reason for hiding this comment

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

The expand all button should only be enabled when there is a trace span that is collapsed. Likewise, the collapse all button should only be enabled if there is a trace span that is NOT collapsed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented conditional enabling: "Expand All" is only enabled when there are collapsed spans, and "Collapse All" is only enabled when there are spans that can be collapsed (have children and are not already collapsed). The menu state updates dynamically as spans are toggled (commit 91db72a).

{
Text = ControlStringsLoc[nameof(ControlsStrings.ExpandAllSpansText)],
Icon = new Icons.Regular.Size16.ArrowExpandAll(),
OnClick = ExpandAllSpansAsync,
IsDisabled = !HasCollapsedSpans()
});

_traceActionsMenuItems.Add(new MenuButtonItem
{
Text = ControlStringsLoc[nameof(ControlsStrings.CollapseAllSpansText)],
Icon = new Icons.Regular.Size16.ArrowCollapseAll(),
OnClick = CollapseAllSpansAsync,
IsDisabled = !HasExpandedSpans()
});
}

// Internal to be used in unit tests
@@ -186,6 +233,7 @@ private void UpdateDetailViewData()
_spanWaterfallViewModels = null;
_maxDepth = 0;
_resourceCount = 0;
UpdateTraceActionsMenu();
return;
}

@@ -220,6 +268,8 @@ private void UpdateDetailViewData()
}
}
_resourceCount = apps.Count;

UpdateTraceActionsMenu();
}

private async Task HandleAfterFilterBindAsync()
@@ -281,22 +331,37 @@ private string GetRowClass(SpanWaterfallViewModel viewModel)
public TraceDetailSelectedDataViewModel? SelectedData { get; set; }

private async Task OnToggleCollapse(SpanWaterfallViewModel viewModel)
{
SetSpanCollapsedState(viewModel, !viewModel.IsCollapsed);
await RefreshSpanViewAsync();
}

private void SetSpanCollapsedState(SpanWaterfallViewModel viewModel, bool isCollapsed)
{
// View model data is recreated if the trace updates.
// Persist the collapsed state in a separate list.
if (viewModel.IsCollapsed)
viewModel.IsCollapsed = isCollapsed;
if (isCollapsed)
{
viewModel.IsCollapsed = false;
_collapsedSpanIds.Remove(viewModel.Span.SpanId);
_collapsedSpanIds.Add(viewModel.Span.SpanId);
}
else
{
viewModel.IsCollapsed = true;
_collapsedSpanIds.Add(viewModel.Span.SpanId);
_collapsedSpanIds.Remove(viewModel.Span.SpanId);
}
}

private async Task RefreshSpanViewAsync()
{
UpdateDetailViewData();
UpdateTraceActionsMenu();
await _dataGrid.SafeRefreshDataAsync();

await InvokeAsync(StateHasChanged);

// Close mobile toolbar if open, as the content has changed.
Debug.Assert(_layout is not null);
await _layout.CloseMobileToolbarAsync();
}

private async Task OnShowPropertiesAsync(SpanWaterfallViewModel viewModel, string? buttonId)
@@ -364,6 +429,64 @@ private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)
_elementIdBeforeDetailsViewOpened = null;
}

private bool HasCollapsedSpans()
{
if (_spanWaterfallViewModels is null)
{
return false;
}

return _spanWaterfallViewModels.Any(vm => vm.IsCollapsed);
}

private bool HasExpandedSpans()
{
if (_spanWaterfallViewModels is null)
{
return false;
}

// Don't consider root spans (depth 0) when determining if collapse all should be enabled
return _spanWaterfallViewModels.Any(vm => vm.Depth > RootSpanDepth && !vm.IsCollapsed && vm.Children.Count > 0);
}

private async Task CollapseAllSpansAsync()
{
if (_spanWaterfallViewModels is null)
{
return;
}

foreach (var viewModel in _spanWaterfallViewModels)
Copy link
Member

Choose a reason for hiding this comment

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

You should be using OnToggleCollapse

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to follow the same state management pattern as OnToggleCollapse - manually updating both viewModel.IsCollapsed and _collapsedSpanIds list (commit 91db72a).

{
// Don't collapse root spans.
if (viewModel.Depth > RootSpanDepth && viewModel.Children.Count > 0 && !viewModel.IsCollapsed)
{
SetSpanCollapsedState(viewModel, true);
}
}

await RefreshSpanViewAsync();
}

private async Task ExpandAllSpansAsync()
{
if (_spanWaterfallViewModels is null)
{
return;
}

foreach (var viewModel in _spanWaterfallViewModels)
Copy link
Member

Choose a reason for hiding this comment

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

You should be using OnToggleCollapse

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to follow the same state management pattern as OnToggleCollapse - manually updating both viewModel.IsCollapsed and _collapsedSpanIds list (commit 91db72a).

Copy link
Member

Choose a reason for hiding this comment

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

No, extract this logic and the logic in OnToggleCollapse into a helper function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted the common logic into helper functions SetSpanCollapsedState and RefreshSpanViewAsync. All three methods now use the shared logic to avoid duplication (commit 90020cf).

{
if (viewModel.IsCollapsed)
{
SetSpanCollapsedState(viewModel, false);
}
}

await RefreshSpanViewAsync();
}

private string GetResourceName(OtlpResourceView app) => OtlpResource.GetResourceName(app, _resources);

private async Task ToggleSpanLogsAsync(OtlpLogEntry logEntry)
2 changes: 2 additions & 0 deletions src/Aspire.Dashboard/Model/Otlp/SpanWaterfallViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// 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 System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Otlp.Model;

namespace Aspire.Dashboard.Model.Otlp;

[DebuggerDisplay("Span = {Span.SpanId}, Depth = {Depth}, Children = {Children.Count}, HasUninstrumentedPeer = {HasUninstrumentedPeer}, IsHidden = {IsHidden}")]
public sealed class SpanWaterfallViewModel
{
public required List<SpanWaterfallViewModel> Children { get; init; }
27 changes: 27 additions & 0 deletions src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Aspire.Dashboard/Resources/ControlsStrings.resx
Original file line number Diff line number Diff line change
@@ -469,6 +469,15 @@
<data name="ToggleNesting" xml:space="preserve">
<value>Toggle nesting</value>
</data>
<data name="CollapseAllSpansText" xml:space="preserve">
<value>Collapse all</value>
</data>
<data name="ExpandAllSpansText" xml:space="preserve">
<value>Expand all</value>
</data>
<data name="ViewStructuredLogsText" xml:space="preserve">
<value>View structured logs</value>
</data>
<data name="ShowHiddenResources" xml:space="preserve">
<value>Show hidden resources</value>
</data>
15 changes: 15 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Oops, something went wrong.
Loading
Oops, something went wrong.