Skip to content

Commit cb5c0d5

Browse files
authored
Show exception details in text visualizer dialog (#6230)
Exception details are shown behind a button in two places: 1. In the "Message" column of the structured logs table, when an exception exists. 2. In the "Health reports" grid in resource details, when a health report includes exception details. Previously, the stack trace would be shown in a popup when the mouse hovered over the icon, sort of like a tooltip. This commit changes the behaviour so that exception details are shown in the existing text visualizer dialog whenever the icon is clicked. The icon becomes a `FluentButton` for consistency with the menu button to the right of the cell. Also some optimisations in `GridValue`: - Don't use `FluentHighlighter` when the text to highlight is empty, which will be the common case. This improves UI performance. - Remove `MaxDisplayLength`. We never set this property, and it used space on every `GridValue` instance, and we create a lot of these objects. - Remove some redundant DOM elements. - Support adding arbitrary buttons to the right-hand side of the area (used for the "exception details" button).
1 parent 8685dd0 commit cb5c0d5

File tree

7 files changed

+93
-103
lines changed

7 files changed

+93
-103
lines changed
Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,35 @@
11
@namespace Aspire.Dashboard.Components
22

33
@using Aspire.Dashboard.Extensions
4+
@using Aspire.Dashboard.Components.Dialogs
45
@using Aspire.Dashboard.Otlp.Model
56
@using Aspire.Dashboard.Resources
67

78
@inject IStringLocalizer<ControlsStrings> Loc
89

9-
@{
10-
var iconId = Guid.NewGuid().ToString();
11-
@*
12-
* We don't want a browser tooltip to display when hovering over the error icon.
13-
* Use alt instead of title for screen readers. Also, the column has a title set
14-
* so add a whitespace title to prevent a browser tooltip from displaying.
15-
*@
16-
var title = " ";
17-
}
18-
1910
@if (!string.IsNullOrWhiteSpace(ExceptionText))
2011
{
21-
<FluentIcon alt="@Loc[nameof(ControlsStrings.ExceptionDetailsTitle)]"
22-
title="@title"
23-
Style="flex-shrink: 0"
24-
Id="@iconId"
25-
Icon="Icons.Filled.Size16.DocumentError"
26-
Color="Color.Accent"
27-
Class="severity-icon" />
28-
29-
<FluentTooltip Anchor="@iconId" MaxWidth="650px">
30-
<div style="padding: 10px">
31-
<h4>@Loc[nameof(ControlsStrings.ExceptionDetailsTitle)]</h4>
32-
33-
<pre style="white-space: pre-wrap; overflow-wrap: break-word; max-height: 300px; overflow: auto;">@ExceptionText</pre>
34-
35-
<div style="float: right; margin-bottom: 10px;">
36-
<FluentButton Id="@Guid.NewGuid().ToString()"
37-
Appearance="Appearance.Lightweight"
38-
AdditionalAttributes="@FluentUIExtensions.GetClipboardCopyAdditionalAttributes(ExceptionText, Loc[nameof(ControlsStrings.GridValueCopyToClipboard)].ToString(), Loc[nameof(ControlsStrings.GridValueCopied)].ToString())">
39-
@Loc[nameof(ControlsStrings.GridValueCopyToClipboard)]
40-
41-
<FluentIcon Class="copy-icon align-text-bottom" Style="display:inline;" Icon="Icons.Regular.Size16.Copy" />
42-
<FluentIcon Class="checkmark-icon align-text-bottom" Style="display:none;" Icon="Icons.Regular.Size16.Checkmark" />
43-
</FluentButton>
44-
</div>
45-
</div>
46-
</FluentTooltip>
12+
<FluentButton Appearance="Appearance.Lightweight"
13+
OnClick="OnExceptionDetailsClicked"
14+
Title="@Loc[nameof(ControlsStrings.ExceptionDetailsTitle)]"
15+
Class="exception-details-button">
16+
<FluentIcon Icon="Icons.Filled.Size16.DocumentError"
17+
Color="Color.Accent" />
18+
</FluentButton>
4719
}
4820

4921
@code {
5022
[Parameter, EditorRequired]
5123
public required string ExceptionText { get; set; }
24+
25+
[CascadingParameter]
26+
public required ViewportInformation ViewportInformation { get; init; }
27+
28+
[Inject]
29+
public required IDialogService DialogService { get; init; }
30+
31+
private void OnExceptionDetailsClicked(MouseEventArgs e)
32+
{
33+
_ = TextVisualizerDialog.OpenDialogAsync(ViewportInformation, DialogService, Loc[nameof(ControlsStrings.ExceptionDetailsTitle)], ExceptionText);
34+
}
5235
}

src/Aspire.Dashboard/Components/Controls/GridValue.razor

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,27 @@
33
@inject IStringLocalizer<Dialogs> DialogsLoc
44

55
<div class="@GetContainerClass()" style="width: inherit;">
6+
7+
@* Value area *@
8+
69
@if (EnableMasking && IsMasked)
710
{
8-
<span class="cellText">
11+
<span class="grid-value masked">
912
&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;
1013
</span>
1114
}
12-
else if (EnableHighlighting)
13-
{
14-
<span class="cellText" title="@(ToolTip ?? Value)">
15-
@ContentBeforeValue
16-
<FluentHighlighter HighlightedText="@HighlightText" Text="@Value" />
17-
@ContentAfterValue
18-
</span>
19-
}
2015
else
2116
{
22-
<span class="cellText" title="@(ToolTip ?? Value)">
17+
<span class="grid-value" title="@(ToolTip ?? Value)">
2318
@ContentBeforeValue
24-
@(MaxDisplayLength.HasValue ? TrimLength(Value) : Value)
19+
@if (EnableHighlighting && !string.IsNullOrEmpty(HighlightText))
20+
{
21+
<FluentHighlighter HighlightedText="@HighlightText" Text="@Value" />
22+
}
23+
else
24+
{
25+
@Value
26+
}
2527
@ContentAfterValue
2628
</span>
2729
}
@@ -35,46 +37,52 @@
3537
];
3638
}
3739

