mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Converge remaining drawer detail rows
This commit is contained in:
@@ -215,6 +215,7 @@ overflow.
|
||||
1a. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts`
|
||||
1b. `frontend-modern/src/components/Workloads/nodeDrawerModel.ts`
|
||||
1c. `frontend-modern/src/features/docker/dockerHostDrawerModel.ts`
|
||||
1d. `frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsx`
|
||||
2. `frontend-modern/src/components/Settings/Settings.tsx`
|
||||
3. `frontend-modern/src/components/Settings/SettingsDialogs.tsx`
|
||||
4. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
|
||||
@@ -1226,6 +1227,12 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
otherwise the shared `lg` transition is canonical. Feature surfaces own the
|
||||
labels, values, wrapping, and selection behavior, but must not restore
|
||||
drawer-width `justify-between` rows on desktop.
|
||||
This contract also covers secondary drawer facts in availability status and
|
||||
suggestion cards, resource change-history entries, Docker/PBS/PMG service
|
||||
support panels, and Docker container-update management cards. Headers,
|
||||
status summaries, actions, disk capacity summaries, and RAID state pairs may
|
||||
retain intentional endpoint alignment; ordinary label/value facts in those
|
||||
surfaces may not.
|
||||
Read-only metadata chips belong to `MetadataBadge` and domain wrappers over
|
||||
it. Organization role and share-status chips must use
|
||||
`OrganizationRoleBadge` and `OrganizationShareStatusBadge`, so role/status
|
||||
@@ -6185,6 +6192,10 @@ Known platform resources use that same compact presentation when availability
|
||||
is attached. `AvailabilityProbeStatusCard` is the shared detail primitive for
|
||||
Workloads and Docker host drawers; it renders the complete target, protocol,
|
||||
latest result, latency when relevant, evidence freshness, and last observation.
|
||||
Its fact rows, and the matching service/probe/target rows in
|
||||
`AvailabilityProbeSuggestionCard`, compose `InfoCardKeyValueRow` so phone
|
||||
layouts remain condensed while wide cards keep each value adjacent to its
|
||||
label.
|
||||
Plural attached checks render as repeated bounded cards from
|
||||
`availabilityChecks`, while the row keeps one compatibility summary. Expired
|
||||
successful evidence must render an amber `Stale` state with no green
|
||||
|
||||
@@ -4899,6 +4899,7 @@
|
||||
"frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx",
|
||||
"frontend-modern/src/components/SetupWizard/SetupWizard.tsx",
|
||||
"frontend-modern/src/components/Toast/Toast.tsx",
|
||||
"frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsx",
|
||||
"frontend-modern/src/components/Workloads/nodeDrawerModel.ts",
|
||||
"frontend-modern/src/features/docker/dockerHostDrawerModel.ts",
|
||||
"frontend-modern/src/features/platformPage/platformEstateOverviewModel.ts",
|
||||
@@ -5185,8 +5186,21 @@
|
||||
"frontend-modern/src/utils/__tests__/reportingResourceTypes.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "workload-presentation-helpers",
|
||||
{
|
||||
"id": "compact-info-card-consumers",
|
||||
"label": "compact information card consumer proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsx"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "workload-presentation-helpers",
|
||||
"label": "workload presentation helper proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
|
||||
@@ -1329,6 +1329,11 @@ and temperature facts must also compose the frontend-primitives
|
||||
`InfoCardKeyValueRow`. Mobile rows retain their condensed endpoint layout;
|
||||
desktop rows use the shared fixed label track so labels and values remain
|
||||
visually adjacent instead of spanning the full drawer width.
|
||||
The same boundary applies to availability facts, resource change-history
|
||||
metadata, Docker/PBS/PMG service facts, nested PMG queue/mail breakdowns, and
|
||||
Docker container-update management facts. Their headers, actions, and compact
|
||||
status summaries remain feature-owned, while ordinary label/value rows compose
|
||||
`InfoCardKeyValueRow` rather than restoring a drawer-width flex split.
|
||||
Curated technical inventory follows the shared compact-row contract instead
|
||||
of the secondary-card contract. Docker-host drawers must project system,
|
||||
runtime, memory, storage, and telemetry facts through
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "636367e96ee97ce56c7b1353635236a418a3922c",
|
||||
"verified_at": "2026-08-27T19:10:47Z",
|
||||
"base_sha": "b0c1eca4cdace3cb798a77b2fa0c01a220f76981",
|
||||
"verified_at": "2026-08-27T19:35:10Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/alerts/AlertEscalationSection.tsx",
|
||||
"frontend-modern/src/features/alerts/helpers.ts",
|
||||
"frontend-modern/src/features/alerts/useAlertScheduleState.ts"
|
||||
"frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx",
|
||||
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx",
|
||||
"frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsx",
|
||||
"frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/alerts/AlertEscalationSection.tsx": "f80f94d529196fffb4e184288c5ce9369f673bc8cabc0eb2047ebc10b7dfe363",
|
||||
"frontend-modern/src/features/alerts/helpers.ts": "27feef6ede9bdb6668af5c447c5b9256f019b8df5a291ae8b5d436d371363794",
|
||||
"frontend-modern/src/features/alerts/useAlertScheduleState.ts": "84ec18253c926be70d941daf5b56bf045cf372ded5026033849d38f76c23e84f"
|
||||
"frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx": "2d991718c178866ba5bd7d1b89e7a1a9bec6fcd30133bc2fd549aa2fd3ccf4e5",
|
||||
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx": "ac68e2e7f5d954ca5bd23b44f347817ce9a39414ab493d22ad52340ceb3296e2",
|
||||
"frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsx": "a8c549b1284befee652c9ecc79c3c449602d1a102f3867acb81f4a5c9f5fd042",
|
||||
"frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx": "ab63fba8e1a06cb5951e0d3dc637d92ef371f2b5db5da2dfb80408cd477c0ea5"
|
||||
},
|
||||
"routes": ["/alerts/schedule"],
|
||||
"routes": ["/standalone/availability", "/proxmox/backups/date", "/docker/overview"],
|
||||
"viewports": [
|
||||
{ "width": 1280, "height": 720 },
|
||||
{ "width": 1920, "height": 800 },
|
||||
{ "width": 390, "height": 844 }
|
||||
],
|
||||
"states": [
|
||||
"alert escalation enabled with one level and selected disabled destinations visible",
|
||||
"level delay normalized to the 5 minute lower bound",
|
||||
"level delay normalized to the 180 minute upper bound",
|
||||
"upper-bound level retained in the narrow layout without horizontal overflow"
|
||||
"Solar inverter web panel expanded with availability facts visible",
|
||||
"backup-vault expanded with Platform details and PBS service facts visible",
|
||||
"auth-service-01 expanded on Manage with Container updates visible"
|
||||
],
|
||||
"interactions": [
|
||||
"enabled alert escalation and added an escalation level",
|
||||
"entered 0 minutes and blurred the field, confirming it normalized to 5",
|
||||
"entered 999 minutes and blurred the field, confirming it normalized to 180",
|
||||
"resized from desktop to 390px and confirmed zero document-level horizontal overflow",
|
||||
"confirmed the browser console contained no warnings or errors"
|
||||
"expanded the Solar inverter web panel and confirmed desktop availability values use an adjacent fixed label track",
|
||||
"confirmed availability facts retain condensed right-aligned values and clean wrapping at 390px",
|
||||
"expanded backup-vault, opened Platform details, and revealed the PBS service section",
|
||||
"confirmed PBS service facts use adjacent desktop values and retain compact mobile alignment",
|
||||
"expanded auth-service-01, selected Manage, and confirmed Container updates facts use adjacent desktop values",
|
||||
"confirmed Docker management facts retain condensed right alignment at 390px"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -466,6 +466,7 @@
|
||||
{ "path": "src/components/shared/cards/RootDiskCard.tsx" },
|
||||
{ "path": "src/components/shared/cards/SystemInfoCard.tsx" },
|
||||
{ "path": "src/components/shared/cards/TemperaturesCard.tsx" },
|
||||
{ "path": "src/components/Workloads/AvailabilityProbeSuggestionCard.tsx" },
|
||||
{ "path": "src/components/Workloads/DrawerDiskListCard.tsx" },
|
||||
{ "path": "src/features/docker/DockerHostDrawerOverview.tsx" },
|
||||
{ "path": "src/features/storageBackups/detailPresentation.ts" }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { For, Show, createMemo } from 'solid-js';
|
||||
import { Activity, AlertCircle, Check } from 'lucide-solid';
|
||||
|
||||
import type { ResourceAvailabilityMeta } from '@/types/resource';
|
||||
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
|
||||
import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
import {
|
||||
getAvailabilityProbeMethodLabel,
|
||||
getAvailabilityProbeEndpointLabel,
|
||||
@@ -129,130 +129,111 @@ export function AvailabilityProbeStatusCard(props: AvailabilityProbeStatusCardPr
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-1.5 text-[11px]">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Latency</span>
|
||||
<Show
|
||||
when={isUp() && latency()}
|
||||
fallback={<span class="text-red-600 dark:text-red-400 font-medium">—</span>}
|
||||
>
|
||||
<span
|
||||
class="font-medium"
|
||||
classList={{
|
||||
'text-emerald-600 dark:text-emerald-400': !isStale(),
|
||||
'text-amber-600 dark:text-amber-300': isStale(),
|
||||
}}
|
||||
<InfoCardKeyValueRow
|
||||
label="Latency"
|
||||
value={
|
||||
<Show
|
||||
when={isUp() && latency()}
|
||||
fallback={<span class="text-red-600 dark:text-red-400">—</span>}
|
||||
>
|
||||
{latency()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Method</span>
|
||||
<span class="font-medium text-base-content" title={targetAddr()}>
|
||||
{method()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Target</span>
|
||||
<span class="font-medium text-base-content truncate ml-2" title={targetAddr()}>
|
||||
{targetAddr()}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
classList={{
|
||||
'text-emerald-600 dark:text-emerald-400': !isStale(),
|
||||
'text-amber-600 dark:text-amber-300': isStale(),
|
||||
}}
|
||||
>
|
||||
{latency()}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
<InfoCardKeyValueRow label="Method" value={method()} valueTitle={targetAddr()} />
|
||||
<InfoCardKeyValueRow
|
||||
label="Target"
|
||||
value={targetAddr()}
|
||||
valueClass="truncate"
|
||||
valueTitle={targetAddr()}
|
||||
/>
|
||||
<Show when={lastChecked()}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Checked</span>
|
||||
<span class="text-base-content/70">{lastChecked()}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Checked"
|
||||
value={lastChecked()}
|
||||
valueClass="text-base-content/70"
|
||||
/>
|
||||
</Show>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Freshness</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
classList={{
|
||||
'text-amber-600 dark:text-amber-300': presentation()?.freshnessLabel === 'stale',
|
||||
'text-base-content': presentation()?.freshnessLabel !== 'stale',
|
||||
}}
|
||||
>
|
||||
{presentation()?.freshnessLabel ?? 'freshness unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Freshness"
|
||||
value={presentation()?.freshnessLabel ?? 'freshness unknown'}
|
||||
valueClass={
|
||||
presentation()?.freshnessLabel === 'stale' ? 'text-amber-600 dark:text-amber-300' : ''
|
||||
}
|
||||
/>
|
||||
<Show when={presentation()?.correlationLabel}>
|
||||
{(label) => (
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="text-muted">Resource</span>
|
||||
<span class="text-right text-amber-600 dark:text-amber-300">{label()}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Resource"
|
||||
value={label()}
|
||||
valueClass="text-amber-600 dark:text-amber-300"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={certificate()}>
|
||||
{(cert) => (
|
||||
<div class="mt-1.5 space-y-1.5 border-t border-base-200 pt-1.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Certificate</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
classList={{
|
||||
'text-emerald-600 dark:text-emerald-300': certificateTrust().tone === 'success',
|
||||
'text-red-600 dark:text-red-300': certificateTrust().tone === 'danger',
|
||||
'text-base-content': certificateTrust().tone === 'neutral',
|
||||
}}
|
||||
title={cert().trustError || undefined}
|
||||
>
|
||||
{certificateTrust().label}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Certificate"
|
||||
value={certificateTrust().label}
|
||||
valueClass={
|
||||
certificateTrust().tone === 'success'
|
||||
? 'text-emerald-600 dark:text-emerald-300'
|
||||
: certificateTrust().tone === 'danger'
|
||||
? 'text-red-600 dark:text-red-300'
|
||||
: ''
|
||||
}
|
||||
valueTitle={cert().trustError || undefined}
|
||||
/>
|
||||
<Show when={cert().subject}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Subject</span>
|
||||
<span class="ml-2 truncate font-medium text-base-content" title={cert().subject}>
|
||||
{cert().subject}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Subject"
|
||||
value={cert().subject}
|
||||
valueClass="truncate"
|
||||
valueTitle={cert().subject}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={certificateExpiry()}>
|
||||
{(expiry) => (
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Expires</span>
|
||||
<span
|
||||
class="font-medium text-base-content"
|
||||
title={`Warning window: ${props.availability.certificateExpiryWarningDays ?? 30} days`}
|
||||
>
|
||||
{expiry()}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Expires"
|
||||
value={expiry()}
|
||||
valueTitle={`Warning window: ${props.availability.certificateExpiryWarningDays ?? 30} days`}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Hostname</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
classList={{
|
||||
'text-emerald-600 dark:text-emerald-300': cert().hostnameValid,
|
||||
'text-red-600 dark:text-red-300': !cert().hostnameValid,
|
||||
}}
|
||||
>
|
||||
{cert().hostnameValid ? 'Matches' : 'Mismatch'}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Hostname"
|
||||
value={cert().hostnameValid ? 'Matches' : 'Mismatch'}
|
||||
valueClass={
|
||||
cert().hostnameValid
|
||||
? 'text-emerald-600 dark:text-emerald-300'
|
||||
: 'text-red-600 dark:text-red-300'
|
||||
}
|
||||
/>
|
||||
<Show when={cert().issuer}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Issuer</span>
|
||||
<span class="ml-2 truncate text-base-content" title={cert().issuer}>
|
||||
{cert().issuer}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Issuer"
|
||||
value={cert().issuer}
|
||||
valueClass="truncate font-normal"
|
||||
valueTitle={cert().issuer}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={cert().fingerprintSha256}>
|
||||
{(fingerprint) => (
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">SHA-256</span>
|
||||
<span
|
||||
class="ml-2 truncate font-mono text-[10px] text-base-content"
|
||||
title={fingerprint()}
|
||||
>
|
||||
{fingerprint().slice(0, 16)}…
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="SHA-256"
|
||||
value={`${fingerprint().slice(0, 16)}…`}
|
||||
valueClass="truncate font-mono text-[10px] font-normal"
|
||||
valueTitle={fingerprint()}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
+103
-127
@@ -18,7 +18,7 @@ import { RaidCard } from '@/components/shared/cards/RaidCard';
|
||||
import { DiscoveryTab } from '@/components/Discovery/DiscoveryTab';
|
||||
import { DiscoveryLoadingFallback } from '@/components/shared/DiscoveryLoadingFallback';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
|
||||
import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
import { WebInterfaceUrlField } from '@/components/shared/WebInterfaceUrlField';
|
||||
import {
|
||||
WEB_INTERFACE_LINK_COLOR_CLASS,
|
||||
@@ -575,31 +575,27 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
<span class="text-muted">{sourceTypePresentation.label}</span>
|
||||
</div>
|
||||
<div class="mt-1 space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Confidence</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatConfidenceLabel(change.confidence)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Adapter</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{sourceAdapterPresentation?.label || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Confidence"
|
||||
value={formatConfidenceLabel(change.confidence)}
|
||||
/>
|
||||
<InfoCardKeyValueRow
|
||||
label="Adapter"
|
||||
value={sourceAdapterPresentation?.label || '—'}
|
||||
/>
|
||||
<Show when={change.actor}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Actor</span>
|
||||
<span class="font-medium text-base-content">{change.actor}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Actor"
|
||||
value={change.actor}
|
||||
valueClass="break-words"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={change.from || change.to}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Transition</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{change.from || '—'} → {change.to || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Transition"
|
||||
value={`${change.from || '—'} → ${change.to || '—'}`}
|
||||
valueClass="break-words"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={change.reason}>
|
||||
@@ -726,27 +722,24 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5 text-[11px]">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Containers</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(drawer.dockerContainerCount())}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Updates</span>
|
||||
<span
|
||||
class={`font-medium ${drawer.dockerUpdatesAvailable() > 0 ? 'text-sky-700 dark:text-sky-300' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(drawer.dockerUpdatesAvailable())}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Containers"
|
||||
value={formatInteger(drawer.dockerContainerCount())}
|
||||
/>
|
||||
<InfoCardKeyValueRow
|
||||
label="Updates"
|
||||
value={formatInteger(drawer.dockerUpdatesAvailable())}
|
||||
valueClass={
|
||||
drawer.dockerUpdatesAvailable() > 0
|
||||
? 'text-sky-700 dark:text-sky-300'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<Show when={drawer.dockerUpdatesCheckedRelative()}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Checked</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{drawer.dockerUpdatesCheckedRelative()}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Checked"
|
||||
value={drawer.dockerUpdatesCheckedRelative()}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={drawer.showDockerUpdateControls()}>
|
||||
@@ -758,24 +751,24 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
}
|
||||
>
|
||||
<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">Action</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatIdentifierLabel(drawer.dockerHostCommand()?.type, {
|
||||
fallback: 'command',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between gap-2">
|
||||
<span class="text-muted">State</span>
|
||||
<span
|
||||
class={`font-medium ${drawer.dockerHostCommandActive() ? 'text-sky-700 dark:text-sky-300' : 'text-base-content'}`}
|
||||
>
|
||||
{formatIdentifierLabel(drawer.dockerHostCommand()?.status, {
|
||||
fallback: 'unknown',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Action"
|
||||
value={formatIdentifierLabel(drawer.dockerHostCommand()?.type, {
|
||||
fallback: 'command',
|
||||
})}
|
||||
/>
|
||||
<InfoCardKeyValueRow
|
||||
class="mt-1"
|
||||
label="State"
|
||||
value={formatIdentifierLabel(drawer.dockerHostCommand()?.status, {
|
||||
fallback: 'unknown',
|
||||
})}
|
||||
valueClass={
|
||||
drawer.dockerHostCommandActive()
|
||||
? 'text-sky-700 dark:text-sky-300'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<Show when={drawer.dockerHostCommand()?.message}>
|
||||
<div
|
||||
class="mt-1 text-muted truncate"
|
||||
@@ -885,33 +878,26 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
</Show>
|
||||
</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 ${connection.text}`}>
|
||||
{connection.label}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="State"
|
||||
value={connection.label}
|
||||
valueClass={connection.text}
|
||||
/>
|
||||
<Show when={pbs().version}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Version</span>
|
||||
<span class="font-medium text-base-content">{pbs().version}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow label="Version" value={pbs().version} />
|
||||
</Show>
|
||||
<Show when={pbs().uptimeSeconds || 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(pbs().uptimeSeconds ?? resource.uptime ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Uptime"
|
||||
value={formatUptime(pbs().uptimeSeconds ?? resource.uptime ?? 0)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={drawer.pbsActiveTaskCount() > 0}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Active tasks</span>
|
||||
<span class="font-medium text-emerald-700 dark:text-emerald-300">
|
||||
{formatInteger(drawer.pbsActiveTaskCount())}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Active tasks"
|
||||
value={formatInteger(drawer.pbsActiveTaskCount())}
|
||||
valueClass="text-emerald-700 dark:text-emerald-300"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={drawer.showPbsJobDetail()}>
|
||||
<div class="space-y-1.5 border-t border-indigo-200 pt-2 dark:border-indigo-700">
|
||||
@@ -1141,25 +1127,19 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
</Show>
|
||||
</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 ${connection.text}`}>
|
||||
{connection.label}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="State"
|
||||
value={connection.label}
|
||||
valueClass={connection.text}
|
||||
/>
|
||||
<Show when={pmg().version}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Version</span>
|
||||
<span class="font-medium text-base-content">{pmg().version}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow label="Version" value={pmg().version} />
|
||||
</Show>
|
||||
<Show when={pmg().uptimeSeconds || 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(pmg().uptimeSeconds ?? resource.uptime ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Uptime"
|
||||
value={formatUptime(pmg().uptimeSeconds ?? resource.uptime ?? 0)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={drawer.showPmgMailFlowDetail()}>
|
||||
<div class="space-y-1.5 border-t border-rose-200 pt-2 dark:border-rose-700">
|
||||
@@ -1187,22 +1167,21 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
class="space-y-1.5 rounded border border-dashed border-rose-200 bg-surface px-2 py-1.5 text-[10px] dark:border-rose-700"
|
||||
>
|
||||
<Show when={pmg().nodeCount}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Nodes</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(pmg().nodeCount)}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Nodes"
|
||||
value={formatInteger(pmg().nodeCount)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={drawer.pmgUpdatedRelative()}>
|
||||
<div
|
||||
class={`flex items-center justify-between gap-2 ${pmg().nodeCount ? 'border-t border-rose-200 pt-1.5 dark:border-rose-700' : ''}`}
|
||||
>
|
||||
<span class="text-muted">Updated</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{drawer.pmgUpdatedRelative()}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
class={
|
||||
pmg().nodeCount
|
||||
? 'border-t border-rose-200 pt-1.5 dark:border-rose-700'
|
||||
: ''
|
||||
}
|
||||
label="Updated"
|
||||
value={drawer.pmgUpdatedRelative()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -1213,14 +1192,13 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
<div class="mt-2 space-y-1.5 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<For each={drawer.pmgVisibleQueueBreakdown()}>
|
||||
{(entry) => (
|
||||
<div class="flex items-center justify-between gap-2 text-muted">
|
||||
<span>{entry.label}</span>
|
||||
<span
|
||||
class={`font-medium ${entry.warn ? 'text-amber-600 dark:text-amber-400' : 'text-base-content'}`}
|
||||
>
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label={entry.label}
|
||||
value={formatInteger(entry.value)}
|
||||
valueClass={
|
||||
entry.warn ? 'text-amber-600 dark:text-amber-400' : ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
@@ -1232,12 +1210,10 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
<div class="mt-2 space-y-1.5 border-t border-rose-200 pt-2 text-[10px] dark:border-rose-700">
|
||||
<For each={drawer.pmgVisibleMailBreakdown()}>
|
||||
{(entry) => (
|
||||
<div class="flex items-center justify-between gap-2 text-muted">
|
||||
<span>{entry.label}</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatInteger(entry.value)}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label={entry.label}
|
||||
value={formatInteger(entry.value)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
+6
@@ -802,6 +802,12 @@ describe('ResourceDetailDrawer change history section', () => {
|
||||
expect(panel.getByText('Routine restart requested')).toBeInTheDocument();
|
||||
expect(panel.getByText('Confidence')).toBeInTheDocument();
|
||||
expect(panel.getByText('Adapter')).toBeInTheDocument();
|
||||
expect(panel.getByText('Confidence').parentElement).toHaveClass(
|
||||
'justify-between',
|
||||
'lg:grid',
|
||||
'lg:grid-cols-[7rem_minmax(0,1fr)]',
|
||||
);
|
||||
expect(panel.getByText('High')).toHaveClass('text-right', 'lg:text-left');
|
||||
expect(panel.getByText('Metadata')).toBeInTheDocument();
|
||||
expect(panel.getByText(/"ticket": "INC-1234"/)).toBeInTheDocument();
|
||||
expect(panel.queryByText('Capabilities')).toBeNull();
|
||||
|
||||
+17
@@ -113,6 +113,12 @@ describe('ResourceDetailDrawer service cards', () => {
|
||||
expect(serviceDetails.queryByText('Connection')).toBeNull();
|
||||
expect(serviceDetails.getAllByText('State').length).toBeGreaterThan(0);
|
||||
expect(serviceDetails.getAllByText('pbs-main.local').length).toBeGreaterThan(0);
|
||||
expect(serviceDetails.getByText('State').parentElement).toHaveClass(
|
||||
'justify-between',
|
||||
'lg:grid',
|
||||
'lg:grid-cols-[7rem_minmax(0,1fr)]',
|
||||
);
|
||||
expect(serviceDetails.getByText('Healthy')).toHaveClass('text-right', 'lg:text-left');
|
||||
expect(queryByText('Backup summary')).toBeNull();
|
||||
expect(queryByText('Job breakdown')).toBeNull();
|
||||
expect(queryByText('Types')).toBeNull();
|
||||
@@ -307,6 +313,12 @@ describe('ResourceDetailDrawer service cards', () => {
|
||||
expect(serviceDetails.queryByText('Connection')).toBeNull();
|
||||
expect(serviceDetails.getAllByText('State').length).toBeGreaterThan(0);
|
||||
expect(serviceDetails.getAllByText('pmg-main.local').length).toBeGreaterThan(0);
|
||||
expect(serviceDetails.getByText('State').parentElement).toHaveClass(
|
||||
'justify-between',
|
||||
'lg:grid',
|
||||
'lg:grid-cols-[7rem_minmax(0,1fr)]',
|
||||
);
|
||||
expect(serviceDetails.getByText('Healthy')).toHaveClass('text-right', 'lg:text-left');
|
||||
expect(queryByText('Mail flow summary')).toBeNull();
|
||||
expect(queryByText('Queue breakdown')).toBeNull();
|
||||
expect(queryByText('Mail processing')).toBeNull();
|
||||
@@ -384,6 +396,11 @@ describe('ResourceDetailDrawer service cards', () => {
|
||||
expect(getByText('18 containers · 4 updates')).toBeInTheDocument();
|
||||
fireEvent.click(getByRole('button', { name: 'Show service' }));
|
||||
expect(getByText('Docker runtime')).toBeInTheDocument();
|
||||
expect(getByText('Containers').parentElement).toHaveClass(
|
||||
'justify-between',
|
||||
'lg:grid',
|
||||
'lg:grid-cols-[7rem_minmax(0,1fr)]',
|
||||
);
|
||||
expect(queryByText('Container Updates')).toBeNull();
|
||||
expect(queryByText('Check now')).toBeNull();
|
||||
expect(queryByText('Show update controls')).toBeNull();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type AvailabilityTarget,
|
||||
} from '@/api/availabilityTargets';
|
||||
import type { AvailabilityProbeSuggestion } from '@/types/discovery';
|
||||
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
|
||||
import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
|
||||
interface AvailabilityProbeSuggestionCardProps {
|
||||
suggestion: AvailabilityProbeSuggestion;
|
||||
@@ -64,28 +64,19 @@ export function AvailabilityProbeSuggestionCard(props: AvailabilityProbeSuggesti
|
||||
</div>
|
||||
<div class="space-y-1.5 text-[11px]">
|
||||
<Show when={state() !== 'created'}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Service</span>
|
||||
<span
|
||||
class="font-medium text-base-content truncate ml-2"
|
||||
title={props.suggestion.service_name}
|
||||
>
|
||||
{props.suggestion.service_name}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Probe</span>
|
||||
<span class="font-medium text-base-content">{protocolLabel()}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Target</span>
|
||||
<span
|
||||
class="font-medium text-base-content truncate ml-2"
|
||||
title={props.suggestion.address}
|
||||
>
|
||||
{props.suggestion.address}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Service"
|
||||
value={props.suggestion.service_name}
|
||||
valueClass="truncate"
|
||||
valueTitle={props.suggestion.service_name}
|
||||
/>
|
||||
<InfoCardKeyValueRow label="Probe" value={protocolLabel()} />
|
||||
<InfoCardKeyValueRow
|
||||
label="Target"
|
||||
value={props.suggestion.address}
|
||||
valueClass="truncate"
|
||||
valueTitle={props.suggestion.address}
|
||||
/>
|
||||
<Show when={state() === 'error'}>
|
||||
<div class="text-[10px] text-red-600 dark:text-red-400 mt-1">{error()}</div>
|
||||
</Show>
|
||||
|
||||
+6
@@ -21,6 +21,12 @@ describe('AvailabilityProbeStatusCard', () => {
|
||||
expect(screen.getByText('Not checked')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Down')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('freshness unknown')).toBeInTheDocument();
|
||||
expect(screen.getByText('Latency').parentElement).toHaveClass(
|
||||
'justify-between',
|
||||
'lg:grid',
|
||||
'lg:grid-cols-[7rem_minmax(0,1fr)]',
|
||||
);
|
||||
expect(screen.getByText('freshness unknown')).toHaveClass('text-right', 'lg:text-left');
|
||||
});
|
||||
|
||||
it('shows stale evidence and an unresolved canonical resource link', () => {
|
||||
|
||||
@@ -1171,6 +1171,7 @@ describe('shared primitive guardrails', () => {
|
||||
'src/components/shared/cards/RootDiskCard.tsx',
|
||||
'src/components/shared/cards/SystemInfoCard.tsx',
|
||||
'src/components/shared/cards/TemperaturesCard.tsx',
|
||||
'src/components/Workloads/AvailabilityProbeSuggestionCard.tsx',
|
||||
'src/components/Workloads/DrawerDiskListCard.tsx',
|
||||
'src/features/docker/DockerHostDrawerOverview.tsx',
|
||||
'src/features/storageBackups/detailPresentation.ts',
|
||||
@@ -1233,6 +1234,19 @@ describe('shared primitive guardrails', () => {
|
||||
expect(source).toContain('InfoCardKeyValueRow');
|
||||
expect(source).not.toContain('flex items-center justify-between');
|
||||
}
|
||||
|
||||
for (const consumerPath of [
|
||||
'src/components/Infrastructure/AvailabilityProbeStatusCard.tsx',
|
||||
'src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx',
|
||||
'src/components/Workloads/AvailabilityProbeSuggestionCard.tsx',
|
||||
'src/features/docker/DockerHostDrawerOverview.tsx',
|
||||
]) {
|
||||
const source = readFrontendSource(consumerPath);
|
||||
expect(source).toContain('InfoCardKeyValueRow');
|
||||
expect(source).not.toMatch(
|
||||
/class="flex items-(?:center|start) justify-between gap-2">\s*<span class="text-muted">/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps shared subtabs as one primitive and leaves shell styling to owning surfaces', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type DrawerDiskListItem,
|
||||
} from '@/components/Workloads/DrawerDiskListCard';
|
||||
import { AvailabilityProbeStatusCards } from '@/components/Infrastructure/AvailabilityProbeStatusCard';
|
||||
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
|
||||
import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
import { TechnicalDetailsSection } from '@/components/shared/TechnicalDetailsDisclosure';
|
||||
import { DrawerAttentionSection } from '@/components/shared/DrawerAttentionSection';
|
||||
import {
|
||||
@@ -99,32 +99,19 @@ export function DockerHostDrawerManagement(props: DockerHostDrawerOverviewProps)
|
||||
Container updates
|
||||
</h3>
|
||||
<div class="space-y-1.5 text-[11px]">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Available</span>
|
||||
<span class="font-medium text-base-content">{updatesAvailable()}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow label="Available" value={updatesAvailable()} />
|
||||
<Show when={checkedAtMillis()} keyed>
|
||||
{(timestamp) => (
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Last checked</span>
|
||||
<span class="font-medium text-base-content">{formatRelativeTime(timestamp)}</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow label="Last checked" value={formatRelativeTime(timestamp)} />
|
||||
)}
|
||||
</Show>
|
||||
<Show when={hostCommand()?.type || hostCommand()?.status}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">
|
||||
{titleCase(cleanText(hostCommand()?.type).replace(/_/g, ' ') || 'Command')}
|
||||
</span>
|
||||
<span
|
||||
class={`truncate text-right font-medium ${
|
||||
hostCommandActive() ? 'text-sky-700 dark:text-sky-300' : 'text-base-content'
|
||||
}`}
|
||||
title={hostCommand()?.failureReason || hostCommand()?.message || undefined}
|
||||
>
|
||||
{titleCase(cleanText(hostCommand()?.status).replace(/_/g, ' ') || 'unknown')}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label={titleCase(cleanText(hostCommand()?.type).replace(/_/g, ' ') || 'Command')}
|
||||
value={titleCase(cleanText(hostCommand()?.status).replace(/_/g, ' ') || 'unknown')}
|
||||
valueClass={`truncate ${hostCommandActive() ? 'text-sky-700 dark:text-sky-300' : ''}`}
|
||||
valueTitle={hostCommand()?.failureReason || hostCommand()?.message || undefined}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={updateActions.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">
|
||||
|
||||
Reference in New Issue
Block a user