feat(redis): move scan page size to connections

This commit is contained in:
dalew authored and GitHub committed 2026-06-26 11:13:34 +08:00
1 parent 367d3682d2
commit d815a16b86
22 files changed
+70 -63

No files matched your search

@@ -17,6 +17,7 @@ import type { ConnectionConfig, DatabaseType, JdbcDriverInfo, JdbcMavenBundleInf
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos";
import { useConnectionStore } from "@/stores/connectionStore";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redisKeyPattern";
import { useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
@@ -152,6 +153,7 @@ const defaultForm = (): ConnectionForm => ({
redis_sentinel_tls: false,
redis_cluster_nodes: "",
redis_key_separator: ":",
redis_scan_page_size: REDIS_SCAN_PAGE_SIZE_DEFAULT,
etcd_endpoints: "",
gbase_server: "",
informix_server: "",
@@ -924,6 +926,7 @@ watch(
redis_sentinel_tls: config.redis_sentinel_tls || false,
redis_cluster_nodes: config.redis_cluster_nodes || "",
redis_key_separator: config.redis_key_separator ?? ":",
redis_scan_page_size: config.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT,
etcd_endpoints: config.etcd_endpoints || "",
informix_server: config.informix_server || "",
read_only: config.read_only || false,
@@ -1573,6 +1576,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.redis_sentinel_tls = undefined;
config.redis_cluster_nodes = undefined;
config.redis_key_separator = undefined;
config.redis_scan_page_size = undefined;
} else if (config.redis_connection_mode === "sentinel") {
config.redis_sentinel_master = config.redis_sentinel_master?.trim() || "";
config.redis_sentinel_nodes = normalizeRedisSentinelNodes(config.redis_sentinel_nodes || "");
@@ -1606,6 +1610,8 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
}
if (config.db_type === "redis") {
config.redis_key_separator = config.redis_key_separator?.trim() ?? ":";
const scanSize = Number(config.redis_scan_page_size);
config.redis_scan_page_size = Number.isFinite(scanSize) && scanSize >= REDIS_SCAN_PAGE_SIZE_MIN && scanSize <= REDIS_SCAN_PAGE_SIZE_MAX ? Math.round(scanSize) : REDIS_SCAN_PAGE_SIZE_DEFAULT;
}
if (config.db_type === "zookeeper") {
const normalizedConnectString = normalizeZooKeeperConnectString(config.connection_string || "");
@@ -3859,6 +3865,22 @@ function openExternalUrl(url: string) {
<span class="text-xs text-muted-foreground">{{ t("connection.readOnlyHint") }}</span>
</label>
</div>
<div v-show="form.db_type === 'redis'" class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("settings.redisScanPageSize") }}</Label>
<div class="col-span-3 flex flex-col gap-1">
<Select :model-value="String(form.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT)" @update:model-value="form.redis_scan_page_size = Number($event)">
<SelectTrigger>
<SelectValue :placeholder="t('settings.redisScanPageSize')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="size in REDIS_SCAN_PAGE_SIZE_OPTIONS" :key="size" :value="String(size)">
{{ t("settings.redisScanPageSizeOption", { count: size }) }}
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">{{ t("settings.redisScanPageSizeDescription") }}</p>
</div>
</div>
</div>
</TabsContent>
@@ -206,7 +206,6 @@ const editTableColumnTemplateDatabaseType = ref<DatabaseType>(TABLE_COLUMN_TEMPL
const tableColumnTemplateSectionRef = ref<HTMLElement | null>(null);
const draggedTableColumnTemplateRowId = ref<string | null>(null);
let tableColumnTemplatePointerDragCleanup: (() => void) | null = null;
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
const editSqlFormatter = ref<SqlFormatterSettings>(normalizeSqlFormatterSettings(settingsStore.editorSettings.sqlFormatter));
const sqlFormatterConfigValid = ref(true);
@@ -226,7 +225,6 @@ const editExportRowLimitEnabled = ref(settingsStore.editorSettings.exportRowLimi
const editExportRowLimit = ref(settingsStore.editorSettings.exportRowLimit);
const editQueryExportKeysetOptimizationEnabled = ref(settingsStore.editorSettings.queryExportKeysetOptimizationEnabled);
const editToolbarItems = ref({ ...settingsStore.editorSettings.toolbarItems });
const redisScanPageSizeOptions = [200, 1000, 5000, 10000];
const systemFonts = ref<string[]>([]);
const systemFontsLoading = ref(false);
const systemFontsLoaded = ref(false);
@@ -477,7 +475,6 @@ watch(
editInfiniteScroll.value = settingsStore.editorSettings.infiniteScroll;
editInfiniteScrollMaxRows.value = settingsStore.editorSettings.infiniteScrollMaxRows;
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields);
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
editSqlFormatter.value = normalizeSqlFormatterSettings(settingsStore.editorSettings.sqlFormatter);
sqlFormatterConfigValid.value = true;
@@ -540,7 +537,6 @@ function hasChanges(): boolean {
editInfiniteScroll.value !== settingsStore.editorSettings.infiniteScroll ||
editInfiniteScrollMaxRows.value !== settingsStore.editorSettings.infiniteScrollMaxRows ||
JSON.stringify(normalizedEditTableColumnTemplateFields.value) !== JSON.stringify(settingsStore.editorSettings.tableColumnTemplateFields) ||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
JSON.stringify(editSqlFormatter.value) !== JSON.stringify(normalizeSqlFormatterSettings(settingsStore.editorSettings.sqlFormatter)) ||
editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation ||
@@ -584,7 +580,6 @@ async function persistSettings() {
infiniteScroll: editInfiniteScroll.value,
infiniteScrollMaxRows: editInfiniteScrollMaxRows.value,
tableColumnTemplateFields: normalizedEditTableColumnTemplateFields.value,
redisScanPageSize: editRedisScanPageSize.value,
shortcuts: editShortcuts.value,
sqlFormatter: normalizeSqlFormatterSettings(editSqlFormatter.value),
sidebarActivation: editSidebarActivation.value,
@@ -674,8 +669,6 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editExportRowLimitEnabled.value = DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled;
editExportRowLimit.value = DEFAULT_EDITOR_SETTINGS.exportRowLimit;
editQueryExportKeysetOptimizationEnabled.value = DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled;
} else if (tab === "redis") {
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
} else if (tab === "shortcuts") {
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
} else if (tab === "snippets") {
@@ -708,7 +701,6 @@ function resetAllDefaults() {
editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll;
editInfiniteScrollMaxRows.value = DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields);
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
editSqlFormatter.value = normalizeSqlFormatterSettings(DEFAULT_EDITOR_SETTINGS.sqlFormatter);
sqlFormatterConfigValid.value = true;
@@ -910,11 +902,6 @@ function onLocaleChange(v: any) {
if (typeof v === "string") void setLocale(v as Locale);
}
function onRedisScanPageSizeChange(v: any) {
const value = Number(v);
if (redisScanPageSizeOptions.includes(value)) editRedisScanPageSize.value = value;
}
function setSidebarObjectDisplay(value: "grouped" | "simple") {
editSidebarObjectDisplay.value = value;
}
@@ -1007,14 +994,13 @@ function setSidebarActivation(value: "single" | "double") {
const activeSettingsTab = ref("editor");
const isWeb = !isTauriRuntime();
const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : ""));
type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "redis" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
{ value: "editor", label: t("settings.editorTab") },
{ value: "formatter", label: t("settings.sqlFormatterTab") },
{ value: "appearance", label: t("settings.appearanceTab") },
{ value: "navigation", label: t("settings.navigationTab") },
{ value: "data", label: t("settings.dataTab") },
{ value: "redis", label: t("settings.redisTab") },
{ value: "shortcuts", label: t("settings.shortcutsTab") },
{ value: "snippets", label: t("settings.snippetsTab") },
...(isWeb ? [] : [{ value: "sync" as const, label: t("settings.syncTab") }]),
@@ -1023,7 +1009,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[
...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []),
{ value: "about", label: t("settings.aboutTab") },
]);
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "redis", "shortcuts", "snippets"]);
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "shortcuts", "snippets"]);
function hasSettingsApplyFooter(value: SettingsCategory): boolean {
return settingsTabsWithApplyFooter.has(value);
@@ -2729,23 +2715,6 @@ watch(
</div>
</section>
<section v-else-if="activeSettingsTab === 'redis'" class="flex flex-col gap-5 py-2">
<div class="space-y-2">
<Label>{{ t("settings.redisScanPageSize") }}</Label>
<Select :model-value="String(editRedisScanPageSize)" @update:model-value="onRedisScanPageSizeChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.redisScanPageSize')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="size in redisScanPageSizeOptions" :key="size" :value="String(size)">
{{ t("settings.redisScanPageSizeOption", { count: size }) }}
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">{{ t("settings.redisScanPageSizeDescription") }}</p>
</div>
</section>
<section v-else-if="activeSettingsTab === 'shortcuts'" class="flex flex-col gap-2 py-2">
<div class="overflow-hidden rounded-md border border-border/70 bg-background">
<div v-for="definition in SHORTCUT_DEFINITIONS" :key="definition.id" class="group -mt-px grid gap-2 border-t border-border/70 px-3 py-2 transition-colors first:mt-0 first:border-t-0 hover:bg-muted/40 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
@@ -21,7 +21,6 @@ import * as api from "@/lib/api";
import type { RedisKeyInfo, RedisScanResult, RedisValue, HistoryEntry } from "@/lib/api";
import { uuid } from "@/lib/utils";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { buildRedisKeyTree, collectExpandedGroupIds, collectRedisGroupKeyRaws, flattenVisibleRedisKeyTree, mergeKeysIntoRedisKeyTree, redisKeyToFlatTreeRow, type RedisKeyTreeNode } from "@/lib/redisKeyTree";
import { classifyRedisCommandSafety } from "@/lib/redisCommandSafety";
import { isRedisMutatingCommand } from "@/lib/redisCommandTable";
@@ -31,11 +30,11 @@ import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle";
import { useToast } from "@/composables/useToast";
import { redisKeySearchPattern } from "@/lib/redisKeyPattern";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redisKeyPattern";
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
const settingsStore = useSettingsStore();
const editorFontFamilyStyle = useEditorFontFamilyStyle();
type RedisSearchMode = "key" | "value" | "all";
@@ -115,6 +114,7 @@ const searchPlaceholder = computed(() => {
});
const loadingEmptyText = computed(() => (isValueSearchMode.value && valueQuery.value ? t(searchMode.value === "all" ? "redis.searchingAll" : "redis.searchingValues") : t("redis.loadingKeys")));
const redisKeySeparator = computed(() => connectionStore.getConfig(props.connectionId)?.redis_key_separator ?? ":");
const redisScanPageSize = computed(() => connectionStore.getConfig(props.connectionId)?.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT);
watch(redisKeySeparator, () => {
if (flatKeys.value.length > 0) rebuildTree(false);
});
@@ -202,14 +202,14 @@ function mergeTree(newKeys: RedisKeyInfo[]) {
}
async function fetchScanPage(): Promise<RedisScanResult> {
const pageSize = settingsStore.editorSettings.redisScanPageSize;
const pageSize = redisScanPageSize.value;
return isValueSearchMode.value ? await api.redisScanValues(props.connectionId, props.db, scanCursor.value, "*", valueQuery.value, pageSize, searchMode.value === "all") : await api.redisScanKeysBatch(props.connectionId, props.db, scanCursor.value, effectivePattern.value, pageSize, 1, false);
}
/// Batch-scan variant that performs multiple SCAN iterations server-side.
/// Dramatically reduces frontend↔backend roundtrips for bulk loading.
async function fetchScanBatchPage(maxIterations: number, options: { count?: number; includeTypes?: boolean } = {}): Promise<RedisScanResult> {
const pageSize = options.count ?? settingsStore.editorSettings.redisScanPageSize;
const pageSize = options.count ?? redisScanPageSize.value;
// Value search cannot be batched because each key requires a GET.
if (isValueSearchMode.value) {
return api.redisScanValues(props.connectionId, props.db, scanCursor.value, "*", valueQuery.value, pageSize, searchMode.value === "all");
@@ -269,19 +269,19 @@ async function streamValueSearch(requestId: number) {
}
async function fillInitialKeyBatch(requestId: number) {
const targetCount = Math.max(1, settingsStore.editorSettings.redisScanPageSize);
// Use server-side batching to fill the initial view quickly.
// Each batch iteration does one SCAN round-trip; 5 iterations with
// COUNT=1000 should return enough keys for most cases.
const maxIter = Math.max(1, Math.ceil(targetCount / Math.max(1, settingsStore.editorSettings.redisScanPageSize)));
const result = await fetchScanBatchPage(Math.min(maxIter, 8));
if (requestId !== searchRequestId) return;
appendScanResult(result);
// If we still need more keys, do one more batch
if (flatKeys.value.length < targetCount && hasMore.value && requestId === searchRequestId) {
const result2 = await fetchScanBatchPage(Math.min(maxIter, 8));
// Initial batch should fetch a substantial number of keys regardless of
// redisScanPageSize. We scan in batches of at most 8 iterations each and
// keep going until we've done at least 16 total iterations (enough to cover
// most key spaces even with COUNT=200) or hasMore becomes false.
const MAX_TOTAL_ITERATIONS = 16;
const BATCH_ITERATIONS = 8;
let totalIters = 0;
while (requestId === searchRequestId && hasMore.value && totalIters < MAX_TOTAL_ITERATIONS) {
const batchSize = Math.min(BATCH_ITERATIONS, MAX_TOTAL_ITERATIONS - totalIters);
const result = await fetchScanBatchPage(batchSize);
if (requestId !== searchRequestId) return;
appendScanResult(result2);
appendScanResult(result);
totalIters += batchSize;
}
}