mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 21:47:20 +00:00
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:
@@ -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"
|
||||
]
|
||||
}
|
||||
+6
-4
@@ -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();
|
||||
|
||||
+59
@@ -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',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user