Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@using Aspire.Dashboard.Components.Controls.Grid
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@using Humanizer
Expand Down Expand Up @@ -99,6 +100,54 @@
</PropertyGrid>
</FluentAccordionItem>
}
<FluentAccordionItem Heading="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsReferences)]" Expanded="@_isRelationshipsExpanded">
<div slot="end">
<FluentBadge Appearance="Appearance.Neutral" Circular="true">
@FilteredRelationships.Count()
</FluentBadge>
</div>
<FluentDataGrid TGridItem="ResourceDetailRelationship"
Items="@FilteredRelationships"
ItemKey="r => r.ResourceName"
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="1fr 1fr 0.5fr"
ShowHover="true">
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsResourceHeader)]">
@context.ResourceName
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsTypeHeader)]" TooltipText="@(c => string.Join(", ", c.Types))">
@string.Join(", ", context.Types)
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]" Class="no-ellipsis">
<FluentButton Appearance="Appearance.Lightweight" OnClick="@(() => OnViewRelationshipAsync(context))">@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
<FluentAccordionItem Heading="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsBackReferences)]" Expanded="@_isBackRelationshipsExpanded">
<div slot="end">
<FluentBadge Appearance="Appearance.Neutral" Circular="true">
@FilteredBackRelationships.Count()
</FluentBadge>
</div>
<FluentDataGrid TGridItem="ResourceDetailRelationship"
Items="@FilteredBackRelationships"
ItemKey="r => r.ResourceName"
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="1fr 1fr 0.5fr"
ShowHover="true">
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsResourceHeader)]">
@context.ResourceName
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.ResourceDetailsTypeHeader)]" TooltipText="@(c => string.Join(", ", c.Types))">
@string.Join(", ", context.Types)
</TemplateColumn>
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]" Class="no-ellipsis">
<FluentButton Appearance="Appearance.Lightweight" OnClick="@(() => OnViewRelationshipAsync(context))">@ControlStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
<FluentAccordionItem Heading="@ControlStringsLoc[nameof(ControlsStrings.ResourceHealthChecksHeader)]" Expanded="@_isHealthChecksExpanded">
<div slot="end">
<FluentBadge Appearance="Appearance.Neutral" Circular="true">
Expand Down
103 changes: 103 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
Expand All @@ -14,9 +16,15 @@ public partial class ResourceDetails
[Parameter, EditorRequired]
public required ResourceViewModel Resource { get; set; }

[Parameter]
public required ConcurrentDictionary<string, ResourceViewModel> ResourceByName { get; set; }

[Parameter]
public bool ShowSpecOnlyToggle { get; set; }

[Inject]
public required NavigationManager NavigationManager { get; init; }

private bool IsSpecOnlyToggleDisabled => !Resource.Environment.All(i => !i.FromSpec) && !GetResourceProperties(ordered: false).Any(static vm => vm.KnownProperty is null);

// NOTE Excludes endpoints as they don't expose sensitive items (and enumerating endpoints is non-trivial)
Expand All @@ -36,6 +44,16 @@ public partial class ResourceDetails
.Where(vm => vm.MatchesFilter(_filter))
.AsQueryable();

internal IQueryable<ResourceDetailRelationship> FilteredRelationships =>
GetRelationships()
.Where(vm => vm.MatchesFilter(_filter))
.AsQueryable();

internal IQueryable<ResourceDetailRelationship> FilteredBackRelationships =>
GetBackRelationships()
.Where(vm => vm.MatchesFilter(_filter))
.AsQueryable();

internal IQueryable<VolumeViewModel> FilteredVolumes =>
Resource.Volumes
.Where(vm => vm.MatchesFilter(_filter))
Expand All @@ -55,6 +73,8 @@ public partial class ResourceDetails
private bool _isEnvironmentVariablesExpanded;
private bool _isEndpointsExpanded;
private bool _isHealthChecksExpanded;
private bool _isRelationshipsExpanded;
private bool _isBackRelationshipsExpanded;

private string _filter = "";
private bool? _isMaskAllChecked;
Expand Down Expand Up @@ -85,6 +105,8 @@ protected override void OnParametersSet()
_isEnvironmentVariablesExpanded = _resource.Environment.Any();
_isVolumesExpanded = _resource.Volumes.Any();
_isHealthChecksExpanded = _resource.HealthReports.Any() || _resource.HealthStatus is null; // null means we're waiting for health reports
_isRelationshipsExpanded = GetRelationships().Any();
_isBackRelationshipsExpanded = GetBackRelationships().Any();

