diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor b/src/Aspire.Dashboard/Components/Controls/GridValue.razor new file mode 100644 index 0000000000..a7189556e7 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor @@ -0,0 +1,32 @@ +@inject IJSRuntime JS + +
+ @if (EnableMasking && IsMasked) + { + + ●●●●●●●● + + } + else + { + + + + } + @if (EnableMasking) + { + + } + + @PreCopyText +
diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs new file mode 100644 index 0000000000..b1134042b5 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components; +using Microsoft.Fast.Components.FluentUI; +using Microsoft.JSInterop; + +namespace Aspire.Dashboard.Components.Controls; + +public partial class GridValue +{ + [Parameter, EditorRequired] + public string? Value { get; set; } + + /// + /// Determines whether or not masking support is enabled for this value + /// + [Parameter] + public bool EnableMasking { get; set; } + + /// + /// Determines whether or not the value should currently be masked + /// + [Parameter] + public bool IsMasked { get; set; } + + /// + /// The text to highlight within the value when the value is displayed unmasked + /// + [Parameter] + public string? HighlightText { get; set; } + + [Parameter] + public EventCallback IsMaskedChanged { get; set; } + + private const string PreCopyText = "Copy to clipboard"; + private const string PostCopyText = "Copied!"; + + 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 string GetContainerClass() + => EnableMasking ? "container masking-enabled" : "container"; + + private async Task ToggleMaskStateAsync() + => await IsMaskedChanged.InvokeAsync(!IsMasked); + + private async Task CopyTextToClipboardAsync(string? text, string id) + => await JS.InvokeVoidAsync("copyTextToClipboard", id, text, PreCopyText, PostCopyText); +} diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css new file mode 100644 index 0000000000..ebe277e5ce --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css @@ -0,0 +1,22 @@ +.container { + display: grid; + grid-template-columns: 1fr auto; + gap: calc((6 + (var(--design-unit) * var(--density))) * 1px); +} + +.masking-enabled { + grid-template-columns: 1fr auto auto; +} + +::deep .cellText { + text-overflow: ellipsis; + overflow: hidden; +} + +::deep .defaultHidden { + visibility: hidden; +} + +::deep:hover .defaultHidden { + visibility: visible; +} diff --git a/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor new file mode 100644 index 0000000000..9db5eb3d0f --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor @@ -0,0 +1,17 @@ +@typeparam TItem + + + + + + + + + + diff --git a/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor.cs b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor.cs new file mode 100644 index 0000000000..b834729a0d --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components; +using Microsoft.Fast.Components.FluentUI; + +namespace Aspire.Dashboard.Components.Controls; + +public partial class PropertyGrid +{ + [Parameter, EditorRequired] + public IQueryable? Items { get; set; } + + [Parameter] + public string GridTemplateColumns { get; set; } = "1fr 1fr"; + + [Parameter] + public string NameColumnTitle { get; set; } = "Name"; + + [Parameter] + public string ValueColumnTitle { get; set; } = "Value"; + + [Parameter] + public GridSort? NameSort { get; set; } + + [Parameter] + public GridSort? ValueSort { get; set; } + + [Parameter] + public bool IsNameSortable { get; set; } = true; + + [Parameter] + public bool IsValueSortable { get; set; } = true; + + [Parameter] + public bool EnableValueMasking { get; set; } + + [Parameter] + public Func NameColumnValue { get; set; } = item => item?.ToString(); + + [Parameter] + public Func ValueColumnValue { get; set; } = item => item?.ToString(); + + [Parameter] + public Func GetIsItemMasked { get; set; } = item => false; + + [Parameter] + public Action SetIsItemMasked { get; set; } = (item, newValue) => { }; + + [Parameter] + public string? HighlightText { get; set; } + + [Parameter] + public EventCallback IsMaskedChanged { get; set; } + + public readonly record struct PropertyGridIsMaskedChangedArgs(TItem Item, bool NewValue); + + private async Task OnIsMaskedChanged(TItem item, bool newValue) + { + SetIsItemMasked(item, newValue); + await IsMaskedChanged.InvokeAsync(item); + } +} diff --git a/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor.css b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor b/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor index 3d316ec1ec..9336fb7657 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor @@ -1,90 +1,46 @@ @using Aspire.Dashboard.Model @implements IDialogContentComponent -@inject IJSRuntime JS - -
+
+ @if (Content?.ShowSpecOnlyToggle == true) { + IconEnd="@(_showAll ? _showSpecOnlyIcon : _showAllIcon)" + Title="@(_showAll ? "Show Spec Only" : "Show All")" + aria-label="@(_showAll ? "Show Spec Only" : "Show All")" + OnClick="() => _showAll = !_showAll" + slot="end" /> } + IconEnd="@(_defaultMasked ? _unmaskIcon : _maskIcon)" + Title="@(_defaultMasked ? "Show Values" : "Hide Values")" + aria-label="@(_defaultMasked ? "Show Values" : "Hide Values")" + OnClick="ToggleMaskState" + slot="end" /> + Immediate="true" + Autofocus="true" + @bind-Value="_filter" + @oninput="HandleFilter" + AfterBindValue="HandleClear" + slot="end" /> - - - - - - - @{ - var anchor = "name-" + context.Name; - } - - @PreCopyText - - - - - @if (context.IsValueMasked) - { - - ●●●●●●●● - - } - else - { - - - - } - - @{ - var anchor = "copy-" + context.Name; - } - - @PreCopyText - - - +
+ +
- -
- + +
diff --git a/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.cs index 60ba1a44c4..cf50dcbc50 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.cs @@ -4,7 +4,6 @@ using Aspire.Dashboard.Model; using Microsoft.AspNetCore.Components; using Microsoft.Fast.Components.FluentUI; -using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Dialogs; public partial class EnvironmentVariables @@ -22,9 +21,6 @@ public partial class EnvironmentVariables vm.Value?.Contains(_filter, StringComparison.CurrentCultureIgnoreCase) == true) )?.AsQueryable(); - private const string PreCopyText = "Copy to clipboard"; - private const string PostCopyText = "Copied!"; - private string _filter = ""; private bool _defaultMasked = true; @@ -48,12 +44,6 @@ private void ToggleMaskState() } } - private void ToggleMaskState(EnvironmentVariableViewModel vm) - { - vm.IsValueMasked = !vm.IsValueMasked; - CheckAllMaskStates(); - } - private void HandleFilter(ChangeEventArgs args) { if (args.Value is string newFilter) @@ -89,9 +79,4 @@ private void CheckAllMaskStates() } } } - - private async Task CopyTextToClipboardAsync(string? text, string id) - { - await JS.InvokeVoidAsync("copyTextToClipboard", id, text, PreCopyText, PostCopyText); - } } diff --git a/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.css b/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.css deleted file mode 100644 index 579d8f4103..0000000000 --- a/src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor.css +++ /dev/null @@ -1,35 +0,0 @@ -.GridContainer { - width: auto; - height: auto; - max-width: 1000px; - max-height: 500px; - min-width: 650px; - overflow-x: auto; - overflow-y: auto; -} - -::deep fluent-toolbar { - width: 100%; -} - -::deep .defaultHidden { - visibility: hidden; -} - -::deep .valueColumn:hover .defaultHidden, -::deep .nameColumn:hover .defaultHidden { - visibility: visible; -} - -::deep .nameColumn .cellText { - min-width: 90%; - text-overflow: ellipsis; - overflow: hidden; -} - - -::deep .valueColumn .cellText { - min-width: 80%; - text-overflow: ellipsis; - overflow: hidden; -} diff --git a/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor index 666b79a714..180286ccab 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor @@ -1,58 +1,27 @@ @using Aspire.Dashboard.Model @using Microsoft.Fast.Components.FluentUI; @implements IDialogContentComponent> -@inject IJSRuntime JS -
- - - - - - - - - - - @{ - var anchor = "name-" + context.Name; - } - - @PreCopyText - - - - - - - - @{ - var anchor = "copy-" + context.Name; - } - - @PreCopyText - - - - -
+ + + + +
+ +
+
diff --git a/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.cs index 7719712e06..58f0a5b732 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.cs @@ -4,7 +4,6 @@ using Aspire.Dashboard.Model; using Microsoft.AspNetCore.Components; using Microsoft.Fast.Components.FluentUI; -using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Dialogs; public partial class LogDetailsDialog @@ -18,9 +17,6 @@ public partial class LogDetailsDialog vm.Value?.Contains(_filter, StringComparison.CurrentCultureIgnoreCase) == true )?.AsQueryable(); - private const string PreCopyText = "Copy to clipboard"; - private const string PostCopyText = "Copied!"; - private string _filter = ""; private readonly GridSort _nameSort = GridSort.ByAscending(vm => vm.Name); @@ -38,9 +34,4 @@ private void HandleClear(string? value) { _filter = value ?? string.Empty; } - - private async Task CopyTextToClipboardAsync(string? text, string id) - { - await JS.InvokeVoidAsync("copyTextToClipboard", id, text, PreCopyText, PostCopyText); - } -} \ No newline at end of file +} diff --git a/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.css b/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.css deleted file mode 100644 index fe32488d7b..0000000000 --- a/src/Aspire.Dashboard/Components/Dialogs/LogDetailsDialog.razor.css +++ /dev/null @@ -1,35 +0,0 @@ -.GridContainer { - width: auto; - height: auto; - max-width: 1000px; - max-height: 500px; - min-width: 800px; - overflow-x: auto; - overflow-y: auto; -} - -::deep fluent-toolbar { - width: 100%; -} - -::deep .defaultHidden { - visibility: hidden; -} - -::deep .valueColumn:hover .defaultHidden, -::deep .nameColumn:hover .defaultHidden { - visibility: visible; -} - -::deep .nameColumn .cellText { - width: 95%; - text-overflow: ellipsis; - overflow: hidden; -} - - -::deep .valueColumn .cellText { - width: 95%; - text-overflow: ellipsis; - overflow: hidden; -} diff --git a/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor index d21d781c91..85ae374d42 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor @@ -2,10 +2,9 @@ @using Aspire.Dashboard.Otlp.Model @using Microsoft.Fast.Components.FluentUI; @implements IDialogContentComponent -@inject IJSRuntime JS - -
+
+
@@ -29,85 +28,25 @@ AfterBindValue="HandleClear" slot="end" /> - - - - - - - @{ - var anchor = "name-" + context.Name; - } - - @PreCopyText - - - - - - - - @{ - var anchor = "copy-" + context.Name; - } - - @PreCopyText - - - +
+

