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

@@ -39,6 +39,7 @@ import {
type InterfaceLayout,
type DisconnectTabHandlingMode,
type OpenTabsRestoreMode,
type SidebarObjectInfoMode,
type SqlSemanticDiagnosticsMode,
type UpdateDownloadSource,
type CustomThemeColors,
@@ -356,7 +357,7 @@ const editReuseDataTab = ref(settingsStore.editorSettings.reuseDataTab);
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
const editSidebarHideTableComments = ref(settingsStore.editorSettings.sidebarHideTableComments);
const editSidebarObjectInfoMode = ref<SidebarObjectInfoMode>(settingsStore.editorSettings.sidebarObjectInfoMode);
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
const editExportBatchSize = ref(settingsStore.editorSettings.exportBatchSize);
const editGlobalDateTimeDisplayFormat = ref(settingsStore.editorSettings.globalDateTimeDisplayFormat);
@@ -456,7 +457,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
reuseDataTab: editReuseDataTab.value,
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
sidebarHideTableComments: editSidebarHideTableComments.value,
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
exportBatchSize: editExportBatchSize.value,
@@ -709,7 +710,7 @@ function syncEditorSettingsDraftFromStore() {
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
editSidebarHideTableComments.value = settingsStore.editorSettings.sidebarHideTableComments;
editSidebarObjectInfoMode.value = settingsStore.editorSettings.sidebarObjectInfoMode;
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
editExportBatchSize.value = settingsStore.editorSettings.exportBatchSize;
editGlobalDateTimeDisplayFormat.value = settingsStore.editorSettings.globalDateTimeDisplayFormat;
@@ -817,6 +818,7 @@ async function persistSettings() {
const sidebarTablePageSizeChanged = editSidebarTablePageSize.value !== (settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE);
if (Object.keys(editorSettingsPatch).length > 0) {
settingsStore.updateEditorSettings(editorSettingsPatch);
await settingsStore.persistEditorSettings();
editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings);
}
await settingsStore.updateDesktopSettings({
@@ -907,7 +909,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editReuseDataTab.value = DEFAULT_EDITOR_SETTINGS.reuseDataTab;
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
editSidebarHideTableComments.value = DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments;
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
@@ -992,7 +994,7 @@ function resetAllDefaults() {
editReuseDataTab.value = DEFAULT_EDITOR_SETTINGS.reuseDataTab;
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
editSidebarHideTableComments.value = DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments;
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
@@ -4002,14 +4004,24 @@ onUnmounted(cleanupPreviewEditor);
{{ t(`settings.${disconnectTabHandlingModeDescriptionKey}`) }}
</p>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-2 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="sidebar-hide-table-comments">{{ t("settings.sidebarHideTableComments") }}</Label>
<HelpTooltip :label="t('settings.sidebarHideTableComments')">
{{ t("settings.sidebarHideTableCommentsDescription") }}
<Label for="sidebar-object-info-mode">{{ t("settings.sidebarObjectInfoMode") }}</Label>
<HelpTooltip :label="t('settings.sidebarObjectInfoMode')">
{{ t("settings.sidebarObjectInfoModeDescription") }}
</HelpTooltip>
</div>
<Switch id="sidebar-hide-table-comments" v-model="editSidebarHideTableComments" />
<Select :model-value="editSidebarObjectInfoMode" @update:model-value="(value) => (editSidebarObjectInfoMode = value as SidebarObjectInfoMode)">
<SelectTrigger id="sidebar-object-info-mode" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="comment-aligned">{{ t("settings.sidebarObjectInfoModeCommentAligned") }}</SelectItem>
<SelectItem value="comment-inline">{{ t("settings.sidebarObjectInfoModeCommentInline") }}</SelectItem>
<SelectItem value="size">{{ t("settings.sidebarObjectInfoModeSize") }}</SelectItem>
<SelectItem value="hidden">{{ t("settings.sidebarObjectInfoModeHidden") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component, type ComponentPublicInstance, type CSSProperties } from "vue";
import { ref, shallowRef, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component, type ComponentPublicInstance, type CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { Search, X, ListFilter, ListOrdered, ArrowDownAZ, ArrowUpZA, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
@@ -38,6 +38,9 @@ import type { SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDial
import { resetSidebarTreeDialogState } from "./sidebarTreeDialogState";
import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs";
import { sortConnectionListForDisplay } from "@/lib/sidebar/connectionListSort";
import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
import { alignedSidebarCommentLabelWidths, isSidebarCommentAlignableNode, sidebarTreeNodeComment } from "@/lib/sidebar/sidebarTreeItemLayout";
import { sidebarTableStorageScopes, supportsSidebarTableStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
const { t } = useI18n();
const store = useConnectionStore();
@@ -357,6 +360,64 @@ const flatNodes = computed<FlatTreeNode[]>(() =>
activeQueries: store.sidebarTableSearchQueries,
}),
);
const sidebarCommentLabelWidths = shallowRef(new Map<string, number>());
let sidebarCommentMeasureFrame = 0;
const sidebarTableNameDisplayTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "mongo-collection", "vector-collection", "elasticsearch-index"]);
function sidebarCommentLabel(node: TreeNode): string {
const label = sidebarTableNameDisplayTypes.has(node.type) ? sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes) : node.label;
return node.valid === false ? `${label} · INVALID` : label;
}
function measureSidebarCommentLabelWidths() {
sidebarCommentMeasureFrame = 0;
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "comment-aligned" || typeof document === "undefined" || !rootRef.value) {
sidebarCommentLabelWidths.value = new Map();
return;
}
const context = document.createElement("canvas").getContext("2d");
if (!context) return;
const style = window.getComputedStyle(rootRef.value);
context.font = style.font || `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
sidebarCommentLabelWidths.value = alignedSidebarCommentLabelWidths(
flatNodes.value.map(({ id, depth, node }) => ({
id,
depth,
alignable: isSidebarCommentAlignableNode(node),
hasComment: !!sidebarTreeNodeComment(node),
labelWidth: context.measureText(sidebarCommentLabel(node)).width,
})),
);
}
function scheduleSidebarCommentLabelMeasure() {
if (typeof window === "undefined") {
measureSidebarCommentLabelWidths();
return;
}
if (sidebarCommentMeasureFrame) window.cancelAnimationFrame(sidebarCommentMeasureFrame);
sidebarCommentMeasureFrame = window.requestAnimationFrame(measureSidebarCommentLabelWidths);
}
watch([flatNodes, () => settingsStore.editorSettings.sidebarObjectInfoMode, () => settingsStore.editorSettings.sidebarHiddenTablePrefixes, () => settingsStore.editorSettings.uiFontFamily, () => settingsStore.editorSettings.uiScale], scheduleSidebarCommentLabelMeasure, {
flush: "post",
immediate: true,
});
const visibleSidebarTableStorageScopes = computed(() => {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size") return [];
return sidebarTableStorageScopes(flatNodes.value.map(({ node }) => node)).filter((scope) => supportsSidebarTableStorage(store.getConfig(scope.connectionId)));
});
watch(
visibleSidebarTableStorageScopes,
(scopes) => {
for (const scope of scopes) void store.loadSidebarTableStorage(scope);
},
{ flush: "post", immediate: true },
);
// Build all lookup tables in one linear pass whenever the visible tree changes.
// Selection, scrolling and sticky headers then avoid repeated full-array scans.
const flatTreeIndex = computed(() =>
@@ -1306,6 +1367,7 @@ onUnmounted(() => {
sidebarScrollbarResizeObserver?.disconnect();
window.cancelAnimationFrame(sidebarScrollbarAnimationFrame);
window.clearTimeout(sidebarScrollingTimer);
if (sidebarCommentMeasureFrame) window.cancelAnimationFrame(sidebarCommentMeasureFrame);
});
defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
@@ -1413,6 +1475,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
:drag-disabled="isFiltering || isConnectionListAlphabeticallySorted"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.node.id"
:comment-label-width="sidebarCommentLabelWidths.get(item.node.id)"
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
@rename-started="pendingRenameGroupId = null"
@group-created="startRenamingCreatedGroup"
@@ -1420,7 +1483,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
</template>
</RecycleScroller>
<div v-if="stickyNode" class="sticky-database-header pointer-events-auto absolute inset-x-0 top-0 z-[5] border-b border-border/60" :style="stickyHeaderStyle">
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" @context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)" />
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" :comment-label-width="sidebarCommentLabelWidths.get(stickyNode.node.id)" @context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)" />
</div>
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
@@ -1436,6 +1499,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
:drag-disabled="isFiltering || isConnectionListAlphabeticallySorted"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.id"
:comment-label-width="sidebarCommentLabelWidths.get(item.node.id)"
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
@rename-started="pendingRenameGroupId = null"
@group-created="startRenamingCreatedGroup"
@@ -44,8 +44,9 @@ import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import LightTooltip from "@/components/ui/LightTooltip.vue";
import type { ColumnInfo, ConnectionConfig, DatabaseType, TreeNode, TreeNodeType } from "@/types/database";
import { canTreeNodeShowExpander, trailingCommentAvailableWidth, trailingCommentGapPx, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
import { canTreeNodeShowExpander, sidebarTreeNodeComment, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
import { clearActiveTableReferencePayload, createTableReferencePayload, createTableReferenceDropEvent, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
import { formatSidebarObjectStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
import { dataTabOpenModeFromTreeClick } from "@/lib/sidebar/dataTabOpenPolicy";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { hexToRgba } from "@/lib/common/color";
@@ -68,22 +69,12 @@ const labelRef = ref<HTMLElement>();
const rowRef = ref<HTMLElement>();
const trailingCommentLayoutRef = ref<HTMLElement>();
const trailingCommentLeadingRef = ref<HTMLElement>();
const trailingCommentMaxWidth = ref(0);
const labelOverflowing = ref(false);
let labelResizeObserver: ResizeObserver | null = null;
let trailingCommentResizeObserver: ResizeObserver | null = null;
let labelMeasureFrame = 0;
let trailingCommentMeasureFrame = 0;
function cancelLabelOverflowMeasure() {
if (!labelMeasureFrame) return;
window.cancelAnimationFrame(labelMeasureFrame);
@@ -150,6 +141,7 @@ const props = defineProps<{
dragDisabled?: boolean;
pendingRename?: boolean;
highlighted?: boolean;
commentLabelWidth?: number;
}>();
const emit = defineEmits<{
@@ -549,71 +541,26 @@ const isNodeDefaultDatabase = computed(
);
const trailingComment = computed(() => {
if (settingsStore.editorSettings.sidebarHideTableComments) return null;
if (activeNode.value.type === "column" && activeNode.value.meta && "comment" in activeNode.value.meta) return (activeNode.value.meta as any).comment || null;
if ((activeNode.value.type === "schema" || activeNode.value.type === "table" || activeNode.value.type === "view" || activeNode.value.type === "mongo-collection" || activeNode.value.type === "vector-collection" || activeNode.value.type === "elasticsearch-index") && activeNode.value.comment) {
return activeNode.value.comment;
}
return null;
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "comment-inline" && settingsStore.editorSettings.sidebarObjectInfoMode !== "comment-aligned") return null;
return sidebarTreeNodeComment(activeNode.value);
});
function cancelTrailingCommentMeasure() {
if (!trailingCommentMeasureFrame) return;
window.cancelAnimationFrame(trailingCommentMeasureFrame);
trailingCommentMeasureFrame = 0;
function formattedObjectStorage(): string {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size" || (activeNode.value.type !== "database" && activeNode.value.type !== "table" && activeNode.value.type !== "materialized_view")) return "";
return formatSidebarObjectStorage(activeNode.value.sizeBytes);
}
function measureTrailingCommentLayout() {
const container = trailingCommentLayoutRef.value;
const leading = trailingCommentLeadingRef.value;
if (!trailingComment.value || !container || !leading) {
trailingCommentMaxWidth.value = 0;
return;
}
const alignedCommentLabelWidth = computed(() => (settingsStore.editorSettings.sidebarObjectInfoMode === "comment-aligned" ? props.commentLabelWidth : undefined));
// The leading group keeps the complete table name ahead of the comment.
// Only the width remaining after that name and the fixed gap may be used
// by the comment; once it reaches zero, the comment is hidden.
trailingCommentMaxWidth.value = trailingCommentAvailableWidth(container.clientWidth, leading.scrollWidth);
function hasTrailingMetadata(): boolean {
return !!trailingComment.value || !!formattedObjectStorage();
}
function scheduleTrailingCommentMeasure() {
if (typeof window === "undefined") {
measureTrailingCommentLayout();
return;
}
cancelTrailingCommentMeasure();
trailingCommentMeasureFrame = window.requestAnimationFrame(() => {
trailingCommentMeasureFrame = 0;
measureTrailingCommentLayout();
});
}
function refreshTrailingCommentMeasurement() {
trailingCommentResizeObserver?.disconnect();
trailingCommentResizeObserver = null;
if (!trailingComment.value || !trailingCommentLayoutRef.value || !trailingCommentLeadingRef.value) {
trailingCommentMaxWidth.value = 0;
return;
}
scheduleTrailingCommentMeasure();
if (typeof ResizeObserver !== "undefined") {
trailingCommentResizeObserver = new ResizeObserver(scheduleTrailingCommentMeasure);
trailingCommentResizeObserver.observe(trailingCommentLayoutRef.value);
}
}
// Keep comment rows constrained to the sidebar. When space is tight, the
// comment truncates before the table name instead of creating a large gap.
const usesFullWidthLabel = computed(() => usesFullWidthTreeLabel(activeNode.value.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll, !!trailingComment.value));
const usesFullWidthLabel = computed(() => usesFullWidthTreeLabel(activeNode.value.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll, hasTrailingMetadata()));
const rowWidthClass = computed(() => (usesFullWidthLabel.value ? "w-max min-w-full" : "w-full min-w-0"));
const labelWidthClass = computed(() => treeLabelWidthClass({ fullWidth: usesFullWidthLabel.value, hasTrailingComment: !!trailingComment.value }));
watch(() => [trailingComment.value, visibleLabel(activeNode.value), trailingCommentLayoutRef.value, trailingCommentLeadingRef.value], refreshTrailingCommentMeasurement, { flush: "post", immediate: true });
const labelWidthClass = computed(() => treeLabelWidthClass({ fullWidth: usesFullWidthLabel.value, hasTrailingComment: hasTrailingMetadata() }));
const paddingLeft = computed(() => treeItemPaddingLeft(props.depth));
@@ -921,8 +868,6 @@ watch(
onBeforeUnmount(() => {
stopPasteHandlerRegistration();
handleMouseLeave();
trailingCommentResizeObserver?.disconnect();
cancelTrailingCommentMeasure();
finishTableReferenceDrag();
});
@@ -1058,8 +1003,8 @@ function onKeydown(event: KeyboardEvent) {
<DatabaseIcon v-if="node.type === 'connection'" :db-type="connectionIconType(node.connectionId)" class="h-3.5 w-3.5 shrink-0" />
<Loader2 v-else-if="node.type === 'load-more' && node.isLoading" class="w-3.5 h-3.5 shrink-0 animate-spin text-primary" />
<component v-else :is="getIconInfo(node)?.icon || Database" class="w-3.5 h-3.5 shrink-0" :class="databaseOpenVisual.iconClass" />
<div ref="trailingCommentLayoutRef" :class="trailingComment ? 'flex flex-1 min-w-0 items-center' : 'contents'">
<div ref="trailingCommentLeadingRef" :class="trailingComment ? 'flex max-w-full min-w-0 shrink-0 items-center gap-2' : 'contents'">
<div :class="hasTrailingMetadata() ? 'flex flex-1 min-w-0 items-center' : 'contents'">
<div :class="trailingComment ? 'flex max-w-full min-w-0 shrink-0 items-center gap-2' : formattedObjectStorage() ? 'flex min-w-0 flex-1 items-center gap-2' : 'contents'" :style="alignedCommentLabelWidth ? { width: `${alignedCommentLabelWidth}px` } : undefined">
<input
v-if="isRenamingGroup"
ref="renameInputRef"
@@ -1084,20 +1029,14 @@ function onKeydown(event: KeyboardEvent) {
{{ t("editor.defaultDatabase") }}
</Badge>
</div>
<span v-if="trailingComment && trailingCommentMaxWidth > 0" class="min-w-0 flex-1" aria-hidden="true" />
<span
v-if="trailingComment && trailingCommentMaxWidth > 0"
class="sidebar-object-comment min-w-0 shrink-0 truncate text-left"
:class="{ 'sidebar-object-comment--windows': useWindowsSidebarCommentFont }"
:style="{ marginLeft: `${trailingCommentGapPx}px`, maxWidth: `${trailingCommentMaxWidth}px` }"
>{{ trailingComment }}</span
>
<span v-if="trailingComment" class="sidebar-object-comment ml-2 min-w-0 flex-1 truncate text-left" :class="{ 'sidebar-object-comment--windows': useWindowsSidebarCommentFont }">{{ trailingComment }}</span>
</div>
<span v-if="node.type === 'connection' && node.connectionId && connectionStore.connectedIds.has(node.connectionId)" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
<span v-if="databaseOpenVisual.showsIndicator" class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" />
<Badge v-if="isConnectionReadonly" variant="secondary" class="h-4 px-1.5 text-[10px] gap-0.5"><Lock class="w-2.5 h-2.5" />{{ t("connection.readOnlyBadge") }}</Badge>
<ConnectionErrorIndicator v-if="node.type === 'connection'" :connection-id="node.connectionId" trigger-class="h-4 w-4" />
<Pin v-if="isPinned" class="w-3 h-3 shrink-0 text-primary fill-current" aria-hidden="true" />
<span v-if="formattedObjectStorage()" class="ml-auto shrink-0 text-right text-xs tabular-nums text-muted-foreground">{{ formattedObjectStorage() }}</span>
<button
v-if="isConnecting"
type="button"
@@ -1145,11 +1084,6 @@ function onKeydown(event: KeyboardEvent) {
font-size: 10px;
line-height: 1rem;
opacity: 0.6;
/* Use the comment's natural width whenever the row has room for it. */
width: max-content;
max-width: 100%;
/* Preserve table names when a narrow row needs to truncate its comment. */
flex-shrink: 999;
/* Sidebar rows repaint on hover; avoid heavier font shaping and fallback here. */
text-rendering: auto;
}