diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor index de916bdc3c0..8d390ce1e49 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor +++ b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor @@ -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 @@ -99,6 +100,54 @@ } + +
+ + @FilteredRelationships.Count() + +
+ + + @context.ResourceName + + + @string.Join(", ", context.Types) + + + @ControlStringsLoc[nameof(ControlsStrings.ViewAction)] + + +
+ +
+ + @FilteredBackRelationships.Count() + +
+ + + @context.ResourceName + + + @string.Join(", ", context.Types) + + + @ControlStringsLoc[nameof(ControlsStrings.ViewAction)] + + +
diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs index f3452aad6d1..6e64734fc7f 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs @@ -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; @@ -14,9 +16,15 @@ public partial class ResourceDetails [Parameter, EditorRequired] public required ResourceViewModel Resource { get; set; } + [Parameter] + public required ConcurrentDictionary 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) @@ -36,6 +44,16 @@ public partial class ResourceDetails .Where(vm => vm.MatchesFilter(_filter)) .AsQueryable(); + internal IQueryable FilteredRelationships => + GetRelationships() + .Where(vm => vm.MatchesFilter(_filter)) + .AsQueryable(); + + internal IQueryable FilteredBackRelationships => + GetBackRelationships() + .Where(vm => vm.MatchesFilter(_filter)) + .AsQueryable(); + internal IQueryable FilteredVolumes => Resource.Volumes .Where(vm => vm.MatchesFilter(_filter)) @@ -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; @@ -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) { @@ -100,6 +122,68 @@ protected override void OnParametersSet() } } + private IEnumerable GetRelationships() + { + if (ResourceByName == null) + { + return []; + } + + var items = new List(); + + 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 GetBackRelationships() + { + if (ResourceByName == null) + { + return []; + } + + var items = new List(); + + 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 GetEndpoints() { return ResourceEndpointHelpers.GetEndpoints(Resource, includeInternalUrls: true); @@ -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 Types { get; set; } + + public bool MatchesFilter(string filter) + { + return Resource.DisplayName.Contains(filter, StringComparison.CurrentCultureIgnoreCase) || + Types.Any(t => t.Contains(filter, StringComparison.CurrentCultureIgnoreCase)); + } } diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index 0f72ae046bb..9458df282e6 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -147,7 +147,7 @@
- +
diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 987c5790b2b..d35461e8b75 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -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 _resourceByName = new(StringComparers.ResourceName); private readonly ConcurrentDictionary _allResourceTypes = []; private readonly ConcurrentDictionary _visibleResourceTypes = new(StringComparers.ResourceName); - private readonly List _expandedResourceNames = []; + private readonly HashSet _expandedResourceNames = []; private string _filter = ""; private bool _isTypeFilterVisible; private Task? _resourceSubscriptionTask; @@ -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 newApplicationUnviewedErrorCounts) { if (_applicationUnviewedErrorCounts == null || _applicationUnviewedErrorCounts.Count != newApplicationUnviewedErrorCounts.Count) @@ -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(); } } diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.css b/src/Aspire.Dashboard/Components/Pages/Resources.razor.css index d2be3aaf3c1..a08ad89fbec 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.css +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.css @@ -37,7 +37,3 @@ height: 24px; display: inline-block; } - -::deep .resource-name-text { - vertical-align: middle; -} diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs index 6cff2989bc3..61515708603 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs @@ -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; } diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 8cf1524381b..c99bcccf3c3 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -34,6 +34,7 @@ public sealed class ResourceViewModel public required ImmutableArray Environment { get; init; } public required ImmutableArray Urls { get; init; } public required ImmutableArray Volumes { get; init; } + public required ImmutableArray Relationships { get; init; } public required ImmutableDictionary Properties { get; init; } public required ImmutableArray Commands { get; init; } /// The health status of the resource. indicates that health status is expected but not yet available. @@ -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; + } +} diff --git a/src/Aspire.Dashboard/ResourceService/Partials.cs b/src/Aspire.Dashboard/ResourceService/Partials.cs index efcfee12e2b..bf8593cfa09 100644 --- a/src/Aspire.Dashboard/ResourceService/Partials.cs +++ b/src/Aspire.Dashboard/ResourceService/Partials.cs @@ -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, @@ -79,6 +80,13 @@ ImmutableArray GetEnvironment() .ToImmutableArray(); } + ImmutableArray GetRelationships() + { + return Relationships + .Select(r => new RelationshipViewModel(r.ResourceName, r.Type)) + .ToImmutableArray(); + } + ImmutableArray GetUrls() { // Filter out bad urls diff --git a/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs b/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs index 255ac2b3012..040eee935d3 100644 --- a/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs +++ b/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs @@ -564,6 +564,15 @@ public static string PropertyGridValueColumnHeader { } } + /// + /// Looks up a localized string similar to Back references. + /// + public static string ResourceDetailsBackReferences { + get { + return ResourceManager.GetString("ResourceDetailsBackReferences", resourceCulture); + } + } + /// /// Looks up a localized string similar to Endpoints. /// @@ -582,6 +591,15 @@ public static string ResourceDetailsEnvironmentVariablesHeader { } } + /// + /// Looks up a localized string similar to References. + /// + public static string ResourceDetailsReferences { + get { + return ResourceManager.GetString("ResourceDetailsReferences", resourceCulture); + } + } + /// /// Looks up a localized string similar to Resource. /// @@ -591,6 +609,15 @@ public static string ResourceDetailsResourceHeader { } } + /// + /// Looks up a localized string similar to Type. + /// + public static string ResourceDetailsTypeHeader { + get { + return ResourceManager.GetString("ResourceDetailsTypeHeader", resourceCulture); + } + } + /// /// Looks up a localized string similar to Volumes. /// diff --git a/src/Aspire.Dashboard/Resources/ControlsStrings.resx b/src/Aspire.Dashboard/Resources/ControlsStrings.resx index f84bef4fe6b..2d12f06f5d5 100644 --- a/src/Aspire.Dashboard/Resources/ControlsStrings.resx +++ b/src/Aspire.Dashboard/Resources/ControlsStrings.resx @@ -413,4 +413,13 @@ Structured logs - + + Type + + + References + + + Back references + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf index 9d0475863b6..b2db5f20466 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf @@ -282,6 +282,11 @@ Hodnota + + Back references + Back references + + Endpoints Koncové body @@ -292,11 +297,21 @@ Proměnné prostředí + + References + References + + Resource Prostředek + + Type + Type + + Volumes Svazky diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf index 1a44fdd769a..41f20fbc32c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf @@ -282,6 +282,11 @@ Wert + + Back references + Back references + + Endpoints Endpunkte @@ -292,11 +297,21 @@ Umgebungsvariablen + + References + References + + Resource Ressource + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf index 876d8ba824b..accccf354d8 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf @@ -282,6 +282,11 @@ Valor + + Back references + Back references + + Endpoints Puntos de conexión @@ -292,11 +297,21 @@ Variables de entorno + + References + References + + Resource Recurso + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf index a01a7d08e1a..1131c0373aa 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf @@ -282,6 +282,11 @@ Valeur + + Back references + Back references + + Endpoints Points de terminaison @@ -292,11 +297,21 @@ Variables d’environnement + + References + References + + Resource Ressource + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf index 314e89a9a53..f68ae80ac2a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf @@ -282,6 +282,11 @@ Valore + + Back references + Back references + + Endpoints Endpoint @@ -292,11 +297,21 @@ Variabili di ambiente + + References + References + + Resource Risorsa + + Type + Type + + Volumes Volumi diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf index 2dfde10f034..32f68d2a780 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf @@ -282,6 +282,11 @@ + + Back references + Back references + + Endpoints エンドポイント @@ -292,11 +297,21 @@ 環境変数 + + References + References + + Resource リソース + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf index 5cc62dbd2a9..7becac43c98 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf @@ -282,6 +282,11 @@ + + Back references + Back references + + Endpoints 엔드포인트 @@ -292,11 +297,21 @@ 환경 변수 + + References + References + + Resource 리소스 + + Type + Type + + Volumes 볼륨 diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf index 04bac8cf13f..c7364922692 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf @@ -282,6 +282,11 @@ Wartość + + Back references + Back references + + Endpoints Punkty końcowe @@ -292,11 +297,21 @@ Zmienne środowiskowe + + References + References + + Resource Zasób + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf index c64c0fa7421..0fc28db3701 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf @@ -282,6 +282,11 @@ Valor + + Back references + Back references + + Endpoints Pontos de extremidade @@ -292,11 +297,21 @@ Variáveis de ambiente + + References + References + + Resource Recurso + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf index 9fc387f9f22..b32cc30347e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf @@ -282,6 +282,11 @@ Значение + + Back references + Back references + + Endpoints Конечные точки @@ -292,11 +297,21 @@ Переменные среды + + References + References + + Resource Ресурс + + Type + Type + + Volumes Тома diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf index 1c58cf46b6c..fc4c604826f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf @@ -282,6 +282,11 @@ Değer + + Back references + Back references + + Endpoints Uç Noktalar @@ -292,11 +297,21 @@ Ortam değişkenleri + + References + References + + Resource Kaynak + + Type + Type + + Volumes Birimler diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf index 19f8c0d9293..0a95586fabf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf @@ -282,6 +282,11 @@ + + Back references + Back references + + Endpoints 终结点 @@ -292,11 +297,21 @@ 环境变量 + + References + References + + Resource 资源 + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf index 0b994a4e426..4f4a317ad9a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf @@ -282,6 +282,11 @@ + + Back references + Back references + + Endpoints 端點 @@ -292,11 +297,21 @@ 環境變數 + + References + References + + Resource 資源 + + Type + Type + + Volumes Volumes diff --git a/src/Aspire.Dashboard/Utils/DashboardUrls.cs b/src/Aspire.Dashboard/Utils/DashboardUrls.cs index edc59671df0..2dcb629ba4e 100644 --- a/src/Aspire.Dashboard/Utils/DashboardUrls.cs +++ b/src/Aspire.Dashboard/Utils/DashboardUrls.cs @@ -13,9 +13,15 @@ internal static class DashboardUrls public const string StructuredLogsBasePath = "structuredlogs"; public const string TracesBasePath = "traces"; - public static string ResourcesUrl() + public static string ResourcesUrl(string? resource = null) { - return "/"; + var url = "/"; + if (resource != null) + { + url = QueryHelpers.AddQueryString(url, "resource", resource); + } + + return url; } public static string ConsoleLogsUrl(string? resource = null) diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index 1ebece067c4..d80d3dfea89 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -206,6 +206,8 @@ public static IResourceBuilder WithPgAdmin(this IResourceBuilder builde configureContainer?.Invoke(pgAdminContainerBuilder); + pgAdminContainerBuilder.WithRelationship(builder.Resource, "PgAdmin"); + return builder; } } @@ -267,7 +269,6 @@ public static IResourceBuilder WithHostPort(this IResour /// A reference to the . public static IResourceBuilder WithPgWeb(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) { - if (builder.ApplicationBuilder.Resources.OfType().SingleOrDefault() is { } existingPgWebResource) { var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingPgWebResource); @@ -290,6 +291,8 @@ public static IResourceBuilder WithPgWeb(this IResourceB configureContainer?.Invoke(pgwebContainerBuilder); + pgwebContainerBuilder.WithRelationship(builder.Resource, "PgWeb"); + builder.ApplicationBuilder.Eventing.Subscribe(async (e, ct) => { var adminResource = builder.ApplicationBuilder.Resources.OfType().Single(); diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index 7f7d1932007..ac6f0f48e18 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -125,6 +125,8 @@ public static IResourceBuilder WithRedisCommander(this IResourceB configureContainer?.Invoke(resourceBuilder); + resourceBuilder.WithRelationship(builder.Resource, "RedisCommander"); + return builder; } } diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index 31a60f2b928..164c9ec55cc 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using Aspire.Dashboard.Model; using Aspire.Hosting.Dcp.Model; using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus; @@ -105,6 +106,11 @@ internal init /// public ImmutableArray Commands { get; init; } = []; + /// + /// The relationships to other resources. + /// + public ImmutableArray Relationships { get; init; } = []; + internal static HealthStatus? ComputeHealthStatus(ImmutableArray healthReports, string? state) { if (state != KnownResourceStates.Running) @@ -162,6 +168,13 @@ public sealed record UrlSnapshot(string Name, string Url, bool IsInternal); /// Whether the volume mount is read-only or not. public sealed record VolumeSnapshot(string? Source, string Target, string MountType, bool IsReadOnly); +/// +/// A snapshot of a relationship. +/// +/// The name of the resource the relationship is to. +/// The relationship type. +public sealed record RelationshipSnapshot(string ResourceName, string Type); + /// /// A snapshot of the resource property. /// @@ -312,3 +325,23 @@ public static class KnownResourceStates /// public static readonly IReadOnlyList TerminalStates = [Finished, FailedToStart, Exited]; } + +internal static class ResourceSnapshotBuilder +{ + public static ImmutableArray BuildRelationships(IResource resource) + { + var relationships = ImmutableArray.CreateBuilder(); + + if (resource is IResourceWithParent resourceWithParent) + { + relationships.Add(new(resourceWithParent.Parent.Name, KnownRelationshipTypes.Parent)); + } + + foreach (var annotation in resource.Annotations.OfType()) + { + relationships.Add(new(annotation.Resource.Name, annotation.Type)); + } + + return relationships.ToImmutable(); + } +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs index 4402c6f4a8a..68e8db0c0bb 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs @@ -8,7 +8,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Represents a command annotation for a resource. /// -[DebuggerDisplay("Type = {GetType().Name,nq}, Type = {Type}")] +[DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")] public sealed class ResourceCommandAnnotation : IResourceAnnotation { /// diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs index 97aaac84e18..ccb2f85cb0e 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs @@ -491,7 +491,8 @@ private static CustomResourceSnapshot GetCurrentSnapshot(IResource resource, Res previousState ??= new CustomResourceSnapshot() { ResourceType = resource.GetType().Name, - Properties = [] + Properties = [], + Relationships = ResourceSnapshotBuilder.BuildRelationships(resource) }; } diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceRelationshipAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ResourceRelationshipAnnotation.cs new file mode 100644 index 00000000000..b2b501d4c6a --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ResourceRelationshipAnnotation.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// An annotation which represents the relationship between two resources. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}, Resource = {Resource.Name}, RelationshipType = {Type}")] +public sealed class ResourceRelationshipAnnotation(IResource resource, string type) : IResourceAnnotation +{ + /// + /// The resource that the relationship is to. + /// + public IResource Resource { get; } = resource; + + /// + /// The relationship type. + /// + public string Type { get; } = type; +} diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj index 96a447e5b15..a8656ac8608 100644 --- a/src/Aspire.Hosting/Aspire.Hosting.csproj +++ b/src/Aspire.Hosting/Aspire.Hosting.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs index 06871521ec3..a6c90fcbbf5 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs @@ -45,6 +45,7 @@ static GenericResourceSnapshot CreateResourceSnapshot(IResource resource, string Urls = snapshot.Urls, Volumes = snapshot.Volumes, Environment = snapshot.EnvironmentVariables, + Relationships = snapshot.Relationships, ExitCode = snapshot.ExitCode, State = snapshot.State?.Text, StateStyle = snapshot.State?.Style, diff --git a/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs b/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs index fcc55b0fd01..37e6c90b39a 100644 --- a/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs +++ b/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs @@ -25,6 +25,7 @@ internal abstract class ResourceSnapshot public required ImmutableArray Environment { get; init; } public required ImmutableArray Volumes { get; init; } public required ImmutableArray Urls { get; init; } + public required ImmutableArray Relationships { get; init; } public required ImmutableArray HealthReports { get; init; } public required ImmutableArray Commands { get; init; } diff --git a/src/Aspire.Hosting/Dashboard/proto/Partials.cs b/src/Aspire.Hosting/Dashboard/proto/Partials.cs index 9ea268b571f..6b067c8a416 100644 --- a/src/Aspire.Hosting/Dashboard/proto/Partials.cs +++ b/src/Aspire.Hosting/Dashboard/proto/Partials.cs @@ -45,6 +45,15 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot) resource.Urls.Add(new Url { Name = url.Name, FullUrl = url.Url, IsInternal = url.IsInternal }); } + foreach (var relationship in snapshot.Relationships) + { + resource.Relationships.Add(new ResourceRelationship + { + ResourceName = relationship.ResourceName, + Type = relationship.Type + }); + } + foreach (var property in snapshot.Properties) { resource.Properties.Add(new ResourceProperty { Name = property.Name, Value = property.Value, IsSensitive = property.IsSensitive }); diff --git a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto index fef720fc5f7..2010afe7428 100644 --- a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto @@ -162,6 +162,13 @@ enum HealthStatus { DEGRADED = 2; } +message ResourceRelationship { + // The name of the resource. + string resource_name = 1; + // The type of relationship. + string type = 2; +} + message ResourceProperty { // Name of the data item, e.g. "container.id", "executable.pid", "project.path", ... string name = 1; @@ -197,7 +204,7 @@ message Resource { // - Projects: process_id, project_path repeated ResourceProperty properties = 12; - // The list of urls that this resource exposes + // The list of urls that this resource exposes. repeated Url urls = 13; // The style of the state. This is used to determine the state icon. @@ -215,6 +222,9 @@ message Resource { optional google.protobuf.Timestamp started_at = 18; // The resource stop time. optional google.protobuf.Timestamp stopped_at = 19; + + // The list of relationships for this resource. + repeated ResourceRelationship relationships = 20; } //////////////////////////////////////////// diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index b5e97f4da7a..1b818e9d254 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -614,6 +614,13 @@ private CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSna var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env); var state = container.AppModelInitialState == KnownResourceStates.Hidden ? KnownResourceStates.Hidden : container.Status?.State; + var relationships = ImmutableArray.Empty; + if (container.AppModelResourceName is not null && + _applicationModel.TryGetValue(container.AppModelResourceName, out var appModelResource)) + { + relationships = ResourceSnapshotBuilder.BuildRelationships(appModelResource); + } + return previous with { ResourceType = KnownResourceTypes.Container, @@ -633,7 +640,8 @@ private CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSna StartTimeStamp = container.Status?.StartupTimestamp?.ToUniversalTime(), StopTimeStamp = container.Status?.FinishTimestamp?.ToUniversalTime(), Urls = urls, - Volumes = volumes + Volumes = volumes, + Relationships = relationships }; ImmutableArray GetPorts() @@ -663,9 +671,10 @@ ContainerLifetime GetContainerLifetime() private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSnapshot previous) { string? projectPath = null; + IResource? appModelResource = null; if (executable.AppModelResourceName is not null && - _applicationModel.TryGetValue(executable.AppModelResourceName, out var appModelResource)) + _applicationModel.TryGetValue(executable.AppModelResourceName, out appModelResource)) { projectPath = appModelResource is ProjectResource p ? p.GetProjectMetadata().ProjectPath : null; } @@ -676,6 +685,12 @@ private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceS var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env); + var relationships = ImmutableArray.Empty; + if (appModelResource != null) + { + relationships = ResourceSnapshotBuilder.BuildRelationships(appModelResource); + } + if (projectPath is not null) { return previous with @@ -694,7 +709,8 @@ private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceS CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(), StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(), StopTimeStamp = executable.Status?.FinishTimestamp?.ToUniversalTime(), - Urls = urls + Urls = urls, + Relationships = relationships }; } @@ -713,7 +729,8 @@ private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceS CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(), StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(), StopTimeStamp = executable.Status?.FinishTimestamp?.ToUniversalTime(), - Urls = urls + Urls = urls, + Relationships = relationships }; } diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index 8a57d064b74..5ff55e84ce6 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable Aspire.Hosting.ApplicationModel.ContainerLifetime.Session = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetime Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? +Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Relationships.get -> System.Collections.Immutable.ImmutableArray +Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Relationships.init -> void Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildArguments.get -> System.Collections.Generic.Dictionary! Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildSecrets.get -> System.Collections.Generic.Dictionary! @@ -84,6 +86,12 @@ Aspire.Hosting.ApplicationModel.HealthCheckAnnotation Aspire.Hosting.ApplicationModel.HealthCheckAnnotation.HealthCheckAnnotation(string! key) -> void Aspire.Hosting.ApplicationModel.HealthCheckAnnotation.Key.get -> string! Aspire.Hosting.ApplicationModel.IResourceWithWaitSupport +Aspire.Hosting.ApplicationModel.RelationshipSnapshot +Aspire.Hosting.ApplicationModel.RelationshipSnapshot.RelationshipSnapshot(string! ResourceName, string! Type) -> void +Aspire.Hosting.ApplicationModel.RelationshipSnapshot.ResourceName.get -> string! +Aspire.Hosting.ApplicationModel.RelationshipSnapshot.ResourceName.init -> void +Aspire.Hosting.ApplicationModel.RelationshipSnapshot.Type.get -> string! +Aspire.Hosting.ApplicationModel.RelationshipSnapshot.Type.init -> void Aspire.Hosting.ApplicationModel.ResourceCommandAnnotation.ConfirmationMessage.get -> string? Aspire.Hosting.ApplicationModel.ResourceCommandAnnotation.DisplayDescription.get -> string? Aspire.Hosting.ApplicationModel.ResourceCommandAnnotation.Name.get -> string! @@ -136,6 +144,10 @@ Aspire.Hosting.ApplicationModel.ResourceReadyEvent Aspire.Hosting.ApplicationModel.ResourceReadyEvent.Resource.get -> Aspire.Hosting.ApplicationModel.IResource! Aspire.Hosting.ApplicationModel.ResourceReadyEvent.ResourceReadyEvent(Aspire.Hosting.ApplicationModel.IResource! resource, System.IServiceProvider! services) -> void Aspire.Hosting.ApplicationModel.ResourceReadyEvent.Services.get -> System.IServiceProvider! +Aspire.Hosting.ApplicationModel.ResourceRelationshipAnnotation +Aspire.Hosting.ApplicationModel.ResourceRelationshipAnnotation.Resource.get -> Aspire.Hosting.ApplicationModel.IResource! +Aspire.Hosting.ApplicationModel.ResourceRelationshipAnnotation.ResourceRelationshipAnnotation(Aspire.Hosting.ApplicationModel.IResource! resource, string! type) -> void +Aspire.Hosting.ApplicationModel.ResourceRelationshipAnnotation.Type.get -> string! Aspire.Hosting.ApplicationModel.UpdateCommandStateContext Aspire.Hosting.ApplicationModel.UpdateCommandStateContext.ResourceSnapshot.get -> Aspire.Hosting.ApplicationModel.CustomResourceSnapshot! Aspire.Hosting.ApplicationModel.UpdateCommandStateContext.ResourceSnapshot.init -> void @@ -241,6 +253,7 @@ static Aspire.Hosting.ResourceBuilderExtensions.WithCommand(this Aspire.Hosti static Aspire.Hosting.ResourceBuilderExtensions.WithHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! key) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ResourceBuilderExtensions.WithHttpHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? path = null, int? statusCode = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ResourceBuilderExtensions.WithHttpsHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? path = null, int? statusCode = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.ResourceBuilderExtensions.WithRelationship(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResource! resource, string! type) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.Utils.VolumeNameGenerator.Generate(Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! suffix) -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.FailedToStart -> string! diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 7206fa16323..19ea1f7cbf8 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; +using Aspire.Dashboard.Model; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Publishing; using HealthChecks.Uris; @@ -349,6 +350,8 @@ public static IResourceBuilder WithReference(this IR var resource = source.Resource; connectionName ??= resource.Name; + builder.WithRelationship(resource, KnownRelationshipTypes.Reference); + return builder.WithEnvironment(context => { var connectionStringName = resource.ConnectionStringEnvironmentVariable ?? $"{ConnectionStringEnvironmentName}{connectionName}"; @@ -452,6 +455,8 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc { endpointReferenceAnnotation.EndpointNames.Add(endpointName); } + + builder.WithRelationship(resourceWithEndpoints, KnownRelationshipTypes.Reference); } /// @@ -701,6 +706,8 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I builder.WaitFor(parentBuilder); } + builder.WithRelationship(dependency.Resource, KnownRelationshipTypes.WaitFor); + return builder.WithAnnotation(new WaitAnnotation(dependency.Resource, WaitType.WaitUntilHealthy)); } @@ -745,6 +752,8 @@ public static IResourceBuilder WaitForCompletion(this IResourceBuilder throw new DistributedApplicationException($"The '{builder.Resource.Name}' resource cannot wait for its parent '{dependency.Resource.Name}'."); } + builder.WithRelationship(dependency.Resource, KnownRelationshipTypes.WaitFor); + return builder.WithAnnotation(new WaitAnnotation(dependency.Resource, WaitType.WaitForCompletion, exitCode)); } @@ -998,4 +1007,39 @@ public static IResourceBuilder WithCommand( return builder.WithAnnotation(new ResourceCommandAnnotation(name, displayName, updateState ?? (c => ResourceCommandState.Enabled), executeCommand, displayDescription, parameter, confirmationMessage, iconName, iconVariant, isHighlighted)); } + + /// + /// Adds a to the resource annotations to add a relationship. + /// + /// The type of the resource. + /// The resource builder. + /// The resource that the relationship is to. + /// The relationship type. + /// A resource builder. + /// + /// + /// The WithRelationship method is used to add relationships to the resource. Relationships are used to link + /// resources together in UI. The indicates information about the relationship type. + /// + /// + /// + /// This example shows adding a relationship between two resources. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var backend = builder.AddProject<Projects.Backend>("backend"); + /// var manager = builder.AddProject<Projects.Manager>("manager") + /// .WithRelationship(backend.Resource, "Manager"); + /// + /// + public static IResourceBuilder WithRelationship( + this IResourceBuilder builder, + IResource resource, + string type) where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(type); + + return builder.WithAnnotation(new ResourceRelationshipAnnotation(resource, type)); + } } diff --git a/src/Shared/Model/KnownRelationshipTypes.cs b/src/Shared/Model/KnownRelationshipTypes.cs new file mode 100644 index 00000000000..196852a3baf --- /dev/null +++ b/src/Shared/Model/KnownRelationshipTypes.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Model; + +internal static class KnownRelationshipTypes +{ + public const string WaitFor = "WaitFor"; + public const string Reference = "Reference"; + public const string Parent = "Parent"; +} diff --git a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/CreateResourceSelectModelsTests.cs b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/CreateResourceSelectModelsTests.cs index f2c3e99b6f5..e424897b4d2 100644 --- a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/CreateResourceSelectModelsTests.cs +++ b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/CreateResourceSelectModelsTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; diff --git a/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs index bc39540e67c..5c657ca1da1 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs @@ -201,7 +201,8 @@ private static GenericResourceSnapshot CreateResourceSnapshot(string name) Volumes = [], Environment = [], HealthReports = [], - Commands = [] + Commands = [], + Relationships = [] }; } diff --git a/tests/Shared/DashboardModel/ModelTestHelpers.cs b/tests/Shared/DashboardModel/ModelTestHelpers.cs index fd0f8e903a9..301458aa317 100644 --- a/tests/Shared/DashboardModel/ModelTestHelpers.cs +++ b/tests/Shared/DashboardModel/ModelTestHelpers.cs @@ -38,7 +38,8 @@ public static ResourceViewModel CreateResource( KnownState = state, StateStyle = stateStyle, HealthReports = reportHealthStatus is null && !createNullHealthReport ? [] : [new HealthReportViewModel("healthcheck", reportHealthStatus, null, null)], - Commands = [] + Commands = [], + Relationships = [], }; } }