Skip to content

Modernize the dashboard experience - #21412

Merged
Sébastien Ros (sebastienros) merged 92 commits into
mainfrom
sebros/dashboard-modern-experience
Oct 30, 2026
Merged

Sébastien Ros (sebastienros) merged 92 commits into
mainfrom
sebros/dashboard-modern-experience

Conversation

@sebastienros

@sebastienros Sébastien Ros (sebastienros) commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

This PR introduces a redesigned dashboard experience. It's an early experiment: we want to learn what works and what doesn't before going further. Users can switch back to the previous dashboard at any time from Settings, and the dashboard asks for feedback along the way.

testshop-home

What's new

  • Home view: the run's status, live activity, slowest endpoints, busiest resources and recent errors on one page.
  • Resource pane: every page keeps the resource list on the left, with search, filters, kind toggles, multi-selection and Select all. Logs, traces and metrics follow the selected resources.
  • Grouping by tags: resources and their parameters can be grouped by tags, type or state. Tags can come from the AppHost or be added in the dashboard.
  • Resource overview: choose between a summary, cards, a list or a graph, with quick actions on each resource.
  • Parameters: set or delete a parameter's value directly from its overview.
  • Metrics: instruments are shown as cards with live sparklines. You can pin the ones you care about, filter across meters, and group or sort instruments across resources.
  • Recordings: switch between the live run and recorded runs from the header.

Try it

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

Install the Aspire CLI built from this PR with:

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 21412

Or run remotely in PowerShell:

iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 21412"

- Add top-level Home, Resources, Parameters, and Graph navigation.
- Add a resources layout with a resizable, collapsible, multi-select resource
  pane and per-resource tabs for overview, console, structured logs, traces,
  and metrics.
- Add a card-based resource overview with status, endpoints, relationship
  graph, console preview, telemetry summaries, properties, environment
  variables, volumes, and health checks.
- Add a Home page with overall health, telemetry totals, resource tiles, and
  recent errors and failed traces.
- Redirect legacy resource URLs to the new views.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Replace header navigation with a vertical icon rail (Resources, Tags,
  Parameters, Graph, Terminals and Extensions placeholders).
- Resource list becomes the second navigation pane; Parameters and Graph
  render without it.
- Add a tag editor to the resource overview, persisted per application in
  a SQLite settings database, and a Tags pane grouping resources by tag.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Selecting it clears the resource selection and the overview shows a card per resource.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Matches the tab look and feel of the main resources page.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move the tag group chevron to the right and make the tag name select all resources in the group.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the filter popover with an AspireMenuButton menu matching the
resource commands popup. Menu items support group headers and checkbox
toggles that keep the menu open.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the misleading 'Live run' selector with a history icon and a menu
listing the live view and previous recordings with their date and duration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The header button reads 'Recordings' while the live view is shown and the
selected recording's date otherwise. 'Live view' gets a green play icon.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… padding, hide unlocked action

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Home: live app-state insights replacing underwhelming log/trace/metric totals
- Nav: disabled-state cursor affordance, responsive popup menu for narrow/toolbar-constrained tab rows
- Console logs toolbar: consistent row/padding, right-aligned filters, removed noisy status text
- Consistent top-nav/content padding and gaps across pages
- Metrics page: sensible default selections, reworked back-to-traces button UX
- Resource relationship graph: toned-down focus color, larger centered graphic, icon-button for open console
- Panel icon alignment and icon-button affordances
- Overview: conditional panel display, loading skeletons, endpoints card dedupe/grouping
- Container image values now link out to their registry page in a new tab
- Volumes: reveal bind-mount source path in OS file explorer (Windows/macOS/Linux)
…d-modern-layout

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Integrate the dedicated Terminals page and conditional navigation with the modern dashboard shell, preserve recordings and resource tabs, and reconcile localized resource classes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2b0b6dd9-44ee-4ba7-82c5-a5ba3b43c5f8
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6cc982e7-b012-4f63-ab6f-51ef1f51d849
Preserve upstream dashboard histogram, graph lifecycle, trace overflow, command toast, and telemetry fixes. Adapt hidden-resource indicators and migrated browser coverage to the modern navigation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6cc982e7-b012-4f63-ab6f-51ef1f51d849
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6cc982e7-b012-4f63-ab6f-51ef1f51d849
Replace per-row trace overflow and span-log FluentTooltip components with shared on-demand tooltips. Preserve log metadata and bounded rich tooltips, and cover large span-log collections.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6cc982e7-b012-4f63-ab6f-51ef1f51d849
Add resource grouping, accessible drag and keyboard ordering, and AppHost-scoped persistence. Improve resource metadata accessibility and text visualizer wrap and copy controls.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6cc982e7-b012-4f63-ab6f-51ef1f51d849
Add an all-resource console shortcut, refresh shortcut discovery and bindings, and separate keyboard shortcuts from dashboard documentation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21d03854-668d-4570-b7fd-179f09f5587d
Support multi-value traits, span-based WithTags, dashboard position defaults, and persisted user overrides. Mark AppHost-defined tags with accessible lock indicators.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21d03854-668d-4570-b7fd-179f09f5587d
Traits declared with WithTrait, WithTags and WithDashboardPosition are now
always listed in the resource overview Properties card and the resource
details panel, after the resource's own properties, instead of only under
"Show all". List values display as comma-separated text, and the reserved
dashboard traits use readable names ("Dashboard tags", "Dashboard position").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rols

