feat(query): add manual transaction controls

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-02 01:30:31 +08:00
1 parent f871e00330
commit d55a708d6a
27 files changed
+927 -40

No files matched your search

@@ -1,7 +1,7 @@
<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, Upload } from "@lucide/vue";
import { Play, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, Layers, X, Shield, Upload, RotateCcw, AlertTriangle } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@@ -13,7 +13,7 @@ import { useSchemaOptions } from "@/composables/useSchemaOptions";
import { connectionIconType } from "@/lib/connectionPresentation";
import { formatDatabaseLabel, isDefaultDatabase } from "@/lib/defaultDatabase";
import { connectionDisplayName } from "@/lib/tabPresentation";
import { isSingleDatabase } from "@/lib/databaseCapabilities";
import { isSingleDatabase, supportsTransaction as supportsTransactionFeature } from "@/lib/databaseCapabilities";
import { hexToRgba } from "@/lib/color";
import type { QueryTab, ConnectionConfig } from "@/types/database";
@@ -25,6 +25,9 @@ const props = defineProps<{
blockDangerousRedisCommands?: boolean;
sqlKeywordCase: "preserve" | "upper" | "lower";
databaseRequiredSignal?: number;
autoCommit?: boolean;
txnSessionId?: string;
txnAutoRolledBack?: boolean;
}>();
const emit = defineEmits<{
@@ -43,6 +46,10 @@ const emit = defineEmits<{
setDefaultDatabase: [];
clearDefaultDatabase: [];
"update:blockDangerousRedisCommands": [value: boolean];
"update:autoCommit": [value: boolean];
commit: [];
rollback: [];
dismissTxnRolledBack: [];
}>();
const { t } = useI18n();
@@ -64,12 +71,22 @@ const supportsExplain = computed(() => {
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && 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 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")));
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 isTransactionActive = computed(() => !!props.txnSessionId);
const showSchemaSelector = computed(() => {
const connection = props.activeConnection;
@@ -175,6 +192,41 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
>
<span class="font-bold" style="font-size: 9px">A</span>
</Button>
<!-- Transaction toggle -->
<Tooltip v-if="supportsTransaction">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6"
:class="isTransactionActive || autoCommit === false ? 'text-orange-600 bg-orange-100 dark:text-orange-300 dark:bg-orange-900/30' : 'text-muted-foreground/50'"
:disabled="activeTab.isExecuting || activeTab.isExplaining"
@click="emit('update:autoCommit', autoCommit === false)"
>
<span class="font-bold" style="font-size: 9px">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')">
@@ -342,6 +394,13 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
<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>