mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
Remove redundant drawer summary heading
This commit is contained in:
@@ -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.
|
||||
|
||||
+169
-172
@@ -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
|
||||
|
||||
+4
-5
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user