Show each identifier once in the drawer's Identity card

A Machines agent drawer listed agent:host-linux-1 as its Primary ID, its
Discovery target and its Metrics Target, then again in Aliases next to
the hostname and Platform ID it had just shown. Every platform's drawer
shares this card, and a vSphere datastore repeated its ID the same way.

Discovery and Metrics Target rows now appear only when they name
something not already on screen: the header's name, the rows above,
or the Platform ID row. Aliases leave out those same values. Names the
drawer never shows do not count, so a distinct target still appears.
A shown Platform ID counts as identity data, so filtering cannot leave
a false "No identity metadata yet". Search and alias lookups keep the
full lists; only the card's display changes.
This commit is contained in:
rcourtman
2026-10-06 16:03:54 +01:00
parent 27051deb27
commit 4884052bc5
7 changed files with 166 additions and 9 deletions
@@ -4331,7 +4331,13 @@ must render from that shared projection instead of rescanning raw job arrays or
inventing local PBS status heuristics,
`resourceDetailDrawerIdentityModel.ts` owns the pure identity-card,
discovery-summary, source-debug, and debug-bundle derivations that feed the
overview and debug drawer surfaces,
overview and debug drawer surfaces (the `Identity` card shows each identifier
once: Discovery and Metrics Target rows appear only when they name something
not already on screen, meaning the header's `getPreferredInfrastructureDisplayName`,
the rows above, or the Platform ID row, and Aliases omits the same displayed
values; candidate names the drawer never shows do not count, a shown Platform
ID counts as identity data for the empty state, and a Machines agent drawer
used to list one ID four times),
`useResourceDetailDrawerDockerActionsState.ts` owns Docker action runtime, and
the overview/debug render-heavy surfaces live in dedicated drawer-local owners
instead of staying inline in the shell.
@@ -0,0 +1,37 @@
{
"version": 1,
"base_sha": "27051deb279b8dc42a82922f14ebdddc7e8fbfcc",
"verified_at": "2026-10-06T14:58:00Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Infrastructure/resourceDetailDrawerIdentityModel.ts",
"frontend-modern/src/utils/resourceIdentity.ts"
],
"content_sha256": {
"frontend-modern/src/components/Infrastructure/resourceDetailDrawerIdentityModel.ts": "902911e665585a831c072675c85609bc1e6015e71db0deb79877767d9b454892",
"frontend-modern/src/utils/resourceIdentity.ts": "c76d4279e7caca8b96b1a52e4c166f791264217730d631f47b8de3cedf411c32"
},
"routes": [
"/standalone/machines",
"/vmware/storage"
],
"viewports": [
{
"width": 1440,
"height": 1300
},
{
"width": 390,
"height": 844
}
],
"states": [
"Private mock stack on main c4db7a3fba plus this change (rebased onto 27051deb27, whose node-alert dedup touches no file here); opened a Machines agent drawer (Apollo-114) and a vSphere datastore drawer (archive-tier)",
"Before (same stack with this change stashed): Machines Identity read Hostname apollo-114 | Primary ID agent:host-linux-1 | Discovery agent:host-linux-1 | Metrics Target agent:host-linux-1 | Platform ID host-linux-1 | IP Addresses | Aliases agent:host-linux-1, host-linux-1, apollo-114, agent-acdfdee2953587fb; vSphere repeated the Primary ID as Metrics Target and in Aliases, plus the hostname and Platform ID",
"After: Machines reads Hostname apollo-114 | Primary ID agent:host-linux-1 | Platform ID host-linux-1 | IP Addresses | Aliases agent-acdfdee2953587fb; vSphere reads Hostname | Primary ID | Platform ID | Tags | Aliases vc-mock-1:datastore:datastore-202, datastore-202. Every identifier appears once",
"Phone (390): both drawers' Identity cards read the same single values, stacked label/value rows, nothing clipped"
],
"interactions": [
"Opened each drawer and read its Identity card, before and after"
]
}
@@ -799,7 +799,7 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
expect(getByText('Primary ID').parentElement?.textContent).toContain('node:homelab-delly');
});
it('shows canonical metrics target identity for docker-backed host resources', () => {
it('shows the canonical metrics target once, as the Primary ID', () => {
const resource = baseResource({
id: 'hash-docker-resource',
type: 'docker-host',
@@ -824,12 +824,14 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
},
});
const { container, getByText, getAllByText } = render(() => (
const { container, getByText, getAllByText, queryByText } = render(() => (
<ResourceDetailDrawer resource={resource} />
));
expect(getByText('Metrics Target')).toBeInTheDocument();
expect(getAllByText('docker-host:docker-host-1').length).toBeGreaterThan(1);
// The metrics target is the Primary ID, so neither its row nor an alias
// repeats it.
expect(queryByText('Metrics Target')).toBeNull();
expect(getAllByText('docker-host:docker-host-1')).toHaveLength(1);
expect(getByText('Aliases')).toBeInTheDocument();
expect(getAllByText('docker-host-1').length).toBeGreaterThan(0);
expect(container.querySelector('.text-\\[11px\\].text-muted.truncate')).toBeNull();
@@ -25,6 +25,65 @@ const baseResource = (overrides: Partial<Resource> = {}): Resource => ({
});
describe('resourceDetailDrawerIdentityModel', () => {
it('filters aliases against the name the drawer header actually shows', () => {
const identityView = buildResourceIdentityView(
baseResource({
displayName: '',
name: 'legacy',
platformId: 'legacy',
canonicalIdentity: {
displayName: 'Canonical label',
aliases: ['legacy', 'Canonical label'],
},
} as Partial<Resource>),
);
// The header reads "Canonical label"; "legacy" is shown nowhere else.
expect(identityView.identityAliasValues).toContain('legacy');
expect(identityView.identityAliasValues).not.toContain('Canonical label');
});
it('counts a shown Platform ID as identity data', () => {
const identityView = buildResourceIdentityView(
baseResource({
name: 'vm-101',
displayName: 'VM 101',
platformId: 'qemu-101',
canonicalIdentity: { aliases: ['qemu-101'] },
} as Partial<Resource>),
);
expect(identityView.identityAliasValues).toEqual([]);
expect(identityView.identityCardHasRichData).toBe(true);
});
it('lists only aliases the drawer does not already show', () => {
const identityView = buildResourceIdentityView(
baseResource({
id: 'agent-acdfdee2953587fb',
type: 'agent',
name: 'Apollo-114',
displayName: 'Apollo-114',
platformId: 'host-linux-1',
identity: { hostname: 'apollo-114' },
discoveryTarget: {
resourceType: 'agent',
agentId: 'host-linux-1',
resourceId: 'host-linux-1',
},
metricsTarget: { resourceType: 'agent', resourceId: 'host-linux-1' },
canonicalIdentity: {
aliases: ['agent:host-linux-1', 'host-linux-1', 'apollo-114', 'agent-acdfdee2953587fb'],
},
} as Partial<Resource>),
);
// Hostname, Primary ID and the Platform ID row already show these.
expect(identityView.primaryIdentityRows.map((row) => row.label)).toEqual([
'Hostname',
'Primary ID',
]);
expect(identityView.identityAliasValues).toEqual(['agent-acdfdee2953587fb']);
});
it('builds canonical identity card state including alias collapse policy', () => {
const identityView = buildResourceIdentityView(
baseResource({
@@ -1,7 +1,9 @@
import type { Resource } from '@/types/resource';
import {
getPrimaryResourceIdentityRows,
getPreferredInfrastructureDisplayName,
getResourceIdentityAliases,
shouldShowResourcePlatformId,
type ResourceIdentityRow,
} from '@/utils/resourceIdentity';
import { formatIdentifierLabel } from '@/utils/textPresentation';
@@ -26,9 +28,24 @@ export type ResourceDetailDrawerIdentityView = {
const ALIAS_COLLAPSE_THRESHOLD = 4;
export const buildResourceIdentityView = (resource: Resource): ResourceDetailDrawerIdentityView => {
const identityAliasValues = getResourceIdentityAliases(resource);
const identityIpValues = resource.identity?.ips ?? [];
const primaryIdentityRows = getPrimaryResourceIdentityRows(resource);
// The drawer header already shows the resource's display name, and the rows
// above show these IDs and (when it differs from every name) the platform
// ID, so Aliases lists only the other names the resource goes by.
const platformIdShown = shouldShowResourcePlatformId(resource);
const shown = new Set(
[
getPreferredInfrastructureDisplayName(resource),
platformIdShown ? resource.platformId : undefined,
...primaryIdentityRows.map((row) => row.value),
]
.map((value) => value?.trim().toLowerCase())
.filter((value): value is string => Boolean(value)),
);
const identityAliasValues = getResourceIdentityAliases(resource).filter(
(alias) => !shown.has(alias.trim().toLowerCase()),
);
return {
identityAliasValues,
@@ -36,6 +53,7 @@ export const buildResourceIdentityView = (resource: Resource): ResourceDetailDra
primaryIdentityRows,
identityCardHasRichData:
primaryIdentityRows.length > 0 ||
platformIdShown ||
identityIpValues.length > 0 ||
(resource.tags?.length || 0) > 0 ||
identityAliasValues.length > 0,
@@ -187,7 +187,32 @@ describe('resourceIdentity', () => {
{ label: 'Cluster', value: 'cluster-a' },
{ label: 'Parent', value: 'parent-1' },
{ label: 'Discovery', value: 'agent:agent-1' },
{ label: 'Metrics Target', value: 'docker-host:docker-host-1' },
// The metrics target is the Primary ID shown above, so it is not repeated.
]);
});
it('keeps a discovery target that matches only a name the drawer never shows', () => {
const rows = getPrimaryResourceIdentityRows(
makeResource({
displayName: 'Friendly host',
canonicalIdentity: { displayName: 'agent:other' },
discoveryTarget: { resourceType: 'agent', agentId: 'other', resourceId: 'other' },
metricsTarget: { resourceType: 'node', resourceId: 'self' },
} as Parameters<typeof makeResource>[0]),
);
expect(rows.find((row) => row.label === 'Discovery')?.value).toBe('agent:other');
});
it('shows an identity once when discovery and metrics targets name the resource itself', () => {
const rows = getPrimaryResourceIdentityRows(
makeResource({
identity: { hostname: 'tower.local' },
discoveryTarget: { resourceType: 'agent', agentId: 'agent-1', resourceId: 'agent-1' },
metricsTarget: { resourceType: 'agent', resourceId: 'agent-1' },
}),
);
expect(rows.filter((row) => row.value === 'agent:agent-1').map((row) => row.label)).toEqual([
'Primary ID',
]);
});
+12 -2
View File
@@ -289,11 +289,21 @@ export const getPrimaryResourceIdentityRows = (resource: Resource): ResourceIden
rows.push({ label: 'Parent', value: resource.parentId });
}
// Discovery and metrics targets usually point at the resource itself, so
// they show only when they name something not already on screen: the
// drawer header's name, the rows above, or the Platform ID row.
const shownIdentity = (value: string): boolean =>
[
getPreferredInfrastructureDisplayName(resource),
shouldShowResourcePlatformId(resource) ? asTrimmedString(resource.platformId) : undefined,
...rows.map((row) => row.value),
].some((shown) => Boolean(shown) && shown!.toLowerCase() === value.toLowerCase());
const discoveryIdentity = formatIdentityTarget(
resource.discoveryTarget?.resourceType,
resource.discoveryTarget?.resourceId,
);
if (discoveryIdentity) {
if (discoveryIdentity && !shownIdentity(discoveryIdentity)) {
rows.push({ label: 'Discovery', value: discoveryIdentity });
}
@@ -301,7 +311,7 @@ export const getPrimaryResourceIdentityRows = (resource: Resource): ResourceIden
resource.metricsTarget?.resourceType,
resource.metricsTarget?.resourceId,
);
if (metricsIdentity) {
if (metricsIdentity && !shownIdentity(metricsIdentity)) {
rows.push({ label: 'Metrics Target', value: metricsIdentity });
}