Migrate Ops interface patterns

This commit is contained in:
2026-08-03 15:18:32 +02:00
parent cdcb477b55
commit 2b3264372b
8 changed files with 367 additions and 25 deletions
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import {
DismissibleAlert,
DocumentationHelpLink,
LoadingFrame,
MetricCard,
StatusBadge,
@@ -8,6 +9,7 @@ import {
type ApiSettings
} from "@govoplan/core-webui";
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
import { OPS_DOCUMENTATION } from "./interfacePatterns";
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
const [status, setStatus] = useState<OpsStatus | null>(null);
@@ -38,6 +40,9 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
return (
<LoadingFrame loading={loading} label="Loading operations status">
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
<div className="button-row compact-actions">
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
</div>
<div className="metric-grid inside dashboard-widget-metrics">
<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"} />
+62 -22
View File
@@ -1,18 +1,22 @@
import { useEffect, useState } from "react";
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
import {
ActionBlockerHint,
Button,
Card,
ConfirmDialog,
DataGrid,
DismissibleAlert,
DocumentationHelpLink,
LoadingFrame,
MetricCard,
PageScrollViewport,
PageTitle,
StatusBadge,
TableActionGroup,
adminErrorMessage,
hasAnyScope,
i18nMessage,
type ApiSettings,
type AuthInfo,
type DataGridColumn } from
@@ -30,10 +34,16 @@ import {
type OpsSizingAssumption,
type OpsStatus
} from "../../api/ops";
import {
OPS_DOCUMENTATION,
OPS_I18N,
OPS_RECOVERY_DOCUMENTATION,
} from "./interfacePatterns";
export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
const [status, setStatus] = useState<OpsStatus | null>(null);
const [loading, setLoading] = useState(true);
const [runningProbes, setRunningProbes] = useState(false);
const [error, setError] = useState("");
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
const [nodeActionId, setNodeActionId] = useState("");
@@ -51,6 +61,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
}
async function runChecks() {
setRunningProbes(true);
setLoading(true);
setError("");
try {
@@ -58,6 +69,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setRunningProbes(false);
setLoading(false);
}
}
@@ -97,6 +109,13 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
const errorCount = checks.filter((item) => item.state === "error").length;
const ready = status?.readiness.ready ?? false;
const canRunChecks = hasAnyScope(auth, ["ops:operations:run", "system:settings:write"]);
const runProbesDisabledReason = runningProbes
? OPS_I18N.runningProbes
: loading
? OPS_I18N.loading
: !canRunChecks
? OPS_I18N.runPermissionRequired
: undefined;
const queueDepths = status?.summary.worker_metrics.queue_depths ?? {};
const queuedTasks = Object.values(queueDepths).reduce((total, value) => total + value, 0);
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
@@ -110,12 +129,25 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
</div>
<div className="button-row compact-actions">
{canRunChecks ? <Button variant="primary" onClick={() => void runChecks()} disabled={loading}>Run probes</Button> : null}
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
<Button variant="primary" onClick={() => void runChecks()} disabled={Boolean(runProbesDisabledReason)} disabledReason={runProbesDisabledReason}>i18n:govoplan-ops.surface.run_probes</Button>
<Button onClick={() => void load()} disabled={loading} disabledReason={loading ? OPS_I18N.loading : undefined}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{status && !ready && (
<ActionBlockerHint
reason={{
summary: "i18n:govoplan-ops.readiness_blocked_summary",
details: i18nMessage("i18n:govoplan-ops.readiness_blocked_details", { value0: status.readiness.blockers.length }),
requiredAction: "i18n:govoplan-ops.readiness_blocked_action",
actor: "i18n:govoplan-ops.operations_operator",
target: "i18n:govoplan-ops.readiness_blocked_target"
}}
documentation={OPS_RECOVERY_DOCUMENTATION}
/>
)}
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
<div className="metric-grid">
@@ -172,9 +204,9 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
</LoadingFrame>
<ConfirmDialog
open={Boolean(drainTarget)}
title="Drain runtime node"
message={`Stop routing new work to ${drainTarget?.node_id ?? "this node"}. In-flight work is allowed to finish.`}
confirmLabel="Drain node"
title="i18n:govoplan-ops.surface.drain_node"
message={i18nMessage("i18n:govoplan-ops.drain_runtime_node_message", { value0: drainTarget?.node_id ?? "i18n:govoplan-ops.this_node" })}
confirmLabel="i18n:govoplan-ops.drain_node"
busy={Boolean(nodeActionId)}
onCancel={() => setDrainTarget(null)}
onConfirm={() => void confirmDrain()}
@@ -244,24 +276,32 @@ function RuntimeNodeTable({
width: 56,
value: (node) => node.state,
render: (node) => {
if (!canManage || node.state === "stopped") return null;
const busy = busyNodeId === node.node_id;
return node.state === "draining" ? (
<Button
variant="ghost"
title="Cancel drain"
aria-label={`Cancel drain for ${node.node_id}`}
disabled={busy}
onClick={() => onCancelDrain(node)}
><PlayCircle size={16} /></Button>
) : (
<Button
variant="ghost"
title="Drain node"
aria-label={`Drain ${node.node_id}`}
disabled={busy || node.stale}
onClick={() => onDrain(node)}
><PauseCircle size={16} /></Button>
const cancelling = node.state === "draining";
const disabledReason = !canManage
? OPS_I18N.runPermissionRequired
: busy
? OPS_I18N.nodeActionActive
: node.state === "stopped"
? OPS_I18N.stoppedNode
: !cancelling && node.stale
? OPS_I18N.staleNode
: undefined;
return (
<TableActionGroup
minimumSlots={1}
label={i18nMessage("i18n:govoplan-ops.lifecycle_actions_for_value", { value0: node.node_id })}
actions={[{
id: cancelling ? "cancel-drain" : "drain",
label: cancelling
? i18nMessage("i18n:govoplan-ops.cancel_drain_for_value", { value0: node.node_id })
: i18nMessage("i18n:govoplan-ops.drain_value", { value0: node.node_id }),
icon: cancelling ? <PlayCircle size={16} /> : <PauseCircle size={16} />,
disabled: Boolean(disabledReason),
disabledReason,
onClick: () => cancelling ? onCancelDrain(node) : onDrain(node)
}]}
/>
);
}
}
@@ -0,0 +1,20 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const OPS_DOCUMENTATION = {
topicId: "ops.health-governance-and-sizing",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const OPS_RECOVERY_DOCUMENTATION = {
topicId: "ops.runtime-coordination-and-recovery",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const OPS_I18N = {
loading: "i18n:govoplan-ops.reason.loading",
runningProbes: "i18n:govoplan-ops.reason.running_probes",
runPermissionRequired: "i18n:govoplan-ops.reason.run_permission_required",
nodeActionActive: "i18n:govoplan-ops.reason.node_action_active",
staleNode: "i18n:govoplan-ops.reason.stale_node",
stoppedNode: "i18n:govoplan-ops.reason.stopped_node"
} as const;
+56
View File
@@ -2,6 +2,34 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"i18n:govoplan-ops.surface.navigation": "Operations navigation",
"i18n:govoplan-ops.surface.page": "Operations workspace",
"i18n:govoplan-ops.surface.summary": "Operations summary",
"i18n:govoplan-ops.surface.health": "Health checks",
"i18n:govoplan-ops.surface.runtime": "Runtime cluster",
"i18n:govoplan-ops.surface.recovery": "Recovery evidence",
"i18n:govoplan-ops.surface.governance": "Governance inventory",
"i18n:govoplan-ops.surface.deployment": "Deployment profiles",
"i18n:govoplan-ops.surface.sizing": "Sizing assumptions",
"i18n:govoplan-ops.surface.run_probes": "Run operational probes",
"i18n:govoplan-ops.surface.drain_node": "Drain runtime node",
"i18n:govoplan-ops.reason.loading": "Operations status is loading.",
"i18n:govoplan-ops.reason.running_probes": "Operational probes are already running.",
"i18n:govoplan-ops.reason.run_permission_required": "Operations-run permission is required.",
"i18n:govoplan-ops.reason.node_action_active": "A lifecycle action is already running for this node.",
"i18n:govoplan-ops.reason.stale_node": "A stale node cannot be drained because its current incarnation is not reporting.",
"i18n:govoplan-ops.reason.stopped_node": "A stopped node cannot accept lifecycle actions.",
"i18n:govoplan-ops.readiness_blocked_summary": "The runtime is not ready for normal traffic.",
"i18n:govoplan-ops.readiness_blocked_details": "{value0} readiness blocker(s) are active.",
"i18n:govoplan-ops.readiness_blocked_action": "Review the health checks, runtime cluster, and owning-module recovery evidence before restoring traffic.",
"i18n:govoplan-ops.operations_operator": "Operations operator",
"i18n:govoplan-ops.readiness_blocked_target": "Health checks, Runtime cluster, and Recovery evidence below",
"i18n:govoplan-ops.lifecycle_actions_for_value": "Lifecycle actions for {value0}",
"i18n:govoplan-ops.cancel_drain_for_value": "Cancel drain for {value0}",
"i18n:govoplan-ops.drain_value": "Drain {value0}",
"i18n:govoplan-ops.drain_runtime_node_message": "Stop routing new work to {value0}. In-flight work is allowed to finish.",
"i18n:govoplan-ops.this_node": "this node",
"i18n:govoplan-ops.drain_node": "Drain node",
"i18n:govoplan-ops.area.2745deba": "Area",
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
"i18n:govoplan-ops.authority.8802e425": "Authority",
@@ -43,6 +71,34 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
},
"de": {
"i18n:govoplan-ops.surface.navigation": "Betriebsnavigation",
"i18n:govoplan-ops.surface.page": "Betriebsarbeitsbereich",
"i18n:govoplan-ops.surface.summary": "Betriebsübersicht",
"i18n:govoplan-ops.surface.health": "Systemprüfungen",
"i18n:govoplan-ops.surface.runtime": "Laufzeitcluster",
"i18n:govoplan-ops.surface.recovery": "Wiederherstellungsnachweise",
"i18n:govoplan-ops.surface.governance": "Governance-Inventar",
"i18n:govoplan-ops.surface.deployment": "Bereitstellungsprofile",
"i18n:govoplan-ops.surface.sizing": "Dimensionierungsannahmen",
"i18n:govoplan-ops.surface.run_probes": "Betriebsprüfungen ausführen",
"i18n:govoplan-ops.surface.drain_node": "Laufzeitknoten leeren",
"i18n:govoplan-ops.reason.loading": "Der Betriebsstatus wird geladen.",
"i18n:govoplan-ops.reason.running_probes": "Betriebsprüfungen werden bereits ausgeführt.",
"i18n:govoplan-ops.reason.run_permission_required": "Die Berechtigung zum Ausführen von Betriebsprüfungen ist erforderlich.",
"i18n:govoplan-ops.reason.node_action_active": "Für diesen Knoten wird bereits eine Lebenszyklusaktion ausgeführt.",
"i18n:govoplan-ops.reason.stale_node": "Ein veralteter Knoten kann nicht geleert werden, weil seine aktuelle Instanz keine Statusmeldungen sendet.",
"i18n:govoplan-ops.reason.stopped_node": "Ein gestoppter Knoten kann keine Lebenszyklusaktionen annehmen.",
"i18n:govoplan-ops.readiness_blocked_summary": "Die Laufzeitumgebung ist nicht für normalen Datenverkehr bereit.",
"i18n:govoplan-ops.readiness_blocked_details": "{value0} Bereitschaftsblocker sind aktiv.",
"i18n:govoplan-ops.readiness_blocked_action": "Prüfen Sie Systemprüfungen, Laufzeitcluster und Wiederherstellungsnachweise der zuständigen Module, bevor der Datenverkehr wieder freigegeben wird.",
"i18n:govoplan-ops.operations_operator": "Betriebsverantwortliche Person",
"i18n:govoplan-ops.readiness_blocked_target": "Systemprüfungen, Laufzeitcluster und Wiederherstellungsnachweise weiter unten",
"i18n:govoplan-ops.lifecycle_actions_for_value": "Lebenszyklusaktionen für {value0}",
"i18n:govoplan-ops.cancel_drain_for_value": "Leeren von {value0} abbrechen",
"i18n:govoplan-ops.drain_value": "{value0} leeren",
"i18n:govoplan-ops.drain_runtime_node_message": "Keine neue Arbeit mehr an {value0} weiterleiten. Laufende Arbeit darf abgeschlossen werden.",
"i18n:govoplan-ops.this_node": "diesen Knoten",
"i18n:govoplan-ops.drain_node": "Knoten leeren",
"i18n:govoplan-ops.area.2745deba": "Area",
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
"i18n:govoplan-ops.authority.8802e425": "Berechtigungen",
+13 -2
View File
@@ -36,11 +36,22 @@ export const opsModule: PlatformWebModule = {
optionalDependencies: ["audit", "docs", "notifications"],
translations,
viewSurfaces: [
{ id: "ops.navigation", moduleId: "ops", kind: "navigation", label: "i18n:govoplan-ops.surface.navigation", order: 10 },
{ id: "ops.page", moduleId: "ops", kind: "route", label: "i18n:govoplan-ops.surface.page", order: 20 },
{ id: "ops.page.summary", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.summary", parentId: "ops.page", order: 10 },
{ id: "ops.page.health", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.health", parentId: "ops.page", order: 20 },
{ id: "ops.page.runtime", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.runtime", parentId: "ops.page", order: 30 },
{ id: "ops.page.recovery", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.recovery", parentId: "ops.page", order: 40 },
{ id: "ops.page.governance", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.governance", parentId: "ops.page", order: 50 },
{ id: "ops.page.deployment", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.deployment", parentId: "ops.page", order: 60 },
{ id: "ops.page.sizing", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.sizing", parentId: "ops.page", order: 70 },
{ id: "ops.action.run-probes", moduleId: "ops", kind: "action", label: "i18n:govoplan-ops.surface.run_probes", parentId: "ops.page.health", order: 80 },
{ id: "ops.action.drain-node", moduleId: "ops", kind: "action", label: "i18n:govoplan-ops.surface.drain_node", parentId: "ops.page.runtime", order: 90 },
{ id: "ops.widget.health", moduleId: "ops", kind: "section", label: "Operations health widget", order: 100 }
],
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890 }],
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890, surfaceId: "ops.navigation" }],
routes: [
{ path: "/ops", anyOf: opsReadScopes, order: 890, render: ({ settings, auth }) => createElement(OpsPage, { settings, auth }) }],
{ path: "/ops", anyOf: opsReadScopes, order: 890, surfaceId: "ops.page", render: ({ settings, auth }) => createElement(OpsPage, { settings, auth }) }],
uiCapabilities: {
"dashboard.widgets": dashboardWidgets
}