38-
<div @onclick:stopPropagation="true" class="defaultHidden">
39-
<FluentButton Appearance="Appearance.Lightweight"
40-
Id="@_menuAnchorId"
41-
Class="defaultHidden"
42-
Style="float: right; flex-shrink: 0"
43-
OnClick="@ToggleMenuOpen">
44-
<FluentIcon Style="display:inline;" Icon="Icons.Regular.Size16.MoreHorizontal" />
45-
</FluentButton>
40+
@* Button area *@
4641

47-
<FluentMenu Anchor="@_menuAnchorId" @bind-Open="_isMenuOpen" VerticalThreshold="170" HorizontalPosition="HorizontalPosition.End">
48-
<FluentMenuItem
49-
Id="@_copyId"
50-
AdditionalAttributes="@FluentUIExtensions.GetClipboardCopyAdditionalAttributes(ValueToCopy ?? Value, PreCopyToolTip, PostCopyToolTip, uncapturedCopyAttributes)">
51-
<span slot="start">
52-
<FluentIcon Class="copy-icon" Style="display:inline; vertical-align: text-bottom" Icon="Icons.Regular.Size16.Copy" Slot="start" />
53-
<FluentIcon Class="checkmark-icon" Style="display:none; vertical-align: text-bottom" Icon="Icons.Regular.Size16.Checkmark" Slot="start" />
54-
</span>
55-
@PreCopyToolTip
56-
</FluentMenuItem>
42+
<div @onclick:stopPropagation="true">
5743

