@@ -3,14 +3,23 @@ import 'package:querya_desktop/features/workspace/query_plan.dart';
33import 'package:querya_desktop/shared/widgets/widgets.dart' ;
44
55/// The plan as a tree, top-down. The node with the largest own cost is
6- /// highlighted; each node shows its share of the total cost as a bar.
7- class PlanTreeView extends material.StatelessWidget {
6+ /// highlighted; each node shows its share of the total cost as a bar. A click
7+ /// on a node shows every field the driver reported for it.
8+ class PlanTreeView extends material.StatefulWidget {
89 const PlanTreeView ({super .key, required this .root});
910
1011 final PlanNode root;
1112
13+ @override
14+ material.State <PlanTreeView > createState () => _PlanTreeViewState ();
15+ }
16+
17+ class _PlanTreeViewState extends material.State <PlanTreeView > {
18+ PlanNode ? _selected;
19+
1220 @override
1321 material.Widget build (material.BuildContext context) {
22+ final root = widget.root;
1423 final hottest = QueryPlanParser .hottest (root);
1524 final total = root.cost ?? _sumOwn (root);
1625 final rows = < material.Widget > [];
@@ -19,19 +28,25 @@ class PlanTreeView extends material.StatelessWidget {
1928 node: node,
2029 depth: depth,
2130 hottest: identical (node, hottest),
31+ selected: identical (node, _selected),
2232 total: total,
33+ onTap: () => setState (() => _selected = identical (node, _selected) ? null : node),
2334 ));
2435 for (final child in node.children) {
2536 add (child, depth + 1 );
2637 }
2738 }
2839
2940 add (root, 0 );
41+ final selected = _selected;
3042 return material.SingleChildScrollView (
3143 padding: const material.EdgeInsets .all (8 ),
3244 child: material.Column (
3345 crossAxisAlignment: material.CrossAxisAlignment .start,
34- children: rows,
46+ children: [
47+ ...rows,
48+ if (selected != null ) _DetailsPanel (node: selected),
49+ ],
3550 ),
3651 );
3752 }
@@ -50,13 +65,17 @@ class _PlanRow extends material.StatelessWidget {
5065 required this .node,
5166 required this .depth,
5267 required this .hottest,
68+ required this .selected,
5369 required this .total,
70+ required this .onTap,
5471 });
5572
5673 final PlanNode node;
5774 final int depth;
5875 final bool hottest;
76+ final bool selected;
5977 final double total;
78+ final material.VoidCallback onTap;
6079
6180 @override
6281 material.Widget build (material.BuildContext context) {
@@ -69,13 +88,20 @@ class _PlanRow extends material.StatelessWidget {
6988 '${node .actualRows == null ? '' : ' → ${node .actualRows !.round ()}' }' ;
7089 return material.Padding (
7190 padding: material.EdgeInsets .only (left: depth * 18.0 , bottom: 6 ),
72- child: material.Container (
91+ child: material.GestureDetector (
92+ behavior: material.HitTestBehavior .opaque,
93+ onTap: onTap,
94+ child: material.Container (
7395 padding: const material.EdgeInsets .symmetric (horizontal: 10 , vertical: 6 ),
7496 decoration: material.BoxDecoration (
7597 color: hottest ? wb.accent.withValues (alpha: 0.12 ) : null ,
7698 border: material.Border .all (
77- color: hottest ? wb.accent : wb.borderSubtle,
78- width: hottest ? 1.5 : 1 ,
99+ color: selected
100+ ? Theme .of (context).colorScheme.foreground
101+ : hottest
102+ ? wb.accent
103+ : wb.borderSubtle,
104+ width: hottest || selected ? 1.5 : 1 ,
79105 ),
80106 borderRadius: material.BorderRadius .circular (6 ),
81107 ),
@@ -107,6 +133,57 @@ class _PlanRow extends material.StatelessWidget {
107133 ],
108134 ],
109135 ),
136+ ),
137+ ),
138+ );
139+ }
140+ }
141+
142+ /// Every field the driver reported for a node, in the order it reported them.
143+ class _DetailsPanel extends material.StatelessWidget {
144+ const _DetailsPanel ({required this .node});
145+
146+ final PlanNode node;
147+
148+ @override
149+ material.Widget build (material.BuildContext context) {
150+ final wb = context.workbench;
151+ final fields = < String , String > {
152+ 'Operation' : node.operation,
153+ if (node.relation != null ) 'Relation' : node.relation! ,
154+ if (node.estimatedRows != null )
155+ 'Estimated rows' : '${node .estimatedRows !.round ()}' ,
156+ if (node.actualRows != null ) 'Actual rows' : '${node .actualRows !.round ()}' ,
157+ if (node.cost != null ) 'Cost' : node.cost! .toStringAsFixed (2 ),
158+ if (node.ownCost != null ) 'Own cost' : node.ownCost! .toStringAsFixed (2 ),
159+ ...node.details,
160+ };
161+ return material.Container (
162+ margin: const material.EdgeInsets .only (top: 8 ),
163+ padding: const material.EdgeInsets .all (10 ),
164+ decoration: material.BoxDecoration (
165+ border: material.Border .all (color: wb.borderSubtle),
166+ borderRadius: material.BorderRadius .circular (6 ),
167+ ),
168+ child: material.Column (
169+ crossAxisAlignment: material.CrossAxisAlignment .start,
170+ children: [
171+ for (final e in fields.entries)
172+ material.Padding (
173+ padding: const material.EdgeInsets .symmetric (vertical: 2 ),
174+ child: material.Row (
175+ crossAxisAlignment: material.CrossAxisAlignment .start,
176+ children: [
177+ material.SizedBox (
178+ width: 140 ,
179+ child: material.Text (e.key,
180+ style: material.TextStyle (color: wb.mutedForeground)),
181+ ),
182+ material.Expanded (child: material.SelectableText (e.value)),
183+ ],
184+ ),
185+ ),
186+ ],
110187 ),
111188 );
112189 }
0 commit comments