Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9b9eb28
Move GridValue copy button inside a menu
Jul 17, 2024
1aeb0de
add empty dialog
Jul 17, 2024
10f7cd2
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
Jul 22, 2024
1714da7
test
Jul 22, 2024
eebf7a4
Style dialog, work around fluentui onclick issue for FluentMenuItem
adamint Jul 22, 2024
86c1a5c
Remove hardcoded string
adamint Jul 22, 2024
ce36464
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Jul 23, 2024
633250a
Update unformatted name, use JsonSerializer.Serialize, use key compar…
adamint Jul 23, 2024
2b9ebab
Display dialog like in console logs
adamint Jul 23, 2024
fa0b2ea
Format based on language
adamint Jul 29, 2024
7a6336e
stream data
adamint Jul 29, 2024
3afba4c
Add TextVisualizerDialog component tests
adamint Jul 30, 2024
8cbdad8
fix logical error
adamint Jul 30, 2024
987c800
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Jul 30, 2024
1c618f2
fix console log css
adamint Jul 30, 2024
fd2dc69
Don't loose comments when formatting JSON
JamesNK Jul 31, 2024
e7ebf22
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Jul 31, 2024
1b8d8be
run custom tool after merge
adamint Jul 31, 2024
813cb75
Update ThemeManager to obtain effective theme, conditionally highligh…
adamint Aug 1, 2024
665b30b
remove unnecessary top margin, re-highlight if container content has …
adamint Aug 1, 2024
681789f
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Aug 1, 2024
78eac50
Add more tests
adamint Aug 1, 2024
68edbfa
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Aug 2, 2024
729889c
start at line 1, move copy button to top of dialog
adamint Aug 2, 2024
4b262ab
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Aug 7, 2024
6cd0349
change width/height display of dialog
adamint Aug 7, 2024
1593d0d
Update src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
JamesNK Aug 8, 2024
1892bc8
Style fixes
JamesNK Aug 8, 2024
bb2d8d9
Change text visualizer theming from server to client side
JamesNK Aug 8, 2024
d52819f
Move parameter to codebehind, properly disconnect observer
adamint Aug 8, 2024
18cdf6c
Allow users to click the menu item icons to prompt copy/open text vis…
adamint Aug 8, 2024
e29961f
Merge branch 'refs/heads/main' into dev/adamint/4412-multiline-string…
adamint Aug 10, 2024
bdedc1e
set width to 0 on cell menubutton to show as much column content as p…
adamint Aug 10, 2024
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
33 changes: 27 additions & 6 deletions src/Aspire.Dashboard/Components/Controls/GridValue.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@using Aspire.Dashboard.Extensions
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<ControlsStrings> Loc
@inject IStringLocalizer<Dialogs> DialogsLoc

<div class="@GetContainerClass()" style="width: inherit;">
@if (EnableMasking && IsMasked)
Expand Down Expand Up @@ -34,7 +35,7 @@
}

