feat(grid): load all remaining rows with confirmation guard

This commit is contained in:
zipg authored and GitHub committed 2026-09-24 12:53:00 +08:00
1 parent 0b3e79331b
commit 35d624764f
25 files changed
+336 -27

No files matched your search

+1 -1
View File
@@ -4336,7 +4336,7 @@ onUnmounted(() => {
@format-error="toast(t('toolbar.formatSqlFailed'))"
@save-sql="(tabId: string) => void openSaveSqlDialog(tabId)"
@reload="(tabId: string, sql: any, searchText: any, whereInput: any, orderBy: any, limit: any, offset: any, intent: any) => onReloadData(tabId, sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="(tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string) => onPaginate(tabId, offset, limit, whereInput, orderBy)"
@paginate="(tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean) => onPaginate(tabId, offset, limit, whereInput, orderBy, appendResult)"
@sort="(tabId: string, column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => onSort(tabId, column, columnIndex, direction, whereInput, mode)"
@execute-sql="(tabId: string, sql: string) => onExecuteSql(tabId, sql)"
@click-table="(_tabId: string, target: SqlObjectNavigationTarget) => onClickTable(target)"
@@ -2957,6 +2957,7 @@ defineExpose({ focusSearch });
:total-row-count-is-exact="totalIsExact"
:inexact-total-row-count-mode="documentStoreProvider.kind === 'mongodb' ? 'estimated' : 'at-least'"
:pagination-total-row-count="pageTotal"
:load-all-rows-enabled="false"
:count-total-rows="countExactDocumentTotal"
:full-export-result="documentStoreProvider.kind === 'mongodb' || documentStoreProvider.kind === 'dynamodb' || documentStoreProvider.kind === 'elasticsearch' ? exportAllDocumentStoreDocuments : undefined"
@sort="onSort"
+139 -10
View File
@@ -208,6 +208,7 @@ import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid
import {
canFetchNextDataGridSegment,
canGoNextDataGridPage,
dataGridLoadAllSegment,
dataGridTotalRowCountLabelKey,
dataGridTruncationHintKey,
ELASTICSEARCH_PAGE_JUMP_WARNING_REQUESTS,
@@ -493,6 +494,7 @@ interface DataGridProps {
inexactTotalRowCountMode?: DataGridInexactTotalRowCountMode;
paginationTotalRowCount?: number;
paginationEnabled?: boolean;
loadAllRowsEnabled?: boolean;
totalRowCountLoading?: boolean;
pageJumpProgress?: QueryPageJumpProgress;
/** Document stores (e.g. MongoDB) count exactly on demand without SQL tableMeta/countSql. */
@@ -556,6 +558,7 @@ const props = withDefaults(defineProps<DataGridProps>(), {
totalRowCountIsExact: true,
inexactTotalRowCountMode: "at-least",
paginationEnabled: true,
loadAllRowsEnabled: true,
// Omitted row-action limits must keep normal table-data editing.
allowInsertRows: undefined,
allowDeleteRows: undefined,
@@ -588,7 +591,7 @@ function logDataGridTiming(message: string, payload?: Record<string, unknown>) {
const emit = defineEmits<{
reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent];
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string];
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean];
sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode];
"update:whereInput": [value: string];
"update:orderByInput": [value: string];
@@ -3009,6 +3012,8 @@ let infiniteScrollCheckScheduled = false;
let infiniteScrollAllLoaded = false;
let infiniteScrollRequestedOffset: number | undefined;
let infiniteScrollRequestedLimit: number | undefined;
let infiniteScrollLoadAllPending = false;
const loadAllRowsActive = ref(false);
// Tracks whether the current loading cycle was triggered by a refresh/rollback
// (as opposed to a normal paginate). Used to decide whether to auto-redirect
// when the current page no longer exists after data was deleted.
@@ -3046,11 +3051,14 @@ watch(
},
{ immediate: true },
);
// Clear infinite-scroll loading when the parent finishes loading new data
// Complete an append only after both the loading state and appended result have
// propagated. Large results can update those props in separate render cycles.
watch(
() => props.loading,
(loading, prevLoading) => {
if (prevLoading && !loading && infiniteScrollLoading.value) {
() => [props.loading, props.result.rows.length, props.result.appended_from_row_count] as const,
([loading]) => {
if (!loading && infiniteScrollLoading.value) {
const shouldSelectLastRow = infiniteScrollLoadAllPending;
infiniteScrollLoadAllPending = false;
infiniteScrollLoading.value = false;
isInfiniteScrollPaginating.value = false;
const requestedOffset = infiniteScrollRequestedOffset;
@@ -3062,19 +3070,31 @@ watch(
// optimistic page marker so a later scroll can retry the same segment.
currentPage.value = Math.max(1, currentPage.value - 1);
lastInfiniteScrollPage = Math.max(0, currentPage.value - 1);
loadAllRowsActive.value = false;
return;
}
const appendedRows = props.result.rows.length - requestedOffset;
if (props.result.rows.length >= infiniteScrollMaxRows.value || appendedRows < (requestedLimit ?? pageSize.value)) {
infiniteScrollAllLoaded = true;
}
if (shouldSelectLastRow) selectAndRevealLastLoadedRow();
}
},
{ flush: "post" },
);
const manualTotalRowCount = ref<number | undefined>(undefined);
const manualTotalRowCountLoading = ref(false);
const esDeepPageJumpConfirmOpen = ref(false);
const pendingEsDeepPageJump = ref<{ targetPage: number; requestCount: number; updateCurrentPage: boolean }>();
// One "load all" click fetches the whole remaining segment in a single request;
// with the result-row cap disabled that segment is effectively unbounded, so a
// large shot needs an explicit confirmation the way ES deep page jumps do.
const LOAD_ALL_ROWS_CONFIRM_ROW_THRESHOLD = 100_000;
const loadAllRowsConfirmOpen = ref(false);
const pendingLoadAllRows = ref<{ remaining: number }>();
watch(loadAllRowsConfirmOpen, (open) => {
if (!open) pendingLoadAllRows.value = undefined;
});
watch(esDeepPageJumpConfirmOpen, (open) => {
if (!open) {
pendingEsDeepPageJump.value = undefined;
@@ -3318,8 +3338,6 @@ watch(
(values, previousValues) => {
if (!didDataGridInfiniteScrollContextChange(values, previousValues)) return;
manualTotalRowCount.value = undefined;
// Reset infinite-scroll allLoaded when query context changes
infiniteScrollAllLoaded = false;
},
);
@@ -3402,6 +3420,79 @@ function infiniteScrollNextPage() {
// row identities, which would invalidate pending edits while the user scrolls.
emit("paginate", nextOffset, nextLimit, currentWhereInput(), currentOrderBy());
}
function selectAndRevealLastLoadedRow() {
nextTick(() => {
let rowIndex = displayRowRefs.value.length - 1;
while (rowIndex >= 0 && !("sourceIndex" in displayRowRefs.value[rowIndex])) rowIndex--;
if (rowIndex < 0) return;
selectRow(rowIndex);
if (showTranspose.value) {
transposeRowIndex.value = rowIndex;
nextTick(() => scrollTransposeRecordIntoView(rowIndex, "nearest"));
return;
}
const rowHeight = useCanvasGridRows.value ? CANVAS_DATA_GRID_ROW_HEIGHT : DOM_DATA_GRID_ROW_HEIGHT;
const expectedRowBottom = (rowIndex + 1) * rowHeight;
let remainingFrames = 12;
const revealWhenReady = () => {
const scroller = gridScrollerElement();
if (scroller) {
if (useCanvasGridRows.value) scrollCanvasRowIntoView(rowIndex, "end");
else scrollDomRowIntoView(rowIndex, "end");
const expectedScrollTop = Math.max(0, expectedRowBottom - scroller.clientHeight);
if (scroller.scrollTop >= expectedScrollTop - 1) return;
}
remainingFrames--;
if (remainingFrames > 0) requestAnimationFrame(revealWhenReady);
};
requestAnimationFrame(revealWhenReady);
});
}
function loadAllRowsAndGoToLast() {
if (!props.loadAllRowsEnabled || gridSurfaceBusy.value || infiniteScrollLoading.value || props.result.rows.length === 0) return;
// search_after cursor paging fetches page by page; a single giant append
// against those cursors is untested, so ES/Easysearch grids keep the
// reveal-only shortcut instead of loading everything.
if (isResultsContext.value && (resolvedDatabaseType.value === "elasticsearch" || resolvedDatabaseType.value === "easysearch")) return;
const segment = dataGridLoadAllSegment(props.result.rows.length, infiniteScrollMaxRows.value, !infiniteScrollAllLoaded && canFetchNextInfiniteScrollSegment.value);
if (!segment) {
loadAllRowsActive.value = true;
infiniteScrollAllLoaded = true;
selectAndRevealLastLoadedRow();
return;
}
const knownTotal = displayedTotalRowCount.value;
const remaining = typeof knownTotal === "number" && Number.isFinite(knownTotal) && knownTotal >= props.result.rows.length ? knownTotal - props.result.rows.length : segment.limit;
if (remaining > LOAD_ALL_ROWS_CONFIRM_ROW_THRESHOLD) {
pendingLoadAllRows.value = { remaining };
loadAllRowsConfirmOpen.value = true;
return;
}
startLoadAllRows(segment);
}
function startLoadAllRows(segment: { offset: number; limit: number }) {
loadAllRowsActive.value = true;
infiniteScrollLoadAllPending = true;
infiniteScrollLoading.value = true;
isInfiniteScrollPaginating.value = true;
infiniteScrollRequestedOffset = segment.offset;
infiniteScrollRequestedLimit = segment.limit;
currentPage.value++;
emit("paginate", segment.offset, segment.limit, currentWhereInput(), currentOrderBy(), true);
}
function confirmLoadAllRows() {
const pending = pendingLoadAllRows.value;
if (!pending) return;
pendingLoadAllRows.value = undefined;
loadAllRowsConfirmOpen.value = false;
const segment = dataGridLoadAllSegment(props.result.rows.length, infiniteScrollMaxRows.value, !infiniteScrollAllLoaded && canFetchNextInfiniteScrollSegment.value);
if (segment) startLoadAllRows(segment);
}
function checkInfiniteScroll(scroller: HTMLElement) {
if (!infiniteScrollEnabled.value || infiniteScrollLoading.value || props.loading) return;
if (infiniteScrollAllLoaded) return;
@@ -3423,6 +3514,7 @@ function changePageSize(size: number) {
currentPage.value = 1;
lastInfiniteScrollPage = 0;
infiniteScrollAllLoaded = false;
loadAllRowsActive.value = false;
infiniteScrollPositions = new WeakMap();
resetGridVerticalScroll(true);
emit("paginate", 0, normalizedSize, currentWhereInput(), currentOrderBy());
@@ -4199,13 +4291,15 @@ function resetInfiniteScrollState() {
infiniteScrollRequestedLimit = undefined;
isInfiniteScrollPaginating.value = false;
infiniteScrollLoading.value = false;
infiniteScrollLoadAllPending = false;
loadAllRowsActive.value = false;
infiniteScrollPositions = new WeakMap();
resetGridVerticalScroll(true);
}
function prepareFullReload() {
const viewportAnchor = captureViewportAnchorForRefresh();
if (infiniteScrollEnabled.value) {
if (infiniteScrollEnabled.value || loadAllRowsActive.value) {
resetInfiniteScrollState();
}
const selection = captureCurrentSelectionForRefresh();
@@ -6114,7 +6208,7 @@ function applyColumnSort(column: string, columnIndex: number, direction: "asc" |
toast(t("grid.largeValueLocalSortUnavailable"), 5000);
return;
}
if (mode === "database" && infiniteScrollEnabled.value) {
if (mode === "database" && (infiniteScrollEnabled.value || loadAllRowsActive.value)) {
resetInfiniteScrollState();
} else {
currentPage.value = 1;
@@ -9983,8 +10077,21 @@ watch(
infiniteScrollLoading.value = false;
isInfiniteScrollPaginating.value = false;
}
// The append completion above already reset `infiniteScrollLoading`, so the
// post-flush loading watcher cannot observe this append; a "load all" run
// must still reveal its last row from here.
if (infiniteScrollLoadAllPending) {
infiniteScrollLoadAllPending = false;
selectAndRevealLastLoadedRow();
}
return;
}
// A non-append result replaces the whole data set, so a running "load all" is over.
loadAllRowsActive.value = false;
// The replacement also invalidates the all-loaded marker: a filter change or
// page jump swaps in a fresh first page whose remaining segments must be
// re-derived instead of being silently skipped.
infiniteScrollAllLoaded = false;
if (getResetScrollAfterResult()) {
clearResetScrollAfterResult();
resetGridVerticalScroll();
@@ -13872,9 +13979,12 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
:selection-summary="selectionSummary"
:selection-summary-sum-text="selectionSummarySumText"
:selection-summary-average-text="selectionSummaryAverageText"
:loading="gridPaginationBusy"
:loading="gridPaginationBusy || infiniteScrollLoading"
:infinite-scroll-enabled="infiniteScrollEnabled"
:infinite-scroll-all-loaded="infiniteScrollAllLoaded"
:load-all-rows-active="loadAllRowsActive"
:load-all-rows-enabled="loadAllRowsEnabled"
:can-load-all-rows="result.rows.length > 0"
:page-size="pageSize"
:default-page-size="defaultPageSize"
:page-size-menu-items="pageSizeMenuItems"
@@ -13891,6 +14001,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@next-page="nextPage"
@jump-page="jumpPage"
@last-page="lastPage"
@load-all-rows="loadAllRowsAndGoToLast"
@select-export="selectExportMenuItem"
/>
</div>
@@ -13999,6 +14110,24 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
</DialogContent>
</Dialog>
<Dialog v-model:open="loadAllRowsConfirmOpen">
<DialogContent class="sm:max-w-[480px]">
<DialogHeader>
<DialogTitle class="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<AlertTriangle class="h-5 w-5" />
{{ t("grid.loadAllRowsConfirmTitle") }}
</DialogTitle>
</DialogHeader>
<p class="py-3 text-sm leading-6 text-muted-foreground">
{{ t("grid.loadAllRowsConfirmMessage", { count: pendingLoadAllRows?.remaining ?? 0 }) }}
</p>
<DialogFooter>
<Button variant="outline" @click="loadAllRowsConfirmOpen = false">{{ t("dangerDialog.cancel") }}</Button>
<Button @click="confirmLoadAllRows">{{ t("grid.loadAllRowsContinue") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="generateIncrementDialogOpen">
<DialogContent class="sm:max-w-[380px]">
<DialogHeader>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Check, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2 } from "@lucide/vue";
import { Check, ChevronLeft, ChevronRight, ChevronsDown, ChevronsLeft, ChevronsRight, Loader2 } from "@lucide/vue";
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
@@ -20,6 +20,9 @@ const props = withDefaults(
loading: boolean;
infiniteScrollEnabled: boolean;
infiniteScrollAllLoaded: boolean;
loadAllRowsActive: boolean;
loadAllRowsEnabled: boolean;
canLoadAllRows: boolean;
pageSize: number;
defaultPageSize: number;
pageSizeMenuItems: LightDropdownItem[];
@@ -29,7 +32,7 @@ const props = withDefaults(
canGoNextPage: boolean;
canJumpLastPage: boolean;
}>(),
{ paginationEnabled: true },
{ paginationEnabled: true, loadAllRowsActive: false, loadAllRowsEnabled: true, canLoadAllRows: false },
);
const customPageSizeInput = defineModel<string>("customPageSizeInput", { default: "" });
@@ -49,6 +52,7 @@ const emit = defineEmits<{
nextPage: [];
jumpPage: [page: number];
lastPage: [];
loadAllRows: [];
selectExport: [value: string];
applyCustomPageSizeAndSetDefault: [];
}>();
@@ -96,10 +100,10 @@ function handlePageInputKeydown(event: KeyboardEvent) {
</div>
</div>
<Loader2 class="w-3 h-3 text-muted-foreground" :class="loading ? 'animate-spin' : 'invisible'" aria-hidden="true" />
<template v-if="paginationEnabled && infiniteScrollEnabled">
<template v-if="paginationEnabled && (infiniteScrollEnabled || loadAllRowsActive)">
<span v-if="infiniteScrollAllLoaded" class="text-xs text-muted-foreground shrink-0">{{ t("grid.allLoaded") }}</span>
</template>
<template v-if="paginationEnabled && !infiniteScrollEnabled">
<template v-if="paginationEnabled && !infiniteScrollEnabled && !loadAllRowsActive">
<LightDropdown
:model-value="String(pageSize)"
:items="pageSizeMenuItems"
@@ -152,6 +156,14 @@ function handlePageInputKeydown(event: KeyboardEvent) {
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || !canGoNextPage" @click="emit('nextPage')"><ChevronRight class="h-3 w-3" /></Button>
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || !canJumpLastPage" @click="emit('lastPage')"><ChevronsRight class="h-3 w-3" /></Button>
</template>
<Tooltip v-if="paginationEnabled && loadAllRowsEnabled">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || !canLoadAllRows" :aria-label="t('grid.loadAllAndGoToLastRow')" @click="emit('loadAllRows')">
<ChevronsDown class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.loadAllAndGoToLastRow") }}</TooltipContent>
</Tooltip>
<DataGridExportMenu :items="exportMenuItems" :label="t('grid.export')" :on-select="(value) => emit('selectExport', value)" />
</div>
</template>
@@ -30,6 +30,7 @@ vi.mock("@lucide/vue", async () => {
ChevronsLeft: icon,
ChevronsRight: icon,
Download: icon,
ChevronsDown: icon,
Filter: icon,
Focus: icon,
FileDiff: icon,
@@ -288,7 +289,7 @@ describe("DataGridPagination", () => {
onNextPage: nextPage,
onLastPage: lastPage,
});
const navigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0");
const navigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0" && node.props["aria-label"] !== "grid.loadAllAndGoToLastRow");
expect(navigation.map((node) => node.props.disabled)).toEqual([true, true, true, true]);
navigation.forEach((node) => dispatch(node, "click"));
@@ -298,7 +299,7 @@ describe("DataGridPagination", () => {
expect(lastPage).not.toHaveBeenCalled();
await mounted.setProps({ currentPage: 2, canGoNextPage: true, canJumpLastPage: true });
const enabledNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0");
const enabledNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0" && node.props["aria-label"] !== "grid.loadAllAndGoToLastRow");
expect(enabledNavigation.map((node) => node.props.disabled)).toEqual([false, false, false, false]);
enabledNavigation.forEach((node) => dispatch(node, "click"));
expect(firstPage).toHaveBeenCalledOnce();
@@ -307,7 +308,7 @@ describe("DataGridPagination", () => {
expect(lastPage).toHaveBeenCalledOnce();
await mounted.setProps({ loading: true });
const busyNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0");
const busyNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0" && node.props["aria-label"] !== "grid.loadAllAndGoToLastRow");
expect(busyNavigation.map((node) => node.props.disabled)).toEqual([true, true, true, true]);
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.jumpToPage").props.disabled).toBe(true);
});
@@ -375,6 +376,36 @@ describe("DataGridPagination", () => {
expect(findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0")).toHaveLength(0);
});
it("loads all paginated rows and moves to the last row", async () => {
const loadAllRows = vi.fn();
const mounted = mountComponent(DataGridPagination, {
selectionSummary: null,
selectionSummarySumText: "",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
canLoadAllRows: true,
pageSize: 100,
customPageSizeInput: "",
pageSizeMenuItems: [],
exportMenuItems: [],
currentPage: 1,
canGoNextPage: true,
canJumpLastPage: true,
onLoadAllRows: loadAllRows,
});
const loadAllButton = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow");
expect(loadAllButton.props.disabled).toBe(false);
dispatch(loadAllButton, "click");
expect(loadAllRows).toHaveBeenCalledOnce();
await mounted.setProps({ loading: true });
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow").props.disabled).toBe(true);
await mounted.setProps({ loading: false, canLoadAllRows: false });
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow").props.disabled).toBe(true);
});
});
describe("DataGridColumnHeader", () => {
@@ -2301,7 +2301,7 @@ defineExpose({
@update:order-by-input="(v: string) => (activeTab.orderByInput = v)"
@local-column-filters-change="(filters: Record<string, string[]>) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)"
@reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', activeTab.id, sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy)"
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy, appendResult)"
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', activeTab.id, column, columnIndex, direction, whereInput, mode)"
@change-query-timeout="(connectionId: string) => emit('openConnectionSettings', connectionId, 'advanced')"
>
@@ -2727,7 +2727,7 @@ defineExpose({
@update:order-by-input="(v: string) => (activeTab.orderByInput = v)"
@local-column-filters-change="(filters: Record<string, string[]>) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)"
@reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', activeTab.id, sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy)"
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy, appendResult)"
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', activeTab.id, column, columnIndex, direction, whereInput, mode)"
@change-query-timeout="(connectionId: string) => emit('openConnectionSettings', connectionId, 'advanced')"
>
@@ -91,7 +91,7 @@ export interface ContentAreaSurfaceEmits {
editorStateFlushed: [tabId: string];
formatError: [tabId: string];
reload: [tabId: string, sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent];
paginate: [tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string];
paginate: [tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean];
sort: [tabId: string, column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode];
executeSql: [tabId: string, sql: string];
clickTable: [tabId: string, target: SqlObjectNavigationTarget];
@@ -478,10 +478,10 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
await queryStore.executeCurrentTab();
}
async function onPaginate(tabId: string | undefined, offset: number, limit: number, whereInput?: string, orderBy?: string) {
async function onPaginate(tabId: string | undefined, offset: number, limit: number, whereInput?: string, orderBy?: string, appendRequested = false) {
const tab = resolveActionTab(tabId);
if (!tab) return;
const appendResult = settingsStore.editorSettings.infiniteScroll && offset > 0 && offset === tab.result?.rows.length;
const appendResult = (appendRequested || settingsStore.editorSettings.infiniteScroll) && offset > 0 && offset === tab.result?.rows.length;
const appendOptions = appendResult
? {
appendResult: {
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import az from "../locales/az";
import en from "../locales/en";
import es from "../locales/es";
import it_ from "../locales/it";
import ja from "../locales/ja";
import ko from "../locales/ko";
import ptBR from "../locales/pt-BR";
import tr from "../locales/tr";
import zhCN from "../locales/zh-CN";
import zhTW from "../locales/zh-TW";
const locales: Array<[string, Record<string, unknown>]> = [
["az", az],
["en", en],
["es", es],
["it", it_],
["ja", ja],
["ko", ko],
["pt-BR", ptBR],
["tr", tr],
["zh-CN", zhCN],
["zh-TW", zhTW],
];
// The grid toolbar renders in whatever locale the user runs, so a missing key
// would leak a raw key next to the "load all" button.
const GRID_LOAD_ALL_KEYS = ["loadAllAndGoToLastRow", "allLoaded", "loadAllRowsConfirmTitle", "loadAllRowsConfirmMessage", "loadAllRowsContinue"] as const;
describe("grid load-all locale parity", () => {
it.each(locales)("%s exposes the load-all copy", (_name, locale) => {
const grid = (locale as { grid: Record<string, unknown> }).grid;
for (const key of GRID_LOAD_ALL_KEYS) {
expect(grid[key], `${_name}: grid.${key}`).toBeTypeOf("string");
expect(grid[key] as string, `${_name}: grid.${key}`).not.toHaveLength(0);
}
});
});
+4
View File
@@ -1763,6 +1763,10 @@ export default withEnglishFallback({
esDeepPageJumpConfirmMessage:
"Elasticsearch search_after birbaşa səhifəyə keçə bilmir. DBX çatışmayan kursorları ardıcıllıqla əldə etməlidir və geriyə keçid 1-ci səhifədən yenidən başlayır. {page} nömrəli səhifəyə keçid zamanı {requests} sorğu göndəriləcəyi gözlənilir. Bu, uzun çəkə və klasterin yükünü artıra bilər. Davam edilsin?",
esDeepPageJumpContinue: "Davam et",
loadAllAndGoToLastRow: "Hamısını yüklə və sonuncu sətrə keç",
loadAllRowsConfirmTitle: "Bütün sətirlərin yüklənməsini təsdiqlə",
loadAllRowsConfirmMessage: "Hamısını yükləmək bir sorğu ilə ən çox {count} sətir əlavə gətirə bilər. Çox böyük nəticələr yaddaşı çox istifadə edir və uzun çəkə bilər. Davam edilsin?",
loadAllRowsContinue: "Hamısını yüklə",
allLoaded: "hamısı yükləndi",
calculateTotalRows: "Sətirlərin ümumi sayını hesabla",
calculateTotalRowsInline: "(sətirlərin ümumi sayını hesabla)",
+4
View File
@@ -1992,6 +1992,10 @@ export default {
"Elasticsearch search_after cannot jump directly to a page. DBX must fetch missing cursors in order, and backward navigation restarts from page 1. Jumping to page {page} is expected to send {requests} queries. This may take a long time and increase cluster load. Continue?",
esDeepPageJumpContinue: "Continue",
allLoaded: "all loaded",
loadAllAndGoToLastRow: "Load all and go to the last row",
loadAllRowsConfirmTitle: "Confirm loading all rows",
loadAllRowsConfirmMessage: "Loading everything fetches up to {count} more rows in one request. Very large results consume significant memory and can take a long time. Continue?",
loadAllRowsContinue: "Load all rows",
calculateTotalRows: "Count total rows",
calculateTotalRowsInline: "(count total rows)",
calculateTotalRowsFailed: "Count failed: {message}",
+4
View File
@@ -1925,6 +1925,10 @@ export default withEnglishFallback({
"search_after de Elasticsearch no puede saltar directamente a una página. DBX debe obtener en orden los cursores que faltan y, al retroceder, comienza de nuevo desde la página 1. Saltar a la página {page} enviará aproximadamente {requests} consultas. Puede tardar y aumentar la carga del clúster. ¿Continuar?",
esDeepPageJumpContinue: "Continuar",
allLoaded: "todo cargado",
loadAllAndGoToLastRow: "Cargar todo e ir a la última fila",
loadAllRowsConfirmTitle: "Confirmar carga de todas las filas",
loadAllRowsConfirmMessage: "Cargar todo puede traer hasta {count} filas más en una sola solicitud. Los resultados muy grandes consumen mucha memoria y pueden tardar. ¿Continuar?",
loadAllRowsContinue: "Cargar todas las filas",
calculateTotalRows: "Contar filas totales",
calculateTotalRowsInline: "(contar filas totales)",
calculateTotalRowsFailed: "Error al contar: {message}",
+4
View File
@@ -1922,6 +1922,10 @@ export default withEnglishFallback({
"search_after di Elasticsearch non può passare direttamente a una pagina. DBX deve recuperare in ordine i cursori mancanti e, tornando indietro, riparte dalla pagina 1. Il passaggio alla pagina {page} invierà circa {requests} query. Potrebbe richiedere tempo e aumentare il carico del cluster. Continuare?",
esDeepPageJumpContinue: "Continua",
allLoaded: "tutto caricato",
loadAllAndGoToLastRow: "Carica tutto e vai all'ultima riga",
loadAllRowsConfirmTitle: "Conferma caricamento di tutte le righe",
loadAllRowsConfirmMessage: "Il caricamento completo può recuperare fino a {count} righe aggiuntive in un'unica richiesta. Risultati molto grandi consumano molta memoria e possono richiedere tempo. Continuare?",
loadAllRowsContinue: "Carica tutte le righe",
calculateTotalRows: "Conta righe totali",
calculateTotalRowsInline: "(conta righe totali)",
calculateTotalRowsFailed: "Conteggio non riuscito: {message}",
+4
View File
@@ -2509,6 +2509,10 @@ export default withEnglishFallback({
"Elasticsearch の search_after では指定ページへ直接移動できません。DBX は不足しているカーソルを順番に取得し、前のページへ戻る場合は 1 ページ目からやり直します。ページ {page} への移動では約 {requests} 回のクエリが送信される見込みです。時間がかかり、クラスター負荷が増える可能性があります。続行しますか?",
esDeepPageJumpContinue: "移動を続ける",
allLoaded: "すべて読み込み済み",
loadAllAndGoToLastRow: "すべて読み込み、最終行へ移動",
loadAllRowsConfirmTitle: "全行ロードの確認",
loadAllRowsConfirmMessage: "すべて読み込むと、1 回のリクエストで最大 {count} 行を追加取得します。非常に大きな結果セットは多くのメモリを消費し、時間がかかることがあります。続行しますか?",
loadAllRowsContinue: "ロードを続行",
previewSqlEmpty: "プレビューする保留中のSQL変更はありません",
renderMode: "レンダーモード",
undoChange: "変更を元に戻す",
+4
View File
@@ -1872,6 +1872,10 @@ export default withEnglishFallback({
"Elasticsearch의 search_after는 지정한 페이지로 바로 이동할 수 없습니다. DBX는 누락된 커서를 순서대로 가져와야 하며 이전 페이지로 이동할 때는 1페이지부터 다시 시작합니다. {page}페이지로 이동하면 약 {requests}개의 쿼리가 전송됩니다. 시간이 오래 걸리고 클러스터 부하가 증가할 수 있습니다. 계속하시겠습니까?",
esDeepPageJumpContinue: "계속 이동",
allLoaded: "모두 로드됨",
loadAllAndGoToLastRow: "모두 불러오고 마지막 행으로 이동",
loadAllRowsConfirmTitle: "전체 로드 확인",
loadAllRowsConfirmMessage: "모두 로드하면 한 번의 요청으로 최대 {count}개의 행을 더 가져옵니다. 매우 큰 결과 집합은 많은 메모리를 사용하고 시간이 오래 걸릴 수 있습니다. 계속하시겠습니까?",
loadAllRowsContinue: "계속 로드",
calculateTotalRows: "전체 행 수 집계",
calculateTotalRowsInline: "(전체 행 수 집계)",
calculateTotalRowsFailed: "집계 실패: {message}",
+4
View File
@@ -1924,6 +1924,10 @@ export default withEnglishFallback({
"O search_after do Elasticsearch não pode ir diretamente para uma página. O DBX precisa buscar os cursores ausentes em ordem e, ao voltar, reinicia pela página 1. Ir para a página {page} deve enviar cerca de {requests} consultas. Isso pode demorar e aumentar a carga do cluster. Continuar?",
esDeepPageJumpContinue: "Continuar",
allLoaded: "tudo carregado",
loadAllAndGoToLastRow: "Carregar tudo e ir para a última linha",
loadAllRowsConfirmTitle: "Confirmar carregamento de todas as linhas",
loadAllRowsConfirmMessage: "Carregar tudo pode buscar até {count} linhas adicionais em uma única solicitação. Resultados muito grandes consomem muita memória e podem demorar. Continuar?",
loadAllRowsContinue: "Carregar todas as linhas",
calculateTotalRows: "Contar total de linhas",
calculateTotalRowsInline: "(contar total de linhas)",
calculateTotalRowsFailed: "Falha ao contar: {message}",
+4
View File
@@ -1764,6 +1764,10 @@ export default withEnglishFallback({
esDeepPageJumpConfirmMessage:
"Elasticsearch search_after doğrudan bir sayfaya atlayamaz. DBX eksik imleçleri sırayla getirmelidir ve geri gitme 1. sayfadan yeniden başlar. {page}. sayfaya atlamak için {requests} sorgu gönderilmesi bekleniyor. Bu uzun sürebilir ve küme yükünü artırabilir. Devam edilsin mi?",
esDeepPageJumpContinue: "Devam et",
loadAllAndGoToLastRow: "Tümünü yükle ve son satıra git",
loadAllRowsConfirmTitle: "Tüm satırları yükleme onayı",
loadAllRowsConfirmMessage: "Tümünü yüklemek tek istekte en fazla {count} satır daha çekebilir. Çok büyük sonuç kümeleri belleği yoğun kullanır ve uzun sürebilir. Devam edilsin mi?",
loadAllRowsContinue: "Tümünü yükle",
allLoaded: "tümü yüklendi",
calculateTotalRows: "Toplam satırları say",
calculateTotalRowsInline: "(toplam satırları say)",
+4
View File
@@ -1905,6 +1905,10 @@ export default withEnglishFallback({
esDeepPageJumpConfirmMessage: "Elasticsearch 的 search_after 不能直接定位到指定页面。DBX 必须按顺序获取缺失的游标,跳回较小页码时还会从第 1 页重新开始。跳转到第 {page} 页预计会发送 {requests} 次查询,可能耗时较长并增加集群负载。是否继续?",
esDeepPageJumpContinue: "继续跳转",
allLoaded: "已全部加载",
loadAllAndGoToLastRow: "加载全部并转到末行",
loadAllRowsConfirmTitle: "确认加载全部行",
loadAllRowsConfirmMessage: "加载全部将通过一次请求最多再获取 {count} 行。超大结果集会占用大量内存并可能耗时较长。是否继续?",
loadAllRowsContinue: "继续加载",
calculateTotalRows: "统计总行数",
calculateTotalRowsInline: "(统计总行数)",
calculateTotalRowsFailed: "统计失败:{message}",
+4
View File
@@ -1913,6 +1913,10 @@ export default withEnglishFallback({
esDeepPageJumpConfirmMessage: "Elasticsearch 的 search_after 無法直接定位到指定頁面。DBX 必須依序取得缺少的游標,跳回較小頁碼時還會從第 1 頁重新開始。跳轉到第 {page} 頁預計會傳送 {requests} 次查詢,可能耗時較長並增加叢集負載。是否繼續?",
esDeepPageJumpContinue: "繼續跳轉",
allLoaded: "已全部載入",
loadAllAndGoToLastRow: "載入全部並前往最後一列",
loadAllRowsConfirmTitle: "確認載入全部資料列",
loadAllRowsConfirmMessage: "載入全部將透過一次請求最多再取得 {count} 筆。超大型結果集會佔用大量記憶體且可能耗時較長。是否繼續?",
loadAllRowsContinue: "繼續載入",
calculateTotalRows: "統計總筆數",
calculateTotalRowsInline: "(統計總筆數)",
calculateTotalRowsFailed: "統計失敗:{message}",
@@ -31,6 +31,11 @@ export interface CanFetchNextDataGridSegmentOptions {
allRowsLoaded?: boolean;
}
export interface DataGridLoadAllSegment {
offset: number;
limit: number;
}
export type DataGridInexactTotalRowCountMode = "at-least" | "estimated";
export const ELASTICSEARCH_PAGE_JUMP_WARNING_REQUESTS = 100;
@@ -109,3 +114,10 @@ export function canFetchNextDataGridSegment(options: CanFetchNextDataGridSegment
if (options.allRowsLoaded === true) return false;
return options.loadedRowCount >= Math.max(1, options.pageSize);
}
export function dataGridLoadAllSegment(loadedRowCount: number, maxRows: number, canFetchMore: boolean): DataGridLoadAllSegment | null {
const offset = Number.isFinite(loadedRowCount) ? Math.max(0, Math.trunc(loadedRowCount)) : 0;
const boundedMaxRows = Number.isFinite(maxRows) ? Math.max(0, Math.trunc(maxRows)) : 0;
if (!canFetchMore || offset >= boundedMaxRows) return null;
return { offset, limit: boundedMaxRows - offset };
}