Files
dbx/apps/desktop/src/components/layout/EditorToolbar.vue
T

586 lines
29 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import { Play, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, Layers, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2 } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import TruncatedTextTooltip from "@/components/ui/TruncatedTextTooltip.vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { catalogDatabaseOptionsKey, databaseAfterCatalogChange, normalizedQueryTabCatalog, queryCatalogSelectorVisible, selectedQueryCatalogName, useDatabaseOptions } from "@/composables/useDatabaseOptions";
import { useSchemaOptions } from "@/composables/useSchemaOptions";
import { connectionIconType } from "@/lib/connection/connectionPresentation";
import { formatDatabaseLabel, isDefaultDatabase } from "@/lib/database/defaultDatabase";
import { connectionDisplayName } from "@/lib/tabs/tabPresentation";
import { useConnectionGroupLabel } from "@/composables/useConnectionGroupLabel";
import { isSingleDatabase, supportsClearableQuerySchema, supportsSqlInListPaste, supportsTransaction as supportsTransactionFeature } from "@/lib/database/databaseCapabilities";
import { connectionIsDorisFamilyCatalogCapable } from "@/lib/database/databaseFeatureSupport";
import { hexToRgba } from "@/lib/common/color";
import { productionContextForDatabase } from "@/lib/database/productionSafety";
import type { QueryTab, ConnectionConfig } from "@/types/database";
const props = defineProps<{
activeTab: QueryTab;
activeConnection?: ConnectionConfig;
executableSql: string;
explainMode?: string;
blockDangerousRedisCommands?: boolean;
sqlKeywordCase: "preserve" | "upper" | "lower";
databaseRequiredSignal?: number;
autoCommit?: boolean;
txnSessionId?: string;
txnAutoRolledBack?: boolean;
}>();
const emit = defineEmits<{
execute: [];
cancel: [];
explain: [];
"update:explainMode": [mode: "explain" | "autotrace"];
formatSql: [];
compressSql: [];
toggleSqlKeywordCase: [];
saveSql: [];
openSql: [];
importResultArchive: [];
pasteSqlInCondition: [];
changeConnection: [connectionId: string];
changeCatalog: [catalog: string | undefined, database: string];
changeDatabase: [database: string];
changeSchema: [schema: string | undefined];
setDefaultDatabase: [];
clearDefaultDatabase: [];
"update:blockDangerousRedisCommands": [value: boolean];
"update:autoCommit": [value: boolean];
commit: [];
rollback: [];
dismissTxnRolledBack: [];
}>();
const { t } = useI18n();
const connectionStore = useConnectionStore();
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions();
const activeCatalogs = computed(() => {
const connection = props.activeConnection;
return connection ? (catalogOptions.value[connection.id] ?? []) : [];
});
const activeCatalogNames = computed(() => activeCatalogs.value.map((catalog) => catalog.name));
const showCatalogSelector = computed(() => connectionIsDorisFamilyCatalogCapable(props.activeConnection) && queryCatalogSelectorVisible(activeCatalogs.value));
const activeCatalogValue = computed(() => selectedQueryCatalogName(activeCatalogs.value, props.activeTab.catalog));
const activeCatalogDatabaseKey = computed(() => (props.activeConnection && props.activeTab.catalog ? catalogDatabaseOptionsKey(props.activeConnection.id, props.activeTab.catalog) : ""));
const activeDatabaseOptions = computed(() => {
const connection = props.activeConnection;
if (!connection) return [];
if (props.activeTab.catalog) return catalogDatabaseOptions.value[activeCatalogDatabaseKey.value] ?? [];
return databaseOptions.value[connection.id] ?? [];
});
const loadingActiveDatabaseOptions = computed(() => {
const connection = props.activeConnection;
if (!connection) return false;
if (props.activeTab.catalog) return loadingCatalogDatabaseOptions.value[activeCatalogDatabaseKey.value] ?? false;
return loadingDatabaseOptions.value[connection.id] ?? false;
});
const switchingCatalog = ref(false);
const connectionOptionIds = computed(() => connectionStore.connections.map((connection) => connection.id));
const { connectionGroupLabel } = useConnectionGroupLabel();
const activeDatabaseValue = computed(() => props.activeTab.database || "");
const activeProductionContext = computed(() => productionContextForDatabase(props.activeConnection, props.activeTab.database));
const showConnectionProductionBadge = computed(() => activeProductionContext.value.reason === "connection");
const showDatabaseProductionBadge = computed(() => activeProductionContext.value.reason === "database");
const activeConnectionValue = computed(() => props.activeConnection?.id || "");
const activeSchemaValue = computed(() => props.activeTab.schema || "");
const supportsExplain = computed(() => {
const dbType = props.activeConnection?.db_type;
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "easysearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "chromadb" && dbType !== "etcd" && dbType !== "zookeeper" && dbType !== "mq" && dbType !== "nacos";
});
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
const supportsExPaste = computed(() => supportsSqlInListPaste(props.activeConnection?.db_type));
const supportsTransaction = computed(() => supportsTransactionFeature(props.activeConnection?.db_type));
const hasDefaultDatabaseOption = computed(() => activeDatabaseOptions.value.includes(""));
const schemaDatabaseKey = computed(() => props.activeTab.database || (isSingleDb.value ? "_" : ""));
const saveTooltip = computed(() => (props.activeTab.objectSource ? t("objects.saveSource") : t("toolbar.saveSql")));
// DM calls it autotrace, Postgres EXPLAIN ANALYZE, SQL Server the actual execution
// plan (SET STATISTICS XML); all three execute the statement.
const supportsExplainAnalyze = computed(() => {
const dbType = props.activeConnection?.db_type;
return dbType === "dameng" || dbType === "postgres" || dbType === "sqlserver";
});
const explainAnalyzeTooltip = computed(() => {
const dbType = props.activeConnection?.db_type;
if (dbType === "postgres") return t("toolbar.explainAnalyze");
if (dbType === "sqlserver") return t("toolbar.actualPlan");
return t("toolbar.autotrace");
});
const canSaveSql = computed(() => !!props.activeTab.externalSqlPath || !!props.activeTab.sql.trim());
const keywordCaseIsLower = computed(() => props.sqlKeywordCase === "lower");
const keywordCaseToggleTooltip = computed(() => (keywordCaseIsLower.value ? t("toolbar.keywordCaseUpper") : t("toolbar.keywordCaseLower")));
const transactionTooltip = computed(() => {
const isAgent = (props.activeConnection?.db_type as string) === "agent";
const isManual = props.autoCommit === false;
if (isAgent && isManual) return t("toolbar.manualTransactionAgent");
if (isAgent) return t("toolbar.autoCommitAgent");
return isManual ? t("toolbar.manualTransaction") : t("toolbar.autoCommit");
});
const executeButtonClass = computed(() => {
if (props.activeTab.isExecuting) return "";
return activeProductionContext.value.active ? "bg-red-500/10 text-red-700 hover:bg-red-500/20 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200" : "bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200";
});
const isTransactionActive = computed(() => !!props.txnSessionId);
const showSchemaSelector = computed(() => {
const connection = props.activeConnection;
return connection && isSchemaAware(connection.id) && (props.activeTab.database || isSingleDb.value || hasDefaultDatabaseOption.value);
});
const activeSchemaOptions = computed(() => {
const connection = props.activeConnection;
if (!connection) return [];
return getSchemaOptionsForDb(connection.id, schemaDatabaseKey.value);
});
const databaseRequiredVisible = ref(false);
watch(
() => props.databaseRequiredSignal,
(signal) => {
if (!signal) return;
databaseRequiredVisible.value = false;
requestAnimationFrame(() => {
databaseRequiredVisible.value = true;
});
},
);
watch(activeDatabaseValue, (database) => {
if (database) databaseRequiredVisible.value = false;
});
watchEffect(() => {
const connection = props.activeConnection;
if (connection && showSchemaSelector.value) {
loadSchemaOptions(connection.id, schemaDatabaseKey.value).catch(() => {});
}
});
watchEffect(() => {
const connection = props.activeConnection;
if (!connection || !connectionIsDorisFamilyCatalogCapable(connection)) return;
void loadCatalogOptions(connection.id).catch(() => {});
});
watchEffect(() => {
const connection = props.activeConnection;
const catalog = props.activeTab.catalog;
if (!connection || !catalog) return;
void loadCatalogDatabaseOptions(connection.id, catalog).catch(() => {});
});
const isActiveDatabaseDefault = computed(() => isDefaultDatabase(props.activeConnection, activeDatabaseValue.value));
const toolbarStyle = computed(() => {
const color = props.activeConnection?.color;
if (!color) return undefined;
return {
backgroundColor: hexToRgba(color, 0.1),
boxShadow: `inset 0 1px 0 ${hexToRgba(color, 0.18)}`,
};
});
function databaseDisplayName(database: string): string {
return formatDatabaseLabel(props.activeConnection, database, {
defaultDatabase: t("editor.defaultDatabase"),
noDatabase: t("editor.noDatabase"),
});
}
function connectionById(connectionId: string): ConnectionConfig | undefined {
return connectionStore.getConfig(connectionId);
}
function databaseOptionIsProduction(database: string): boolean {
if (!database || props.activeConnection?.is_production) return false;
return productionContextForDatabase(props.activeConnection, database).reason === "database";
}
async function changeCatalog(selectedCatalog: string) {
const connection = props.activeConnection;
if (!connection) return;
switchingCatalog.value = true;
try {
const catalog = normalizedQueryTabCatalog(activeCatalogs.value, selectedCatalog);
const databases = catalog ? await loadCatalogDatabaseOptions(connection.id, selectedCatalog) : await loadDatabaseOptions(connection.id).then(() => databaseOptions.value[connection.id] ?? []);
emit("changeCatalog", catalog, databaseAfterCatalogChange(props.activeTab.database, databases));
} finally {
switchingCatalog.value = false;
}
}
</script>
<template>
<div class="app-editor-toolbar h-9 shrink-0 border-b bg-background/80 px-3 flex items-center gap-1 text-xs text-muted-foreground relative z-10" :style="toolbarStyle">
<div class="flex items-center gap-0.5">
<Tooltip>
<TooltipTrigger as-child>
<Button
:variant="activeTab.isExecuting ? 'destructive' : 'ghost'"
size="icon"
class="h-6 w-6"
:class="executeButtonClass"
:disabled="activeTab.isCancelling || activeTab.isExplaining || (!activeTab.isExecuting && !executableSql.trim())"
@click="activeTab.isExecuting ? emit('cancel') : emit('execute')"
>
<Loader2 v-if="activeTab.isCancelling" class="h-3.5 w-3.5 animate-spin" />
<Square v-else-if="activeTab.isExecuting" class="h-3.5 w-3.5 fill-current" />
<Play v-else class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ activeTab.isExecuting ? t("toolbar.stopQuery") : t("toolbar.executeShortcut") }}</TooltipContent>
</Tooltip>
<Tooltip v-if="supportsExplain">
<TooltipTrigger as-child>
<Button
:variant="activeTab.isExplaining ? 'destructive' : 'ghost'"
size="icon"
class="h-6 w-6"
:class="activeTab.isExplaining ? '' : 'text-violet-600 hover:bg-violet-500/10 hover:text-violet-700 dark:text-violet-300 dark:hover:text-violet-200'"
:disabled="activeTab.isExecuting || (!activeTab.isExplaining && !executableSql.trim())"
@click="activeTab.isExplaining ? emit('cancel') : emit('explain')"
>
<Square v-if="activeTab.isExplaining" class="h-3.5 w-3.5 fill-current" />
<GitBranch v-else class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ activeTab.isExplaining ? t("toolbar.stopExplain") : t("toolbar.explainPlan") }}</TooltipContent>
</Tooltip>
<!-- Autotrace (DM) / EXPLAIN ANALYZE (Postgres) / actual plan (SQL Server) toggle -->
<Tooltip v-if="supportsExplainAnalyze">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6"
:class="props.explainMode === 'autotrace' ? 'text-green-600 bg-green-100 dark:text-green-300 dark:bg-green-900/30' : 'text-muted-foreground/50'"
:disabled="activeTab.isExecuting"
:aria-label="explainAnalyzeTooltip"
:aria-pressed="props.explainMode === 'autotrace'"
@click="emit('update:explainMode', props.explainMode === 'autotrace' ? 'explain' : 'autotrace')"
>
<span class="font-bold" style="font-size: 9px">A</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ explainAnalyzeTooltip }}</TooltipContent>
</Tooltip>
<!-- Transaction toggle -->
<Tooltip v-if="supportsTransaction">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6"
:class="isTransactionActive || autoCommit === false ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300' : 'text-orange-600/70 hover:bg-orange-500/10 hover:text-orange-700 dark:text-orange-300/70 dark:hover:text-orange-200'"
:disabled="activeTab.isExecuting || activeTab.isExplaining"
@click="emit('update:autoCommit', autoCommit === false)"
>
<span class="text-xs font-bold leading-none">Tx</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ transactionTooltip }}</TooltipContent>
</Tooltip>
<!-- Commit button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-green-600 hover:bg-green-500/10 hover:text-green-700 dark:text-green-300 dark:hover:text-green-200" :disabled="activeTab.isExecuting" @click="emit('commit')">
<Check class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.commit") }}</TooltipContent>
</Tooltip>
<!-- Rollback button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-red-600 hover:bg-red-500/10 hover:text-red-700 dark:text-red-300 dark:hover:text-red-200" :disabled="activeTab.isExecuting" @click="emit('rollback')">
<RotateCcw class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.rollback") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-amber-600 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200" :disabled="activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()" @click="emit('formatSql')">
<AlignLeft class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.formatSql") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-amber-600 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200" :disabled="activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()" @click="emit('compressSql')">
<Minimize2 class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.compressSql") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6 font-mono text-sm font-semibold leading-none"
:class="keywordCaseIsLower ? 'bg-amber-500/10 text-amber-700 hover:bg-amber-500/20 hover:text-amber-800 dark:text-amber-300 dark:hover:text-amber-200' : 'text-amber-600/70 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300/70 dark:hover:text-amber-200'"
:aria-label="keywordCaseToggleTooltip"
@click="emit('toggleSqlKeywordCase')"
>
{{ keywordCaseIsLower ? "a" : "A" }}
</Button>
</TooltipTrigger>
<TooltipContent>{{ keywordCaseToggleTooltip }}</TooltipContent>
</Tooltip>
<Tooltip v-if="activeConnection?.db_type === 'redis'">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6"
:class="blockDangerousRedisCommands !== false ? 'text-orange-600 bg-orange-100 dark:text-orange-300 dark:bg-orange-900/30' : 'text-muted-foreground/50'"
@click="emit('update:blockDangerousRedisCommands', blockDangerousRedisCommands === false)"
>
<Shield class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.blockDangerousRedisCommands") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-blue-600 hover:bg-blue-500/10 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200" :disabled="!canSaveSql" @click="emit('saveSql')">
<Save class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ saveTooltip }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200" @click="emit('openSql')">
<FolderOpen class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.openSql") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-cyan-600 hover:bg-cyan-500/10 hover:text-cyan-700 dark:text-cyan-300 dark:hover:text-cyan-200" @click="emit('importResultArchive')">
<Download class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("tabs.importResultArchive") }}</TooltipContent>
</Tooltip>
<Tooltip v-if="supportsExPaste">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-teal-600 hover:bg-teal-500/10 hover:text-teal-700 dark:text-teal-300 dark:hover:text-teal-200" @click="emit('pasteSqlInCondition')">
<ClipboardPaste class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.exPasteSqlInCondition") }}</TooltipContent>
</Tooltip>
</div>
<span class="flex-1 min-w-0" />
<div class="flex items-center gap-2 shrink-0">
<div class="flex items-center gap-1">
<span v-if="activeConnection?.color" class="h-4 w-1 rounded-full shrink-0" :style="{ backgroundColor: activeConnection.color }" />
<SearchableSelect
:model-value="activeConnectionValue"
:options="connectionOptionIds"
:placeholder="t('editor.selectConnection')"
:search-placeholder="t('editor.searchConnection')"
:empty-text="t('grid.noSearchResults')"
:loading-text="t('common.loading')"
trigger-variant="ghost"
trigger-class="font-medium text-foreground"
trigger-icon-class="h-3 w-3"
:display-name="connectionDisplayName"
list-class="w-96 max-w-[calc(100vw-2rem)]"
item-class="min-h-9 h-auto py-1"
@update:model-value="(connectionId) => emit('changeConnection', connectionId)"
>
<template #trigger-label="{ label }">
<div v-if="activeConnection" class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="connectionIconType(activeConnection)" class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ label }}</span>
<ProductionContextBadge v-if="showConnectionProductionBadge" compact />
</div>
<span v-else class="truncate text-muted-foreground">{{ t("editor.selectConnection") }}</span>
</template>
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(connectionById(option))" class="h-3.5 w-3.5 shrink-0" />
<div class="flex min-w-0 flex-1 items-center gap-2">
<span class="block min-w-0 max-w-48 shrink-0 whitespace-normal break-words rounded-sm bg-muted/70 px-1.5 py-0.5 text-[11px] leading-tight text-muted-foreground">
{{ connectionGroupLabel(option) }}
</span>
<TruncatedTextTooltip :text="label" class="block min-w-[7rem] flex-1 text-sm font-medium" side="left" :side-offset="8" />
</div>
</div>
</template>
</SearchableSelect>
</div>
<div v-if="showCatalogSelector" class="flex items-center gap-1">
<SearchableSelect
:model-value="activeCatalogValue"
:options="activeCatalogNames"
:placeholder="t('editor.selectCatalog')"
:search-placeholder="t('editor.searchCatalog')"
:empty-text="t('grid.noSearchResults')"
:loading-text="t('common.loading')"
:loading="loadingCatalogOptions[activeConnection?.id || ''] || switchingCatalog"
trigger-variant="ghost"
trigger-class="gap-1.5"
trigger-icon-class="h-3 w-3"
@update:model-value="changeCatalog"
@update:open="
(open: boolean) => {
if (open && activeConnection) loadCatalogOptions(activeConnection.id).catch(() => {});
}
"
>
<template #trigger-label="{ label, loading }">
<Layers class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ loading ? t("common.loading") : label }}</span>
</template>
</SearchableSelect>
</div>
<div
v-if="
activeConnection?.db_type !== 'elasticsearch' &&
activeConnection?.db_type !== 'easysearch' &&
activeConnection?.db_type !== 'qdrant' &&
activeConnection?.db_type !== 'milvus' &&
activeConnection?.db_type !== 'weaviate' &&
activeConnection?.db_type !== 'chromadb' &&
activeConnection?.db_type !== 'zookeeper' &&
!isSingleDb
"
class="flex items-center gap-1"
:class="{ 'database-required-prompt': databaseRequiredVisible }"
>
<SearchableSelect
:model-value="activeDatabaseValue"
:options="activeDatabaseOptions.length ? activeDatabaseOptions : activeDatabaseValue ? [activeDatabaseValue] : []"
:placeholder="t('editor.selectDatabase')"
:search-placeholder="t('editor.searchDatabase')"
:empty-text="t('grid.noSearchResults')"
:loading-text="t('common.loading')"
:loading="loadingActiveDatabaseOptions"
:display-name="databaseDisplayName"
trigger-variant="ghost"
trigger-class="gap-1.5"
trigger-icon-class="h-3 w-3"
@update:model-value="(database) => emit('changeDatabase', database)"
@update:open="
(open: boolean) => {
if (!open || !activeConnection) return;
if (activeTab.catalog) loadCatalogDatabaseOptions(activeConnection.id, activeTab.catalog).catch(() => {});
else loadDatabaseOptions(activeConnection.id).catch(() => {});
}
"
>
<template #trigger-label="{ label, loading }">
<Database class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ loading ? t("common.loading") : label }}</span>
<ProductionContextBadge v-if="showDatabaseProductionBadge" compact />
</template>
<template #option-label="{ option, label }">
<div class="flex min-w-0 flex-1 items-center gap-1.5">
<TruncatedTextTooltip :text="label" class="min-w-0 flex-1" side="left" :side-offset="8" />
<ProductionContextBadge v-if="databaseOptionIsProduction(option)" compact />
</div>
</template>
</SearchableSelect>
<Tooltip v-if="activeDatabaseValue && !isSingleDb">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground hover:text-foreground" @click="emit('changeDatabase', '')">
<X class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("editor.clearDatabase") }}</TooltipContent>
</Tooltip>
<Button v-if="activeDatabaseValue && !activeTab.catalog" variant="ghost" size="sm" class="h-6 px-2 text-[11px]" @click="isActiveDatabaseDefault ? emit('clearDefaultDatabase') : emit('setDefaultDatabase')">
<Check v-if="isActiveDatabaseDefault" class="h-3 w-3" />
{{ isActiveDatabaseDefault ? t("editor.defaultDatabase") : t("editor.setDefaultDatabase") }}
</Button>
</div>
<div v-if="showSchemaSelector" class="flex items-center gap-1">
<SearchableSelect
:model-value="activeSchemaValue"
:options="activeSchemaOptions.length ? activeSchemaOptions : activeSchemaValue ? [activeSchemaValue] : []"
:placeholder="t('editor.selectSchema')"
:search-placeholder="t('editor.searchSchema')"
:empty-text="t('grid.noSearchResults')"
:loading-text="t('common.loading')"
:loading="!!activeConnection && isLoadingSchemas(activeConnection.id, schemaDatabaseKey)"
:clear-selected-option="supportsClearableQuerySchema(activeConnection?.db_type)"
trigger-variant="ghost"
trigger-class="gap-1.5"
trigger-icon-class="h-3 w-3"
@update:model-value="(schema) => emit('changeSchema', schema || undefined)"
@update:open="
(open: boolean) => {
if (open && activeConnection) loadSchemaOptions(activeConnection.id, schemaDatabaseKey).catch(() => {});
}
"
>
<template #trigger-label="{ label, loading }">
<Layers class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ loading ? t("common.loading") : label }}</span>
</template>
<template #option-label="{ label }">
<TruncatedTextTooltip :text="label" class="min-w-0 flex-1" side="left" :side-offset="8" />
</template>
</SearchableSelect>
</div>
</div>
<div v-if="activeTab.mode === 'data' && activeTab.tableMeta" class="ml-2 inline-flex shrink-0 items-center gap-1 rounded border border-border bg-muted/30 px-2 py-0.5 font-medium text-muted-foreground tabular-nums">
<Table2 class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ activeTab.tableMeta.columns.length }} {{ t("tree.columns") }}</span>
</div>
</div>
<div v-if="txnAutoRolledBack" class="flex items-center gap-2 px-3 py-1 text-xs bg-amber-500/10 text-amber-700 dark:text-amber-300 border-b border-amber-500/20">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<span>{{ t("toolbar.txnAutoRolledBack") }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="emit('dismissTxnRolledBack')">
<X class="h-3 w-3" />
</Button>
</div>
</template>
<style scoped>
.database-required-prompt {
color: var(--destructive);
animation: database-required-shake 420ms ease;
}
.database-required-prompt :deep(button) {
color: var(--destructive);
border-color: color-mix(in oklch, var(--destructive) 55%, transparent);
background: color-mix(in oklch, var(--destructive) 10%, transparent);
}
@keyframes database-required-shake {
0%,
100% {
transform: translateX(0);
}
12%,
36%,
60% {
transform: translateX(-3px);
}
24%,
48%,
72% {
transform: translateX(3px);
}
}
</style>