58-
<FluentMenuItem
59-
Disabled="@(ValueToVisualize is null && Value is null)"
60-
AdditionalAttributes="@FluentUIExtensions.GetOpenTextVisualizerAdditionalAttributes(ValueToVisualize ?? Value!, !string.IsNullOrEmpty(TextVisualizerTitle) ? TextVisualizerTitle : ValueDescription)">
61-
<span slot="start">
62-
<FluentIcon Style="display:inline; vertical-align: text-bottom" Icon="Icons.Regular.Size16.Open" Slot="start"/>
63-
</span>
44+
<span class="defaultHidden">
45+
<FluentButton Appearance="Appearance.Lightweight"
46+
Id="@_menuAnchorId"
47+
OnClick="@ToggleMenuOpen">
48+
<FluentIcon Icon="Icons.Regular.Size16.MoreHorizontal" />
49+
</FluentButton>
6450

65-
@DialogsLoc[nameof(Dialogs.OpenInTextVisualizer)]
66-
</FluentMenuItem>
67-
</FluentMenu>
68-
</div>
51+
<FluentMenu Anchor="@_menuAnchorId" @bind-Open="_isMenuOpen" VerticalThreshold="170" HorizontalPosition="HorizontalPosition.End">
52+
<FluentMenuItem
53+
Id="@_copyId"
54+
AdditionalAttributes="@FluentUIExtensions.GetClipboardCopyAdditionalAttributes(ValueToCopy ?? Value, PreCopyToolTip, PostCopyToolTip, uncapturedCopyAttributes)">
55+
<span slot="start">
56+
<FluentIcon Style="vertical-align: text-bottom" Icon="Icons.Regular.Size16.Copy" />
57+
</span>
58+
@PreCopyToolTip
59+
</FluentMenuItem>
6960

70-
@if (EnableMasking)
71-
{
72-
<div @onclick:stopPropagation="true">
61+
<FluentMenuItem
62+
Disabled="@(ValueToVisualize is null && Value is null)"
63+
AdditionalAttributes="@FluentUIExtensions.GetOpenTextVisualizerAdditionalAttributes(ValueToVisualize ?? Value!, !string.IsNullOrEmpty(TextVisualizerTitle) ? TextVisualizerTitle : ValueDescription)">
64+
<span slot="start">
65+
<FluentIcon Style="vertical-align: text-bottom" Icon="Icons.Regular.Size16.Open" />
66+
</span>
67+
68+
@DialogsLoc[nameof(Dialogs.OpenInTextVisualizer)]
69+
</FluentMenuItem>
70+
</FluentMenu>
71+
</span>
72+
73+
@if (ContentInButtonArea is not null)
74+
{
75+
@ContentInButtonArea
76+
}
77+
78+
@if (EnableMasking)
79+
{
7380
<FluentButton Appearance="Appearance.Lightweight"
7481
IconEnd="@(IsMasked ? _unmaskIcon : _maskIcon)"
7582
Title="@(IsMasked ? Loc[nameof(ControlsStrings.GridValueMaskShowValue)] : Loc[nameof(ControlsStrings.GridValueMaskHideValue)])"
7683
OnClick="ToggleMaskStateAsync"
7784
aria-label="@(IsMasked ? Loc[nameof(ControlsStrings.GridValueMaskShowValue)] : Loc[nameof(ControlsStrings.GridValueMaskHideValue)])" />
78-
</div>
79-
}
85+
}
86+
87+
</div>
8088
</div>

src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public partial class GridValue
3030
[Parameter]
3131
public RenderFragment? ContentAfterValue { get; set; }
3232

33+
/// <summary>
34+
/// Content to include, if any, in button area to right. Intended for adding extra buttons.
35+
/// </summary>
36+
[Parameter]
37+
public RenderFragment? ContentInButtonArea { get; set; }
38+
3339
/// <summary>
3440
/// If set, copies this value instead of <see cref="Value"/>.
3541
/// </summary>
@@ -66,9 +72,6 @@ public partial class GridValue
6672
[Parameter]
6773
public EventCallback<bool> IsMaskedChanged { get; set; }
6874

69-
[Parameter]
70-
public int? MaxDisplayLength { get; set; }
71-
7275
[Parameter]
7376
public string? ToolTip { get; set; }
7477

