@tabularis/explain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/dist/diagnostics-DPSgsjFk.d.ts +157 -0
- package/dist/flow.d.ts +40 -0
- package/dist/flow.js +171 -0
- package/dist/index.d.ts +325 -0
- package/dist/index.js +1792 -0
- package/dist/react.d.ts +91 -0
- package/dist/react.js +1617 -0
- package/package.json +95 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,1617 @@
|
|
|
1
|
+
import { memo, useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Handle, Position, ReactFlowProvider, useReactFlow, useNodesState, useEdgesState, ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
|
|
3
|
+
import '@xyflow/react/dist/style.css';
|
|
4
|
+
import dagre from 'dagre';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Network, BarChart3, TableProperties, PieChart, FileText, Sparkles, CircleSlash, DatabaseZap, Repeat, Users, Layers, ScanSearch, Filter, HardDrive, TrendingDown, TrendingUp, Flame, Info, AlertTriangle, ChevronDown, ChevronRight, Layers2, Clock3, Database, LayoutDashboard, BookOpenText } from 'lucide-react';
|
|
7
|
+
import clsx from 'clsx';
|
|
8
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
// src/components/ExplainGraph.tsx
|
|
11
|
+
|
|
12
|
+
// src/metrics.ts
|
|
13
|
+
var EXPLAIN_METRIC_KINDS = [
|
|
14
|
+
"time",
|
|
15
|
+
"rows",
|
|
16
|
+
"cost",
|
|
17
|
+
"buffers"
|
|
18
|
+
];
|
|
19
|
+
function loopCount(node) {
|
|
20
|
+
return node.actual_loops != null && node.actual_loops > 0 ? node.actual_loops : 1;
|
|
21
|
+
}
|
|
22
|
+
function isSubplanChild(node) {
|
|
23
|
+
const relationship = node.extra["Parent Relationship"];
|
|
24
|
+
return relationship === "InitPlan" || relationship === "SubPlan";
|
|
25
|
+
}
|
|
26
|
+
function bufferCount(node) {
|
|
27
|
+
if (node.buffers_hit == null && node.buffers_read == null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return (node.buffers_hit ?? 0) + (node.buffers_read ?? 0);
|
|
31
|
+
}
|
|
32
|
+
function selfValue(total, childSum) {
|
|
33
|
+
if (total == null) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return Math.max(0, total - childSum);
|
|
37
|
+
}
|
|
38
|
+
function computeExplainMetrics(plan) {
|
|
39
|
+
const byId = /* @__PURE__ */ new Map();
|
|
40
|
+
const order = [];
|
|
41
|
+
let nextIndex = 1;
|
|
42
|
+
function walk(node, depth) {
|
|
43
|
+
const metrics = {
|
|
44
|
+
nodeId: node.id,
|
|
45
|
+
index: nextIndex,
|
|
46
|
+
depth,
|
|
47
|
+
inclusiveTimeMs: null,
|
|
48
|
+
exclusiveTimeMs: null,
|
|
49
|
+
totalCost: node.total_cost,
|
|
50
|
+
exclusiveCost: null,
|
|
51
|
+
totalRows: null,
|
|
52
|
+
inclusiveBuffers: bufferCount(node),
|
|
53
|
+
exclusiveBuffers: null,
|
|
54
|
+
timeShare: null,
|
|
55
|
+
neverExecuted: node.actual_loops === 0
|
|
56
|
+
};
|
|
57
|
+
nextIndex += 1;
|
|
58
|
+
byId.set(node.id, metrics);
|
|
59
|
+
order.push(metrics);
|
|
60
|
+
let childTimeMs = 0;
|
|
61
|
+
let childCost = 0;
|
|
62
|
+
let childBuffers = 0;
|
|
63
|
+
for (const child of node.children) {
|
|
64
|
+
const contribution = walk(child, depth + 1);
|
|
65
|
+
childCost += contribution.cost;
|
|
66
|
+
childBuffers += contribution.buffers;
|
|
67
|
+
if (!isSubplanChild(child)) {
|
|
68
|
+
childTimeMs += contribution.timeMs;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const loops = loopCount(node);
|
|
72
|
+
metrics.inclusiveTimeMs = node.actual_time_ms != null ? node.actual_time_ms * loops : null;
|
|
73
|
+
metrics.exclusiveTimeMs = selfValue(metrics.inclusiveTimeMs, childTimeMs);
|
|
74
|
+
metrics.exclusiveCost = selfValue(metrics.totalCost, childCost);
|
|
75
|
+
metrics.exclusiveBuffers = selfValue(metrics.inclusiveBuffers, childBuffers);
|
|
76
|
+
metrics.totalRows = node.actual_rows != null ? node.actual_rows * loops : null;
|
|
77
|
+
return {
|
|
78
|
+
timeMs: metrics.inclusiveTimeMs ?? 0,
|
|
79
|
+
cost: metrics.totalCost ?? 0,
|
|
80
|
+
buffers: metrics.inclusiveBuffers ?? 0
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
walk(plan.root, 0);
|
|
84
|
+
let maxExclusiveTimeMs = 0;
|
|
85
|
+
let maxExclusiveCost = 0;
|
|
86
|
+
let maxTotalRows = 0;
|
|
87
|
+
let maxExclusiveBuffers = 0;
|
|
88
|
+
let totalExclusiveTimeMs = 0;
|
|
89
|
+
for (const metrics of order) {
|
|
90
|
+
maxExclusiveTimeMs = Math.max(
|
|
91
|
+
maxExclusiveTimeMs,
|
|
92
|
+
metrics.exclusiveTimeMs ?? 0
|
|
93
|
+
);
|
|
94
|
+
maxExclusiveCost = Math.max(maxExclusiveCost, metrics.exclusiveCost ?? 0);
|
|
95
|
+
maxTotalRows = Math.max(maxTotalRows, metrics.totalRows ?? 0);
|
|
96
|
+
maxExclusiveBuffers = Math.max(
|
|
97
|
+
maxExclusiveBuffers,
|
|
98
|
+
metrics.exclusiveBuffers ?? 0
|
|
99
|
+
);
|
|
100
|
+
totalExclusiveTimeMs += metrics.exclusiveTimeMs ?? 0;
|
|
101
|
+
}
|
|
102
|
+
if (totalExclusiveTimeMs > 0) {
|
|
103
|
+
for (const metrics of order) {
|
|
104
|
+
if (metrics.exclusiveTimeMs != null) {
|
|
105
|
+
metrics.timeShare = metrics.exclusiveTimeMs / totalExclusiveTimeMs;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
byId,
|
|
111
|
+
order,
|
|
112
|
+
maxExclusiveTimeMs,
|
|
113
|
+
maxExclusiveCost,
|
|
114
|
+
maxTotalRows,
|
|
115
|
+
maxExclusiveBuffers,
|
|
116
|
+
totalExclusiveTimeMs
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function getMetricValue(metrics, kind) {
|
|
120
|
+
switch (kind) {
|
|
121
|
+
case "time":
|
|
122
|
+
return metrics.exclusiveTimeMs;
|
|
123
|
+
case "rows":
|
|
124
|
+
return metrics.totalRows;
|
|
125
|
+
case "cost":
|
|
126
|
+
return metrics.exclusiveCost;
|
|
127
|
+
case "buffers":
|
|
128
|
+
return metrics.exclusiveBuffers;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function getMetricMax(metrics, kind) {
|
|
132
|
+
switch (kind) {
|
|
133
|
+
case "time":
|
|
134
|
+
return metrics.maxExclusiveTimeMs;
|
|
135
|
+
case "rows":
|
|
136
|
+
return metrics.maxTotalRows;
|
|
137
|
+
case "cost":
|
|
138
|
+
return metrics.maxExclusiveCost;
|
|
139
|
+
case "buffers":
|
|
140
|
+
return metrics.maxExclusiveBuffers;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isMetricAvailable(metrics, kind) {
|
|
144
|
+
return metrics.order.some((node) => getMetricValue(node, kind) != null);
|
|
145
|
+
}
|
|
146
|
+
function getAvailableMetricKinds(metrics) {
|
|
147
|
+
return EXPLAIN_METRIC_KINDS.filter((kind) => isMetricAvailable(metrics, kind));
|
|
148
|
+
}
|
|
149
|
+
function getDefaultMetricKind(metrics) {
|
|
150
|
+
if (metrics.maxExclusiveTimeMs > 0) {
|
|
151
|
+
return "time";
|
|
152
|
+
}
|
|
153
|
+
const available = getAvailableMetricKinds(metrics);
|
|
154
|
+
return available.includes("cost") ? "cost" : available[0] ?? null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/flow.ts
|
|
158
|
+
function explainPlanToFlow(plan, selectedNodeId, metrics, diagnostics) {
|
|
159
|
+
const planMetrics = metrics ?? computeExplainMetrics(plan);
|
|
160
|
+
const rawNodes = [];
|
|
161
|
+
const edges = [];
|
|
162
|
+
function walk(node) {
|
|
163
|
+
const nodeMetrics = planMetrics.byId.get(node.id);
|
|
164
|
+
const data = {
|
|
165
|
+
node,
|
|
166
|
+
metrics: nodeMetrics ?? null,
|
|
167
|
+
maxExclusiveCost: planMetrics.maxExclusiveCost,
|
|
168
|
+
maxExclusiveTimeMs: planMetrics.maxExclusiveTimeMs,
|
|
169
|
+
diagnostics: diagnostics?.get(node.id) ?? [],
|
|
170
|
+
hasAnalyzeData: plan.has_analyze_data,
|
|
171
|
+
isSelected: selectedNodeId === node.id
|
|
172
|
+
};
|
|
173
|
+
rawNodes.push({
|
|
174
|
+
id: node.id,
|
|
175
|
+
type: "explainPlan",
|
|
176
|
+
position: { x: 0, y: 0 },
|
|
177
|
+
data
|
|
178
|
+
});
|
|
179
|
+
for (const child of node.children) {
|
|
180
|
+
edges.push({
|
|
181
|
+
id: `${node.id}-${child.id}`,
|
|
182
|
+
source: node.id,
|
|
183
|
+
target: child.id,
|
|
184
|
+
animated: true,
|
|
185
|
+
style: { stroke: "#6366f1" }
|
|
186
|
+
});
|
|
187
|
+
walk(child);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
walk(plan.root);
|
|
191
|
+
return layoutExplainNodes(rawNodes, edges);
|
|
192
|
+
}
|
|
193
|
+
function layoutExplainNodes(nodes, edges) {
|
|
194
|
+
const g = new dagre.graphlib.Graph();
|
|
195
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
196
|
+
g.setGraph({ rankdir: "TB", ranksep: 80, nodesep: 40 });
|
|
197
|
+
const NODE_WIDTH = 280;
|
|
198
|
+
for (const node of nodes) {
|
|
199
|
+
const data = node.data;
|
|
200
|
+
const lines = 3 + (data.hasAnalyzeData ? 2 : 0) + (data.node.filter ? 1 : 0) + (data.diagnostics.length > 0 ? 1 : 0);
|
|
201
|
+
const height = 28 + lines * 22;
|
|
202
|
+
g.setNode(node.id, { width: NODE_WIDTH, height });
|
|
203
|
+
}
|
|
204
|
+
for (const edge of edges) {
|
|
205
|
+
g.setEdge(edge.source, edge.target);
|
|
206
|
+
}
|
|
207
|
+
dagre.layout(g);
|
|
208
|
+
const layoutedNodes = nodes.map((node) => {
|
|
209
|
+
const pos = g.node(node.id);
|
|
210
|
+
return {
|
|
211
|
+
...node,
|
|
212
|
+
position: {
|
|
213
|
+
x: pos.x - NODE_WIDTH / 2,
|
|
214
|
+
y: pos.y - pos.height / 2
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
return { nodes: layoutedNodes, edges };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/plan.ts
|
|
222
|
+
function getNodeCostStyle(cost, maxCost) {
|
|
223
|
+
if (maxCost <= 0) return { border: "border-l-green-500", headerBg: "bg-green-950/30" };
|
|
224
|
+
const ratio = cost / maxCost;
|
|
225
|
+
if (ratio < 0.2) return { border: "border-l-green-500", headerBg: "bg-green-950/30" };
|
|
226
|
+
if (ratio < 0.6) return { border: "border-l-yellow-500", headerBg: "bg-yellow-950/30" };
|
|
227
|
+
return { border: "border-l-red-500", headerBg: "bg-red-950/30" };
|
|
228
|
+
}
|
|
229
|
+
function getHeatBarClass(value, max) {
|
|
230
|
+
if (max <= 0) return "bg-green-500/70";
|
|
231
|
+
const ratio = value / max;
|
|
232
|
+
if (ratio < 0.2) return "bg-green-500/70";
|
|
233
|
+
if (ratio < 0.6) return "bg-yellow-500/70";
|
|
234
|
+
return "bg-red-500/70";
|
|
235
|
+
}
|
|
236
|
+
function formatCost(n) {
|
|
237
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
238
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
239
|
+
if (n >= 100) return n.toFixed(0);
|
|
240
|
+
if (n >= 1) return n.toFixed(1);
|
|
241
|
+
return n.toFixed(2);
|
|
242
|
+
}
|
|
243
|
+
function formatTime(ms) {
|
|
244
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)} s`;
|
|
245
|
+
if (ms >= 1) return `${ms.toFixed(2)} ms`;
|
|
246
|
+
return `${(ms * 1e3).toFixed(0)} us`;
|
|
247
|
+
}
|
|
248
|
+
function formatRows(n) {
|
|
249
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
250
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
251
|
+
return n.toFixed(0);
|
|
252
|
+
}
|
|
253
|
+
function formatRatio(n) {
|
|
254
|
+
if (n >= 100) return `${n.toFixed(0)}x`;
|
|
255
|
+
if (n >= 10) return `${n.toFixed(1)}x`;
|
|
256
|
+
return `${n.toFixed(2)}x`;
|
|
257
|
+
}
|
|
258
|
+
function getMaxCost(node) {
|
|
259
|
+
let max = node.total_cost ?? 0;
|
|
260
|
+
for (const child of node.children) {
|
|
261
|
+
const childMax = getMaxCost(child);
|
|
262
|
+
if (childMax > max) max = childMax;
|
|
263
|
+
}
|
|
264
|
+
return max;
|
|
265
|
+
}
|
|
266
|
+
function flattenExplainNodes(root) {
|
|
267
|
+
const nodes = [];
|
|
268
|
+
function walk(node) {
|
|
269
|
+
nodes.push(node);
|
|
270
|
+
for (const child of node.children) {
|
|
271
|
+
walk(child);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
walk(root);
|
|
275
|
+
return nodes;
|
|
276
|
+
}
|
|
277
|
+
function findExplainNode(root, nodeId) {
|
|
278
|
+
if (!nodeId) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (root.id === nodeId) {
|
|
282
|
+
return root;
|
|
283
|
+
}
|
|
284
|
+
for (const child of root.children) {
|
|
285
|
+
const found = findExplainNode(child, nodeId);
|
|
286
|
+
if (found) {
|
|
287
|
+
return found;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function getRowEstimateRatio(node) {
|
|
293
|
+
if (node.plan_rows == null || node.actual_rows == null || node.plan_rows <= 0 || node.actual_rows <= 0) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return node.actual_rows / node.plan_rows;
|
|
297
|
+
}
|
|
298
|
+
function getMismatchMagnitude(node) {
|
|
299
|
+
const ratio = getRowEstimateRatio(node);
|
|
300
|
+
if (ratio == null) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return ratio >= 1 ? ratio : 1 / ratio;
|
|
304
|
+
}
|
|
305
|
+
function isSequentialScan(node) {
|
|
306
|
+
const normalizedType = node.node_type.toLowerCase();
|
|
307
|
+
const accessType = typeof node.extra.access_type === "string" ? node.extra.access_type.toLowerCase() : "";
|
|
308
|
+
return normalizedType.includes("seq scan") || normalizedType.includes("table scan") || normalizedType.includes("full scan") || accessType === "all";
|
|
309
|
+
}
|
|
310
|
+
function isTempOperation(node) {
|
|
311
|
+
const normalizedType = node.node_type.toLowerCase();
|
|
312
|
+
const extraText = Object.values(node.extra).filter((value) => typeof value === "string").join(" ").toLowerCase();
|
|
313
|
+
return normalizedType.includes("sort") || normalizedType.includes("filesort") || normalizedType.includes("temporary") || extraText.includes("using temporary") || extraText.includes("using filesort");
|
|
314
|
+
}
|
|
315
|
+
function getExplainPlanSummary(plan, metrics) {
|
|
316
|
+
const nodes = flattenExplainNodes(plan.root);
|
|
317
|
+
const planMetrics = metrics ?? computeExplainMetrics(plan);
|
|
318
|
+
let highestCostNode = null;
|
|
319
|
+
let slowestNode = null;
|
|
320
|
+
let largestRowMismatchNode = null;
|
|
321
|
+
let sequentialScans = 0;
|
|
322
|
+
let tempOperations = 0;
|
|
323
|
+
for (const node of nodes) {
|
|
324
|
+
const nodeMetrics = planMetrics.byId.get(node.id);
|
|
325
|
+
const exclusiveCost = nodeMetrics?.exclusiveCost ?? null;
|
|
326
|
+
const exclusiveTimeMs = nodeMetrics?.exclusiveTimeMs ?? null;
|
|
327
|
+
if (exclusiveCost != null && (highestCostNode == null || exclusiveCost > highestCostNode.value)) {
|
|
328
|
+
highestCostNode = {
|
|
329
|
+
nodeId: node.id,
|
|
330
|
+
nodeType: node.node_type,
|
|
331
|
+
relation: node.relation,
|
|
332
|
+
value: exclusiveCost
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (exclusiveTimeMs != null && (slowestNode == null || exclusiveTimeMs > slowestNode.value)) {
|
|
336
|
+
slowestNode = {
|
|
337
|
+
nodeId: node.id,
|
|
338
|
+
nodeType: node.node_type,
|
|
339
|
+
relation: node.relation,
|
|
340
|
+
value: exclusiveTimeMs
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const ratio = getRowEstimateRatio(node);
|
|
344
|
+
const magnitude = getMismatchMagnitude(node);
|
|
345
|
+
if (ratio != null && magnitude != null && (largestRowMismatchNode == null || magnitude > largestRowMismatchNode.value)) {
|
|
346
|
+
largestRowMismatchNode = {
|
|
347
|
+
nodeId: node.id,
|
|
348
|
+
nodeType: node.node_type,
|
|
349
|
+
relation: node.relation,
|
|
350
|
+
value: magnitude,
|
|
351
|
+
ratio
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (isSequentialScan(node)) {
|
|
355
|
+
sequentialScans += 1;
|
|
356
|
+
}
|
|
357
|
+
if (isTempOperation(node)) {
|
|
358
|
+
tempOperations += 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
highestCostNode,
|
|
363
|
+
slowestNode,
|
|
364
|
+
largestRowMismatchNode,
|
|
365
|
+
sequentialScans,
|
|
366
|
+
tempOperations
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function getExplainDriverLegend(plan) {
|
|
370
|
+
switch (plan.driver) {
|
|
371
|
+
case "postgres":
|
|
372
|
+
return plan.has_analyze_data ? [
|
|
373
|
+
"editor.visualExplain.postgresAnalyzeLegend1",
|
|
374
|
+
"editor.visualExplain.postgresAnalyzeLegend2"
|
|
375
|
+
] : [
|
|
376
|
+
"editor.visualExplain.postgresEstimateLegend1",
|
|
377
|
+
"editor.visualExplain.postgresEstimateLegend2"
|
|
378
|
+
];
|
|
379
|
+
case "mysql":
|
|
380
|
+
return plan.has_analyze_data ? [
|
|
381
|
+
"editor.visualExplain.mysqlAnalyzeLegend1",
|
|
382
|
+
"editor.visualExplain.mysqlAnalyzeLegend2"
|
|
383
|
+
] : [
|
|
384
|
+
"editor.visualExplain.mysqlEstimateLegend1",
|
|
385
|
+
"editor.visualExplain.mysqlEstimateLegend2"
|
|
386
|
+
];
|
|
387
|
+
case "sqlite":
|
|
388
|
+
return [
|
|
389
|
+
"editor.visualExplain.sqliteLegend1",
|
|
390
|
+
"editor.visualExplain.sqliteLegend2"
|
|
391
|
+
];
|
|
392
|
+
default:
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
var KIND_ICONS = {
|
|
397
|
+
hotspot: Flame,
|
|
398
|
+
"over-estimate": TrendingUp,
|
|
399
|
+
"under-estimate": TrendingDown,
|
|
400
|
+
"disk-sort": HardDrive,
|
|
401
|
+
"filter-loss": Filter,
|
|
402
|
+
"large-seq-scan": ScanSearch,
|
|
403
|
+
"heap-fetches": Layers,
|
|
404
|
+
"workers-underused": Users,
|
|
405
|
+
"high-loops": Repeat,
|
|
406
|
+
"cache-miss": DatabaseZap,
|
|
407
|
+
"never-executed": CircleSlash
|
|
408
|
+
};
|
|
409
|
+
var SEVERITY_ICONS = {
|
|
410
|
+
critical: AlertTriangle,
|
|
411
|
+
warning: AlertTriangle,
|
|
412
|
+
info: Info
|
|
413
|
+
};
|
|
414
|
+
function severityChipClass(severity) {
|
|
415
|
+
switch (severity) {
|
|
416
|
+
case "critical":
|
|
417
|
+
return "border-red-500/40 bg-red-950/30 text-red-200";
|
|
418
|
+
case "warning":
|
|
419
|
+
return "border-amber-500/40 bg-amber-950/30 text-amber-200";
|
|
420
|
+
case "info":
|
|
421
|
+
return "border-default bg-surface-secondary/40 text-secondary";
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function severityTextClass(severity) {
|
|
425
|
+
switch (severity) {
|
|
426
|
+
case "critical":
|
|
427
|
+
return "text-red-300";
|
|
428
|
+
case "warning":
|
|
429
|
+
return "text-amber-300";
|
|
430
|
+
case "info":
|
|
431
|
+
return "text-secondary";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function ExplainDiagnosticChips({
|
|
435
|
+
diagnostics,
|
|
436
|
+
iconsOnly = false,
|
|
437
|
+
className
|
|
438
|
+
}) {
|
|
439
|
+
const { t } = useTranslation();
|
|
440
|
+
if (diagnostics.length === 0) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
return /* @__PURE__ */ jsx("div", { className: clsx("flex flex-wrap items-center gap-1", className), children: diagnostics.map((diagnostic) => {
|
|
444
|
+
const Icon = KIND_ICONS[diagnostic.kind];
|
|
445
|
+
const label = t(diagnostic.labelKey);
|
|
446
|
+
return /* @__PURE__ */ jsxs(
|
|
447
|
+
"span",
|
|
448
|
+
{
|
|
449
|
+
title: `${label}${diagnostic.value ? ` (${diagnostic.value})` : ""} \u2014 ${t(diagnostic.descriptionKey)}`,
|
|
450
|
+
className: clsx(
|
|
451
|
+
"inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] leading-none",
|
|
452
|
+
severityChipClass(diagnostic.severity)
|
|
453
|
+
),
|
|
454
|
+
children: [
|
|
455
|
+
/* @__PURE__ */ jsx(Icon, { size: 10, className: "shrink-0" }),
|
|
456
|
+
!iconsOnly && /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
|
|
457
|
+
diagnostic.value && /* @__PURE__ */ jsx("span", { className: "font-mono opacity-90", children: diagnostic.value })
|
|
458
|
+
]
|
|
459
|
+
},
|
|
460
|
+
diagnostic.kind
|
|
461
|
+
);
|
|
462
|
+
}) });
|
|
463
|
+
}
|
|
464
|
+
function ExplainDiagnosticList({
|
|
465
|
+
diagnostics
|
|
466
|
+
}) {
|
|
467
|
+
const { t } = useTranslation();
|
|
468
|
+
if (diagnostics.length === 0) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return /* @__PURE__ */ jsx("div", { className: "divide-y divide-default/40", children: diagnostics.map((diagnostic) => {
|
|
472
|
+
const Icon = SEVERITY_ICONS[diagnostic.severity];
|
|
473
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex gap-2 px-4 py-2.5", children: [
|
|
474
|
+
/* @__PURE__ */ jsx(
|
|
475
|
+
Icon,
|
|
476
|
+
{
|
|
477
|
+
size: 13,
|
|
478
|
+
className: clsx("mt-0.5 shrink-0", severityTextClass(diagnostic.severity))
|
|
479
|
+
}
|
|
480
|
+
),
|
|
481
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
482
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
483
|
+
/* @__PURE__ */ jsx(
|
|
484
|
+
"span",
|
|
485
|
+
{
|
|
486
|
+
className: clsx(
|
|
487
|
+
"text-[11px] font-semibold",
|
|
488
|
+
severityTextClass(diagnostic.severity)
|
|
489
|
+
),
|
|
490
|
+
children: t(diagnostic.labelKey)
|
|
491
|
+
}
|
|
492
|
+
),
|
|
493
|
+
diagnostic.value && /* @__PURE__ */ jsx("span", { className: "font-mono text-[11px] text-secondary", children: diagnostic.value })
|
|
494
|
+
] }),
|
|
495
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-[11px] leading-relaxed text-muted", children: t(diagnostic.descriptionKey) })
|
|
496
|
+
] })
|
|
497
|
+
] }, diagnostic.kind);
|
|
498
|
+
}) });
|
|
499
|
+
}
|
|
500
|
+
var ExplainPlanNodeComponent = memo(
|
|
501
|
+
({ data }) => {
|
|
502
|
+
const { t } = useTranslation();
|
|
503
|
+
const {
|
|
504
|
+
node,
|
|
505
|
+
metrics,
|
|
506
|
+
maxExclusiveCost,
|
|
507
|
+
maxExclusiveTimeMs,
|
|
508
|
+
diagnostics,
|
|
509
|
+
hasAnalyzeData,
|
|
510
|
+
isSelected
|
|
511
|
+
} = data;
|
|
512
|
+
const useTimeHeat = hasAnalyzeData && maxExclusiveTimeMs > 0;
|
|
513
|
+
const costStyle = useTimeHeat ? getNodeCostStyle(metrics?.exclusiveTimeMs ?? 0, maxExclusiveTimeMs) : getNodeCostStyle(metrics?.exclusiveCost ?? 0, maxExclusiveCost);
|
|
514
|
+
const rowRatio = getRowEstimateRatio(node);
|
|
515
|
+
const mismatch = rowRatio != null && (rowRatio >= 4 || rowRatio <= 0.25) ? rowRatio >= 1 ? {
|
|
516
|
+
value: formatRatio(rowRatio),
|
|
517
|
+
label: t("editor.visualExplain.overEstimate")
|
|
518
|
+
} : {
|
|
519
|
+
value: formatRatio(1 / rowRatio),
|
|
520
|
+
label: t("editor.visualExplain.underEstimate")
|
|
521
|
+
} : null;
|
|
522
|
+
return /* @__PURE__ */ jsxs(
|
|
523
|
+
"div",
|
|
524
|
+
{
|
|
525
|
+
className: clsx(
|
|
526
|
+
"bg-elevated border border-strong rounded shadow-xl min-w-[260px] max-w-[300px] overflow-hidden transition-all",
|
|
527
|
+
"border-l-4",
|
|
528
|
+
costStyle.border,
|
|
529
|
+
isSelected && "ring-2 ring-blue-400/70 border-blue-400/70"
|
|
530
|
+
),
|
|
531
|
+
children: [
|
|
532
|
+
/* @__PURE__ */ jsxs("div", { className: clsx("px-3 py-2 border-b border-default", costStyle.headerBg), children: [
|
|
533
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
534
|
+
metrics && /* @__PURE__ */ jsxs("span", { className: "shrink-0 rounded bg-surface-secondary/70 px-1.5 py-0.5 font-mono text-[10px] text-muted", children: [
|
|
535
|
+
"#",
|
|
536
|
+
metrics.index
|
|
537
|
+
] }),
|
|
538
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-bold text-primary", children: node.node_type })
|
|
539
|
+
] }),
|
|
540
|
+
node.relation && /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted mt-0.5", children: [
|
|
541
|
+
t("editor.visualExplain.relation"),
|
|
542
|
+
": ",
|
|
543
|
+
node.relation
|
|
544
|
+
] })
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ jsxs("div", { className: "px-3 py-2 space-y-1", children: [
|
|
547
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
548
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.estRows") }),
|
|
549
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: node.plan_rows != null ? formatRows(node.plan_rows) : "-" })
|
|
550
|
+
] }),
|
|
551
|
+
metrics?.exclusiveCost != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
552
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.selfCost") }),
|
|
553
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: formatCost(metrics.exclusiveCost) })
|
|
554
|
+
] }),
|
|
555
|
+
mismatch && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
556
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.largestEstimateGap") }),
|
|
557
|
+
/* @__PURE__ */ jsx("span", { className: "text-amber-300 font-mono font-semibold", children: mismatch.value })
|
|
558
|
+
] }),
|
|
559
|
+
hasAnalyzeData && node.actual_rows != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
560
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.actualRows") }),
|
|
561
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-mono font-semibold", children: formatRows(node.actual_rows) })
|
|
562
|
+
] }),
|
|
563
|
+
hasAnalyzeData && metrics?.exclusiveTimeMs != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
564
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.selfTime") }),
|
|
565
|
+
/* @__PURE__ */ jsxs("span", { className: "text-primary font-mono font-semibold", children: [
|
|
566
|
+
formatTime(metrics.exclusiveTimeMs),
|
|
567
|
+
metrics.timeShare != null && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted font-normal", children: [
|
|
568
|
+
"(",
|
|
569
|
+
Math.round(metrics.timeShare * 100),
|
|
570
|
+
"%)"
|
|
571
|
+
] })
|
|
572
|
+
] })
|
|
573
|
+
] }),
|
|
574
|
+
hasAnalyzeData && metrics?.inclusiveTimeMs != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
575
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.totalTime") }),
|
|
576
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: formatTime(metrics.inclusiveTimeMs) })
|
|
577
|
+
] }),
|
|
578
|
+
hasAnalyzeData && node.actual_loops != null && node.actual_loops > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
579
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.loops") }),
|
|
580
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: node.actual_loops })
|
|
581
|
+
] }),
|
|
582
|
+
diagnostics.length > 0 && /* @__PURE__ */ jsx(
|
|
583
|
+
ExplainDiagnosticChips,
|
|
584
|
+
{
|
|
585
|
+
diagnostics,
|
|
586
|
+
className: "border-t border-default/50 pt-1.5"
|
|
587
|
+
}
|
|
588
|
+
),
|
|
589
|
+
node.filter && /* @__PURE__ */ jsxs("div", { className: "text-[10px] text-muted mt-1 font-mono truncate border-t border-default/50 pt-1", children: [
|
|
590
|
+
t("editor.visualExplain.filter"),
|
|
591
|
+
": ",
|
|
592
|
+
node.filter
|
|
593
|
+
] }),
|
|
594
|
+
node.index_condition && /* @__PURE__ */ jsxs("div", { className: "text-[10px] text-muted font-mono truncate", children: [
|
|
595
|
+
t("editor.visualExplain.indexCondition"),
|
|
596
|
+
": ",
|
|
597
|
+
node.index_condition
|
|
598
|
+
] })
|
|
599
|
+
] }),
|
|
600
|
+
/* @__PURE__ */ jsx(
|
|
601
|
+
Handle,
|
|
602
|
+
{
|
|
603
|
+
type: "target",
|
|
604
|
+
position: Position.Top,
|
|
605
|
+
className: "!w-2 !h-2 !bg-indigo-500 !border-strong"
|
|
606
|
+
}
|
|
607
|
+
),
|
|
608
|
+
/* @__PURE__ */ jsx(
|
|
609
|
+
Handle,
|
|
610
|
+
{
|
|
611
|
+
type: "source",
|
|
612
|
+
position: Position.Bottom,
|
|
613
|
+
className: "!w-2 !h-2 !bg-indigo-500 !border-strong"
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
]
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
var nodeTypes = {
|
|
622
|
+
explainPlan: ExplainPlanNodeComponent
|
|
623
|
+
};
|
|
624
|
+
function ExplainGraphInner({
|
|
625
|
+
plan,
|
|
626
|
+
metrics,
|
|
627
|
+
diagnostics,
|
|
628
|
+
selectedNodeId,
|
|
629
|
+
onSelectNode
|
|
630
|
+
}) {
|
|
631
|
+
const { fitView } = useReactFlow();
|
|
632
|
+
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
|
633
|
+
() => explainPlanToFlow(plan, selectedNodeId, metrics, diagnostics),
|
|
634
|
+
[plan, selectedNodeId, metrics, diagnostics]
|
|
635
|
+
);
|
|
636
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
637
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
638
|
+
useEffect(() => {
|
|
639
|
+
setNodes(initialNodes);
|
|
640
|
+
setEdges(initialEdges);
|
|
641
|
+
}, [initialNodes, initialEdges, setNodes, setEdges]);
|
|
642
|
+
const handleInit = useCallback(() => {
|
|
643
|
+
setTimeout(() => fitView({ padding: 0.2 }), 50);
|
|
644
|
+
}, [fitView]);
|
|
645
|
+
return /* @__PURE__ */ jsxs(
|
|
646
|
+
ReactFlow,
|
|
647
|
+
{
|
|
648
|
+
nodes,
|
|
649
|
+
edges,
|
|
650
|
+
onNodesChange,
|
|
651
|
+
onEdgesChange,
|
|
652
|
+
nodeTypes,
|
|
653
|
+
onInit: handleInit,
|
|
654
|
+
onNodeClick: (_, node) => onSelectNode(node.id),
|
|
655
|
+
fitView: true,
|
|
656
|
+
proOptions: { hideAttribution: true },
|
|
657
|
+
minZoom: 0.1,
|
|
658
|
+
maxZoom: 2,
|
|
659
|
+
children: [
|
|
660
|
+
/* @__PURE__ */ jsx(Background, {}),
|
|
661
|
+
/* @__PURE__ */ jsx(Controls, {}),
|
|
662
|
+
nodes.length > 10 && /* @__PURE__ */ jsx(
|
|
663
|
+
MiniMap,
|
|
664
|
+
{
|
|
665
|
+
nodeStrokeWidth: 3,
|
|
666
|
+
className: "!bg-base !border-default"
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
var ExplainGraph = ({
|
|
674
|
+
plan,
|
|
675
|
+
metrics,
|
|
676
|
+
diagnostics,
|
|
677
|
+
selectedNodeId,
|
|
678
|
+
onSelectNode
|
|
679
|
+
}) => {
|
|
680
|
+
return /* @__PURE__ */ jsx("div", { className: "h-full w-full", children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(
|
|
681
|
+
ExplainGraphInner,
|
|
682
|
+
{
|
|
683
|
+
plan,
|
|
684
|
+
metrics,
|
|
685
|
+
diagnostics,
|
|
686
|
+
selectedNodeId,
|
|
687
|
+
onSelectNode
|
|
688
|
+
}
|
|
689
|
+
) }) });
|
|
690
|
+
};
|
|
691
|
+
function ExplainNodeDetails({
|
|
692
|
+
node,
|
|
693
|
+
hasAnalyzeData,
|
|
694
|
+
metrics = null,
|
|
695
|
+
diagnostics = []
|
|
696
|
+
}) {
|
|
697
|
+
const { t } = useTranslation();
|
|
698
|
+
if (!node) {
|
|
699
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 text-xs text-muted", children: t("editor.visualExplain.selectNode") });
|
|
700
|
+
}
|
|
701
|
+
const generalEntries = [
|
|
702
|
+
[t("editor.visualExplain.nodeType"), node.node_type],
|
|
703
|
+
...node.relation ? [[t("editor.visualExplain.relation"), node.relation]] : [],
|
|
704
|
+
...node.startup_cost != null && node.total_cost != null ? [
|
|
705
|
+
[
|
|
706
|
+
t("editor.visualExplain.cost"),
|
|
707
|
+
`${formatCost(node.startup_cost)} - ${formatCost(node.total_cost)}`
|
|
708
|
+
]
|
|
709
|
+
] : node.total_cost != null ? [[t("editor.visualExplain.cost"), formatCost(node.total_cost)]] : [],
|
|
710
|
+
...metrics?.exclusiveCost != null ? [
|
|
711
|
+
[
|
|
712
|
+
t("editor.visualExplain.selfCost"),
|
|
713
|
+
formatCost(metrics.exclusiveCost)
|
|
714
|
+
]
|
|
715
|
+
] : [],
|
|
716
|
+
...node.plan_rows != null ? [[t("editor.visualExplain.estRows"), formatRows(node.plan_rows)]] : [],
|
|
717
|
+
...node.filter ? [[t("editor.visualExplain.filter"), node.filter]] : [],
|
|
718
|
+
...node.index_condition ? [[t("editor.visualExplain.indexCondition"), node.index_condition]] : [],
|
|
719
|
+
...node.join_type ? [[t("editor.visualExplain.joinType"), node.join_type]] : [],
|
|
720
|
+
...node.hash_condition ? [[t("editor.visualExplain.hashCondition"), node.hash_condition]] : []
|
|
721
|
+
];
|
|
722
|
+
const analyzeEntries = hasAnalyzeData ? [
|
|
723
|
+
...node.actual_rows != null ? [[t("editor.visualExplain.actualRows"), formatRows(node.actual_rows)]] : [],
|
|
724
|
+
...metrics?.totalRows != null && node.actual_loops != null && node.actual_loops > 1 ? [
|
|
725
|
+
[
|
|
726
|
+
t("editor.visualExplain.rowsAllLoops"),
|
|
727
|
+
formatRows(metrics.totalRows)
|
|
728
|
+
]
|
|
729
|
+
] : [],
|
|
730
|
+
...metrics?.exclusiveTimeMs != null ? [
|
|
731
|
+
[
|
|
732
|
+
t("editor.visualExplain.selfTime"),
|
|
733
|
+
metrics.timeShare != null ? `${formatTime(metrics.exclusiveTimeMs)} (${Math.round(metrics.timeShare * 100)}%)` : formatTime(metrics.exclusiveTimeMs)
|
|
734
|
+
]
|
|
735
|
+
] : [],
|
|
736
|
+
...metrics?.inclusiveTimeMs != null ? [
|
|
737
|
+
[
|
|
738
|
+
t("editor.visualExplain.totalTime"),
|
|
739
|
+
formatTime(metrics.inclusiveTimeMs)
|
|
740
|
+
]
|
|
741
|
+
] : [],
|
|
742
|
+
...node.actual_time_ms != null ? [
|
|
743
|
+
[
|
|
744
|
+
t("editor.visualExplain.timePerLoop"),
|
|
745
|
+
formatTime(node.actual_time_ms)
|
|
746
|
+
]
|
|
747
|
+
] : [],
|
|
748
|
+
...node.actual_loops != null ? [[t("editor.visualExplain.loops"), String(node.actual_loops)]] : [],
|
|
749
|
+
...node.buffers_hit != null ? [[t("editor.visualExplain.buffersHit"), String(node.buffers_hit)]] : [],
|
|
750
|
+
...node.buffers_read != null ? [[t("editor.visualExplain.buffersRead"), String(node.buffers_read)]] : []
|
|
751
|
+
] : [];
|
|
752
|
+
const extraEntries = Object.entries(node.extra).map(
|
|
753
|
+
([key, value]) => [key, typeof value === "string" ? value : JSON.stringify(value)]
|
|
754
|
+
);
|
|
755
|
+
return /* @__PURE__ */ jsxs("div", { className: "text-xs", children: [
|
|
756
|
+
diagnostics.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-b border-default/60", children: [
|
|
757
|
+
/* @__PURE__ */ jsx("div", { className: "bg-base/60 px-4 py-3 text-[11px] font-semibold uppercase tracking-wide text-muted", children: t("editor.visualExplain.diagnostics.title") }),
|
|
758
|
+
/* @__PURE__ */ jsx(ExplainDiagnosticList, { diagnostics })
|
|
759
|
+
] }),
|
|
760
|
+
/* @__PURE__ */ jsx(
|
|
761
|
+
DetailSection,
|
|
762
|
+
{
|
|
763
|
+
title: t("editor.visualExplain.general"),
|
|
764
|
+
entries: generalEntries
|
|
765
|
+
}
|
|
766
|
+
),
|
|
767
|
+
analyzeEntries.length > 0 && /* @__PURE__ */ jsx(
|
|
768
|
+
DetailSection,
|
|
769
|
+
{
|
|
770
|
+
title: t("editor.visualExplain.analyzeData"),
|
|
771
|
+
entries: analyzeEntries
|
|
772
|
+
}
|
|
773
|
+
),
|
|
774
|
+
extraEntries.length > 0 && /* @__PURE__ */ jsx(
|
|
775
|
+
DetailSection,
|
|
776
|
+
{
|
|
777
|
+
title: t("editor.visualExplain.extraDetails"),
|
|
778
|
+
entries: extraEntries
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
] });
|
|
782
|
+
}
|
|
783
|
+
function DetailSection({ title, entries }) {
|
|
784
|
+
if (entries.length === 0) {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-default/60 last:border-b-0", children: [
|
|
788
|
+
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-[11px] uppercase tracking-wide text-muted font-semibold bg-base/60", children: title }),
|
|
789
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y divide-default/40", children: entries.map(([label, value]) => /* @__PURE__ */ jsxs("div", { className: "px-4 py-2.5", children: [
|
|
790
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] text-muted mb-1", children: label }),
|
|
791
|
+
/* @__PURE__ */ jsx("div", { className: "text-secondary break-words font-mono leading-relaxed", children: value })
|
|
792
|
+
] }, label)) })
|
|
793
|
+
] });
|
|
794
|
+
}
|
|
795
|
+
function ExplainTableView({
|
|
796
|
+
plan,
|
|
797
|
+
metrics,
|
|
798
|
+
diagnostics,
|
|
799
|
+
selectedId,
|
|
800
|
+
onSelect
|
|
801
|
+
}) {
|
|
802
|
+
const { t } = useTranslation();
|
|
803
|
+
const [expandedIds, setExpandedIds] = useState(
|
|
804
|
+
() => collectExpandedIds(plan.root)
|
|
805
|
+
);
|
|
806
|
+
useEffect(() => {
|
|
807
|
+
setExpandedIds(collectExpandedIds(plan.root));
|
|
808
|
+
}, [plan]);
|
|
809
|
+
const toggleExpand = useCallback((id) => {
|
|
810
|
+
setExpandedIds((prev) => {
|
|
811
|
+
const next = new Set(prev);
|
|
812
|
+
if (next.has(id)) next.delete(id);
|
|
813
|
+
else next.add(id);
|
|
814
|
+
return next;
|
|
815
|
+
});
|
|
816
|
+
}, []);
|
|
817
|
+
const selectedNode = findExplainNode(plan.root, selectedId);
|
|
818
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
|
|
819
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto border-r border-default min-w-0", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
|
|
820
|
+
/* @__PURE__ */ jsx("thead", { className: "sticky top-0 z-10 bg-base border-b border-default", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
821
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 text-muted font-semibold whitespace-nowrap", children: "#" }),
|
|
822
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.nodeType") }),
|
|
823
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.relation") }),
|
|
824
|
+
/* @__PURE__ */ jsx("th", { className: "text-right px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.cost") }),
|
|
825
|
+
/* @__PURE__ */ jsx("th", { className: "text-right px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.estRows") }),
|
|
826
|
+
plan.has_analyze_data && /* @__PURE__ */ jsx("th", { className: "text-right px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.actualRows") }),
|
|
827
|
+
/* @__PURE__ */ jsx("th", { className: "text-right px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.selfTime") }),
|
|
828
|
+
/* @__PURE__ */ jsx("th", { className: "text-right px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.largestEstimateGap") }),
|
|
829
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 text-muted font-semibold whitespace-nowrap", children: t("editor.visualExplain.filter") })
|
|
830
|
+
] }) }),
|
|
831
|
+
/* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx(
|
|
832
|
+
TreeRows,
|
|
833
|
+
{
|
|
834
|
+
node: plan.root,
|
|
835
|
+
depth: 0,
|
|
836
|
+
expandedIds,
|
|
837
|
+
selectedId,
|
|
838
|
+
onToggle: toggleExpand,
|
|
839
|
+
onSelect,
|
|
840
|
+
hasAnalyzeData: plan.has_analyze_data,
|
|
841
|
+
metrics,
|
|
842
|
+
diagnostics
|
|
843
|
+
}
|
|
844
|
+
) })
|
|
845
|
+
] }) }),
|
|
846
|
+
/* @__PURE__ */ jsx("div", { className: "w-[320px] shrink-0 overflow-y-auto bg-base/50", children: /* @__PURE__ */ jsx(
|
|
847
|
+
ExplainNodeDetails,
|
|
848
|
+
{
|
|
849
|
+
node: selectedNode,
|
|
850
|
+
hasAnalyzeData: plan.has_analyze_data,
|
|
851
|
+
metrics: selectedNode ? metrics.byId.get(selectedNode.id) ?? null : null,
|
|
852
|
+
diagnostics: selectedNode ? diagnostics.get(selectedNode.id) ?? [] : []
|
|
853
|
+
}
|
|
854
|
+
) })
|
|
855
|
+
] });
|
|
856
|
+
}
|
|
857
|
+
function TreeRows({
|
|
858
|
+
node,
|
|
859
|
+
depth,
|
|
860
|
+
expandedIds,
|
|
861
|
+
selectedId,
|
|
862
|
+
onToggle,
|
|
863
|
+
onSelect,
|
|
864
|
+
hasAnalyzeData,
|
|
865
|
+
metrics,
|
|
866
|
+
diagnostics
|
|
867
|
+
}) {
|
|
868
|
+
const isExpanded = expandedIds.has(node.id);
|
|
869
|
+
const hasChildren = node.children.length > 0;
|
|
870
|
+
const isSelected = selectedId === node.id;
|
|
871
|
+
const nodeMetrics = metrics.byId.get(node.id);
|
|
872
|
+
const nodeDiagnostics = diagnostics.get(node.id) ?? [];
|
|
873
|
+
const costStr = node.startup_cost != null && node.total_cost != null ? `${formatCost(node.startup_cost)} - ${formatCost(node.total_cost)}` : node.total_cost != null ? formatCost(node.total_cost) : "-";
|
|
874
|
+
const timeStr = hasAnalyzeData && nodeMetrics?.exclusiveTimeMs != null ? formatTime(nodeMetrics.exclusiveTimeMs) : "-";
|
|
875
|
+
const rowsStr = node.plan_rows != null ? formatRows(node.plan_rows) : "-";
|
|
876
|
+
const actualRowsStr = hasAnalyzeData && node.actual_rows != null ? formatRows(node.actual_rows) : "-";
|
|
877
|
+
const rowRatio = getRowEstimateRatio(node);
|
|
878
|
+
const ratioStr = rowRatio != null ? rowRatio >= 1 ? formatRatio(rowRatio) : formatRatio(1 / rowRatio) : "-";
|
|
879
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
880
|
+
/* @__PURE__ */ jsxs(
|
|
881
|
+
"tr",
|
|
882
|
+
{
|
|
883
|
+
className: clsx(
|
|
884
|
+
"cursor-pointer transition-colors border-b border-default/30",
|
|
885
|
+
isSelected ? "bg-blue-900/30" : "hover:bg-surface-hover"
|
|
886
|
+
),
|
|
887
|
+
onClick: () => onSelect(node.id),
|
|
888
|
+
children: [
|
|
889
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 font-mono text-[10px] text-muted whitespace-nowrap", children: nodeMetrics ? `#${nodeMetrics.index}` : "" }),
|
|
890
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 whitespace-nowrap", children: /* @__PURE__ */ jsxs(
|
|
891
|
+
"div",
|
|
892
|
+
{
|
|
893
|
+
className: "flex items-center gap-1",
|
|
894
|
+
style: { paddingLeft: `${depth * 20}px` },
|
|
895
|
+
children: [
|
|
896
|
+
hasChildren ? /* @__PURE__ */ jsx(
|
|
897
|
+
"button",
|
|
898
|
+
{
|
|
899
|
+
onClick: (e) => {
|
|
900
|
+
e.stopPropagation();
|
|
901
|
+
onToggle(node.id);
|
|
902
|
+
},
|
|
903
|
+
className: "p-0.5 text-muted hover:text-primary",
|
|
904
|
+
children: isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { size: 12 }) : /* @__PURE__ */ jsx(ChevronRight, { size: 12 })
|
|
905
|
+
}
|
|
906
|
+
) : /* @__PURE__ */ jsx("span", { className: "w-4" }),
|
|
907
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-medium", children: node.node_type }),
|
|
908
|
+
nodeDiagnostics.length > 0 && /* @__PURE__ */ jsx(ExplainDiagnosticChips, { diagnostics: nodeDiagnostics, iconsOnly: true })
|
|
909
|
+
]
|
|
910
|
+
}
|
|
911
|
+
) }),
|
|
912
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-secondary whitespace-nowrap", children: node.relation ?? "" }),
|
|
913
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right text-secondary font-mono whitespace-nowrap", children: costStr }),
|
|
914
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right text-secondary font-mono whitespace-nowrap", children: rowsStr }),
|
|
915
|
+
hasAnalyzeData && /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right text-secondary font-mono whitespace-nowrap", children: actualRowsStr }),
|
|
916
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right text-secondary font-mono whitespace-nowrap", children: timeStr }),
|
|
917
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right whitespace-nowrap", children: /* @__PURE__ */ jsx(
|
|
918
|
+
"span",
|
|
919
|
+
{
|
|
920
|
+
className: clsx(
|
|
921
|
+
"font-mono",
|
|
922
|
+
rowRatio == null ? "text-secondary" : rowRatio >= 4 || rowRatio <= 0.25 ? "text-amber-300" : "text-secondary"
|
|
923
|
+
),
|
|
924
|
+
children: ratioStr
|
|
925
|
+
}
|
|
926
|
+
) }),
|
|
927
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-muted truncate max-w-[200px]", children: node.filter ?? "" })
|
|
928
|
+
]
|
|
929
|
+
}
|
|
930
|
+
),
|
|
931
|
+
isExpanded && node.children.map((child) => /* @__PURE__ */ jsx(
|
|
932
|
+
TreeRows,
|
|
933
|
+
{
|
|
934
|
+
node: child,
|
|
935
|
+
depth: depth + 1,
|
|
936
|
+
expandedIds,
|
|
937
|
+
selectedId,
|
|
938
|
+
onToggle,
|
|
939
|
+
onSelect,
|
|
940
|
+
hasAnalyzeData,
|
|
941
|
+
metrics,
|
|
942
|
+
diagnostics
|
|
943
|
+
},
|
|
944
|
+
child.id
|
|
945
|
+
))
|
|
946
|
+
] });
|
|
947
|
+
}
|
|
948
|
+
function collectExpandedIds(root) {
|
|
949
|
+
const ids = /* @__PURE__ */ new Set();
|
|
950
|
+
function walk(node) {
|
|
951
|
+
ids.add(node.id);
|
|
952
|
+
for (const child of node.children) {
|
|
953
|
+
walk(child);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
walk(root);
|
|
957
|
+
return ids;
|
|
958
|
+
}
|
|
959
|
+
var METRIC_LABEL_KEYS = {
|
|
960
|
+
time: "editor.visualExplain.selfTime",
|
|
961
|
+
rows: "editor.visualExplain.actualRows",
|
|
962
|
+
cost: "editor.visualExplain.selfCost",
|
|
963
|
+
buffers: "editor.visualExplain.buffers"
|
|
964
|
+
};
|
|
965
|
+
function formatMetricValue(value, kind) {
|
|
966
|
+
switch (kind) {
|
|
967
|
+
case "time":
|
|
968
|
+
return formatTime(value);
|
|
969
|
+
case "cost":
|
|
970
|
+
return formatCost(value);
|
|
971
|
+
case "rows":
|
|
972
|
+
case "buffers":
|
|
973
|
+
return formatRows(value);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function ExplainDiagramView({
|
|
977
|
+
plan,
|
|
978
|
+
metrics,
|
|
979
|
+
diagnostics,
|
|
980
|
+
selectedId,
|
|
981
|
+
onSelect
|
|
982
|
+
}) {
|
|
983
|
+
const { t } = useTranslation();
|
|
984
|
+
const availableKinds = useMemo(
|
|
985
|
+
() => getAvailableMetricKinds(metrics),
|
|
986
|
+
[metrics]
|
|
987
|
+
);
|
|
988
|
+
const defaultKind = useMemo(() => getDefaultMetricKind(metrics), [metrics]);
|
|
989
|
+
const [requestedKind, setRequestedKind] = useState(
|
|
990
|
+
null
|
|
991
|
+
);
|
|
992
|
+
const metricKind = requestedKind != null && availableKinds.includes(requestedKind) ? requestedKind : defaultKind;
|
|
993
|
+
const rows = useMemo(() => {
|
|
994
|
+
const nodesById = new Map(
|
|
995
|
+
flattenExplainNodes(plan.root).map((node) => [node.id, node])
|
|
996
|
+
);
|
|
997
|
+
return metrics.order.flatMap((nodeMetrics) => {
|
|
998
|
+
const node = nodesById.get(nodeMetrics.nodeId);
|
|
999
|
+
return node ? [{ node, metrics: nodeMetrics }] : [];
|
|
1000
|
+
});
|
|
1001
|
+
}, [plan, metrics]);
|
|
1002
|
+
const max = metricKind != null ? getMetricMax(metrics, metricKind) : 0;
|
|
1003
|
+
const selectedNode = findExplainNode(plan.root, selectedId);
|
|
1004
|
+
const handleSelect = useCallback(
|
|
1005
|
+
(id) => {
|
|
1006
|
+
onSelect(id);
|
|
1007
|
+
},
|
|
1008
|
+
[onSelect]
|
|
1009
|
+
);
|
|
1010
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
|
|
1011
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col border-r border-default", children: [
|
|
1012
|
+
availableKinds.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-default bg-base/50 px-4 py-2", children: [
|
|
1013
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] uppercase tracking-wide text-muted font-semibold", children: t("editor.visualExplain.metric") }),
|
|
1014
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 rounded-lg bg-surface-secondary p-0.5", children: availableKinds.map((kind) => /* @__PURE__ */ jsx(
|
|
1015
|
+
"button",
|
|
1016
|
+
{
|
|
1017
|
+
onClick: () => setRequestedKind(kind),
|
|
1018
|
+
className: clsx(
|
|
1019
|
+
"rounded px-2 py-1 text-xs transition-colors",
|
|
1020
|
+
metricKind === kind ? "bg-blue-900/40 text-blue-300" : "text-muted hover:text-primary"
|
|
1021
|
+
),
|
|
1022
|
+
children: t(METRIC_LABEL_KEYS[kind])
|
|
1023
|
+
},
|
|
1024
|
+
kind
|
|
1025
|
+
)) })
|
|
1026
|
+
] }),
|
|
1027
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto", children: rows.length === 0 || metricKind == null ? /* @__PURE__ */ jsx("div", { className: "p-4 text-xs text-muted", children: t("editor.visualExplain.noMetricData") }) : /* @__PURE__ */ jsx("table", { className: "w-full text-xs", children: /* @__PURE__ */ jsx("tbody", { children: rows.map(({ node, metrics: nodeMetrics }) => {
|
|
1028
|
+
const value = getMetricValue(nodeMetrics, metricKind);
|
|
1029
|
+
const ratio = value != null && max > 0 ? value / max : 0;
|
|
1030
|
+
const nodeDiagnostics = diagnostics.get(node.id) ?? [];
|
|
1031
|
+
const isSelected = selectedId === node.id;
|
|
1032
|
+
return /* @__PURE__ */ jsxs(
|
|
1033
|
+
"tr",
|
|
1034
|
+
{
|
|
1035
|
+
onClick: () => handleSelect(node.id),
|
|
1036
|
+
className: clsx(
|
|
1037
|
+
"cursor-pointer border-b border-default/30 transition-colors",
|
|
1038
|
+
isSelected ? "bg-blue-900/30" : "hover:bg-surface-hover"
|
|
1039
|
+
),
|
|
1040
|
+
children: [
|
|
1041
|
+
/* @__PURE__ */ jsxs("td", { className: "w-10 px-3 py-1.5 align-top font-mono text-[10px] text-muted", children: [
|
|
1042
|
+
"#",
|
|
1043
|
+
nodeMetrics.index
|
|
1044
|
+
] }),
|
|
1045
|
+
/* @__PURE__ */ jsx("td", { className: "px-1 py-1.5 align-top", children: /* @__PURE__ */ jsxs(
|
|
1046
|
+
"div",
|
|
1047
|
+
{
|
|
1048
|
+
style: { paddingLeft: `${nodeMetrics.depth * 12}px` },
|
|
1049
|
+
className: "min-w-0",
|
|
1050
|
+
children: [
|
|
1051
|
+
/* @__PURE__ */ jsx("div", { className: "truncate font-medium text-primary", children: node.node_type }),
|
|
1052
|
+
node.relation && /* @__PURE__ */ jsx("div", { className: "truncate text-[11px] text-muted", children: node.relation }),
|
|
1053
|
+
nodeDiagnostics.length > 0 && /* @__PURE__ */ jsx(
|
|
1054
|
+
ExplainDiagnosticChips,
|
|
1055
|
+
{
|
|
1056
|
+
diagnostics: nodeDiagnostics,
|
|
1057
|
+
iconsOnly: true,
|
|
1058
|
+
className: "mt-1"
|
|
1059
|
+
}
|
|
1060
|
+
)
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
) }),
|
|
1064
|
+
/* @__PURE__ */ jsx("td", { className: "w-1/2 px-3 py-1.5 align-middle", children: /* @__PURE__ */ jsx("div", { className: "h-2.5 w-full overflow-hidden rounded bg-surface-secondary/60", children: /* @__PURE__ */ jsx(
|
|
1065
|
+
"div",
|
|
1066
|
+
{
|
|
1067
|
+
className: clsx(
|
|
1068
|
+
"h-full rounded transition-all",
|
|
1069
|
+
getHeatBarClass(value ?? 0, max)
|
|
1070
|
+
),
|
|
1071
|
+
style: {
|
|
1072
|
+
width: `${Math.min(100, Math.max(ratio * 100, value != null && value > 0 ? 1 : 0))}%`
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
) }) }),
|
|
1076
|
+
/* @__PURE__ */ jsx("td", { className: "w-24 whitespace-nowrap px-3 py-1.5 text-right align-middle font-mono text-secondary", children: value != null ? formatMetricValue(value, metricKind) : "-" })
|
|
1077
|
+
]
|
|
1078
|
+
},
|
|
1079
|
+
node.id
|
|
1080
|
+
);
|
|
1081
|
+
}) }) }) })
|
|
1082
|
+
] }),
|
|
1083
|
+
/* @__PURE__ */ jsx("div", { className: "w-[320px] shrink-0 overflow-y-auto bg-base/50", children: /* @__PURE__ */ jsx(
|
|
1084
|
+
ExplainNodeDetails,
|
|
1085
|
+
{
|
|
1086
|
+
node: selectedNode,
|
|
1087
|
+
hasAnalyzeData: plan.has_analyze_data,
|
|
1088
|
+
metrics: selectedNode ? metrics.byId.get(selectedNode.id) ?? null : null,
|
|
1089
|
+
diagnostics: selectedNode ? diagnostics.get(selectedNode.id) ?? [] : []
|
|
1090
|
+
}
|
|
1091
|
+
) })
|
|
1092
|
+
] });
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/stats.ts
|
|
1096
|
+
function addTo(sum, value) {
|
|
1097
|
+
if (value != null) {
|
|
1098
|
+
sum.total += value;
|
|
1099
|
+
sum.hasData = true;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function resolve(sum) {
|
|
1103
|
+
return sum.hasData ? sum.total : null;
|
|
1104
|
+
}
|
|
1105
|
+
function newSum() {
|
|
1106
|
+
return { total: 0, hasData: false };
|
|
1107
|
+
}
|
|
1108
|
+
function indexNameOf(node) {
|
|
1109
|
+
const value = node.extra["Index Name"];
|
|
1110
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1111
|
+
}
|
|
1112
|
+
function getExplainPlanStats(plan, metrics) {
|
|
1113
|
+
const nodes = flattenExplainNodes(plan.root);
|
|
1114
|
+
const byNodeType = /* @__PURE__ */ new Map();
|
|
1115
|
+
const byRelation = /* @__PURE__ */ new Map();
|
|
1116
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
1117
|
+
let maxDepth = 0;
|
|
1118
|
+
let neverExecutedCount = 0;
|
|
1119
|
+
const totalTime = newSum();
|
|
1120
|
+
for (const node of nodes) {
|
|
1121
|
+
const nodeMetrics = metrics.byId.get(node.id);
|
|
1122
|
+
const exclusiveTimeMs = nodeMetrics?.exclusiveTimeMs ?? null;
|
|
1123
|
+
maxDepth = Math.max(maxDepth, (nodeMetrics?.depth ?? 0) + 1);
|
|
1124
|
+
if (nodeMetrics?.neverExecuted) {
|
|
1125
|
+
neverExecutedCount += 1;
|
|
1126
|
+
}
|
|
1127
|
+
addTo(totalTime, exclusiveTimeMs);
|
|
1128
|
+
const typeStat = byNodeType.get(node.node_type) ?? {
|
|
1129
|
+
count: 0,
|
|
1130
|
+
time: newSum()
|
|
1131
|
+
};
|
|
1132
|
+
typeStat.count += 1;
|
|
1133
|
+
addTo(typeStat.time, exclusiveTimeMs);
|
|
1134
|
+
byNodeType.set(node.node_type, typeStat);
|
|
1135
|
+
if (node.relation) {
|
|
1136
|
+
const relationStat = byRelation.get(node.relation) ?? {
|
|
1137
|
+
accessCount: 0,
|
|
1138
|
+
nodeTypes: [],
|
|
1139
|
+
time: newSum(),
|
|
1140
|
+
rows: newSum()
|
|
1141
|
+
};
|
|
1142
|
+
relationStat.accessCount += 1;
|
|
1143
|
+
if (!relationStat.nodeTypes.includes(node.node_type)) {
|
|
1144
|
+
relationStat.nodeTypes.push(node.node_type);
|
|
1145
|
+
}
|
|
1146
|
+
addTo(relationStat.time, exclusiveTimeMs);
|
|
1147
|
+
addTo(relationStat.rows, nodeMetrics?.totalRows ?? null);
|
|
1148
|
+
byRelation.set(node.relation, relationStat);
|
|
1149
|
+
}
|
|
1150
|
+
const indexName = indexNameOf(node);
|
|
1151
|
+
if (indexName) {
|
|
1152
|
+
const indexStat = byIndex.get(indexName) ?? {
|
|
1153
|
+
relation: node.relation,
|
|
1154
|
+
scanCount: 0,
|
|
1155
|
+
time: newSum()
|
|
1156
|
+
};
|
|
1157
|
+
indexStat.scanCount += 1;
|
|
1158
|
+
indexStat.relation = indexStat.relation ?? node.relation;
|
|
1159
|
+
addTo(indexStat.time, exclusiveTimeMs);
|
|
1160
|
+
byIndex.set(indexName, indexStat);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const planTotalTime = resolve(totalTime);
|
|
1164
|
+
const nodeTypes2 = Array.from(byNodeType.entries()).map(([nodeType, stat]) => {
|
|
1165
|
+
const exclusiveTimeMs = resolve(stat.time);
|
|
1166
|
+
return {
|
|
1167
|
+
nodeType,
|
|
1168
|
+
count: stat.count,
|
|
1169
|
+
exclusiveTimeMs,
|
|
1170
|
+
timeShare: exclusiveTimeMs != null && planTotalTime != null && planTotalTime > 0 ? exclusiveTimeMs / planTotalTime : null
|
|
1171
|
+
};
|
|
1172
|
+
}).sort(compareByTimeThenCount);
|
|
1173
|
+
const relations = Array.from(byRelation.entries()).map(([relation, stat]) => ({
|
|
1174
|
+
relation,
|
|
1175
|
+
accessCount: stat.accessCount,
|
|
1176
|
+
nodeTypes: stat.nodeTypes,
|
|
1177
|
+
exclusiveTimeMs: resolve(stat.time),
|
|
1178
|
+
totalRows: resolve(stat.rows)
|
|
1179
|
+
})).sort(
|
|
1180
|
+
(a, b) => (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.accessCount - a.accessCount || a.relation.localeCompare(b.relation)
|
|
1181
|
+
);
|
|
1182
|
+
const indexes = Array.from(byIndex.entries()).map(([indexName, stat]) => ({
|
|
1183
|
+
indexName,
|
|
1184
|
+
relation: stat.relation,
|
|
1185
|
+
scanCount: stat.scanCount,
|
|
1186
|
+
exclusiveTimeMs: resolve(stat.time)
|
|
1187
|
+
})).sort(
|
|
1188
|
+
(a, b) => (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.scanCount - a.scanCount || a.indexName.localeCompare(b.indexName)
|
|
1189
|
+
);
|
|
1190
|
+
return {
|
|
1191
|
+
nodeCount: nodes.length,
|
|
1192
|
+
maxDepth,
|
|
1193
|
+
neverExecutedCount,
|
|
1194
|
+
totalExclusiveTimeMs: planTotalTime,
|
|
1195
|
+
nodeTypes: nodeTypes2,
|
|
1196
|
+
relations,
|
|
1197
|
+
indexes
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function compareByTimeThenCount(a, b) {
|
|
1201
|
+
return (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.count - a.count || a.nodeType.localeCompare(b.nodeType);
|
|
1202
|
+
}
|
|
1203
|
+
function ExplainStatsView({ plan, metrics }) {
|
|
1204
|
+
const { t } = useTranslation();
|
|
1205
|
+
const stats = useMemo(
|
|
1206
|
+
() => getExplainPlanStats(plan, metrics),
|
|
1207
|
+
[plan, metrics]
|
|
1208
|
+
);
|
|
1209
|
+
return /* @__PURE__ */ jsxs("div", { className: "h-full overflow-y-auto px-4 py-3 text-xs", children: [
|
|
1210
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-wrap gap-2", children: [
|
|
1211
|
+
/* @__PURE__ */ jsx(
|
|
1212
|
+
StatTile,
|
|
1213
|
+
{
|
|
1214
|
+
label: t("editor.visualExplain.stats.nodeCount"),
|
|
1215
|
+
value: String(stats.nodeCount)
|
|
1216
|
+
}
|
|
1217
|
+
),
|
|
1218
|
+
/* @__PURE__ */ jsx(
|
|
1219
|
+
StatTile,
|
|
1220
|
+
{
|
|
1221
|
+
label: t("editor.visualExplain.stats.maxDepth"),
|
|
1222
|
+
value: String(stats.maxDepth)
|
|
1223
|
+
}
|
|
1224
|
+
),
|
|
1225
|
+
stats.totalExclusiveTimeMs != null && /* @__PURE__ */ jsx(
|
|
1226
|
+
StatTile,
|
|
1227
|
+
{
|
|
1228
|
+
label: t("editor.visualExplain.stats.totalSelfTime"),
|
|
1229
|
+
value: formatTime(stats.totalExclusiveTimeMs)
|
|
1230
|
+
}
|
|
1231
|
+
),
|
|
1232
|
+
stats.neverExecutedCount > 0 && /* @__PURE__ */ jsx(
|
|
1233
|
+
StatTile,
|
|
1234
|
+
{
|
|
1235
|
+
label: t("editor.visualExplain.stats.neverExecutedNodes"),
|
|
1236
|
+
value: String(stats.neverExecutedCount)
|
|
1237
|
+
}
|
|
1238
|
+
)
|
|
1239
|
+
] }),
|
|
1240
|
+
/* @__PURE__ */ jsx(StatsSection, { title: t("editor.visualExplain.stats.byNodeType"), children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
|
|
1241
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx(
|
|
1242
|
+
StatsHeaderRow,
|
|
1243
|
+
{
|
|
1244
|
+
columns: [
|
|
1245
|
+
{ key: "type", label: t("editor.visualExplain.nodeType") },
|
|
1246
|
+
{ key: "count", label: t("editor.visualExplain.stats.count"), align: "right" },
|
|
1247
|
+
{ key: "time", label: t("editor.visualExplain.selfTime"), align: "right" },
|
|
1248
|
+
{ key: "share", label: t("editor.visualExplain.stats.share") }
|
|
1249
|
+
]
|
|
1250
|
+
}
|
|
1251
|
+
) }),
|
|
1252
|
+
/* @__PURE__ */ jsx("tbody", { children: stats.nodeTypes.map((entry) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-default/25", children: [
|
|
1253
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-primary", children: entry.nodeType }),
|
|
1254
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.count }),
|
|
1255
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.exclusiveTimeMs != null ? formatTime(entry.exclusiveTimeMs) : "-" }),
|
|
1256
|
+
/* @__PURE__ */ jsx("td", { className: "w-1/3 px-3 py-1.5", children: /* @__PURE__ */ jsx(ShareBar, { share: entry.timeShare }) })
|
|
1257
|
+
] }, entry.nodeType)) })
|
|
1258
|
+
] }) }),
|
|
1259
|
+
stats.relations.length > 0 && /* @__PURE__ */ jsx(StatsSection, { title: t("editor.visualExplain.stats.byRelation"), children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
|
|
1260
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx(
|
|
1261
|
+
StatsHeaderRow,
|
|
1262
|
+
{
|
|
1263
|
+
columns: [
|
|
1264
|
+
{ key: "relation", label: t("editor.visualExplain.relation") },
|
|
1265
|
+
{
|
|
1266
|
+
key: "accesses",
|
|
1267
|
+
label: t("editor.visualExplain.stats.accesses"),
|
|
1268
|
+
align: "right"
|
|
1269
|
+
},
|
|
1270
|
+
{ key: "ops", label: t("editor.visualExplain.stats.operations") },
|
|
1271
|
+
{
|
|
1272
|
+
key: "rows",
|
|
1273
|
+
label: t("editor.visualExplain.actualRows"),
|
|
1274
|
+
align: "right"
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
key: "time",
|
|
1278
|
+
label: t("editor.visualExplain.selfTime"),
|
|
1279
|
+
align: "right"
|
|
1280
|
+
}
|
|
1281
|
+
]
|
|
1282
|
+
}
|
|
1283
|
+
) }),
|
|
1284
|
+
/* @__PURE__ */ jsx("tbody", { children: stats.relations.map((entry) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-default/25", children: [
|
|
1285
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 font-mono text-primary", children: entry.relation }),
|
|
1286
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.accessCount }),
|
|
1287
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-muted", children: entry.nodeTypes.join(", ") }),
|
|
1288
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.totalRows != null ? formatRows(entry.totalRows) : "-" }),
|
|
1289
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.exclusiveTimeMs != null ? formatTime(entry.exclusiveTimeMs) : "-" })
|
|
1290
|
+
] }, entry.relation)) })
|
|
1291
|
+
] }) }),
|
|
1292
|
+
stats.indexes.length > 0 && /* @__PURE__ */ jsx(StatsSection, { title: t("editor.visualExplain.stats.byIndex"), children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
|
|
1293
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx(
|
|
1294
|
+
StatsHeaderRow,
|
|
1295
|
+
{
|
|
1296
|
+
columns: [
|
|
1297
|
+
{ key: "index", label: t("editor.visualExplain.stats.indexName") },
|
|
1298
|
+
{ key: "relation", label: t("editor.visualExplain.relation") },
|
|
1299
|
+
{
|
|
1300
|
+
key: "scans",
|
|
1301
|
+
label: t("editor.visualExplain.stats.scans"),
|
|
1302
|
+
align: "right"
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
key: "time",
|
|
1306
|
+
label: t("editor.visualExplain.selfTime"),
|
|
1307
|
+
align: "right"
|
|
1308
|
+
}
|
|
1309
|
+
]
|
|
1310
|
+
}
|
|
1311
|
+
) }),
|
|
1312
|
+
/* @__PURE__ */ jsx("tbody", { children: stats.indexes.map((entry) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-default/25", children: [
|
|
1313
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 font-mono text-primary", children: entry.indexName }),
|
|
1314
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 font-mono text-secondary", children: entry.relation ?? "-" }),
|
|
1315
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.scanCount }),
|
|
1316
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-mono text-secondary", children: entry.exclusiveTimeMs != null ? formatTime(entry.exclusiveTimeMs) : "-" })
|
|
1317
|
+
] }, entry.indexName)) })
|
|
1318
|
+
] }) })
|
|
1319
|
+
] });
|
|
1320
|
+
}
|
|
1321
|
+
function StatTile({ label, value }) {
|
|
1322
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-[130px] rounded-xl border border-default bg-surface-secondary/20 px-3 py-2", children: [
|
|
1323
|
+
/* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-[0.14em] text-muted", children: label }),
|
|
1324
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 font-mono text-sm font-semibold text-primary", children: value })
|
|
1325
|
+
] });
|
|
1326
|
+
}
|
|
1327
|
+
function StatsSection({ title, children }) {
|
|
1328
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-4 overflow-hidden rounded-xl border border-default", children: [
|
|
1329
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-default bg-base/60 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted", children: title }),
|
|
1330
|
+
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children })
|
|
1331
|
+
] });
|
|
1332
|
+
}
|
|
1333
|
+
function StatsHeaderRow({ columns }) {
|
|
1334
|
+
return /* @__PURE__ */ jsx("tr", { className: "border-b border-default/60", children: columns.map((column) => /* @__PURE__ */ jsx(
|
|
1335
|
+
"th",
|
|
1336
|
+
{
|
|
1337
|
+
className: clsx(
|
|
1338
|
+
"whitespace-nowrap px-3 py-1.5 font-semibold text-muted",
|
|
1339
|
+
column.align === "right" ? "text-right" : "text-left"
|
|
1340
|
+
),
|
|
1341
|
+
children: column.label
|
|
1342
|
+
},
|
|
1343
|
+
column.key
|
|
1344
|
+
)) });
|
|
1345
|
+
}
|
|
1346
|
+
function ShareBar({ share }) {
|
|
1347
|
+
if (share == null) {
|
|
1348
|
+
return /* @__PURE__ */ jsx("span", { className: "text-muted", children: "-" });
|
|
1349
|
+
}
|
|
1350
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1351
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 flex-1 overflow-hidden rounded bg-surface-secondary/60", children: /* @__PURE__ */ jsx(
|
|
1352
|
+
"div",
|
|
1353
|
+
{
|
|
1354
|
+
className: "h-full rounded bg-blue-500/70",
|
|
1355
|
+
style: { width: `${Math.min(100, share * 100)}%` }
|
|
1356
|
+
}
|
|
1357
|
+
) }),
|
|
1358
|
+
/* @__PURE__ */ jsxs("span", { className: "w-10 shrink-0 text-right font-mono text-[11px] text-muted", children: [
|
|
1359
|
+
Math.round(share * 100),
|
|
1360
|
+
"%"
|
|
1361
|
+
] })
|
|
1362
|
+
] });
|
|
1363
|
+
}
|
|
1364
|
+
function ExplainOverviewBar({
|
|
1365
|
+
plan,
|
|
1366
|
+
metrics,
|
|
1367
|
+
onSelectNode
|
|
1368
|
+
}) {
|
|
1369
|
+
const { t } = useTranslation();
|
|
1370
|
+
const summary = useMemo(
|
|
1371
|
+
() => getExplainPlanSummary(plan, metrics),
|
|
1372
|
+
[plan, metrics]
|
|
1373
|
+
);
|
|
1374
|
+
const legend = useMemo(() => getExplainDriverLegend(plan), [plan]);
|
|
1375
|
+
const [overviewExpanded, setOverviewExpanded] = useState(true);
|
|
1376
|
+
const findings = [
|
|
1377
|
+
summary.highestCostNode && {
|
|
1378
|
+
key: "highest-cost",
|
|
1379
|
+
label: t("editor.visualExplain.highestSelfCost"),
|
|
1380
|
+
value: formatCost(summary.highestCostNode.value),
|
|
1381
|
+
description: formatNodeLabel(
|
|
1382
|
+
summary.highestCostNode.nodeType,
|
|
1383
|
+
summary.highestCostNode.relation
|
|
1384
|
+
),
|
|
1385
|
+
nodeId: summary.highestCostNode.nodeId,
|
|
1386
|
+
icon: Layers2,
|
|
1387
|
+
tone: "blue"
|
|
1388
|
+
},
|
|
1389
|
+
summary.slowestNode && {
|
|
1390
|
+
key: "slowest-step",
|
|
1391
|
+
label: t("editor.visualExplain.slowestSelfStep"),
|
|
1392
|
+
value: formatTime(summary.slowestNode.value),
|
|
1393
|
+
description: formatNodeLabel(
|
|
1394
|
+
summary.slowestNode.nodeType,
|
|
1395
|
+
summary.slowestNode.relation
|
|
1396
|
+
),
|
|
1397
|
+
nodeId: summary.slowestNode.nodeId,
|
|
1398
|
+
icon: Clock3,
|
|
1399
|
+
tone: "amber"
|
|
1400
|
+
},
|
|
1401
|
+
summary.largestRowMismatchNode?.ratio != null && {
|
|
1402
|
+
key: "estimate-gap",
|
|
1403
|
+
label: t("editor.visualExplain.largestEstimateGap"),
|
|
1404
|
+
value: formatRatio(summary.largestRowMismatchNode.value),
|
|
1405
|
+
description: t(
|
|
1406
|
+
summary.largestRowMismatchNode.ratio >= 1 ? "editor.visualExplain.overEstimate" : "editor.visualExplain.underEstimate"
|
|
1407
|
+
),
|
|
1408
|
+
nodeId: summary.largestRowMismatchNode.nodeId,
|
|
1409
|
+
icon: AlertTriangle,
|
|
1410
|
+
tone: "red"
|
|
1411
|
+
},
|
|
1412
|
+
summary.sequentialScans > 0 && {
|
|
1413
|
+
key: "sequential-scans",
|
|
1414
|
+
label: t("editor.visualExplain.sequentialScans"),
|
|
1415
|
+
value: String(summary.sequentialScans),
|
|
1416
|
+
description: t("editor.visualExplain.scanOperations"),
|
|
1417
|
+
nodeId: summary.highestCostNode?.nodeId ?? plan.root.id,
|
|
1418
|
+
icon: ScanSearch,
|
|
1419
|
+
tone: "amber"
|
|
1420
|
+
},
|
|
1421
|
+
summary.tempOperations > 0 && {
|
|
1422
|
+
key: "temp-operations",
|
|
1423
|
+
label: t("editor.visualExplain.tempOperations"),
|
|
1424
|
+
value: String(summary.tempOperations),
|
|
1425
|
+
description: t("editor.visualExplain.sortOrTempOperations"),
|
|
1426
|
+
nodeId: summary.slowestNode?.nodeId ?? plan.root.id,
|
|
1427
|
+
icon: Database,
|
|
1428
|
+
tone: "purple"
|
|
1429
|
+
}
|
|
1430
|
+
].filter(Boolean);
|
|
1431
|
+
return /* @__PURE__ */ jsx("div", { className: "border-b border-default bg-base/40 px-4 py-2", children: /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-default bg-surface-secondary/20 overflow-hidden", children: [
|
|
1432
|
+
/* @__PURE__ */ jsxs(
|
|
1433
|
+
"button",
|
|
1434
|
+
{
|
|
1435
|
+
onClick: () => setOverviewExpanded((prev) => !prev),
|
|
1436
|
+
className: "w-full flex items-center gap-3 px-3 py-2.5 hover:bg-surface-secondary/35 transition-colors",
|
|
1437
|
+
children: [
|
|
1438
|
+
/* @__PURE__ */ jsx("div", { className: "p-1.5 rounded-md bg-blue-900/25 text-blue-300", children: /* @__PURE__ */ jsx(LayoutDashboard, { size: 13 }) }),
|
|
1439
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 text-left", children: [
|
|
1440
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] uppercase tracking-[0.14em] text-muted font-semibold", children: t("editor.visualExplain.overview") }),
|
|
1441
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs text-secondary", children: [
|
|
1442
|
+
findings.length,
|
|
1443
|
+
" ",
|
|
1444
|
+
t("editor.visualExplain.topIssues").toLowerCase(),
|
|
1445
|
+
legend.length > 0 ? ` \u2022 ${legend.length} ${t("editor.visualExplain.driverNotes").toLowerCase()}` : ""
|
|
1446
|
+
] })
|
|
1447
|
+
] }),
|
|
1448
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2 text-[11px] text-muted", children: [
|
|
1449
|
+
/* @__PURE__ */ jsx("span", { children: overviewExpanded ? t("editor.visualExplain.hideOverview") : t("editor.visualExplain.showOverview") }),
|
|
1450
|
+
overviewExpanded ? /* @__PURE__ */ jsx(ChevronDown, { size: 14 }) : /* @__PURE__ */ jsx(ChevronRight, { size: 14 })
|
|
1451
|
+
] })
|
|
1452
|
+
]
|
|
1453
|
+
}
|
|
1454
|
+
),
|
|
1455
|
+
overviewExpanded && /* @__PURE__ */ jsxs("div", { className: "px-3 pb-3 space-y-3", children: [
|
|
1456
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: findings.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-xs text-secondary", children: t("editor.visualExplain.noIssues") }) : findings.map((finding) => {
|
|
1457
|
+
if (!finding) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
const Icon = finding.icon;
|
|
1461
|
+
return /* @__PURE__ */ jsxs(
|
|
1462
|
+
"button",
|
|
1463
|
+
{
|
|
1464
|
+
onClick: () => onSelectNode(finding.nodeId),
|
|
1465
|
+
className: clsx(
|
|
1466
|
+
"min-w-[170px] flex items-start gap-2 rounded-xl border px-3 py-2 text-left transition-colors",
|
|
1467
|
+
getToneClass(finding.tone)
|
|
1468
|
+
),
|
|
1469
|
+
children: [
|
|
1470
|
+
/* @__PURE__ */ jsx(Icon, { size: 14, className: "mt-0.5 shrink-0" }),
|
|
1471
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1472
|
+
/* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-[0.14em] opacity-80", children: finding.label }),
|
|
1473
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold leading-tight mt-1", children: finding.value }),
|
|
1474
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] opacity-80 mt-1", children: finding.description })
|
|
1475
|
+
] })
|
|
1476
|
+
]
|
|
1477
|
+
},
|
|
1478
|
+
finding.key
|
|
1479
|
+
);
|
|
1480
|
+
}) }),
|
|
1481
|
+
legend.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-default bg-base/45 px-3 py-2.5", children: [
|
|
1482
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
1483
|
+
/* @__PURE__ */ jsx("div", { className: "p-1.5 rounded-md bg-cyan-900/30 text-cyan-300", children: /* @__PURE__ */ jsx(BookOpenText, { size: 13 }) }),
|
|
1484
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] uppercase tracking-[0.14em] text-muted font-semibold", children: t("editor.visualExplain.driverNotes") })
|
|
1485
|
+
] }),
|
|
1486
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-1.5", children: legend.map((entry) => /* @__PURE__ */ jsxs(
|
|
1487
|
+
"div",
|
|
1488
|
+
{
|
|
1489
|
+
className: "flex items-start gap-2 text-xs text-secondary leading-relaxed max-w-[720px]",
|
|
1490
|
+
children: [
|
|
1491
|
+
/* @__PURE__ */ jsx("span", { className: "mt-[6px] h-1.5 w-1.5 shrink-0 rounded-full bg-cyan-400/80" }),
|
|
1492
|
+
/* @__PURE__ */ jsx("span", { children: t(entry) })
|
|
1493
|
+
]
|
|
1494
|
+
},
|
|
1495
|
+
entry
|
|
1496
|
+
)) })
|
|
1497
|
+
] })
|
|
1498
|
+
] })
|
|
1499
|
+
] }) });
|
|
1500
|
+
}
|
|
1501
|
+
function formatNodeLabel(nodeType, relation) {
|
|
1502
|
+
return relation ? `${nodeType} \xB7 ${relation}` : nodeType;
|
|
1503
|
+
}
|
|
1504
|
+
function getToneClass(tone) {
|
|
1505
|
+
switch (tone) {
|
|
1506
|
+
case "blue":
|
|
1507
|
+
return "border-blue-500/30 bg-blue-950/20 text-blue-200 hover:bg-blue-950/30";
|
|
1508
|
+
case "amber":
|
|
1509
|
+
return "border-amber-500/30 bg-amber-950/20 text-amber-200 hover:bg-amber-950/30";
|
|
1510
|
+
case "red":
|
|
1511
|
+
return "border-red-500/30 bg-red-950/20 text-red-200 hover:bg-red-950/30";
|
|
1512
|
+
case "purple":
|
|
1513
|
+
return "border-fuchsia-500/30 bg-fuchsia-950/20 text-fuchsia-200 hover:bg-fuchsia-950/30";
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
var ExplainSummaryBar = memo(
|
|
1517
|
+
({ plan, viewMode, onViewModeChange, aiEnabled }) => {
|
|
1518
|
+
const { t } = useTranslation();
|
|
1519
|
+
if (!plan) return null;
|
|
1520
|
+
const maxCost = getMaxCost(plan.root);
|
|
1521
|
+
const toggleButtonClass = (mode) => `flex items-center gap-1.5 px-2 py-1 rounded text-xs transition-colors ${viewMode === mode ? mode === "ai" ? "bg-purple-900/40 text-purple-300" : "bg-blue-900/40 text-blue-300" : "text-muted hover:text-primary"}`;
|
|
1522
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 px-4 py-2 border-b border-default bg-base/50 text-xs", children: [
|
|
1523
|
+
plan.planning_time_ms != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1524
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted", children: [
|
|
1525
|
+
t("editor.visualExplain.planningTime"),
|
|
1526
|
+
":"
|
|
1527
|
+
] }),
|
|
1528
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-mono font-semibold", children: formatTime(plan.planning_time_ms) })
|
|
1529
|
+
] }),
|
|
1530
|
+
plan.execution_time_ms != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1531
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted", children: [
|
|
1532
|
+
t("editor.visualExplain.executionTime"),
|
|
1533
|
+
":"
|
|
1534
|
+
] }),
|
|
1535
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-mono font-semibold", children: formatTime(plan.execution_time_ms) })
|
|
1536
|
+
] }),
|
|
1537
|
+
maxCost > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1538
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted", children: [
|
|
1539
|
+
t("editor.visualExplain.totalCost"),
|
|
1540
|
+
":"
|
|
1541
|
+
] }),
|
|
1542
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-mono font-semibold", children: formatCost(maxCost) })
|
|
1543
|
+
] }),
|
|
1544
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
1545
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 bg-surface-secondary rounded-lg p-0.5", children: [
|
|
1546
|
+
/* @__PURE__ */ jsxs(
|
|
1547
|
+
"button",
|
|
1548
|
+
{
|
|
1549
|
+
onClick: () => onViewModeChange("graph"),
|
|
1550
|
+
className: toggleButtonClass("graph"),
|
|
1551
|
+
children: [
|
|
1552
|
+
/* @__PURE__ */ jsx(Network, { size: 12 }),
|
|
1553
|
+
t("editor.visualExplain.graphView")
|
|
1554
|
+
]
|
|
1555
|
+
}
|
|
1556
|
+
),
|
|
1557
|
+
/* @__PURE__ */ jsxs(
|
|
1558
|
+
"button",
|
|
1559
|
+
{
|
|
1560
|
+
onClick: () => onViewModeChange("diagram"),
|
|
1561
|
+
className: toggleButtonClass("diagram"),
|
|
1562
|
+
children: [
|
|
1563
|
+
/* @__PURE__ */ jsx(BarChart3, { size: 12 }),
|
|
1564
|
+
t("editor.visualExplain.diagramView")
|
|
1565
|
+
]
|
|
1566
|
+
}
|
|
1567
|
+
),
|
|
1568
|
+
/* @__PURE__ */ jsxs(
|
|
1569
|
+
"button",
|
|
1570
|
+
{
|
|
1571
|
+
onClick: () => onViewModeChange("table"),
|
|
1572
|
+
className: toggleButtonClass("table"),
|
|
1573
|
+
children: [
|
|
1574
|
+
/* @__PURE__ */ jsx(TableProperties, { size: 12 }),
|
|
1575
|
+
t("editor.visualExplain.tableView")
|
|
1576
|
+
]
|
|
1577
|
+
}
|
|
1578
|
+
),
|
|
1579
|
+
/* @__PURE__ */ jsxs(
|
|
1580
|
+
"button",
|
|
1581
|
+
{
|
|
1582
|
+
onClick: () => onViewModeChange("stats"),
|
|
1583
|
+
className: toggleButtonClass("stats"),
|
|
1584
|
+
children: [
|
|
1585
|
+
/* @__PURE__ */ jsx(PieChart, { size: 12 }),
|
|
1586
|
+
t("editor.visualExplain.statsView")
|
|
1587
|
+
]
|
|
1588
|
+
}
|
|
1589
|
+
),
|
|
1590
|
+
/* @__PURE__ */ jsxs(
|
|
1591
|
+
"button",
|
|
1592
|
+
{
|
|
1593
|
+
onClick: () => onViewModeChange("raw"),
|
|
1594
|
+
className: toggleButtonClass("raw"),
|
|
1595
|
+
children: [
|
|
1596
|
+
/* @__PURE__ */ jsx(FileText, { size: 12 }),
|
|
1597
|
+
t("editor.visualExplain.rawOutput")
|
|
1598
|
+
]
|
|
1599
|
+
}
|
|
1600
|
+
),
|
|
1601
|
+
aiEnabled && /* @__PURE__ */ jsxs(
|
|
1602
|
+
"button",
|
|
1603
|
+
{
|
|
1604
|
+
onClick: () => onViewModeChange("ai"),
|
|
1605
|
+
className: toggleButtonClass("ai"),
|
|
1606
|
+
children: [
|
|
1607
|
+
/* @__PURE__ */ jsx(Sparkles, { size: 12 }),
|
|
1608
|
+
t("editor.visualExplain.aiAnalysis")
|
|
1609
|
+
]
|
|
1610
|
+
}
|
|
1611
|
+
)
|
|
1612
|
+
] })
|
|
1613
|
+
] });
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
export { ExplainDiagnosticChips, ExplainDiagnosticList, ExplainDiagramView, ExplainGraph, ExplainNodeDetails, ExplainOverviewBar, ExplainPlanNodeComponent, ExplainStatsView, ExplainSummaryBar, ExplainTableView };
|