@{
(string, object)[] uncapturedAttributes = [
(string, object)[] uncapturedCopyAttributes = [
("alt", PreCopyToolTip),
("title", string.Empty),
("aria-label", Loc[nameof(ControlsStrings.GridValueCopyToClipboard)]),
Expand All @@ -43,12 +44,32 @@
}

<FluentButton Appearance="Appearance.Lightweight"
Id="@_anchorId"
Id="@_menuAnchorId"
Class="defaultHidden"
Style="float: right; flex-shrink: 0"
AdditionalAttributes="@FluentUIExtensions.GetClipboardCopyAdditionalAttributes(ValueToCopy ?? Value, PreCopyToolTip, PostCopyToolTip, uncapturedAttributes)">
<FluentIcon Class="copy-icon" Style="display:inline;" Icon="Icons.Regular.Size16.Copy" />
<FluentIcon Class="checkmark-icon" Style="display:none;" Icon="Icons.Regular.Size16.Checkmark" />
OnClick="@ToggleMenuOpen">
<FluentIcon Style="display:inline;" Icon="Icons.Regular.Size16.MoreVertical" />
</FluentButton>
<FluentTooltip @ref="_tooltipComponent" Anchor="@_anchorId" Position="TooltipPosition.Top" HideTooltipOnCursorLeave="true">@PreCopyToolTip</FluentTooltip>
</div>

<FluentMenu Anchor="@_menuAnchorId" @bind-Open="_isMenuOpen" VerticalThreshold="170">
<FluentMenuItem
Id="@_copyId"
AdditionalAttributes="@FluentUiExtensions.GetClipboardCopyAdditionalAttributes(ValueToCopy ?? Value, PreCopyToolTip, PostCopyToolTip, uncapturedCopyAttributes)">
<span slot="start">
<FluentIcon Class="copy-icon" Style="display:inline; vertical-align: text-bottom" Icon="Icons.Regular.Size16.Copy" Slot="start" />
<FluentIcon Class="checkmark-icon" Style="display:none; vertical-align: text-bottom" Icon="Icons.Regular.Size16.Checkmark" Slot="start" />
</span>
@PreCopyToolTip
</FluentMenuItem>

<FluentMenuItem
Disabled="@(Value is null)"
AdditionalAttributes="@FluentUiExtensions.GetOpenTextVisualizerAdditionalAttributes(Value!, ValueDescription)">
<span slot="start">
<FluentIcon Style="display:inline; vertical-align: text-bottom" Icon="Icons.Regular.Size16.Open" Slot="start"/>
</span>

@DialogsLoc[nameof(Dialogs.OpenInTextVisualizer)]
</FluentMenuItem>
</FluentMenu>
34 changes: 25 additions & 9 deletions src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Resources;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Components.Controls;

public partial class GridValue : IDisposable
public partial class GridValue
{
[Parameter, EditorRequired]
public string? Value { get; set; }

[Parameter, EditorRequired]
public required string ValueDescription { get; set; }

/// <summary>
/// Content to include, if any, after the Value string
/// </summary>
Expand Down Expand Up @@ -54,23 +59,34 @@ public partial class GridValue : IDisposable
[Parameter]
public string? ToolTip { get; set; }

[Parameter] public string PreCopyToolTip { get; set; } = null!;
[Parameter]
public string PreCopyToolTip { get; set; } = null!;

[Parameter]
public string PostCopyToolTip { get; set; } = null!;

[Inject]
public required IDialogService DialogService { get; init; }

[Parameter] public string PostCopyToolTip { get; set; } = null!;
[Inject]
public required IJSRuntime JS { get; init; }

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

private readonly Icon _maskIcon = new Icons.Regular.Size16.EyeOff();
private readonly Icon _unmaskIcon = new Icons.Regular.Size16.Eye();
private readonly string _anchorId = $"copy-{Guid.NewGuid():N}";

private FluentTooltip? _tooltipComponent;
private readonly string _copyId = $"copy-{Guid.NewGuid():N}";
private readonly string _menuAnchorId = $"menu-{Guid.NewGuid():N}";
private bool _isMenuOpen;

protected override void OnInitialized()
{
PreCopyToolTip = Loc[nameof(ControlsStrings.GridValueCopyToClipboard)];
PostCopyToolTip = Loc[nameof(ControlsStrings.GridValueCopied)];
}

private string GetContainerClass() => EnableMasking ? "container masking-enabled wrap" : "container wrap";
private string GetContainerClass() => EnableMasking ? "container masking-enabled" : "container";

private async Task ToggleMaskStateAsync()
=> await IsMaskedChanged.InvokeAsync(!IsMasked);
Expand All @@ -85,8 +101,8 @@ private string TrimLength(string? text)
return text ?? "";
}

public void Dispose()
private void ToggleMenuOpen()
{
_tooltipComponent?.Dispose();
_isMenuOpen = !_isMenuOpen;
}
}
13 changes: 9 additions & 4 deletions src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
GridTemplateColumns="@GridTemplateColumns"
ShowHover="true">
<TemplateColumn Title="@(NameColumnTitle ?? Loc[nameof(ControlsStrings.NameColumnHeader)])" Class="nameColumn" SortBy="@NameSort" Sortable="@IsNameSortable">
<GridValue Value="@NameColumnValue(context)" HighlightText="@HighlightText" />
<GridValue
ValueDescription="@(NameColumnTitle ?? Loc[nameof(ControlsStrings.NameColumnHeader)])"
Value="@NameColumnValue(context)"
HighlightText="@HighlightText" />
</TemplateColumn>
<TemplateColumn Title="@(ValueColumnTitle ?? Loc[nameof(ControlsStrings.PropertyGridValueColumnHeader)])" Class="valueColumn" SortBy="@ValueSort" Sortable="@IsValueSortable">
<GridValue Value="@ValueColumnValue(context)" HighlightText="@HighlightText"
EnableMasking="@EnableValueMasking" IsMasked="@GetIsItemMasked(context)"
IsMaskedChanged="(newValue) => OnIsMaskedChanged(context, newValue)" />
<GridValue
ValueDescription="@(ValueColumnTitle ?? Loc[nameof(ControlsStrings.PropertyGridValueColumnHeader)])"
Value="@ValueColumnValue(context)" HighlightText="@HighlightText"
EnableMasking="@EnableValueMasking" IsMasked="@GetIsItemMasked(context)"
IsMaskedChanged="(newValue) => OnIsMaskedChanged(context, newValue)"/>
@ExtraValueContent(context)
</TemplateColumn>
</FluentDataGrid>
Expand Down
17 changes: 13 additions & 4 deletions src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@
GridTemplateColumns="1fr 1.5fr"
ShowHover="true">
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
<GridValue Value="@(context.KnownProperty?.DisplayName ?? context.Key)" ToolTip="@context.Key" />
<GridValue
Value="@(context.KnownProperty?.DisplayName ?? context.Key)"
ValueDescription="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]"
ToolTip="@context.Key" />
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]" Class="valueColumn">
<GridValue Value="@GetDisplayedValue(TimeProvider, context)" ToolTip="@context.Tooltip" />
<GridValue
Value="@GetDisplayedValue(TimeProvider, context)"
ValueDescription="@(context.KnownProperty?.DisplayName ?? context.Key)"
ToolTip="@context.Tooltip" />
</TemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
Expand All @@ -70,10 +76,13 @@
GridTemplateColumns="1fr 1.5fr"
ShowHover="true">
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
<GridValue Value="@context.Name" />
<GridValue Value="@context.Name" ValueDescription="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" />
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]" Class="valueColumn">
<GridValue Value="@context.Text" MaxDisplayLength="0">
<GridValue
Value="@context.Text"
ValueDescription="@context.Name"
MaxDisplayLength="0">
<ContentAfterValue>
@if (context.Url != null)
{
Expand Down
51 changes: 51 additions & 0 deletions src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Resources

@inject IStringLocalizer<Dialogs> Loc
@implements IDialogContentComponent<Aspire.Dashboard.Model.TextVisualizerDialogViewModel>

<FluentDialogHeader ShowDismiss="true">
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Regular.Size24.SlideSearch())" />
<FluentLabel Typo="Typography.PaneHeader">
@Content.Description
</FluentLabel>

<FluentButton id="@_openSelectFormatButtonId" slot="end"
Style="margin-left: auto"
Appearance="Appearance.Accent"
IconEnd="@(new Icons.Regular.Size24.Filter())"
@onclick="() => _isSelectFormatPopupOpen = !_isSelectFormatPopupOpen"
Title="@Loc[nameof(Dialogs.TextVisualizerSelectFormatType)]"
aria-label="@Loc[nameof(Dialogs.TextVisualizerSelectFormatType)]"/>

<FluentPopover AnchorId="@_openSelectFormatButtonId"
@bind-Open="@_isSelectFormatPopupOpen"
AutoFocus="true">
<Body>
<FluentRadioGroup aria-labelledby="@_openSelectFormatButtonId"
@bind-Value="_selectedOption"
@bind-Value:after="@OnFormatOptionChanged"
Orientation="Orientation.Vertical">
@foreach (var option in _options)
{
<FluentRadio Value="@option.Id">@option.Name</FluentRadio>
}
</FluentRadioGroup>
</Body>
</FluentPopover>
</FluentStack>
</FluentDialogHeader>

<FluentDialogBody>
<pre style="white-space: pre-wrap; overflow-wrap: break-word">
@_formattedText
</pre>
</FluentDialogBody>

<FluentDialogFooter Visible="false" />

