feat(redis): move scan page size to connections
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user