@marimo-team/islands 0.24.1-dev113 → 0.24.1-dev115
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/dist/{ConnectedDataExplorerComponent-CF7VGRML.js → ConnectedDataExplorerComponent-3vyrwCHu.js} +12 -12
- package/dist/{ErrorBoundary-Ca9Ulp6E.js → ErrorBoundary-QF0xsxMw.js} +3 -3
- package/dist/{any-language-editor-D6lJfTIq.js → any-language-editor-B2e3EkTe.js} +4 -4
- package/dist/{button-BeWeF5Z2.js → button-CKBkqR9J.js} +1 -1
- package/dist/{capabilities-C7Q0J4_N.js → capabilities-CMBBJTcc.js} +1 -1
- package/dist/{chat-ui-DwtGBnE7.js → chat-ui-BgGHMO2G.js} +16 -16
- package/dist/{check-DGKNz9MB.js → check-C_-c2CXl.js} +1 -1
- package/dist/{common-CHiGez3b.js → common-Cdx64lyR.js} +23 -23
- package/dist/{copy-T56I_gVc.js → copy-D4GHy7GA.js} +2 -2
- package/dist/{dist-CSjGX93W.js → dist-mymH-A_x.js} +2 -2
- package/dist/{dropdown-menu-D-pePvPo.js → dropdown-menu-Div6gwlw.js} +6 -6
- package/dist/{error-banner-C0gx40vn.js → error-banner-DmgOcFS3.js} +2 -2
- package/dist/{esm-aqYR5Dl6.js → esm-CmFEQJjP.js} +1 -1
- package/dist/{extends-G2pVMdwU.js → extends-CGTKDWHv.js} +4 -4
- package/dist/{formats-CX_fkiT0.js → formats-DuRRWVwb.js} +3 -3
- package/dist/{glide-data-editor-BXnH2j9C.js → glide-data-editor-ekJ5GwaZ.js} +8 -8
- package/dist/{glide-utils-AvKWWLSq.js → glide-utils-Dwc8Eh8G.js} +3 -3
- package/dist/{html-to-image-Cf3vfRXj.js → html-to-image-u8Ik6EnK.js} +20 -20
- package/dist/{label-BUGMEGks.js → label-DdGS_CZZ.js} +2 -2
- package/dist/{loader-BIZus5lj.js → loader-VZ__bv4g.js} +1 -1
- package/dist/main.js +38 -38
- package/dist/{mermaid-CzrpWuKV.js → mermaid-Bt45Zzl4.js} +4 -4
- package/dist/{process-output-BZs2qaau.js → process-output-DQiqt-_U.js} +3 -3
- package/dist/{reveal-component-BSz_VbT2.js → reveal-component-tQ-rgxcQ.js} +9 -9
- package/dist/{schemas-CLB4UWgG.js → schemas-BjeOpZLk.js} +1 -1
- package/dist/{select-foAi7oM_.js → select-DotAQ4jp.js} +12 -12
- package/dist/{spec-JFqQpvZq.js → spec-BesaYkGC.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-Bqzl6az1.js → swiper-component-C90YjhaV.js} +2 -2
- package/dist/{toDate-DxgaJjev.js → toDate-CrAIOASL.js} +3 -3
- package/dist/{tooltip-B1X8ECYn.js → tooltip-COXZxyl7.js} +3 -3
- package/dist/{useAsyncData-BkvtR_Y2.js → useAsyncData-x0_tWDWI.js} +1 -1
- package/dist/{useDateFormatter-DGIhtjdF.js → useDateFormatter-B3V__WqX.js} +2 -2
- package/dist/{useDeepCompareMemoize-DcZEHOmt.js → useDeepCompareMemoize-KaRHJEEU.js} +1 -1
- package/dist/{useIframeCapabilities-Bj4ORYGz.js → useIframeCapabilities-De3C-9b4.js} +1 -1
- package/dist/{useLifecycle-BHpzFL9q.js → useLifecycle-x0OriHo-.js} +10 -10
- package/dist/{useTheme-BlOgV7zf.js → useTheme-Bm9_FyqO.js} +2 -2
- package/dist/{vega-component-B4rwjseP.js → vega-component-nD2oP6WN.js} +9 -9
- package/package.json +1 -1
- package/src/components/dependency-graph/__tests__/elements.test.ts +151 -0
- package/src/components/dependency-graph/custom-edge.tsx +68 -0
- package/src/components/dependency-graph/custom-node.tsx +54 -26
- package/src/components/dependency-graph/dependency-graph-tree.tsx +110 -21
- package/src/components/dependency-graph/elements.ts +225 -31
- package/src/components/dependency-graph/panels.tsx +23 -2
- package/src/components/dependency-graph/utils/__tests__/layout.test.ts +78 -0
- package/src/components/dependency-graph/utils/layout.ts +108 -18
- package/src/components/ui/__tests__/badge.test.ts +63 -0
- package/src/components/ui/__tests__/button.test.ts +19 -0
- package/src/components/ui/__tests__/menu-items.test.ts +31 -0
- package/src/components/ui/badge.tsx +9 -7
- package/src/components/ui/button.tsx +1 -0
- package/src/components/ui/combobox.tsx +1 -1
- package/src/components/ui/date-input.tsx +1 -1
- package/src/components/ui/dialog.tsx +3 -1
- package/src/components/ui/menu-items.tsx +9 -9
- package/src/components/ui/native-select.tsx +2 -2
- package/src/components/ui/navigation.tsx +1 -1
- package/src/components/ui/range-slider.tsx +5 -3
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/toast.tsx +8 -5
- package/src/components/ui/toaster.tsx +2 -2
- package/src/css/globals.css +28 -0
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +25 -25
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { type Atom, atom, useAtomValue } from "jotai";
|
|
4
4
|
import { MapPinIcon } from "lucide-react";
|
|
5
|
-
import React, {
|
|
5
|
+
import React, {
|
|
6
|
+
type PropsWithChildren,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
6
11
|
import useEvent from "react-use-event-hook";
|
|
7
12
|
import ReactFlow, {
|
|
8
13
|
Background,
|
|
@@ -15,6 +20,7 @@ import ReactFlow, {
|
|
|
15
20
|
useNodesState,
|
|
16
21
|
useReactFlow,
|
|
17
22
|
} from "reactflow";
|
|
23
|
+
import { edgeTypes } from "@/components/dependency-graph/custom-edge";
|
|
18
24
|
import {
|
|
19
25
|
EdgeMarkerContext,
|
|
20
26
|
nodeTypes,
|
|
@@ -25,11 +31,16 @@ import type { CellData } from "@/core/cells/types";
|
|
|
25
31
|
import { store } from "@/core/state/jotai";
|
|
26
32
|
import type { Variables } from "@/core/variables/types";
|
|
27
33
|
import { Events } from "@/utils/events";
|
|
28
|
-
import { scrollAndHighlightCell } from "../editor/links/cell-link";
|
|
29
34
|
import { Tooltip } from "../ui/tooltip";
|
|
30
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
computeDefsByCell,
|
|
37
|
+
type NodeData,
|
|
38
|
+
nodeDimensions,
|
|
39
|
+
TreeElementsBuilder,
|
|
40
|
+
} from "./elements";
|
|
31
41
|
import { GraphSelectionPanel } from "./panels";
|
|
32
42
|
import type { GraphSelection, GraphSettings, LayoutDirection } from "./types";
|
|
43
|
+
import { extractCellPreview } from "./utils/cell-preview";
|
|
33
44
|
import { layoutElements } from "./utils/layout";
|
|
34
45
|
import { useFitToViewOnDimensionChange } from "./utils/useFitToViewOnDimensionChange";
|
|
35
46
|
|
|
@@ -43,6 +54,24 @@ interface Props {
|
|
|
43
54
|
|
|
44
55
|
const elementsBuilder = new TreeElementsBuilder();
|
|
45
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Apply the current expand/collapse selection to freshly-built nodes, resizing
|
|
59
|
+
* each so the layout engine and the DOM agree on node dimensions.
|
|
60
|
+
*/
|
|
61
|
+
function withExpansion(
|
|
62
|
+
nodes: Node<NodeData>[],
|
|
63
|
+
expandedIds: Set<CellId>,
|
|
64
|
+
): Node<NodeData>[] {
|
|
65
|
+
return nodes.map((node) => {
|
|
66
|
+
const data: NodeData = {
|
|
67
|
+
...node.data,
|
|
68
|
+
expanded: expandedIds.has(node.id as CellId),
|
|
69
|
+
};
|
|
70
|
+
const { width, height } = nodeDimensions(data);
|
|
71
|
+
return { ...node, data, width, height };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
|
|
47
76
|
cellIds,
|
|
48
77
|
variables,
|
|
@@ -51,22 +80,23 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
|
|
|
51
80
|
layoutDirection,
|
|
52
81
|
settings,
|
|
53
82
|
}) => {
|
|
83
|
+
// Cells whose node is expanded to show its full code (toggled by double-click).
|
|
84
|
+
const [expandedIds, setExpandedIds] = useState<Set<CellId>>(() => new Set());
|
|
85
|
+
|
|
54
86
|
// oxlint-disable-next-line react/hook-use-state
|
|
55
87
|
const [initial] = useState(() => {
|
|
56
|
-
|
|
88
|
+
const elements = elementsBuilder.createElements(
|
|
57
89
|
cellIds,
|
|
58
90
|
cellAtoms,
|
|
59
91
|
variables,
|
|
60
92
|
settings.hidePureMarkdown,
|
|
61
93
|
settings.hideReusableFunctions,
|
|
62
94
|
);
|
|
63
|
-
|
|
64
|
-
nodes: elements.nodes,
|
|
95
|
+
return layoutElements({
|
|
96
|
+
nodes: withExpansion(elements.nodes, expandedIds),
|
|
65
97
|
edges: elements.edges,
|
|
66
98
|
direction: layoutDirection,
|
|
67
99
|
});
|
|
68
|
-
|
|
69
|
-
return elements;
|
|
70
100
|
// Only run once
|
|
71
101
|
});
|
|
72
102
|
|
|
@@ -82,22 +112,68 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
|
|
|
82
112
|
edges: elements.edges,
|
|
83
113
|
direction: layoutDirection,
|
|
84
114
|
});
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
// Rebuilt elements are fresh objects; carry selection over so a
|
|
116
|
+
// re-layout (e.g. toggling expansion) doesn't silently clear it while
|
|
117
|
+
// the selection panel still targets the element.
|
|
118
|
+
setNodes((prev) => {
|
|
119
|
+
const selected = new Set(
|
|
120
|
+
prev.filter((node) => node.selected).map((node) => node.id),
|
|
121
|
+
);
|
|
122
|
+
return result.nodes.map((node) =>
|
|
123
|
+
selected.has(node.id) ? { ...node, selected: true } : node,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
setEdges((prev) => {
|
|
127
|
+
const selected = new Set(
|
|
128
|
+
prev.filter((edge) => edge.selected).map((edge) => edge.id),
|
|
129
|
+
);
|
|
130
|
+
return result.edges.map((edge) =>
|
|
131
|
+
selected.has(edge.id) ? { ...edge, selected: true } : edge,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
87
134
|
},
|
|
88
135
|
);
|
|
89
136
|
|
|
90
|
-
//
|
|
137
|
+
// Node sizes depend on live cell code: the line count when expanded, or the
|
|
138
|
+
// preview when a cell defines nothing. The cell atoms keep their identity
|
|
139
|
+
// across edits, so the rebuild effect below can't see code changes through
|
|
140
|
+
// its deps — subscribe to the size-relevant inputs explicitly.
|
|
141
|
+
const defsByCell = useMemo(() => computeDefsByCell(variables), [variables]);
|
|
142
|
+
const sizeSignature = useAtomValue(
|
|
143
|
+
useMemo(
|
|
144
|
+
() =>
|
|
145
|
+
atom((get) =>
|
|
146
|
+
cellIds
|
|
147
|
+
.map((cellId, index) => {
|
|
148
|
+
const code = get(cellAtoms[index]).code;
|
|
149
|
+
if (expandedIds.has(cellId)) {
|
|
150
|
+
return `e${code.trim().split("\n").length}`;
|
|
151
|
+
}
|
|
152
|
+
if ((defsByCell.get(cellId) ?? []).length > 0) {
|
|
153
|
+
// Collapsed nodes with defs size off the defs, not the code.
|
|
154
|
+
return "d";
|
|
155
|
+
}
|
|
156
|
+
return `p${extractCellPreview(code).text?.length ?? 0}`;
|
|
157
|
+
})
|
|
158
|
+
.join(),
|
|
159
|
+
),
|
|
160
|
+
[cellIds, cellAtoms, expandedIds, defsByCell],
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Rebuild + re-layout when the graph inputs or the expand/collapse set change.
|
|
91
165
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
settings.hideReusableFunctions,
|
|
99
|
-
),
|
|
166
|
+
const elements = elementsBuilder.createElements(
|
|
167
|
+
cellIds,
|
|
168
|
+
cellAtoms,
|
|
169
|
+
variables,
|
|
170
|
+
settings.hidePureMarkdown,
|
|
171
|
+
settings.hideReusableFunctions,
|
|
100
172
|
);
|
|
173
|
+
syncChanges({
|
|
174
|
+
nodes: withExpansion(elements.nodes, expandedIds),
|
|
175
|
+
edges: elements.edges,
|
|
176
|
+
});
|
|
101
177
|
}, [
|
|
102
178
|
cellIds,
|
|
103
179
|
variables,
|
|
@@ -105,6 +181,8 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
|
|
|
105
181
|
syncChanges,
|
|
106
182
|
settings.hidePureMarkdown,
|
|
107
183
|
settings.hideReusableFunctions,
|
|
184
|
+
expandedIds,
|
|
185
|
+
sizeSignature,
|
|
108
186
|
]);
|
|
109
187
|
|
|
110
188
|
const [selection, setSelection] = useState<GraphSelection>();
|
|
@@ -120,6 +198,7 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
|
|
|
120
198
|
nodes={nodes}
|
|
121
199
|
edges={edges}
|
|
122
200
|
nodeTypes={nodeTypes}
|
|
201
|
+
edgeTypes={edgeTypes}
|
|
123
202
|
minZoom={0.2}
|
|
124
203
|
fitViewOptions={{
|
|
125
204
|
minZoom: 0.5,
|
|
@@ -137,7 +216,17 @@ export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
|
|
|
137
216
|
});
|
|
138
217
|
}}
|
|
139
218
|
onNodeDoubleClick={(_event, node) => {
|
|
140
|
-
|
|
219
|
+
// Expand/collapse the node to reveal its full code in place.
|
|
220
|
+
const id = node.id as CellId;
|
|
221
|
+
setExpandedIds((prev) => {
|
|
222
|
+
const next = new Set(prev);
|
|
223
|
+
if (next.has(id)) {
|
|
224
|
+
next.delete(id);
|
|
225
|
+
} else {
|
|
226
|
+
next.add(id);
|
|
227
|
+
}
|
|
228
|
+
return next;
|
|
229
|
+
});
|
|
141
230
|
}}
|
|
142
231
|
fitView={true}
|
|
143
232
|
onNodesChange={onNodesChange}
|
|
@@ -4,26 +4,177 @@ import type { Atom } from "jotai";
|
|
|
4
4
|
import { type Edge, MarkerType, type Node, type NodeProps } from "reactflow";
|
|
5
5
|
import { getNotebook } from "@/core/cells/cells";
|
|
6
6
|
import type { CellId } from "@/core/cells/ids";
|
|
7
|
-
import type { CellData } from "@/core/cells/types";
|
|
7
|
+
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
8
8
|
import { store } from "@/core/state/jotai";
|
|
9
|
-
import type { Variables } from "@/core/variables/types";
|
|
9
|
+
import type { Variable, VariableName, Variables } from "@/core/variables/types";
|
|
10
10
|
import { Arrays } from "@/utils/arrays";
|
|
11
|
+
import { extractCellPreview } from "./utils/cell-preview";
|
|
12
|
+
|
|
13
|
+
/** The edge type registered for dependency edges (see `custom-edge.tsx`). */
|
|
14
|
+
export const DEPENDENCY_EDGE_TYPE = "dependency";
|
|
15
|
+
|
|
16
|
+
// Stroke/marker colors shared by the dependency edge and its arrow marker.
|
|
17
|
+
export const STATE_FLOW_EDGE_COLOR = "var(--amber-10)";
|
|
18
|
+
export const DEFAULT_EDGE_COLOR = "var(--gray-8)";
|
|
19
|
+
|
|
20
|
+
export interface DependencyEdgeData {
|
|
21
|
+
/** Variables carried across this edge. */
|
|
22
|
+
variables: VariableName[];
|
|
23
|
+
/** True when any carried variable is a mutable `State`/`SetFunctor` flow. */
|
|
24
|
+
isStateFlow: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A `State`/`SetFunctor` (from `mo.state`) is mutable mid-run, so it is drawn
|
|
28
|
+
// distinctly. `UIElement` is intentionally excluded: it is frontend-driven and
|
|
29
|
+
// immutable during a run.
|
|
30
|
+
const STATE_DATATYPES = new Set(["State", "SetFunctor"]);
|
|
31
|
+
|
|
32
|
+
export function isStateFlowVariable(variable: Variable): boolean {
|
|
33
|
+
return variable.dataType != null && STATE_DATATYPES.has(variable.dataType);
|
|
34
|
+
}
|
|
11
35
|
|
|
12
36
|
export interface NodeData {
|
|
13
37
|
atom: Atom<CellData>;
|
|
14
|
-
|
|
38
|
+
/** Variables this cell defines — the node's primary identity when collapsed. */
|
|
39
|
+
defs: VariableName[];
|
|
40
|
+
/** When true the node renders its full code instead of the compact ref list. */
|
|
41
|
+
expanded?: boolean;
|
|
15
42
|
}
|
|
16
43
|
export type CustomNodeProps = NodeProps<NodeData>;
|
|
17
44
|
|
|
45
|
+
const LINE_HEIGHT = 11; // matches TinyCode.css
|
|
46
|
+
|
|
18
47
|
export function getNodeHeight(linesOfCode: number) {
|
|
19
|
-
const LINE_HEIGHT = 11; // matches TinyCode.css
|
|
20
48
|
return Math.min(linesOfCode * LINE_HEIGHT + 35, 200);
|
|
21
49
|
}
|
|
22
50
|
|
|
51
|
+
// Compact-node sizing. A collapsed node wraps its defined variables into
|
|
52
|
+
// comma-separated lines at a character budget, so it grows taller rather than
|
|
53
|
+
// wider; the same budget drives the estimated width so dagre and the DOM agree.
|
|
54
|
+
export const NODE_HEADER_HEIGHT = 22;
|
|
55
|
+
const DEF_LINE_HEIGHT = 16;
|
|
56
|
+
const BODY_V_PADDING = 10;
|
|
57
|
+
const EXPANDED_NODE_WIDTH = 300;
|
|
58
|
+
const CHAR_WIDTH = 6.5; // text-xs monospace-ish
|
|
59
|
+
const NODE_H_PADDING = 22;
|
|
60
|
+
const MIN_NODE_WIDTH = 90;
|
|
61
|
+
const MAX_NODE_WIDTH = 240;
|
|
62
|
+
// Wrap defs to the longest single name, but never narrower than this — short
|
|
63
|
+
// names pack several per line, a long one gets a line to itself.
|
|
64
|
+
const WRAP_MIN_CHARS = 12;
|
|
65
|
+
// ... and never wider than fits MAX_NODE_WIDTH, or packed lines would be
|
|
66
|
+
// clipped by the width clamp. A single name over this budget still gets its
|
|
67
|
+
// own line; the renderer truncates it with an ellipsis and a title.
|
|
68
|
+
const WRAP_MAX_CHARS = Math.floor(
|
|
69
|
+
(MAX_NODE_WIDTH - NODE_H_PADDING) / CHAR_WIDTH,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export function collapsedNodeWidth(label: string): number {
|
|
73
|
+
const raw = label.length * CHAR_WIDTH + NODE_H_PADDING;
|
|
74
|
+
return Math.min(Math.max(raw, MIN_NODE_WIDTH), MAX_NODE_WIDTH);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The per-line character budget used to wrap a node's defs. */
|
|
78
|
+
function defsWrapBudget(defs: VariableName[]): number {
|
|
79
|
+
const longest = defs.reduce(
|
|
80
|
+
(max, def) => Math.max(max, def.length),
|
|
81
|
+
WRAP_MIN_CHARS,
|
|
82
|
+
);
|
|
83
|
+
return Math.min(longest, WRAP_MAX_CHARS);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Greedily pack `defs` into comma-separated lines, each no wider than the
|
|
88
|
+
* budget (the longest name, or `WRAP_MIN_CHARS`). No trailing commas.
|
|
89
|
+
*/
|
|
90
|
+
export function wrapDefs(defs: VariableName[]): string[] {
|
|
91
|
+
const budget = defsWrapBudget(defs);
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
let current = "";
|
|
94
|
+
for (const def of defs) {
|
|
95
|
+
if (current === "") {
|
|
96
|
+
current = def;
|
|
97
|
+
} else if (current.length + 2 + def.length <= budget) {
|
|
98
|
+
current = `${current}, ${def}`;
|
|
99
|
+
} else {
|
|
100
|
+
lines.push(current);
|
|
101
|
+
current = def;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (current !== "") {
|
|
105
|
+
lines.push(current);
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The lines shown in a collapsed node's body: its defs wrapped to a budget, or
|
|
112
|
+
* a single code-preview line when the cell defines nothing.
|
|
113
|
+
*/
|
|
114
|
+
export function nodeBodyLines(data: NodeData): string[] {
|
|
115
|
+
if (data.defs.length > 0) {
|
|
116
|
+
return wrapDefs(data.defs);
|
|
117
|
+
}
|
|
118
|
+
const preview = extractCellPreview(store.get(data.atom).code).text;
|
|
119
|
+
return preview ? [preview] : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function nodeDimensions(data: NodeData): {
|
|
123
|
+
width: number;
|
|
124
|
+
height: number;
|
|
125
|
+
} {
|
|
126
|
+
if (data.expanded) {
|
|
127
|
+
const lines = store.get(data.atom).code.trim().split("\n").length;
|
|
128
|
+
return { width: EXPANDED_NODE_WIDTH, height: getNodeHeight(lines) };
|
|
129
|
+
}
|
|
130
|
+
const lines = nodeBodyLines(data);
|
|
131
|
+
const widthChars =
|
|
132
|
+
data.defs.length > 0
|
|
133
|
+
? defsWrapBudget(data.defs)
|
|
134
|
+
: lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
135
|
+
const bodyLines = Math.max(lines.length, 1);
|
|
136
|
+
return {
|
|
137
|
+
width: collapsedNodeWidth("x".repeat(widthChars)),
|
|
138
|
+
height: NODE_HEADER_HEIGHT + bodyLines * DEF_LINE_HEIGHT + BODY_V_PADDING,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
23
142
|
// The nodes must have the same handle IDs to ensure edges connect correctly
|
|
24
143
|
export const OUTPUTS_HANDLE_ID = "outputs";
|
|
25
144
|
export const INPUTS_HANDLE_ID = "inputs";
|
|
26
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Cells hidden by the reusable-functions filter: those the kernel marked as
|
|
148
|
+
* valid top-level definitions (serialization hint `Valid`).
|
|
149
|
+
*/
|
|
150
|
+
export function reusableCellIds(
|
|
151
|
+
cellIds: CellId[],
|
|
152
|
+
cellRuntime: Record<CellId, CellRuntimeState>,
|
|
153
|
+
): Set<CellId> {
|
|
154
|
+
const reusable = new Set<CellId>();
|
|
155
|
+
for (const cellId of cellIds) {
|
|
156
|
+
if (cellRuntime[cellId]?.serialization?.toLowerCase() === "valid") {
|
|
157
|
+
reusable.add(cellId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return reusable;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Map each cell to the names of the variables it declares. */
|
|
164
|
+
export function computeDefsByCell(
|
|
165
|
+
variables: Variables,
|
|
166
|
+
): Map<CellId, VariableName[]> {
|
|
167
|
+
const defsByCell = new Map<CellId, VariableName[]>();
|
|
168
|
+
for (const variable of Object.values(variables)) {
|
|
169
|
+
for (const cellId of variable.declaredBy) {
|
|
170
|
+
const defs = defsByCell.get(cellId) ?? [];
|
|
171
|
+
defs.push(variable.name);
|
|
172
|
+
defsByCell.set(cellId, defs);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return defsByCell;
|
|
176
|
+
}
|
|
177
|
+
|
|
27
178
|
interface ElementsBuilder {
|
|
28
179
|
createElements: (
|
|
29
180
|
cellIds: CellId[],
|
|
@@ -59,16 +210,17 @@ export class VerticalElementsBuilder implements ElementsBuilder {
|
|
|
59
210
|
private createNode(
|
|
60
211
|
id: string,
|
|
61
212
|
atom: Atom<CellData>,
|
|
213
|
+
defs: VariableName[],
|
|
62
214
|
prevY: number,
|
|
63
215
|
): Node<NodeData> {
|
|
64
|
-
const
|
|
65
|
-
const height =
|
|
216
|
+
const data: NodeData = { atom, defs };
|
|
217
|
+
const { width, height } = nodeDimensions(data);
|
|
66
218
|
return {
|
|
67
219
|
id: id,
|
|
68
|
-
data
|
|
69
|
-
width
|
|
220
|
+
data,
|
|
221
|
+
width,
|
|
70
222
|
type: "custom",
|
|
71
|
-
height
|
|
223
|
+
height,
|
|
72
224
|
position: { x: 0, y: prevY + 20 },
|
|
73
225
|
};
|
|
74
226
|
}
|
|
@@ -83,8 +235,14 @@ export class VerticalElementsBuilder implements ElementsBuilder {
|
|
|
83
235
|
let prevY = 0;
|
|
84
236
|
const nodes: Node<NodeData>[] = [];
|
|
85
237
|
const edges: Edge[] = [];
|
|
238
|
+
const defsByCell = computeDefsByCell(variables);
|
|
86
239
|
for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
|
|
87
|
-
const node = this.createNode(
|
|
240
|
+
const node = this.createNode(
|
|
241
|
+
cellId,
|
|
242
|
+
cellAtom,
|
|
243
|
+
defsByCell.get(cellId) ?? [],
|
|
244
|
+
prevY,
|
|
245
|
+
);
|
|
88
246
|
nodes.push(node);
|
|
89
247
|
prevY = node.position.y + (node.height || 0);
|
|
90
248
|
}
|
|
@@ -110,16 +268,24 @@ export class VerticalElementsBuilder implements ElementsBuilder {
|
|
|
110
268
|
}
|
|
111
269
|
}
|
|
112
270
|
|
|
271
|
+
interface AggregatedEdge {
|
|
272
|
+
source: CellId;
|
|
273
|
+
target: CellId;
|
|
274
|
+
variables: VariableName[];
|
|
275
|
+
isStateFlow: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
113
278
|
export class TreeElementsBuilder implements ElementsBuilder {
|
|
114
|
-
private createEdge(
|
|
279
|
+
private createEdge(edge: AggregatedEdge): Edge<DependencyEdgeData> {
|
|
280
|
+
const { source, target, variables, isStateFlow } = edge;
|
|
115
281
|
return {
|
|
116
|
-
|
|
282
|
+
type: DEPENDENCY_EDGE_TYPE,
|
|
117
283
|
markerEnd: {
|
|
118
284
|
type: MarkerType.ArrowClosed,
|
|
285
|
+
color: isStateFlow ? STATE_FLOW_EDGE_COLOR : DEFAULT_EDGE_COLOR,
|
|
119
286
|
},
|
|
120
287
|
id: `${source}-${target}`,
|
|
121
|
-
|
|
122
|
-
style: { strokeWidth: 2 },
|
|
288
|
+
data: { variables, isStateFlow },
|
|
123
289
|
source: source,
|
|
124
290
|
// Use the same handle ids as the custom node
|
|
125
291
|
sourceHandle: OUTPUTS_HANDLE_ID,
|
|
@@ -128,15 +294,19 @@ export class TreeElementsBuilder implements ElementsBuilder {
|
|
|
128
294
|
};
|
|
129
295
|
}
|
|
130
296
|
|
|
131
|
-
private createNode(
|
|
132
|
-
|
|
133
|
-
|
|
297
|
+
private createNode(
|
|
298
|
+
id: string,
|
|
299
|
+
atom: Atom<CellData>,
|
|
300
|
+
defs: VariableName[],
|
|
301
|
+
): Node<NodeData> {
|
|
302
|
+
const data: NodeData = { atom, defs };
|
|
303
|
+
const { width, height } = nodeDimensions(data);
|
|
134
304
|
return {
|
|
135
305
|
id: id,
|
|
136
|
-
data
|
|
137
|
-
width
|
|
306
|
+
data,
|
|
307
|
+
width,
|
|
138
308
|
type: "custom",
|
|
139
|
-
height
|
|
309
|
+
height,
|
|
140
310
|
position: { x: 0, y: 0 },
|
|
141
311
|
};
|
|
142
312
|
}
|
|
@@ -149,10 +319,19 @@ export class TreeElementsBuilder implements ElementsBuilder {
|
|
|
149
319
|
hideReusableFunctions: boolean,
|
|
150
320
|
) {
|
|
151
321
|
const nodes: Node<NodeData>[] = [];
|
|
152
|
-
|
|
322
|
+
|
|
323
|
+
const cellRuntime = getNotebook().cellRuntime;
|
|
324
|
+
// Hidden reusable cells are dropped along with their edges — a reusable
|
|
325
|
+
// function is nearly always referenced somewhere, so gating on "no edges"
|
|
326
|
+
// would make the filter a no-op.
|
|
327
|
+
const hiddenReusable = hideReusableFunctions
|
|
328
|
+
? reusableCellIds(cellIds, cellRuntime)
|
|
329
|
+
: new Set<CellId>();
|
|
153
330
|
|
|
154
331
|
const nodesWithEdges = new Set<CellId>();
|
|
155
|
-
|
|
332
|
+
// Dedupe cell→cell edges, aggregating every variable that crosses them so a
|
|
333
|
+
// single edge can carry multiple refs and flag whether any is a state flow.
|
|
334
|
+
const edgesByPair = new Map<string, AggregatedEdge>();
|
|
156
335
|
|
|
157
336
|
for (const variable of Object.values(variables)) {
|
|
158
337
|
// Skip marimo, since likely every cell uses it
|
|
@@ -160,40 +339,55 @@ export class TreeElementsBuilder implements ElementsBuilder {
|
|
|
160
339
|
continue;
|
|
161
340
|
}
|
|
162
341
|
|
|
342
|
+
const isState = isStateFlowVariable(variable);
|
|
163
343
|
const { declaredBy, usedBy } = variable;
|
|
164
344
|
for (const fromId of declaredBy) {
|
|
165
345
|
for (const toId of usedBy) {
|
|
166
|
-
|
|
167
|
-
if (visited.has(key)) {
|
|
346
|
+
if (hiddenReusable.has(fromId) || hiddenReusable.has(toId)) {
|
|
168
347
|
continue;
|
|
169
348
|
}
|
|
170
|
-
|
|
349
|
+
const key = `${fromId}-${toId}`;
|
|
350
|
+
const existing = edgesByPair.get(key);
|
|
351
|
+
if (existing) {
|
|
352
|
+
existing.variables.push(variable.name);
|
|
353
|
+
existing.isStateFlow ||= isState;
|
|
354
|
+
} else {
|
|
355
|
+
edgesByPair.set(key, {
|
|
356
|
+
source: fromId,
|
|
357
|
+
target: toId,
|
|
358
|
+
variables: [variable.name],
|
|
359
|
+
isStateFlow: isState,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
171
362
|
nodesWithEdges.add(fromId);
|
|
172
363
|
nodesWithEdges.add(toId);
|
|
173
|
-
edges.push(this.createEdge(fromId, toId));
|
|
174
364
|
}
|
|
175
365
|
}
|
|
176
366
|
}
|
|
177
367
|
|
|
178
|
-
const
|
|
368
|
+
const edges = [...edgesByPair.values()].map((edge) =>
|
|
369
|
+
this.createEdge(edge),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const defsByCell = computeDefsByCell(variables);
|
|
179
373
|
|
|
180
374
|
for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
|
|
181
375
|
const code = store.get(cellAtom).code.trim();
|
|
182
376
|
const hasEdge = nodesWithEdges.has(cellId);
|
|
183
377
|
const isMarkdown = code.startsWith("mo.md");
|
|
184
|
-
const runtime = cellRuntime[cellId];
|
|
185
|
-
const isReusable = runtime?.serialization?.toLowerCase() === "valid";
|
|
186
378
|
|
|
187
379
|
// Apply filters
|
|
188
380
|
if (hidePureMarkdown && isMarkdown && !hasEdge) {
|
|
189
381
|
continue;
|
|
190
382
|
}
|
|
191
|
-
if (
|
|
383
|
+
if (hiddenReusable.has(cellId)) {
|
|
192
384
|
continue;
|
|
193
385
|
}
|
|
194
386
|
|
|
195
387
|
// Show every cell that wasn't filtered out
|
|
196
|
-
nodes.push(
|
|
388
|
+
nodes.push(
|
|
389
|
+
this.createNode(cellId, cellAtom, defsByCell.get(cellId) ?? []),
|
|
390
|
+
);
|
|
197
391
|
}
|
|
198
392
|
|
|
199
393
|
return { nodes, edges };
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ArrowRightFromLineIcon,
|
|
5
5
|
ArrowRightIcon,
|
|
6
6
|
ArrowRightToLineIcon,
|
|
7
|
+
ExternalLinkIcon,
|
|
7
8
|
MoreVerticalIcon,
|
|
8
9
|
NetworkIcon,
|
|
9
10
|
SettingsIcon,
|
|
@@ -13,17 +14,18 @@ import {
|
|
|
13
14
|
} from "lucide-react";
|
|
14
15
|
import React, { memo, useId } from "react";
|
|
15
16
|
import { type Edge, Panel } from "reactflow";
|
|
16
|
-
import { getCellEditorView } from "@/core/cells/cells";
|
|
17
|
+
import { getCellEditorView, useCellActions } from "@/core/cells/cells";
|
|
17
18
|
import type { CellId } from "@/core/cells/ids";
|
|
18
19
|
import { goToVariableDefinition } from "@/core/codemirror/go-to-definition/commands";
|
|
19
20
|
import type { Variable, Variables } from "@/core/variables/types";
|
|
20
21
|
import { ConnectionCellActionsDropdown } from "../editor/cell/cell-actions";
|
|
21
|
-
import { CellLink } from "../editor/links/cell-link";
|
|
22
|
+
import { CellLink, scrollAndHighlightCell } from "../editor/links/cell-link";
|
|
22
23
|
import { CellLinkList } from "../editor/links/cell-link-list";
|
|
23
24
|
import { Button } from "../ui/button";
|
|
24
25
|
import { Checkbox } from "../ui/checkbox";
|
|
25
26
|
import { Label } from "../ui/label";
|
|
26
27
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
28
|
+
import { Tooltip } from "../ui/tooltip";
|
|
27
29
|
import { VariableName } from "../variables/common";
|
|
28
30
|
import type { GraphSelection, GraphSettings, LayoutDirection } from "./types";
|
|
29
31
|
|
|
@@ -122,6 +124,8 @@ export const GraphSelectionPanel: React.FC<{
|
|
|
122
124
|
edges: Edge[];
|
|
123
125
|
variables: Variables;
|
|
124
126
|
}> = memo(({ selection, variables, onClearSelection }) => {
|
|
127
|
+
const { showCellIfHidden } = useCellActions();
|
|
128
|
+
|
|
125
129
|
if (!selection) {
|
|
126
130
|
return null;
|
|
127
131
|
}
|
|
@@ -194,6 +198,23 @@ export const GraphSelectionPanel: React.FC<{
|
|
|
194
198
|
<SquareFunction className="w-5 h-5" />
|
|
195
199
|
<CellLink cellId={selection.id} />
|
|
196
200
|
<div className="flex-1" />
|
|
201
|
+
<Tooltip content="Open cell in editor" delayDuration={200}>
|
|
202
|
+
<Button
|
|
203
|
+
aria-label="Open cell in editor"
|
|
204
|
+
variant="text"
|
|
205
|
+
size="icon"
|
|
206
|
+
onClick={() => {
|
|
207
|
+
// The cell may sit in a collapsed section; reveal it first
|
|
208
|
+
// or the scroll target won't be in the DOM.
|
|
209
|
+
showCellIfHidden({ cellId: selection.id });
|
|
210
|
+
requestAnimationFrame(() => {
|
|
211
|
+
scrollAndHighlightCell(selection.id, "focus");
|
|
212
|
+
});
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<ExternalLinkIcon className="w-4 h-4" />
|
|
216
|
+
</Button>
|
|
217
|
+
</Tooltip>
|
|
197
218
|
<ConnectionCellActionsDropdown cellId={selection.id}>
|
|
198
219
|
<Button variant="ghost" size="icon">
|
|
199
220
|
<MoreVerticalIcon className="w-4 h-4" />
|