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