chore: sync GovOPlaN module split state
This commit is contained in:
30
webui/package.json
Normal file
30
webui/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@govoplan/ops-webui",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.6",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
58
webui/src/api/ops.ts
Normal file
58
webui/src/api/ops.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||
|
||||
export type OpsCheck = {
|
||||
id: string;
|
||||
label: string;
|
||||
state: "ok" | "warning" | "error" | string;
|
||||
detail: string;
|
||||
readiness_critical?: boolean;
|
||||
};
|
||||
|
||||
export type OpsDeploymentProfile = {
|
||||
id: string;
|
||||
name: string;
|
||||
current: boolean;
|
||||
components: string[];
|
||||
fit: string;
|
||||
};
|
||||
|
||||
export type OpsSizingAssumption = {
|
||||
area: string;
|
||||
baseline: string;
|
||||
scale_trigger: string;
|
||||
operator_note: string;
|
||||
};
|
||||
|
||||
export type OpsStatus = {
|
||||
summary: {
|
||||
app_env: string;
|
||||
active_profile: string;
|
||||
module_count: number;
|
||||
celery_enabled: boolean;
|
||||
celery_queues: string[];
|
||||
redis_url: string;
|
||||
maintenance_mode: {
|
||||
enabled: boolean;
|
||||
message?: string | null;
|
||||
};
|
||||
database_url: string;
|
||||
file_storage_backend: string;
|
||||
};
|
||||
readiness: {
|
||||
ready: boolean;
|
||||
profile: string;
|
||||
blockers: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
state: string;
|
||||
detail: string;
|
||||
}>;
|
||||
};
|
||||
checks: OpsCheck[];
|
||||
deployment_profiles: OpsDeploymentProfile[];
|
||||
sizing: OpsSizingAssumption[];
|
||||
};
|
||||
|
||||
export function fetchOpsStatus(settings: ApiSettings): Promise<OpsStatus> {
|
||||
return apiFetch(settings, "/api/v1/ops/status");
|
||||
}
|
||||
54
webui/src/features/ops/OpsHealthWidget.tsx
Normal file
54
webui/src/features/ops/OpsHealthWidget.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
DismissibleAlert,
|
||||
LoadingFrame,
|
||||
MetricCard,
|
||||
StatusBadge,
|
||||
adminErrorMessage,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
|
||||
|
||||
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
|
||||
const [status, setStatus] = useState<OpsStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
fetchOpsStatus(settings).
|
||||
then((next) => {if (!cancelled) setStatus(next);}).
|
||||
catch((err) => {if (!cancelled) setError(adminErrorMessage(err));}).
|
||||
finally(() => {if (!cancelled) setLoading(false);});
|
||||
return () => {cancelled = true;};
|
||||
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, refreshKey]);
|
||||
|
||||
const checks = status?.checks ?? [];
|
||||
const warningCount = checks.filter((item) => item.state === "warning").length;
|
||||
const errorCount = checks.filter((item) => item.state === "error").length;
|
||||
const ready = status?.readiness.ready ?? false;
|
||||
|
||||
return (
|
||||
<LoadingFrame loading={loading} label="Loading operations status">
|
||||
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
||||
<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 ? "split" : "off"} tone={status?.summary.celery_enabled ? "good" : "warning"} detail={status?.summary.celery_queues?.length ? status.summary.celery_queues.join(", ") : "single-process"} />
|
||||
<MetricCard label="Warnings" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="Current health checks" />
|
||||
</div>
|
||||
{status?.readiness.blockers.length ?
|
||||
<dl className="detail-list dashboard-compact-list below-grid">
|
||||
{status.readiness.blockers.slice(0, 3).map((blocker) =>
|
||||
<div key={blocker.id}>
|
||||
<dt><StatusBadge status={blocker.state === "error" ? "error" : "warning"} label={blocker.state} /></dt>
|
||||
<dd><strong>{blocker.label}</strong><span className="muted"> · {blocker.detail}</span></dd>
|
||||
</div>
|
||||
)}
|
||||
</dl> :
|
||||
<p className="muted below-grid">No readiness blockers reported.</p>
|
||||
}
|
||||
</LoadingFrame>);
|
||||
}
|
||||
|
||||
147
webui/src/features/ops/OpsPage.tsx
Normal file
147
webui/src/features/ops/OpsPage.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DismissibleAlert,
|
||||
LoadingFrame,
|
||||
MetricCard,
|
||||
PageTitle,
|
||||
StatusBadge,
|
||||
adminErrorMessage,
|
||||
type ApiSettings } from
|
||||
"@govoplan/core-webui";
|
||||
import { fetchOpsStatus, type OpsCheck, type OpsDeploymentProfile, type OpsSizingAssumption, type OpsStatus } from "../../api/ops";
|
||||
|
||||
export default function OpsPage({ settings }: {settings: ApiSettings;}) {
|
||||
const [status, setStatus] = useState<OpsStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
setStatus(await fetchOpsStatus(settings));
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {void load();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||
|
||||
const checks = status?.checks ?? [];
|
||||
const warningCount = checks.filter((item) => item.state === "warning").length;
|
||||
const errorCount = checks.filter((item) => item.state === "error").length;
|
||||
const ready = status?.readiness.ready ?? false;
|
||||
|
||||
return (
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading split workspace-heading">
|
||||
<div>
|
||||
<PageTitle loading={loading}>i18n:govoplan-ops.ops.907a54c2</PageTitle>
|
||||
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
|
||||
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
|
||||
<div className="metric-grid">
|
||||
<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.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.workers.b6ef3acd" value={status?.summary.celery_enabled ? "split" : "off"} tone={status?.summary.celery_enabled ? "good" : "warning"} detail={status?.summary.celery_queues?.length ? status.summary.celery_queues.join(", ") : "i18n:govoplan-ops.celery_worker_setting.323d7737"} />
|
||||
<MetricCard label="i18n:govoplan-ops.redis.5eaa1f2f" value={status?.summary.redis_url ? "configured" : "-"} tone={status?.summary.celery_enabled ? "info" : "neutral"} detail={status?.summary.redis_url ?? "-"} />
|
||||
<MetricCard label="i18n:govoplan-ops.warnings.1430f976" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="i18n:govoplan-ops.current_health_checks.7830bccf" />
|
||||
</div>
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<Card title="i18n:govoplan-ops.health_checks.201c869f">
|
||||
<CheckList checks={checks} />
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-ops.deployment_profiles.b0caa179">
|
||||
<ProfileList profiles={status?.deployment_profiles ?? []} />
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-ops.sizing_assumptions.6ade9a90">
|
||||
<SizingTable items={status?.sizing ?? []} />
|
||||
</Card>
|
||||
</div>
|
||||
</LoadingFrame>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
function CheckList({ checks }: {checks: OpsCheck[];}) {
|
||||
if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>;
|
||||
return (
|
||||
<dl className="detail-list">
|
||||
{checks.map((check) =>
|
||||
<div key={check.id}>
|
||||
<dt><StatusBadge status={stateTone(check.state)} label={check.state} /></dt>
|
||||
<dd><strong>{check.label}</strong><span className="muted"> · {check.detail}</span></dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>);
|
||||
|
||||
}
|
||||
|
||||
function ProfileList({ profiles }: {profiles: OpsDeploymentProfile[];}) {
|
||||
if (!profiles.length) return <p className="muted">i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db</p>;
|
||||
return (
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr><th>i18n:govoplan-ops.profile.ff4fc027</th><th>i18n:govoplan-ops.status.bae7d5be</th><th>i18n:govoplan-ops.components.9289473e</th><th>i18n:govoplan-ops.fit.dab564d8</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{profiles.map((profile) =>
|
||||
<tr key={profile.id}>
|
||||
<td><strong>{profile.name}</strong><span className="muted block">{profile.id}</span></td>
|
||||
<td><StatusBadge status={profile.current ? "success" : "inactive"} label={profile.current ? "current" : "reference"} /></td>
|
||||
<td>{profile.components.join(", ")}</td>
|
||||
<td>{profile.fit}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
function SizingTable({ items }: {items: OpsSizingAssumption[];}) {
|
||||
if (!items.length) return <p className="muted">i18n:govoplan-ops.no_sizing_assumptions_reported.17515959</p>;
|
||||
return (
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr><th>i18n:govoplan-ops.area.2745deba</th><th>i18n:govoplan-ops.baseline.e6ab7982</th><th>i18n:govoplan-ops.scale_trigger.1c85e10e</th><th>i18n:govoplan-ops.operator_note.1dc58f7b</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) =>
|
||||
<tr key={item.area}>
|
||||
<td><strong>{item.area}</strong></td>
|
||||
<td>{item.baseline}</td>
|
||||
<td>{item.scale_trigger}</td>
|
||||
<td>{item.operator_note}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
function stateTone(state: string): string {
|
||||
if (state === "ok") return "success";
|
||||
if (state === "warning") return "warning";
|
||||
if (state === "error") return "error";
|
||||
return "inactive";
|
||||
}
|
||||
64
webui/src/i18n/generatedTranslations.ts
Normal file
64
webui/src/i18n/generatedTranslations.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-ops.area.2745deba": "Area",
|
||||
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
||||
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
|
||||
"i18n:govoplan-ops.components.9289473e": "Components",
|
||||
"i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks",
|
||||
"i18n:govoplan-ops.deployment_profiles.b0caa179": "Deployment Profiles",
|
||||
"i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry",
|
||||
"i18n:govoplan-ops.fit.dab564d8": "Fit",
|
||||
"i18n:govoplan-ops.health_checks.201c869f": "Health Checks",
|
||||
"i18n:govoplan-ops.loading_operations_status.6890fe6e": "Loading operations status...",
|
||||
"i18n:govoplan-ops.modules.04e9462c": "Modules",
|
||||
"i18n:govoplan-ops.no_database_url.51a2db0c": "No database URL",
|
||||
"i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.",
|
||||
"i18n:govoplan-ops.no_health_checks_reported.03c067c4": "No health checks reported.",
|
||||
"i18n:govoplan-ops.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.",
|
||||
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
|
||||
"i18n:govoplan-ops.ops.907a54c2": "Ops",
|
||||
"i18n:govoplan-ops.profile.ff4fc027": "Profile",
|
||||
"i18n:govoplan-ops.readiness.1db9d6fb": "Readiness",
|
||||
"i18n:govoplan-ops.reload.cce71553": "Reload",
|
||||
"i18n:govoplan-ops.redis.5eaa1f2f": "Redis",
|
||||
"i18n:govoplan-ops.no_readiness_blockers.0df259bd": "No readiness blockers",
|
||||
"i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156": "Runtime health, deployment profile, worker split, and sizing assumptions.",
|
||||
"i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",
|
||||
"i18n:govoplan-ops.sizing_assumptions.6ade9a90": "Sizing Assumptions",
|
||||
"i18n:govoplan-ops.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-ops.warnings.1430f976": "Warnings",
|
||||
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
|
||||
},
|
||||
"de": {
|
||||
"i18n:govoplan-ops.area.2745deba": "Area",
|
||||
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
||||
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
|
||||
"i18n:govoplan-ops.components.9289473e": "Components",
|
||||
"i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks",
|
||||
"i18n:govoplan-ops.deployment_profiles.b0caa179": "Deployment Profiles",
|
||||
"i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry",
|
||||
"i18n:govoplan-ops.fit.dab564d8": "Fit",
|
||||
"i18n:govoplan-ops.health_checks.201c869f": "Health Checks",
|
||||
"i18n:govoplan-ops.loading_operations_status.6890fe6e": "Loading operations status...",
|
||||
"i18n:govoplan-ops.modules.04e9462c": "Module",
|
||||
"i18n:govoplan-ops.no_database_url.51a2db0c": "No database URL",
|
||||
"i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.",
|
||||
"i18n:govoplan-ops.no_health_checks_reported.03c067c4": "No health checks reported.",
|
||||
"i18n:govoplan-ops.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.",
|
||||
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
|
||||
"i18n:govoplan-ops.ops.907a54c2": "Betrieb",
|
||||
"i18n:govoplan-ops.profile.ff4fc027": "Profil",
|
||||
"i18n:govoplan-ops.readiness.1db9d6fb": "Bereitschaft",
|
||||
"i18n:govoplan-ops.reload.cce71553": "Neu laden",
|
||||
"i18n:govoplan-ops.redis.5eaa1f2f": "Redis",
|
||||
"i18n:govoplan-ops.no_readiness_blockers.0df259bd": "Keine Bereitschaftsblocker",
|
||||
"i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156": "Runtime health, deployment profile, worker split, and sizing assumptions.",
|
||||
"i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",
|
||||
"i18n:govoplan-ops.sizing_assumptions.6ade9a90": "Sizing Assumptions",
|
||||
"i18n:govoplan-ops.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-ops.warnings.1430f976": "Warnungen",
|
||||
"i18n:govoplan-ops.workers.b6ef3acd": "Worker"
|
||||
}
|
||||
};
|
||||
2
webui/src/index.ts
Normal file
2
webui/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { opsModule as default, opsModule } from "./module";
|
||||
export { default as OpsPage } from "./features/ops/OpsPage";
|
||||
46
webui/src/module.ts
Normal file
46
webui/src/module.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { DashboardWidgetsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||
import OpsHealthWidget from "./features/ops/OpsHealthWidget";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
|
||||
const OpsPage = lazy(() => import("./features/ops/OpsPage"));
|
||||
|
||||
const opsReadScopes = ["ops:operations:read", "system:settings:read", "admin:settings:read"];
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
de: generatedTranslations.de
|
||||
};
|
||||
const dashboardWidgets: DashboardWidgetsUiCapability = {
|
||||
widgets: [
|
||||
{
|
||||
id: "ops.health",
|
||||
title: "Operations health",
|
||||
description: "Readiness, worker mode, and current warning count.",
|
||||
moduleId: "ops",
|
||||
category: "Operations",
|
||||
order: 100,
|
||||
defaultSize: "wide",
|
||||
anyOf: opsReadScopes,
|
||||
refreshIntervalMs: 30_000,
|
||||
render: ({ settings, refreshKey }) => createElement(OpsHealthWidget, { settings, refreshKey })
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const opsModule: PlatformWebModule = {
|
||||
id: "ops",
|
||||
label: "i18n:govoplan-ops.ops.907a54c2",
|
||||
version: "1.0.0",
|
||||
dependencies: ["access"],
|
||||
optionalDependencies: ["audit", "docs", "notifications"],
|
||||
translations,
|
||||
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890 }],
|
||||
routes: [
|
||||
{ path: "/ops", anyOf: opsReadScopes, order: 890, render: ({ settings }) => createElement(OpsPage, { settings }) }],
|
||||
uiCapabilities: {
|
||||
"dashboard.widgets": dashboardWidgets
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default opsModule;
|
||||
Reference in New Issue
Block a user