feat(admin): refine table query tools
This commit is contained in:
@@ -28,6 +28,7 @@ export function AdminDatabaseTablesPage({
|
||||
const [result, setResult] = useState<AdminDatabaseTableRowsResponse | null>(null);
|
||||
const [detailRow, setDetailRow] = useState<AdminDatabaseTableRowPayload | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [copyMessage, setCopyMessage] = useState('');
|
||||
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
||||
const [isLoadingRows, setIsLoadingRows] = useState(false);
|
||||
|
||||
@@ -91,20 +92,31 @@ export function AdminDatabaseTablesPage({
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRows(nextTableName = tableName) {
|
||||
async function refreshRows(
|
||||
nextTableName = tableName,
|
||||
options: {
|
||||
search?: string;
|
||||
filters?: string;
|
||||
limit?: string;
|
||||
} = {},
|
||||
) {
|
||||
const normalizedTableName = nextTableName.trim();
|
||||
if (!normalizedTableName) {
|
||||
return;
|
||||
}
|
||||
const querySearch = options.search ?? search;
|
||||
const queryFilters = options.filters ?? filters;
|
||||
const queryLimit = options.limit ?? limit;
|
||||
setIsLoadingRows(true);
|
||||
setErrorMessage('');
|
||||
try {
|
||||
const response = await getAdminDatabaseTableRows(token, normalizedTableName, {
|
||||
search,
|
||||
filters,
|
||||
limit: parseLimit(limit),
|
||||
search: querySearch,
|
||||
filters: queryFilters,
|
||||
limit: parseLimit(queryLimit),
|
||||
});
|
||||
setResult(response);
|
||||
setCopyMessage('');
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
@@ -125,6 +137,27 @@ export function AdminDatabaseTablesPage({
|
||||
}
|
||||
}
|
||||
|
||||
function handleResetQuery() {
|
||||
setSearch('');
|
||||
setFilters('');
|
||||
setLimit('100');
|
||||
void refreshRows(tableName, {search: '', filters: '', limit: '100'});
|
||||
}
|
||||
|
||||
async function handleCopyDetailJson() {
|
||||
if (!detailRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copiedText = JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2);
|
||||
try {
|
||||
await navigator.clipboard.writeText(copiedText);
|
||||
setCopyMessage('已复制 JSON');
|
||||
} catch {
|
||||
setCopyMessage('复制失败,请手动选中后复制');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page admin-page-wide">
|
||||
<div className="admin-page-heading">
|
||||
@@ -201,6 +234,20 @@ export function AdminDatabaseTablesPage({
|
||||
<span>{isLoadingRows ? '查询中' : '查询'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-action-row admin-query-action-row">
|
||||
<button
|
||||
className="admin-ghost-button admin-query-reset-button"
|
||||
disabled={isLoadingRows}
|
||||
type="button"
|
||||
onClick={handleResetQuery}
|
||||
>
|
||||
重置条件
|
||||
</button>
|
||||
<div className="admin-query-summary">
|
||||
<span>已选表:{tableName || '-'}</span>
|
||||
<span>显示列:{visibleColumns.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{errorMessage ? (
|
||||
@@ -265,17 +312,27 @@ export function AdminDatabaseTablesPage({
|
||||
<section className="admin-detail-panel" role="dialog" aria-modal="true">
|
||||
<div className="admin-panel-heading">
|
||||
<h3>行详情</h3>
|
||||
<button
|
||||
className="admin-ghost-button"
|
||||
title="关闭"
|
||||
type="button"
|
||||
onClick={() => setDetailRow(null)}
|
||||
>
|
||||
<X size={17} aria-hidden="true" />
|
||||
</button>
|
||||
<div className="admin-detail-actions">
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
type="button"
|
||||
onClick={() => void handleCopyDetailJson()}
|
||||
>
|
||||
<span>复制 JSON</span>
|
||||
</button>
|
||||
<button
|
||||
className="admin-ghost-button"
|
||||
title="关闭"
|
||||
type="button"
|
||||
onClick={() => setDetailRow(null)}
|
||||
>
|
||||
<X size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{copyMessage ? <div className="admin-status admin-status-ok">{copyMessage}</div> : null}
|
||||
<pre className="admin-code-block">
|
||||
{JSON.stringify(detailRow.cells, null, 2)}
|
||||
{JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -327,10 +327,34 @@ button:disabled {
|
||||
.admin-action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-query-action-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-query-summary,
|
||||
.admin-detail-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-query-summary {
|
||||
color: #667682;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.admin-query-reset-button {
|
||||
width: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.admin-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
- 提供基础查询能力:表选择、关键词搜索、JSON 条件过滤、条数限制、刷新、查看行详情。
|
||||
- 不修改 SpacetimeDB 表结构,不新增 reducer,不引入写操作。
|
||||
|
||||
## 后续增强
|
||||
|
||||
- 查询页增加“重置条件”快捷操作,便于运营快速回到默认筛选状态。
|
||||
- 行详情支持一键复制完整 JSON,减少人工选中复制的操作成本。
|
||||
- 查询页顶部增加轻量摘要,显示当前选表和可见列数,方便移动端快速确认上下文。
|
||||
|
||||
## 后端接口
|
||||
|
||||
### `GET /admin/api/database/tables`
|
||||
|
||||
Reference in New Issue
Block a user