Remove redundant drawer summary heading

This commit is contained in:
rcourtman
2026-03-23 12:02:54 +00:00
parent 37b198299e
commit 0c8dd92725
3 changed files with 186 additions and 189 deletions
@@ -410,30 +410,31 @@ detail behind the reveal.
Change-related summary badges now belong to the `Change history` section
instead of the `Runtime` card, so current-state facts and timeline context do
not compete for the same ownership on first read.
The overview now begins with one primary `Summary` shell instead of separate
peer `Runtime` and `Identity` cards, so current state and canonical identity
read as one first-screen answer rather than two adjacent mini-surfaces.
That summary now uses the same shared `Card` primitive as the workload drawers,
The overview now begins directly with paired `Current state` and `Identity`
cards instead of a wrapper section title or separate peer runtime shell, so
current state and canonical identity read as one first-screen answer rather
than layered labels around adjacent mini-surfaces.
Those cards now use the same shared `Card` primitive as the workload drawers,
with a responsive two-column grid on wider screens, so the first read stays
compact while each side still has a consistent bounded card.
The drawer header now stays focused on canonical identity and source/type
badges only, while workload/service drill-down links and Kubernetes platform
signals live with the summary shell, so the top strip does not compete with
the resource name, status, or primary identity line.
signals live with the current-state card, so the top strip does not compete
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 summary shell's `Quick links` row now only promotes runtime-scoped
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.
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.
Inside that summary, the operational and supporting context rows stay inline
Inside that top card pair, the operational and supporting context rows stay inline
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 summary.
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
@@ -454,9 +455,9 @@ 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
collapsed sibling disclosures under the primary summary, so the drawer keeps
the top-level shape to `Summary` plus `Change history` before any secondary
operational context appears.
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.
The `Change history` filter controls now stack vertically instead of using a
paired filter grid, so the timeline reads like a simple inspection surface
rather than a two-field form on wider screens.
@@ -88,176 +88,154 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
return (
<div class="space-y-3">
<div data-testid="resource-summary-section">
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">Summary</div>
<div class="mt-3 grid gap-3 sm:grid-cols-2">
<Card data-testid="resource-current-state-section" padding="sm" class="h-full shadow-sm">
<div class="mb-2 text-[10px] font-medium uppercase tracking-wide text-base-content">
Current state
<div data-testid="resource-summary-section" class="grid gap-3 sm:grid-cols-2">
<Card data-testid="resource-current-state-section" padding="sm" class="h-full shadow-sm">
<div class="mb-2 text-[10px] font-medium uppercase tracking-wide text-base-content">
Current state
</div>
<div class="space-y-1.5 text-[11px]">
<div class="flex items-center justify-between gap-2">
<span class="text-muted">State</span>
<span class="font-medium text-base-content capitalize">
{resource.status || 'unknown'}
</span>
</div>
<div class="space-y-1.5 text-[11px]">
<Show when={resource.uptime}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">State</span>
<span class="font-medium text-base-content capitalize">
{resource.status || 'unknown'}
<span class="text-muted">Uptime</span>
<span class="font-medium text-base-content">
{formatUptime(resource.uptime ?? 0)}
</span>
</div>
<Show when={resource.uptime}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Uptime</span>
<span class="font-medium text-base-content">
{formatUptime(resource.uptime ?? 0)}
</span>
</div>
</Show>
<Show when={resource.lastSeen}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Last Seen</span>
<span class="font-medium text-base-content" title={drawer.lastSeenAbsolute()}>
{drawer.lastSeen() || '—'}
</span>
</div>
</Show>
<Show when={drawer.sourceSummary()}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Sources</span>
<span
class={`font-medium ${drawer.sourceSummary()!.className}`}
title={drawer.sourceSummary()!.title}
>
{drawer.sourceSummary()!.label}
</span>
</div>
</Show>
<Show when={(resource.alerts?.length || 0) > 0}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Alerts</span>
<span class="font-medium text-amber-600 dark:text-amber-400">
{formatInteger(resource.alerts?.length)}
</span>
</div>
</Show>
<Show when={showPlatformId && !drawer.hasRuntimeOperationalContext()}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Platform ID</span>
<span class="font-medium text-base-content truncate" title={resource.platformId}>
{resource.platformId}
</span>
</div>
</Show>
<Show when={drawer.hasRuntimeOperationalContext()}>
<div class="mt-2 space-y-1.5">
<Show when={showPlatformId}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Platform ID</span>
<span class="font-medium text-base-content truncate" title={resource.platformId}>
{resource.platformId}
</span>
</div>
</Show>
<Show when={drawer.kubernetesCapabilityBadges().length > 0}>
<div class="flex flex-col gap-1">
<span class="text-muted">Platform signals</span>
<div class="flex flex-wrap gap-1">
<For each={drawer.kubernetesCapabilityBadges()}>
{(badge) => (
<span class={badge.classes} title={badge.title}>
{badge.label}
</span>
)}
</For>
</div>
</div>
</Show>
<Show when={drawer.relatedLinks().length > 0}>
<div class="flex flex-col gap-1">
<span class="text-muted">Quick links</span>
<div class="flex flex-wrap gap-2">
<For each={drawer.relatedLinks()}>
{(link) => (
<a
href={link.href}
aria-label={link.ariaLabel}
class="inline-flex items-center rounded border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-900"
>
{link.compactLabel}
</a>
)}
</For>
</div>
</div>
</Show>
</div>
</Show>
</div>
</Card>
<Card data-testid="resource-identity-section" padding="sm" class="h-full shadow-sm">
<div class="mb-2 text-[10px] font-medium uppercase tracking-wide text-base-content">
Identity
</div>
<div class="space-y-1.5 text-[11px]">
<For each={drawer.primaryIdentityRows()}>
{(row) => (
</Show>
<Show when={resource.lastSeen}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Last Seen</span>
<span class="font-medium text-base-content" title={drawer.lastSeenAbsolute()}>
{drawer.lastSeen() || '—'}
</span>
</div>
</Show>
<Show when={drawer.sourceSummary()}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Sources</span>
<span
class={`font-medium ${drawer.sourceSummary()!.className}`}
title={drawer.sourceSummary()!.title}
>
{drawer.sourceSummary()!.label}
</span>
</div>
</Show>
<Show when={(resource.alerts?.length || 0) > 0}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Alerts</span>
<span class="font-medium text-amber-600 dark:text-amber-400">
{formatInteger(resource.alerts?.length)}
</span>
</div>
</Show>
<Show when={showPlatformId && !drawer.hasRuntimeOperationalContext()}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Platform ID</span>
<span class="font-medium text-base-content truncate" title={resource.platformId}>
{resource.platformId}
</span>
</div>
</Show>
<Show when={drawer.hasRuntimeOperationalContext()}>
<div class="mt-2 space-y-1.5">
<Show when={showPlatformId}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">{row.label}</span>
<span class="font-medium text-base-content truncate" title={row.value}>
{row.value}
<span class="text-muted">Platform ID</span>
<span class="font-medium text-base-content truncate" title={resource.platformId}>
{resource.platformId}
</span>
</div>
)}
</For>
<Show when={drawer.identityIpValues().length > 0}>
<div class="flex flex-col gap-1">
<span class="text-muted">IP Addresses</span>
<div class="flex flex-wrap gap-1">
<For each={drawer.identityIpValues()}>
{(ip) => (
<span
class="inline-flex items-center rounded bg-blue-100 px-1.5 py-0.5 text-[10px] text-blue-700 dark:bg-blue-900 dark:text-blue-200"
title={ip}
>
{ip}
</span>
)}
</For>
</div>
</div>
</Show>
<Show when={resource.tags && resource.tags.length > 0}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Tags</span>
<TagBadges tags={resource.tags} maxVisible={6} />
</div>
</Show>
<Show when={drawer.identityAliasValues().length > 0}>
<Show
when={drawer.hasAliasOverflow()}
fallback={
<div class="flex flex-col gap-1">
<span class="text-muted">Aliases</span>
<div class="flex flex-wrap gap-1">
<For each={drawer.aliasPreviewValues()}>
{(value) => (
<span
class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 text-[10px]"
title={value}
>
{value}
</span>
)}
</For>
</div>
</Show>
<Show when={drawer.kubernetesCapabilityBadges().length > 0}>
<div class="flex flex-col gap-1">
<span class="text-muted">Platform signals</span>
<div class="flex flex-wrap gap-1">
<For each={drawer.kubernetesCapabilityBadges()}>
{(badge) => (
<span class={badge.classes} title={badge.title}>
{badge.label}
</span>
)}
</For>
</div>
}
>
<details class="rounded border border-border bg-surface px-2 py-1.5">
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
<span>Aliases</span>
<span class="text-muted">{drawer.identityAliasValues().length}</span>
</summary>
<div class="mt-2 flex flex-wrap gap-1 border-t border-border pt-2">
<For each={drawer.identityAliasValues()}>
</div>
</Show>
<Show when={drawer.relatedLinks().length > 0}>
<div class="flex flex-col gap-1">
<span class="text-muted">Quick links</span>
<div class="flex flex-wrap gap-2">
<For each={drawer.relatedLinks()}>
{(link) => (
<a
href={link.href}
aria-label={link.ariaLabel}
class="inline-flex items-center rounded border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-900"
>
{link.compactLabel}
</a>
)}
</For>
</div>
</div>
</Show>
</div>
</Show>
</div>
</Card>
<Card data-testid="resource-identity-section" padding="sm" class="h-full shadow-sm">
<div class="mb-2 text-[10px] font-medium uppercase tracking-wide text-base-content">
Identity
</div>
<div class="space-y-1.5 text-[11px]">
<For each={drawer.primaryIdentityRows()}>
{(row) => (
<div class="flex items-center justify-between gap-2">
<span class="text-muted">{row.label}</span>
<span class="font-medium text-base-content truncate" title={row.value}>
{row.value}
</span>
</div>
)}
</For>
<Show when={drawer.identityIpValues().length > 0}>
<div class="flex flex-col gap-1">
<span class="text-muted">IP Addresses</span>
<div class="flex flex-wrap gap-1">
<For each={drawer.identityIpValues()}>
{(ip) => (
<span
class="inline-flex items-center rounded bg-blue-100 px-1.5 py-0.5 text-[10px] text-blue-700 dark:bg-blue-900 dark:text-blue-200"
title={ip}
>
{ip}
</span>
)}
</For>
</div>
</div>
</Show>
<Show when={resource.tags && resource.tags.length > 0}>
<div class="flex items-center justify-between gap-2">
<span class="text-muted">Tags</span>
<TagBadges tags={resource.tags} maxVisible={6} />
</div>
</Show>
<Show when={drawer.identityAliasValues().length > 0}>
<Show
when={drawer.hasAliasOverflow()}
fallback={
<div class="flex flex-col gap-1">
<span class="text-muted">Aliases</span>
<div class="flex flex-wrap gap-1">
<For each={drawer.aliasPreviewValues()}>
{(value) => (
<span
class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 text-[10px]"
@@ -268,17 +246,36 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
)}
</For>
</div>
</details>
</Show>
</div>
}
>
<details class="rounded border border-border bg-surface px-2 py-1.5">
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
<span>Aliases</span>
<span class="text-muted">{drawer.identityAliasValues().length}</span>
</summary>
<div class="mt-2 flex flex-wrap gap-1 border-t border-border pt-2">
<For each={drawer.identityAliasValues()}>
{(value) => (
<span
class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 text-[10px]"
title={value}
>
{value}
</span>
)}
</For>
</div>
</details>
</Show>
<Show when={!drawer.identityCardHasRichData()}>
<div class="rounded border border-dashed bg-surface-hover px-2 py-1.5 text-[10px] ">
No identity metadata yet.
</div>
</Show>
</div>
</Card>
</div>
</Show>
<Show when={!drawer.identityCardHasRichData()}>
<div class="rounded border border-dashed bg-surface-hover px-2 py-1.5 text-[10px] ">
No identity metadata yet.
</div>
</Show>
</div>
</Card>
</div>
<div
@@ -281,7 +281,7 @@ describe('ResourceDetailDrawer change history section', () => {
await screen.findByText('Changes loaded');
const changeHistorySection = screen.getByTestId('resource-change-history-section');
expect(screen.queryByRole('button', { name: 'Discovery' })).toBeNull();
expect(screen.getByText('Summary')).toBeInTheDocument();
expect(screen.queryByText('Summary')).toBeNull();
expect(screen.getByText('Current state')).toBeInTheDocument();
expect(screen.queryByText('Runtime')).toBeNull();
expect(screen.getByText('Change history')).toBeInTheDocument();
@@ -294,10 +294,9 @@ describe('ResourceDetailDrawer change history section', () => {
.length,
).toBe(0);
const summarySection = screen.getByTestId('resource-summary-section');
expect(summarySection.querySelector('.mt-3.grid.gap-3')).toBeTruthy();
expect(
summarySection.querySelector('.mt-3.grid.gap-3')?.classList.contains('sm:grid-cols-2'),
).toBe(true);
expect(summarySection.classList.contains('grid')).toBe(true);
expect(summarySection.classList.contains('gap-3')).toBe(true);
expect(summarySection.classList.contains('sm:grid-cols-2')).toBe(true);
expect(
screen.getByTestId('resource-current-state-section').classList.contains('rounded-md'),
).toBe(true);