feat(query): add MySQL standard explain table view

This commit is contained in:
onenewcode authored and GitHub committed 2026-07-11 20:34:54 +08:00
1 parent 830e3c2183
commit 54632f4c16
16 files changed
+480 -28

No files matched your search

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { AlertCircle, Braces, GitBranch, Table2, FileText } from "@lucide/vue";
import type { ParsedExplainPlan, ExplainPlanNode } from "@/lib/diagram/explainPlan";
import { flattenExplainPlanNodes } from "@/lib/diagram/explainPlan";
import { Button } from "@/components/ui/button";
import type { QueryResult } from "@/types/database";
import ExplainPlanNodeTree from "./ExplainPlanNodeTree.vue";
const props = defineProps<{
@@ -13,10 +14,17 @@ const props = defineProps<{
loading?: boolean;
sourceSql?: string;
explainSql?: string;
tableResult?: QueryResult;
tableError?: string;
}>();
const { t } = useI18n();
const activeView = ref<"tree" | "summary" | "raw">("tree");
const activeView = ref<"tree" | "summary" | "raw" | "table">("tree");
const hasTableView = computed(() => !!props.tableResult || !!props.tableError);
watch(hasTableView, (available) => {
if (!available && activeView.value === "table") activeView.value = "tree";
});
const flatRows = computed(() => {
const rows: Array<{ node: ExplainPlanNode; depth: number }> = [];
@@ -38,6 +46,11 @@ const rawContent = computed(() => {
const isRawString = computed(() => typeof props.plan?.raw === "string");
const nodeCount = computed(() => (props.plan ? flattenExplainPlanNodes(props.plan.nodes).length : 0));
function tableCellText(value: unknown): string {
if (value === null) return "NULL";
return value === undefined ? "" : String(value);
}
</script>
<template>
@@ -47,30 +60,64 @@ const nodeCount = computed(() => (props.plan ? flattenExplainPlanNodes(props.pla
<GitBranch class="h-3.5 w-3.5" />
{{ t("explain.title") }}
</span>
<span v-if="plan" class="text-muted-foreground">{{ plan.databaseType.toUpperCase() }} · {{ t("explain.nodeCount", { count: nodeCount }) }}</span>
<span v-if="plan || hasTableView" class="text-muted-foreground">
{{ plan?.databaseType.toUpperCase() || "MYSQL" }}<template v-if="plan"> · {{ t("explain.nodeCount", { count: nodeCount }) }}</template>
</span>
<span v-if="plan?.databaseType === 'dameng' && isRawString && rawContent.includes('->')" class="ml-1 inline-flex items-center gap-1 rounded bg-green-100 px-1.5 py-0.5 font-semibold text-green-700 dark:bg-green-900/30 dark:text-green-300" style="font-size: 10px">A-TRACE</span>
<span class="flex-1" />
<div v-if="plan" class="inline-flex rounded-md border bg-muted/40 p-0.5">
<Button size="sm" :variant="activeView === 'tree' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'tree'">
<div v-if="plan || hasTableView" class="inline-flex rounded-md border bg-muted/40 p-0.5">
<Button v-if="plan" size="sm" :variant="activeView === 'tree' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'tree'">
<GitBranch class="h-3.5 w-3.5" />
{{ t("explain.tree") }}
</Button>
<Button size="sm" :variant="activeView === 'summary' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'summary'">
<Button v-if="plan" size="sm" :variant="activeView === 'summary' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'summary'">
<Table2 class="h-3.5 w-3.5" />
{{ t("explain.summary") }}
</Button>
<Button size="sm" :variant="activeView === 'raw' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'raw'">
<Button v-if="plan" size="sm" :variant="activeView === 'raw' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'raw'">
<FileText v-if="isRawString" class="h-3.5 w-3.5" />
<Braces v-else class="h-3.5 w-3.5" />
{{ isRawString ? "TEXT" : "JSON" }}
</Button>
<Button v-if="hasTableView" size="sm" :variant="activeView === 'table' ? 'secondary' : 'ghost'" class="h-6 px-2 text-xs gap-1" @click="activeView = 'table'">
<Table2 class="h-3.5 w-3.5" />
{{ t("explain.standardTable") }}
</Button>
</div>
</div>
<div v-if="loading" class="flex-1 min-h-0 flex items-center justify-center text-sm text-muted-foreground">
<div v-if="loading && !(activeView === 'table' && hasTableView)" class="flex-1 min-h-0 flex items-center justify-center text-sm text-muted-foreground">
{{ t("explain.running") }}
</div>
<div v-else-if="activeView === 'table' && hasTableView" class="flex-1 min-h-0 overflow-auto">
<div v-if="tableError" class="flex h-full min-h-0 items-center justify-center p-3">
<div class="flex max-w-xl items-start gap-2 rounded border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive">
<AlertCircle class="mt-0.5 h-4 w-4 shrink-0" />
<span>{{ tableError }}</span>
</div>
</div>
<div v-else-if="tableResult" class="p-3">
<div class="overflow-x-auto rounded border">
<table class="min-w-full w-max text-left text-xs">
<thead class="bg-muted/70 text-muted-foreground">
<tr>
<th v-for="(column, columnIndex) in tableResult.columns" :key="columnIndex" class="whitespace-nowrap px-2 py-1.5 font-medium">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in tableResult.rows" :key="rowIndex" class="border-t">
<td v-for="(_column, columnIndex) in tableResult.columns" :key="columnIndex" class="whitespace-nowrap px-2 py-1.5 text-muted-foreground">
{{ tableCellText(row[columnIndex]) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div v-else-if="error" class="flex-1 min-h-0 flex items-center justify-center">
<div class="flex max-w-xl items-start gap-2 rounded border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive">
<AlertCircle class="mt-0.5 h-4 w-4 shrink-0" />
@@ -297,7 +297,7 @@ const activeQueryError = computed(() => {
if (!result?.columns.includes("Error")) return "";
return String(result.rows[0]?.[0] ?? "");
});
const hasQueryOutput = computed(() => !!props.activeTab.result || !!props.activeTab.explainPlan || !!props.activeTab.explainError || props.activeTab.isExecuting === true || props.activeTab.isExplaining === true);
const hasQueryOutput = computed(() => !!props.activeTab.result || !!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));
const allResultExportSheets = computed(() =>
@@ -1001,7 +1001,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
:class="resultToolbarCompact ? 'w-7 gap-0 px-0' : 'gap-1 px-2'"
:title="t('explain.title')"
:aria-label="t('explain.title')"
:disabled="!activeTab.explainPlan && !activeTab.explainError && !activeTab.isExplaining"
:disabled="!activeTab.explainPlan && !activeTab.explainError && !activeTab.explainTableResult && !activeTab.explainTableError && !activeTab.isExplaining"
@click="emit('update:activeOutputView', 'explain')"
>
<GitBranch class="h-3.5 w-3.5" />
@@ -1234,7 +1234,17 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
</div>
</div>
<ExplainPlanViewer v-if="activeOutputView === 'explain'" class="flex-1 min-h-0" :plan="activeTab.explainPlan" :error="activeTab.explainError" :loading="activeTab.isExplaining" :source-sql="activeTab.lastExplainedSql" :explain-sql="activeTab.explainSql" />
<ExplainPlanViewer
v-if="activeOutputView === 'explain'"
class="flex-1 min-h-0"
:plan="activeTab.explainPlan"
:error="activeTab.explainError"
:loading="activeTab.isExplaining"
:source-sql="activeTab.lastExplainedSql"
:explain-sql="activeTab.explainSql"
:table-result="activeTab.explainTableResult"
:table-error="activeTab.explainTableError"
/>
<QueryChart v-else-if="activeOutputView === 'chart' && activeTab.result" class="flex-1 min-h-0" :result="activeTab.result" />