Application

- - - - - - - @{ - var anchor = "name-" + context.Name; - } - - @PreCopyText - - - - - - - - @{ - var anchor = "copy-" + context.Name; - } - - @PreCopyText - - - - -
+ +
+
+
diff --git a/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.cs index c06e642a26..ddc3908586 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.cs @@ -4,7 +4,6 @@ using Aspire.Dashboard.Model; using Microsoft.AspNetCore.Components; using Microsoft.Fast.Components.FluentUI; -using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Dialogs; public partial class SpanDetailsDialog @@ -25,9 +24,6 @@ public partial class SpanDetailsDialog vm.Value?.Contains(_filter, StringComparison.CurrentCultureIgnoreCase) == true )?.AsQueryable(); - private const string PreCopyText = "Copy to clipboard"; - private const string PostCopyText = "Copied!"; - private string _filter = ""; private readonly GridSort _nameSort = GridSort.ByAscending(vm => vm.Name); @@ -45,9 +41,4 @@ private void HandleClear(string? value) { _filter = value ?? string.Empty; } - - private async Task CopyTextToClipboardAsync(string? text, string id) - { - await JS.InvokeVoidAsync("copyTextToClipboard", id, text, PreCopyText, PostCopyText); - } -} \ No newline at end of file +} diff --git a/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.css b/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.css deleted file mode 100644 index 58dad3386a..0000000000 --- a/src/Aspire.Dashboard/Components/Dialogs/SpanDetailsDialog.razor.css +++ /dev/null @@ -1,26 +0,0 @@ -.GridContainer { - width: auto; - height: auto; - max-width: 1000px; - max-height: 500px; - min-width: 800px; - overflow-x: auto; - overflow-y: auto; -} - -::deep fluent-toolbar { - width: 100%; -} - -::deep .defaultHidden { - visibility: hidden; -} - -::deep .valueColumn:hover .defaultHidden, -::deep .nameColumn:hover .defaultHidden { - visibility: visible; -} - -.cellText { - flex-grow: 1; -} diff --git a/src/Aspire.Dashboard/Components/_Imports.razor b/src/Aspire.Dashboard/Components/_Imports.razor index d069a5c3d0..a9945c0bb0 100644 --- a/src/Aspire.Dashboard/Components/_Imports.razor +++ b/src/Aspire.Dashboard/Components/_Imports.razor @@ -7,4 +7,5 @@ @using Microsoft.JSInterop @using Aspire.Dashboard @using Aspire.Dashboard.Components +@using Aspire.Dashboard.Components.Controls @using Microsoft.Fast.Components.FluentUI diff --git a/src/Aspire.Dashboard/wwwroot/css/app.css b/src/Aspire.Dashboard/wwwroot/css/app.css index 40d625d494..f1ceb2e50f 100644 --- a/src/Aspire.Dashboard/wwwroot/css/app.css +++ b/src/Aspire.Dashboard/wwwroot/css/app.css @@ -61,9 +61,16 @@ fluent-dialog::part(control) { background: var(--neutral-layer-floating); } +fluent-dialog fluent-toolbar { + background-color: var(--neutral-layer-floating); +} + /* Changing the dialog's fill color means stealth buttons on the dialog have the wrong background, so change it to match */ -fluent-dialog fluent-button[appearance=stealth]:not(:hover)::part(control) { +fluent-dialog fluent-button[appearance=stealth]:not(:hover)::part(control), +fluent-dialog fluent-button[appearance=lightweight]:not(:hover)::part(control), +fluent-dialog fluent-anchor[appearance=stealth]:not(:hover)::part(control), +fluent-dialog fluent-anchor[appearance=lightweight]:not(:hover)::part(control) { background: var(--neutral-layer-floating); } @@ -72,3 +79,10 @@ fluent-dialog fluent-button[appearance=stealth]:not(:hover)::part(control) { fluent-dialog fluent-data-grid-cell[cell-type=columnheader] fluent-button[appearance=stealth]:not(:hover)::part(control) { background: var(--fill-layer); } + +.dialog-grid-container { + width: 1000px; + height: 500px; + overflow-x: auto; + overflow-y: auto; +}