Skip to content

Add "Collapse All"/"Expand All" buttons in the trace page #9474

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
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.

## Formatting

@@ -28,6 +30,7 @@
* 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.

## Running tests

4 changes: 4 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor
Original file line number Diff line number Diff line change
@@ -67,6 +67,10 @@
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)]" />
<FluentAnchor Appearance="Appearance.Lightweight" Href="@DashboardUrls.StructuredLogsUrl(traceId: trace.TraceId)">@ControlStringsLoc[nameof(ControlsStrings.ViewLogsLink)]</FluentAnchor>
</div>
</div>
107 changes: 102 additions & 5 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
@@ -8,8 +8,10 @@
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Microsoft.FluentUI.AspNetCore.Components;
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
using Microsoft.JSInterop;
@@ -35,6 +37,7 @@ 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 = new();

[Parameter]
public required string TraceId { get; set; }
@@ -64,6 +67,9 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
[Inject]
public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; }

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

protected override void OnInitialized()
{
_gridColumns = [
@@ -82,8 +88,31 @@ protected override void OnInitialized()
}));
}

UpdateTraceActionsMenu();

TelemetryContextProvider.Initialize(TelemetryContext);
}

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

_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 = ControlsLoc[nameof(ControlsStrings.ExpandAllSpansText)],
Icon = new Icons.Regular.Size16.ArrowExpandAll(),
OnClick = ExpandAllSpansAsync,
IsDisabled = !HasCollapsedSpans()
});

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

// Internal to be used in unit tests
internal ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridItemsProviderRequest<SpanWaterfallViewModel> request)
@@ -192,6 +221,7 @@ private void UpdateDetailViewData()
_spanWaterfallViewModels = null;
_maxDepth = 0;
_resourceCount = 0;
UpdateTraceActionsMenu(); // Update menu when no trace
return;
}

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

UpdateTraceActionsMenu(); // Update menu whenever view data changes
}

private async Task HandleAfterFilterBindAsync()
@@ -253,21 +285,30 @@ private string GetRowClass(SpanWaterfallViewModel viewModel)
public SpanDetailsViewModel? SelectedSpan { 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();
}

@@ -333,6 +374,62 @@ 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;
}

return _spanWaterfallViewModels.Any(vm => !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).

{
if (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(OtlpApplicationView app) => OtlpApplication.GetResourceName(app, _applications);

public void Dispose()
18 changes: 18 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.

6 changes: 6 additions & 0 deletions src/Aspire.Dashboard/Resources/ControlsStrings.resx
Original file line number Diff line number Diff line change
@@ -490,4 +490,10 @@
<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>
</root>
10 changes: 10 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.

10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf

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

10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf

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

10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf

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

10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf

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

10 changes: 10 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.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.