diff --git a/docs/specs/dashboard-persistence.md b/docs/specs/dashboard-persistence.md index 77c76341a96..8f5282a4a63 100644 --- a/docs/specs/dashboard-persistence.md +++ b/docs/specs/dashboard-persistence.md @@ -74,10 +74,12 @@ builder.AddProject("api") `WithTags` accepts a single tag or multiple tags using `params ReadOnlySpan`, avoiding a temporary argument array in C#. Polyglot SDKs accept an array of tags. -It uses the reserved trait `Aspire_Dashboard_Tag`. These tags participate in -dashboard tag grouping but cannot be removed in the dashboard. User-added tags are -stored separately in the application settings database, so restarting an AppHost -does not lose customizations or copy declared tags from a different run. +It uses the reserved trait `Aspire_Tag`, which is not dashboard-specific: it also +appears as the `resource.trait.Aspire_Tag` property in `aspire describe --format json`. +These tags participate in dashboard tag grouping but cannot be removed in the +dashboard. User-added tags are stored separately in browser local storage, so +restarting an AppHost does not lose customizations or copy declared tags from a +different run. `WithDashboardPosition` uses `Aspire_Dashboard_Position`; repeated calls replace the previous value. Lower positions appear first; defaults can change between runs until diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 53f7ab22a6d..b65990eb019 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -60,7 +60,7 @@ public IReadOnlyList GetTraitValues(string name) => ? property.Value.ListValue.Values.Where(v => v.HasStringValue).Select(v => v.StringValue).ToArray() : []; - internal IReadOnlyList AppHostTags => GetTraitValues(KnownResourceTraits.DashboardTag); + internal IReadOnlyList AppHostTags => GetTraitValues(KnownResourceTraits.Tag); internal int? DefaultDashboardPosition { @@ -407,11 +407,11 @@ public sealed class DisplayedResourcePropertyViewModel : IPropertyGridItem public int SortOrder => _propertyViewModel.SortOrder; public bool IsTrait => _propertyViewModel.IsTrait; - // The AppHost sends the raw trait name as the display name, e.g. "Aspire_Dashboard_Tag". Reserved dashboard + // The AppHost sends the raw trait name as the display name, e.g. "Aspire_Tag". Reserved Aspire // traits get a readable, localized name instead. public string DisplayName => _propertyViewModel.Name switch { - KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag => _loc[nameof(Resources.Resources.ResourceTraitDashboardTags)], + KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag => _loc[nameof(Resources.Resources.ResourceTraitDashboardTags)], KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardPosition => _loc[nameof(Resources.Resources.ResourceTraitDashboardPosition)], _ => _propertyViewModel.DisplayName ?? _propertyViewModel.KnownProperty?.GetDisplayName(_loc) ?? _propertyViewModel.Name }; diff --git a/src/Aspire.Hosting/ApplicationModel/TraitAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/TraitAnnotation.cs index 701845e4581..50699a909b2 100644 --- a/src/Aspire.Hosting/ApplicationModel/TraitAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/TraitAnnotation.cs @@ -21,10 +21,10 @@ public TraitAnnotation(string name, string value) ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(value); - if (name == KnownResourceTraits.DashboardTag && + if (name == KnownResourceTraits.Tag && (value.Length > 50 || value != string.Join(' ', value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)))) { - throw new ArgumentException("Dashboard tags must be at most 50 characters and use single spaces between words, without leading or trailing whitespace.", nameof(value)); + throw new ArgumentException("Tags must be at most 50 characters and use single spaces between words, without leading or trailing whitespace.", nameof(value)); } if (name == KnownResourceTraits.DashboardPosition && !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { diff --git a/src/Aspire.Hosting/ResourceTraitBuilderExtensions.cs b/src/Aspire.Hosting/ResourceTraitBuilderExtensions.cs index b643a9d33bc..2e19bc60315 100644 --- a/src/Aspire.Hosting/ResourceTraitBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceTraitBuilderExtensions.cs @@ -46,7 +46,7 @@ public static IResourceBuilder WithTags(this IResourceBuilder builder, foreach (var tag in tags) { - builder.WithTrait(KnownResourceTraits.DashboardTag, tag); + builder.WithTrait(KnownResourceTraits.Tag, tag); } return builder; diff --git a/src/Shared/Model/KnownProperties.cs b/src/Shared/Model/KnownProperties.cs index 08602f3ce81..3b6c9ac3dc5 100644 --- a/src/Shared/Model/KnownProperties.cs +++ b/src/Shared/Model/KnownProperties.cs @@ -111,6 +111,8 @@ internal static class KnownResourcePropertySortOrder internal static class KnownResourceTraits { - public const string DashboardTag = "Aspire_Dashboard_Tag"; + // Tags are not dashboard-only: they also appear in `aspire describe` output and other tooling, so the + // name has no "Dashboard" scope. The "Aspire_" prefix keeps it apart from user traits added with WithTrait. + public const string Tag = "Aspire_Tag"; public const string DashboardPosition = "Aspire_Dashboard_Position"; } diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/ResourceDetailsTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/ResourceDetailsTests.cs index aaa91c2389e..047efba17be 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/ResourceDetailsTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/ResourceDetailsTests.cs @@ -659,7 +659,7 @@ public void Render_AppHostTraits_ShowInDefaultResourceDetailsWithReadableNamesAn var properties = new[] { Trait("Owner", "Finance", "Platform"), - Trait(KnownResourceTraits.DashboardTag, "Backend", "API"), + Trait(KnownResourceTraits.Tag, "Backend", "API"), Trait(KnownResourceTraits.DashboardPosition, "10"), new ResourcePropertyViewModel("provider.diagnostic", Value.ForString("hidden"), false, null, int.MaxValue, null, false) }.ToDictionary(p => p.Name); diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/ResourcesLayoutTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/ResourcesLayoutTests.cs index c896991bd5e..418dd99affc 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/ResourcesLayoutTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/ResourcesLayoutTests.cs @@ -739,7 +739,7 @@ public void AppHostTagsParticipateInGroupingWithoutUserTags() { var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false); var channel = Channel.CreateUnbounded>(); - var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag; + var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag; ResourceSetupHelpers.SetupResourcesLayout(this, viewport, new TestDashboardClient(isEnabled: true, initialResources: [ @@ -767,7 +767,7 @@ public void Grouping_ParametersJoinTagGroupsAndKeepTheirSectionOtherwise(string { var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false); var channel = Channel.CreateUnbounded>(); - var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag; + var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag; Dictionary Tags(params string[] tags) => new() { [name] = new(name, Value.ForList([.. tags.Select(Value.ForString)]), false, null, 0, null, false) @@ -1364,7 +1364,7 @@ static CommandViewModel Command(string name, CommandViewModelState state) => public void Relationships_KeepParentWaitAndReferenceMeaningInRepeatedTagMemberships() { var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false); - var tagKey = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag; + var tagKey = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag; var resource = ModelTestHelpers.CreateResource("catalogdb", properties: new() { [KnownProperties.Resource.ParentName] = new(KnownProperties.Resource.ParentName, Value.ForString("postgres"), false, null, 0, null, false), diff --git a/tests/Aspire.Dashboard.Playwright.Tests/Integration/Playwright/ResourcePaneResponsiveTests.cs b/tests/Aspire.Dashboard.Playwright.Tests/Integration/Playwright/ResourcePaneResponsiveTests.cs index 3e24863419d..cc2f176c06a 100644 --- a/tests/Aspire.Dashboard.Playwright.Tests/Integration/Playwright/ResourcePaneResponsiveTests.cs +++ b/tests/Aspire.Dashboard.Playwright.Tests/Integration/Playwright/ResourcePaneResponsiveTests.cs @@ -2248,7 +2248,7 @@ public static IReadOnlyList CreateResources() => private static ResourceViewModel Create(string name, string type, KnownResourceState state, HealthStatus? health, DateTime? startTime, ImmutableArray urls, string[] tags) { - var tagKey = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag; + var tagKey = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag; var properties = ImmutableDictionary.Empty.Add(tagKey, new(tagKey, Value.ForList([.. tags.Select(Value.ForString)]), false, null, 0, null, false)); if (type != KnownResourceTypes.Parameter) diff --git a/tests/Aspire.Dashboard.Tests/Model/ResourceTagStoreTests.cs b/tests/Aspire.Dashboard.Tests/Model/ResourceTagStoreTests.cs index 5a4138b3b81..ff76197ce0e 100644 --- a/tests/Aspire.Dashboard.Tests/Model/ResourceTagStoreTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/ResourceTagStoreTests.cs @@ -30,7 +30,7 @@ public void AppHostTagsAreMergedWithoutPersistingDeclaredTags() var store = CreateStore(); store.AddTag("api", "backend"); store.AddTag("api", "Personal"); - var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag; + var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag; var resource = ModelTestHelpers.CreateResource("api", properties: new() { [name] = new(name, Value.ForList(Value.ForString("Backend"), Value.ForString("API")), false, null, 0, null, false) diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceDataTerminalTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceDataTerminalTests.cs index 242d85c6b9d..8c3db3c2f23 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceDataTerminalTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceDataTerminalTests.cs @@ -33,13 +33,13 @@ public async Task Snapshot_WithRepeatedTraits_PreservesAllValuesAlongsideTermina using var _ = data; resource.Annotations.Add(new TraitAnnotation("Owner", "Finance, Inc.")); resource.Annotations.Add(new TraitAnnotation("Owner", "Platform")); - resource.Annotations.Add(new TraitAnnotation("Aspire_Dashboard_Tag", "Backend")); + resource.Annotations.Add(new TraitAnnotation("Aspire_Tag", "Backend")); await notifications.PublishUpdateAsync(resource, s => s with { State = new ResourceStateSnapshot("Running", null) }).DefaultTimeout(); var snapshot = await data.WaitForResourceAsync(resource.Name, r => r.Properties.Any(p => p.Name == "resource.trait.Owner")).DefaultTimeout(); var owner = Assert.Single(snapshot.Properties, p => p.Name == "resource.trait.Owner"); Assert.Equal(["Finance, Inc.", "Platform"], owner.Value.ListValue.Values.Select(v => v.StringValue)); - Assert.Equal(["Backend"], Assert.Single(snapshot.Properties, p => p.Name == "resource.trait.Aspire_Dashboard_Tag").Value.ListValue.Values.Select(v => v.StringValue)); + Assert.Equal(["Backend"], Assert.Single(snapshot.Properties, p => p.Name == "resource.trait.Aspire_Tag").Value.ListValue.Values.Select(v => v.StringValue)); Assert.Equal("true", GetTerminalProperty(snapshot, "terminal.enabled").Value.StringValue); } diff --git a/tests/Aspire.Hosting.Tests/ResourceTraitBuilderTests.cs b/tests/Aspire.Hosting.Tests/ResourceTraitBuilderTests.cs index 9d72754af9e..06b3be329c0 100644 --- a/tests/Aspire.Hosting.Tests/ResourceTraitBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceTraitBuilderTests.cs @@ -20,8 +20,8 @@ public void RepeatedTraitsAndTagsPreserveAllValues() Assert.Equal( [ - (KnownResourceTraits.DashboardTag, "Backend"), - (KnownResourceTraits.DashboardTag, "API"), + (KnownResourceTraits.Tag, "Backend"), + (KnownResourceTraits.Tag, "API"), ("Owner", "Finance"), ("Owner", "Platform"), (KnownResourceTraits.DashboardPosition, "-10") @@ -32,8 +32,8 @@ public void RepeatedTraitsAndTagsPreserveAllValues() [Theory] [InlineData("", "value")] [InlineData("Owner", " ")] - [InlineData("Aspire_Dashboard_Tag", " Backend")] - [InlineData("Aspire_Dashboard_Tag", "Back end")] + [InlineData("Aspire_Tag", " Backend")] + [InlineData("Aspire_Tag", "Back end")] [InlineData("Aspire_Dashboard_Position", "invalid")] [InlineData("Aspire_Dashboard_Position", "2147483648")] public void InvalidTraitsAreRejected(string name, string value) @@ -53,7 +53,7 @@ public void TraitsProduceListValuedSnapshotProperties() var properties = ResourceTraitSnapshotProperties.AddTraitProperties(resource.Resource, [source]); Assert.Same(source, properties[0]); - Assert.Equal(["existing", "resource.trait.Aspire_Dashboard_Tag", "resource.trait.Owner"], properties.Select(p => p.Name)); + Assert.Equal(["existing", "resource.trait.Aspire_Tag", "resource.trait.Owner"], properties.Select(p => p.Name)); Assert.Equal(["Backend", "API"], Assert.IsType(properties[1].Value)); Assert.Equal(["Finance, Inc.", "Platform"], Assert.IsType(properties[2].Value)); Assert.Equal("Owner", properties[2].DisplayName); @@ -67,7 +67,7 @@ public void PositionTraitReplacesOnlyPositionAndValidatesBeforeReplacing() Assert.Throws(() => resource.WithTrait(KnownResourceTraits.DashboardPosition, "invalid")); resource.WithTrait(KnownResourceTraits.DashboardPosition, "20"); - Assert.Equal([(KnownResourceTraits.DashboardTag, "Backend"), (KnownResourceTraits.DashboardPosition, "20")], + Assert.Equal([(KnownResourceTraits.Tag, "Backend"), (KnownResourceTraits.DashboardPosition, "20")], resource.Resource.Annotations.OfType().Select(t => (t.Name, t.Value))); }