mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Deduplicate versioned infrastructure system badges
This commit is contained in:
parent
825f230dec
commit
4d5d77cf79
9 changed files with 123 additions and 21 deletions
|
|
@ -666,6 +666,10 @@ prompt explain the same operator-facing priority.
|
|||
`pveVersion` or a Pulse Agent report whose OS identity resolves to Unraid or
|
||||
Proxmox VE. They must omit the version rather than showing unrelated
|
||||
collector OS versions, such as Debian 12, beside an API-backed PVE badge.
|
||||
System title metadata must apply the same identity rule: once the primary
|
||||
system badge names a platform with its version, source/method context may
|
||||
still add collection labels such as Pulse Agent, but it must not repeat the
|
||||
same platform again as an unversioned source badge.
|
||||
9. Keep top-of-page summary interaction on shared primitives. Infrastructure, workloads, and storage summary cards must route sticky-shell behavior through `frontend-modern/src/components/shared/StickySummarySection.tsx` and route row-hover or focused-series rendering through shared chart primitives such as `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/DensityMap.tsx`, rather than page-local sticky wrappers or metric-card-specific hover logic. When a page keeps summary charts visible below the desktop breakpoint, it must use the shared `stickyDesktopOnly` mode instead of adding page-local media queries, so wrapped two-column summaries scroll as normal content and only become sticky once the large-screen layout is active. The shared summary-card contract must also own stable summary-card geometry for chart-backed cards so row hover, focus, synchronized readouts, or idle header metadata cannot ratchet the sticky summary taller across rerenders.
|
||||
10. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. `frontend-modern/src/components/Recovery/RecoverySummary.tsx` is explicitly outside this interaction dialect: recovery posture cards may share summary framing, but they must not silently grow row/group/chart hover behavior without a separate governed product decision.
|
||||
11. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
|
||||
|
|
|
|||
|
|
@ -812,6 +812,10 @@ adding extra Agent badge width or sorting primarily by the telemetry method.
|
|||
When a row is only known through an agent or container runtime, the table must
|
||||
prefer reported OS/appliance identity before falling back to Docker/runtime
|
||||
capability labels.
|
||||
When the displayed system badge already includes the platform version, row title
|
||||
metadata must not spend extra badge/title budget repeating that same platform as
|
||||
an unversioned source; it may keep non-duplicate collection context such as
|
||||
Pulse Agent.
|
||||
That derived workload owner now also routes grouped row windowing through
|
||||
`frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts`, which
|
||||
owns row-window thresholds, overscan behavior, reveal-index clamping, and
|
||||
|
|
|
|||
|
|
@ -156,6 +156,9 @@ cross-source deduplication.
|
|||
infrastructure platform first, while full merged-source detail remains
|
||||
available for tooltips, accessibility metadata, and routing. Agent telemetry
|
||||
is collection-method detail when a provider/API platform is also present.
|
||||
When the primary system identity includes a platform version, table metadata
|
||||
must suppress an otherwise duplicate unversioned platform source badge while
|
||||
preserving non-duplicate collection context such as Pulse Agent.
|
||||
Infrastructure table presentation controls must describe the table mode
|
||||
rather than a platform-specific resource concept: the grouped/flat toggle
|
||||
uses operator-facing `Grouped` and `List` wording with a `Group by`
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
|
|||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import {
|
||||
dedupeResourceBadges,
|
||||
getInfrastructureSystemIdentityBadges,
|
||||
getInfrastructureSystemTitleBadges,
|
||||
getPlatformBadge,
|
||||
getSourceBadge,
|
||||
getUnifiedSourceBadges,
|
||||
|
|
@ -322,7 +322,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
getInfrastructureSystemIdentityBadges(resource),
|
||||
);
|
||||
const systemTitleBadges = createMemo(() =>
|
||||
dedupeResourceBadges([...systemBadges(), ...sourceBadges()]),
|
||||
getInfrastructureSystemTitleBadges(systemBadges(), sourceBadges()),
|
||||
);
|
||||
const policyBadges = createMemo(() =>
|
||||
getResourcePolicyTableBadges(resource.policy),
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/shared/Table';
|
||||
import {
|
||||
dedupeResourceBadges,
|
||||
getInfrastructureSystemIdentityBadges,
|
||||
getInfrastructureSystemTitleBadges,
|
||||
getPlatformBadge,
|
||||
getSourceBadge,
|
||||
getUnifiedSourceBadges,
|
||||
|
|
@ -123,7 +123,7 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
getInfrastructureSystemIdentityBadges(resource),
|
||||
);
|
||||
const systemTitleBadges = createMemo(() =>
|
||||
dedupeResourceBadges([...systemBadges(), ...sourceBadges()]),
|
||||
getInfrastructureSystemTitleBadges(systemBadges(), sourceBadges()),
|
||||
);
|
||||
const healthClass = createMemo(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/shared/Table';
|
||||
import {
|
||||
dedupeResourceBadges,
|
||||
getInfrastructureSystemIdentityBadges,
|
||||
getInfrastructureSystemTitleBadges,
|
||||
getPlatformBadge,
|
||||
getSourceBadge,
|
||||
getUnifiedSourceBadges,
|
||||
|
|
@ -137,7 +137,7 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
getInfrastructureSystemIdentityBadges(resource),
|
||||
);
|
||||
const systemTitleBadges = createMemo(() =>
|
||||
dedupeResourceBadges([...systemBadges(), ...sourceBadges()]),
|
||||
getInfrastructureSystemTitleBadges(systemBadges(), sourceBadges()),
|
||||
);
|
||||
const healthClass = createMemo(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -217,11 +217,49 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('Unraid')).toBeInTheDocument();
|
||||
expect(getByText('Unraid 7.1.0')).toBeInTheDocument();
|
||||
});
|
||||
expect(queryByText('Docker')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps versioned platform identity from repeating as a plain source in system titles', async () => {
|
||||
const resources = [
|
||||
makeResource(1, {
|
||||
type: 'agent',
|
||||
displayName: 'pi',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['proxmox', 'agent'],
|
||||
platformData: {
|
||||
sources: ['proxmox', 'agent'],
|
||||
proxmox: {
|
||||
pveVersion: '',
|
||||
},
|
||||
agent: {
|
||||
platform: 'raspbian',
|
||||
osName: 'Proxmox VE',
|
||||
osVersion: '8.3.3',
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const { container, getByText } = render(() => (
|
||||
<UnifiedResourceTable
|
||||
resources={resources}
|
||||
expandedResourceId={null}
|
||||
onExpandedResourceChange={vi.fn()}
|
||||
groupingMode="flat"
|
||||
/>
|
||||
));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('PVE 8.3.3')).toBeInTheDocument();
|
||||
});
|
||||
expect(container.querySelector('[aria-label="System: PVE 8.3.3, Agent"]')).toBeTruthy();
|
||||
expect(container.querySelector('[aria-label="System: PVE 8.3.3, PVE, Agent"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders compact canonical health reasons without replacing host identity', async () => {
|
||||
const resources = [
|
||||
makeResource(1, {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
getInfrastructurePlatformBadges,
|
||||
getInfrastructureSystemIdentityBadges,
|
||||
getInfrastructureSystemIdentitySortLabel,
|
||||
getInfrastructureSystemTitleBadges,
|
||||
getPlatformBadge,
|
||||
getSourceBadge,
|
||||
getTypeBadge,
|
||||
|
|
@ -137,9 +138,9 @@ describe('resourceBadgePresentation', () => {
|
|||
} as Resource['storage'],
|
||||
});
|
||||
|
||||
expect(getInfrastructureSystemIdentityBadges(unraidStorage).map((badge) => badge.label)).toEqual(
|
||||
['Unraid'],
|
||||
);
|
||||
expect(
|
||||
getInfrastructureSystemIdentityBadges(unraidStorage).map((badge) => badge.label),
|
||||
).toEqual(['Unraid']);
|
||||
expect(getInfrastructureSystemIdentitySortLabel(unraidStorage)).toBe('Unraid');
|
||||
|
||||
const pbsDatastore = makeResource({
|
||||
|
|
@ -154,9 +155,9 @@ describe('resourceBadgePresentation', () => {
|
|||
} as Resource['storage'],
|
||||
});
|
||||
|
||||
expect(getInfrastructureSystemIdentityBadges(pbsDatastore).map((badge) => badge.label)).toEqual([
|
||||
'PBS',
|
||||
]);
|
||||
expect(getInfrastructureSystemIdentityBadges(pbsDatastore).map((badge) => badge.label)).toEqual(
|
||||
['PBS'],
|
||||
);
|
||||
});
|
||||
|
||||
it('does not let stale platform sources override explicit agent host profiles', () => {
|
||||
|
|
@ -301,6 +302,35 @@ describe('resourceBadgePresentation', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('does not repeat the unversioned platform source after a versioned system identity', () => {
|
||||
const agentDiscoveredPve = makeResource({
|
||||
type: 'agent',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['proxmox', 'agent'],
|
||||
platformData: {
|
||||
sources: ['proxmox', 'agent'],
|
||||
agent: {
|
||||
platform: 'raspbian',
|
||||
osName: 'Proxmox VE',
|
||||
osVersion: '8.3.3',
|
||||
},
|
||||
proxmox: {
|
||||
pveVersion: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const systemBadges = getInfrastructureSystemIdentityBadges(agentDiscoveredPve);
|
||||
const sourceBadges = getUnifiedSourceBadges(agentDiscoveredPve.sources);
|
||||
|
||||
expect(systemBadges.map((badge) => badge.label)).toEqual(['PVE 8.3.3']);
|
||||
expect(sourceBadges.map((badge) => badge.label)).toEqual(['PVE', 'Agent']);
|
||||
expect(
|
||||
getInfrastructureSystemTitleBadges(systemBadges, sourceBadges).map((badge) => badge.label),
|
||||
).toEqual(['PVE 8.3.3', 'Agent']);
|
||||
});
|
||||
|
||||
it('keeps top-level platform facets ahead of collection method identity', () => {
|
||||
const resource = makeResource({
|
||||
platformType: 'agent',
|
||||
|
|
|
|||
|
|
@ -144,6 +144,20 @@ const withBadgeVersion = (badge: ResourceBadge, version: string): ResourceBadge
|
|||
};
|
||||
};
|
||||
|
||||
const VERSIONED_BADGE_SUFFIX_PATTERN = /\s+v?\d+(?:[._-]\d+)*(?:[-+~:][0-9a-z][0-9a-z._:+~-]*)?$/i;
|
||||
|
||||
const normalizeBadgeIdentityLabel = (value: string): string =>
|
||||
value.trim().toLowerCase().replace(VERSIONED_BADGE_SUFFIX_PATTERN, '').trim();
|
||||
|
||||
const badgeIdentityLabels = (badge: ResourceBadge): string[] =>
|
||||
Array.from(
|
||||
new Set(
|
||||
[badge.label, badge.title]
|
||||
.map((value) => (value ? normalizeBadgeIdentityLabel(value) : ''))
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
const normalizeUnifiedSourceKeys = (sources?: string[] | null): KnownSourcePlatform[] => {
|
||||
if (!sources || sources.length === 0) return [];
|
||||
const normalized = sources
|
||||
|
|
@ -483,13 +497,7 @@ const getSystemSourceVersion = (
|
|||
}
|
||||
case 'vmware-vsphere': {
|
||||
const vmware = getFacetRecord(resource, platformData, 'vmware');
|
||||
return getRecordVersion(
|
||||
vmware,
|
||||
'version',
|
||||
'productVersion',
|
||||
'apiVersion',
|
||||
'vcenterVersion',
|
||||
);
|
||||
return getRecordVersion(vmware, 'version', 'productVersion', 'apiVersion', 'vcenterVersion');
|
||||
}
|
||||
case 'kubernetes': {
|
||||
const kubernetes = getFacetRecord(resource, platformData, 'kubernetes');
|
||||
|
|
@ -500,7 +508,9 @@ const getSystemSourceVersion = (
|
|||
return getRecordVersion(docker, 'runtimeVersion', 'dockerVersion', 'version');
|
||||
}
|
||||
case 'unraid':
|
||||
return getAgentPlatformVersion(resource, source) || getRecordVersion(platformData, 'osVersion');
|
||||
return (
|
||||
getAgentPlatformVersion(resource, source) || getRecordVersion(platformData, 'osVersion')
|
||||
);
|
||||
default:
|
||||
return (
|
||||
getRecordVersion(sourceRecord, 'version', 'osVersion', 'productVersion') ||
|
||||
|
|
@ -627,6 +637,19 @@ export function dedupeResourceBadges(
|
|||
});
|
||||
}
|
||||
|
||||
export function getInfrastructureSystemTitleBadges(
|
||||
systemBadges: ResourceBadge[],
|
||||
sourceBadges: ResourceBadge[],
|
||||
): ResourceBadge[] {
|
||||
const systemIdentities = new Set(systemBadges.flatMap(badgeIdentityLabels));
|
||||
return dedupeResourceBadges([
|
||||
...systemBadges,
|
||||
...sourceBadges.filter((badge) =>
|
||||
badgeIdentityLabels(badge).every((identity) => !systemIdentities.has(identity)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function getContainerRuntimeBadge(
|
||||
platformType?: PlatformType,
|
||||
platformData?: Record<string, unknown> | null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue