feat(results): add execution summary view

This commit is contained in:
t8y2 committed 2026-06-08 14:13:47 +08:00
1 parent f1a94262b5
commit 2a3844f2c6
7 files changed
+231 -23

No files matched your search

@@ -17,6 +17,7 @@ import {
Inbox,
RefreshCcw,
Wrench,
ListChecks,
} from "@lucide/vue";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
@@ -54,7 +55,7 @@ const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explai
const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue"));
import { useQueryStore } from "@/stores/queryStore";
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState";
import { databaseDisplayNameForTab } from "@/lib/tabPresentation";
import { databaseDisplayNameForTab, executionSummaryItems, tabularResultItems } from "@/lib/tabPresentation";
import { isTableDataEditable } from "@/lib/tableEditing";
import { tableMetaForDataTab } from "@/lib/tableDataTabMeta";
import { formatShortcut } from "@/lib/shortcutRegistry";
@@ -90,14 +91,14 @@ const props = defineProps<{
activeTab: QueryTab;
activeConnection?: ConnectionConfig;
executableSql: string;
activeOutputView: "result" | "explain" | "chart";
activeOutputView: "result" | "summary" | "explain" | "chart";
formatSqlRequestId: number;
selectedSql: string;
cursorPos: number;
}>();
const emit = defineEmits<{
"update:activeOutputView": [value: "result" | "explain" | "chart"];
"update:activeOutputView": [value: "result" | "summary" | "explain" | "chart"];
fixWithAi: [errorMessage: string];
execute: [sqlOverride?: string];
saveSql: [];
@@ -207,6 +208,13 @@ const hasQueryOutput = computed(
props.activeTab.isExecuting === true ||
props.activeTab.isExplaining === true,
);
const tabularResults = computed(() => tabularResultItems(props.activeTab.results));
const summaryItems = computed(() => executionSummaryItems(props.activeTab));
const hasExecutionSummary = computed(() => summaryItems.value.length > 0 || props.activeTab.isExecuting);
const hasTabularResult = computed(() => {
if (props.activeTab.result?.columns.length) return true;
return tabularResults.value.length > 0;
});
const resultsPaneOpen = ref(false);
watch(
@@ -224,6 +232,17 @@ watch(
},
);
watch(
() => [props.activeTab.id, props.activeTab.result, props.activeTab.results, props.activeTab.isExecuting] as const,
() => {
if (props.activeTab.isExecuting) return;
if (hasExecutionSummary.value && !hasTabularResult.value && props.activeOutputView === "result") {
emit("update:activeOutputView", "summary");
}
},
{ immediate: true },
);
watch(
() => [props.activeTab.isExecuting, props.activeTab.isExplaining],
([isExecuting, isExplaining]) => {
@@ -433,33 +452,38 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
size="sm"
:variant="activeOutputView === 'result' ? 'secondary' : 'ghost'"
class="h-6 px-2 text-xs"
:disabled="!activeTab.result && !activeTab.isExecuting"
:disabled="!hasTabularResult && !activeTab.isExecuting"
@click="emit('update:activeOutputView', 'result')"
>
{{ t("tabs.tableData") }}
</Button>
<template v-if="activeOutputView === 'result' && activeTab.results && activeTab.results.length > 1">
<template v-if="tabularResults.length > 1">
<span class="mx-1 h-4 w-px bg-border" />
<Button
v-for="(_, rIdx) in activeTab.results"
:key="rIdx"
v-for="item in tabularResults"
:key="item.index"
size="sm"
:variant="activeTab.activeResultIndex === rIdx ? 'default' : 'ghost'"
:variant="
activeOutputView === 'result' && activeTab.activeResultIndex === item.index ? 'default' : 'ghost'
"
class="h-6 px-2 text-xs shrink-0"
@click="queryStore.setActiveResultIndex(activeTab.id, rIdx)"
@click="
queryStore.setActiveResultIndex(activeTab.id, item.index);
emit('update:activeOutputView', 'result');
"
>
{{ t("tabs.resultN", { n: rIdx + 1 }) }}
{{ t("tabs.resultN", { n: item.n }) }}
</Button>
</template>
<Button
size="sm"
:variant="activeOutputView === 'explain' ? 'secondary' : 'ghost'"
:variant="activeOutputView === 'summary' ? 'secondary' : 'ghost'"
class="h-6 px-2 text-xs gap-1"
:disabled="!activeTab.explainPlan && !activeTab.explainError && !activeTab.isExplaining"
@click="emit('update:activeOutputView', 'explain')"
:disabled="!hasExecutionSummary"
@click="emit('update:activeOutputView', 'summary')"
>
<GitBranch class="h-3.5 w-3.5" />
{{ t("explain.title") }}
<ListChecks class="h-3.5 w-3.5" />
{{ t("tabs.executionSummary") }}
</Button>
<Button
size="sm"
@@ -471,6 +495,17 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
<BarChart3 class="h-3.5 w-3.5" />
{{ t("chart.title") }}
</Button>
<span class="mx-1 h-4 w-px shrink-0 bg-border" />
<Button
size="sm"
:variant="activeOutputView === 'explain' ? 'secondary' : 'ghost'"
class="h-6 px-2 text-xs gap-1"
:disabled="!activeTab.explainPlan && !activeTab.explainError && !activeTab.isExplaining"
@click="emit('update:activeOutputView', 'explain')"
>
<GitBranch class="h-3.5 w-3.5" />
{{ t("explain.title") }}
</Button>
<Popover v-if="activeOutputView === 'result' && activeTab.result">
<PopoverTrigger as-child>
<Button
@@ -517,7 +552,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
</PopoverContent>
</Popover>
<Button
v-if="activeOutputView === 'result' && activeTab.result"
v-if="activeOutputView === 'result' && hasTabularResult"
variant="ghost"
size="sm"
class="h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
@@ -532,7 +567,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
variant="ghost"
size="sm"
class="h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
:class="{ 'ml-auto': activeOutputView !== 'result' || !activeTab.result }"
:class="{ 'ml-auto': activeOutputView !== 'result' || !hasTabularResult }"
@click="resultsPaneOpen = false"
>
<ChevronDown class="h-3.5 w-3.5" />
@@ -556,9 +591,67 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
:result="activeTab.result"
/>
<div v-else-if="activeOutputView === 'summary'" class="flex-1 min-h-0 overflow-auto bg-background">
<div
v-if="activeTab.isExecuting"
class="flex h-full items-center justify-center text-sm text-muted-foreground"
>
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{{ t("executionSummary.executing") }}
</div>
<div
v-else-if="summaryItems.length === 0"
class="flex h-full items-center justify-center text-sm text-muted-foreground"
>
{{ t("executionSummary.empty") }}
</div>
<div v-else>
<div class="overflow-hidden border-b">
<div
class="grid grid-cols-[4rem_1fr_8rem_8rem_7rem] border-b bg-muted/30 px-3 py-2 text-xs font-medium text-muted-foreground"
>
<div>{{ t("executionSummary.statement") }}</div>
<div>{{ t("executionSummary.type") }}</div>
<div class="text-right">{{ t("executionSummary.rows") }}</div>
<div class="text-right">{{ t("executionSummary.affected") }}</div>
<div class="text-right">{{ t("executionSummary.time") }}</div>
</div>
<div
v-for="item in summaryItems"
:key="item.index"
class="grid grid-cols-[4rem_1fr_8rem_8rem_7rem] items-center border-b px-3 py-2 text-xs last:border-b-0"
>
<div class="font-mono text-muted-foreground">#{{ item.index + 1 }}</div>
<div class="flex min-w-0 items-center gap-2">
<span
class="inline-flex h-5 items-center rounded-full border px-2 text-[10px]"
:class="
item.isError
? 'border-destructive/40 bg-destructive/10 text-destructive'
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
"
>
{{ item.isError ? t("executionSummary.error") : t("executionSummary.success") }}
</span>
<span class="truncate">
{{
item.hasTabularResult
? t("executionSummary.returnedTable", { count: item.returnedColumns })
: t("executionSummary.noTable")
}}
</span>
</div>
<div class="text-right tabular-nums">{{ item.returnedRows.toLocaleString() }}</div>
<div class="text-right tabular-nums">{{ item.affectedRows.toLocaleString() }}</div>
<div class="text-right tabular-nums">{{ item.executionTimeMs }}ms</div>
</div>
</div>
</div>
</div>
<template v-else>
<DataGrid
v-if="activeTab.result"
v-if="activeTab.result && hasTabularResult"
ref="dataGridRef"
:key="`${activeTab.id}-${activeTab.activeResultIndex ?? 0}`"
:cache-key="`${activeTab.id}-${activeTab.activeResultIndex ?? 0}`"