172 lines
4.8 KiB
TypeScript
172 lines
4.8 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>{stat.tableName}</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>
|
|
);
|
|
}
|