Split infrastructure drawer render owners

This commit is contained in:
rcourtman 2026-03-22 23:31:02 +00:00
parent a64b252f2d
commit 6f3741dbfb
10 changed files with 1421 additions and 1460 deletions

View file

@ -3105,6 +3105,9 @@
"frontend-modern/src/components/Discovery/useDiscoveryTabState.ts",
"frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx",
"frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts",
"frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
@ -3179,6 +3182,9 @@
"frontend-modern/src/components/Discovery/useDiscoveryTabState.ts",
"frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx",
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx",
"frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts",
"frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",

View file

@ -44,13 +44,16 @@ cross-source deduplication.
22. `internal/unifiedresources/privacy.go`
23. `internal/unifiedresources/actions.go`
24. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx`
25. `frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx`
26. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts`
27. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
28. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx`
29. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts`
30. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
31. `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
25. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx`
26. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx`
27. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx`
28. `frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx`
29. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts`
30. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
31. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx`
32. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts`
33. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
34. `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
## Shared Boundaries
@ -77,7 +80,7 @@ assembly branch.
4. Add metrics-target normalization or synthetic metrics support through `internal/unifiedresources/metrics_targets.go` and `internal/unifiedresources/metrics.go`
5. Add platform registry, resolution, or host-dedup behavior through `internal/unifiedresources/registry.go`, `internal/unifiedresources/resolve.go`, `internal/unifiedresources/resolved_host_set.go`, `internal/unifiedresources/snapshot_source_filter.go`, `internal/unifiedresources/store.go`, `internal/unifiedresources/kubernetes_capabilities.go`, and `internal/unifiedresources/pbs_rollups.go`
6. Add canonical governed name-resolution or policy-aware resource lookup behavior through `internal/unifiedresources/resolve.go` and `internal/unifiedresources/resolve_context.go`
7. Add or change resource drawer timeline/facet presentation through `frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx`, `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts`, `frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx`, `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`, and the governed `internal/api/resources.go` facet/timeline contract together
7. Add or change resource drawer timeline/facet presentation through `frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx`, `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx`, `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx`, `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts`, `frontend-modern/src/components/Infrastructure/ResourceFacetSummary.tsx`, `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`, and the governed `internal/api/resources.go` facet/timeline contract together
8. Add or change discovery-support runtime under the resource drawer through `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` for shell/presentation ownership and `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts` for fetch, websocket-progress, and notes-mutation ownership
## Forbidden Paths
@ -131,6 +134,11 @@ hints.
Those same relationship changes now summarize the actual edge(s) in `from` and
`to`, so the canonical timeline keeps the relationship transition readable without
needing the drawer to reconstruct an edge summary from raw endpoints.
The infrastructure resource drawer now follows the explicit shell/state/render
split used elsewhere in v6: `ResourceDetailDrawer.tsx` owns composition,
`useResourceDetailDrawerState.ts` owns runtime state and fetch orchestration,
and the overview/debug render-heavy surfaces live in dedicated drawer-local
owners instead of staying inline in the shell.
The backend AI and Patrol context renderers now derive their canonical change
kind, source type, source adapter, actor, reason, and related-resource
fragments from `internal/unifiedresources/change_presentation.go`, so the

View file

