feat: tablero por modulo funcional (estado+carga+funciones en sidebar)
This commit is contained in:
1 parent
a209133e43
commit
e20e9d6657
1 file changed
+130
-89
+130
-89
@@ -1,67 +1,77 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import SamShell from "@/components/SamShell";
|
import SamShell from "@/components/SamShell";
|
||||||
|
import { useModules, useStats } from "@/lib/store";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
displayName,
|
displayName,
|
||||||
|
envOf,
|
||||||
getModuleFunctions,
|
getModuleFunctions,
|
||||||
|
getStatusInfo,
|
||||||
moduleUrl,
|
moduleUrl,
|
||||||
type ApiResponse,
|
|
||||||
type ModuleState,
|
|
||||||
} from "@/lib/sam";
|
} from "@/lib/sam";
|
||||||
|
import {
|
||||||
|
memUsed,
|
||||||
|
pct,
|
||||||
|
shortUptime,
|
||||||
|
statFor,
|
||||||
|
} from "@/lib/stats";
|
||||||
|
|
||||||
const API_URL =
|
const API_URL =
|
||||||
process.env.NEXT_PUBLIC_API_URL || "https://api.devops.norteamericano.cl";
|
process.env.NEXT_PUBLIC_API_URL || "https://api.devops.norteamericano.cl";
|
||||||
|
|
||||||
function subscribeHash(cb: () => void) {
|
function Bar({ value, color }: { value: number; color: string }) {
|
||||||
window.addEventListener("hashchange", cb);
|
return (
|
||||||
return () => window.removeEventListener("hashchange", cb);
|
<div className="progress" style={{ height: 8 }}>
|
||||||
}
|
<div
|
||||||
|
className="progress-bar"
|
||||||
function snapshotHash() {
|
role="progressbar"
|
||||||
return window.location.hash.slice(1);
|
style={{ width: `${value}%`, backgroundColor: color }}
|
||||||
|
aria-valuenow={value}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tablero funcional del módulo: estado + carga de trabajo +
|
||||||
|
// funciones API en el sidebar (plantilla SamShell).
|
||||||
export default function ModuleBoard() {
|
export default function ModuleBoard() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const name = Array.isArray(params.name) ? params.name[0] : params.name ?? "";
|
const name = Array.isArray(params.name) ? params.name[0] : params.name ?? "";
|
||||||
const [data, setData] = useState<ApiResponse | null>(null);
|
const { data } = useModules();
|
||||||
const [loading, setLoading] = useState(true);
|
const stats = useStats();
|
||||||
const hash =
|
const loading = data === null;
|
||||||
useSyncExternalStore(subscribeHash, snapshotHash, () => "") || null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
const mod = data?.modules.find(
|
||||||
fetch("/api/modules", { cache: "no-store" })
|
(m) => m.name.toLowerCase() === name.toLowerCase()
|
||||||
.then((r) => r.json())
|
|
||||||
.then((j) => setData(j))
|
|
||||||
.catch(() => setData(null))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const mod: ModuleState | undefined = useMemo(
|
|
||||||
() => data?.modules.find((m) => m.name.toLowerCase() === name.toLowerCase()),
|
|
||||||
[data, name]
|
|
||||||
);
|
);
|
||||||
|
const functions = getModuleFunctions(name);
|
||||||
const functions = useMemo(() => getModuleFunctions(name), [name]);
|
const stat = mod ? statFor(stats, `${mod.name}-frontend`) : undefined;
|
||||||
const activePath = hash || functions[0]?.path || null;
|
const status = mod ? getStatusInfo(mod) : null;
|
||||||
const activeFn = functions.find((f) => f.path === activePath) ?? functions[0];
|
const env = mod ? envOf(mod.branch) : null;
|
||||||
|
const cpu = pct(stat?.cpu);
|
||||||
|
const mem = pct(stat?.memPerc);
|
||||||
const title = mod ? displayName(mod.name) : displayName(name || "Módulo");
|
const title = mod ? displayName(mod.name) : displayName(name || "Módulo");
|
||||||
|
|
||||||
const menu = functions.map((f) => ({
|
const menu = [
|
||||||
label: f.label,
|
{ label: "Estado", icon: "bx-grid-alt", href: "#estado" },
|
||||||
icon: f.icon,
|
...functions.map((f) => ({
|
||||||
href: `#${f.path}`,
|
label: f.label,
|
||||||
}));
|
icon: f.icon,
|
||||||
|
href: `/m/${name}#${f.path}`,
|
||||||
|
})),
|
||||||
|
{ label: "Aplicaciones", icon: "bx-layer", href: "/" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SamShell
|
<SamShell
|
||||||
brand={title}
|
brand={title}
|
||||||
subtitle="Tablero del módulo"
|
subtitle="Tablero del módulo"
|
||||||
menu={menu}
|
menu={menu}
|
||||||
activeHref={activeFn ? `#${activeFn.path}` : undefined}
|
activeHref="#estado"
|
||||||
topUser="SAM"
|
topUser="SAM"
|
||||||
topSede={mod ? `${mod.subdomain}.devops.norteamericano.cl` : undefined}
|
topSede={mod ? `${mod.subdomain}.devops.norteamericano.cl` : undefined}
|
||||||
backHref="/"
|
backHref="/"
|
||||||
@@ -82,78 +92,109 @@ export default function ModuleBoard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && mod && activeFn && (
|
{!loading && mod && status && env && (
|
||||||
<>
|
<>
|
||||||
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
<div className="row row-cards" id="estado">
|
||||||
<span>
|
|
||||||
<i className={`bx ${activeFn.icon} me-2`} />
|
|
||||||
{activeFn.label}
|
|
||||||
<small className="text-muted ms-2 d-block d-md-inline">
|
|
||||||
{activeFn.desc}
|
|
||||||
</small>
|
|
||||||
</span>
|
|
||||||
<span className="d-flex gap-2">
|
|
||||||
<span className="badge bg-secondary font-monospace">
|
|
||||||
GET {activeFn.path}
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
href={`${API_URL}/${activeFn.path}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="btn btn-sm btn-outline-primary"
|
|
||||||
>
|
|
||||||
<i className="bx bx-link-external me-1" />
|
|
||||||
API
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={moduleUrl(mod)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
>
|
|
||||||
<i className="bx bx-window-open me-1" />
|
|
||||||
Abrir aplicación
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row row-cards">
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="app-card p-4">
|
<div className="app-card p-4">
|
||||||
<h6 className="fw-bold mb-3">
|
<div className="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
||||||
<i className="bx bx-list-ul me-2" />
|
<h6 className="fw-bold mb-0">
|
||||||
Funciones disponibles de la API
|
<i className="bx bx-pulse me-2" />
|
||||||
</h6>
|
Estado y carga de trabajo
|
||||||
|
</h6>
|
||||||
|
<span className="d-flex gap-2 align-items-center">
|
||||||
|
<span className={`status-dot ${status.cls}`} />
|
||||||
|
<strong>{status.label}</strong>
|
||||||
|
<span className={`badge ${env.cls} badge-respaldo`}>
|
||||||
|
{env.label}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="row g-3 mb-3">
|
||||||
|
<div className="col-md-3 col-6">
|
||||||
|
<small className="text-muted d-block">Uptime</small>
|
||||||
|
<strong>{shortUptime(stat?.status ?? mod.container)}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3 col-6">
|
||||||
|
<small className="text-muted d-block">Puerto</small>
|
||||||
|
<strong>:{mod.port}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3 col-6">
|
||||||
|
<small className="text-muted d-block">Rama</small>
|
||||||
|
<strong>{mod.branch}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3 col-6">
|
||||||
|
<small className="text-muted d-block">Red</small>
|
||||||
|
<small className="fw-semibold">{stat?.netIO ?? "—"}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<small className="text-muted">CPU</small>
|
||||||
|
<small className="fw-semibold">{stat?.cpu ?? "—"}</small>
|
||||||
|
</div>
|
||||||
|
<Bar value={cpu} color="#0d6efd" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<small className="text-muted">MEM {memUsed(stat)}</small>
|
||||||
|
<small className="fw-semibold">{stat?.memPerc ?? "—"}</small>
|
||||||
|
</div>
|
||||||
|
<Bar value={mem} color="#6f42c1" />
|
||||||
|
</div>
|
||||||
|
<div className="d-flex gap-2 flex-wrap">
|
||||||
|
<a
|
||||||
|
href={moduleUrl(mod)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
>
|
||||||
|
<i className="bx bx-window-open me-1" />
|
||||||
|
Abrir aplicación
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`${API_URL}/api/${mod.name}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
<i className="bx bx-link-external me-1" />
|
||||||
|
Ver en API
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
|
||||||
|
<i className="bx bx-list-ul me-1" /> Funciones disponibles de la API
|
||||||
|
</div>
|
||||||
|
<div className="row row-cards">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="app-card p-3">
|
||||||
<div className="list-group">
|
<div className="list-group">
|
||||||
{functions.map((f) => (
|
{functions.map((f) => (
|
||||||
<button
|
<a
|
||||||
key={f.path}
|
key={f.path}
|
||||||
onClick={() => {
|
href={`${API_URL}/${f.path}`}
|
||||||
window.location.hash = f.path;
|
target="_blank"
|
||||||
}}
|
rel="noopener noreferrer"
|
||||||
className={`list-group-item list-group-item-action d-flex justify-content-between align-items-center${
|
className="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
||||||
f.path === activeFn.path ? " active" : ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<i className={`bx ${f.icon} me-2`} />
|
<i className={`bx ${f.icon} me-2`} />
|
||||||
{f.label}
|
{f.label}
|
||||||
<small className="text-muted d-block">{f.desc}</small>
|
<small className="text-muted d-block">{f.desc}</small>
|
||||||
</span>
|
</span>
|
||||||
<code className="small">{f.path}</code>
|
<code className="small">GET {f.path}</code>
|
||||||
</button>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted small mt-3 mb-0">
|
|
||||||
Las funciones se consumen desde {API_URL}. Usa «Abrir
|
|
||||||
aplicación» para operar en {moduleUrl(mod)}.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</SamShell>
|
</SamShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user