Adopt shared WebUI layout primitives

This commit is contained in:
2026-08-18 11:30:40 +02:00
parent d1114ee637
commit de5bf357c9
2 changed files with 14 additions and 10 deletions
+6 -4
View File
@@ -1,3 +1,5 @@
import { DescriptionList } from "@govoplan/core-webui";
import { MetricGrid } from "@govoplan/core-webui";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
DismissibleAlert, DismissibleAlert,
@@ -43,7 +45,7 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
<div className="button-row compact-actions"> <div className="button-row compact-actions">
<DocumentationHelpLink reference={OPS_DOCUMENTATION} /> <DocumentationHelpLink reference={OPS_DOCUMENTATION} />
</div> </div>
<div className="metric-grid inside dashboard-widget-metrics"> <MetricGrid columns={3} spacing="none">
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} /> <MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
<MetricCard label="Workers" value={status?.summary.celery_enabled ? workerMetrics?.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !missingQueues.length ? "good" : "warning"} detail={missingQueues.length ? `${missingQueues.length} queue(s) without a consumer` : status?.summary.celery_enabled ? "All configured queues covered" : "Single-process mode"} /> <MetricCard label="Workers" value={status?.summary.celery_enabled ? workerMetrics?.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !missingQueues.length ? "good" : "warning"} detail={missingQueues.length ? `${missingQueues.length} queue(s) without a consumer` : status?.summary.celery_enabled ? "All configured queues covered" : "Single-process mode"} />
<MetricCard label="Active tasks" value={workerMetrics?.active_tasks ?? 0} tone="info" detail={status?.summary.celery_enabled ? "Reported by live workers" : "Workers disabled"} /> <MetricCard label="Active tasks" value={workerMetrics?.active_tasks ?? 0} tone="info" detail={status?.summary.celery_enabled ? "Reported by live workers" : "Workers disabled"} />
@@ -54,16 +56,16 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
<MetricCard label="Backup" value={status?.summary.backup_state ?? "unknown"} tone={status?.summary.backup_state === "ok" ? "good" : "warning"} detail="Backup and restore evidence" /> <MetricCard label="Backup" value={status?.summary.backup_state ?? "unknown"} tone={status?.summary.backup_state === "ok" ? "good" : "warning"} detail="Backup and restore evidence" />
<MetricCard label="Probes" value={status?.summary.operational_probe_count ?? 0} tone="neutral" detail="Module-owned operational checks" /> <MetricCard label="Probes" value={status?.summary.operational_probe_count ?? 0} tone="neutral" detail="Module-owned operational checks" />
<MetricCard label="Warnings" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="Current health checks" /> <MetricCard label="Warnings" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="Current health checks" />
</div> </MetricGrid>
{status?.readiness.blockers.length ? {status?.readiness.blockers.length ?
<dl className="detail-list dashboard-compact-list below-grid"> <DescriptionList variant="inline" termWidth="compact" className="below-grid">
{status.readiness.blockers.slice(0, 3).map((blocker) => {status.readiness.blockers.slice(0, 3).map((blocker) =>
<div key={blocker.id}> <div key={blocker.id}>
<dt><StatusBadge status={blocker.state === "error" ? "error" : "warning"} label={blocker.state} /></dt> <dt><StatusBadge status={blocker.state === "error" ? "error" : "warning"} label={blocker.state} /></dt>
<dd><strong>{blocker.label}</strong><span className="muted"> · {blocker.detail}</span></dd> <dd><strong>{blocker.label}</strong><span className="muted"> · {blocker.detail}</span></dd>
</div> </div>
)} )}
</dl> : </DescriptionList> :
<p className="muted below-grid">No readiness blockers reported.</p> <p className="muted below-grid">No readiness blockers reported.</p>
} }
</LoadingFrame>); </LoadingFrame>);
+8 -6
View File
@@ -1,3 +1,5 @@
import { DescriptionList } from "@govoplan/core-webui";
import { MetricGrid } from "@govoplan/core-webui";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react"; import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
import { ContentGrid, import { ContentGrid,
@@ -146,7 +148,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
)} )}
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e"> <LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
<div className="metric-grid"> <MetricGrid>
<MetricCard label="i18n:govoplan-ops.profile.ff4fc027" value={status?.summary.active_profile ?? "-"} tone="info" detail={status?.summary.database_url ?? "i18n:govoplan-ops.no_database_url.51a2db0c"} /> <MetricCard label="i18n:govoplan-ops.profile.ff4fc027" value={status?.summary.active_profile ?? "-"} tone="info" detail={status?.summary.database_url ?? "i18n:govoplan-ops.no_database_url.51a2db0c"} />
<MetricCard label="i18n:govoplan-ops.readiness.1db9d6fb" value={ready ? "ready" : "not ready"} tone={ready ? "good" : "danger"} detail={status?.readiness.blockers.length ? `${status.readiness.blockers.length} blocker(s)` : "i18n:govoplan-ops.no_readiness_blockers.0df259bd"} /> <MetricCard label="i18n:govoplan-ops.readiness.1db9d6fb" value={ready ? "ready" : "not ready"} tone={ready ? "good" : "danger"} detail={status?.readiness.blockers.length ? `${status.readiness.blockers.length} blocker(s)` : "i18n:govoplan-ops.no_readiness_blockers.0df259bd"} />
<MetricCard label="i18n:govoplan-ops.modules.04e9462c" value={status?.summary.module_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d" /> <MetricCard label="i18n:govoplan-ops.modules.04e9462c" value={status?.summary.module_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d" />
@@ -165,7 +167,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
<MetricCard label="Recovery" value={status?.summary.recovery_required_count ?? 0} tone={status?.summary.recovery_required_count ? "danger" : "good"} detail="Operations requiring recovery attention" /> <MetricCard label="Recovery" value={status?.summary.recovery_required_count ?? 0} tone={status?.summary.recovery_required_count ? "danger" : "good"} detail="Operations requiring recovery attention" />
<MetricCard label="Provider bindings" value={status?.governance.summary.configured_external_provider_count ?? 0} tone={status?.governance.summary.provider_attention_count ? "warning" : "good"} detail={`${status?.governance.summary.provider_attention_count ?? 0} requiring attention`} /> <MetricCard label="Provider bindings" value={status?.governance.summary.configured_external_provider_count ?? 0} tone={status?.governance.summary.provider_attention_count ? "warning" : "good"} detail={`${status?.governance.summary.provider_attention_count ?? 0} requiring attention`} />
<MetricCard label="i18n:govoplan-ops.infrastructure_capabilities" value={status?.summary.infrastructure_capability_count ?? 0} tone={status?.infrastructure.available ? "good" : "neutral"} detail={i18nMessage("i18n:govoplan-ops.pending_post_install_tasks", { value0: status?.summary.pending_post_install_task_count ?? 0 })} /> <MetricCard label="i18n:govoplan-ops.infrastructure_capabilities" value={status?.summary.infrastructure_capability_count ?? 0} tone={status?.infrastructure.available ? "good" : "neutral"} detail={i18nMessage("i18n:govoplan-ops.pending_post_install_tasks", { value0: status?.summary.pending_post_install_task_count ?? 0 })} />
</div> </MetricGrid>
<ContentGrid columns={2} collapseAt="workspace" className=""> <ContentGrid columns={2} collapseAt="workspace" className="">
<Card title="i18n:govoplan-ops.health_checks.201c869f"> <Card title="i18n:govoplan-ops.health_checks.201c869f">
@@ -196,12 +198,12 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
<Card title="i18n:govoplan-ops.infrastructure_capabilities"> <Card title="i18n:govoplan-ops.infrastructure_capabilities">
<InfrastructureCapabilityTable items={status?.infrastructure.capabilities ?? []} /> <InfrastructureCapabilityTable items={status?.infrastructure.capabilities ?? []} />
{(status?.infrastructure.post_install_tasks.length ?? 0) > 0 && <dl className="detail-list"> {(status?.infrastructure.post_install_tasks.length ?? 0) > 0 && <DescriptionList variant="inline">
{status?.infrastructure.post_install_tasks.map((task) => <div key={task.resume_key}> {status?.infrastructure.post_install_tasks.map((task) => <div key={task.resume_key}>
<dt><StatusBadge status="warning" label={task.state} /></dt> <dt><StatusBadge status="warning" label={task.state} /></dt>
<dd><strong>{task.summary}</strong><span className="muted"> · {task.owner_module} · {task.required_inputs.join(", ")}</span></dd> <dd><strong>{task.summary}</strong><span className="muted"> · {task.owner_module} · {task.required_inputs.join(", ")}</span></dd>
</div>)} </div>)}
</dl>} </DescriptionList>}
</Card> </Card>
<Card title="i18n:govoplan-ops.sizing_assumptions.6ade9a90"> <Card title="i18n:govoplan-ops.sizing_assumptions.6ade9a90">
@@ -476,14 +478,14 @@ function GovernanceTable({ modules }: { modules: OpsGovernanceModule[] }) {
function CheckList({ checks }: {checks: OpsCheck[];}) { function CheckList({ checks }: {checks: OpsCheck[];}) {
if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>; if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>;
return ( return (
<dl className="detail-list"> <DescriptionList variant="inline">
{checks.map((check) => {checks.map((check) =>
<div key={check.id}> <div key={check.id}>
<dt><StatusBadge status={stateTone(check.state)} label={check.state} /></dt> <dt><StatusBadge status={stateTone(check.state)} label={check.state} /></dt>
<dd><strong>{check.label}</strong><span className="muted"> · {check.detail}</span></dd> <dd><strong>{check.label}</strong><span className="muted"> · {check.detail}</span></dd>
</div> </div>
)} )}
</dl>); </DescriptionList>);
} }