mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-16 11:19:11 +00:00
Compress service panel detail density
This commit is contained in:
parent
80249a91d1
commit
981d85cb2e
4 changed files with 363 additions and 218 deletions
|
|
@ -280,6 +280,10 @@ Type-specific Docker, PBS, and PMG operational panels now also live inside a
|
|||
collapsed `Service details` support block, so lane-specific controls and
|
||||
breakdowns stay available without displacing the common runtime and identity
|
||||
hierarchy on first read.
|
||||
When `Service details` is expanded, each service card remains summary-first and
|
||||
pushes heavier breakdowns or update controls behind one more service-local
|
||||
reveal, so the opened state still scans as current state before deeper
|
||||
operations.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
|||
const [showDiscoveryContext, setShowDiscoveryContext] = createSignal(false);
|
||||
const [showHostDetails, setShowHostDetails] = createSignal(false);
|
||||
const [showServiceDetails, setShowServiceDetails] = createSignal(false);
|
||||
const [showDockerUpdateControls, setShowDockerUpdateControls] = createSignal(false);
|
||||
const [showPbsJobDetail, setShowPbsJobDetail] = createSignal(false);
|
||||
const [showPmgMailFlowDetail, setShowPmgMailFlowDetail] = createSignal(false);
|
||||
|
||||
const displayName = createMemo(() => getPreferredResourceDisplayName(props.resource));
|
||||
const kubernetesClusterName = createMemo(() =>
|
||||
|
|
@ -622,6 +625,16 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
|||
|
||||
return null;
|
||||
});
|
||||
const pbsServiceSummary = createMemo(() => {
|
||||
const pbs = pbsData();
|
||||
if (!pbs) return null;
|
||||
return `${formatInteger(pbs.datastoreCount)} datastores · ${formatInteger(pbsJobTotal())} jobs`;
|
||||
});
|
||||
const pmgServiceSummary = createMemo(() => {
|
||||
const pmg = pmgData();
|
||||
if (!pmg) return null;
|
||||
return `${formatInteger(pmg.nodeCount)} node${pmg.nodeCount === 1 ? '' : 's'} · ${formatInteger(pmg.queueTotal)} queue total · ${formatInteger(pmgQueueBacklog())} backlog`;
|
||||
});
|
||||
const workloadsHref = createMemo(() => buildWorkloadsHref(props.resource));
|
||||
const headerIdentity = createMemo(() => getPrimaryResourceIdentity(props.resource));
|
||||
const relatedLinks = createMemo(() => {
|
||||
|
|
@ -1128,139 +1141,151 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={dockerHostCommand()?.type || dockerHostCommand()?.status}>
|
||||
<div class="rounded border border-sky-200 bg-surface px-2 py-1.5 text-[10px] dark:border-sky-700">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Command</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatIdentifierLabel(dockerHostCommand()?.type, {
|
||||
fallback: 'command',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Status</span>
|
||||
<span
|
||||
class={`font-medium ${dockerHostCommandActive() ? 'text-sky-700 dark:text-sky-300' : 'text-base-content'}`}
|
||||
>
|
||||
{formatIdentifierLabel(dockerHostCommand()?.status, {
|
||||
fallback: 'unknown',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={dockerHostCommand()?.message}>
|
||||
<div class="mt-1 text-muted truncate" title={dockerHostCommand()?.message}>
|
||||
{dockerHostCommand()?.message}
|
||||
<Show when={showDockerUpdateControls()}>
|
||||
<div class="space-y-1.5 border-t border-sky-200 pt-2 dark:border-sky-700">
|
||||
<Show when={dockerHostCommand()?.type || dockerHostCommand()?.status}>
|
||||
<div class="rounded border border-sky-200 bg-surface px-2 py-1.5 text-[10px] dark:border-sky-700">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Command</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatIdentifierLabel(dockerHostCommand()?.type, {
|
||||
fallback: 'command',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Status</span>
|
||||
<span
|
||||
class={`font-medium ${dockerHostCommandActive() ? 'text-sky-700 dark:text-sky-300' : 'text-base-content'}`}
|
||||
>
|
||||
{formatIdentifierLabel(dockerHostCommand()?.status, {
|
||||
fallback: 'unknown',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={dockerHostCommand()?.message}>
|
||||
<div class="mt-1 text-muted truncate" title={dockerHostCommand()?.message}>
|
||||
{dockerHostCommand()?.message}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={dockerHostCommand()?.failureReason}>
|
||||
<div
|
||||
class="mt-1 text-red-700 dark:text-red-300 truncate"
|
||||
title={dockerHostCommand()?.failureReason}
|
||||
>
|
||||
{dockerHostCommand()?.failureReason}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={dockerHostCommand()?.failureReason}>
|
||||
<div
|
||||
class="mt-1 text-red-700 dark:text-red-300 truncate"
|
||||
title={dockerHostCommand()?.failureReason}
|
||||
>
|
||||
{dockerHostCommand()?.failureReason}
|
||||
|
||||
<Show when={dockerActionError()}>
|
||||
<div class="rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[10px] text-red-700 dark:border-red-700 dark:bg-red-900 dark:text-red-200">
|
||||
{dockerActionError()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={dockerActionNote()}>
|
||||
<div class="rounded border border-sky-200 bg-surface px-2 py-1.5 text-[10px] text-base-content dark:border-sky-700">
|
||||
{dockerActionNote()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
dockerActionBusy() ||
|
||||
dockerUpdateActionsLoading() ||
|
||||
dockerHostCommandActive() ||
|
||||
dockerHostSourceId() === null
|
||||
}
|
||||
onClick={async () => {
|
||||
setDockerActionError('');
|
||||
setDockerActionNote('');
|
||||
setConfirmUpdateAll(false);
|
||||
const hostId = dockerHostSourceId();
|
||||
if (!hostId) return;
|
||||
try {
|
||||
setDockerActionBusy(true);
|
||||
await MonitoringAPI.checkDockerUpdates(hostId);
|
||||
setDockerActionNote(
|
||||
'Update check queued. Results will refresh on the next agent report.',
|
||||
);
|
||||
} catch (err) {
|
||||
setDockerActionError(
|
||||
(err as Error)?.message || 'Failed to queue update check',
|
||||
);
|
||||
} finally {
|
||||
setDockerActionBusy(false);
|
||||
}
|
||||
}}
|
||||
class="rounded-md border border-border bg-surface px-2.5 py-1 text-[11px] font-semibold text-base-content hover:bg-surface-hover disabled:opacity-60"
|
||||
title={dockerUpdateActionsLoading() ? 'Loading server settings...' : undefined}
|
||||
>
|
||||
Check Updates
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
dockerActionBusy() ||
|
||||
dockerUpdateActionsLoading() ||
|
||||
dockerUpdateActionsDisabled() ||
|
||||
dockerHostCommandActive() ||
|
||||
dockerHostSourceId() === null ||
|
||||
dockerUpdatesAvailable() <= 0
|
||||
}
|
||||
onClick={async () => {
|
||||
setDockerActionError('');
|
||||
setDockerActionNote('');
|
||||
const hostId = dockerHostSourceId();
|
||||
if (!hostId) return;
|
||||
|
||||
if (!confirmUpdateAll()) {
|
||||
setConfirmUpdateAll(true);
|
||||
setDockerActionNote(
|
||||
`Click again to confirm updating ${dockerUpdatesAvailable()} container(s).`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDockerActionBusy(true);
|
||||
await MonitoringAPI.updateAllDockerContainers(hostId);
|
||||
setDockerActionNote(
|
||||
'Batch update queued. Progress will appear as the agent reports back.',
|
||||
);
|
||||
} catch (err) {
|
||||
setDockerActionError(
|
||||
(err as Error)?.message || 'Failed to queue batch update',
|
||||
);
|
||||
} finally {
|
||||
setDockerActionBusy(false);
|
||||
setConfirmUpdateAll(false);
|
||||
}
|
||||
}}
|
||||
class="rounded-md border border-sky-200 bg-sky-600 px-2.5 py-1 text-[11px] font-semibold text-white hover:bg-sky-700 disabled:opacity-60 disabled:hover:bg-sky-600 dark:border-sky-700 dark:bg-sky-600 dark:hover:bg-sky-500 dark:disabled:hover:bg-sky-600"
|
||||
title={
|
||||
dockerUpdateActionsDisabled()
|
||||
? 'Docker updates are disabled by server configuration.'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{confirmUpdateAll()
|
||||
? 'Confirm Update All'
|
||||
: `Update All${dockerUpdatesAvailable() > 0 ? ` (${dockerUpdatesAvailable()})` : ''}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={dockerActionError()}>
|
||||
<div class="rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[10px] text-red-700 dark:border-red-700 dark:bg-red-900 dark:text-red-200">
|
||||
{dockerActionError()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={dockerActionNote()}>
|
||||
<div class="rounded border border-sky-200 bg-surface px-2 py-1.5 text-[10px] text-base-content dark:border-sky-700">
|
||||
{dockerActionNote()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
dockerActionBusy() ||
|
||||
dockerUpdateActionsLoading() ||
|
||||
dockerHostCommandActive() ||
|
||||
dockerHostSourceId() === null
|
||||
}
|
||||
onClick={async () => {
|
||||
setDockerActionError('');
|
||||
setDockerActionNote('');
|
||||
setConfirmUpdateAll(false);
|
||||
const hostId = dockerHostSourceId();
|
||||
if (!hostId) return;
|
||||
try {
|
||||
setDockerActionBusy(true);
|
||||
await MonitoringAPI.checkDockerUpdates(hostId);
|
||||
setDockerActionNote(
|
||||
'Update check queued. Results will refresh on the next agent report.',
|
||||
);
|
||||
} catch (err) {
|
||||
setDockerActionError(
|
||||
(err as Error)?.message || 'Failed to queue update check',
|
||||
);
|
||||
} finally {
|
||||
setDockerActionBusy(false);
|
||||
}
|
||||
}}
|
||||
class="rounded-md border border-border bg-surface px-2.5 py-1 text-[11px] font-semibold text-base-content hover:bg-surface-hover disabled:opacity-60"
|
||||
title={dockerUpdateActionsLoading() ? 'Loading server settings...' : undefined}
|
||||
>
|
||||
Check Updates
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
dockerActionBusy() ||
|
||||
dockerUpdateActionsLoading() ||
|
||||
dockerUpdateActionsDisabled() ||
|
||||
dockerHostCommandActive() ||
|
||||
dockerHostSourceId() === null ||
|
||||
dockerUpdatesAvailable() <= 0
|
||||
}
|
||||
onClick={async () => {
|
||||
setDockerActionError('');
|
||||
setDockerActionNote('');
|
||||
const hostId = dockerHostSourceId();
|
||||
if (!hostId) return;
|
||||
|
||||
if (!confirmUpdateAll()) {
|
||||
setConfirmUpdateAll(true);
|
||||
setDockerActionNote(
|
||||
`Click again to confirm updating ${dockerUpdatesAvailable()} container(s).`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDockerActionBusy(true);
|
||||
await MonitoringAPI.updateAllDockerContainers(hostId);
|
||||
setDockerActionNote(
|
||||
'Batch update queued. Progress will appear as the agent reports back.',
|
||||
);
|
||||
} catch (err) {
|
||||
setDockerActionError(
|
||||
(err as Error)?.message || 'Failed to queue batch update',
|
||||
);
|
||||
} finally {
|
||||
setDockerActionBusy(false);
|
||||
setConfirmUpdateAll(false);
|
||||
}
|
||||
}}
|
||||
class="rounded-md border border-sky-200 bg-sky-600 px-2.5 py-1 text-[11px] font-semibold text-white hover:bg-sky-700 disabled:opacity-60 disabled:hover:bg-sky-600 dark:border-sky-700 dark:bg-sky-600 dark:hover:bg-sky-500 dark:disabled:hover:bg-sky-600"
|
||||
title={
|
||||
dockerUpdateActionsDisabled()
|
||||
? 'Docker updates are disabled by server configuration.'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{confirmUpdateAll()
|
||||
? 'Confirm Update All'
|
||||
: `Update All${dockerUpdatesAvailable() > 0 ? ` (${dockerUpdatesAvailable()})` : ''}`}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDockerUpdateControls((value) => !value)}
|
||||
class="inline-flex items-center rounded-md border border-sky-200 bg-surface px-2.5 py-1 text-[10px] font-medium text-sky-700 transition-colors hover:bg-base dark:border-sky-700 dark:text-sky-300"
|
||||
>
|
||||
{showDockerUpdateControls() ? 'Hide update controls' : 'Show update controls'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -1305,38 +1330,57 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
|||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="grid grid-cols-2 gap-2 pt-1">
|
||||
<div class="rounded border border-indigo-200 bg-surface px-2 py-1.5 dark:border-indigo-700">
|
||||
<div class="text-[10px] text-muted">Datastores</div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{formatInteger(pbs().datastoreCount)}
|
||||
<Show when={pbsServiceSummary()}>
|
||||
<div class="rounded border border-indigo-200 bg-surface px-2 py-1.5 text-[10px] dark:border-indigo-700">
|
||||
<div class="text-muted">Backup summary</div>
|
||||
<div class="mt-1 font-medium text-base-content">
|
||||
{pbsServiceSummary()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded border border-indigo-200 bg-surface px-2 py-1.5 dark:border-indigo-700">
|
||||
<div class="text-[10px] text-muted">Total Jobs</div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{formatInteger(pbsJobTotal())}
|
||||
</Show>
|
||||
<Show when={showPbsJobDetail()}>
|
||||
<div class="space-y-1.5 border-t border-indigo-200 pt-2 dark:border-indigo-700">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded border border-indigo-200 bg-surface px-2 py-1.5 dark:border-indigo-700">
|
||||
<div class="text-[10px] text-muted">Datastores</div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{formatInteger(pbs().datastoreCount)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded border border-indigo-200 bg-surface px-2 py-1.5 dark:border-indigo-700">
|
||||
<div class="text-[10px] text-muted">Total Jobs</div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{formatInteger(pbsJobTotal())}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="rounded border border-indigo-200 bg-surface px-2 py-1.5 dark:border-indigo-700">
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
|
||||
<span>Job breakdown</span>
|
||||
<span class="text-muted">{pbsVisibleJobBreakdown().length} types</span>
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1 border-t border-indigo-200 pt-2 text-[10px] dark:border-indigo-700">
|
||||
<For each={pbsVisibleJobBreakdown()}>
|
||||
{(entry) => (
|
||||
<span class="text-muted">
|
||||
{entry.label}:{' '}
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<details class="rounded border border-indigo-200 bg-surface px-2 py-1.5 dark:border-indigo-700">
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
|
||||
<span>Job breakdown</span>
|
||||
<span class="text-muted">{pbsVisibleJobBreakdown().length} types</span>
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1 border-t border-indigo-200 pt-2 text-[10px] dark:border-indigo-700">
|
||||
<For each={pbsVisibleJobBreakdown()}>
|
||||
{(entry) => (
|
||||
<span class="text-muted">
|
||||
{entry.label}:{' '}
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPbsJobDetail((value) => !value)}
|
||||
class="inline-flex items-center rounded-md border border-indigo-200 bg-surface px-2.5 py-1 text-[10px] font-medium text-indigo-700 transition-colors hover:bg-base dark:border-indigo-700 dark:text-indigo-300"
|
||||
>
|
||||
{showPbsJobDetail() ? 'Hide job detail' : 'Show job detail'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1383,74 +1427,93 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
|||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="grid grid-cols-3 gap-2 pt-1">
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<div class="text-[10px] text-muted">Nodes</div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{formatInteger(pmg().nodeCount)}
|
||||
<Show when={pmgServiceSummary()}>
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 text-[10px] dark:border-rose-700">
|
||||
<div class="text-muted">Mail flow summary</div>
|
||||
<div class="mt-1 font-medium text-base-content">
|
||||
{pmgServiceSummary()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<div class="text-[10px] text-muted">Queue Total</div>
|
||||
<div
|
||||
class={`text-sm font-semibold ${pmgQueueBacklog() > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(pmg().queueTotal)}
|
||||
</Show>
|
||||
<Show when={showPmgMailFlowDetail()}>
|
||||
<div class="space-y-1.5 border-t border-rose-200 pt-2 dark:border-rose-700">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<div class="text-[10px] text-muted">Nodes</div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{formatInteger(pmg().nodeCount)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<div class="text-[10px] text-muted">Queue Total</div>
|
||||
<div
|
||||
class={`text-sm font-semibold ${pmgQueueBacklog() > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(pmg().queueTotal)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<div class="text-[10px] text-muted">Backlog</div>
|
||||
<div
|
||||
class={`text-sm font-semibold ${pmgQueueBacklog() > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(pmgQueueBacklog())}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
|
||||
<span>Queue breakdown</span>
|
||||
<span class="text-muted">{pmgVisibleQueueBreakdown().length} signals</span>
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<For each={pmgVisibleQueueBreakdown()}>
|
||||
{(entry) => (
|
||||
<span class="text-muted">
|
||||
{entry.label}:{' '}
|
||||
<span
|
||||
class={`font-medium ${entry.warn ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
<details class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
|
||||
<span>Mail processing</span>
|
||||
<span class="text-muted">{pmgVisibleMailBreakdown().length} signals</span>
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-3 gap-x-3 gap-y-1 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<For each={pmgVisibleMailBreakdown()}>
|
||||
{(entry) => (
|
||||
<span class="text-muted">
|
||||
{entry.label}:{' '}
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={pmgUpdatedRelative()}>
|
||||
<div class="mt-2 flex items-center justify-between gap-2 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<span class="text-muted">Updated</span>
|
||||
<span class="font-medium text-base-content">{pmgUpdatedRelative()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</details>
|
||||
</div>
|
||||
<div class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<div class="text-[10px] text-muted">Backlog</div>
|
||||
<div
|
||||
class={`text-sm font-semibold ${pmgQueueBacklog() > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(pmgQueueBacklog())}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
|
||||
<span>Queue breakdown</span>
|
||||
<span class="text-muted">{pmgVisibleQueueBreakdown().length} signals</span>
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<For each={pmgVisibleQueueBreakdown()}>
|
||||
{(entry) => (
|
||||
<span class="text-muted">
|
||||
{entry.label}:{' '}
|
||||
<span
|
||||
class={`font-medium ${entry.warn ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
<details class="rounded border border-rose-200 bg-surface px-2 py-1.5 dark:border-rose-700">
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between text-[10px] font-medium text-muted">
|
||||
<span>Mail processing</span>
|
||||
<span class="text-muted">{pmgVisibleMailBreakdown().length} signals</span>
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-3 gap-x-3 gap-y-1 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<For each={pmgVisibleMailBreakdown()}>
|
||||
{(entry) => (
|
||||
<span class="text-muted">
|
||||
{entry.label}:{' '}
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={pmgUpdatedRelative()}>
|
||||
<div class="mt-2 flex items-center justify-between gap-2 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<span class="text-muted">Updated</span>
|
||||
<span class="font-medium text-base-content">{pmgUpdatedRelative()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</details>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPmgMailFlowDetail((value) => !value)}
|
||||
class="inline-flex items-center rounded-md border border-rose-200 bg-surface px-2.5 py-1 text-[10px] font-medium text-rose-700 transition-colors hover:bg-base dark:border-rose-700 dark:text-rose-300"
|
||||
>
|
||||
{showPmgMailFlowDetail() ? 'Hide mail flow detail' : 'Show mail flow detail'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -324,6 +324,42 @@ describe('ResourceDetailDrawer change history section', () => {
|
|||
expect(panel.queryByText('Runs on')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps service details summary-first until the service-local reveal is opened', () => {
|
||||
facetBundleMock.getFacetBundle.mockResolvedValueOnce({
|
||||
capabilities: [],
|
||||
relationships: [],
|
||||
recentChanges: [],
|
||||
});
|
||||
|
||||
const resource = baseResource({
|
||||
id: 'pbs-1',
|
||||
type: 'pbs',
|
||||
name: 'pbs-main',
|
||||
displayName: 'PBS Main',
|
||||
platformId: 'pbs-main',
|
||||
platformType: 'proxmox-pbs',
|
||||
platformData: {
|
||||
sources: ['pbs'],
|
||||
pbs: {
|
||||
hostname: 'pbs-main.local',
|
||||
connectionHealth: 'online',
|
||||
datastoreCount: 2,
|
||||
backupJobCount: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(() => <ResourceDetailDrawer resource={resource} />);
|
||||
|
||||
expect(screen.getByText('Service details')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show service details' }));
|
||||
expect(screen.getByText('PBS Service')).toBeInTheDocument();
|
||||
expect(screen.getByText('Backup summary')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Job breakdown')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show job detail' }));
|
||||
expect(screen.getByText('Job breakdown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters timeline entries by kind and source type', async () => {
|
||||
const unfilteredFacetBundle = {
|
||||
capabilities: [
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ describe('ResourceDetailDrawer service cards', () => {
|
|||
fireEvent.click(getByRole('button', { name: 'Show service details' }));
|
||||
expect(getByText('PBS Service')).toBeInTheDocument();
|
||||
expect(getAllByText('pbs-main.local').length).toBeGreaterThan(0);
|
||||
expect(getByText('Backup summary')).toBeInTheDocument();
|
||||
expect(queryByText('Job breakdown')).toBeNull();
|
||||
fireEvent.click(getByRole('button', { name: 'Show job detail' }));
|
||||
expect(getByText('Datastores')).toBeInTheDocument();
|
||||
expect(getByText('Total Jobs')).toBeInTheDocument();
|
||||
expect(getByText('Job breakdown')).toBeInTheDocument();
|
||||
|
|
@ -121,6 +124,10 @@ describe('ResourceDetailDrawer service cards', () => {
|
|||
expect(getByText('Mail Gateway')).toBeInTheDocument();
|
||||
});
|
||||
expect(getAllByText('pmg-main.local').length).toBeGreaterThan(0);
|
||||
expect(getByText('Mail flow summary')).toBeInTheDocument();
|
||||
expect(queryByText('Queue breakdown')).toBeNull();
|
||||
expect(queryByText('Mail processing')).toBeNull();
|
||||
fireEvent.click(getByRole('button', { name: 'Show mail flow detail' }));
|
||||
expect(getByText('Queue Total')).toBeInTheDocument();
|
||||
expect(getByText('Backlog')).toBeInTheDocument();
|
||||
expect(getByText('Queue breakdown')).toBeInTheDocument();
|
||||
|
|
@ -130,4 +137,39 @@ describe('ResourceDetailDrawer service cards', () => {
|
|||
'/alerts/thresholds/mail-gateway',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps docker update controls behind a secondary reveal', () => {
|
||||
const resource = baseResource({
|
||||
id: 'docker-host-1',
|
||||
type: 'docker-host',
|
||||
name: 'docker-main',
|
||||
displayName: 'Docker Main',
|
||||
platformId: 'docker-main',
|
||||
platformType: 'docker',
|
||||
sourceType: 'agent',
|
||||
platformData: {
|
||||
sources: ['docker', 'agent'],
|
||||
docker: {
|
||||
hostSourceId: 'docker-host-1',
|
||||
hostname: 'docker-main.local',
|
||||
runtime: 'Docker Engine 28.0',
|
||||
containerCount: 18,
|
||||
updatesAvailableCount: 4,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(() => (
|
||||
<ResourceDetailDrawer resource={resource} />
|
||||
));
|
||||
|
||||
expect(getByText('Service details')).toBeInTheDocument();
|
||||
expect(getByText('18 containers · 4 updates')).toBeInTheDocument();
|
||||
fireEvent.click(getByRole('button', { name: 'Show service details' }));
|
||||
expect(getByText('Container Updates')).toBeInTheDocument();
|
||||
expect(queryByText('Check Updates')).toBeNull();
|
||||
fireEvent.click(getByRole('button', { name: 'Show update controls' }));
|
||||
expect(getByText('Check Updates')).toBeInTheDocument();
|
||||
expect(getByText('Update All (4)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue