feat: portal SAM - Next.js 15 + Bootstrap, integración con deployer API
This commit is contained in:
1 parent
229cbd074a
commit
7691c01ff1
9 files changed
+1005
-107
No files matched your search
+280
-61
@@ -1,69 +1,288 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ModuleState {
|
||||
name: string;
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
subdomain: string;
|
||||
port: number;
|
||||
active: boolean;
|
||||
lastDeploy?: {
|
||||
sha: string;
|
||||
time: string;
|
||||
ok: boolean;
|
||||
log: string;
|
||||
};
|
||||
container?: string | null;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
backend: {
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
container: string | null;
|
||||
lastDeploy: ModuleState["lastDeploy"] | null;
|
||||
};
|
||||
modules: ModuleState[];
|
||||
}
|
||||
|
||||
const iconMap: Record<string, { icon: string; bg: string }> = {
|
||||
ventas: { icon: "bx-money", bg: "bg-success" },
|
||||
comercial: { icon: "bx-money", bg: "bg-success" },
|
||||
finanzas: { icon: "bx-detail", bg: "bg-danger" },
|
||||
contabilidad: { icon: "bx-detail", bg: "bg-danger" },
|
||||
academico: { icon: "bx-user", bg: "bg-primary" },
|
||||
"academico-empresas": { icon: "bx-briefcase", bg: "bg-primary" },
|
||||
profesor: { icon: "bx-pencil", bg: "bg-warning text-dark" },
|
||||
profesoras: { icon: "bx-pencil", bg: "bg-warning text-dark" },
|
||||
empresas: { icon: "bx-building", bg: "bg-info" },
|
||||
pagos: { icon: "bx-credit-card", bg: "bg-success" },
|
||||
default: { icon: "bx-cube", bg: "bg-secondary" },
|
||||
};
|
||||
|
||||
function getModuleMeta(name: string) {
|
||||
const key = name.toLowerCase();
|
||||
return iconMap[key] || iconMap.default;
|
||||
}
|
||||
|
||||
function getStatusInfo(m: ModuleState) {
|
||||
if (!m.container) return { label: "Detenido", cls: "status-unhealthy" };
|
||||
if (m.container.includes("healthy")) return { label: "Activo", cls: "status-healthy" };
|
||||
if (m.container.includes("starting")) return { label: "Iniciando", cls: "status-starting" };
|
||||
return { label: "Activo", cls: "status-healthy" };
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [data, setData] = useState<ApiResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch("/api/modules", { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Error al cargar módulos");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchModules();
|
||||
const id = setInterval(fetchModules, 15000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const modules = data?.modules ?? [];
|
||||
const backend = data?.backend;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert h-5 w-[100px]"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<>
|
||||
<div className="sidenav">
|
||||
<div className="row text-light login-main-text">
|
||||
<div className="col-12 text-center">
|
||||
<div
|
||||
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3"
|
||||
style={{ width: 80, height: 80 }}
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
<i className="bx bx-layer text-primary" style={{ fontSize: 48 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-3">
|
||||
<h1 className="fw-bold fs-2">Sistema SAM</h1>
|
||||
</div>
|
||||
<div className="col-12 text-center">
|
||||
<h4 className="fw-semibold opacity-75">Aplicaciones</h4>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-4">
|
||||
<Link href="/upload" className="btn btn-light btn-sm px-4">
|
||||
<i className="bx bx-plus me-1" /> Nuevo Módulo
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-3">
|
||||
<small className="opacity-50">Portal de integración</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert h-[14px] w-4"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="main">
|
||||
<div className="row" id="modulos">
|
||||
<div className="col-lg-1" />
|
||||
<div className="col-lg-10 mt-4">
|
||||
{loading && (
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status" />
|
||||
<p className="mt-2 text-muted">Cargando módulos...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger d-flex align-items-center">
|
||||
<i className="bx bx-error-circle me-2 fs-5" />
|
||||
<div>
|
||||
<strong>Error:</strong> {error}
|
||||
<button
|
||||
onClick={fetchModules}
|
||||
className="btn btn-sm btn-outline-danger ms-3"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="row row-cards">
|
||||
<div className="col-12">
|
||||
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i className="bx bx-grid-alt me-1" /> Selección de Aplicación
|
||||
</span>
|
||||
<span className="badge bg-primary rounded-pill">
|
||||
{modules.length} módulo{modules.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modules.length === 0 ? (
|
||||
<div className="col-12">
|
||||
<div className="app-card p-4 text-center">
|
||||
<i className="bx bx-inbox fs-1 text-muted" />
|
||||
<p className="text-muted mt-2 mb-3">
|
||||
No hay módulos integrados. ¡Sube tu primer proyecto!
|
||||
</p>
|
||||
<Link href="/upload" className="btn btn-primary btn-sm">
|
||||
<i className="bx bx-upload me-1" /> Subir Proyecto
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
modules.map((m) => {
|
||||
const meta = getModuleMeta(m.name);
|
||||
const status = getStatusInfo(m);
|
||||
const url = `https://${m.subdomain}.devops.norteamericano.cl`;
|
||||
const displayName =
|
||||
m.name.charAt(0).toUpperCase() + m.name.slice(1).replace(/-/g, " ");
|
||||
return (
|
||||
<div className="col-md-6" key={m.name}>
|
||||
<div className="app-card p-2">
|
||||
<table className="table table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ps-3" style={{ width: 56 }}>
|
||||
<i
|
||||
className={`bx ${meta.icon} bx-sm bx-border text-light ${meta.bg}`}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||
>
|
||||
{displayName}
|
||||
</a>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className={`status-dot ${status.cls}`} />
|
||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||
{status.label} · :{m.port} · {m.branch}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end pe-3" style={{ width: 40 }}>
|
||||
<i className="bx bx-chevron-right text-muted" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{backend && (
|
||||
<>
|
||||
<div className="col-12 mt-4 mb-2">
|
||||
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
||||
<i className="bx bx-server me-1" /> Backend & Sistema
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="app-card p-2">
|
||||
<table className="table table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ps-3" style={{ width: 56 }}>
|
||||
<i className="bx bx-code-alt bx-sm bx-border text-light bg-dark" />
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="https://api.devops.norteamericano.cl/health"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||
>
|
||||
API Backend
|
||||
</a>
|
||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||
{backend.container ? "Activo" : "Detenido"} · {backend.branch}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="app-card p-2">
|
||||
<table className="table table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ps-3" style={{ width: 56 }}>
|
||||
<i className="bx bx-cog bx-sm bx-border text-light bg-secondary" />
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
href="/upload"
|
||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||
>
|
||||
Gestionar Módulos
|
||||
</Link>
|
||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||
Subir / actualizar proyectos
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="col-12 mt-4 mb-4 text-center">
|
||||
<small className="text-muted">
|
||||
Auto-actualizado cada 15s ·{" "}
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); fetchModules(); }}>
|
||||
actualizar ahora
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user