mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Demote resource drawer host detail cards
This commit is contained in:
@@ -264,6 +264,10 @@ 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.
|
||||
Host and node system or hardware cards now also live behind a collapsed
|
||||
`Host details` support block instead of rendering before the primary overview
|
||||
cards, so runtime status, identity, and next investigation steps stay first
|
||||
while deeper machine detail remains available on demand.
|
||||
The same facet bundle now also returns grouped recent-change counts by
|
||||
canonical change kind, so the detail drawer can surface the distribution of
|
||||
state transitions, restarts, config updates, and anomalies without
|
||||
|
||||
@@ -143,6 +143,7 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
||||
const [showInvestigationContext, setShowInvestigationContext] = createSignal(false);
|
||||
const [showCorrelationContext, setShowCorrelationContext] = createSignal(false);
|
||||
const [showDiscoveryContext, setShowDiscoveryContext] = createSignal(false);
|
||||
const [showHostDetails, setShowHostDetails] = createSignal(false);
|
||||
|
||||
const displayName = createMemo(() => getPreferredResourceDisplayName(props.resource));
|
||||
const kubernetesClusterName = createMemo(() =>
|
||||
@@ -550,6 +551,38 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
||||
|
||||
return config.hostname ? `${discoveryMode} via ${config.hostname}` : discoveryMode;
|
||||
});
|
||||
const hostDetailCards = createMemo(() => {
|
||||
const cards: string[] = [];
|
||||
|
||||
if (proxmoxNode()) {
|
||||
cards.push('system', 'hardware', 'storage');
|
||||
}
|
||||
|
||||
const agent = agentInfo();
|
||||
if (agent) {
|
||||
cards.push('system', 'hardware');
|
||||
if ((agent.networkInterfaces?.length ?? 0) > 0) cards.push('network');
|
||||
if ((agent.disks?.length ?? 0) > 0) cards.push('disks');
|
||||
if ((agentMeta()?.raid?.length ?? 0) > 0) cards.push('raid');
|
||||
if (temperatureRows().length > 0) cards.push('temperatures');
|
||||
}
|
||||
|
||||
return cards;
|
||||
});
|
||||
const hasHostDetails = createMemo(() => hostDetailCards().length > 0);
|
||||
const hostDetailSummary = createMemo(() => {
|
||||
const labels = Array.from(new Set(hostDetailCards()));
|
||||
if (labels.length === 0) return null;
|
||||
|
||||
const categories =
|
||||
labels.length === 1
|
||||
? labels[0]
|
||||
: labels.length === 2
|
||||
? `${labels[0]} and ${labels[1]}`
|
||||
: `${labels.slice(0, -1).join(', ')}, and ${labels[labels.length - 1]}`;
|
||||
|
||||
return `${hostDetailCards().length} detail card${hostDetailCards().length === 1 ? '' : 's'} covering ${categories}.`;
|
||||
});
|
||||
const workloadsHref = createMemo(() => buildWorkloadsHref(props.resource));
|
||||
const headerIdentity = createMemo(() => getPrimaryResourceIdentity(props.resource));
|
||||
const relatedLinks = createMemo(() => {
|
||||
@@ -769,33 +802,7 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
||||
|
||||
{/* Overview Tab */}
|
||||
<div class={activeTab() === 'overview' ? '' : 'hidden'} style={{ 'overflow-anchor': 'none' }}>
|
||||
<Show when={proxmoxNode() || agentInfo()}>
|
||||
<div class="flex flex-wrap gap-3 [&>*]:flex-1 [&>*]:basis-[calc(25%-0.75rem)] [&>*]:min-w-[200px] [&>*]:max-w-full [&>*]:overflow-hidden">
|
||||
<Show when={proxmoxNode()}>
|
||||
{(node) => (
|
||||
<>
|
||||
<SystemInfoCard variant="node" node={node()} />
|
||||
<HardwareCard variant="node" node={node()} />
|
||||
<RootDiskCard node={node()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={agentInfo()}>
|
||||
{(agent) => (
|
||||
<>
|
||||
<SystemInfoCard variant="agent" agent={agent()} />
|
||||
<HardwareCard variant="agent" agent={agent()} />
|
||||
<NetworkInterfacesCard interfaces={agent().networkInterfaces} />
|
||||
<DisksCard disks={agent().disks} />
|
||||
<RaidCard arrays={agentMeta()?.raid} />
|
||||
<TemperaturesCard rows={temperatureRows()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3 mt-3">
|
||||
<div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="rounded border border-border bg-surface p-3">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content mb-2">
|
||||
Runtime
|
||||
@@ -1349,6 +1356,58 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={hasHostDetails()}>
|
||||
<div class="mt-3 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">
|
||||
Host details
|
||||
</div>
|
||||
<div class="mt-1 text-[10px] text-muted">
|
||||
Secondary system and hardware detail for deeper inspection.
|
||||
</div>
|
||||
<Show when={hostDetailSummary()}>
|
||||
<div class="mt-1 text-[10px] text-base-content">{hostDetailSummary()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowHostDetails((value) => !value)}
|
||||
class="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"
|
||||
>
|
||||
{showHostDetails() ? 'Hide host details' : 'Show host details'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={showHostDetails()}>
|
||||
<div class="mt-3 flex flex-wrap gap-3 [&>*]:flex-1 [&>*]:basis-[calc(25%-0.75rem)] [&>*]:min-w-[200px] [&>*]:max-w-full [&>*]:overflow-hidden">
|
||||
<Show when={proxmoxNode()}>
|
||||
{(node) => (
|
||||
<>
|
||||
<SystemInfoCard variant="node" node={node()} />
|
||||
<HardwareCard variant="node" node={node()} />
|
||||
<RootDiskCard node={node()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={agentInfo()}>
|
||||
{(agent) => (
|
||||
<>
|
||||
<SystemInfoCard variant="agent" agent={agent()} />
|
||||
<HardwareCard variant="agent" agent={agent()} />
|
||||
<NetworkInterfacesCard interfaces={agent().networkInterfaces} />
|
||||
<DisksCard disks={agent().disks} />
|
||||
<RaidCard arrays={agentMeta()?.raid} />
|
||||
<TemperaturesCard rows={temperatureRows()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={hasInvestigationContext()}>
|
||||
<div
|
||||
data-testid="resource-investigation-context"
|
||||
|
||||
+1
@@ -187,6 +187,7 @@ describe('ResourceDetailDrawer change history section', () => {
|
||||
const changeHistorySection = screen.getByTestId('resource-change-history-section');
|
||||
expect(screen.queryByRole('button', { name: 'Discovery' })).toBeNull();
|
||||
expect(screen.getByText('Change history')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Host details')).toBeNull();
|
||||
expect(screen.getByText('Discovery context')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Supporting metadata only. The web interface path above stays primary.'),
|
||||
|
||||
+55
@@ -147,6 +147,61 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps host detail cards behind a secondary overview disclosure', async () => {
|
||||
const resource = baseResource({
|
||||
platformData: {
|
||||
sources: ['agent'],
|
||||
agent: {
|
||||
agentId: 'agent-1',
|
||||
hostname: 'host-1',
|
||||
platform: 'linux',
|
||||
osName: 'Ubuntu',
|
||||
osVersion: '24.04',
|
||||
kernelVersion: '6.8.0',
|
||||
architecture: 'x86_64',
|
||||
uptimeSeconds: 7200,
|
||||
cpuCount: 8,
|
||||
agentVersion: '1.2.3',
|
||||
memory: { total: 16 * 1024 * 1024 * 1024 },
|
||||
networkInterfaces: [
|
||||
{
|
||||
name: 'eth0',
|
||||
mac: '00:11:22:33:44:55',
|
||||
addresses: ['192.0.2.10'],
|
||||
},
|
||||
],
|
||||
disks: [
|
||||
{
|
||||
mountpoint: '/',
|
||||
total: 100 * 1024 * 1024 * 1024,
|
||||
used: 50 * 1024 * 1024 * 1024,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByRole, getByText, queryByText } = render(() => (
|
||||
<ResourceDetailDrawer resource={resource} />
|
||||
));
|
||||
|
||||
expect(getByText('Host details')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('4 detail cards covering system, hardware, network, and disks.'),
|
||||
).toBeInTheDocument();
|
||||
expect(queryByText('Hardware')).toBeNull();
|
||||
expect(queryByText('Network')).toBeNull();
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Show host details' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Hardware')).toBeInTheDocument();
|
||||
});
|
||||
expect(getByText('Network')).toBeInTheDocument();
|
||||
expect(getByText('Disks')).toBeInTheDocument();
|
||||
expect(getByText('eth0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to source list summary when per-source health is unavailable', () => {
|
||||
const resource = baseResource({
|
||||
sourceType: 'api',
|
||||
|
||||
Reference in New Issue
Block a user