Skip to content
Closed
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
10 changes: 6 additions & 4 deletions docs/specs/dashboard-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ builder.AddProject<Projects.Api>("api")

`WithTags` accepts a single tag or multiple tags using `params ReadOnlySpan<string>`,
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
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public IReadOnlyList<string> GetTraitValues(string name) =>
? property.Value.ListValue.Values.Where(v => v.HasStringValue).Select(v => v.StringValue).ToArray()
: [];

internal IReadOnlyList<string> AppHostTags => GetTraitValues(KnownResourceTraits.DashboardTag);
internal IReadOnlyList<string> AppHostTags => GetTraitValues(KnownResourceTraits.Tag);

internal int? DefaultDashboardPosition
{
Expand Down Expand Up @@ -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
};
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting/ApplicationModel/TraitAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/ResourceTraitBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static IResourceBuilder<T> WithTags<T>(this IResourceBuilder<T> builder,

foreach (var tag in tags)
{
builder.WithTrait(KnownResourceTraits.DashboardTag, tag);
builder.WithTrait(KnownResourceTraits.Tag, tag);
}

return builder;
Expand Down
4 changes: 3 additions & 1 deletion src/Shared/Model/KnownProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ public void AppHostTagsParticipateInGroupingWithoutUserTags()
{
var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
var channel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag;
var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag;
ResourceSetupHelpers.SetupResourcesLayout(this, viewport, new TestDashboardClient(isEnabled: true,
initialResources:
[
Expand Down Expand Up @@ -767,7 +767,7 @@ public void Grouping_ParametersJoinTagGroupsAndKeepTheirSectionOtherwise(string
{
var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
var channel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag;
var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag;
Dictionary<string, ResourcePropertyViewModel> Tags(params string[] tags) => new()
{
[name] = new(name, Value.ForList([.. tags.Select(Value.ForString)]), false, null, 0, null, false)
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2248,7 +2248,7 @@ public static IReadOnlyList<ResourceViewModel> CreateResources() =>
private static ResourceViewModel Create(string name, string type, KnownResourceState state, HealthStatus? health,
DateTime? startTime, ImmutableArray<UrlViewModel> urls, string[] tags)
{
var tagKey = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag;
var tagKey = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.Tag;
var properties = ImmutableDictionary<string, ResourcePropertyViewModel>.Empty.Add(tagKey,
new(tagKey, Value.ForList([.. tags.Select(Value.ForString)]), false, null, 0, null, false));
if (type != KnownResourceTypes.Parameter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 6 additions & 6 deletions tests/Aspire.Hosting.Tests/ResourceTraitBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand All @@ -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<string[]>(properties[1].Value));
Assert.Equal(["Finance, Inc.", "Platform"], Assert.IsType<string[]>(properties[2].Value));
Assert.Equal("Owner", properties[2].DisplayName);
Expand All @@ -67,7 +67,7 @@ public void PositionTraitReplacesOnlyPositionAndValidatesBeforeReplacing()
Assert.Throws<ArgumentException>(() => 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<TraitAnnotation>().Select(t => (t.Name, t.Value)));
}

Expand Down
Loading