@ -0,0 +1,93 @@
import { For, Show } from 'solid-js';
import type { Component } from 'solid-js';
import type { Resource } from '@/types/resource';
import { formatRelativeTime } from '@/utils/format';
import type { UseResourceDetailDrawerStateResult } from './useResourceDetailDrawerState';
interface ResourceDetailDrawerDebugTabProps {
resource: Resource;
drawer: UseResourceDetailDrawerStateResult;
}
export const ResourceDetailDrawerDebugTab: Component<ResourceDetailDrawerDebugTabProps> = (
props,
) => {
const { drawer } = props;
return (
<div class="space-y-3">
<div class="flex items-center justify-between gap-3">
<div class="text-xs text-muted">
Debug mode is enabled via localStorage (<code>pulse_debug_mode</code>).
</div>
<button
type="button"
onClick={drawer.handleCopyJson}
class="rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
>
{drawer.copied() ? 'Copied' : 'Copy JSON'}
</button>
</div>
<div class="mt-3 space-y-4">
<div>
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content mb-2">
Unified Resource
</div>
<pre class="max-h-[280px] overflow-auto rounded-md bg-base p-3 text-[11px] text-base-content">
{JSON.stringify(props.resource, null, 2)}
</pre>
</div>
<div>
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content mb-2">
Identity Matching
</div>
<pre class="max-h-[220px] overflow-auto rounded-md bg-base p-3 text-[11px] text-base-content">
{JSON.stringify(
{
identity: props.resource.identity,
matchInfo: drawer.identityMatchInfo(),
},
null,
2,
)}
</pre>
</div>
<div>
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content mb-2">
Sources
</div>
<div class="space-y-2">
<For each={drawer.sourceSections()}>
{(section) => {
const status = drawer.sourceStatus()[section.id];
const lastSeenText = formatRelativeTime(status?.lastSeen);
return (
<details class="rounded-md border border-border bg-surface p-3">
<summary class="flex cursor-pointer list-none items-center justify-between text-sm font-medium text-base-content">
<span>{section.label}</span>
<span class="text-[11px] text-muted">
{status?.status ?? 'unknown'}
{lastSeenText ? `${lastSeenText}` : ''}
</span>
</summary>
<Show when={status?.error}>
<div class="mt-2 text-[11px] text-amber-600 dark:text-amber-300">
{status?.error}
</div>
</Show>
<pre class="mt-3 max-h-[220px] overflow-auto rounded-md bg-base p-3 text-[11px] text-base-content">
{JSON.stringify(section.payload ?? {}, null, 2)}
</pre>
</details>
);
}}
</For>
</div>
</div>
</div>
</div>
);
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
import { Show } from 'solid-js';
import type { Component, JSX } from 'solid-js';
interface ResourceDetailDrawerSupportDisclosureProps {
title: string;
summary?: string | null;
expanded: boolean;
onToggle: () => void;
showLabel: string;
hideLabel: string;
children: JSX.Element;
class?: string;
buttonClass?: string;
contentClass?: string;
dataTestId?: string;
}
export const ResourceDetailDrawerSupportDisclosure: Component<
ResourceDetailDrawerSupportDisclosureProps
> = (props) => {
const summary = () => props.summary?.trim() ?? '';
return (
<div
data-testid={props.dataTestId}
class={props.class ?? 'rounded border border-dashed border-border bg-surface-hover p-3'}
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">
{props.title}
</div>
<Show when={summary()}>
<div class="mt-1 text-[10px] text-base-content">{summary()}</div>
</Show>
</div>
<button
type="button"
onClick={props.onToggle}
class={
props.buttonClass ??
'inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base'
}
>
{props.expanded ? props.hideLabel : props.showLabel}
</button>
</div>
<Show when={props.expanded}>
<div class={props.contentClass ?? 'mt-3'}>{props.children}</div>
</Show>
</div>
);
};

View file

@ -3,7 +3,8 @@ import { fireEvent, render, screen, within } from '@solidjs/testing-library';
import discoveryTabSource from '@/components/Discovery/DiscoveryTab.tsx?raw';
import discoveryTabStateSource from '@/components/Discovery/useDiscoveryTabState.ts?raw';
import resourceDetailDrawerSource from '@/components/Infrastructure/ResourceDetailDrawer.tsx?raw';
import resourceDetailDrawerShellSource from '@/components/Infrastructure/ResourceDetailDrawer.tsx?raw';
import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw';
import type { Resource } from '@/types/resource';
import { ResourceDetailDrawer } from '@/components/Infrastructure/ResourceDetailDrawer';
@ -109,10 +110,15 @@ describe('ResourceDetailDrawer change history section', () => {
expect(discoveryTabSource).not.toContain('getConnectedAgents(');
expect(discoveryTabSource).not.toContain('triggerDiscovery(');
expect(discoveryTabSource).not.toContain('updateDiscoveryNotes(');
expect(resourceDetailDrawerSource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerSource).not.toContain(
expect(resourceDetailDrawerOverviewSource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerOverviewSource).not.toContain(
"from '@/components/Dashboard/TagBadges'",
);
expect(resourceDetailDrawerShellSource).toContain(
"from './ResourceDetailDrawerOverviewTab'",
);
expect(resourceDetailDrawerShellSource).toContain("from './ResourceDetailDrawerDebugTab'");
expect(resourceDetailDrawerShellSource).not.toContain('Change history');
});
it('keeps compact timeline summary chips in overview while showing one embedded change history section', async () => {

View file

@ -181,6 +181,7 @@ export interface UseResourceDetailDrawerStateResult {
>;
hasRuntimeOperationalContext: Accessor<boolean>;
sourceSections: Accessor<Array<{ id: string; label: string; payload: unknown }>>;
sourceStatus: Accessor<NonNullable<PlatformData['sourceStatus']>>;
identityMatchInfo: Accessor<unknown>;
debugJson: Accessor<string>;
tabs: Accessor<Array<{ id: DrawerTab; label: string }>>;
@ -910,6 +911,7 @@ export const useResourceDetailDrawerState = (
relatedLinks,
hasRuntimeOperationalContext,
sourceSections,
sourceStatus,
identityMatchInfo,
debugJson,
tabs,

View file

@ -5,7 +5,7 @@ import monitoredSystemLimitWarningBannerSource from '@/components/shared/Monitor
import selectionCardGroupSource from '@/components/shared/SelectionCardGroup.tsx?raw';
import tagBadgesSource from '@/components/shared/TagBadges.tsx?raw';
import guestRowSource from '@/components/Dashboard/GuestRow.tsx?raw';
import resourceDetailDrawerSource from '@/components/Infrastructure/ResourceDetailDrawer.tsx?raw';
import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw';
import aiSettingsDialogsSource from '@/components/Settings/AISettingsDialogs.tsx?raw';
import generalSettingsPanelSource from '@/components/Settings/GeneralSettingsPanel.tsx?raw';
import proxmoxSettingsPanelSource from '@/components/Settings/ProxmoxSettingsPanel.tsx?raw';
@ -100,8 +100,8 @@ describe('shared primitive guardrails', () => {
expect(tagBadgesSource).toContain("from '@/components/shared/Tooltip'");
expect(guestRowSource).toContain("from '@/components/shared/TagBadges'");
expect(guestRowSource).not.toContain("from './TagBadges'");
expect(resourceDetailDrawerSource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerSource).not.toContain(
expect(resourceDetailDrawerOverviewSource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerOverviewSource).not.toContain(
"from '@/components/Dashboard/TagBadges'",
);
});

View file

@ -243,6 +243,8 @@ import licensePresentationSource from '@/utils/licensePresentation.ts?raw';
import securityScorePresentationSource from '@/utils/securityScorePresentation.ts?raw';
import securityAuthPresentationSource from '@/utils/securityAuthPresentation.ts?raw';
import resourceDetailDrawerShellSource from '@/components/Infrastructure/ResourceDetailDrawer.tsx?raw';
import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw';
import resourceDetailDrawerDebugSource from '@/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx?raw';
import infrastructureSummarySource from '@/components/Infrastructure/InfrastructureSummary.tsx?raw';
import resourceDetailMappersSource from '@/components/Infrastructure/resourceDetailMappers.ts?raw';
import resourceDetailDrawerStateSource from '@/components/Infrastructure/useResourceDetailDrawerState.ts?raw';
@ -518,6 +520,8 @@ const aiSettingsSource = [
const resourceDetailDrawerSource = [
resourceDetailDrawerShellSource,
resourceDetailDrawerOverviewSource,
resourceDetailDrawerDebugSource,
resourceDetailDrawerStateSource,
].join('\n');
@ -861,10 +865,14 @@ describe('frontend resource type boundaries', () => {
expect(guestRowCellsSource).toContain('useTooltip');
expect(guestRowSource).not.toContain('buildGuestId');
expect(tagBadgesSource).toContain("from '@/components/shared/Tooltip'");
expect(resourceDetailDrawerShellSource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerShellSource).not.toContain(
expect(resourceDetailDrawerOverviewSource).toContain("from '@/components/shared/TagBadges'");
expect(resourceDetailDrawerOverviewSource).not.toContain(
"from '@/components/Dashboard/TagBadges'",
);
expect(resourceDetailDrawerShellSource).toContain(
"from './ResourceDetailDrawerOverviewTab'",
);
expect(resourceDetailDrawerShellSource).toContain("from './ResourceDetailDrawerDebugTab'");
expect(guestDrawerSource).toContain('useGuestDrawerState');
expect(guestDrawerSource).toContain('GuestDrawerOverview');
expect(guestDrawerStateSource).toContain('getCanonicalWorkloadId');