Files
Genarrative/apps/admin-web/src/pages/AdminOverviewPage.tsx

182 lines
5.1 KiB
TypeScript

import {RefreshCw} from 'lucide-react';
import {useCallback, useEffect, useState} from 'react';
import {getAdminOverview} from '../api/adminApiClient';
import type {
AdminDatabaseTableStatPayload,
AdminOverviewResponse,
} from '../api/adminApiTypes';
import {handlePageError} from './pageUtils';
interface AdminOverviewPageProps {
token: string;
onUnauthorized: (message?: string) => void;
}
export function AdminOverviewPage({
token,
onUnauthorized,
}: AdminOverviewPageProps) {
const [overview, setOverview] = useState<AdminOverviewResponse | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const loadOverview = useCallback(async () => {
setIsLoading(true);
setErrorMessage('');
try {
const response = await getAdminOverview(token);
setOverview(response);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
}, [onUnauthorized, token]);
useEffect(() => {
void loadOverview();
}, [loadOverview]);
return (
<section className="admin-page">
<div className="admin-page-heading">
<div>
<h2></h2>
<p></p>
</div>
<button
className="admin-secondary-button"
disabled={isLoading}
type="button"
onClick={() => void loadOverview()}
>
<RefreshCw size={17} aria-hidden="true" />
<span>{isLoading ? '刷新中' : '刷新'}</span>
</button>
</div>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
</div>
) : null}
<div className="admin-overview-grid">
<InfoPanel
title="服务"
rows={[
['监听', overview ? `${overview.service.bindHost}:${overview.service.bindPort}` : '-'],
['JWT issuer', overview?.service.jwtIssuer ?? '-'],
['后台', overview?.service.adminEnabled ? '已启用' : '未启用'],
]}
/>
<InfoPanel
title="SpacetimeDB"
rows={[
['Server', overview?.service.spacetimeServerUrl ?? '-'],
['Database', overview?.service.spacetimeDatabase ?? '-'],
['Identity', overview?.database.databaseIdentity ?? '-'],
['Owner', overview?.database.ownerIdentity ?? '-'],
['Host', overview?.database.hostType ?? '-'],
]}
/>
</div>
<section className="admin-panel">
<div className="admin-panel-heading">
<h3></h3>
<span>{overview?.database.schemaTableNames.length ?? 0} tables</span>
</div>
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{(overview?.database.tableStats ?? []).map((stat) => (
<TableStatRow key={stat.tableName} stat={stat} />
))}
{overview && overview.database.tableStats.length === 0 ? (
<tr>
<td colSpan={3}></td>
</tr>
) : null}
</tbody>
</table>
</div>
</section>
{overview?.database.fetchErrors.length ? (
<section className="admin-panel admin-panel-warning">
<div className="admin-panel-heading">
<h3></h3>
<span>{overview.database.fetchErrors.length}</span>
</div>
<ul className="admin-error-list">
{overview.database.fetchErrors.map((message) => (
<li key={message}>{message}</li>
))}
</ul>
</section>
) : null}
</section>
);
}
function InfoPanel({
title,
rows,
}: {
title: string;
rows: Array<[string, string]>;
}) {
return (
<section className="admin-panel">
<div className="admin-panel-heading">
<h3>{title}</h3>
</div>
<dl className="admin-info-list">
{rows.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
</section>
);
}
function TableStatRow({stat}: {stat: AdminDatabaseTableStatPayload}) {
return (
<tr>
<td>
<button
className="admin-text-button"
type="button"
onClick={() => {
window.location.hash = `#tables?table=${encodeURIComponent(stat.tableName)}`;
}}
>
{stat.tableName}
</button>
</td>
<td>{typeof stat.rowCount === 'number' ? stat.rowCount : '-'}</td>
<td>
{stat.errorMessage ? (
<span className="admin-status admin-status-error">
{stat.errorMessage}
</span>
) : (
<span className="admin-status admin-status-ok">OK</span>
)}
</td>
</tr>
);
}