feat(query): close individual query result and release result session
This commit is contained in:
14 files changed
+166
-1
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1265,6 +1265,7 @@ export default withEnglishFallback({
|
||||
resultRuns: "実行履歴",
|
||||
resultSets: "結果セット",
|
||||
removeRun: "実行 {n} を削除",
|
||||
closeResult: "結果を閉じる",
|
||||
autoKeepResults: "クエリ結果を自動保持",
|
||||
autoKeepResultsEnabled: "結果の自動保持をオンにしました",
|
||||
autoKeepResultsDisabled: "結果の自動保持をオフにしました",
|
||||
|
||||
@@ -1160,6 +1160,7 @@ export default withEnglishFallback({
|
||||
resultRuns: "결과 실행",
|
||||
resultSets: "결과 집합",
|
||||
removeRun: "실행 {n} 제거",
|
||||
closeResult: "결과 닫기",
|
||||
autoKeepResults: "쿼리 결과 자동 보관",
|
||||
autoKeepResultsEnabled: "결과 자동 보관 활성화됨",
|
||||
autoKeepResultsDisabled: "결과 자동 보관 비활성화됨",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1196,6 +1196,7 @@ export default withEnglishFallback({
|
||||
resultRuns: "执行结果",
|
||||
resultSets: "语句结果",
|
||||
removeRun: "删除执行 {n}",
|
||||
closeResult: "关闭结果",
|
||||
autoKeepResults: "自动保留查询结果",
|
||||
autoKeepResultsEnabled: "已开启自动保留结果",
|
||||
autoKeepResultsDisabled: "已关闭自动保留结果",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user