feat(sidebar): add configurable object metadata display
This commit is contained in:
29 files changed
+889
-143
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user