feat(query): add MySQL standard explain table view
This commit is contained in:
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" />
|
||||
|
||||
|
||||
Reference in new issue
Block a user