hit-server-manager/frontend/src/components/ServersTable.tsx

143 lines
7.2 KiB
TypeScript

import type { Server } from "../types/Server";
interface Props {
servers: Server[];
loading: boolean;
error: string | null;
page: number;
pageSize: number;
totalPages: number;
totalItems: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
}
const PAGE_SIZE_OPTIONS = [5, 10, 20, 50];
export const ServersTable = ({
servers,
loading,
error,
page,
pageSize,
totalPages,
totalItems,
onPageChange,
onPageSizeChange,
}: Props) => {
const showingFrom = totalItems === 0 ? 0 : page * pageSize + 1;
const showingTo = totalItems === 0 ? 0 : Math.min((page + 1) * pageSize, totalItems);
const hasResults = servers.length > 0;
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 && hasResults && (
<>
<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}>Senha</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}>
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{server.password}</code>
</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 className={Styles.pagination}>
<div className={Styles.pageInfo}>
Mostrando {showingFrom} - {showingTo} de {totalItems}
</div>
<div className={Styles.paginationControls}>
<label className={Styles.pageSizeLabel}>
Linhas por página
<select
className={Styles.pageSizeSelect}
value={pageSize}
onChange={(event) => onPageSizeChange(Number(event.target.value))}
>
{PAGE_SIZE_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<div className={Styles.pageButtons}>
<button
type="button"
className={Styles.pageButton}
onClick={() => onPageChange(page - 1)}
disabled={page <= 0}
>
Anterior
</button>
<span className={Styles.pageIndicator}>
Página {page + 1} de {Math.max(totalPages, 1)}
</span>
<button
type="button"
className={Styles.pageButton}
onClick={() => onPageChange(page + 1)}
disabled={totalPages === 0 || page >= totalPages - 1}
>
Próxima
</button>
</div>
</div>
</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",
pagination: "flex flex-col gap-2 border-t border-cardBorder bg-white px-4 py-3 sm:flex-row sm:items-center sm:justify-between",
pageInfo: "text-sm text-text-secondary",
paginationControls: "flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4",
pageSizeLabel: "text-sm text-text flex items-center gap-2",
pageSizeSelect: "rounded-md border border-cardBorder bg-white px-2 py-1 text-sm text-text outline-none focus:border-accent focus:ring-1 focus:ring-accent",
pageButtons: "flex items-center gap-3",
pageButton: "rounded-md border border-cardBorder px-3 py-1.5 text-sm font-medium text-text hover:bg-bg disabled:opacity-50 disabled:hover:bg-transparent",
pageIndicator: "text-sm text-text-secondary",
};