foreach (var item in SensitiveGridItems)
{
Expand All @@ -100,6 +122,68 @@ protected override void OnParametersSet()
}
}

private IEnumerable<ResourceDetailRelationship> GetRelationships()
{
if (ResourceByName == null)
{
return [];
}

var items = new List<ResourceDetailRelationship>();

foreach (var resourceRelationships in Resource.Relationships.GroupBy(r => r.ResourceName, StringComparers.ResourceName))
{
var matches = ResourceByName.Values
.Where(r => string.Equals(r.DisplayName, resourceRelationships.Key, StringComparisons.ResourceName))
.Where(r => r.KnownState != KnownResourceState.Hidden)
.ToList();

foreach (var match in matches)
{
items.Add(new()
{
Resource = match,
ResourceName = ResourceViewModel.GetResourceName(match, ResourceByName),
Types = resourceRelationships.Select(r => r.Type).OrderBy(r => r).ToList()
});
}
}

return items.OrderBy(r => r.ResourceName, StringComparers.ResourceName);
}

private IEnumerable<ResourceDetailRelationship> GetBackRelationships()
{
if (ResourceByName == null)
{
return [];
}

var items = new List<ResourceDetailRelationship>();

var otherResources = ResourceByName.Values
.Where(r => r != Resource)
.Where(r => r.KnownState != KnownResourceState.Hidden);

foreach (var otherResource in otherResources)
{
foreach (var resourceRelationships in otherResource.Relationships.GroupBy(r => r.ResourceName, StringComparers.ResourceName))
{
if (string.Equals(resourceRelationships.Key, Resource.DisplayName, StringComparisons.ResourceName))
{
items.Add(new()
{
Resource = otherResource,
ResourceName = ResourceViewModel.GetResourceName(otherResource, ResourceByName),
Types = resourceRelationships.Select(r => r.Type).OrderBy(r => r).ToList()
});
}
}
}

return items.OrderBy(r => r.ResourceName, StringComparers.ResourceName);
}

private List<DisplayedEndpoint> GetEndpoints()
{
return ResourceEndpointHelpers.GetEndpoints(Resource, includeInternalUrls: true);
Expand Down Expand Up @@ -150,4 +234,23 @@ private void OnValueMaskedChanged(IPropertyGridItem vm)
}
}
}

public Task OnViewRelationshipAsync(ResourceDetailRelationship relationship)
{
NavigationManager.NavigateTo(DashboardUrls.ResourcesUrl(resource: relationship.Resource.Name));
return Task.CompletedTask;
}
}

public sealed class ResourceDetailRelationship
{
public required ResourceViewModel Resource { get; init; }
public required string ResourceName { get; init; }
public required List<string> Types { get; set; }

public bool MatchesFilter(string filter)
{
return Resource.DisplayName.Contains(filter, StringComparison.CurrentCultureIgnoreCase) ||
Types.Any(t => t.Contains(filter, StringComparison.CurrentCultureIgnoreCase));
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
</GridColumnManager>
</Summary>
<Details>
<ResourceDetails Resource="context" ShowSpecOnlyToggle="true" />
<ResourceDetails Resource="context" ResourceByName="_resourceByName" ShowSpecOnlyToggle="true" />
</Details>
</SummaryDetailsView>
</MainSection>
Expand Down
38 changes: 37 additions & 1 deletion src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ public partial class Resources : ComponentBase, IAsyncDisposable
[SupplyParameterFromQuery]
public string? VisibleTypes { get; set; }

[Parameter]
[SupplyParameterFromQuery(Name = "resource")]
public string? ResourceName { get; set; }

private ResourceViewModel? SelectedResource { get; set; }

private readonly CancellationTokenSource _watchTaskCancellationTokenSource = new();
private readonly ConcurrentDictionary<string, ResourceViewModel> _resourceByName = new(StringComparers.ResourceName);
private readonly ConcurrentDictionary<string, bool> _allResourceTypes = [];
private readonly ConcurrentDictionary<string, bool> _visibleResourceTypes = new(StringComparers.ResourceName);
private readonly List<string> _expandedResourceNames = [];
private readonly HashSet<string> _expandedResourceNames = [];
private string _filter = "";
private bool _isTypeFilterVisible;
private Task? _resourceSubscriptionTask;
Expand Down Expand Up @@ -282,6 +286,20 @@ private void UpdateMaxHighlightedCount()
_maxHighlightedCount = Math.Min(maxHighlightedCount, 2);
}

protected override async Task OnParametersSetAsync()
{
if (ResourceName is not null)
{
if (_resourceByName.TryGetValue(ResourceName, out var selectedResource))
{
await ShowResourceDetailsAsync(selectedResource, buttonId: null);
}

// Navigate to remove ?resource=xxx in the URL.
NavigationManager.NavigateTo(DashboardUrls.ResourcesUrl(), new NavigationOptions { ReplaceHistoryEntry = true });
}
}

private bool ApplicationErrorCountsChanged(Dictionary<ApplicationKey, int> newApplicationUnviewedErrorCounts)
{
if (_applicationUnviewedErrorCounts == null || _applicationUnviewedErrorCounts.Count != newApplicationUnviewedErrorCounts.Count)
Expand Down Expand Up @@ -311,6 +329,24 @@ private async Task ShowResourceDetailsAsync(ResourceViewModel resource, string?
else
{
SelectedResource = resource;

// Ensure that the selected resource is visible in the grid. All parents must be expanded.
var current = resource;
while (current != null)
{
if (current.GetResourcePropertyValue(KnownProperties.Resource.ParentName) is { Length: > 0 } value)
{
if (_resourceByName.TryGetValue(value, out current))
{
_expandedResourceNames.Add(value);
continue;
}
}

break;
}

await _dataGrid.SafeRefreshDataAsync();
}
}

Expand Down
4 changes: 0 additions & 4 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,3 @@
height: 24px;
display: inline-block;
}

::deep .resource-name-text {
vertical-align: middle;
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public partial class TraceDetail : ComponentBase, IDisposable

[Parameter]
[SupplyParameterFromQuery]
public required string? SpanId { get; set; }
public string? SpanId { get; set; }

[Inject]
public required TelemetryRepository TelemetryRepository { get; init; }
Expand Down
17 changes: 17 additions & 0 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public sealed class ResourceViewModel
public required ImmutableArray<EnvironmentVariableViewModel> Environment { get; init; }
public required ImmutableArray<UrlViewModel> Urls { get; init; }
public required ImmutableArray<VolumeViewModel> Volumes { get; init; }
public required ImmutableArray<RelationshipViewModel> Relationships { get; init; }
public required ImmutableDictionary<string, ResourcePropertyViewModel> Properties { get; init; }
public required ImmutableArray<CommandViewModel> Commands { get; init; }
/// <summary>The health status of the resource. <see langword="null"/> indicates that health status is expected but not yet available.</summary>
Expand Down Expand Up @@ -380,3 +381,19 @@ public bool MatchesFilter(string filter)
_humanizedHealthStatus?.Contains(filter, StringComparison.OrdinalIgnoreCase) is true;
}
}

[DebuggerDisplay("ResourceName = {ResourceName}, Type = {Type}")]
public sealed class RelationshipViewModel
{
public string ResourceName { get; }
public string Type { get; }

public RelationshipViewModel(string resourceName, string type)
{
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
ArgumentException.ThrowIfNullOrWhiteSpace(type);

ResourceName = resourceName;
Type = type;
}
}
8 changes: 8 additions & 0 deletions src/Aspire.Dashboard/ResourceService/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public ResourceViewModel ToViewModel(BrowserTimeProvider timeProvider, IKnownPro
Environment = GetEnvironment(),
Urls = GetUrls(),
Volumes = GetVolumes(),
Relationships = GetRelationships(),
State = HasState ? State : null,
KnownState = HasState ? Enum.TryParse(State, out KnownResourceState knownState) ? knownState : null : null,
StateStyle = HasStateStyle ? StateStyle : null,
Expand Down Expand Up @@ -79,6 +80,13 @@ ImmutableArray<EnvironmentVariableViewModel> GetEnvironment()
.ToImmutableArray();
}

ImmutableArray<RelationshipViewModel> GetRelationships()
{
return Relationships
.Select(r => new RelationshipViewModel(r.ResourceName, r.Type))
.ToImmutableArray();
}

ImmutableArray<UrlViewModel> GetUrls()
{
// Filter out bad urls
Expand Down
Loading