feat(results): add execution summary view
This commit is contained in:
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}`"
|
||||
|
||||
Reference in new issue
Block a user