From 3c51288aa0973987f7ae55945592a3b8eb62865c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 23 Mar 2026 14:07:02 +0000 Subject: [PATCH] Introduce drawer access surface --- .../subsystems/frontend-primitives.md | 7 +- .../internal/subsystems/unified-resources.md | 55 +++---- .../ResourceDetailDrawerOverviewTab.tsx | 146 +++++++++++------- .../ResourceDetailDrawer.history.test.tsx | 26 +++- ...urceDetailDrawer.identity-runtime.test.tsx | 9 +- ...sourceDetailDrawerOperationalModel.test.ts | 21 ++- .../resourceDetailDrawerOperationalModel.ts | 20 ++- .../useResourceDetailDrawerDerivedState.ts | 115 +++++--------- .../useResourceDetailDrawerState.ts | 3 + .../shared/WebInterfaceUrlField.tsx | 9 +- .../__tests__/WebInterfaceUrlField.test.tsx | 15 ++ .../shared/webInterfaceUrlFieldModel.ts | 2 + 12 files changed, 254 insertions(+), 174 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index fe606bcd4..d2c7dd0e3 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -324,8 +324,11 @@ render shell, `frontend-modern/src/components/shared/useWebInterfaceUrlFieldStat owns metadata fetch/save/remove lifecycle, success/error state, and suggested URL runtime, and `frontend-modern/src/components/shared/webInterfaceUrlFieldModel.ts` owns URL validation, target-label normalization, and suggested-URL presentation -rules. Future web-interface URL work should extend those owners instead of -pushing metadata transport or validation back into the shared shell. +rules. The shared primitive now also supports an embedded mode with a caller- +owned title so feature drawers can place web-interface controls inside a larger +access surface without forking the save/remove/runtime behavior. Future +web-interface URL work should extend those owners instead of pushing metadata +transport or validation back into the shared shell. The shared help icon now follows that same owner split. `frontend-modern/src/components/shared/HelpIcon.tsx` stays the render shell, `frontend-modern/src/components/shared/useHelpIconState.ts` owns open state, diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 04a2ca9fc..9af2601e1 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -424,10 +424,10 @@ with the resource name, status, or primary identity line. That header badge row now also deduplicates identical visible labels, so agent-backed nodes do not repeat `Agent` when both the canonical resource type and a merged source resolve to the same badge text. -The current-state card's `Quick links` row now only promotes runtime-scoped -workloads drill-down routes, so ordinary host drawers do not surface a generic -host-wide `Workloads` jump that reads like default navigation chrome instead of -resource-specific operational context. +Runtime-scoped workloads drill-down routes now live in a dedicated `Access` +disclosure instead of a `Current state` row, so ordinary host drawers do not +surface a generic host-wide `Workloads` jump as default runtime chrome and +first-read status stays separate from the next place a user can go or inspect. That same `Current state` card now only shows `Mode` when the resource carries an actual canonical source mode, so ordinary hosts do not surface an empty or meaningless mode row when no source-type contract is present. @@ -435,26 +435,27 @@ Inside that top card pair, the operational and supporting context rows stay inli instead of sitting in a collapsed `Details` disclosure or nested bordered cards, so the first read stays like one linear sheet rather than a stack of cards inside the overview shell. -Discovery support now also lives inside an overview-only `Analysis` -instead of a peer drawer tab, so supplemental discovery detail stays available -without claiming the same navigation weight as runtime, identity, or -service-specific operational views. -That discovery surface is now a compact support row with a one-line summary and -an on-demand metadata panel, so the actionable web-interface path stays primary -while deeper discovery inspection remains available without reading like a -second peer overview surface. -For ordinary host discovery, that support row now stays even quieter: the -surface is titled `Analysis`, and the collapsed state does not repeat a -baseline `Host analysis via ` summary when the discovery target is -just the same host identity already shown elsewhere in the drawer. -The discovery metadata panel now expands directly under the outer support -disclosure instead of nesting a second bordered card, so the support surface -reads as one flattened reveal instead of a card inside a card. -That discovery support surface now also follows the same shell/runtime split -as the rest of the drawer: `DiscoveryTab.tsx` owns presentation and -disclosures, while `useDiscoveryTabState.ts` owns API fetches, websocket -progress, and note/discovery mutations. -The overview keeps host, service, investigation, and discovery detail as +Discovery support now lives as an `Analysis` reveal inside that same +overview-only `Access` surface instead of a peer drawer tab, so supplemental +inspection stays available without claiming the same navigation weight as +runtime, identity, or service-specific operational views. +That access surface is now a compact support row with a one-line summary, +embedded web-interface controls, scoped runtime links, and an on-demand +analysis panel, so the actionable access path stays primary while deeper +discovery inspection remains available without reading like a second peer +overview surface. +For ordinary host discovery, that analysis entry stays even quieter: the +collapsed `Access` state does not repeat a baseline `Host analysis via +` summary when the discovery target is just the same host identity +already shown elsewhere in the drawer. +The analysis panel now expands directly inside the outer `Access` disclosure +instead of as a second peer support block, so the access surface reads as one +flattened reveal instead of another card group under the overview. +That access-side analysis surface still follows the same shell/runtime split as +the rest of the drawer: `DiscoveryTab.tsx` owns presentation and disclosures, +while `useDiscoveryTabState.ts` owns API fetches, websocket progress, and +note/discovery mutations. +The overview keeps access, host, service, and investigation detail as collapsed sibling disclosures under the primary card pair, so the drawer keeps the top-level shape to current-state/identity plus `Change history` before any secondary operational context appears. @@ -480,8 +481,8 @@ That host-details section now reads as a simple vertical stack of detail cards instead of a wrapped card grid, so the opened state stays linear instead of feeling like a second dashboard. Within that summary shell, current-state facts now stay operational: only -distinct platform IDs, quick links, and platform-signal badges remain with -runtime status, while aliases, IPs, and tags live only under the dedicated +distinct platform IDs and platform-signal badges remain with runtime status, +while scoped links move to `Access` and aliases, IPs, and tags live only under the dedicated `Identity` card. That keeps first read status-first while still preserving canonical identity metadata on the same top-level summary surface instead of mixing identity @@ -498,7 +499,7 @@ breakdowns stay available without displacing the common runtime and identity hierarchy on first read. The drawer’s secondary support sections now share the same responsive flex-wrap card-group pattern used by the workloads drawer, so change history, -service details, host details, investigation context, and discovery context +access, service, host, and investigation context read side by side on wider screens instead of as a single full-width stack. Host uses that same flex-wrap pattern inside the disclosure for the system, hardware, storage, and network cards, so the drawer matches the diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx index 031ca77bd..55b818d61 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx @@ -167,24 +167,6 @@ export const ResourceDetailDrawerOverviewTab: Component - 0}> -
- Quick links -
- - {(link) => ( - - {link.compactLabel} - - )} - -
-
-
@@ -914,6 +896,95 @@ export const ResourceDetailDrawerOverviewTab: Component + + drawer.setShowAccessContext((value) => !value)} + showLabel="Show access" + hideLabel="Hide access" + class="h-full" + contentClass="mt-3 space-y-3" + dataTestId="resource-access-section" + > + 0}> +
+
+ Links +
+
+ + {(link) => ( + + {link.compactLabel} + + )} + +
+
+
+ + + {(config) => ( +
+ + +
+
+
+
+ Analysis +
+ +
+ {drawer.discoveryContextSummary()} +
+
+
+ +
+ + + +
+ {getDiscoveryLoadingState().text} +
+ } + > + +
+
+
+
+ )} +
+
+
+ - - - {(config) => ( -
- - - drawer.setShowDiscoveryContext((value) => !value)} - showLabel="Open analysis" - hideLabel="Hide analysis" - class="h-full" - dataTestId="resource-discovery-context" - > - -
- {getDiscoveryLoadingState().text} -
- } - > - -
-
-
- )} -
); }; diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index 5f9f69b62..c75b31ad2 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -8,6 +8,7 @@ import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/Reso import resourceDetailDrawerHistoryStateSource from '@/components/Infrastructure/useResourceDetailDrawerHistoryState.ts?raw'; import resourceDetailDrawerDerivedStateSource from '@/components/Infrastructure/useResourceDetailDrawerDerivedState.ts?raw'; import resourceDetailDrawerDiscoveryModelSource from '@/components/Infrastructure/resourceDetailDiscoveryModel.ts?raw'; +import resourceDetailDrawerIdentityModelSource from '@/components/Infrastructure/resourceDetailDrawerIdentityModel.ts?raw'; import resourceDetailDrawerOperationalModelSource from '@/components/Infrastructure/resourceDetailDrawerOperationalModel.ts?raw'; import resourceDetailDrawerServiceModelSource from '@/components/Infrastructure/resourceDetailDrawerServiceModel.ts?raw'; import resourceDetailDrawerDockerActionsStateSource from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts?raw'; @@ -150,13 +151,27 @@ describe('ResourceDetailDrawer change history section', () => { expect(resourceDetailDrawerDerivedStateSource).toContain( "from './resourceDetailDrawerServiceModel'", ); + expect(resourceDetailDrawerDerivedStateSource).toContain( + "from './resourceDetailDrawerIdentityModel'", + ); expect(resourceDetailDrawerDiscoveryModelSource).toContain('export const toDiscoveryConfig'); + expect(resourceDetailDrawerIdentityModelSource).toContain( + 'export const buildResourceIdentityView', + ); + expect(resourceDetailDrawerIdentityModelSource).toContain( + 'export const buildDiscoveryContextSummary', + ); + expect(resourceDetailDrawerIdentityModelSource).toContain( + 'export const buildResourceDebugBundle', + ); expect(resourceDetailDrawerDerivedStateSource).not.toContain('buildWorkloadsHref'); expect(resourceDetailDrawerDerivedStateSource).not.toContain('buildServiceDetailLinks'); expect(resourceDetailDrawerDerivedStateSource).not.toContain('const supportedBadge ='); expect(resourceDetailDrawerDerivedStateSource).not.toContain( 'const links: Array<{ href: string;', ); + expect(resourceDetailDrawerDerivedStateSource).not.toContain('ALIAS_COLLAPSE_THRESHOLD'); + expect(resourceDetailDrawerDerivedStateSource).not.toContain('formatIdentifierLabel'); expect(resourceDetailDrawerOperationalModelSource).toContain( 'export const buildKubernetesCapabilityBadges', ); @@ -338,14 +353,15 @@ describe('ResourceDetailDrawer change history section', () => { expect(screen.queryByText('Container Updates')).toBeNull(); expect(screen.queryByText('Check Updates')).toBeNull(); expect(screen.queryByText('Show update controls')).toBeNull(); - expect(screen.getByText('Analysis')).toBeInTheDocument(); + expect(screen.getByText('Access')).toBeInTheDocument(); + expect(screen.queryByText('Analysis')).toBeNull(); expect( screen.queryByText('Supporting metadata only. The web interface path above stays primary.'), ).toBeNull(); - expect(screen.getByRole('button', { name: 'Open analysis' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Show access' })).toBeInTheDocument(); expect( screen - .getByTestId('resource-discovery-context') + .getByTestId('resource-access-section') .querySelector('.mt-3.rounded.border.border-border.bg-surface.p-2\\.5'), ).toBeNull(); expect(screen.queryByText('Details')).toBeNull(); @@ -376,6 +392,10 @@ describe('ResourceDetailDrawer change history section', () => { expect(screen.queryByText('Relationships 1')).toBeNull(); expect(screen.queryByText('AI')).toBeNull(); + fireEvent.click(screen.getByRole('button', { name: 'Show access' })); + expect(screen.getByText('Analysis')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Open analysis' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Show context' })); await screen.findByText('AI'); expect( diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx index c7ba15644..ff1b819de 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx @@ -143,18 +143,23 @@ describe('ResourceDetailDrawer runtime and identity cards', () => { ); expect(queryByRole('button', { name: 'Analysis' })).toBeNull(); - expect(getByText('Analysis')).toBeInTheDocument(); + expect(getByText('Access')).toBeInTheDocument(); + expect(queryByText('Analysis')).toBeNull(); expect(queryByText('Host analysis via host-1')).toBeNull(); expect( queryByText('Supporting metadata only. The web interface path above stays primary.'), ).toBeNull(); expect(queryByTestId('discovery-tab')).toBeNull(); expect( - getByTestId('resource-discovery-context').querySelector( + getByTestId('resource-access-section').querySelector( '.mt-3.rounded.border.border-border.bg-surface.p-2\\.5', ), ).toBeNull(); + expect(getByRole('button', { name: 'Show access' })).toBeInTheDocument(); + + fireEvent.click(getByRole('button', { name: 'Show access' })); + expect(getByText('Analysis')).toBeInTheDocument(); expect(getByRole('button', { name: 'Open analysis' })).toBeInTheDocument(); fireEvent.click(getByRole('button', { name: 'Open analysis' })); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerOperationalModel.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerOperationalModel.test.ts index f26690710..5fd56a9db 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerOperationalModel.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerOperationalModel.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + buildAccessSummary, buildHostDetailCards, buildHostDetailSummary, buildKubernetesCapabilityBadges, @@ -118,7 +119,7 @@ describe('resourceDetailDrawerOperationalModel', () => { expect(buildHostDetailSummary(cards)).toBe( 'System, Hardware, Storage, Network, Disks, and Temperatures', ); - expect(hasRuntimeOperationalContext([], [])).toBe(false); + expect(hasRuntimeOperationalContext([])).toBe(false); }); it('builds canonical related links from workloads and service detail surfaces', () => { @@ -153,4 +154,22 @@ describe('resourceDetailDrawerOperationalModel', () => { ), ).toEqual([]); }); + + it('builds access summaries from web access and scoped links', () => { + expect(buildAccessSummary({ hasWebInterface: true, links: [] })).toBe('Web interface'); + expect( + buildAccessSummary({ + hasWebInterface: true, + links: [ + { + href: '/workloads?type=app-container&agent=agent-1', + label: 'Open in Workloads', + compactLabel: 'Workloads', + ariaLabel: 'Open related workloads for Host 1', + }, + ], + }), + ).toBe('Web interface · 1 link'); + expect(buildAccessSummary({ hasWebInterface: false, links: [] })).toBeNull(); + }); }); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts index 4e7a8f2ca..06ab7578d 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts @@ -176,6 +176,23 @@ export const buildHostDetailSummary = (hostDetailCards: string[]): string | null return categories; }; +export const buildAccessSummary = (options: { + hasWebInterface: boolean; + links: ResourceDetailDrawerOperationalLink[]; +}): string | null => { + const parts: string[] = []; + + if (options.hasWebInterface) { + parts.push('Web interface'); + } + + if (options.links.length > 0) { + parts.push(`${options.links.length} link${options.links.length === 1 ? '' : 's'}`); + } + + return parts.join(' · ') || null; +}; + export const buildRelatedLinks = ( resource: Resource, displayName: string, @@ -204,5 +221,4 @@ export const buildRelatedLinks = ( export const hasRuntimeOperationalContext = ( badges: ResourceDetailDrawerOperationalBadge[], - links: ResourceDetailDrawerOperationalLink[], -): boolean => badges.length > 0 || links.length > 0; +): boolean => badges.length > 0; diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts index 32bcf637e..325c84941 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts @@ -11,8 +11,6 @@ import { getUnifiedSourceBadges, } from '@/utils/resourceBadgePresentation'; import { - getPrimaryResourceIdentityRows, - getResourceIdentityAliases, getPreferredResourceClusterName, getPreferredResourceDisplayName, } from '@/utils/resourceIdentity'; @@ -26,7 +24,6 @@ import { } from '@/utils/resourcePolicyPresentation'; import type { ResourceIntelligence } from '@/types/aiIntelligence'; import { - ALIAS_COLLAPSE_THRESHOLD, buildTemperatureRows, toAgentFromResource, toNodeFromProxmox, @@ -36,7 +33,6 @@ import { type PlatformData, } from '@/components/Infrastructure/resourceDetailMappers'; import { toDiscoveryConfig } from '@/components/Infrastructure/resourceDetailDiscoveryModel'; -import { formatIdentifierLabel } from '@/utils/textPresentation'; import { buildPbsVisibleJobBreakdown, buildPmgVisibleMailBreakdown, @@ -46,6 +42,7 @@ import { getServiceDetailsSummary, } from './resourceDetailDrawerServiceModel'; import { + buildAccessSummary, buildHostDetailCards, buildHostDetailSummary, buildKubernetesCapabilityBadges, @@ -53,6 +50,13 @@ import { buildSourceSummary, hasRuntimeOperationalContext as buildHasRuntimeOperationalContext, } from './resourceDetailDrawerOperationalModel'; +import { + buildDiscoveryContextSummary, + buildIdentityMatchInfo, + buildResourceDebugBundle, + buildResourceIdentityView, + buildSourceSections, +} from './resourceDetailDrawerIdentityModel'; type DrawerTab = 'overview' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; @@ -212,39 +216,16 @@ export const useResourceDetailDrawerDerivedState = ( ); const sourceSummary = createMemo(() => buildSourceSummary(mergedSources(), sourceStatus())); - const identityAliasValues = createMemo(() => getResourceIdentityAliases(resource)); - const identityIpValues = createMemo(() => resource.identity?.ips ?? []); - const primaryIdentityRows = createMemo(() => getPrimaryResourceIdentityRows(resource)); - const identityCardHasRichData = createMemo( - () => - primaryIdentityRows().length > 0 || - identityIpValues().length > 0 || - (resource.tags?.length || 0) > 0 || - identityAliasValues().length > 0, - ); - const aliasPreviewValues = createMemo(() => - identityAliasValues().slice(0, ALIAS_COLLAPSE_THRESHOLD), - ); - const hasAliasOverflow = createMemo( - () => identityAliasValues().length > ALIAS_COLLAPSE_THRESHOLD, - ); + const identityView = createMemo(() => buildResourceIdentityView(resource)); + const identityAliasValues = createMemo(() => identityView().identityAliasValues); + const identityIpValues = createMemo(() => identityView().identityIpValues); + const primaryIdentityRows = createMemo(() => identityView().primaryIdentityRows); + const identityCardHasRichData = createMemo(() => identityView().identityCardHasRichData); + const aliasPreviewValues = createMemo(() => identityView().aliasPreviewValues); + const hasAliasOverflow = createMemo(() => identityView().hasAliasOverflow); const hasMergedSources = createMemo(() => mergedSources().length > 1); const discoveryConfig = createMemo(() => toDiscoveryConfig(resource)); - const discoveryContextSummary = createMemo(() => { - const config = discoveryConfig(); - if (!config) return null; - - if (config.resourceType === 'agent') { - return null; - } - - const discoveryMode = - config.resourceType === 'agent' - ? 'Host analysis' - : `${formatIdentifierLabel(config.resourceType)} analysis`; - - return config.hostname ? `${discoveryMode} via ${config.hostname}` : discoveryMode; - }); + const discoveryContextSummary = createMemo(() => buildDiscoveryContextSummary(discoveryConfig())); const hostDetailCards = createMemo(() => buildHostDetailCards({ @@ -271,53 +252,29 @@ export const useResourceDetailDrawerDerivedState = ( }); const relatedLinks = createMemo(() => buildRelatedLinks(resource, displayName())); + const accessSummary = createMemo(() => + buildAccessSummary({ + hasWebInterface: Boolean(discoveryConfig()), + links: relatedLinks(), + }), + ); + const hasAccessContext = createMemo( + () => Boolean(discoveryConfig()) || relatedLinks().length > 0, + ); const hasRuntimeOperationalContext = createMemo( - () => buildHasRuntimeOperationalContext(kubernetesCapabilityBadges(), relatedLinks()), + () => buildHasRuntimeOperationalContext(kubernetesCapabilityBadges()), ); - const sourceSections = createMemo(() => { - const data = platformData(); - if (!data) { - return [] as Array<{ id: string; label: string; payload: unknown }>; - } - const sections = [ - { id: 'proxmox', label: 'Proxmox', payload: data.proxmox }, - { id: 'agent', label: 'Agent', payload: data.agent }, - { id: 'docker', label: 'Containers', payload: data.docker }, - { id: 'pbs', label: 'PBS', payload: data.pbs }, - { id: 'pmg', label: 'PMG', payload: data.pmg }, - { id: 'kubernetes', label: 'Kubernetes', payload: data.kubernetes }, - { id: 'metrics', label: 'Metrics', payload: data.metrics }, - ]; - return sections.filter((section) => section.payload !== undefined); - }); - const identityMatchInfo = createMemo(() => { - const data = platformData(); - return ( - data?.identityMatch ?? - data?.matchResults ?? - data?.matchCandidates ?? - data?.matches ?? - undefined - ); - }); - const debugBundle = createMemo(() => ({ - resource, - identity: { - resourceIdentity: resource.identity, - matchInfo: identityMatchInfo(), - }, - sources: { + const sourceSections = createMemo(() => buildSourceSections(platformData())); + const identityMatchInfo = createMemo(() => buildIdentityMatchInfo(platformData())); + const debugBundle = createMemo(() => + buildResourceDebugBundle({ + resource, + platformData: platformData(), sourceStatus: sourceStatus(), - proxmox: platformData()?.proxmox, - agent: platformData()?.agent, - docker: platformData()?.docker, - pbs: platformData()?.pbs, - pmg: platformData()?.pmg, - kubernetes: platformData()?.kubernetes, - metrics: platformData()?.metrics, - }, - })); + identityMatchInfo: identityMatchInfo(), + }), + ); const debugJson = createMemo(() => JSON.stringify(debugBundle(), null, 2)); const tabs = createMemo(() => { @@ -402,6 +359,8 @@ export const useResourceDetailDrawerDerivedState = ( hasMergedSources, discoveryConfig, discoveryContextSummary, + accessSummary, + hasAccessContext, hasHostDetails, hostDetailSummary, hasServiceDetails, diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts index c207506d2..b203ee9a8 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts @@ -21,6 +21,7 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta const [debugEnabled] = createLocalStorageBooleanSignal(STORAGE_KEYS.DEBUG_MODE, false); const [copied, setCopied] = createSignal(false); const [showReportModal, setShowReportModal] = createSignal(false); + const [showAccessContext, setShowAccessContext] = createSignal(false); const [showInvestigationContext, setShowInvestigationContext] = createSignal(false); const [showCorrelationContext, setShowCorrelationContext] = createSignal(false); const [showDiscoveryContext, setShowDiscoveryContext] = createSignal(false); @@ -86,6 +87,8 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta copied, showReportModal, setShowReportModal, + showAccessContext, + setShowAccessContext, showInvestigationContext, setShowInvestigationContext, showCorrelationContext, diff --git a/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx b/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx index 2d400c1b0..04c711a41 100644 --- a/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx +++ b/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx @@ -6,12 +6,17 @@ export type { WebInterfaceUrlFieldProps } from './webInterfaceUrlFieldModel'; export const WebInterfaceUrlField: Component = (props) => { const state = useWebInterfaceUrlFieldState(props); + const title = () => props.title?.trim() || 'Web Interface URL'; + const rootClass = () => + props.embedded + ? props.class ?? '' + : `rounded border border-border bg-surface p-3 shadow-sm ${props.class ?? ''}`.trim(); return ( -
+
- Web Interface URL + {title()}
{ expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument(); }); + it('supports embedded rendering with a custom title', async () => { + const { container } = render(() => ( + + )); + + expect(await screen.findByText('Web interface')).toBeInTheDocument(); + expect(container.querySelector('.shadow-sm')).toBeNull(); + }); + it('saves a host URL through metadata API', async () => { render(() => ( void; suggestedUrl?: string; @@ -11,6 +12,7 @@ export interface WebInterfaceUrlFieldProps { suggestedUrlReasonTitle?: string; suggestedUrlDiagnostic?: string; discoveryLoading?: boolean; + embedded?: boolean; class?: string; }