feat(frontend): modulariza dashboard
- Extrai cards de métricas de tipo em componente dedicado - Isola tabela de servidores reaproveitando estados - Expõe labels traduzidos de ServersType para consumo no UImaster
parent
af7511195d
commit
d48a2633d0
|
|
@ -0,0 +1,59 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import api from "../Api";
|
||||
import { ServerTypeLabels, type ServersType } from "../types/enums";
|
||||
|
||||
interface ServerCount {
|
||||
type: ServersType;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const ServerCardMetrics = () => {
|
||||
const [counts, setCounts] = useState<ServerCount[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCounts = async () => {
|
||||
try {
|
||||
const { data } = await api.get<Record<ServersType, number>>("/api/servers/type");
|
||||
const normalized = Object.entries(data).map(([type, total]) => ({
|
||||
type: type as ServersType,
|
||||
total,
|
||||
}));
|
||||
setCounts(normalized);
|
||||
} catch (err: any) {
|
||||
const message = err?.response?.data?.message || "Falha ao carregar o total de servidores.";
|
||||
setError(message);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCounts();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div className={Styles.error}>{error}</div>;
|
||||
}
|
||||
|
||||
if (counts.length === 0) {
|
||||
return <div className={Styles.placeholder}>Carregando métricas...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Styles.grid}>
|
||||
{counts.map(({ type, total }) => (
|
||||
<div key={type} className={Styles.card}>
|
||||
<p className={Styles.label}>{ServerTypeLabels[type]}</p>
|
||||
<p className={Styles.value}>{total}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Styles = {
|
||||
grid: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
||||
card: "rounded-lg border border-cardBorder bg-card p-4 shadow-sm space-y-1",
|
||||
label: "text-sm text-text-secondary uppercase tracking-wide",
|
||||
value: "text-3xl font-semibold text-text",
|
||||
placeholder: "p-4 rounded-lg border border-cardBorder bg-card text-text-secondary text-sm",
|
||||
error: "p-4 rounded-lg border border-red-200 bg-red-50 text-red-600 text-sm",
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import type { Server } from "../types/Server";
|
||||
|
||||
interface Props {
|
||||
servers: Server[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const ServersTable = ({ servers, loading, error }: Props) => {
|
||||
return (
|
||||
<div className={Styles.card}>
|
||||
{loading && <div className={Styles.status}>Carregando servidores...</div>}
|
||||
{error && <div className={Styles.error}>{error}</div>}
|
||||
{!loading && !error && servers.length === 0 && (
|
||||
<div className={Styles.status}>Nenhum servidor encontrado.</div>
|
||||
)}
|
||||
{!loading && !error && servers.length > 0 && (
|
||||
<div className={Styles.tableWrapper}>
|
||||
<table className={Styles.table}>
|
||||
<thead className={Styles.tableHead}>
|
||||
<tr className="text-left">
|
||||
<th className={Styles.tableHeadCell}>Nome</th>
|
||||
<th className={Styles.tableHeadCell}>IP</th>
|
||||
<th className={Styles.tableHeadCell}>Porta</th>
|
||||
<th className={Styles.tableHeadCell}>Usuário</th>
|
||||
<th className={Styles.tableHeadCell}>Tipo</th>
|
||||
<th className={Styles.tableHeadCell}>Aplicação</th>
|
||||
<th className={Styles.tableHeadCell}>Banco</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={Styles.tableBody}>
|
||||
{servers.map((server) => (
|
||||
<tr
|
||||
key={server.id}
|
||||
className="hover:bg-gray-50 transition-colors even:bg-gray-50/50"
|
||||
>
|
||||
<td className={Styles.rowCell}>{server.name}</td>
|
||||
<td className={Styles.rowCell}>{server.ip}</td>
|
||||
<td className={Styles.rowCell}>{server.port}</td>
|
||||
<td className={Styles.rowCell}>{server.user}</td>
|
||||
<td className={`${Styles.rowCell} capitalize`}>{server.type.toLowerCase()}</td>
|
||||
<td className={`${Styles.rowCell} capitalize`}>{server.application.toLowerCase()}</td>
|
||||
<td className={`${Styles.rowCell} capitalize`}>{server.dbType.toLowerCase()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Styles = {
|
||||
card: "bg-card border border-cardBorder shadow-sm rounded-lg overflow-hidden",
|
||||
status: "p-4 text-text-secondary text-sm",
|
||||
error: "p-4 text-red-600 text-sm",
|
||||
tableWrapper: "overflow-x-auto rounded-lg shadow-sm border border-cardBorder",
|
||||
table: "min-w-full divide-y divide-cardBorder table-auto",
|
||||
tableHead: "bg-gray-50 sticky top-0",
|
||||
tableHeadCell: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
||||
tableBody: "bg-white divide-y divide-cardBorder",
|
||||
rowCell: "px-4 py-3 text-sm text-text",
|
||||
};
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import api from "../Api";
|
||||
import { Layout } from "../components/Layout";
|
||||
import { ServerCardMetrics } from "../components/ServerCardMetrics";
|
||||
import { ServersTable } from "../components/ServersTable";
|
||||
import type { Server } from "../types/Server";
|
||||
|
||||
export const Dashboard = () => {
|
||||
|
|
@ -27,52 +29,9 @@ export const Dashboard = () => {
|
|||
return (
|
||||
<Layout className="h-screen py-10">
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-semibold text-text">Servidores</h1>
|
||||
|
||||
<div className={Styles.card}>
|
||||
{loading && <div className="p-4 text-text-secondary text-sm">Carregando servidores...</div>}
|
||||
{error && <div className="p-4 text-red-600 text-sm">{error}</div>}
|
||||
{!loading && !error && servers.length === 0 && (
|
||||
<div className="p-4 text-text-secondary text-sm">Nenhum servidor encontrado.</div>
|
||||
)}
|
||||
{!loading && !error && servers.length > 0 && (
|
||||
<div className="overflow-x-auto rounded-lg shadow-sm border border-cardBorder">
|
||||
<table className="min-w-full divide-y divide-cardBorder table-auto">
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<tr className="text-left">
|
||||
<th className={Styles.tableHeadCell}>Nome</th>
|
||||
<th className={Styles.tableHeadCell}>IP</th>
|
||||
<th className={Styles.tableHeadCell}>Porta</th>
|
||||
<th className={Styles.tableHeadCell}>Usuário</th>
|
||||
<th className={Styles.tableHeadCell}>Tipo</th>
|
||||
<th className={Styles.tableHeadCell}>Aplicação</th>
|
||||
<th className={Styles.tableHeadCell}>Banco</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-cardBorder">
|
||||
{servers.map((server) => (
|
||||
<tr key={server.id} className="hover:bg-gray-50 transition-colors even:bg-gray-50/50">
|
||||
<td className={Styles.rowCell}>{server.name}</td>
|
||||
<td className={Styles.rowCell}>{server.ip}</td>
|
||||
<td className={Styles.rowCell}>{server.port}</td>
|
||||
<td className={Styles.rowCell}>{server.user}</td>
|
||||
<td className={`${Styles.rowCell} capitalize`}>{server.type.toLowerCase()}</td>
|
||||
<td className={`${Styles.rowCell} capitalize`}>{server.application.toLowerCase()}</td>
|
||||
<td className={`${Styles.rowCell} capitalize`}>{server.dbType.toLowerCase()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ServerCardMetrics />
|
||||
<ServersTable servers={servers} loading={loading} error={error} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
const Styles = {
|
||||
card: "bg-card border border-cardBorder shadow-sm rounded-lg overflow-hidden",
|
||||
tableHeadCell: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
||||
rowCell: "px-4 py-3 text-sm text-text",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,3 +16,9 @@ export type Applications =
|
|||
| 'HITPHONE';
|
||||
|
||||
export type ServersType = 'PRODUCTION' | 'HOMOLOGATION' | 'DATABASE';
|
||||
|
||||
export const ServerTypeLabels: Record<ServersType, string> = {
|
||||
PRODUCTION: "Produção",
|
||||
HOMOLOGATION: "Homologação",
|
||||
DATABASE: "Banco de Dados",
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue