feat(redis): limit the sidebar database list

This commit is contained in:
Melody Ruch
2026-09-18 17:33:28 +08:00
committed by GitHub
parent 745c201f06
commit ec356293aa
18 changed files with 265 additions and 22 deletions
@@ -178,6 +178,7 @@ import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
import { COLUMN_NAME_COPY_SEPARATOR_LABELS, COLUMN_NAME_COPY_SEPARATOR_OPTIONS, isColumnNameCopySeparator, type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
import { REDIS_DATABASE_DISPLAY_LIMIT_MIN, REDIS_DATABASE_DISPLAY_LIMIT_MAX, REDIS_DATABASE_DISPLAY_LIMIT_OPTIONS } from "@/lib/redis/redisDatabaseAlias";
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
import { currentStatementFrameLayer } from "@/lib/editor/codemirrorCurrentStatementFrameLayer";
import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers";
@@ -693,6 +694,7 @@ const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarH
const editSidebarCopyTableNameSeparator = ref<ColumnNameCopySeparator>(settingsStore.editorSettings.sidebarCopyTableNameSeparator);
const editSidebarCopyTableNameIncludeSchema = ref(settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema);
const editRedisKeyTemplates = ref(normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n"));
const editRedisDatabaseDisplayLimit = ref(settingsStore.editorSettings.redisDatabaseDisplayLimit);
const editSidebarObjectInfoMode = ref<SidebarObjectInfoMode>(settingsStore.editorSettings.sidebarObjectInfoMode);
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
const editSidebarShowTooltips = ref(settingsStore.editorSettings.sidebarShowTooltips);
@@ -929,6 +931,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
sidebarCopyTableNameSeparator: editSidebarCopyTableNameSeparator.value,
sidebarCopyTableNameIncludeSchema: editSidebarCopyTableNameIncludeSchema.value,
redisKeyTemplates: normalizeRedisKeyTemplates(editRedisKeyTemplates.value),
redisDatabaseDisplayLimit: editRedisDatabaseDisplayLimit.value,
exportBatchSize: editExportBatchSize.value,
csvQuoteMode: editCsvQuoteMode.value,
globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value,
@@ -1558,6 +1561,7 @@ function syncEditorSettingsDraftFromStore() {
editSidebarCopyTableNameSeparator.value = settingsStore.editorSettings.sidebarCopyTableNameSeparator;
editSidebarCopyTableNameIncludeSchema.value = settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema;
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n");
editRedisDatabaseDisplayLimit.value = settingsStore.editorSettings.redisDatabaseDisplayLimit;
editSidebarObjectInfoMode.value = settingsStore.editorSettings.sidebarObjectInfoMode;
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
editSidebarShowTooltips.value = settingsStore.editorSettings.sidebarShowTooltips;
@@ -1683,6 +1687,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
sidebarCopyTableNameSeparator: editSidebarCopyTableNameSeparator,
sidebarCopyTableNameIncludeSchema: editSidebarCopyTableNameIncludeSchema,
redisKeyTemplates: editRedisKeyTemplates,
redisDatabaseDisplayLimit: editRedisDatabaseDisplayLimit,
exportBatchSize: editExportBatchSize,
csvQuoteMode: editCsvQuoteMode,
exportRowLimitEnabled: editExportRowLimitEnabled,
@@ -1951,6 +1956,10 @@ async function persistSettings() {
settingsStore.updateEditorSettings(editorSettingsPatch);
await settingsStore.persistEditorSettings();
editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings);
// updateEditorSettings clamps out-of-range values; reflect the clamped
// result back into the input so an out-of-range draft doesn't keep
// reporting unsaved changes after a successful apply.
editRedisDatabaseDisplayLimit.value = settingsStore.editorSettings.redisDatabaseDisplayLimit;
}
if (pendingBackgroundImageCleanup) {
const cleanupPath = pendingBackgroundImageCleanup;
@@ -2142,6 +2151,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editDuckDbWorkerMaxProcesses.value = DEFAULT_DESKTOP_SETTINGS.duckdb_worker_max_processes;
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields);
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
editRedisDatabaseDisplayLimit.value = DEFAULT_EDITOR_SETTINGS.redisDatabaseDisplayLimit;
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
editCsvQuoteMode.value = DEFAULT_EDITOR_SETTINGS.csvQuoteMode;
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
@@ -2267,6 +2277,7 @@ function resetAllDefaults() {
editSidebarCopyTableNameSeparator.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameSeparator;
editSidebarCopyTableNameIncludeSchema.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameIncludeSchema;
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
editRedisDatabaseDisplayLimit.value = DEFAULT_EDITOR_SETTINGS.redisDatabaseDisplayLimit;
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
editCsvQuoteMode.value = DEFAULT_EDITOR_SETTINGS.csvQuoteMode;
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
@@ -7716,6 +7727,25 @@ onUnmounted(() => {
{{ t("settings.redisKeyTemplatesDescription") }}
</p>
</div>
<div class="space-y-2">
<Label for="redis-database-display-limit-input">{{ t("settings.redisDatabaseDisplayLimit") }}</Label>
<div class="flex items-center gap-3">
<Input
id="redis-database-display-limit-input"
type="number"
list="redis-database-display-limits"
:min="REDIS_DATABASE_DISPLAY_LIMIT_MIN"
:max="REDIS_DATABASE_DISPLAY_LIMIT_MAX"
step="10"
v-model.number="editRedisDatabaseDisplayLimit"
class="settings-export-number-input h-9 w-28 [&::-webkit-inner-spin-button]:appearance-none"
/>
<datalist id="redis-database-display-limits">
<option v-for="size in REDIS_DATABASE_DISPLAY_LIMIT_OPTIONS" :key="size" :value="size" />
</datalist>
<span class="text-xs text-muted-foreground">{{ t("settings.redisDatabaseDisplayLimitDescription") }}</span>
</div>
</div>
</div>
<Separator />
@@ -0,0 +1,25 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../EditorSettingsDialog.vue", import.meta.url), "utf8");
// Regression found while verifying issue #1236 (Redis sidebar database list
// limit): updateEditorSettings() silently clamps an out-of-range
// redisDatabaseDisplayLimit to [MIN, MAX], but persistSettings() only
// re-synced the dirty-check baseline (editEditorSettingsBase), not the input's
// own v-model ref. Typing a below-minimum value and clicking Apply left the
// draft (the unclamped value) permanently out of sync with the saved
// (clamped) value, so the dialog kept reporting unsaved changes forever after
// a successful apply.
describe("EditorSettingsDialog Redis database display limit clamp resync", () => {
it("reflects the clamped value back into the input after a successful apply", () => {
const block = dialogSource.slice(dialogSource.indexOf("async function persistSettings()"), dialogSource.indexOf("function applySettingsErrorToast"));
expect(block).toContain("editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings);");
expect(block).toContain("editRedisDatabaseDisplayLimit.value = settingsStore.editorSettings.redisDatabaseDisplayLimit;");
// The resync must happen after the clamp-and-persist step, inside the same
// "there is a pending editor-settings patch" branch, not unconditionally.
const clampResyncIndex = block.indexOf("editRedisDatabaseDisplayLimit.value = settingsStore.editorSettings.redisDatabaseDisplayLimit;");
const persistIndex = block.indexOf("await settingsStore.persistEditorSettings();");
expect(clampResyncIndex).toBeGreaterThan(persistIndex);
});
});
+3
View File
@@ -7608,6 +7608,9 @@ export default {
redisScanPageSize: "Redis scan count",
redisScanPageSizeDescription: "Keys requested per Redis SCAN page when browsing keys.",
redisScanPageSizeOption: "{count} keys",
redisDatabaseDisplayLimit: "Sidebar database list limit",
redisDatabaseDisplayLimitDescription: 'Maximum number of databases shown at once in the sidebar; the rest can be revealed with "Load more".',
redisDatabaseDisplayLimitOption: "{count} databases",
exportBatchSize: "Export batch size",
dateTimeSection: "Date and time",
globalDateTimeDisplayFormat: "Global display format",
+3
View File
@@ -7120,6 +7120,9 @@ export default withEnglishFallback({
redisScanPageSize: "Cantidad de escaneo Redis",
redisScanPageSizeDescription: "Claves solicitadas por página SCAN al explorar claves Redis.",
redisScanPageSizeOption: "{count} claves",
redisDatabaseDisplayLimit: "Límite de bases de datos en la barra lateral",
redisDatabaseDisplayLimitDescription: 'Número máximo de bases de datos que se muestran a la vez en la barra lateral; el resto se puede ver con "Cargar más".',
redisDatabaseDisplayLimitOption: "{count} bases de datos",
exportBatchSize: "Tamaño de lote de exportación",
exportBatchSizeDescription: "Filas obtenidas por lote al exportar datos de tabla (100-100000).",
exportRowLimitEnabled: "Limitar filas exportadas",
+3
View File
@@ -7120,6 +7120,9 @@ export default withEnglishFallback({
redisScanPageSize: "Conteggio scansione Redis",
redisScanPageSizeDescription: "Chiavi richieste per pagina SCAN di Redis durante la navigazione delle chiavi.",
redisScanPageSizeOption: "{count} chiavi",
redisDatabaseDisplayLimit: "Limite elenco database nella barra laterale",
redisDatabaseDisplayLimitDescription: 'Numero massimo di database mostrati contemporaneamente nella barra laterale; il resto può essere visualizzato con "Carica altro".',
redisDatabaseDisplayLimitOption: "{count} database",
exportBatchSize: "Dimensione del lotto di esportazione",
exportBatchSizeDescription: "Righe recuperate per lotto durante l'esportazione dei dati della tabella (100-100000).",
exportRowLimitEnabled: "Limita righe esportate",
+3
View File
@@ -7133,6 +7133,9 @@ export default withEnglishFallback({
redisScanPageSize: "Redisスキャンカウント",
redisScanPageSizeDescription: "キー参照時のRedis SCANページあたりの要求キー数。",
redisScanPageSizeOption: "{count}キー",
redisDatabaseDisplayLimit: "サイドバーのデータベース一覧の上限",
redisDatabaseDisplayLimitDescription: "サイドバーに一度に表示するデータベースの最大数。残りは「もっと読み込む」で表示できます。",
redisDatabaseDisplayLimitOption: "{count}個のデータベース",
exportBatchSize: "エクスポートバッチサイズ",
exportBatchSizeDescription: "テーブルデータエクスポート時のバッチあたりの取得行数(100〜100000)。",
exportSection: "エクスポート",
+3
View File
@@ -6886,6 +6886,9 @@ export default withEnglishFallback({
redisScanPageSize: "Redis 스캔 개수",
redisScanPageSizeDescription: "키를 탐색할 때 Redis SCAN 페이지당 요청하는 키 수입니다.",
redisScanPageSizeOption: "키 {count}개",
redisDatabaseDisplayLimit: "사이드바 데이터베이스 목록 제한",
redisDatabaseDisplayLimitDescription: '사이드바에 한 번에 표시할 최대 데이터베이스 수입니다. 나머지는 "더 불러오기"로 볼 수 있습니다.',
redisDatabaseDisplayLimitOption: "데이터베이스 {count}개",
exportBatchSize: "내보내기 배치 크기",
dateTimeSection: "날짜 및 시간",
globalDateTimeDisplayFormat: "전역 표시 형식",
+3
View File
@@ -7122,6 +7122,9 @@ export default withEnglishFallback({
redisScanPageSize: "Contagem de scan do Redis",
redisScanPageSizeDescription: "Chaves solicitadas por página do Redis SCAN ao navegar pelas chaves.",
redisScanPageSizeOption: "{count} chaves",
redisDatabaseDisplayLimit: "Limite da lista de bancos de dados na barra lateral",
redisDatabaseDisplayLimitDescription: 'Número máximo de bancos de dados exibidos de uma vez na barra lateral; o restante pode ser visto com "Carregar mais".',
redisDatabaseDisplayLimitOption: "{count} bancos de dados",
exportBatchSize: "Tamanho do lote de exportação",
exportBatchSizeDescription: "Linhas buscadas por lote ao exportar dados da tabela (100-100000).",
exportRowLimitEnabled: "Limitar linhas exportadas",
+3
View File
@@ -7263,6 +7263,9 @@ export default withEnglishFallback({
redisScanPageSize: "Redis tarama sayısı",
redisScanPageSizeDescription: "Anahtarlara gözatılırken her Redis SCAN sayfasında istenen anahtar sayısı.",
redisScanPageSizeOption: "{count} anahtar",
redisDatabaseDisplayLimit: "Kenar çubuğu veritabanı listesi sınırı",
redisDatabaseDisplayLimitDescription: 'Kenar çubuğunda bir seferde gösterilecek maksimum veritabanı sayısı; kalanlar "Daha fazla yükle" ile görüntülenebilir.',
redisDatabaseDisplayLimitOption: "{count} veritabanı",
exportBatchSize: "Dışa aktarma toplu iş boyutu",
dateTimeSection: "Tarih ve saat",
globalDateTimeDisplayFormat: "Genel görüntüleme biçimi",
+3
View File
@@ -7580,6 +7580,9 @@ export default withEnglishFallback({
redisScanPageSize: "Redis 扫描数量",
redisScanPageSizeDescription: "浏览 Redis Key 时每次 SCAN 请求的 Key 数量。",
redisScanPageSizeOption: "{count} 个 Key",
redisDatabaseDisplayLimit: "侧边栏数据库列表上限",
redisDatabaseDisplayLimitDescription: "侧边栏一次最多展示的数据库数量,超出部分可通过“加载更多”查看。",
redisDatabaseDisplayLimitOption: "{count} 个数据库",
exportBatchSize: "导出批次大小",
dateTimeSection: "日期与时间",
globalDateTimeDisplayFormat: "全局显示格式",
+3
View File
@@ -6445,6 +6445,9 @@ export default withEnglishFallback({
redisScanPageSize: "Redis 掃描數量",
redisScanPageSizeDescription: "瀏覽 Redis Key 時每次 SCAN 請求的 Key 數量。",
redisScanPageSizeOption: "{count} 個 Key",
redisDatabaseDisplayLimit: "側邊欄資料庫清單上限",
redisDatabaseDisplayLimitDescription: "側邊欄一次最多顯示的資料庫數量,超出部分可透過「載入更多」查看。",
redisDatabaseDisplayLimitOption: "{count} 個資料庫",
shortcutExecuteSql: "執行 SQL",
shortcutExecuteSqlInNewResultTab: "在新結果分頁中執行 SQL",
shortcutFind: "尋找",
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias";
import { limitRedisDatabaseList, normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel, REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT } from "@/lib/redis/redisDatabaseAlias";
describe("redisDatabaseAlias", () => {
it("normalizes numeric database keys and trims aliases", () => {
@@ -32,4 +32,22 @@ describe("redisDatabaseAlias", () => {
expect(redisDatabaseLabel(0, undefined, 0)).toBe("db0 (0)");
expect(redisDatabaseLabel(2, { "2": "cache" })).toBe("db2 · cache");
});
it("leaves the list untouched when it fits within the limit", () => {
const items = [1, 2, 3];
expect(limitRedisDatabaseList(items, 10)).toEqual({ visible: items, hasMore: false });
});
it("truncates and reports more items are available", () => {
const items = [1, 2, 3, 4, 5];
expect(limitRedisDatabaseList(items, 2)).toEqual({ visible: [1, 2], hasMore: true });
});
it("falls back to the default limit for missing or invalid values (#1236)", () => {
const items = Array.from({ length: REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT + 1 }, (_, i) => i);
expect(limitRedisDatabaseList(items, undefined).hasMore).toBe(true);
expect(limitRedisDatabaseList(items, 0).hasMore).toBe(true);
expect(limitRedisDatabaseList(items, -5).hasMore).toBe(true);
expect(limitRedisDatabaseList(items.slice(0, REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT), undefined).hasMore).toBe(false);
});
});
@@ -28,3 +28,18 @@ export function redisDatabaseLabel(database: string | number, aliases?: RedisDat
const name = alias ? `db${key} · ${alias}` : `db${key}`;
return totalKeyCount == null ? name : `${name} (${totalKeyCount})`;
}
// Default kept large enough that connections with a typical (or even a few
// hundred) configured databases render exactly as before #1236 — only a
// connection with an unusually large `databases` count hits the cap and gets
// a "load more" node instead of a wall of sidebar rows.
export const REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT = 1000;
export const REDIS_DATABASE_DISPLAY_LIMIT_MIN = 10;
export const REDIS_DATABASE_DISPLAY_LIMIT_MAX = 100_000;
export const REDIS_DATABASE_DISPLAY_LIMIT_OPTIONS = [50, 100, 200, 500, 1000, 2000, 5000] as const;
export function limitRedisDatabaseList<T>(items: T[], limit: number | undefined): { visible: T[]; hasMore: boolean } {
const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT;
if (items.length <= effectiveLimit) return { visible: items, hasMore: false };
return { visible: items.slice(0, effectiveLimit), hasMore: true };
}
@@ -101,6 +101,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"sidebarCopyTableNameSeparator",
"sidebarCopyTableNameIncludeSchema",
"redisKeyTemplates",
"redisDatabaseDisplayLimit",
"exportBatchSize",
"csvQuoteMode",
"exportRowLimitEnabled",
@@ -164,6 +164,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"autoCalculateTotalRows",
"tableColumnTemplateFields",
"redisKeyTemplates",
"redisDatabaseDisplayLimit",
"exportBatchSize",
"csvQuoteMode",
"exportRowLimitEnabled",
@@ -0,0 +1,106 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ConnectionConfig } from "@/types/database";
function installLocalStorage() {
const data = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => data.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
removeItem: vi.fn((key: string) => data.delete(key)),
});
}
function redisConnection(): ConnectionConfig {
return {
id: "redis-1",
name: "Redis",
db_type: "redis",
host: "127.0.0.1",
port: 6379,
username: "",
password: "",
database: "0",
};
}
function manyRedisDatabases(count: number) {
return Array.from({ length: count }, (_, db) => ({ db, keys: db === 0 ? 5 : 0 }));
}
describe("connectionStore Redis database display limit (#1236)", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
installLocalStorage();
setActivePinia(createPinia());
});
it("caps the sidebar list at the configured limit and appends a load-more node", async () => {
const redisListDatabases = vi.fn().mockResolvedValue(manyRedisDatabases(30));
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
redisListDatabases,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
useSettingsStore().updateEditorSettings({ redisDatabaseDisplayLimit: 10 });
const store = useConnectionStore();
const connection = redisConnection();
store.connections = [connection];
store.connectedIds.add(connection.id);
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [] }];
await store.loadRedisDatabases(connection.id);
const children = store.treeNodes[0].children ?? [];
expect(children.filter((node) => node.type === "redis-db")).toHaveLength(10);
const loadMore = children.find((node) => node.type === "load-more");
expect(loadMore).toBeDefined();
expect(loadMore?.loadMore).toEqual({ parentId: connection.id, offset: 10, pageSize: 20 });
await store.loadMoreObjectGroupChildren(loadMore!);
const expandedChildren = store.treeNodes[0].children ?? [];
expect(expandedChildren.filter((node) => node.type === "redis-db")).toHaveLength(30);
expect(expandedChildren.some((node) => node.type === "load-more")).toBe(false);
expect(redisListDatabases).toHaveBeenCalledTimes(2);
});
it("does not truncate when the database count is within the configured limit", async () => {
const redisListDatabases = vi.fn().mockResolvedValue(manyRedisDatabases(16));
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
redisListDatabases,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
// Default limit (1000) — the historical "show everything" behavior must be unchanged.
expect(useSettingsStore().editorSettings.redisDatabaseDisplayLimit).toBe(1000);
const store = useConnectionStore();
const connection = redisConnection();
store.connections = [connection];
store.connectedIds.add(connection.id);
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [] }];
await store.loadRedisDatabases(connection.id);
const children = store.treeNodes[0].children ?? [];
expect(children.filter((node) => node.type === "redis-db")).toHaveLength(16);
expect(children.some((node) => node.type === "load-more")).toBe(false);
});
});
+32 -21
View File
@@ -145,7 +145,7 @@ import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/metad
import { kvRootNodeLabel } from "@/lib/kv/kvRootPresentation";
import { etcdPermissionsAllowKey } from "@/lib/etcd/keyPermissions";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias";
import { limitRedisDatabaseList, normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
import { appendAgentDriverUpdateHint, connectionUsesSsh, hasAgentDriverUpdate, hasInstalledAgentVersion, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
import { appendConnectionErrorHints, isMysqlMissingPasswordFailure, isSqliteMissingEncryptionPasswordFailure } from "@/lib/connection/connectionErrorHints";
@@ -4841,7 +4841,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadRedisDatabases(connectionId: string) {
async function loadRedisDatabases(connectionId: string, options?: { showAll?: boolean }) {
const node = findConnectionNode(connectionId);
return runConnectionTreeMetadataLoad(connectionId, node, async (load) => {
const dbs = await withMetadataLoadTimeout(connectionId, api.redisListDatabases(connectionId), "Redis databases");
@@ -4857,26 +4857,25 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
dbs.map((db) => String(db.db)),
);
setChildren(
targetNode,
withSavedSqlRoot(
const databaseNodes: TreeNode[] = dbs
.filter((db) => visibleNameSet.has(String(db.db)))
.map((db) => ({
id: `${connectionId}:db${db.db}`,
label: redisDatabaseLabel(db.db, config?.redis_database_aliases, db.keys),
type: "redis-db" as const,
connectionId,
dbs
.filter((db) => visibleNameSet.has(String(db.db)))
.map((db) => ({
id: `${connectionId}:db${db.db}`,
label: redisDatabaseLabel(db.db, config?.redis_database_aliases, db.keys),
type: "redis-db" as const,
connectionId,
database: String(db.db),
loadedKeyCount: 0,
totalKeyCount: db.keys,
isExpanded: false,
children: [],
})),
targetNode,
),
);
database: String(db.db),
loadedKeyCount: 0,
totalKeyCount: db.keys,
isExpanded: false,
children: [],
}));
// #1236: a `databases` count in the low hundreds is rare but not unheard
// of, and dumping every one of them into the sidebar at once is the
// reported pain point. Cap the initial render and let a "load more" node
// reveal the rest on demand, unless the user explicitly asked to see all.
const { visible, hasMore } = options?.showAll ? { visible: databaseNodes, hasMore: false } : limitRedisDatabaseList(databaseNodes, useSettingsStore().editorSettings.redisDatabaseDisplayLimit);
setChildren(targetNode, withSavedSqlRoot(connectionId, hasMore ? [...visible, buildLoadMoreNode(targetNode, visible.length, databaseNodes.length - visible.length)] : visible, targetNode));
targetNode.isExpanded = true;
});
}
@@ -6127,6 +6126,18 @@ export const useConnectionStore = defineStore("connection", () => {
if (node.type !== "load-more" || !node.loadMore) return;
const loadMore = node.loadMore;
const parent = findNode(treeNodes.value, node.loadMore.parentId);
if (parent?.type === "connection" && parent.connectionId && getConfig(parent.connectionId)?.db_type === "redis") {
// The full database list is already fetched in one cheap call, so "load
// more" here just re-renders it without the display-limit truncation
// instead of paging through another backend round trip.
node.isLoading = true;
try {
await loadRedisDatabases(parent.connectionId, { showAll: true });
} finally {
node.isLoading = false;
}
return;
}
if (!parent?.connectionId || !hasTreeNodeDatabaseContext(parent)) return;
const parentConnectionId = parent.connectionId;
const configForScope = getConfig(parentConnectionId);
+9
View File
@@ -19,6 +19,7 @@ import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTar
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
import { type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
import { REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT, REDIS_DATABASE_DISPLAY_LIMIT_MIN, REDIS_DATABASE_DISPLAY_LIMIT_MAX } from "@/lib/redis/redisDatabaseAlias";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { normalizeSidebarCopyTableNameSeparator } from "@/lib/sidebar/sidebarTableNameCopy";
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
@@ -889,6 +890,8 @@ export interface EditorSettings {
csvQuoteMode: CsvQuoteMode;
/** Global Redis key-search templates; overridden by non-empty connection templates. */
redisKeyTemplates: string[];
/** Sidebar database-list cap for Redis connections; the rest are revealed via "load more". */
redisDatabaseDisplayLimit: number;
exportRowLimitEnabled: boolean;
exportRowLimit: number;
queryExportKeysetOptimizationEnabled: boolean;
@@ -1135,6 +1138,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
exportBatchSize: 2000,
csvQuoteMode: DEFAULT_CSV_QUOTE_MODE,
redisKeyTemplates: [],
redisDatabaseDisplayLimit: REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT,
exportRowLimitEnabled: false,
exportRowLimit: 100000,
queryExportKeysetOptimizationEnabled: true,
@@ -1669,6 +1673,10 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
exportBatchSize: typeof settings.exportBatchSize === "number" && settings.exportBatchSize >= 100 && settings.exportBatchSize <= 100000 ? Math.round(settings.exportBatchSize) : DEFAULT_EDITOR_SETTINGS.exportBatchSize,
csvQuoteMode: normalizeCsvQuoteMode(settings.csvQuoteMode),
redisKeyTemplates: normalizeRedisKeyTemplates(settings.redisKeyTemplates),
redisDatabaseDisplayLimit:
typeof settings.redisDatabaseDisplayLimit === "number" && settings.redisDatabaseDisplayLimit >= REDIS_DATABASE_DISPLAY_LIMIT_MIN && settings.redisDatabaseDisplayLimit <= REDIS_DATABASE_DISPLAY_LIMIT_MAX
? Math.round(settings.redisDatabaseDisplayLimit)
: DEFAULT_EDITOR_SETTINGS.redisDatabaseDisplayLimit,
exportRowLimitEnabled: typeof settings.exportRowLimitEnabled === "boolean" ? settings.exportRowLimitEnabled : DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled,
exportRowLimit: typeof settings.exportRowLimit === "number" && settings.exportRowLimit >= 100 && settings.exportRowLimit <= 2147483647 ? Math.round(settings.exportRowLimit) : DEFAULT_EDITOR_SETTINGS.exportRowLimit,
queryExportKeysetOptimizationEnabled: typeof settings.queryExportKeysetOptimizationEnabled === "boolean" ? settings.queryExportKeysetOptimizationEnabled : DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled,
@@ -2398,6 +2406,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.exportBatchSize !== undefined) editorSettings.value.exportBatchSize = Math.min(100000, Math.max(100, Math.round(partial.exportBatchSize)));
if (partial.csvQuoteMode !== undefined) editorSettings.value.csvQuoteMode = normalizeCsvQuoteMode(partial.csvQuoteMode);
if (partial.redisKeyTemplates !== undefined) editorSettings.value.redisKeyTemplates = normalizeRedisKeyTemplates(partial.redisKeyTemplates);
if (partial.redisDatabaseDisplayLimit !== undefined) editorSettings.value.redisDatabaseDisplayLimit = Math.min(REDIS_DATABASE_DISPLAY_LIMIT_MAX, Math.max(REDIS_DATABASE_DISPLAY_LIMIT_MIN, Math.round(partial.redisDatabaseDisplayLimit)));
if (partial.exportRowLimitEnabled !== undefined) editorSettings.value.exportRowLimitEnabled = partial.exportRowLimitEnabled;
if (partial.exportRowLimit !== undefined) editorSettings.value.exportRowLimit = Math.min(2147483647, Math.max(100, Math.round(partial.exportRowLimit)));
if (partial.queryExportKeysetOptimizationEnabled !== undefined) editorSettings.value.queryExportKeysetOptimizationEnabled = partial.queryExportKeysetOptimizationEnabled;