feat(query): close individual query result and release result session

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-10 22:40:36 +08:00
1 parent 044bedaabe
commit 3663dc4d7b
14 files changed
+166 -1

No files matched your search

+2 -1
View File
@@ -2182,7 +2182,7 @@ function handleNativeSelectAll(e: KeyboardEvent) {
if (shouldBlockAppNativeSelectAll(e)) e.preventDefault();
}
function handleKeydown(e: KeyboardEvent) {
async function handleKeydown(e: KeyboardEvent) {
if (e.defaultPrevented) return;
const shortcuts = settingsStore.editorSettings.shortcuts;
@@ -2260,6 +2260,7 @@ function handleKeydown(e: KeyboardEvent) {
} else if (showDriverStore.value) {
closeDriverStorePage();
} else if (queryStore.activeTabId) {
if (await queryStore.clearQueryResults(queryStore.activeTabId)) return;
queryStore.closeTab(queryStore.activeTabId);
}
return;
@@ -419,6 +419,7 @@ const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true);
const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.active));
const showResultRunTabs = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "tabs");
const showResultRunSelector = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "list");
const canCloseQueryResult = computed(() => props.activeTab.mode === "query" && !props.activeTab.isExecuting && !props.activeTab.activeResultRunId && (!!props.activeTab.result || !!props.activeTab.results?.length || props.activeTab.resultEvicted === true));
watch(
() => `${resultRunDisplayMode.value}:${resultRuns.value.map((run) => run.id).join(",")}:${props.activeTab.activeResultRunId ?? ""}`,
() => {
@@ -853,6 +854,11 @@ async function removeResultRun(runId: string) {
activeRunTab?.scrollIntoView({ block: "nearest", inline: "nearest" });
}
async function closeCurrentQueryResult() {
if (!(await queryStore.closeQueryResult(props.activeTab.id))) return;
emit("update:activeOutputView", "result");
}
async function selectResultRun(runId: string) {
if (!(await queryStore.setActiveResultRun(props.activeTab.id, runId))) {
toast(t("tabs.missingResultRun"), 4000);
@@ -1056,6 +1062,9 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
>
<Pin class="h-3.5 w-3.5" :class="{ 'fill-current': resultAutoSave }" />
</Button>
<Button v-if="canCloseQueryResult" variant="ghost" size="icon" class="h-6 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('tabs.closeResult')" :aria-label="t('tabs.closeResult')" @click="closeCurrentQueryResult">
<X class="h-3.5 w-3.5" />
</Button>
<template v-if="resultRuns.length > 0 || visibleResultItems.length > 0">
<span class="mx-1 h-4 w-px shrink-0 bg-border" />
<div v-if="showResultRunTabs" data-result-run-tabs-region class="relative min-w-0 flex-1 self-stretch">
+1
View File
@@ -1271,6 +1271,7 @@ export default {
resultRuns: "Result runs",
resultSets: "Result sets",
removeRun: "Remove run {n}",
closeResult: "Close result",
autoKeepResults: "Auto-keep query results",
autoKeepResultsEnabled: "Auto-keep results enabled",
autoKeepResultsDisabled: "Auto-keep results disabled",
+1
View File
@@ -1248,6 +1248,7 @@ export default withEnglishFallback({
resultRuns: "Ejecuciones de resultado",
resultSets: "Conjuntos de resultados",
removeRun: "Eliminar ejecución {n}",
closeResult: "Cerrar resultado",
autoKeepResults: "Conservar resultados automáticamente",
autoKeepResultsEnabled: "Conservación automática activada",
autoKeepResultsDisabled: "Conservación automática desactivada",
+1
View File
@@ -1246,6 +1246,7 @@ export default withEnglishFallback({
resultRuns: "Esecuzioni risultati",
resultSets: "Set di risultati",
removeRun: "Rimuovi esecuzione {n}",
closeResult: "Chiudi risultato",
autoKeepResults: "Mantieni automaticamente i risultati",
autoKeepResultsEnabled: "Mantieni risultati abilitato",
autoKeepResultsDisabled: "Mantieni risultati disabilitato",
+1
View File
@@ -1265,6 +1265,7 @@ export default withEnglishFallback({
resultRuns: "実行履歴",
resultSets: "結果セット",
removeRun: "実行 {n} を削除",
closeResult: "結果を閉じる",
autoKeepResults: "クエリ結果を自動保持",
autoKeepResultsEnabled: "結果の自動保持をオンにしました",
autoKeepResultsDisabled: "結果の自動保持をオフにしました",
+1
View File
@@ -1160,6 +1160,7 @@ export default withEnglishFallback({
resultRuns: "결과 실행",
resultSets: "결과 집합",
removeRun: "실행 {n} 제거",
closeResult: "결과 닫기",
autoKeepResults: "쿼리 결과 자동 보관",
autoKeepResultsEnabled: "결과 자동 보관 활성화됨",
autoKeepResultsDisabled: "결과 자동 보관 비활성화됨",
+1
View File
@@ -1248,6 +1248,7 @@ export default withEnglishFallback({
resultRuns: "Execuções de resultado",
resultSets: "Conjuntos de resultados",
removeRun: "Remover execução {n}",
closeResult: "Fechar resultado",
autoKeepResults: "Manter resultados automaticamente",
autoKeepResultsEnabled: "Manutenção automática de resultados ativada",
autoKeepResultsDisabled: "Manutenção automática de resultados desativada",
+1
View File
@@ -1196,6 +1196,7 @@ export default withEnglishFallback({
resultRuns: "执行结果",
resultSets: "语句结果",
removeRun: "删除执行 {n}",
closeResult: "关闭结果",
autoKeepResults: "自动保留查询结果",
autoKeepResultsEnabled: "已开启自动保留结果",
autoKeepResultsDisabled: "已关闭自动保留结果",
+1
View File
@@ -1247,6 +1247,7 @@ export default withEnglishFallback({
resultRuns: "執行結果",
resultSets: "語句結果",
removeRun: "移除執行 {n}",
closeResult: "關閉結果",
autoKeepResults: "自動保留查詢結果",
autoKeepResultsEnabled: "自動保留結果已啟用",
autoKeepResultsDisabled: "自動保留結果已停用",
@@ -810,4 +810,74 @@ describe("queryStore multi-statement errors", () => {
expect(tab.activeResultRunId).toBe(tab.resultRuns?.[0]?.id);
expect(tab.result?.rows).toEqual([[1]]);
});
it("does not clear a newer result while closing the previous result session", async () => {
const pendingClose = deferred<void>();
mocks.closeQuerySession.mockReturnValue(pendingClose.promise);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query");
const tab = store.tabs.find((item) => item.id === tabId)!;
tab.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, session_id: "result-session-1" };
tab.resultSessionId = "result-session-1";
tab.resultClientSessionId = "result-client-1";
const closing = store.closeQueryResult(tabId);
expect(tab.result).toBeUndefined();
tab.result = { columns: ["value"], rows: [[2]], affected_rows: 0, execution_time_ms: 1, session_id: "result-session-2" };
tab.resultSessionId = "result-session-2";
tab.resultClientSessionId = "result-client-2";
pendingClose.resolve();
await expect(closing).resolves.toBe(true);
expect(tab.result?.rows).toEqual([[2]]);
expect(tab.resultSessionId).toBe("result-session-2");
expect(tab.resultClientSessionId).toBe("result-client-2");
});
it("clears every retained result run while preserving the query tab", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query");
const tab = store.tabs.find((item) => item.id === tabId)!;
tab.sql = "select draft";
tab.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, session_id: "result-session-1" };
tab.resultSessionId = "result-session-1";
tab.resultClientSessionId = "result-client-1";
tab.resultAutoSave = true;
tab.resultRuns = [
{
id: "run-1",
title: "Run 1",
sequence: 1,
sql: "select 1",
createdAt: 1,
result: tab.result,
resultSessionId: "result-session-1",
resultClientSessionId: "result-client-1",
},
{
id: "run-2",
title: "Run 2",
sequence: 2,
sql: "select 2",
createdAt: 2,
result: { columns: ["value"], rows: [[2]], affected_rows: 0, execution_time_ms: 1, session_id: "result-session-2" },
resultSessionId: "result-session-2",
resultClientSessionId: "result-client-2",
},
];
tab.activeResultRunId = "run-1";
await expect(store.clearQueryResults(tabId)).resolves.toBe(true);
expect(mocks.closeQuerySession).toHaveBeenCalledTimes(2);
expect(tab.sql).toBe("select draft");
expect(tab.resultAutoSave).toBe(true);
expect(tab.result).toBeUndefined();
expect(tab.results).toBeUndefined();
expect(tab.resultRuns).toBeUndefined();
expect(tab.activeResultRunId).toBeUndefined();
});
});
+41
View File
@@ -1095,6 +1095,45 @@ export const useQueryStore = defineStore("query", () => {
return true;
}
async function closeQueryResult(id: string) {
const tab = tabs.value.find((item) => item.id === id);
if (!tab || tab.mode !== "query" || tab.isExecuting) return false;
if (tab.activeResultRunId) return removeResultRun(id, tab.activeResultRunId);
if (!tab.result && !tab.results?.length && !tab.resultEvicted) return false;
const closeSession = closeResultSession(tab);
clearResultPayload(tab);
await closeSession;
return true;
}
async function clearQueryResults(id: string) {
const tab = tabs.value.find((item) => item.id === id);
if (!tab || tab.mode !== "query" || tab.isExecuting) return false;
const resultRuns = tab.resultRuns ?? [];
if (!tab.result && !tab.results?.length && !tab.resultEvicted && resultRuns.length === 0) return false;
const closedSessionIds = new Set<string>();
const currentSessionId = tab.resultSessionId ?? tab.result?.session_id;
if (currentSessionId) closedSessionIds.add(currentSessionId);
const closeOperations = [closeResultSession(tab)];
for (const run of resultRuns) {
if (run.resultCacheKey) void deleteTabResultSnapshot(run.resultCacheKey);
if (!run.resultSessionId || closedSessionIds.has(run.resultSessionId)) continue;
closedSessionIds.add(run.resultSessionId);
closeOperations.push(closeResultRunSession(tab, run));
}
tab.resultRuns = undefined;
tab.activeResultRunId = undefined;
clearResultPayload(tab);
await Promise.all(closeOperations);
return true;
}
function nextResultRunSequence(tab: QueryTab): number {
return (tab.resultRuns?.reduce((max, run) => Math.max(max, run.sequence), 0) ?? 0) + 1;
}
@@ -5697,6 +5736,8 @@ export const useQueryStore = defineStore("query", () => {
toggleResultAutoSave,
setActiveResultRun,
removeResultRun,
closeQueryResult,
clearQueryResults,
setActiveResultIndex,
executeCurrentTab,
executeCurrentSql,