Migrate Ops interface patterns
This commit is contained in:
@@ -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)
|
||||
}]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user