feat(sidebar): add configurable object metadata display

This commit is contained in:
zipg authored and GitHub committed 2026-07-23 00:23:19 +08:00
1 parent 8adf802444
commit 57f2d27313
29 files changed
+889 -143

No files matched your search

+19
View File
@@ -1,9 +1,24 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { enforceRightSidebarPanelExclusivity, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, normalizeDesktopSettings, normalizeEditorSettings, normalizeMcpGlobalPolicy, transitionRightSidebarPanels, type RightSidebarPanelState } from "@/stores/settingsStore";
import { createPinia, setActivePinia } from "pinia";
import { isProxy } from "vue";
import type { AiConfigItem } from "@/types/ai";
describe("normalizeEditorSettings", () => {
it("uses aligned comments by default and preserves legacy comment visibility", () => {
expect(normalizeEditorSettings({}).sidebarObjectInfoMode).toBe("comment-aligned");
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "comment-aligned" }).sidebarObjectInfoMode).toBe("comment-aligned");
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "comment-inline" }).sidebarObjectInfoMode).toBe("comment-inline");
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "size" }).sidebarObjectInfoMode).toBe("size");
expect(normalizeEditorSettings({ sidebarTableCommentLayout: "aligned" } as any).sidebarObjectInfoMode).toBe("comment-aligned");
expect(normalizeEditorSettings({ sidebarTableCommentLayout: "hidden" } as any).sidebarObjectInfoMode).toBe("hidden");
expect(normalizeEditorSettings({ sidebarHideTableComments: false } as any).sidebarObjectInfoMode).toBe("comment-aligned");
expect(normalizeEditorSettings({ sidebarHideTableComments: true } as any).sidebarObjectInfoMode).toBe("hidden");
expect(normalizeEditorSettings({ sidebarHideTableComments: true, sidebarShowDatabaseSizes: true } as any).sidebarObjectInfoMode).toBe("hidden");
expect(normalizeEditorSettings({ sidebarShowDatabaseSizes: true } as any).sidebarObjectInfoMode).toBe("size");
expect(normalizeEditorSettings({ sidebarObjectInfoMode: "invalid" } as any).sidebarObjectInfoMode).toBe("comment-aligned");
});
it("defaults SQL execution to the current statement and migrates legacy execute-all settings", () => {
expect(normalizeEditorSettings({}).executeMode).toBe("current");
expect(normalizeEditorSettings({ executeMode: "all" }).executeMode).toBe("current");
@@ -322,6 +337,10 @@ describe("settingsStore sidebar connection sort persistence", () => {
expect(store.editorSettings.sidebarConnectionSortMode).toBe("desc");
expect(saveEditorSettings).toHaveBeenCalledWith(expect.objectContaining({ sidebarConnectionSortMode: "desc" }));
expect(isProxy(saveEditorSettings.mock.calls[0][0])).toBe(false);
await store.persistEditorSettings();
expect(isProxy(saveEditorSettings.mock.calls[1][0])).toBe(false);
});
});
+109
View File
@@ -9,9 +9,11 @@ import type {
ConnectionConfig,
DatabaseType,
DatabaseConnectionInfo,
DatabaseStorageInfo,
CatalogInfo,
ForeignKeyInfo,
ObjectInfo,
ObjectStatistics,
SchemaInfo,
SidebarLayout,
TableInfo,
@@ -97,6 +99,7 @@ import { MetadataTaskLimiter } from "@/lib/metadata/metadataTaskLimiter";
import i18n from "@/i18n";
import type { MqAdminConfig } from "@/types/mq";
import { RABBITMQ_MQ_TENANT, resolveMqSystemKindFromConnection } from "@/lib/mq/mqConsoleDefaults";
import { applySidebarDatabaseStorage, applySidebarTableStorage, sidebarDatabaseNames, supportsSidebarDatabaseStorage, supportsSidebarTableStorage, type SidebarTableStorageScope } from "@/lib/sidebar/sidebarDatabaseStorage";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
@@ -108,6 +111,7 @@ const DISCONNECT_REQUEST_TIMEOUT_MS = 5_000;
const DEFAULT_KEEPALIVE_INTERVAL_SECS = 30;
const METADATA_LIST_PAGE_CACHE_TTL_MS = 30_000;
const METADATA_LIST_PAGE_CACHE_MAX_ENTRIES = 160;
const SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS = 30_000;
export const COMPLETION_METADATA_CONCURRENCY = 2;
const MONGO_LEGACY_DRIVER_PROFILE = "mongodb-legacy";
const MONGO_LEGACY_DRIVER_LABEL = "MongoDB (Legacy)";
@@ -257,6 +261,10 @@ export const useConnectionStore = defineStore("connection", () => {
else localStorage.removeItem(ACTIVE_CONNECTION_STORAGE_KEY);
});
const treeNodes = ref<TreeNode[]>([]);
const sidebarDatabaseStorageCache = new Map<string, { expiresAt: number; value: DatabaseStorageInfo[] }>();
const sidebarDatabaseStorageInFlight = new Map<string, Promise<DatabaseStorageInfo[]>>();
const sidebarTableStorageCache = new Map<string, { expiresAt: number; value: ObjectStatistics[] }>();
const sidebarTableStorageInFlight = new Map<string, Promise<ObjectStatistics[]>>();
const pinnedTreeNodeIds = ref<Set<string>>(new Set());
const connectedIds = ref<Set<string>>(new Set());
const identifierQuotes = ref<Record<string, string>>({});
@@ -2263,6 +2271,104 @@ export const useConnectionStore = defineStore("connection", () => {
beforeConnectHandler = handler;
}
function sidebarDatabaseStorageRequestKey(connectionId: string, databases: readonly string[]): string {
return `${connectionId}\0${[...databases].sort().join("\0")}`;
}
async function loadSidebarDatabaseStorage(connectionId: string, options?: { force?: boolean }): Promise<void> {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size" || !connectedIds.value.has(connectionId)) return;
if (!supportsSidebarDatabaseStorage(getConfig(connectionId))) return;
const connectionNode = findConnectionNode(connectionId);
const databases = sidebarDatabaseNames(connectionNode?.children);
if (!databases.length) return;
const requestKey = sidebarDatabaseStorageRequestKey(connectionId, databases);
const cached = sidebarDatabaseStorageCache.get(requestKey);
if (!options?.force && cached && cached.expiresAt > Date.now()) {
applySidebarDatabaseStorage(connectionNode?.children, cached.value);
return;
}
let request = sidebarDatabaseStorageInFlight.get(requestKey);
if (!request) {
request = api.listDatabaseStorage(connectionId, databases);
sidebarDatabaseStorageInFlight.set(requestKey, request);
}
try {
const storage = await request;
sidebarDatabaseStorageCache.set(requestKey, {
expiresAt: Date.now() + SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS,
value: storage,
});
const currentNode = findConnectionNode(connectionId);
const currentNames = sidebarDatabaseNames(currentNode?.children);
if (sidebarDatabaseStorageRequestKey(connectionId, currentNames) === requestKey) {
applySidebarDatabaseStorage(currentNode?.children, storage);
}
} catch (error) {
console.debug("[DBX][sidebar-database-storage:unavailable]", { connectionId, error });
} finally {
if (sidebarDatabaseStorageInFlight.get(requestKey) === request) {
sidebarDatabaseStorageInFlight.delete(requestKey);
}
}
}
function sidebarTableStorageRequestKey(scope: SidebarTableStorageScope): string {
return `${scope.connectionId}\0${scope.database}\0${scope.schema}`;
}
async function loadSidebarTableStorage(scope: SidebarTableStorageScope, options?: { force?: boolean }): Promise<void> {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size" || !connectedIds.value.has(scope.connectionId)) return;
if (!supportsSidebarTableStorage(getConfig(scope.connectionId))) return;
const requestKey = sidebarTableStorageRequestKey(scope);
const cached = sidebarTableStorageCache.get(requestKey);
if (!options?.force && cached && cached.expiresAt > Date.now()) {
applySidebarTableStorage(treeNodes.value, scope, cached.value);
return;
}
let request = sidebarTableStorageInFlight.get(requestKey);
if (!request) {
request = api.listObjectStatistics(scope.connectionId, scope.database, scope.schema);
sidebarTableStorageInFlight.set(requestKey, request);
}
try {
const statistics = await request;
sidebarTableStorageCache.set(requestKey, {
expiresAt: Date.now() + SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS,
value: statistics,
});
applySidebarTableStorage(treeNodes.value, scope, statistics);
} catch (error) {
console.debug("[DBX][sidebar-table-storage:unavailable]", { ...scope, error });
} finally {
if (sidebarTableStorageInFlight.get(requestKey) === request) {
sidebarTableStorageInFlight.delete(requestKey);
}
}
}
const sidebarDatabaseStorageScope = computed(() => {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size") return "";
return [...connectedIds.value]
.filter((connectionId) => supportsSidebarDatabaseStorage(getConfig(connectionId)))
.map((connectionId) => sidebarDatabaseStorageRequestKey(connectionId, sidebarDatabaseNames(findConnectionNode(connectionId)?.children)))
.sort()
.join("\n");
});
watch(
sidebarDatabaseStorageScope,
() => {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size") return;
for (const connectionId of connectedIds.value) {
void loadSidebarDatabaseStorage(connectionId);
}
},
{ flush: "post" },
);
async function loadDatabases(connectionId: string, options?: LoadTreeOptions) {
const configForScope = getConfig(connectionId);
return runTreeMetadataLoad(
@@ -2410,6 +2516,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
node.isExpanded = true;
if (options?.force) void loadSidebarDatabaseStorage(connectionId, { force: true });
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
@@ -5504,6 +5611,8 @@ export const useConnectionStore = defineStore("connection", () => {
setBeforeConnectHandler,
initFromDisk,
loadDatabases,
loadSidebarDatabaseStorage,
loadSidebarTableStorage,
loadRedisDatabases,
refreshRedisDbKeyCounts,
loadEtcdRoot,
+29 -5
View File
@@ -376,6 +376,8 @@ export interface CustomTheme {
export const DEFAULT_CUSTOM_THEMES: CustomTheme[] = [{ id: "default", name: "Custom", colors: { ...DEFAULT_CUSTOM_THEME_COLORS }, ddlColors: { ...DEFAULT_CUSTOM_THEME_DDL_COLORS } }];
export type SidebarObjectInfoMode = "comment-inline" | "comment-aligned" | "size" | "hidden";
export interface EditorSettings {
fontFamily: string;
fontSize: number;
@@ -438,7 +440,7 @@ export interface EditorSettings {
prefillNewQueryWithSelect: boolean;
updateNotificationsEnabled: boolean;
sidebarHiddenTablePrefixes: string[];
sidebarHideTableComments: boolean;
sidebarObjectInfoMode: SidebarObjectInfoMode;
sidebarAllowHorizontalScroll: boolean;
columnFormatters: Record<string, ColumnFormatterConfig>;
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
@@ -599,7 +601,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
prefillNewQueryWithSelect: true,
updateNotificationsEnabled: true,
sidebarHiddenTablePrefixes: [],
sidebarHideTableComments: false,
sidebarObjectInfoMode: "comment-aligned",
sidebarAllowHorizontalScroll: false,
columnFormatters: {},
customColumnFormatters: {},
@@ -711,6 +713,14 @@ function normalizeConnectionListSortMode(value: unknown): ConnectionListSortMode
return value === "asc" || value === "desc" ? value : "manual";
}
function normalizeSidebarObjectInfoMode(value: unknown, legacyCommentLayout?: unknown, legacyHideTableComments?: unknown, legacyShowDatabaseSizes?: unknown): SidebarObjectInfoMode {
if (value === "comment-inline" || value === "comment-aligned" || value === "size" || value === "hidden") return value;
if (legacyCommentLayout === "hidden" || legacyHideTableComments === true) return "hidden";
if (legacyShowDatabaseSizes === true) return "size";
if (legacyCommentLayout === "aligned") return "comment-aligned";
return DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
}
function normalizeColumnFormatters(value: unknown): Record<string, ColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const formatters: Record<string, ColumnFormatterConfig> = {};
@@ -861,7 +871,12 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
prefillNewQueryWithSelect: typeof settings.prefillNewQueryWithSelect === "boolean" ? settings.prefillNewQueryWithSelect : DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect,
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
sidebarHideTableComments: settings.sidebarHideTableComments ?? DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments,
sidebarObjectInfoMode: normalizeSidebarObjectInfoMode(
settings.sidebarObjectInfoMode,
(settings as Partial<EditorSettings> & { sidebarTableCommentLayout?: string }).sidebarTableCommentLayout,
(settings as Partial<EditorSettings> & { sidebarHideTableComments?: boolean }).sidebarHideTableComments,
(settings as Partial<EditorSettings> & { sidebarShowDatabaseSizes?: boolean }).sidebarShowDatabaseSizes,
),
sidebarAllowHorizontalScroll: settings.sidebarAllowHorizontalScroll ?? DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll,
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
@@ -909,8 +924,12 @@ function clearLegacyEditorSettings() {
safeLocalStorageRemove(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY);
}
function editorSettingsSnapshot(settings: EditorSettings): EditorSettings {
return JSON.parse(JSON.stringify(settings)) as EditorSettings;
}
function saveEditorSettings(settings: EditorSettings) {
void api.saveEditorSettings(settings).catch(() => {});
void api.saveEditorSettings(editorSettingsSnapshot(settings)).catch(() => {});
}
export interface SettingsNavigationRequest {
@@ -1221,7 +1240,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect;
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
if (partial.sidebarHideTableComments !== undefined) editorSettings.value.sidebarHideTableComments = partial.sidebarHideTableComments;
if (partial.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode);
if (partial.sidebarAllowHorizontalScroll !== undefined) editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll;
if (partial.columnFormatters !== undefined) editorSettings.value.columnFormatters = partial.columnFormatters;
if (partial.customColumnFormatters !== undefined) editorSettings.value.customColumnFormatters = partial.customColumnFormatters;
@@ -1243,6 +1262,10 @@ export const useSettingsStore = defineStore("settings", () => {
saveEditorSettings(editorSettings.value);
}
async function persistEditorSettings(): Promise<void> {
await api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value));
}
function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) {
const columnFormatters = { ...editorSettings.value.columnFormatters };
const normalized = normalizeColumnFormatter(formatter);
@@ -1300,6 +1323,7 @@ export const useSettingsStore = defineStore("settings", () => {
mcpGlobalPolicy,
initEditorSettings,
updateEditorSettings,
persistEditorSettings,
initDesktopSettings,
updateDesktopSettings,
initMcpGlobalPolicy,