Skip to content

Commit cd1c9b4

Browse files
committed
Automatically convert URLs in values into links in the dashboard
1 parent 5b04e34 commit cd1c9b4

File tree

4 files changed

+61
-21
lines changed

4 files changed

+61
-21
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@
55
<div class="@GetContainerClass()" style="width: inherit;">
66
@if (EnableMasking && IsMasked)
77
{
8-
<span class="cellText">
8+
<span class="cellText" id="@_cellTextId">
99
&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;
1010
</span>
1111
}
12-
else if (EnableHighlighting)
12+
else if (EnableHighlighting && !string.IsNullOrEmpty(HighlightText))
1313
{
14-
<span class="cellText" title="@(ToolTip ?? Value)">
14+
<span class="cellText" title="@(ToolTip ?? Value)" id="@_cellTextId">
1515
@ContentBeforeValue
1616
<FluentHighlighter HighlightedText="@HighlightText" Text="@Value" />
1717
@ContentAfterValue
1818
</span>
1919
}
2020
else
2121
{
22-
<span class="cellText" title="@(ToolTip ?? Value)">
22+
<span class="cellText" title="@(ToolTip ?? Value)" id="@_cellTextId">
2323
@ContentBeforeValue
24-
@(MaxDisplayLength.HasValue ? TrimLength(Value) : Value)
24+
@((MarkupString)(_formattedValue ?? string.Empty))
2525
@ContentAfterValue
2626
</span>
2727
}

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

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Aspire.Dashboard.ConsoleLogs;
45
using Aspire.Dashboard.Resources;
56
using Microsoft.AspNetCore.Components;
67
using Microsoft.FluentUI.AspNetCore.Components;
@@ -66,9 +67,6 @@ public partial class GridValue
6667
[Parameter]
6768
public EventCallback<bool> IsMaskedChanged { get; set; }
6869

69-
[Parameter]
70-
public int? MaxDisplayLength { get; set; }
71-
7270
[Parameter]
7371
public string? ToolTip { get; set; }
7472

@@ -80,8 +78,11 @@ public partial class GridValue
8078

8179
private readonly Icon _maskIcon = new Icons.Regular.Size16.EyeOff();
8280
private readonly Icon _unmaskIcon = new Icons.Regular.Size16.Eye();
81+
private readonly string _cellTextId = $"celltext-{Guid.NewGuid():N}";
8382
private readonly string _copyId = $"copy-{Guid.NewGuid():N}";
8483
private readonly string _menuAnchorId = $"menu-{Guid.NewGuid():N}";
84+
private string? _value;
85+
private string? _formattedValue;
8586
private bool _isMenuOpen;
8687

8788
protected override void OnInitialized()
@@ -90,23 +91,44 @@ protected override void OnInitialized()
9091
PostCopyToolTip = Loc[nameof(ControlsStrings.GridValueCopied)];
9192
}
9293

93-
private string GetContainerClass() => EnableMasking ? "container masking-enabled" : "container";
94-
95-
private async Task ToggleMaskStateAsync()
94+
protected override void OnParametersSet()
9695
{
97-
IsMasked = !IsMasked;
98-
99-
await IsMaskedChanged.InvokeAsync(IsMasked);
96+
if (_value != Value)
97+
{
98+
_value = Value;
99+
100+
if (UrlParser.TryParse(_value, out var url))
101+
{
102+
_formattedValue = url;
103+
}
104+
else
105+
{
106+
_formattedValue = _value;
107+
}
108+
}
100109
}
101110

102-
private string TrimLength(string? text)
111+
protected override async Task OnAfterRenderAsync(bool firstRender)
103112
{
104-
if (text is not null && MaxDisplayLength is int maxLength && text.Length > maxLength)
113+
if (firstRender)
105114
{
106-
return text[..maxLength];
115+
// If the value and formatted value are different then there are hrefs in the text.
116+
// Add a click event to the cell text that stops propagation if a href is clicked.
117+
// This prevents details view from opening when the value is in a main page grid.
118+
if (_value != _formattedValue)
119+
{
120+
await JS.InvokeVoidAsync("setCellTextClickHandler", _cellTextId);
121+
}
107122
}
123+
}
108124

109-
return text ?? "";
125+
private string GetContainerClass() => EnableMasking ? "container masking-enabled" : "container";
126+
127+
private async Task ToggleMaskStateAsync()
128+
{
129+
IsMasked = !IsMasked;
130+
131+
await IsMaskedChanged.InvokeAsync(IsMasked);
110132
}
111133

112134
private void ToggleMenuOpen()

src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ public static bool TryParse(string? text, [NotNullWhen(true)] out string? modifi
1818
{
1919
var urlMatch = s_urlRegEx.Match(text);
2020

21-
var builder = new StringBuilder(text.Length * 2);
21+
StringBuilder? builder = null;
2222

2323
var nextCharIndex = 0;
2424
while (urlMatch.Success)
2525
{
26+
builder ??= new StringBuilder(text.Length * 2);
27+
2628
if (urlMatch.Index > 0)
2729
{
2830
builder.Append(text[(nextCharIndex)..urlMatch.Index]);
@@ -36,7 +38,7 @@ public static bool TryParse(string? text, [NotNullWhen(true)] out string? modifi
3638
urlMatch = urlMatch.NextMatch();
3739
}
3840

39-
if (builder.Length > 0)
41+
if (builder?.Length > 0)
4042
{
4143
if (nextCharIndex < text.Length)
4244
{

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,20 @@ window.registerOpenTextVisualizerOnClick = function(layout) {
350350

351351
window.unregisterOpenTextVisualizerOnClick = function (obj) {
352352
document.removeEventListener('click', obj.onClickListener);
353-
}
353+
};
354+
355+
window.setCellTextClickHandler = function (id) {
356+
var cellTextElement = document.getElementById(id);
357+
if (!cellTextElement) {
358+
return;
359+
}
360+
361+
cellTextElement.addEventListener('click', e => {
362+
// Propagation behavior:
363+
// - Link click stops. Link will open in a new window.
364+
// - Any other text allows propagation. Potentially opens details view.
365+
if (isElementTagName(e.target, 'a')) {
366+
e.stopPropagation();
367+
}
368+
});
369+
};

0 commit comments

Comments
 (0)