feat(editor): retain multiple query result runs
This commit is contained in:
1 parent
fddc0fdf50
commit
7f20b03177
26 files changed
+846
-109
No files matched your search
@@ -134,6 +134,7 @@ const emit = defineEmits<{
|
||||
cursorChange: [pos: number];
|
||||
formatError: [message: string];
|
||||
execute: [source: SqlExecutionOverride];
|
||||
executeInNewResultTab: [source: SqlExecutionOverride];
|
||||
save: [];
|
||||
clickTable: [target: SqlObjectNavigationTarget];
|
||||
viewTableData: [target: SqlObjectNavigationTarget];
|
||||
@@ -562,6 +563,15 @@ function performNormalTab(view: EditorViewType): boolean {
|
||||
interface RequestExecuteOptions {
|
||||
ignoreSelection?: boolean;
|
||||
bypassPicker?: boolean;
|
||||
openInNewResultTab?: boolean;
|
||||
}
|
||||
|
||||
function emitExecutionRequest(source: SqlExecutionOverride, openInNewResultTab = false) {
|
||||
if (openInNewResultTab) {
|
||||
emit("executeInNewResultTab", source);
|
||||
} else {
|
||||
emit("execute", source);
|
||||
}
|
||||
}
|
||||
|
||||
function requestExecute(options: RequestExecuteOptions = {}) {
|
||||
@@ -570,15 +580,19 @@ function requestExecute(options: RequestExecuteOptions = {}) {
|
||||
return requestExecuteFromView(currentView, currentView.state.selection.main.head, options);
|
||||
}
|
||||
|
||||
function requestExecuteInNewResultTab() {
|
||||
return requestExecute({ bypassPicker: true, openInNewResultTab: true });
|
||||
}
|
||||
|
||||
function requestExecuteFromView(currentView: EditorViewType, cursorPos: number, options: RequestExecuteOptions = {}) {
|
||||
const selection = currentView.state.selection.main;
|
||||
if (!options.ignoreSelection && !selection.empty) {
|
||||
// Has manual selection → execute directly, skip picker.
|
||||
emit("execute", sqlExecutionSnapshotFromView(currentView));
|
||||
emitExecutionRequest(sqlExecutionSnapshotFromView(currentView), options.openInNewResultTab);
|
||||
return true;
|
||||
}
|
||||
if (!supportsExecutionTargetPicker(props.databaseType)) {
|
||||
emit("execute", sqlExecutionSnapshotFromView(currentView));
|
||||
emitExecutionRequest(sqlExecutionSnapshotFromView(currentView), options.openInNewResultTab);
|
||||
return true;
|
||||
}
|
||||
// No selection → resolve the execution target, optionally via the picker.
|
||||
@@ -590,7 +604,7 @@ function requestExecuteFromView(currentView: EditorViewType, cursorPos: number,
|
||||
if (options.bypassPicker || !settingsStore.editorSettings.showExecutionTargetPicker || !hasMultipleExecutionTargets(doc, props.databaseType)) {
|
||||
const preferredKind = settingsStore.editorSettings.executeMode === "current" ? "cursor" : "all";
|
||||
const candidate = candidates.find((item) => item.kind === preferredKind) ?? candidates[0];
|
||||
emit("execute", candidate.sql);
|
||||
emitExecutionRequest(candidate.sql, options.openInNewResultTab);
|
||||
return true;
|
||||
}
|
||||
closePicker();
|
||||
@@ -963,6 +977,12 @@ function executeFromContextMenu() {
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function executeInNewResultTabFromContextMenu() {
|
||||
if (!canExecuteContextSql.value) return;
|
||||
requestExecuteInNewResultTab();
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
async function copySelectedSqlFromContextMenu() {
|
||||
if (!canCopySelectedSql.value) return;
|
||||
try {
|
||||
@@ -1230,6 +1250,13 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
icon: Play,
|
||||
shortcut: shortcuts.executeSql,
|
||||
},
|
||||
{
|
||||
label: t("settings.shortcutExecuteSqlInNewResultTab"),
|
||||
action: executeInNewResultTabFromContextMenu,
|
||||
disabled: !canExecuteContextSql.value,
|
||||
icon: Play,
|
||||
shortcut: shortcuts.executeSqlInNewResultTab,
|
||||
},
|
||||
]),
|
||||
...queryContextObjectActions(contextObjectTarget.value?.type).map(contextObjectMenuItem),
|
||||
{ label: "", separator: true },
|
||||
@@ -1299,6 +1326,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
// Keep the shortcut on the shared execution-mode path (selection priority + configured cursor/all target),
|
||||
// but bypass the picker so the keyboard shortcut always executes directly instead of popping a dialog.
|
||||
const executeBindings = props.hideExecutionControls ? [] : binding(shortcuts.executeSql, () => requestExecute({ bypassPicker: true }));
|
||||
const executeInNewResultTabBindings = props.hideExecutionControls ? [] : binding(shortcuts.executeSqlInNewResultTab, requestExecuteInNewResultTab);
|
||||
return [
|
||||
Prec?.high(
|
||||
codeMirrorKeymap.of([
|
||||
@@ -1309,6 +1337,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
},
|
||||
...binding(shortcuts.find, openSearch),
|
||||
...binding(shortcuts.replace, openReplace),
|
||||
...executeInNewResultTabBindings,
|
||||
...executeBindings,
|
||||
...binding(shortcuts.saveSql, () => {
|
||||
emit("save");
|
||||
@@ -4358,6 +4387,7 @@ defineExpose({
|
||||
openReplace,
|
||||
scrollCursorIntoView,
|
||||
requestExecute,
|
||||
requestExecuteInNewResultTab,
|
||||
pasteClipboardAsSqlInCondition,
|
||||
previewStatementRange,
|
||||
refreshCompletionCache,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
import { canReloadUnavailableDataTab } from "@/lib/table/tableDataRefresh";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Check, Columns3, Columns3Cog, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus, ShieldAlert } from "@lucide/vue";
|
||||
import { Check, Columns3, Columns3Cog, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus, ShieldAlert, PanelsTopLeft } from "@lucide/vue";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -65,7 +65,7 @@ const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explai
|
||||
const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue"));
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode } from "@/stores/settingsStore";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode, type ResultRunDisplayMode } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, queryResultExecutionSql, resultGridCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, statementExecutionMarkers, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
@@ -143,6 +143,7 @@ const emit = defineEmits<{
|
||||
fixWithAi: [errorMessage: string];
|
||||
sendSelectionToAi: [sql: string];
|
||||
execute: [sqlOverride?: SqlExecutionOverride];
|
||||
executeInNewResultTab: [sqlOverride?: SqlExecutionOverride];
|
||||
saveSql: [];
|
||||
cancel: [];
|
||||
explain: [];
|
||||
@@ -210,6 +211,7 @@ const columnVisibilitySearch = ref("");
|
||||
const columnVisibilityOptions = computed(() => dataGridRef.value?.filteredColumnVisibilityOptions(columnVisibilitySearch.value) ?? []);
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
|
||||
const resultRunDisplayMode = computed(() => settingsStore.editorSettings.resultRunDisplayMode);
|
||||
const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWidthDensity);
|
||||
const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize);
|
||||
const redisKeyBrowserRef = ref<SearchableBrowserHandle>();
|
||||
@@ -250,6 +252,10 @@ function setDataGridSearchMode(value: DataGridSearchMode) {
|
||||
settingsStore.updateEditorSettings({ dataGridSearchMode: value });
|
||||
}
|
||||
|
||||
function setResultRunDisplayMode(value: ResultRunDisplayMode) {
|
||||
settingsStore.updateEditorSettings({ resultRunDisplayMode: value });
|
||||
}
|
||||
|
||||
function setColumnWidthDensity(value: "compact" | "standard" | "comfortable") {
|
||||
settingsStore.updateEditorSettings({ columnWidthDensity: value });
|
||||
}
|
||||
@@ -323,7 +329,15 @@ const activeQueryError = computed(() => {
|
||||
});
|
||||
const hasQueryOutput = computed(
|
||||
() =>
|
||||
!!props.activeTab.result || props.activeTab.resultEvicted === true || !!props.activeTab.explainPlan || !!props.activeTab.explainError || !!props.activeTab.explainTableResult || !!props.activeTab.explainTableError || props.activeTab.isExecuting === true || props.activeTab.isExplaining === true,
|
||||
!!props.activeTab.result ||
|
||||
!!props.activeTab.resultRuns?.length ||
|
||||
props.activeTab.resultEvicted === true ||
|
||||
!!props.activeTab.explainPlan ||
|
||||
!!props.activeTab.explainError ||
|
||||
!!props.activeTab.explainTableResult ||
|
||||
!!props.activeTab.explainTableError ||
|
||||
props.activeTab.isExecuting === true ||
|
||||
props.activeTab.isExplaining === true,
|
||||
);
|
||||
const visibleResultItems = computed(() => tabularResultItems(props.activeTab.results ?? (props.activeTab.result ? [props.activeTab.result] : undefined)));
|
||||
const tabularResults = computed(() => tabularResultItems(props.activeTab.results));
|
||||
@@ -335,7 +349,6 @@ const allResultExportSheets = computed(() =>
|
||||
})),
|
||||
);
|
||||
const resultRuns = computed(() => resultRunItems(props.activeTab));
|
||||
const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.active));
|
||||
const activeResultGridCacheKey = computed(() => resultGridCacheKey(props.activeTab));
|
||||
const activeResultSql = computed(() => resultSqlForGrid(props.activeTab));
|
||||
const activeResultExportSql = computed(() => queryResultExecutionSql(props.activeTab));
|
||||
@@ -352,11 +365,16 @@ const activeElasticsearchJsonResponse = computed(() => elasticsearchJsonResponse
|
||||
const resultArchiveExporting = ref(false);
|
||||
const canExportResultArchive = computed(() => props.activeTab.mode === "query" && (!!props.activeTab.result || !!props.activeTab.results?.length || !!props.activeTab.resultRuns?.length));
|
||||
const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true);
|
||||
const showResultRunSelector = computed(() => resultAutoSave.value && resultRuns.value.length > 0);
|
||||
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");
|
||||
watch(
|
||||
() => visibleResultItems.value.map((item) => item.index).join(","),
|
||||
() => `${resultRunDisplayMode.value}:${resultRuns.value.map((run) => run.id).join(",")}:${props.activeTab.activeResultRunId ?? ""}`,
|
||||
() => {
|
||||
nextTick(updateResultTabsScrollbar);
|
||||
nextTick(() => {
|
||||
updateResultTabsScrollbar();
|
||||
resultTabsScrollerRef.value?.querySelector<HTMLElement>('[data-active-result-run="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
});
|
||||
},
|
||||
);
|
||||
const summaryItems = computed(() => executionSummaryItems(props.activeTab));
|
||||
@@ -758,15 +776,44 @@ async function exportResultArchive() {
|
||||
async function removeResultRun(runId: string) {
|
||||
const removedActiveRun = props.activeTab.activeResultRunId === runId;
|
||||
const removed = await queryStore.removeResultRun(props.activeTab.id, runId);
|
||||
if (removed && removedActiveRun) emit("update:activeOutputView", "result");
|
||||
if (!removed) return;
|
||||
if (removedActiveRun) emit("update:activeOutputView", "result");
|
||||
await nextTick();
|
||||
const activeRunTab = resultTabsScrollerRef.value?.querySelector<HTMLElement>('[data-active-result-run="true"]');
|
||||
activeRunTab?.focus({ preventScroll: true });
|
||||
activeRunTab?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
}
|
||||
|
||||
async function selectResultRun(runId: string) {
|
||||
if (!(await queryStore.setActiveResultRun(props.activeTab.id, runId))) {
|
||||
toast(t("tabs.missingResultRun"), 4000);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
emit("update:activeOutputView", "result");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function focusResultRunByIndex(index: number) {
|
||||
const run = resultRuns.value[index];
|
||||
if (!run) return;
|
||||
if (!(await selectResultRun(run.id))) return;
|
||||
await nextTick();
|
||||
const runTabs = resultTabsScrollerRef.value?.querySelectorAll<HTMLElement>("[data-result-run-tab]");
|
||||
const runTab = runTabs?.[index];
|
||||
runTab?.focus({ preventScroll: true });
|
||||
runTab?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
}
|
||||
|
||||
function onResultRunTabKeydown(event: KeyboardEvent, currentIndex: number) {
|
||||
const lastIndex = resultRuns.value.length - 1;
|
||||
let targetIndex: number | undefined;
|
||||
if (event.key === "ArrowLeft") targetIndex = currentIndex > 0 ? currentIndex - 1 : lastIndex;
|
||||
if (event.key === "ArrowRight") targetIndex = currentIndex < lastIndex ? currentIndex + 1 : 0;
|
||||
if (event.key === "Home") targetIndex = 0;
|
||||
if (event.key === "End") targetIndex = lastIndex;
|
||||
if (targetIndex === undefined || targetIndex < 0) return;
|
||||
event.preventDefault();
|
||||
void focusResultRunByIndex(targetIndex);
|
||||
}
|
||||
|
||||
function toggleResultAutoSave() {
|
||||
@@ -794,6 +841,10 @@ function requestQueryEditorExecute() {
|
||||
return queryEditorRef.value?.requestExecute();
|
||||
}
|
||||
|
||||
function requestQueryEditorExecuteInNewResultTab() {
|
||||
return queryEditorRef.value?.requestExecuteInNewResultTab();
|
||||
}
|
||||
|
||||
function pasteClipboardAsSqlInCondition() {
|
||||
return queryEditorRef.value?.pasteClipboardAsSqlInCondition();
|
||||
}
|
||||
@@ -812,7 +863,7 @@ async function executeRedisCommand(command: string): Promise<boolean> {
|
||||
return (await redisKeyBrowserRef.value?.executeCommand?.(command)) ?? false;
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, handleModRTarget, requestQueryEditorExecute, pasteClipboardAsSqlInCondition, applyTableStructureChanges, insertRedisCommand, executeRedisCommand });
|
||||
defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, handleModRTarget, requestQueryEditorExecute, requestQueryEditorExecuteInNewResultTab, pasteClipboardAsSqlInCondition, applyTableStructureChanges, insertRedisCommand, executeRedisCommand });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -860,6 +911,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
@selection-state-change="emit('editorSelectionStateChange', activeTab.id, $event)"
|
||||
@format-error="emit('formatError')"
|
||||
@execute="emit('execute', $event)"
|
||||
@execute-in-new-result-tab="emit('executeInNewResultTab', $event)"
|
||||
@save="emit('saveSql')"
|
||||
@click-table="onHandleClickTable"
|
||||
@view-table-data="onHandleViewTableData"
|
||||
@@ -892,47 +944,82 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
>
|
||||
<Pin class="h-3.5 w-3.5" :class="{ 'fill-current': resultAutoSave }" />
|
||||
</Button>
|
||||
<template v-if="showResultRunSelector">
|
||||
<template v-if="resultRuns.length > 0 || visibleResultItems.length > 0">
|
||||
<span class="mx-1 h-4 w-px shrink-0 bg-border" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-6 shrink-0 gap-1 px-2 text-xs">
|
||||
{{ activeResultRunItem ? t("tabs.runN", { n: activeResultRunItem.sequence }) : t("tabs.resultRuns") }}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" class="w-48">
|
||||
<DropdownMenuItem v-for="run in resultRuns" :key="run.id" class="flex items-center gap-2 pr-1" @select="selectResultRun(run.id)">
|
||||
<Check v-if="run.active" class="h-3.5 w-3.5 shrink-0" />
|
||||
<span v-else class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ t("tabs.runN", { n: run.sequence }) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
:title="t('tabs.removeRun', { n: run.sequence })"
|
||||
:aria-label="t('tabs.removeRun', { n: run.sequence })"
|
||||
@click.stop.prevent="removeResultRun(run.id)"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
<template v-if="visibleResultItems.length > 0">
|
||||
<span class="mx-1 h-4 w-px shrink-0 bg-border" />
|
||||
<div class="relative min-w-0 flex-1 self-stretch">
|
||||
<div v-if="showResultRunTabs" data-result-run-tabs-region class="relative min-w-0 flex-1 self-stretch">
|
||||
<div v-if="hasResultTabOverflow" class="result-tab-scrollbar" :class="{ 'result-tab-scrollbar--dragging': isResultTabsScrollbarDragging }" @pointerdown="startResultTabsScrollbarDrag">
|
||||
<div class="result-tab-scrollbar__thumb" :style="resultTabsScrollbarThumbStyle" />
|
||||
</div>
|
||||
<div ref="resultTabsScrollerRef" class="result-tab-scroll flex h-full items-center gap-1 overflow-x-auto overflow-y-hidden px-1" :style="resultTabsScrollerStyle" @scroll="updateResultTabsScrollbar" @wheel="onResultTabsWheel">
|
||||
<LightTooltip v-for="item in visibleResultItems" :key="item.index" :text="item.label || item.title || t('tabs.resultN', { n: item.n })" :disabled="!item.labelTruncated && !(!item.label && item.title)" :delay="150" :close-delay="0" nowrap>
|
||||
<Button size="sm" :variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'" class="h-6 min-w-0 max-w-48 shrink-0 px-2 text-xs" :aria-label="item.label || t('tabs.resultN', { n: item.n })" @click="selectResultItem(item)">
|
||||
<span class="block min-w-0 max-w-44 whitespace-nowrap">{{ item.displayLabel || item.label || t("tabs.resultN", { n: item.n }) }}</span>
|
||||
</Button>
|
||||
</LightTooltip>
|
||||
<div role="tablist" :aria-label="t('tabs.resultRuns')" class="flex h-full shrink-0 items-center gap-1">
|
||||
<div
|
||||
v-for="(run, runIndex) in resultRuns"
|
||||
:key="run.id"
|
||||
role="presentation"
|
||||
class="group/result-run inline-flex h-7 shrink-0 items-center overflow-hidden rounded-md border transition-colors"
|
||||
:class="run.active ? 'border-border bg-background text-foreground shadow-sm' : 'border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/70 hover:text-foreground'"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
data-result-run-tab
|
||||
:tabindex="run.active ? 0 : -1"
|
||||
:aria-selected="run.active"
|
||||
:data-active-result-run="run.active ? 'true' : undefined"
|
||||
class="h-full whitespace-nowrap pl-2.5 pr-1 text-xs font-medium outline-none focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50"
|
||||
@click="selectResultRun(run.id)"
|
||||
@keydown="onResultRunTabKeydown($event, runIndex)"
|
||||
>
|
||||
{{ t("tabs.runN", { n: run.sequence }) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mr-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/70 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
:title="t('tabs.removeRun', { n: run.sequence })"
|
||||
:aria-label="t('tabs.removeRun', { n: run.sequence })"
|
||||
@click.stop.prevent="removeResultRun(run.id)"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="showResultRunSelector" class="min-w-0 flex-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-6 max-w-48 gap-1 px-2 text-xs">
|
||||
<span class="min-w-0 truncate">{{ activeResultRunItem ? t("tabs.runN", { n: activeResultRunItem.sequence }) : t("tabs.resultRuns") }}</span>
|
||||
<ChevronDown class="h-3.5 w-3.5 shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" class="w-48">
|
||||
<DropdownMenuItem v-for="run in resultRuns" :key="run.id" class="flex items-center gap-2 pr-1" @select="selectResultRun(run.id)">
|
||||
<Check v-if="run.active" class="h-3.5 w-3.5 shrink-0" />
|
||||
<span v-else class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ t("tabs.runN", { n: run.sequence }) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
:title="t('tabs.removeRun', { n: run.sequence })"
|
||||
:aria-label="t('tabs.removeRun', { n: run.sequence })"
|
||||
@click.stop.prevent="removeResultRun(run.id)"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div v-else-if="resultRuns.length > 0" class="min-w-0 flex-1" />
|
||||
<span v-if="resultRuns.length > 0 && visibleResultItems.length > 0" class="mx-1 h-4 w-px shrink-0 bg-border" />
|
||||
<div v-if="visibleResultItems.length > 0" data-result-set-tabs-region role="group" :aria-label="t('tabs.resultSets')" class="flex h-full min-w-0 items-center gap-1" :class="resultRuns.length > 0 ? 'shrink-0' : 'flex-1 overflow-x-auto'">
|
||||
<LightTooltip v-for="item in visibleResultItems" :key="item.index" :text="item.label || item.title || t('tabs.resultN', { n: item.n })" :disabled="!item.labelTruncated && !(!item.label && item.title)" :delay="150" :close-delay="0" nowrap>
|
||||
<Button size="sm" :variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'" class="h-6 min-w-0 max-w-48 shrink-0 px-2 text-xs" :aria-label="item.label || t('tabs.resultN', { n: item.n })" @click="selectResultItem(item)">
|
||||
<span class="block min-w-0 max-w-44 whitespace-nowrap">{{ item.displayLabel || item.label || t("tabs.resultN", { n: item.n }) }}</span>
|
||||
</Button>
|
||||
</LightTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1">
|
||||
<Popover v-if="activeOutputView === 'result' && activeTab.result && hasTabularResult && !activeElasticsearchJsonResponse">
|
||||
@@ -945,6 +1032,30 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
<div class="border-b bg-muted/40 px-3 py-2">
|
||||
<div class="text-xs font-semibold">{{ t("grid.viewOptions") }}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
|
||||
<div class="min-w-0 flex items-center gap-2 font-medium">
|
||||
<PanelsTopLeft class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span>{{ t("grid.resultRunDisplayMode") }}</span>
|
||||
</div>
|
||||
<div role="group" :aria-label="t('grid.resultRunDisplayMode')" class="grid w-32 grid-cols-2 rounded-md border bg-muted/40 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors"
|
||||
:class="resultRunDisplayMode === 'list' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="setResultRunDisplayMode('list')"
|
||||
>
|
||||
{{ t("grid.resultRunDisplayList") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-5 min-w-0 truncate whitespace-nowrap rounded-[5px] px-2 text-xs transition-colors"
|
||||
:class="resultRunDisplayMode === 'tabs' ? 'bg-background font-semibold text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="setResultRunDisplayMode('tabs')"
|
||||
>
|
||||
{{ t("grid.resultRunDisplayTabs") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
|
||||
<div class="min-w-0 flex items-center gap-2 font-medium">
|
||||
<SquareDashed class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
|
||||
Reference in new issue
Block a user