-
Notifications
You must be signed in to change notification settings - Fork 680
Add "Collapse all"/"Expand all" functionality in trace detail page with structured logs integration and root span preservation #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
Changes from all commits
0b57fc7
0fb3044
deb0de6
18c03a6
332b6b1
d5b5c90
34b388e
a14c166
4aa3b14
91db72a
90020cf
ea558a2
7d92864
85950ca
c3ee60c
1158e94
8f27460
a386742
abe9726
fd2a98d
4929653
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
{ | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should be using OnToggleCollapse There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should be using OnToggleCollapse There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted the common logic into helper functions |
||
{ | ||
if (viewModel.IsCollapsed) | ||
{ | ||
SetSpanCollapsedState(viewModel, false); | ||
} | ||
} | ||
|
||
await RefreshSpanViewAsync(); | ||
} | ||
|
||
private string GetResourceName(OtlpResourceView app) => OtlpResource.GetResourceName(app, _resources); | ||
|
||
private async Task ToggleSpanLogsAsync(OtlpLogEntry logEntry) | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).