- The live view and recordings button sits before notifications and opens a
  side panel, like notifications and settings, instead of a dropdown menu. The
  panel lists the live view and past recordings with their durations, protect
  toggles, and a link to the documentation.
- Render F6 as a keyboard key in the keyboard shortcuts dialog hint.
- Draw the resource pane grouping icon at 16px to match the filter and
  collapse buttons.
- Fix switching runs re-creating the page: the outgoing resources layout no
  longer navigates to another tab while it's torn down, and a layout disposed
  during initialization no longer crashes the circuit.
Smooth spline lines, a theme-aware categorical palette with translucent
fills, recessive dotted gridlines, themed hover labels with a crosshair,
smaller ringed exemplar markers, and a compact legend above the plot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The metrics tree gets a filter box matching meter names, instrument names and
descriptions. Instruments show an icon for their type (counter, gauge,
histogram), and the tooltip includes the type and unit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selecting a meter now shows a card per instrument with its type, description,
latest value (P50 for histograms) and a smooth sparkline over the selected
duration, replacing the plain name/description grid. Sparklines are rendered
server-side as SVG using monotone cubic interpolation so they never overshoot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tree item click handler toggles expansion before selecting, so clicking an
expanded meter to open its overview collapsed it. Selecting a meter now always
leaves it expanded. Constant sparklines are drawn on the baseline instead of
mid-height so a value that stays at zero doesn't read as a steady non-zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sparklines now start at zero like the full chart, so a flat 100 no longer draws
at the same height as a flat 0. Counters show bars of the increase per interval
(carried across empty intervals and handling resets), histograms show P50 bars
per interval with data, and gauges and up-down counters keep a line. Bar charts
draw a faint baseline so an interval range without increases reads as zero.

OtlpInstrumentSummary now exposes IsMonotonic, which was stored but not loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Pull request created: #1985

Generated by PR Documentation Check · copilot · auto · 25.2 AIC · ⌖ 1.14 AIC · ⊞ 7.1K

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

📝 Documentation has been drafted in microsoft/aspire.dev#1985 targeting release/17.0.

Added a "Modernized dashboard experience" section to dashboard/overview.mdx. Triggered signal: dashboard_user_facing_page_changed (evidence: Components/Pages/Home.razor, Resources.razor, Metrics.razor, etc.).

Note

This draft PR needs human review before merging.

David Fowler (davidfowl) added a commit that referenced this pull request Nov 2, 2026
…21656)

## Description


`DashboardUrlTests.DashboardDisplaysResourceUpdatesWhileAppHostIsPausedAtBreakpoint`
has failed in every scheduled Outerloop run on `main` since #21412
("Modernize the dashboard experience") made the Cards layout the default
resources view. Before #21412 it passed 12 of 12 runs; since then it has
failed 2 of 2
([41158473015](https://github.com/microsoft/aspire/actions/runs/41158473015),
[41263841334](https://github.com/microsoft/aspire/actions/runs/41263841334)).

**Root cause:** the test waits for `tr.resource-row`, the legacy grid
row. The default view now renders each resource as `<article
class="resource-card" aria-label="{name}">`
(`ApplicationSummary.razor`), so the locator never matches. The aria
snapshot in the CI failure shows the card already displaying
`dashboard-test-resource … State Dashboard test initial`. The product is
fine; only the locator is stale.

**Fix:** locate the resource card by its exact `aria-label`. The state
is still read from `.state-column-cell`, because the card renders the
same `StateColumnDisplay` component the grid did. Both state assertions
are unchanged: the initial custom state, then the updated state
published while the AppHost is held at the breakpoint. There are no
sleeps, retries, or relaxed timeouts.

Fixes #21655

### Validation

- **Before the fix (`main` at 1bccada), locally with real DCP, the
dashboard, and headless Chromium:** fails the same way as CI, `Locator
expected to be visible … waiting for Locator("tr.resource-row")…`. The
snapshot shows the Cards region containing `dashboard-test-resource …
Dashboard test initial`.
- **After the fix:** passes in about 30s. It passed **50 of 50**
consecutive local runs (27–33s each).
- **Outerloop CI on this branch:** I dispatched [run
41310885927](https://github.com/microsoft/aspire/actions/runs/41310885927)
on 7b9e3a3.
- `outerloop_tests / Hosting.Testing (ubuntu-latest)` passed ([job
124868319332](https://github.com/microsoft/aspire/actions/runs/41310885927/job/124868319332):
total 1, succeeded 1, skipped 0). That job runs only this outerloop
test; on `main` it reports total 1, failed 1.
  - `Hosting.Testing (windows-latest)` also passed.
- I then cancelled the run. Its remaining Templates and Radius jobs fail
on every scheduled `main` run for unrelated, already-tracked reasons
(#21413/#21423, MongoDB family).
- Because of that cancellation, the run's `Final Results` check shows as
failed on this commit. It is not one of this PR's required checks.

### Out of scope

`TemplateTestsBase.CheckDashboardHasResourcesActualAsync` (`.main-grid
tr.resource-row`) reads the same legacy grid and affects the Templates
outerloop tests. That path is tracked by #21413/#21423.

## Checklist

- Is this feature complete?
  - [x] Yes. Ready to ship.
  - [ ] No. Follow-up changes expected.
- Are you including unit tests for the changes and scenario tests if
relevant?
  - [x] Yes
  - [ ] No
- Did you add public API?
  - [ ] Yes
  - [x] No
- Does the change make any security assumptions or guarantees?
  - [ ] Yes
  - [x] No

**🤖 Posted by AspireAgent (AI Agent)**

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sébastien Ros (sebastienros) added a commit that referenced this pull request Nov 4, 2026
…y changes (#21143)

## Description

`ChartBase` decides between a full chart initialization and a data
update using the instrument key (meter and instrument names), the
dimension attributes, the theme and the display options. Switching
resources can show an instrument with the same key and dimensions but a
different unit, type or monotonicity. The chart then called
`updateChart`, which keeps the previous instrument's trace names and
layout, so the legend labels could be wrong.

Trace names come from the unit, and from whether the instrument is a
monotonic counter ("per interval"). The type decides which traces are
drawn. This PR adds all three to the re-initialization check.

Comparing dimension attributes by value, which stops charts
re-initializing on every refresh, reached main through #21412. That
removed the re-initialization that previously covered this case. This PR
originally carried that value comparison too; only this follow-up
remains after rebasing onto main.

## Testing

- New test
`PlotlyChartTests.UpdateDataAsync_SameInstrumentKeyWithDifferentUnitTypeOrMonotonicity_ReinitializesChart`
covers a unit change, a type change and a monotonicity change. All three
cases fail without the `ChartBase` change.
- All non-quarantined, non-outerloop tests in
`Aspire.Dashboard.Components.Tests` pass.

Co-authored-by: Sébastien Ros <1165805+sebastienros@users.noreply.github.com>
Eric Erhardt (eerhardt) pushed a commit that referenced this pull request Nov 5, 2026
…sourcePane tests (#21480)

## Description

Fixes a flaky race in two Dashboard Playwright outerloop tests added or
changed by #21412:

-
`DesktopNavMenuTests.MainNavigation_NavigatesBetweenSectionsAndMarksActiveItem`
-
`ResourcePaneInteractionTests.AllResourcesRow_ClearsSelectionAndListsResourcesInOverview`

### Root cause

A resource tab click changes the URL in the browser right away, before
the server renders the destination. Until that render batch reaches the
browser, the tab strip on the page is still the one rendered for the
previous location. The `FluentTabs` in `ResourcesLayout` is `@key`ed by
`renderedLocation`, and `OnTabChanged` deliberately ignores change
events from a strip rendered for an earlier location. The comment in
`ResourcesLayout.razor` explains that this guard stops queued native
change events from a torn-down strip from looking like new user
navigations.

Both tests click a second tab as soon as the URL or `aria-selected`
changes. Both of those update on the client, so neither proves the
server has rendered the new strip. When the second click lands in that
window, it is dropped by design and the test times out waiting for a
navigation that never happens.

Server-side tracing (temporary logging, not in this PR) confirmed the
sequence locally:
1. Click Console logs; `OnParametersSetAsync` runs for `/consolelogs`
(t=.956).
2. The test clicks Structured logs at about .96–.99 on the strip still
keyed to `/resources`.
3. The new strip is rendered at 45.019. The queued event then arrives
with `renderedLocation=/resources` while `_lastLocation=/consolelogs`,
and is ignored.

### Fix (test-only)

Add `PlaywrightTestsBase.ClickResourceTabAndWaitForRenderAsync`. It
takes a handle to the current `.resources-tab-header` element, clicks,
and waits until that element is detached. Because the strip is keyed by
location, detachment means the server's render for the new location has
been applied. The tests then make the same assertions as before. No
sleeps or retries are added and no assertion is weakened.
`AssertActiveItemAsync` now checks the server-rendered active tab rather
than only the client's optimistic selection.

The product guard is intentional, so it is unchanged.

### Evidence

**CI (outerloop, `outerloop_tests / Dashboard.Playwright`; these tests
only run in `tests-outerloop.yml`):**
- Run
[41050874298](https://github.com/microsoft/aspire/actions/runs/41050874298)
is the only outerloop run with Playwright jobs that includes #21412. On
Ubuntu, both tests failed:
- `MainNavigation`: `Assert.NotNull() Failure` for the next tab's
bounding box.
- `AllResourcesRow`: `Page URL expected to match regex '/resources$' But
was: '.../consolelogs'`.
- On Windows, both passed in the same run.
- Denominator so far: Ubuntu 1/1, Windows 0/1 runs that executed the
tests. `DesktopNavMenuTests` failed 0/28 jobs in the 14 scheduled main
runs from Oct 16–30, before #21412. `ResourcePaneInteractionTests`
didn't exist yet.

**Local (Linux, 2 cores):**

| Scenario | Before | After |
|---|---|---|
| `MainNavigation`, idle | 8/8 failed | 10/10 passed |
| `MainNavigation`, CPU stress (2× `yes`) | 10/10 failed | 10/10 passed
|
| `AllResourcesRow`, idle and stress | 18/18 passed (window too narrow
locally) | 20/20 passed |
| `AllResourcesRow` with deterministic repro harness ¹ | 5/5 failed with
the exact CI message (`But was: .../consolelogs`) | 5/5 passed |
| Full `Aspire.Dashboard.Playwright.Tests` | n/a | 126/127 ² |

¹ Repro harness (not committed): an init script delays every
server-to-browser Blazor WebSocket message by 300 ms, in order, after
the Console logs click. This widens the window between the client URL
change and the server render:

```csharp
await page.AddInitScriptAsync("""
    (() => {
        const desc = Object.getOwnPropertyDescriptor(WebSocket.prototype, 'onmessage');
        Object.defineProperty(WebSocket.prototype, 'onmessage', {
            configurable: true,
            get() { return desc.get.call(this); },
            set(handler) {
                let chain = Promise.resolve();
                desc.set.call(this, handler && (e => {
                    const due = Date.now() + (window.__delayBlazor ? 300 : 0);
                    chain = chain.then(() => new Promise(r => setTimeout(r, Math.max(0, due - Date.now())))).then(() => handler.call(this, e));
                }));
            }
        });
    })();
    """);
// ... then before the Console logs click:
await page.EvaluateAsync("() => window.__delayBlazor = true");
```

² The only failure is
`TerminalTests.HeaderMetadata_LiveUpdatesAndFallbackPreserveFocusAndCopyCurrentValues`.
It's a local culture artifact (`'42 %'` vs `'42%'`) in a test that
doesn't use the new helper and isn't touched by this PR.

## Checklist

- Is this feature complete?
  - [x] Yes. Ready to ship.
  - [ ] No. Follow-up changes expected.
- Are you including unit tests for the changes and scenario tests if
relevant?
  - [x] Yes
  - [ ] No
- Did you add public API?
  - [ ] Yes
  - [x] No
- Does the change make any security assumptions or guarantees?
  - [ ] Yes
  - [x] No

**🤖 Posted by AspireAgent (AI Agent)**

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sébastien Ros (sebastienros) added a commit that referenced this pull request Nov 6, 2026
… storage (#21840)

## Description

Moves the dashboard's application-level settings (resource tags,
resource and group order, and pinned metrics) out of the SQLite settings
database (`dashboard-settings.db`) and into browser `localStorage`, like
the dashboard's other preferences. These settings no longer live in the
data directory on disk, and aren't shared between browsers or users.

### What changed

- `DashboardSettingsDatabase` is removed, along with the now-unused
`DashboardRunStore.GetApplicationSettingsDirectory`.
- `ResourceTagStore`, `ResourceOrderStore` and `MetricPinStore` are now
scoped per circuit and keep their synchronous read API. Each loads once
from `localStorage` (`EnsureLoadedAsync`) under a key scoped to the
application name, built with the same `DashboardApplicationNameKey` as
the existing collapsed-resources key: `Aspire_Resources_Tags_<app>`,
`Aspire_Resources_Order_<app>` and `Aspire_Metrics_Pins_<app>`.
- A new `LocalStorageSetting<T>` owns each setting's in-memory value and
its saves:
- **Tabs don't overwrite each other.** Every tab has its own circuit but
shares `localStorage`. Changes are idempotent functions applied in
memory immediately and queued; each save re-reads the stored value,
replays the queued changes onto it, and adopts the result, so a tab also
picks up what other tabs saved.
- **Unreadable values are never silently replaced.** `StorageResult` now
tells transient read failures (`ReadFailed`: disconnected circuit, JS
errors, timeouts) apart from values that will never read
(`InvalidValue`: bad JSON, data protection mismatch, or a bug such as an
unregistered type). Transient failures block writes, and the save is
retried with backoff through `TimeProvider`. An invalid value is copied
once to `<key>_Unreadable` and then replaced, so saving keeps working.
- A load that read storage before a save completed doesn't replace the
newer saved value.
- Values are stored unprotected. Protected values are encrypted with the
dashboard's data protection key, which changes across Aspire versions
and on every run of the standalone dashboard, so they would be lost
regularly. These are names chosen by the user or reported by the app,
not secrets.
- Moves can no longer fail to save synchronously, so the "Unable to save
the resource/group order" messages and their resource strings are
removed (`.xlf` regenerated). `TryMove` and `TryMoveGroup` return
`false` when an item has no position, and the existing
reorder-unavailable toast is shown.
- `ResourcesLayout` starts loading tags and order early and waits for
them only before showing resources. `ResourceOverview` no longer waits
for pins.

### Reviewer notes

- **No migration.** Data in an existing `dashboard-settings.db` isn't
carried over. That file was added in #21412 and hasn't shipped in a
release.
- **Settings are tied to the browser origin, port included.** If an
app's dashboard is served on a different port, its tags, order and pins
don't carry over. The other browser-stored preferences already work this
way.
- Tabs pick up each other's changes when they save or load, not live.

### Tests

- Unit tests run the stores against the real `LocalBrowserStorage` over
an in-memory `localStorage`, so they cover the stored JSON and
source-generated serialization. They cover cross-tab merging, unreadable
and invalid values, retries with `FakeTimeProvider`, and the stale-load
race. The race test fails if the guard is removed.
- The shared `TestJSRuntime` gains an optional callback (replacing a
private copy). `TestLocalStorage` returns saved values like a browser,
and can simulate failed reads.
- Playwright tag tests seed `localStorage` with an init script instead
of using the server-side store.
- Ran: `Aspire.Dashboard.Tests` (2,618 passed),
`Aspire.Dashboard.Components.Tests` (1,269 passed), and the two affected
outerloop Playwright tests
(`TagHeader_SelectsTaggedResourcesAndChevronTogglesGroup`,
`ContextMenu_RepeatedTagRowsRestoreExactTriggerAndRespectLightDismissal`).

## Checklist

- Is this feature complete?
  - [x] Yes. Ready to ship.
  - [ ] No. Follow-up changes expected.
- Are you including unit tests for the changes and scenario tests if
relevant?
  - [x] Yes
  - [ ] No
- Did you add public API?
  - [ ] Yes
    - If yes, did you have an API Review for it?
      - [ ] Yes
      - [ ] No
- Did you add `<remarks />` and `<code />` elements on your triple slash
comments?
      - [ ] Yes
      - [ ] No
  - [x] No
- Does the change make any security assumptions or guarantees?
  - [ ] Yes
    - If yes, have you done a threat model and had a security review?
      - [ ] Yes
      - [ ] No
  - [x] No

---------

Co-authored-by: Sébastien Ros <1165805+sebastienros@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sébastien Ros (sebastienros) added a commit that referenced this pull request Nov 6, 2026
…1881)

## Description

Renames the reserved trait used by `WithTags` from
`Aspire_Dashboard_Tag` to `Aspire_Tag`.

AppHost tags aren't dashboard-only. The CLI backchannel adds traits to
resource snapshots the same way the dashboard does, so `aspire describe
--format json` already includes them:

```json
"properties": {
  "resource.trait.Aspire_Tag": ["frontend", "critical"]
}
```

With the old name, tools reading that output had to know a
dashboard-scoped name for something that isn't dashboard-specific, and
the method name (`WithTags`) and the trait name didn't match in scope.

### What changed

- `KnownResourceTraits.DashboardTag = "Aspire_Dashboard_Tag"` →
`KnownResourceTraits.Tag = "Aspire_Tag"`, with a comment explaining the
naming.
- The `Aspire_` prefix stays so the reserved name can't collide with
user traits added through `WithTrait` (a bare `tag` would pick up tag
validation and dashboard tag behavior).
- `Aspire_Dashboard_Position` / `WithDashboardPosition` are unchanged:
only the dashboard uses a position, so the name matches the scope.
- The `TraitAnnotation` validation message now says "Tags must…" instead
of "Dashboard tags must…".
- `docs/specs/dashboard-persistence.md` uses the new name, says that the
tags appear in `aspire describe`, and no longer says that user-added
tags live in the settings database (they're in browser local storage
since #21840).
- The resource string key `ResourceTraitDashboardTags` keeps its name.
Renaming it would mean editing the resx and Designer files and
regenerating the `.xlf` files, and users never see the key.

### Reviewer notes

- **No compatibility shim.** The trait was added in #21412 and isn't in
any release tag yet, so nothing shipped reads the old name.

### Validation

- `Aspire.Hosting.Tests` (`ResourceTraitBuilderTests`,
`DashboardServiceDataTerminalTests`): 16 passed.
- `Aspire.Dashboard.Tests` (`ResourceTagStoreTests`): 20 passed.
- `Aspire.Dashboard.Components.Tests` (`ResourcesLayoutTests`,
`ResourceDetailsTests`): 168 passed.
- `Aspire.Dashboard.Playwright.Tests` builds. Its tests weren't run
locally.

## Checklist

- Is this feature complete?
  - [x] Yes. Ready to ship.
  - [ ] No. Follow-up changes expected.
- Are you including unit tests for the changes and scenario tests if
relevant?
  - [x] Yes
  - [ ] No
- Did you add public API?
  - [ ] Yes
  - [x] No
- Does the change make any security assumptions or guarantees?
  - [ ] Yes
  - [x] No

Co-authored-by: Sébastien Ros <1165805+sebastienros@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants