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)
+ {
+
+ }
+ CopyTextToClipboardAsync(Value, @_anchorId))"
+ aria-label="Copy to Clipboard" />
+ @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;
- }
- CopyTextToClipboardAsync(context.Name, @anchor))" />
- @PreCopyText
-
-
-
-
- @if (context.IsValueMasked)
- {
-
- ●●●●●●●●
-
- }
- else
- {
-
-
-
- }
-
- @{
- var anchor = "copy-" + context.Name;
- }
- CopyTextToClipboardAsync(context.Value, @anchor))"
- aria-label="Copy to Clipboard" />
- @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;
+}