From 8d73e6e00d7198ad571c946b4b096a4e04402f3b Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 15 May 2026 17:41:26 +0800 Subject: [PATCH] feat(settings): add Redis scan page size setting --- .../editor/EditorSettingsDialog.vue | 930 ++++++++++-------- src/components/redis/RedisKeyBrowser.vue | 5 +- src/i18n/locales/en.ts | 1 + src/i18n/locales/es.ts | 2 + src/i18n/locales/zh-CN.ts | 1 + src/stores/settingsStore.ts | 25 +- tests/settingsDialogLayout.test.ts | 27 + tests/settingsStore.test.ts | 12 + 8 files changed, 560 insertions(+), 443 deletions(-) create mode 100644 tests/settingsDialogLayout.test.ts create mode 100644 tests/settingsStore.test.ts diff --git a/src/components/editor/EditorSettingsDialog.vue b/src/components/editor/EditorSettingsDialog.vue index 3f23375b9f..123ac072c6 100644 --- a/src/components/editor/EditorSettingsDialog.vue +++ b/src/components/editor/EditorSettingsDialog.vue @@ -11,7 +11,6 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useSettingsStore, EDITOR_THEMES, @@ -44,6 +43,8 @@ const editTheme = ref(settingsStore.editorSettings.theme); const editExecuteMode = ref(settingsStore.editorSettings.executeMode); const editWordWrap = ref(settingsStore.editorSettings.wordWrap); const editAppLayout = ref(settingsStore.editorSettings.appLayout); +const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize); +const redisScanPageSizeOptions = [200, 1000, 5000, 10000]; // Sync from store when dialog opens watch( @@ -56,6 +57,7 @@ watch( editExecuteMode.value = settingsStore.editorSettings.executeMode; editWordWrap.value = settingsStore.editorSettings.wordWrap; editAppLayout.value = settingsStore.editorSettings.appLayout; + editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize; } }, ); @@ -67,7 +69,8 @@ function hasChanges(): boolean { editTheme.value !== settingsStore.editorSettings.theme || editExecuteMode.value !== settingsStore.editorSettings.executeMode || editWordWrap.value !== settingsStore.editorSettings.wordWrap || - editAppLayout.value !== settingsStore.editorSettings.appLayout + editAppLayout.value !== settingsStore.editorSettings.appLayout || + editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ); } @@ -79,6 +82,7 @@ function applySettings() { executeMode: editExecuteMode.value, wordWrap: editWordWrap.value, appLayout: editAppLayout.value, + redisScanPageSize: editRedisScanPageSize.value, }); emit("update:open", false); } @@ -90,6 +94,7 @@ function resetDefaults() { editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap; editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; + editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize; } function onExecuteModeChange(v: any) { @@ -104,6 +109,11 @@ function onThemeChange(v: any) { if (typeof v === "string") editTheme.value = v as typeof DEFAULT_EDITOR_SETTINGS.theme; } +function onRedisScanPageSizeChange(v: any) { + const value = Number(v); + if (redisScanPageSizeOptions.includes(value)) editRedisScanPageSize.value = value; +} + function setAppLayout(value: "separated" | "classic") { editAppLayout.value = value; } @@ -111,6 +121,24 @@ function setAppLayout(value: "separated" | "classic") { const activeSettingsTab = ref("editor"); const isWeb = !isTauriRuntime(); const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : "")); +type SettingsCategory = "editor" | "appearance" | "redis" | "ai" | "security" | "about"; +const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [ + { value: "editor", label: t("settings.editorTab") }, + { value: "appearance", label: t("settings.appearanceTab") }, + { value: "redis", label: t("settings.redisTab") }, + { value: "ai", label: t("settings.aiTab") }, + ...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []), + { value: "about", label: t("settings.aboutTab") }, +]); + +function settingsCategoryButton(value: SettingsCategory): string { + return [ + "w-full rounded-md px-3 py-2 text-left text-sm transition-colors", + value === activeSettingsTab.value + ? "bg-primary text-primary-foreground shadow-sm" + : "text-muted-foreground hover:bg-muted hover:text-foreground", + ].join(" "); +} function openExternalUrl(url: string) { if (isTauriRuntime()) { @@ -363,7 +391,7 @@ watch( diff --git a/src/components/redis/RedisKeyBrowser.vue b/src/components/redis/RedisKeyBrowser.vue index 3d3a031a6c..9cf5c3a653 100644 --- a/src/components/redis/RedisKeyBrowser.vue +++ b/src/components/redis/RedisKeyBrowser.vue @@ -26,6 +26,7 @@ import RedisValueViewer from "./RedisValueViewer.vue"; import * as api from "@/lib/api"; import type { RedisKeyInfo } from "@/lib/api"; import { useConnectionStore } from "@/stores/connectionStore"; +import { useSettingsStore } from "@/stores/settingsStore"; import { buildRedisKeyTree, collectExpandedGroupIds, @@ -38,6 +39,7 @@ import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts"; const { t } = useI18n(); const connectionStore = useConnectionStore(); +const settingsStore = useSettingsStore(); const props = defineProps<{ connectionId: string; @@ -67,7 +69,6 @@ const commandResult = ref(null); const commandError = ref(""); const commandRunning = ref(false); -const PAGE_SIZE = 200; const keyGridStyle = { gridTemplateColumns: "minmax(12rem, 0.35fr) 80px 1fr 60px 60px", }; @@ -129,7 +130,7 @@ async function scanNextPage() { props.db, scanCursor.value, effectivePattern.value, - PAGE_SIZE, + settingsStore.editorSettings.redisScanPageSize, ); const existingKeys = new Set(flatKeys.value.map((key) => key.key_raw)); flatKeys.value = [...flatKeys.value, ...result.keys.filter((key) => !existingKeys.has(key.key_raw))]; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 6ad2d9175a..c37b5507ab 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1005,6 +1005,7 @@ export default { title: "Settings", editorTab: "Editor", appearanceTab: "Appearance", + redisTab: "Redis", aiTab: "AI", jdbcTab: "JDBC Drivers", securityTab: "Security", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index a6d77ed559..2b1e59f0fe 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -983,6 +983,8 @@ export default { title: "Configuración", editorTab: "Editor", appearanceTab: "Apariencia", + redisTab: "Redis", + aiTab: "IA", jdbcTab: "Drivers JDBC", securityTab: "Seguridad", aboutTab: "Acerca de", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index da4dced953..25e92b819c 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -987,6 +987,7 @@ export default { title: "设置", editorTab: "编辑器", appearanceTab: "外观", + redisTab: "Redis", aiTab: "AI", jdbcTab: "JDBC 驱动", securityTab: "安全", diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index c63cf624e1..f583d017ae 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -51,6 +51,7 @@ export interface EditorSettings { wordWrap: boolean; appLayout: "separated" | "classic"; pageSize: number; + redisScanPageSize: number; } export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [ @@ -83,26 +84,32 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { wordWrap: false, appLayout: "classic", pageSize: 100, + redisScanPageSize: 1000, }; export const STORAGE_KEY = "dbx-editor-settings"; const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size"; +export function normalizeEditorSettings(settings: Partial): EditorSettings { + return { + fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily, + fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize, + theme: settings.theme ?? DEFAULT_EDITOR_SETTINGS.theme, + executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode, + wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap, + appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout, + pageSize: settings.pageSize ?? DEFAULT_EDITOR_SETTINGS.pageSize, + redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize, + }; +} + function loadEditorSettings(): EditorSettings { // Try new format first try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw) as Partial; - return { - fontFamily: parsed.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily, - fontSize: parsed.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize, - theme: parsed.theme ?? DEFAULT_EDITOR_SETTINGS.theme, - executeMode: parsed.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode, - wordWrap: parsed.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap, - appLayout: parsed.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout, - pageSize: parsed.pageSize ?? DEFAULT_EDITOR_SETTINGS.pageSize, - }; + return normalizeEditorSettings(parsed); } } catch { /* ignore */ diff --git a/tests/settingsDialogLayout.test.ts b/tests/settingsDialogLayout.test.ts new file mode 100644 index 0000000000..26b57149e0 --- /dev/null +++ b/tests/settingsDialogLayout.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from "node:fs"; +import { strict as assert } from "node:assert"; +import test from "node:test"; + +const source = readFileSync("src/components/editor/EditorSettingsDialog.vue", "utf8"); + +test("settings dialog uses a side category navigation", () => { + assert.match(source, /settingsCategoryNav/); + assert.match(source, /settingsCategoryButton/); +}); + +test("Redis scan size lives in its own settings category", () => { + const redisTab = source.indexOf('value: "redis"'); + const redisContent = source.search(/activeSettingsTab === ['"]redis['"]/); + const redisScanSetting = source.indexOf('t("settings.redisScanPageSize")'); + const editorContent = source.search(/activeSettingsTab === ['"]editor['"]/); + + assert.ok(redisTab > -1); + assert.ok(redisContent > -1); + assert.ok(redisScanSetting > redisContent); + assert.ok(redisScanSetting > editorContent); +}); + +test("settings action footer stays at the bottom of the content pane", () => { + assert.match(source, /class="[^"]*min-h-full[^"]*flex-col[^"]*"/); + assert.match(source, / { + assert.equal(DEFAULT_EDITOR_SETTINGS.redisScanPageSize, 1000); + assert.equal(normalizeEditorSettings({}).redisScanPageSize, 1000); +}); + +test("keeps a saved Redis scan page size", () => { + assert.equal(normalizeEditorSettings({ redisScanPageSize: 5000 }).redisScanPageSize, 5000); +});