@tabularis/explain 0.2.0 → 0.3.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/README.md +6 -3
- package/dist/{diagnostics-DPSgsjFk.d.ts → diagnostics-DYKMtJZS.d.ts} +4 -4
- package/dist/flow.d.ts +1 -1
- package/dist/flow.js +2 -8
- package/dist/index.d.ts +157 -19
- package/dist/index.js +529 -47
- package/dist/react.d.ts +22 -8
- package/dist/react.js +774 -311
- package/dist/tableColumns-BWoGh7tK.d.ts +30 -0
- package/package.json +5 -3
package/dist/react.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { memo, useState,
|
|
1
|
+
import { memo, useCallback, useState, useRef, useEffect, useMemo } from 'react';
|
|
2
2
|
import { Handle, Position, ReactFlowProvider, useReactFlow, useNodesState, useEdgesState, ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
|
|
3
3
|
import '@xyflow/react/dist/style.css';
|
|
4
4
|
import dagre from 'dagre';
|
|
5
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,
|
|
6
|
+
import { Network, BarChart3, TableProperties, PieChart, FileText, Sparkles, CircleSlash, DatabaseZap, Repeat, Users, Layers, ScanSearch, Filter, HardDrive, TrendingDown, TrendingUp, Flame, Info, AlertTriangle, Check, Copy, RotateCcw, Download, Layers2, Clock3, Database, LayoutDashboard, ChevronDown, ChevronRight, BookOpenText } from 'lucide-react';
|
|
7
7
|
import clsx from 'clsx';
|
|
8
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
9
|
|
|
@@ -87,16 +87,10 @@ function computeExplainMetrics(plan) {
|
|
|
87
87
|
let maxExclusiveBuffers = 0;
|
|
88
88
|
let totalExclusiveTimeMs = 0;
|
|
89
89
|
for (const metrics of order) {
|
|
90
|
-
maxExclusiveTimeMs = Math.max(
|
|
91
|
-
maxExclusiveTimeMs,
|
|
92
|
-
metrics.exclusiveTimeMs ?? 0
|
|
93
|
-
);
|
|
90
|
+
maxExclusiveTimeMs = Math.max(maxExclusiveTimeMs, metrics.exclusiveTimeMs ?? 0);
|
|
94
91
|
maxExclusiveCost = Math.max(maxExclusiveCost, metrics.exclusiveCost ?? 0);
|
|
95
92
|
maxTotalRows = Math.max(maxTotalRows, metrics.totalRows ?? 0);
|
|
96
|
-
maxExclusiveBuffers = Math.max(
|
|
97
|
-
maxExclusiveBuffers,
|
|
98
|
-
metrics.exclusiveBuffers ?? 0
|
|
99
|
-
);
|
|
93
|
+
maxExclusiveBuffers = Math.max(maxExclusiveBuffers, metrics.exclusiveBuffers ?? 0);
|
|
100
94
|
totalExclusiveTimeMs += metrics.exclusiveTimeMs ?? 0;
|
|
101
95
|
}
|
|
102
96
|
if (totalExclusiveTimeMs > 0) {
|
|
@@ -295,7 +289,7 @@ function getRowEstimateRatio(node) {
|
|
|
295
289
|
}
|
|
296
290
|
return node.actual_rows / node.plan_rows;
|
|
297
291
|
}
|
|
298
|
-
function
|
|
292
|
+
function getRowEstimateGap(node) {
|
|
299
293
|
const ratio = getRowEstimateRatio(node);
|
|
300
294
|
if (ratio == null) {
|
|
301
295
|
return null;
|
|
@@ -341,7 +335,7 @@ function getExplainPlanSummary(plan, metrics) {
|
|
|
341
335
|
};
|
|
342
336
|
}
|
|
343
337
|
const ratio = getRowEstimateRatio(node);
|
|
344
|
-
const magnitude =
|
|
338
|
+
const magnitude = getRowEstimateGap(node);
|
|
345
339
|
if (ratio != null && magnitude != null && (largestRowMismatchNode == null || magnitude > largestRowMismatchNode.value)) {
|
|
346
340
|
largestRowMismatchNode = {
|
|
347
341
|
nodeId: node.id,
|
|
@@ -385,14 +379,27 @@ function getExplainDriverLegend(plan) {
|
|
|
385
379
|
"editor.visualExplain.mysqlEstimateLegend2"
|
|
386
380
|
];
|
|
387
381
|
case "sqlite":
|
|
388
|
-
return [
|
|
389
|
-
"editor.visualExplain.sqliteLegend1",
|
|
390
|
-
"editor.visualExplain.sqliteLegend2"
|
|
391
|
-
];
|
|
382
|
+
return ["editor.visualExplain.sqliteLegend1", "editor.visualExplain.sqliteLegend2"];
|
|
392
383
|
default:
|
|
393
384
|
return [];
|
|
394
385
|
}
|
|
395
386
|
}
|
|
387
|
+
function getExplainTableRow(node, hasAnalyzeData, nodeMetrics, fallbackIndex = 1) {
|
|
388
|
+
return {
|
|
389
|
+
index: nodeMetrics?.index ?? fallbackIndex,
|
|
390
|
+
depth: nodeMetrics?.depth ?? 0,
|
|
391
|
+
nodeType: node.node_type,
|
|
392
|
+
relation: node.relation ?? null,
|
|
393
|
+
startupCost: node.startup_cost ?? null,
|
|
394
|
+
totalCost: node.total_cost ?? null,
|
|
395
|
+
planRows: node.plan_rows ?? null,
|
|
396
|
+
actualRows: hasAnalyzeData ? node.actual_rows ?? null : null,
|
|
397
|
+
actualLoops: hasAnalyzeData ? node.actual_loops ?? null : null,
|
|
398
|
+
exclusiveTimeMs: hasAnalyzeData ? nodeMetrics?.exclusiveTimeMs ?? null : null,
|
|
399
|
+
estimateGap: getRowEstimateGap(node),
|
|
400
|
+
filter: node.filter ?? null
|
|
401
|
+
};
|
|
402
|
+
}
|
|
396
403
|
var KIND_ICONS = {
|
|
397
404
|
hotspot: Flame,
|
|
398
405
|
"over-estimate": TrendingUp,
|
|
@@ -461,9 +468,7 @@ function ExplainDiagnosticChips({
|
|
|
461
468
|
);
|
|
462
469
|
}) });
|
|
463
470
|
}
|
|
464
|
-
function ExplainDiagnosticList({
|
|
465
|
-
diagnostics
|
|
466
|
-
}) {
|
|
471
|
+
function ExplainDiagnosticList({ diagnostics }) {
|
|
467
472
|
const { t } = useTranslation();
|
|
468
473
|
if (diagnostics.length === 0) {
|
|
469
474
|
return null;
|
|
@@ -475,7 +480,10 @@ function ExplainDiagnosticList({
|
|
|
475
480
|
Icon,
|
|
476
481
|
{
|
|
477
482
|
size: 13,
|
|
478
|
-
className: clsx(
|
|
483
|
+
className: clsx(
|
|
484
|
+
"mt-0.5 shrink-0",
|
|
485
|
+
severityTextClass(diagnostic.severity)
|
|
486
|
+
)
|
|
479
487
|
}
|
|
480
488
|
),
|
|
481
489
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
@@ -497,127 +505,125 @@ function ExplainDiagnosticList({
|
|
|
497
505
|
] }, diagnostic.kind);
|
|
498
506
|
}) });
|
|
499
507
|
}
|
|
500
|
-
var ExplainPlanNodeComponent = memo(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
children: [
|
|
532
|
-
/* @__PURE__ */ jsxs("div", { className:
|
|
533
|
-
/* @__PURE__ */ jsxs("
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
metrics.index
|
|
537
|
-
] }),
|
|
538
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm font-bold text-primary", children: node.node_type })
|
|
508
|
+
var ExplainPlanNodeComponent = memo(({ data }) => {
|
|
509
|
+
const { t } = useTranslation();
|
|
510
|
+
const {
|
|
511
|
+
node,
|
|
512
|
+
metrics,
|
|
513
|
+
maxExclusiveCost,
|
|
514
|
+
maxExclusiveTimeMs,
|
|
515
|
+
diagnostics,
|
|
516
|
+
hasAnalyzeData,
|
|
517
|
+
isSelected
|
|
518
|
+
} = data;
|
|
519
|
+
const useTimeHeat = hasAnalyzeData && maxExclusiveTimeMs > 0;
|
|
520
|
+
const costStyle = useTimeHeat ? getNodeCostStyle(metrics?.exclusiveTimeMs ?? 0, maxExclusiveTimeMs) : getNodeCostStyle(metrics?.exclusiveCost ?? 0, maxExclusiveCost);
|
|
521
|
+
const rowRatio = getRowEstimateRatio(node);
|
|
522
|
+
const mismatch = rowRatio != null && (rowRatio >= 4 || rowRatio <= 0.25) ? rowRatio >= 1 ? {
|
|
523
|
+
value: formatRatio(rowRatio),
|
|
524
|
+
label: t("editor.visualExplain.overEstimate")
|
|
525
|
+
} : {
|
|
526
|
+
value: formatRatio(1 / rowRatio),
|
|
527
|
+
label: t("editor.visualExplain.underEstimate")
|
|
528
|
+
} : null;
|
|
529
|
+
return /* @__PURE__ */ jsxs(
|
|
530
|
+
"div",
|
|
531
|
+
{
|
|
532
|
+
className: clsx(
|
|
533
|
+
"bg-elevated border border-strong rounded shadow-xl min-w-[260px] max-w-[300px] overflow-hidden transition-all",
|
|
534
|
+
"border-l-4",
|
|
535
|
+
costStyle.border,
|
|
536
|
+
isSelected && "ring-2 ring-blue-400/70 border-blue-400/70"
|
|
537
|
+
),
|
|
538
|
+
children: [
|
|
539
|
+
/* @__PURE__ */ jsxs("div", { className: clsx("px-3 py-2 border-b border-default", costStyle.headerBg), children: [
|
|
540
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
541
|
+
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: [
|
|
542
|
+
"#",
|
|
543
|
+
metrics.index
|
|
539
544
|
] }),
|
|
540
|
-
|
|
541
|
-
t("editor.visualExplain.relation"),
|
|
542
|
-
": ",
|
|
543
|
-
node.relation
|
|
544
|
-
] })
|
|
545
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-bold text-primary", children: node.node_type })
|
|
545
546
|
] }),
|
|
546
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
547
|
+
node.relation && /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted mt-0.5", children: [
|
|
548
|
+
t("editor.visualExplain.relation"),
|
|
549
|
+
": ",
|
|
550
|
+
node.relation
|
|
551
|
+
] })
|
|
552
|
+
] }),
|
|
553
|
+
/* @__PURE__ */ jsxs("div", { className: "px-3 py-2 space-y-1", children: [
|
|
554
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
555
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.estRows") }),
|
|
556
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: node.plan_rows != null ? formatRows(node.plan_rows) : "-" })
|
|
557
|
+
] }),
|
|
558
|
+
metrics?.exclusiveCost != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
559
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.selfCost") }),
|
|
560
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: formatCost(metrics.exclusiveCost) })
|
|
561
|
+
] }),
|
|
562
|
+
mismatch && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
563
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.largestEstimateGap") }),
|
|
564
|
+
/* @__PURE__ */ jsx("span", { className: "text-amber-300 font-mono font-semibold", children: mismatch.value })
|
|
565
|
+
] }),
|
|
566
|
+
hasAnalyzeData && node.actual_rows != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
567
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.actualRows") }),
|
|
568
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-mono font-semibold", children: formatRows(node.actual_rows) })
|
|
569
|
+
] }),
|
|
570
|
+
hasAnalyzeData && metrics?.exclusiveTimeMs != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
571
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.selfTime") }),
|
|
572
|
+
/* @__PURE__ */ jsxs("span", { className: "text-primary font-mono font-semibold", children: [
|
|
573
|
+
formatTime(metrics.exclusiveTimeMs),
|
|
574
|
+
metrics.timeShare != null && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted font-normal", children: [
|
|
575
|
+
"(",
|
|
576
|
+
Math.round(metrics.timeShare * 100),
|
|
577
|
+
"%)"
|
|
572
578
|
] })
|
|
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
579
|
] })
|
|
599
580
|
] }),
|
|
600
|
-
/* @__PURE__ */
|
|
601
|
-
|
|
581
|
+
hasAnalyzeData && metrics?.inclusiveTimeMs != null && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
582
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.totalTime") }),
|
|
583
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: formatTime(metrics.inclusiveTimeMs) })
|
|
584
|
+
] }),
|
|
585
|
+
hasAnalyzeData && node.actual_loops != null && node.actual_loops > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
586
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted", children: t("editor.visualExplain.loops") }),
|
|
587
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary font-mono", children: node.actual_loops })
|
|
588
|
+
] }),
|
|
589
|
+
diagnostics.length > 0 && /* @__PURE__ */ jsx(
|
|
590
|
+
ExplainDiagnosticChips,
|
|
602
591
|
{
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
className: "!w-2 !h-2 !bg-indigo-500 !border-strong"
|
|
592
|
+
diagnostics,
|
|
593
|
+
className: "border-t border-default/50 pt-1.5"
|
|
606
594
|
}
|
|
607
595
|
),
|
|
608
|
-
/* @__PURE__ */
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
596
|
+
node.filter && /* @__PURE__ */ jsxs("div", { className: "text-[10px] text-muted mt-1 font-mono truncate border-t border-default/50 pt-1", children: [
|
|
597
|
+
t("editor.visualExplain.filter"),
|
|
598
|
+
": ",
|
|
599
|
+
node.filter
|
|
600
|
+
] }),
|
|
601
|
+
node.index_condition && /* @__PURE__ */ jsxs("div", { className: "text-[10px] text-muted font-mono truncate", children: [
|
|
602
|
+
t("editor.visualExplain.indexCondition"),
|
|
603
|
+
": ",
|
|
604
|
+
node.index_condition
|
|
605
|
+
] })
|
|
606
|
+
] }),
|
|
607
|
+
/* @__PURE__ */ jsx(
|
|
608
|
+
Handle,
|
|
609
|
+
{
|
|
610
|
+
type: "target",
|
|
611
|
+
position: Position.Top,
|
|
612
|
+
className: "!w-2 !h-2 !bg-indigo-500 !border-strong"
|
|
613
|
+
}
|
|
614
|
+
),
|
|
615
|
+
/* @__PURE__ */ jsx(
|
|
616
|
+
Handle,
|
|
617
|
+
{
|
|
618
|
+
type: "source",
|
|
619
|
+
position: Position.Bottom,
|
|
620
|
+
className: "!w-2 !h-2 !bg-indigo-500 !border-strong"
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
});
|
|
621
627
|
var nodeTypes = {
|
|
622
628
|
explainPlan: ExplainPlanNodeComponent
|
|
623
629
|
};
|
|
@@ -659,13 +665,7 @@ function ExplainGraphInner({
|
|
|
659
665
|
children: [
|
|
660
666
|
/* @__PURE__ */ jsx(Background, {}),
|
|
661
667
|
/* @__PURE__ */ jsx(Controls, {}),
|
|
662
|
-
nodes.length > 10 && /* @__PURE__ */ jsx(
|
|
663
|
-
MiniMap,
|
|
664
|
-
{
|
|
665
|
-
nodeStrokeWidth: 3,
|
|
666
|
-
className: "!bg-base !border-default"
|
|
667
|
-
}
|
|
668
|
-
)
|
|
668
|
+
nodes.length > 10 && /* @__PURE__ */ jsx(MiniMap, { nodeStrokeWidth: 3, className: "!bg-base !border-default" })
|
|
669
669
|
]
|
|
670
670
|
}
|
|
671
671
|
);
|
|
@@ -688,96 +688,340 @@ var ExplainGraph = ({
|
|
|
688
688
|
}
|
|
689
689
|
) }) });
|
|
690
690
|
};
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
691
|
+
|
|
692
|
+
// src/tableColumns.ts
|
|
693
|
+
var EXPLAIN_TABLE_COLUMNS = [
|
|
694
|
+
"operation",
|
|
695
|
+
"relation",
|
|
696
|
+
"cost",
|
|
697
|
+
"estRows",
|
|
698
|
+
"actualRows",
|
|
699
|
+
"loops",
|
|
700
|
+
"selfTime",
|
|
701
|
+
"estimateGap",
|
|
702
|
+
"filter"
|
|
703
|
+
];
|
|
704
|
+
var EXPLAIN_TABLE_ANALYZE_COLUMNS = /* @__PURE__ */ new Set([
|
|
705
|
+
"actualRows",
|
|
706
|
+
"loops"
|
|
707
|
+
]);
|
|
708
|
+
function isExplainTableColumnId(value) {
|
|
709
|
+
return typeof value === "string" && EXPLAIN_TABLE_COLUMNS.includes(value);
|
|
710
|
+
}
|
|
711
|
+
function normalizeExplainTableColumnOrder(saved) {
|
|
712
|
+
const order = [];
|
|
713
|
+
if (Array.isArray(saved)) {
|
|
714
|
+
for (const id of saved) {
|
|
715
|
+
if (isExplainTableColumnId(id) && !order.includes(id)) order.push(id);
|
|
716
|
+
}
|
|
700
717
|
}
|
|
718
|
+
EXPLAIN_TABLE_COLUMNS.forEach((id, defaultIndex) => {
|
|
719
|
+
if (order.includes(id)) return;
|
|
720
|
+
const previous = EXPLAIN_TABLE_COLUMNS.slice(0, defaultIndex).reverse();
|
|
721
|
+
const anchor = previous.find((candidate) => order.includes(candidate));
|
|
722
|
+
order.splice(anchor ? order.indexOf(anchor) + 1 : 0, 0, id);
|
|
723
|
+
});
|
|
724
|
+
return order;
|
|
725
|
+
}
|
|
726
|
+
function moveExplainTableColumn(order, column, target) {
|
|
727
|
+
const from = order.indexOf(column);
|
|
728
|
+
const to = order.indexOf(target);
|
|
729
|
+
if (from === -1 || to === -1 || from === to) return order;
|
|
730
|
+
const next = order.filter((id) => id !== column);
|
|
731
|
+
next.splice(to, 0, column);
|
|
732
|
+
return next;
|
|
733
|
+
}
|
|
734
|
+
function shiftExplainTableColumn(order, column, direction, visible = order) {
|
|
735
|
+
const shown = order.filter((id) => visible.includes(id));
|
|
736
|
+
const at = shown.indexOf(column);
|
|
737
|
+
const neighbour = at === -1 ? void 0 : shown[at + direction];
|
|
738
|
+
return neighbour ? moveExplainTableColumn(order, column, neighbour) : order;
|
|
739
|
+
}
|
|
740
|
+
function isDefaultExplainTableColumnOrder(order) {
|
|
741
|
+
return order.length === EXPLAIN_TABLE_COLUMNS.length && order.every((id, index) => id === EXPLAIN_TABLE_COLUMNS[index]);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/nodeDetails.ts
|
|
745
|
+
var DEFAULT_LABELS = {
|
|
746
|
+
"editor.visualExplain.nodeType": "Operation",
|
|
747
|
+
"editor.visualExplain.relation": "Table",
|
|
748
|
+
"editor.visualExplain.cost": "Cost",
|
|
749
|
+
"editor.visualExplain.selfCost": "Self Cost",
|
|
750
|
+
"editor.visualExplain.estRows": "Est. Rows",
|
|
751
|
+
"editor.visualExplain.filter": "Filter",
|
|
752
|
+
"editor.visualExplain.indexCondition": "Index Cond.",
|
|
753
|
+
"editor.visualExplain.joinType": "Join Type",
|
|
754
|
+
"editor.visualExplain.hashCondition": "Hash Cond.",
|
|
755
|
+
"editor.visualExplain.actualRows": "Actual Rows",
|
|
756
|
+
"editor.visualExplain.rowsAllLoops": "Rows (All Loops)",
|
|
757
|
+
"editor.visualExplain.selfTime": "Self Time",
|
|
758
|
+
"editor.visualExplain.totalTime": "Total Time",
|
|
759
|
+
"editor.visualExplain.timePerLoop": "Time per Loop",
|
|
760
|
+
"editor.visualExplain.loops": "Loops",
|
|
761
|
+
"editor.visualExplain.buffersHit": "Buffers Hit",
|
|
762
|
+
"editor.visualExplain.buffersRead": "Buffers Read",
|
|
763
|
+
"editor.visualExplain.general": "General",
|
|
764
|
+
"editor.visualExplain.analyzeData": "Analyze Data",
|
|
765
|
+
"editor.visualExplain.extraDetails": "Extra",
|
|
766
|
+
"editor.visualExplain.diagnostics.title": "Diagnostics"
|
|
767
|
+
};
|
|
768
|
+
function getExplainNodeDetailSections(node, options = {}) {
|
|
769
|
+
const { hasAnalyzeData = false, metrics = null, diagnostics = [], t } = options;
|
|
770
|
+
const tr = (key, fallback) => {
|
|
771
|
+
if (t) {
|
|
772
|
+
return t(key, fallback ?? DEFAULT_LABELS[key] ?? key);
|
|
773
|
+
}
|
|
774
|
+
return fallback ?? DEFAULT_LABELS[key] ?? key;
|
|
775
|
+
};
|
|
701
776
|
const generalEntries = [
|
|
702
|
-
|
|
703
|
-
...node.relation ? [
|
|
777
|
+
{ label: tr("editor.visualExplain.nodeType", "Operation"), value: node.node_type },
|
|
778
|
+
...node.relation ? [{ label: tr("editor.visualExplain.relation", "Table"), value: node.relation }] : [],
|
|
704
779
|
...node.startup_cost != null && node.total_cost != null ? [
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
`${formatCost(node.startup_cost)} - ${formatCost(node.total_cost)}`
|
|
708
|
-
|
|
709
|
-
] : node.total_cost != null ? [
|
|
780
|
+
{
|
|
781
|
+
label: tr("editor.visualExplain.cost", "Cost"),
|
|
782
|
+
value: `${formatCost(node.startup_cost)} - ${formatCost(node.total_cost)}`
|
|
783
|
+
}
|
|
784
|
+
] : node.total_cost != null ? [
|
|
785
|
+
{
|
|
786
|
+
label: tr("editor.visualExplain.cost", "Cost"),
|
|
787
|
+
value: formatCost(node.total_cost)
|
|
788
|
+
}
|
|
789
|
+
] : [],
|
|
710
790
|
...metrics?.exclusiveCost != null ? [
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
formatCost(metrics.exclusiveCost)
|
|
714
|
-
|
|
791
|
+
{
|
|
792
|
+
label: tr("editor.visualExplain.selfCost", "Self Cost"),
|
|
793
|
+
value: formatCost(metrics.exclusiveCost)
|
|
794
|
+
}
|
|
715
795
|
] : [],
|
|
716
|
-
...node.plan_rows != null ? [
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
796
|
+
...node.plan_rows != null ? [
|
|
797
|
+
{
|
|
798
|
+
label: tr("editor.visualExplain.estRows", "Est. Rows"),
|
|
799
|
+
value: formatRows(node.plan_rows)
|
|
800
|
+
}
|
|
801
|
+
] : [],
|
|
802
|
+
...node.filter ? [{ label: tr("editor.visualExplain.filter", "Filter"), value: node.filter }] : [],
|
|
803
|
+
...node.index_condition ? [
|
|
804
|
+
{
|
|
805
|
+
label: tr("editor.visualExplain.indexCondition", "Index Cond."),
|
|
806
|
+
value: node.index_condition
|
|
807
|
+
}
|
|
808
|
+
] : [],
|
|
809
|
+
...node.join_type ? [{ label: tr("editor.visualExplain.joinType", "Join Type"), value: node.join_type }] : [],
|
|
810
|
+
...node.hash_condition ? [
|
|
811
|
+
{
|
|
812
|
+
label: tr("editor.visualExplain.hashCondition", "Hash Cond."),
|
|
813
|
+
value: node.hash_condition
|
|
814
|
+
}
|
|
815
|
+
] : []
|
|
721
816
|
];
|
|
722
817
|
const analyzeEntries = hasAnalyzeData ? [
|
|
723
|
-
...node.actual_rows != null ? [
|
|
818
|
+
...node.actual_rows != null ? [
|
|
819
|
+
{
|
|
820
|
+
label: tr("editor.visualExplain.actualRows", "Actual Rows"),
|
|
821
|
+
value: formatRows(node.actual_rows)
|
|
822
|
+
}
|
|
823
|
+
] : [],
|
|
724
824
|
...metrics?.totalRows != null && node.actual_loops != null && node.actual_loops > 1 ? [
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
formatRows(metrics.totalRows)
|
|
728
|
-
|
|
825
|
+
{
|
|
826
|
+
label: tr("editor.visualExplain.rowsAllLoops", "Rows (All Loops)"),
|
|
827
|
+
value: formatRows(metrics.totalRows)
|
|
828
|
+
}
|
|
729
829
|
] : [],
|
|
730
830
|
...metrics?.exclusiveTimeMs != null ? [
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
metrics.timeShare != null ? `${formatTime(metrics.exclusiveTimeMs)} (${Math.round(metrics.timeShare * 100)}%)` : formatTime(metrics.exclusiveTimeMs)
|
|
734
|
-
|
|
831
|
+
{
|
|
832
|
+
label: tr("editor.visualExplain.selfTime", "Self Time"),
|
|
833
|
+
value: metrics.timeShare != null ? `${formatTime(metrics.exclusiveTimeMs)} (${Math.round(metrics.timeShare * 100)}%)` : formatTime(metrics.exclusiveTimeMs)
|
|
834
|
+
}
|
|
735
835
|
] : [],
|
|
736
836
|
...metrics?.inclusiveTimeMs != null ? [
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
formatTime(metrics.inclusiveTimeMs)
|
|
740
|
-
|
|
837
|
+
{
|
|
838
|
+
label: tr("editor.visualExplain.totalTime", "Total Time"),
|
|
839
|
+
value: formatTime(metrics.inclusiveTimeMs)
|
|
840
|
+
}
|
|
741
841
|
] : [],
|
|
742
842
|
...node.actual_time_ms != null ? [
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
formatTime(node.actual_time_ms)
|
|
746
|
-
|
|
843
|
+
{
|
|
844
|
+
label: tr("editor.visualExplain.timePerLoop", "Time per Loop"),
|
|
845
|
+
value: formatTime(node.actual_time_ms)
|
|
846
|
+
}
|
|
747
847
|
] : [],
|
|
748
|
-
...node.actual_loops != null ? [
|
|
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,
|
|
848
|
+
...node.actual_loops != null ? [
|
|
762
849
|
{
|
|
763
|
-
|
|
764
|
-
|
|
850
|
+
label: tr("editor.visualExplain.loops", "Loops"),
|
|
851
|
+
value: String(node.actual_loops)
|
|
765
852
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
DetailSection,
|
|
853
|
+
] : [],
|
|
854
|
+
...node.buffers_hit != null ? [
|
|
769
855
|
{
|
|
770
|
-
|
|
771
|
-
|
|
856
|
+
label: tr("editor.visualExplain.buffersHit", "Buffers Hit"),
|
|
857
|
+
value: String(node.buffers_hit)
|
|
772
858
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
DetailSection,
|
|
859
|
+
] : [],
|
|
860
|
+
...node.buffers_read != null ? [
|
|
776
861
|
{
|
|
777
|
-
|
|
778
|
-
|
|
862
|
+
label: tr("editor.visualExplain.buffersRead", "Buffers Read"),
|
|
863
|
+
value: String(node.buffers_read)
|
|
864
|
+
}
|
|
865
|
+
] : []
|
|
866
|
+
] : [];
|
|
867
|
+
const extraEntries = Object.entries(node.extra ?? {}).map(
|
|
868
|
+
([key, value]) => ({
|
|
869
|
+
label: key,
|
|
870
|
+
value: typeof value === "string" ? value : JSON.stringify(value)
|
|
871
|
+
})
|
|
872
|
+
);
|
|
873
|
+
const sections = [];
|
|
874
|
+
if (generalEntries.length > 0) {
|
|
875
|
+
sections.push({
|
|
876
|
+
title: tr("editor.visualExplain.general", "General"),
|
|
877
|
+
titleKey: "editor.visualExplain.general",
|
|
878
|
+
entries: generalEntries
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (analyzeEntries.length > 0) {
|
|
882
|
+
sections.push({
|
|
883
|
+
title: tr("editor.visualExplain.analyzeData", "Analyze Data"),
|
|
884
|
+
titleKey: "editor.visualExplain.analyzeData",
|
|
885
|
+
entries: analyzeEntries
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (extraEntries.length > 0) {
|
|
889
|
+
sections.push({
|
|
890
|
+
title: tr("editor.visualExplain.extraDetails", "Extra"),
|
|
891
|
+
titleKey: "editor.visualExplain.extraDetails",
|
|
892
|
+
entries: extraEntries
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
896
|
+
const diagEntries = diagnostics.map((d) => ({
|
|
897
|
+
label: tr(d.labelKey, d.kind),
|
|
898
|
+
value: d.value ? `${tr(d.descriptionKey, d.kind)} (${d.value})` : tr(d.descriptionKey, d.kind)
|
|
899
|
+
}));
|
|
900
|
+
sections.push({
|
|
901
|
+
title: tr("editor.visualExplain.diagnostics.title", "Diagnostics"),
|
|
902
|
+
titleKey: "editor.visualExplain.diagnostics.title",
|
|
903
|
+
entries: diagEntries
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return sections;
|
|
907
|
+
}
|
|
908
|
+
function formatExplainNodeDetails(node, options = {}) {
|
|
909
|
+
const sections = getExplainNodeDetailSections(node, options);
|
|
910
|
+
const parts = [];
|
|
911
|
+
for (const section of sections) {
|
|
912
|
+
if (section.entries.length === 0) continue;
|
|
913
|
+
const lines = section.entries.map((entry) => `${entry.label}: ${entry.value}`);
|
|
914
|
+
if (options.includeSectionHeaders || section.titleKey !== "editor.visualExplain.general") {
|
|
915
|
+
parts.push(`${section.title}:
|
|
916
|
+
${lines.join("\n")}`);
|
|
917
|
+
} else {
|
|
918
|
+
parts.push(lines.join("\n"));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return parts.join("\n\n");
|
|
922
|
+
}
|
|
923
|
+
function ExplainNodeDetails({
|
|
924
|
+
node,
|
|
925
|
+
hasAnalyzeData,
|
|
926
|
+
metrics = null,
|
|
927
|
+
diagnostics = [],
|
|
928
|
+
onCopyDetails,
|
|
929
|
+
className
|
|
930
|
+
}) {
|
|
931
|
+
const { t } = useTranslation();
|
|
932
|
+
const translate = useCallback(
|
|
933
|
+
(key, fallback) => {
|
|
934
|
+
return t(key, fallback ? { defaultValue: fallback } : void 0) || fallback || key;
|
|
935
|
+
},
|
|
936
|
+
[t]
|
|
937
|
+
);
|
|
938
|
+
const [copiedNodeId, setCopiedNodeId] = useState(null);
|
|
939
|
+
const copied = node !== null && copiedNodeId === node.id;
|
|
940
|
+
const [isCopying, setIsCopying] = useState(false);
|
|
941
|
+
const timerRef = useRef(null);
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
return () => {
|
|
944
|
+
if (timerRef.current) {
|
|
945
|
+
clearTimeout(timerRef.current);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}, []);
|
|
949
|
+
const handleCopy = useCallback(async () => {
|
|
950
|
+
if (!node || isCopying) return;
|
|
951
|
+
const text = formatExplainNodeDetails(node, {
|
|
952
|
+
hasAnalyzeData,
|
|
953
|
+
metrics,
|
|
954
|
+
diagnostics,
|
|
955
|
+
t: translate
|
|
956
|
+
});
|
|
957
|
+
setIsCopying(true);
|
|
958
|
+
try {
|
|
959
|
+
let success = false;
|
|
960
|
+
if (onCopyDetails) {
|
|
961
|
+
const result = await onCopyDetails(text);
|
|
962
|
+
if (result !== false) {
|
|
963
|
+
success = true;
|
|
964
|
+
}
|
|
965
|
+
} else if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
966
|
+
await navigator.clipboard.writeText(text);
|
|
967
|
+
success = true;
|
|
779
968
|
}
|
|
780
|
-
|
|
969
|
+
if (success) {
|
|
970
|
+
if (timerRef.current) {
|
|
971
|
+
clearTimeout(timerRef.current);
|
|
972
|
+
}
|
|
973
|
+
setCopiedNodeId(node.id);
|
|
974
|
+
timerRef.current = setTimeout(() => {
|
|
975
|
+
setCopiedNodeId(null);
|
|
976
|
+
}, 2e3);
|
|
977
|
+
}
|
|
978
|
+
} catch (err) {
|
|
979
|
+
console.error("Failed to copy node details:", err);
|
|
980
|
+
} finally {
|
|
981
|
+
setIsCopying(false);
|
|
982
|
+
}
|
|
983
|
+
}, [node, isCopying, hasAnalyzeData, metrics, diagnostics, translate, onCopyDetails]);
|
|
984
|
+
const sections = node ? getExplainNodeDetailSections(node, {
|
|
985
|
+
hasAnalyzeData,
|
|
986
|
+
metrics,
|
|
987
|
+
t: translate
|
|
988
|
+
}) : [];
|
|
989
|
+
return /* @__PURE__ */ jsxs("div", { className: clsx("text-xs flex flex-col", className), children: [
|
|
990
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-default/60 bg-base/60 sticky top-0 z-10", children: [
|
|
991
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted", children: t("editor.visualExplain.nodeDetails", "Node Details") }),
|
|
992
|
+
/* @__PURE__ */ jsx(
|
|
993
|
+
"button",
|
|
994
|
+
{
|
|
995
|
+
type: "button",
|
|
996
|
+
onClick: handleCopy,
|
|
997
|
+
disabled: !node || isCopying,
|
|
998
|
+
className: "flex items-center gap-1.5 px-2 py-0.5 text-xs rounded border border-default bg-surface hover:bg-surface-secondary text-primary disabled:opacity-40 disabled:cursor-not-allowed transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-focus",
|
|
999
|
+
title: node ? t("editor.visualExplain.copyDetails", "Copy Details") : t("editor.visualExplain.selectNode", "Select a node to view details"),
|
|
1000
|
+
"aria-label": t("editor.visualExplain.copyDetails", "Copy Details"),
|
|
1001
|
+
children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1002
|
+
/* @__PURE__ */ jsx(Check, { size: 12, className: "text-accent-success" }),
|
|
1003
|
+
/* @__PURE__ */ jsx("span", { className: "text-accent-success text-[11px]", children: t("editor.visualExplain.copied", "Copied") })
|
|
1004
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1005
|
+
/* @__PURE__ */ jsx(Copy, { size: 12, className: "text-secondary" }),
|
|
1006
|
+
/* @__PURE__ */ jsx("span", { className: "text-secondary text-[11px]", children: t("editor.visualExplain.copyDetails", "Copy Details") })
|
|
1007
|
+
] })
|
|
1008
|
+
}
|
|
1009
|
+
)
|
|
1010
|
+
] }),
|
|
1011
|
+
!node ? /* @__PURE__ */ jsx("div", { className: "p-4 text-xs text-muted", children: t("editor.visualExplain.selectNode", "Select a node to view details") }) : /* @__PURE__ */ jsxs("div", { children: [
|
|
1012
|
+
diagnostics.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-b border-default/60", children: [
|
|
1013
|
+
/* @__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", "Diagnostics") }),
|
|
1014
|
+
/* @__PURE__ */ jsx(ExplainDiagnosticList, { diagnostics })
|
|
1015
|
+
] }),
|
|
1016
|
+
sections.map((section) => /* @__PURE__ */ jsx(
|
|
1017
|
+
DetailSection,
|
|
1018
|
+
{
|
|
1019
|
+
title: section.title,
|
|
1020
|
+
entries: section.entries
|
|
1021
|
+
},
|
|
1022
|
+
section.title
|
|
1023
|
+
))
|
|
1024
|
+
] })
|
|
781
1025
|
] });
|
|
782
1026
|
}
|
|
783
1027
|
function DetailSection({ title, entries }) {
|
|
@@ -786,20 +1030,146 @@ function DetailSection({ title, entries }) {
|
|
|
786
1030
|
}
|
|
787
1031
|
return /* @__PURE__ */ jsxs("div", { className: "border-b border-default/60 last:border-b-0", children: [
|
|
788
1032
|
/* @__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((
|
|
1033
|
+
/* @__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
1034
|
/* @__PURE__ */ jsx("div", { className: "text-[11px] text-muted mb-1", children: label }),
|
|
791
1035
|
/* @__PURE__ */ jsx("div", { className: "text-secondary break-words font-mono leading-relaxed", children: value })
|
|
792
1036
|
] }, label)) })
|
|
793
1037
|
] });
|
|
794
1038
|
}
|
|
1039
|
+
|
|
1040
|
+
// src/components/columnDrag.ts
|
|
1041
|
+
function startColumnDrag(startX, startY, options) {
|
|
1042
|
+
const { threshold = 4, onDragStart, onDragMove, onDrop, onEnd } = options;
|
|
1043
|
+
let started = false;
|
|
1044
|
+
let overlay = null;
|
|
1045
|
+
const handleMouseMove = (event) => {
|
|
1046
|
+
if (!started) {
|
|
1047
|
+
if (Math.abs(event.clientX - startX) < threshold && Math.abs(event.clientY - startY) < threshold) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
started = true;
|
|
1051
|
+
overlay = document.createElement("div");
|
|
1052
|
+
overlay.style.cssText = "position:fixed;inset:0;z-index:9999;cursor:grabbing;user-select:none";
|
|
1053
|
+
document.body.appendChild(overlay);
|
|
1054
|
+
onDragStart();
|
|
1055
|
+
}
|
|
1056
|
+
onDragMove(event.clientX, event.clientY);
|
|
1057
|
+
};
|
|
1058
|
+
const handleMouseUp = (event) => {
|
|
1059
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
1060
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
1061
|
+
if (started) {
|
|
1062
|
+
overlay?.remove();
|
|
1063
|
+
const suppressClick = (clickEvent) => {
|
|
1064
|
+
clickEvent.stopPropagation();
|
|
1065
|
+
clickEvent.preventDefault();
|
|
1066
|
+
window.removeEventListener("click", suppressClick, true);
|
|
1067
|
+
};
|
|
1068
|
+
window.addEventListener("click", suppressClick, true);
|
|
1069
|
+
setTimeout(() => window.removeEventListener("click", suppressClick, true), 0);
|
|
1070
|
+
onDrop(event.clientX, event.clientY);
|
|
1071
|
+
}
|
|
1072
|
+
onEnd();
|
|
1073
|
+
};
|
|
1074
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
1075
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
1076
|
+
}
|
|
1077
|
+
var COLUMN_LABEL_KEYS = {
|
|
1078
|
+
operation: "editor.visualExplain.nodeType",
|
|
1079
|
+
relation: "editor.visualExplain.relation",
|
|
1080
|
+
cost: "editor.visualExplain.cost",
|
|
1081
|
+
estRows: "editor.visualExplain.estRows",
|
|
1082
|
+
actualRows: "editor.visualExplain.actualRows",
|
|
1083
|
+
loops: "editor.visualExplain.loops",
|
|
1084
|
+
selfTime: "editor.visualExplain.selfTime",
|
|
1085
|
+
estimateGap: "editor.visualExplain.largestEstimateGap",
|
|
1086
|
+
filter: "editor.visualExplain.filter"
|
|
1087
|
+
};
|
|
1088
|
+
var LEFT_ALIGNED_COLUMNS = /* @__PURE__ */ new Set([
|
|
1089
|
+
"operation",
|
|
1090
|
+
"relation",
|
|
1091
|
+
"filter"
|
|
1092
|
+
]);
|
|
795
1093
|
function ExplainTableView({
|
|
796
1094
|
plan,
|
|
797
1095
|
metrics,
|
|
798
1096
|
diagnostics,
|
|
799
1097
|
selectedId,
|
|
800
|
-
onSelect
|
|
1098
|
+
onSelect,
|
|
1099
|
+
onExportCsv,
|
|
1100
|
+
isExportingCsv,
|
|
1101
|
+
columnOrder,
|
|
1102
|
+
onColumnOrderChange,
|
|
1103
|
+
onCopyDetails
|
|
801
1104
|
}) {
|
|
802
1105
|
const { t } = useTranslation();
|
|
1106
|
+
const [ownColumnOrder, setOwnColumnOrder] = useState(EXPLAIN_TABLE_COLUMNS);
|
|
1107
|
+
const order = useMemo(
|
|
1108
|
+
() => normalizeExplainTableColumnOrder(columnOrder ?? ownColumnOrder),
|
|
1109
|
+
[columnOrder, ownColumnOrder]
|
|
1110
|
+
);
|
|
1111
|
+
const visibleColumns = useMemo(
|
|
1112
|
+
() => order.filter((id) => plan.has_analyze_data || !EXPLAIN_TABLE_ANALYZE_COLUMNS.has(id)),
|
|
1113
|
+
[order, plan.has_analyze_data]
|
|
1114
|
+
);
|
|
1115
|
+
const isDefaultOrder = isDefaultExplainTableColumnOrder(order);
|
|
1116
|
+
const [draggedColumn, setDraggedColumn] = useState(null);
|
|
1117
|
+
const [dropTarget, setDropTarget] = useState(null);
|
|
1118
|
+
const changeOrder = useCallback(
|
|
1119
|
+
(next) => {
|
|
1120
|
+
if (next === order) return;
|
|
1121
|
+
setOwnColumnOrder(next);
|
|
1122
|
+
onColumnOrderChange?.(next);
|
|
1123
|
+
},
|
|
1124
|
+
[order, onColumnOrderChange]
|
|
1125
|
+
);
|
|
1126
|
+
const headerRowRef = useRef(null);
|
|
1127
|
+
const columnAt = useCallback((x) => {
|
|
1128
|
+
const headers = headerRowRef.current?.querySelectorAll("th[data-column]");
|
|
1129
|
+
for (const header of headers ?? []) {
|
|
1130
|
+
const rect = header.getBoundingClientRect();
|
|
1131
|
+
if (x >= rect.left && x <= rect.right) {
|
|
1132
|
+
return header.dataset.column;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}, []);
|
|
1137
|
+
const handleHeaderMouseDown = useCallback(
|
|
1138
|
+
(event, column) => {
|
|
1139
|
+
if (event.button !== 0) return;
|
|
1140
|
+
let target = null;
|
|
1141
|
+
startColumnDrag(event.clientX, event.clientY, {
|
|
1142
|
+
onDragStart: () => setDraggedColumn(column),
|
|
1143
|
+
onDragMove: (x) => {
|
|
1144
|
+
target = columnAt(x);
|
|
1145
|
+
setDropTarget(target);
|
|
1146
|
+
},
|
|
1147
|
+
onDrop: () => {
|
|
1148
|
+
if (target) changeOrder(moveExplainTableColumn(order, column, target));
|
|
1149
|
+
},
|
|
1150
|
+
onEnd: () => {
|
|
1151
|
+
setDraggedColumn(null);
|
|
1152
|
+
setDropTarget(null);
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
},
|
|
1156
|
+
[changeOrder, columnAt, order]
|
|
1157
|
+
);
|
|
1158
|
+
const handleHeaderKeyDown = useCallback(
|
|
1159
|
+
(event, column) => {
|
|
1160
|
+
if (!event.altKey || event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
1161
|
+
event.preventDefault();
|
|
1162
|
+
changeOrder(
|
|
1163
|
+
shiftExplainTableColumn(
|
|
1164
|
+
order,
|
|
1165
|
+
column,
|
|
1166
|
+
event.key === "ArrowLeft" ? -1 : 1,
|
|
1167
|
+
visibleColumns
|
|
1168
|
+
)
|
|
1169
|
+
);
|
|
1170
|
+
},
|
|
1171
|
+
[changeOrder, order, visibleColumns]
|
|
1172
|
+
);
|
|
803
1173
|
const [expandedIds, setExpandedIds] = useState(
|
|
804
1174
|
() => collectExpandedIds(plan.root)
|
|
805
1175
|
);
|
|
@@ -816,40 +1186,98 @@ function ExplainTableView({
|
|
|
816
1186
|
}, []);
|
|
817
1187
|
const selectedNode = findExplainNode(plan.root, selectedId);
|
|
818
1188
|
return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
|
|
819
|
-
/* @__PURE__ */
|
|
820
|
-
/* @__PURE__ */
|
|
821
|
-
/* @__PURE__ */ jsx("
|
|
822
|
-
/* @__PURE__ */
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1189
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col min-w-0 border-r border-default", children: [
|
|
1190
|
+
(onExportCsv || !isDefaultOrder) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-default bg-base/50 text-xs shrink-0", children: [
|
|
1191
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted font-medium", children: t("editor.visualExplain.tableTitle", "Plan Tree") }),
|
|
1192
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1193
|
+
!isDefaultOrder && /* @__PURE__ */ jsxs(
|
|
1194
|
+
"button",
|
|
1195
|
+
{
|
|
1196
|
+
type: "button",
|
|
1197
|
+
onClick: () => changeOrder(EXPLAIN_TABLE_COLUMNS),
|
|
1198
|
+
className: "flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium text-secondary hover:text-primary hover:bg-surface-secondary transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
|
|
1199
|
+
children: [
|
|
1200
|
+
/* @__PURE__ */ jsx(RotateCcw, { size: 13, "aria-hidden": "true" }),
|
|
1201
|
+
/* @__PURE__ */ jsx("span", { children: t(
|
|
1202
|
+
"editor.visualExplain.resetColumnOrder",
|
|
1203
|
+
"Reset column order"
|
|
1204
|
+
) })
|
|
1205
|
+
]
|
|
1206
|
+
}
|
|
1207
|
+
),
|
|
1208
|
+
onExportCsv && /* @__PURE__ */ jsxs(
|
|
1209
|
+
"button",
|
|
1210
|
+
{
|
|
1211
|
+
type: "button",
|
|
1212
|
+
onClick: onExportCsv,
|
|
1213
|
+
disabled: isExportingCsv,
|
|
1214
|
+
className: "flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium text-secondary hover:text-primary hover:bg-surface-secondary transition-colors disabled:opacity-50 disabled:pointer-events-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
|
|
1215
|
+
"aria-label": t("editor.visualExplain.exportCsv", "Export CSV"),
|
|
1216
|
+
title: t("editor.visualExplain.exportCsv", "Export CSV"),
|
|
1217
|
+
children: [
|
|
1218
|
+
/* @__PURE__ */ jsx(Download, { size: 13, "aria-hidden": "true" }),
|
|
1219
|
+
/* @__PURE__ */ jsx("span", { children: isExportingCsv ? t("editor.visualExplain.exportingCsv", "Exporting...") : t("editor.visualExplain.exportCsv", "Export CSV") })
|
|
1220
|
+
]
|
|
1221
|
+
}
|
|
1222
|
+
)
|
|
1223
|
+
] })
|
|
1224
|
+
] }),
|
|
1225
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto min-h-0", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
|
|
1226
|
+
/* @__PURE__ */ jsx("thead", { className: "sticky top-0 z-10 bg-base border-b border-default", children: /* @__PURE__ */ jsxs("tr", { ref: headerRowRef, children: [
|
|
1227
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 text-muted font-semibold whitespace-nowrap", children: "#" }),
|
|
1228
|
+
visibleColumns.map((column) => /* @__PURE__ */ jsx(
|
|
1229
|
+
"th",
|
|
1230
|
+
{
|
|
1231
|
+
"data-column": column,
|
|
1232
|
+
className: clsx(
|
|
1233
|
+
"px-3 py-2 text-muted font-semibold whitespace-nowrap transition-colors",
|
|
1234
|
+
LEFT_ALIGNED_COLUMNS.has(column) ? "text-left" : "text-right",
|
|
1235
|
+
draggedColumn === column && "opacity-50",
|
|
1236
|
+
dropTarget === column && draggedColumn !== column && "bg-accent-primary/15"
|
|
1237
|
+
),
|
|
1238
|
+
children: /* @__PURE__ */ jsx(
|
|
1239
|
+
"button",
|
|
1240
|
+
{
|
|
1241
|
+
type: "button",
|
|
1242
|
+
onMouseDown: (event) => handleHeaderMouseDown(event, column),
|
|
1243
|
+
onKeyDown: (event) => handleHeaderKeyDown(event, column),
|
|
1244
|
+
title: t(
|
|
1245
|
+
"editor.visualExplain.moveColumnHint",
|
|
1246
|
+
"Drag to reorder, or press Alt+Left / Alt+Right"
|
|
1247
|
+
),
|
|
1248
|
+
className: "cursor-grab font-semibold rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
|
|
1249
|
+
children: t(COLUMN_LABEL_KEYS[column])
|
|
1250
|
+
}
|
|
1251
|
+
)
|
|
1252
|
+
},
|
|
1253
|
+
column
|
|
1254
|
+
))
|
|
1255
|
+
] }) }),
|
|
1256
|
+
/* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx(
|
|
1257
|
+
TreeRows,
|
|
1258
|
+
{
|
|
1259
|
+
node: plan.root,
|
|
1260
|
+
depth: 0,
|
|
1261
|
+
expandedIds,
|
|
1262
|
+
selectedId,
|
|
1263
|
+
onToggle: toggleExpand,
|
|
1264
|
+
onSelect,
|
|
1265
|
+
hasAnalyzeData: plan.has_analyze_data,
|
|
1266
|
+
metrics,
|
|
1267
|
+
diagnostics,
|
|
1268
|
+
columns: visibleColumns
|
|
1269
|
+
}
|
|
1270
|
+
) })
|
|
1271
|
+
] }) })
|
|
1272
|
+
] }),
|
|
846
1273
|
/* @__PURE__ */ jsx("div", { className: "w-[320px] shrink-0 overflow-y-auto bg-base/50", children: /* @__PURE__ */ jsx(
|
|
847
1274
|
ExplainNodeDetails,
|
|
848
1275
|
{
|
|
849
1276
|
node: selectedNode,
|
|
850
1277
|
hasAnalyzeData: plan.has_analyze_data,
|
|
851
1278
|
metrics: selectedNode ? metrics.byId.get(selectedNode.id) ?? null : null,
|
|
852
|
-
diagnostics: selectedNode ? diagnostics.get(selectedNode.id) ?? [] : []
|
|
1279
|
+
diagnostics: selectedNode ? diagnostics.get(selectedNode.id) ?? [] : [],
|
|
1280
|
+
onCopyDetails
|
|
853
1281
|
}
|
|
854
1282
|
) })
|
|
855
1283
|
] });
|
|
@@ -863,19 +1291,74 @@ function TreeRows({
|
|
|
863
1291
|
onSelect,
|
|
864
1292
|
hasAnalyzeData,
|
|
865
1293
|
metrics,
|
|
866
|
-
diagnostics
|
|
1294
|
+
diagnostics,
|
|
1295
|
+
columns
|
|
867
1296
|
}) {
|
|
868
1297
|
const isExpanded = expandedIds.has(node.id);
|
|
869
1298
|
const hasChildren = node.children.length > 0;
|
|
870
1299
|
const isSelected = selectedId === node.id;
|
|
871
1300
|
const nodeMetrics = metrics.byId.get(node.id);
|
|
872
1301
|
const nodeDiagnostics = diagnostics.get(node.id) ?? [];
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1302
|
+
const row = getExplainTableRow(node, hasAnalyzeData, nodeMetrics);
|
|
1303
|
+
const cells = {
|
|
1304
|
+
operation: () => /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 whitespace-nowrap", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", style: { paddingLeft: `${depth * 20}px` }, children: [
|
|
1305
|
+
hasChildren ? /* @__PURE__ */ jsx(
|
|
1306
|
+
"button",
|
|
1307
|
+
{
|
|
1308
|
+
onClick: (e) => {
|
|
1309
|
+
e.stopPropagation();
|
|
1310
|
+
onToggle(node.id);
|
|
1311
|
+
},
|
|
1312
|
+
className: "p-0.5 text-muted hover:text-primary",
|
|
1313
|
+
children: isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { size: 12 }) : /* @__PURE__ */ jsx(ChevronRight, { size: 12 })
|
|
1314
|
+
}
|
|
1315
|
+
) : /* @__PURE__ */ jsx("span", { className: "w-4" }),
|
|
1316
|
+
/* @__PURE__ */ jsx("span", { className: "text-primary font-medium", children: node.node_type }),
|
|
1317
|
+
nodeDiagnostics.length > 0 && /* @__PURE__ */ jsx(ExplainDiagnosticChips, { diagnostics: nodeDiagnostics, iconsOnly: true })
|
|
1318
|
+
] }) }, "operation"),
|
|
1319
|
+
relation: () => /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-secondary whitespace-nowrap", children: row.relation ?? "" }, "relation"),
|
|
1320
|
+
cost: () => /* @__PURE__ */ jsx(NumberCell, { value: formatRowCost(row) }, "cost"),
|
|
1321
|
+
estRows: () => /* @__PURE__ */ jsx(
|
|
1322
|
+
NumberCell,
|
|
1323
|
+
{
|
|
1324
|
+
value: row.planRows != null ? formatRows(row.planRows) : "-"
|
|
1325
|
+
},
|
|
1326
|
+
"estRows"
|
|
1327
|
+
),
|
|
1328
|
+
actualRows: () => /* @__PURE__ */ jsx(
|
|
1329
|
+
NumberCell,
|
|
1330
|
+
{
|
|
1331
|
+
value: row.actualRows != null ? formatRows(row.actualRows) : "-"
|
|
1332
|
+
},
|
|
1333
|
+
"actualRows"
|
|
1334
|
+
),
|
|
1335
|
+
loops: () => /* @__PURE__ */ jsx(
|
|
1336
|
+
NumberCell,
|
|
1337
|
+
{
|
|
1338
|
+
value: row.actualLoops != null ? formatRows(row.actualLoops) : "-",
|
|
1339
|
+
title: row.actualLoops != null ? String(row.actualLoops) : void 0
|
|
1340
|
+
},
|
|
1341
|
+
"loops"
|
|
1342
|
+
),
|
|
1343
|
+
selfTime: () => /* @__PURE__ */ jsx(
|
|
1344
|
+
NumberCell,
|
|
1345
|
+
{
|
|
1346
|
+
value: row.exclusiveTimeMs != null ? formatTime(row.exclusiveTimeMs) : "-"
|
|
1347
|
+
},
|
|
1348
|
+
"selfTime"
|
|
1349
|
+
),
|
|
1350
|
+
estimateGap: () => /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right whitespace-nowrap", children: /* @__PURE__ */ jsx(
|
|
1351
|
+
"span",
|
|
1352
|
+
{
|
|
1353
|
+
className: clsx(
|
|
1354
|
+
"font-mono",
|
|
1355
|
+
row.estimateGap != null && row.estimateGap >= 4 ? "text-amber-300" : "text-secondary"
|
|
1356
|
+
),
|
|
1357
|
+
children: row.estimateGap != null ? formatRatio(row.estimateGap) : "-"
|
|
1358
|
+
}
|
|
1359
|
+
) }, "estimateGap"),
|
|
1360
|
+
filter: () => /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-muted truncate max-w-[200px]", children: row.filter ?? "" }, "filter")
|
|
1361
|
+
};
|
|
879
1362
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
880
1363
|
/* @__PURE__ */ jsxs(
|
|
881
1364
|
"tr",
|
|
@@ -887,44 +1370,7 @@ function TreeRows({
|
|
|
887
1370
|
onClick: () => onSelect(node.id),
|
|
888
1371
|
children: [
|
|
889
1372
|
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 font-mono text-[10px] text-muted whitespace-nowrap", children: nodeMetrics ? `#${nodeMetrics.index}` : "" }),
|
|
890
|
-
|
|
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 ?? "" })
|
|
1373
|
+
columns.map((column) => cells[column]())
|
|
928
1374
|
]
|
|
929
1375
|
}
|
|
930
1376
|
),
|
|
@@ -939,12 +1385,29 @@ function TreeRows({
|
|
|
939
1385
|
onSelect,
|
|
940
1386
|
hasAnalyzeData,
|
|
941
1387
|
metrics,
|
|
942
|
-
diagnostics
|
|
1388
|
+
diagnostics,
|
|
1389
|
+
columns
|
|
943
1390
|
},
|
|
944
1391
|
child.id
|
|
945
1392
|
))
|
|
946
1393
|
] });
|
|
947
1394
|
}
|
|
1395
|
+
function formatRowCost(row) {
|
|
1396
|
+
if (row.startupCost != null && row.totalCost != null) {
|
|
1397
|
+
return `${formatCost(row.startupCost)} - ${formatCost(row.totalCost)}`;
|
|
1398
|
+
}
|
|
1399
|
+
return row.totalCost != null ? formatCost(row.totalCost) : "-";
|
|
1400
|
+
}
|
|
1401
|
+
function NumberCell({ value, title }) {
|
|
1402
|
+
return /* @__PURE__ */ jsx(
|
|
1403
|
+
"td",
|
|
1404
|
+
{
|
|
1405
|
+
className: "px-3 py-1.5 text-right text-secondary font-mono whitespace-nowrap",
|
|
1406
|
+
title,
|
|
1407
|
+
children: value
|
|
1408
|
+
}
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
948
1411
|
function collectExpandedIds(root) {
|
|
949
1412
|
const ids = /* @__PURE__ */ new Set();
|
|
950
1413
|
function walk(node) {
|
|
@@ -978,17 +1441,13 @@ function ExplainDiagramView({
|
|
|
978
1441
|
metrics,
|
|
979
1442
|
diagnostics,
|
|
980
1443
|
selectedId,
|
|
981
|
-
onSelect
|
|
1444
|
+
onSelect,
|
|
1445
|
+
onCopyDetails
|
|
982
1446
|
}) {
|
|
983
1447
|
const { t } = useTranslation();
|
|
984
|
-
const availableKinds = useMemo(
|
|
985
|
-
() => getAvailableMetricKinds(metrics),
|
|
986
|
-
[metrics]
|
|
987
|
-
);
|
|
1448
|
+
const availableKinds = useMemo(() => getAvailableMetricKinds(metrics), [metrics]);
|
|
988
1449
|
const defaultKind = useMemo(() => getDefaultMetricKind(metrics), [metrics]);
|
|
989
|
-
const [requestedKind, setRequestedKind] = useState(
|
|
990
|
-
null
|
|
991
|
-
);
|
|
1450
|
+
const [requestedKind, setRequestedKind] = useState(null);
|
|
992
1451
|
const metricKind = requestedKind != null && availableKinds.includes(requestedKind) ? requestedKind : defaultKind;
|
|
993
1452
|
const rows = useMemo(() => {
|
|
994
1453
|
const nodesById = new Map(
|
|
@@ -1045,7 +1504,9 @@ function ExplainDiagramView({
|
|
|
1045
1504
|
/* @__PURE__ */ jsx("td", { className: "px-1 py-1.5 align-top", children: /* @__PURE__ */ jsxs(
|
|
1046
1505
|
"div",
|
|
1047
1506
|
{
|
|
1048
|
-
style: {
|
|
1507
|
+
style: {
|
|
1508
|
+
paddingLeft: `${nodeMetrics.depth * 12}px`
|
|
1509
|
+
},
|
|
1049
1510
|
className: "min-w-0",
|
|
1050
1511
|
children: [
|
|
1051
1512
|
/* @__PURE__ */ jsx("div", { className: "truncate font-medium text-primary", children: node.node_type }),
|
|
@@ -1086,7 +1547,8 @@ function ExplainDiagramView({
|
|
|
1086
1547
|
node: selectedNode,
|
|
1087
1548
|
hasAnalyzeData: plan.has_analyze_data,
|
|
1088
1549
|
metrics: selectedNode ? metrics.byId.get(selectedNode.id) ?? null : null,
|
|
1089
|
-
diagnostics: selectedNode ? diagnostics.get(selectedNode.id) ?? [] : []
|
|
1550
|
+
diagnostics: selectedNode ? diagnostics.get(selectedNode.id) ?? [] : [],
|
|
1551
|
+
onCopyDetails
|
|
1090
1552
|
}
|
|
1091
1553
|
) })
|
|
1092
1554
|
] });
|
|
@@ -1202,10 +1664,7 @@ function compareByTimeThenCount(a, b) {
|
|
|
1202
1664
|
}
|
|
1203
1665
|
function ExplainStatsView({ plan, metrics }) {
|
|
1204
1666
|
const { t } = useTranslation();
|
|
1205
|
-
const stats = useMemo(
|
|
1206
|
-
() => getExplainPlanStats(plan, metrics),
|
|
1207
|
-
[plan, metrics]
|
|
1208
|
-
);
|
|
1667
|
+
const stats = useMemo(() => getExplainPlanStats(plan, metrics), [plan, metrics]);
|
|
1209
1668
|
return /* @__PURE__ */ jsxs("div", { className: "h-full overflow-y-auto px-4 py-3 text-xs", children: [
|
|
1210
1669
|
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-wrap gap-2", children: [
|
|
1211
1670
|
/* @__PURE__ */ jsx(
|
|
@@ -1243,8 +1702,16 @@ function ExplainStatsView({ plan, metrics }) {
|
|
|
1243
1702
|
{
|
|
1244
1703
|
columns: [
|
|
1245
1704
|
{ key: "type", label: t("editor.visualExplain.nodeType") },
|
|
1246
|
-
{
|
|
1247
|
-
|
|
1705
|
+
{
|
|
1706
|
+
key: "count",
|
|
1707
|
+
label: t("editor.visualExplain.stats.count"),
|
|
1708
|
+
align: "right"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
key: "time",
|
|
1712
|
+
label: t("editor.visualExplain.selfTime"),
|
|
1713
|
+
align: "right"
|
|
1714
|
+
},
|
|
1248
1715
|
{ key: "share", label: t("editor.visualExplain.stats.share") }
|
|
1249
1716
|
]
|
|
1250
1717
|
}
|
|
@@ -1294,7 +1761,10 @@ function ExplainStatsView({ plan, metrics }) {
|
|
|
1294
1761
|
StatsHeaderRow,
|
|
1295
1762
|
{
|
|
1296
1763
|
columns: [
|
|
1297
|
-
{
|
|
1764
|
+
{
|
|
1765
|
+
key: "index",
|
|
1766
|
+
label: t("editor.visualExplain.stats.indexName")
|
|
1767
|
+
},
|
|
1298
1768
|
{ key: "relation", label: t("editor.visualExplain.relation") },
|
|
1299
1769
|
{
|
|
1300
1770
|
key: "scans",
|
|
@@ -1361,16 +1831,9 @@ function ShareBar({ share }) {
|
|
|
1361
1831
|
] })
|
|
1362
1832
|
] });
|
|
1363
1833
|
}
|
|
1364
|
-
function ExplainOverviewBar({
|
|
1365
|
-
plan,
|
|
1366
|
-
metrics,
|
|
1367
|
-
onSelectNode
|
|
1368
|
-
}) {
|
|
1834
|
+
function ExplainOverviewBar({ plan, metrics, onSelectNode }) {
|
|
1369
1835
|
const { t } = useTranslation();
|
|
1370
|
-
const summary = useMemo(
|
|
1371
|
-
() => getExplainPlanSummary(plan, metrics),
|
|
1372
|
-
[plan, metrics]
|
|
1373
|
-
);
|
|
1836
|
+
const summary = useMemo(() => getExplainPlanSummary(plan, metrics), [plan, metrics]);
|
|
1374
1837
|
const legend = useMemo(() => getExplainDriverLegend(plan), [plan]);
|
|
1375
1838
|
const [overviewExpanded, setOverviewExpanded] = useState(true);
|
|
1376
1839
|
const findings = [
|