@@ -99,16 +102,6 @@ private async Task ToggleMaskStateAsync()
99102
await IsMaskedChanged.InvokeAsync(IsMasked);
100103
}
101104

102-
private string TrimLength(string? text)
103-
{
104-
if (text is not null && MaxDisplayLength is int maxLength && text.Length > maxLength)
105-
{
106-
return text[..maxLength];
107-
}
108-
109-
return text ?? "";
110-
}
111-
112105
private void ToggleMenuOpen()
113106
{
114107
_isMenuOpen = !_isMenuOpen;

src/Aspire.Dashboard/Components/Controls/GridValue.razor.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
grid-template-columns: 1fr auto auto;
1010
}
1111

12-
::deep .cellText {
12+
::deep .grid-value {
1313
text-overflow: ellipsis;
1414
overflow: hidden;
1515
white-space: nowrap;
@@ -19,9 +19,10 @@
1919
opacity: 0;
2020
cursor: pointer;
2121
width: 0;
22+
display: inline-block;
2223
}
2324

24-
::deep:hover .defaultHidden, ::deep:focus-within .defaultHidden {
25+
::deep:hover .defaultHidden, ::deep:focus-within .defaultHidden {
2526
opacity: 1; /* safari has a bug where hover is not always called on an invisible element, so we use opacity instead */
2627
width: auto;
2728
}

src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@
166166
EnableHighlighting="@(!string.IsNullOrEmpty(_filter))"
167167
HighlightText="@_filter"
168168
TextVisualizerTitle="@context.Name">
169-
<ContentBeforeValue>
169+
<ContentInButtonArea>
170170
<ExceptionDetails ExceptionText="@context.ExceptionText" />
171-
</ContentBeforeValue>
171+
</ContentInButtonArea>
172172
</GridValue>
173173
</AspireTemplateColumn>
174174
</FluentDataGrid>

src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
ValueDescription="@Loc[nameof(StructuredLogs.StructuredLogsMessageColumnHeader)]"
1111
EnableHighlighting="true"
1212
HighlightText="@FilterText">
13-
<ContentBeforeValue>
13+
<ContentInButtonArea>
1414
<ExceptionDetails ExceptionText="@_exceptionText" />
15-
</ContentBeforeValue>
15+
</ContentInButtonArea>
1616
</GridValue>
1717

1818
@code {

src/Aspire.Dashboard/wwwroot/js/app.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,18 @@ window.copyTextToClipboard = function (id, text, precopy, postcopy) {
133133

134134
const copyIcon = button.querySelector('.copy-icon');
135135
const checkmarkIcon = button.querySelector('.checkmark-icon');
136+
136137
const anchoredTooltip = document.querySelector(`fluent-tooltip[anchor="${id}"]`);
137138
const tooltipDiv = anchoredTooltip ? anchoredTooltip.children[0] : null;
138139
navigator.clipboard.writeText(text)
139140
.then(() => {
140141
if (tooltipDiv) {
141142
tooltipDiv.innerText = postcopy;
142143
}
143-
copyIcon.style.display = 'none';
144-
checkmarkIcon.style.display = 'inline';
144+
if (copyIcon && checkmarkIcon) {
145+
copyIcon.style.display = 'none';
146+
checkmarkIcon.style.display = 'inline';
147+
}
145148
})
146149
.catch(() => {
147150
if (tooltipDiv) {
@@ -154,8 +157,10 @@ window.copyTextToClipboard = function (id, text, precopy, postcopy) {
154157
tooltipDiv.innerText = precopy;
155158
}
156159

157-
copyIcon.style.display = 'inline';
158-
checkmarkIcon.style.display = 'none';
160+
if (copyIcon && checkmarkIcon) {
161+
copyIcon.style.display = 'inline';
162+
checkmarkIcon.style.display = 'none';
163+
}
159164
delete button.dataset.copyTimeout;
160165
}, 1500);
161166
};

0 commit comments

Comments
 (0)