Fix admin SQL count parsing for local SpacetimeDB
This commit is contained in:
171
apps/admin-web/src/pages/AdminOverviewPage.tsx
Normal file
171
apps/admin-web/src/pages/AdminOverviewPage.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user