@code {
[Parameter, EditorRequired]
public required TextVisualizerDialogViewModel Content { get; set; }
}
116 changes: 116 additions & 0 deletions src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using System.Text.Json;
using System.Xml;
using System.Xml.Linq;
using Aspire.Dashboard.Model.Otlp;
using Microsoft.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Dialogs;

public partial class TextVisualizerDialog : ComponentBase
{
private const string XmlFormat = "xml";
private const string JsonFormat = "json";
private const string PlaintextFormat = "text";

private List<SelectViewModel<string>> _options = null!;
private string _selectedOption = null!;

private string _formattedText = string.Empty;

private readonly string _openSelectFormatButtonId = $"select-format-{Guid.NewGuid():N}";
private bool _isSelectFormatPopupOpen;

protected override void OnInitialized()
{
_options =
[
new SelectViewModel<string> { Id = PlaintextFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogPlaintextFormat)] },
new SelectViewModel<string> { Id = JsonFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogJsonFormat)] },
new SelectViewModel<string> { Id = XmlFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogXmlFormat)] },
];
}

protected override void OnParametersSet()
{
// We don't know what format the string is in, but we can guess
if (TryFormatXml())
{
_selectedOption = XmlFormat;
return;
}

if (TryFormatJson())
{
_selectedOption = JsonFormat;
return;
}

_selectedOption = PlaintextFormat;
_formattedText = Content.Text;
}

private bool TryFormatXml()
{
try
{
_formattedText = XElement.Parse(Content.Text).ToString();
return true;
}
catch (XmlException)
{
// If the XML is invalid, just show the original text
_formattedText = Content.Text;
return false;
}
}

private bool TryFormatJson()
{
try
{
using var doc = JsonDocument.Parse(
Content.Text,
new JsonDocumentOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
}
);

var memoryStream = new MemoryStream();
using var utf8JsonWriter = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Indented = true });
doc.WriteTo(utf8JsonWriter);
utf8JsonWriter.Flush();

_formattedText = Encoding.Default.GetString(memoryStream.ToArray());
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it's correct to use the default encoding here. The Utf8JsonWriter will write UTF8 bytes, which can only be decoded as UTF8.

Copy link
Member

Choose a reason for hiding this comment

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

+1. Add some unit tests with various width unicode characters.

return true;
}
catch (JsonException)
{
// If the JSON is invalid, just show the original text
_formattedText = Content.Text;
return false;
}
}

private void OnFormatOptionChanged()
{
if (_selectedOption == XmlFormat)
{
TryFormatXml();
}
else if (_selectedOption == JsonFormat)
{
TryFormatJson();
Copy link
Member

Choose a reason for hiding this comment

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

If these "try" operations fail, should we set _selectedOption back to the item representing no formatting?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so. The 'JSON' view for a non-JSON string is just the same as the unformatted view.

Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if you have invalid JSON or xml? Is it only going to format if its valid? I almost want the errors if the user explicitly selects json, but it's not valid json.

}
else
{
_formattedText = Content.Text;
}
}
}

Loading