mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
🐛 fix(data-grid): 刷新总数后跳转尾页
- 为表数据视图增加独立尾页刷新回调 - 按当前筛选条件执行精确 COUNT 后计算最新页码与偏移 - 丢弃过期计数请求并在计数失败时停止旧尾页查询 - 覆盖数据增减、并发翻页和兼容回退测试 Refs #706
This commit is contained in:
@@ -311,7 +311,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
data, columnNames, loading, tableName, columnPinScope, objectType = 'table', exportScope = 'table', dbName, ddlDbName, ddlTableName, connectionId, pkColumns = [], editLocator, readOnly = false,
|
||||
resultSql,
|
||||
resultExportAllSql,
|
||||
onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition,
|
||||
onReload, onSort, onPageChange, onLastPage, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition,
|
||||
onApplyQuickWhereCondition,
|
||||
scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions, showRowNumberColumn, isActive = true, enableSqlLogEvent = false,
|
||||
initialViewMode,
|
||||
@@ -5499,6 +5499,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
onCancelTotalCount,
|
||||
onOpenErTable: openTableByName,
|
||||
onPageChange,
|
||||
onLastPage,
|
||||
onReload,
|
||||
onRequestTotalCount,
|
||||
onSort,
|
||||
|
||||
@@ -1347,6 +1347,7 @@ interface DataGridProps {
|
||||
onReload?: () => void;
|
||||
onSort?: (field: string, order: string) => void;
|
||||
onPageChange?: (page: number, size: number) => void;
|
||||
onLastPage?: (pageSize: number) => void;
|
||||
pagination?: {
|
||||
current: number,
|
||||
pageSize: number,
|
||||
|
||||
@@ -2,7 +2,10 @@ import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import DataGridPaginationBar, { resolveDataGridPaginationBoundaryTarget } from './DataGridPaginationBar';
|
||||
import DataGridPaginationBar, {
|
||||
createDataGridLastPageAction,
|
||||
resolveDataGridPaginationBoundaryTarget,
|
||||
} from './DataGridPaginationBar';
|
||||
|
||||
describe('DataGridPaginationBar boundary navigation', () => {
|
||||
it('resolves the first and last page when the total page count is known', () => {
|
||||
@@ -102,4 +105,58 @@ describe('DataGridPaginationBar boundary navigation', () => {
|
||||
expect(markup).not.toContain('data-grid-pagination-total-count="true"');
|
||||
});
|
||||
|
||||
it('keeps the last-page action available for a fresh tail lookup at the cached boundary', () => {
|
||||
const onPageChange = vi.fn();
|
||||
const onLastPage = vi.fn();
|
||||
const action = createDataGridLastPageAction({
|
||||
current: 10,
|
||||
pageSize: 10,
|
||||
totalPages: 10,
|
||||
totalKnown: true,
|
||||
onPageChange,
|
||||
onLastPage,
|
||||
});
|
||||
const markup = renderToStaticMarkup(
|
||||
<DataGridPaginationBar
|
||||
isV2Ui
|
||||
pagination={{ current: 10, pageSize: 10, total: 100, totalKnown: true }}
|
||||
paginationV2SummaryText="100 rows"
|
||||
paginationSummaryText="100 rows"
|
||||
paginationControlTotal={100}
|
||||
paginationTotalPages={10}
|
||||
paginationPageText="Page 10 / 10"
|
||||
paginationPageSizeOptions={['10']}
|
||||
showKnownPageCount
|
||||
onPageChange={onPageChange}
|
||||
onLastPage={onLastPage}
|
||||
onPageSizeChange={vi.fn()}
|
||||
onV2PageStep={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const lastPageButton = markup.match(/<button[^>]*data-grid-pagination-last="true"[^>]*>/)?.[0];
|
||||
|
||||
expect(lastPageButton).toBeDefined();
|
||||
expect(lastPageButton).not.toContain('disabled');
|
||||
expect(action).toEqual(expect.any(Function));
|
||||
action?.();
|
||||
|
||||
expect(onLastPage).toHaveBeenCalledWith(10);
|
||||
expect(onPageChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to cached last-page navigation when no fresh callback is available', () => {
|
||||
const onPageChange = vi.fn();
|
||||
const action = createDataGridLastPageAction({
|
||||
current: 3,
|
||||
pageSize: 10,
|
||||
totalPages: 10,
|
||||
totalKnown: true,
|
||||
onPageChange,
|
||||
});
|
||||
|
||||
expect(action).toEqual(expect.any(Function));
|
||||
action?.();
|
||||
expect(onPageChange).toHaveBeenCalledWith(10, 10);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface DataGridPaginationBarProps {
|
||||
manualTotalCountAvailable?: boolean;
|
||||
totalCountLoading?: boolean;
|
||||
onPageChange?: (page: number, size: number) => void;
|
||||
onLastPage?: (pageSize: number) => void;
|
||||
onPageSizeChange: (value: string) => void;
|
||||
onV2PageStep: (direction: 'previous' | 'next') => void;
|
||||
onToggleTotalCount?: () => void;
|
||||
@@ -65,6 +66,36 @@ export const resolveDataGridPaginationBoundaryTarget = ({
|
||||
return current < lastPage ? lastPage : null;
|
||||
};
|
||||
|
||||
export const createDataGridLastPageAction = ({
|
||||
current,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalKnown,
|
||||
onPageChange,
|
||||
onLastPage,
|
||||
}: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalKnown: boolean;
|
||||
onPageChange?: (page: number, size: number) => void;
|
||||
onLastPage?: (pageSize: number) => void;
|
||||
}): (() => void) | null => {
|
||||
if (onLastPage) {
|
||||
return () => onLastPage(pageSize);
|
||||
}
|
||||
|
||||
const target = resolveDataGridPaginationBoundaryTarget({
|
||||
boundary: 'last',
|
||||
current,
|
||||
totalPages,
|
||||
totalKnown,
|
||||
canNavigate: Boolean(onPageChange),
|
||||
});
|
||||
if (!onPageChange || target === null) return null;
|
||||
return () => onPageChange(target, pageSize);
|
||||
};
|
||||
|
||||
const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
||||
isV2Ui,
|
||||
pagination,
|
||||
@@ -78,6 +109,7 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
||||
manualTotalCountAvailable = false,
|
||||
totalCountLoading = false,
|
||||
onPageChange,
|
||||
onLastPage,
|
||||
onPageSizeChange,
|
||||
onV2PageStep,
|
||||
onToggleTotalCount,
|
||||
@@ -160,12 +192,13 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
||||
totalKnown: showKnownPageCount,
|
||||
canNavigate: Boolean(onPageChange),
|
||||
});
|
||||
const lastPageTarget = resolveDataGridPaginationBoundaryTarget({
|
||||
boundary: 'last',
|
||||
const lastPageAction = createDataGridLastPageAction({
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
totalPages: paginationTotalPages,
|
||||
totalKnown: showKnownPageCount,
|
||||
canNavigate: Boolean(onPageChange),
|
||||
onPageChange,
|
||||
onLastPage,
|
||||
});
|
||||
const navigateToBoundary = (target: number | null) => {
|
||||
if (!onPageChange || target === null) return;
|
||||
@@ -198,8 +231,8 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
||||
icon={<VerticalLeftOutlined />}
|
||||
iconPosition="end"
|
||||
aria-label={lastPageLabel}
|
||||
disabled={lastPageTarget === null}
|
||||
onClick={() => navigateToBoundary(lastPageTarget)}
|
||||
disabled={lastPageAction === null}
|
||||
onClick={() => lastPageAction?.()}
|
||||
>
|
||||
{lastPageLabel}
|
||||
</Button>
|
||||
|
||||
@@ -228,6 +228,7 @@ const DataGridShell: React.FC<DataGridShellProps> = (props) => {
|
||||
onCancelTotalCount,
|
||||
onOpenErTable,
|
||||
onPageChange,
|
||||
onLastPage,
|
||||
onReload,
|
||||
onRequestTotalCount,
|
||||
onSort,
|
||||
@@ -490,6 +491,7 @@ const renderDataTableView = () => (
|
||||
manualTotalCountAvailable={prefersManualTotalCount && !!onRequestTotalCount}
|
||||
totalCountLoading={pagination?.totalCountLoading}
|
||||
onPageChange={onPageChange}
|
||||
onLastPage={onLastPage}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onV2PageStep={handleV2PageStep}
|
||||
onToggleTotalCount={onRequestTotalCount ? handleToggleTotalCount : undefined}
|
||||
|
||||
@@ -673,6 +673,203 @@ describe('DataViewer safe editing locator', () => {
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: 'shrinks', nextTotal: 51, expectedPage: 6, expectedOffset: 50 },
|
||||
{ label: 'grows', nextTotal: 151, expectedPage: 16, expectedOffset: 150 },
|
||||
])('recounts and navigates to the current last page when table data $label', async ({
|
||||
nextTotal,
|
||||
expectedPage,
|
||||
expectedOffset,
|
||||
}) => {
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
backendApp.DBGetColumns.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
|
||||
});
|
||||
|
||||
let databaseTotal = 101;
|
||||
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
|
||||
const normalizedSql = String(sql || '');
|
||||
if (/count\s*\(/i.test(normalizedSql)) {
|
||||
return {
|
||||
success: true,
|
||||
fields: ['total'],
|
||||
data: [{ total: databaseTotal }],
|
||||
};
|
||||
}
|
||||
const limit = Number(normalizedSql.match(/\bLIMIT\s+(\d+)/i)?.[1] || 101);
|
||||
const offset = Number(normalizedSql.match(/\bOFFSET\s+(\d+)/i)?.[1] || 0);
|
||||
const rowCount = Math.max(0, Math.min(limit, databaseTotal - offset));
|
||||
return {
|
||||
success: true,
|
||||
fields: ['ID', 'NAME'],
|
||||
data: createRows(rowCount),
|
||||
};
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<DataViewer tab={createTab({
|
||||
id: `tab-last-page-${nextTotal}`,
|
||||
dbName: 'main',
|
||||
tableName: 'users',
|
||||
title: 'users',
|
||||
})} />);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(dataGridState.latestProps?.pagination).toMatchObject({ total: 101, totalKnown: true });
|
||||
|
||||
await act(async () => {
|
||||
await dataGridState.latestProps.onPageChange(11, 10);
|
||||
});
|
||||
await flushPromises();
|
||||
expect(dataGridState.latestProps?.pagination).toMatchObject({ current: 11, pageSize: 10, total: 101 });
|
||||
|
||||
databaseTotal = nextTotal;
|
||||
const callsBeforeLastPage = backendApp.DBQuery.mock.calls.length;
|
||||
expect(dataGridState.latestProps?.onLastPage).toEqual(expect.any(Function));
|
||||
await act(async () => {
|
||||
await dataGridState.latestProps.onLastPage(10);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const lastPageSql = backendApp.DBQuery.mock.calls
|
||||
.slice(callsBeforeLastPage)
|
||||
.map((call: any[]) => String(call[2] || ''));
|
||||
expect(lastPageSql[0]).toMatch(/count\s*\(/i);
|
||||
expect(lastPageSql.some((sql: string) => new RegExp(`\\bOFFSET\\s+${expectedOffset}\\b`, 'i').test(sql))).toBe(true);
|
||||
expect(dataGridState.latestProps?.pagination).toMatchObject({
|
||||
current: expectedPage,
|
||||
pageSize: 10,
|
||||
total: nextTotal,
|
||||
totalKnown: true,
|
||||
});
|
||||
expect(dataGridState.latestProps?.data).toHaveLength(1);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('ignores a stale last-page count after a newer page navigation starts', async () => {
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
backendApp.DBGetColumns.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
|
||||
});
|
||||
|
||||
let deferNextCount = false;
|
||||
let resolveDeferredCount: ((value: any) => void) | undefined;
|
||||
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
|
||||
const normalizedSql = String(sql || '');
|
||||
if (/count\s*\(/i.test(normalizedSql)) {
|
||||
if (deferNextCount) {
|
||||
deferNextCount = false;
|
||||
return new Promise((resolve) => {
|
||||
resolveDeferredCount = resolve;
|
||||
});
|
||||
}
|
||||
return { success: true, fields: ['total'], data: [{ total: 101 }] };
|
||||
}
|
||||
const limit = Number(normalizedSql.match(/\bLIMIT\s+(\d+)/i)?.[1] || 101);
|
||||
const offset = Number(normalizedSql.match(/\bOFFSET\s+(\d+)/i)?.[1] || 0);
|
||||
return {
|
||||
success: true,
|
||||
fields: ['ID', 'NAME'],
|
||||
data: createRows(Math.max(0, Math.min(limit, 101 - offset))),
|
||||
};
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<DataViewer tab={createTab({
|
||||
id: 'tab-last-page-race',
|
||||
dbName: 'main',
|
||||
tableName: 'users',
|
||||
title: 'users',
|
||||
})} />);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
deferNextCount = true;
|
||||
let staleLastPageRequest!: Promise<void>;
|
||||
act(() => {
|
||||
staleLastPageRequest = dataGridState.latestProps.onLastPage(10);
|
||||
});
|
||||
await flushPromises();
|
||||
expect(resolveDeferredCount).toEqual(expect.any(Function));
|
||||
|
||||
await act(async () => {
|
||||
await dataGridState.latestProps.onPageChange(2, 10);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
await act(async () => {
|
||||
resolveDeferredCount?.({ success: true, fields: ['total'], data: [{ total: 151 }] });
|
||||
await staleLastPageRequest;
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(dataGridState.latestProps?.pagination).toMatchObject({
|
||||
current: 2,
|
||||
pageSize: 10,
|
||||
total: 101,
|
||||
totalKnown: true,
|
||||
});
|
||||
expect(backendApp.DBQuery.mock.calls
|
||||
.map((call: any[]) => String(call[2] || ''))
|
||||
.some((sql: string) => /\bOFFSET\s+150\b/i.test(sql))).toBe(false);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('reports a fresh last-page count failure without querying the cached tail', async () => {
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
backendApp.DBGetColumns.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
|
||||
});
|
||||
|
||||
let failNextCount = false;
|
||||
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
|
||||
const normalizedSql = String(sql || '');
|
||||
if (/count\s*\(/i.test(normalizedSql)) {
|
||||
if (failNextCount) {
|
||||
failNextCount = false;
|
||||
return { success: false, message: '', data: [] };
|
||||
}
|
||||
return { success: true, fields: ['total'], data: [{ total: 101 }] };
|
||||
}
|
||||
return { success: true, fields: ['ID', 'NAME'], data: createRows(101) };
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<DataViewer tab={createTab({
|
||||
id: 'tab-last-page-count-failure',
|
||||
dbName: 'main',
|
||||
tableName: 'users',
|
||||
title: 'users',
|
||||
})} />);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
failNextCount = true;
|
||||
const callsBeforeLastPage = backendApp.DBQuery.mock.calls.length;
|
||||
await act(async () => {
|
||||
await dataGridState.latestProps.onLastPage(10);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const lastPageSql = backendApp.DBQuery.mock.calls
|
||||
.slice(callsBeforeLastPage)
|
||||
.map((call: any[]) => String(call[2] || ''));
|
||||
expect(lastPageSql).toHaveLength(1);
|
||||
expect(lastPageSql[0]).toMatch(/count\s*\(/i);
|
||||
expect(messageApi.error).toHaveBeenCalledWith('统计总数失败');
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('shows an actionable message for DuckDB timeout interruption errors', async () => {
|
||||
storeState.languagePreference = 'en-US';
|
||||
storeState.connections[0].config.type = 'duckdb';
|
||||
|
||||
@@ -42,6 +42,11 @@ type ViewerPaginationState = {
|
||||
totalCountCancelled: boolean;
|
||||
};
|
||||
|
||||
type DataViewerFetchOptions = {
|
||||
refreshTotal?: boolean;
|
||||
navigateToLastPage?: boolean;
|
||||
};
|
||||
|
||||
type DataViewerTranslator = (key: string, params?: I18nParams) => string;
|
||||
|
||||
const JS_MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
@@ -583,8 +588,9 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
setPagination(prev => ({ ...prev, totalCountLoading: false, totalCountCancelled: true }));
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async (page = pagination.current, size = pagination.pageSize, options?: { refreshTotal?: boolean }) => {
|
||||
const refreshTotal = options?.refreshTotal === true;
|
||||
const fetchData = useCallback(async (page = pagination.current, size = pagination.pageSize, options?: DataViewerFetchOptions) => {
|
||||
const navigateToLastPage = options?.navigateToLastPage === true;
|
||||
const refreshTotal = options?.refreshTotal === true || navigateToLastPage;
|
||||
const seq = ++fetchSeqRef.current;
|
||||
setLoading(true);
|
||||
const conn = connections.find(c => c.id === tab.connectionId);
|
||||
@@ -726,12 +732,81 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
const countSql = isMongoDB
|
||||
? buildMongoCountCommand(tableName, mongoFilter || {})
|
||||
: `SELECT COUNT(*) as total FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`;
|
||||
const countKey = `${tab.connectionId}|${dbName}|${tableName}|${whereSQL}`;
|
||||
let refreshedTotal: number | null = null;
|
||||
|
||||
if (navigateToLastPage) {
|
||||
countSeqRef.current++;
|
||||
manualCountSeqRef.current++;
|
||||
duckdbApproxSeqRef.current++;
|
||||
oracleApproxSeqRef.current++;
|
||||
countKeyRef.current = '';
|
||||
autoCountKeyRef.current = '';
|
||||
manualCountKeyRef.current = '';
|
||||
duckdbApproxKeyRef.current = '';
|
||||
oracleApproxKeyRef.current = '';
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
totalCountLoading: false,
|
||||
totalCountCancelled: false,
|
||||
}));
|
||||
|
||||
latestConfigRef.current = config;
|
||||
latestDbTypeRef.current = dbTypeLower;
|
||||
latestDbNameRef.current = dbName;
|
||||
latestCountSqlRef.current = countSql;
|
||||
latestCountKeyRef.current = countKey;
|
||||
|
||||
const countStart = Date.now();
|
||||
const countConfig = buildRpcConnectionConfig(config, { timeout: 120 });
|
||||
try {
|
||||
const resCount = await DBQuery(countConfig as any, dbName, countSql);
|
||||
addSqlLog({
|
||||
id: `log-${Date.now()}-last-page-count`,
|
||||
timestamp: Date.now(),
|
||||
sql: countSql,
|
||||
status: resCount?.success ? 'success' : 'error',
|
||||
duration: Date.now() - countStart,
|
||||
message: resCount?.success ? '' : String(resCount?.message || tr('data_viewer.message.total_count_failed')),
|
||||
dbName,
|
||||
});
|
||||
|
||||
if (fetchSeqRef.current !== seq) return;
|
||||
if (!resCount?.success) {
|
||||
setPagination(prev => ({ ...prev, totalCountLoading: false }));
|
||||
message.error(String(resCount?.message || tr('data_viewer.message.total_count_failed')));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(resCount.data) || resCount.data.length === 0) {
|
||||
setPagination(prev => ({ ...prev, totalCountLoading: false }));
|
||||
message.error(tr('data_viewer.message.total_count_failed'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshedTotal = parseTotalFromCountRow(resCount.data[0]);
|
||||
if (refreshedTotal === null) {
|
||||
setPagination(prev => ({ ...prev, totalCountLoading: false }));
|
||||
message.error(tr('data_viewer.message.total_count_parse_failed'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (fetchSeqRef.current !== seq) return;
|
||||
setPagination(prev => ({ ...prev, totalCountLoading: false }));
|
||||
message.error(tr('data_viewer.message.total_count_failed_detail', { detail: String(e?.message || e) }));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const orderBySQL = isMongoDB
|
||||
? ''
|
||||
: buildOrderBySQL(dbType, sortInfo, resolveDataViewerOrderFallbackColumns(editLocatorForQuery, pkColumnsForQuery));
|
||||
const totalRows = Number(pagination.total);
|
||||
const totalRows = refreshedTotal ?? Number(pagination.total);
|
||||
const hasFiniteTotal = Number.isFinite(totalRows) && totalRows >= 0;
|
||||
const totalKnown = !refreshTotal && pagination.totalKnown && hasFiniteTotal;
|
||||
const totalKnown = refreshedTotal !== null || (!refreshTotal && pagination.totalKnown && hasFiniteTotal);
|
||||
const approximateTotalRows = Number(pagination.approximateTotal);
|
||||
const hasApproximateTotalPages =
|
||||
!refreshTotal &&
|
||||
@@ -740,9 +815,10 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
pagination.totalApprox &&
|
||||
Number.isFinite(approximateTotalRows) &&
|
||||
approximateTotalRows > 0;
|
||||
const effectiveTotalRows = hasApproximateTotalPages ? approximateTotalRows : (refreshTotal ? 0 : totalRows);
|
||||
const effectiveTotalRows = refreshedTotal ?? (hasApproximateTotalPages ? approximateTotalRows : (refreshTotal ? 0 : totalRows));
|
||||
const totalPages = Number.isFinite(effectiveTotalRows) && effectiveTotalRows > 0 ? Math.max(1, Math.ceil(effectiveTotalRows / size)) : 0;
|
||||
const currentPage = totalPages > 0 ? Math.min(Math.max(1, page), totalPages) : Math.max(1, page);
|
||||
const requestedPage = navigateToLastPage ? Math.max(1, totalPages) : page;
|
||||
const currentPage = totalPages > 0 ? Math.min(Math.max(1, requestedPage), totalPages) : Math.max(1, requestedPage);
|
||||
const offset = (currentPage - 1) * size;
|
||||
const isClickHouse = !isMongoDB && dbTypeLower === 'clickhouse';
|
||||
const reverseOrderSQL = isClickHouse ? reverseOrderBySQL(orderBySQL) : '';
|
||||
@@ -905,7 +981,6 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = `row-${offset + i}`;
|
||||
});
|
||||
setData(resultData);
|
||||
const countKey = `${tab.connectionId}|${dbName}|${tableName}|${whereSQL}`;
|
||||
const derivedTotalKnown = !hasMore;
|
||||
const derivedTotal = derivedTotalKnown ? offset + resultData.length : currentPage * size + 1;
|
||||
const minExpectedTotal = hasMore ? offset + resultData.length + 1 : offset + resultData.length;
|
||||
@@ -1193,6 +1268,9 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
setSortInfo([{ columnKey: normalizedField, order: normalizedOrder, enabled: true }]);
|
||||
}, []);
|
||||
const handlePageChange = useCallback((page: number, size: number) => fetchData(page, size), [fetchData]);
|
||||
const handleLastPage = useCallback((pageSize: number) => (
|
||||
fetchData(1, pageSize, { navigateToLastPage: true })
|
||||
), [fetchData]);
|
||||
const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []);
|
||||
const handleApplyFilter = useCallback((conditions: FilterCondition[]) => {
|
||||
skipNextAutoFetchRef.current = false;
|
||||
@@ -1278,6 +1356,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
onReload={handleReload}
|
||||
onSort={handleSort}
|
||||
onPageChange={handlePageChange}
|
||||
onLastPage={handleLastPage}
|
||||
pagination={pagination}
|
||||
onRequestTotalCount={preferManualTotalCount ? handleManualTotalCount : undefined}
|
||||
onCancelTotalCount={preferManualTotalCount ? handleCancelManualTotalCount : undefined}
|
||||
|
||||
Reference in New Issue
Block a user