panel-reactflow 0.3.1a0__tar.gz → 0.4.0b0__tar.gz
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.
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/PKG-INFO +1 -1
- panel_reactflow-0.4.0b0/docs/how-to/context-menu.md +105 -0
- panel_reactflow-0.4.0b0/docs/how-to/control-handle-connectivity.md +336 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/how-to/define-nodes-edges.md +24 -1
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/how-to/react-to-events.md +1 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/index.md +1 -0
- panel_reactflow-0.4.0b0/examples/context_menu.py +82 -0
- panel_reactflow-0.4.0b0/examples/edge_types_comparison.py +102 -0
- panel_reactflow-0.4.0b0/examples/smart_edges_example.py +99 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/pixi.lock +10007 -9930
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/pixi.toml +1 -1
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/base.py +134 -21
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/dist/css/reactflow.css +9 -0
- panel_reactflow-0.4.0b0/src/panel_reactflow/dist/panel-reactflow.bundle.js +88 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/models/reactflow.jsx +291 -38
- panel_reactflow-0.4.0b0/tests/test_connectable_handles.py +293 -0
- panel_reactflow-0.4.0b0/tests/test_connectable_integration.py +259 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/tests/test_core.py +5 -1
- panel_reactflow-0.4.0b0/tests/ui/test_context_menu.py +152 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/tests/ui/test_ui.py +243 -0
- panel_reactflow-0.3.1a0/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -88
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.copier-answers.yml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.gitattributes +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.github/CODEOWNERS +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.github/dependabot.yml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.github/workflows/build.yml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.github/workflows/docs.yml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.github/workflows/test.yml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.gitignore +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.pre-commit-config.yaml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/.prettierrc +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/LICENSE.txt +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/MANIFEST.in +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/README.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/logo.svg +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/declare-types.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/define-editors-edge.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/define-editors-node.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/define-nodes-edges.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/advanced.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/custom_editor.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/edge_editors.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/schema_types.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/simple.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/quickstart.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/react-to-events.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/assets/screenshots/style-nodes-edges.png +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/advanced.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/custom-editor.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/edge-editors.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/index.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/node-edge-instances.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/schema-types.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/simple.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/threejs-viewer-instances.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/examples/threejs-viewer.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/how-to/declare-types.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/how-to/define-editors.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/how-to/embed-views-in-nodes.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/how-to/style-nodes-edges.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/quickstart.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/reference/panel_reactflow.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/docs/releases.md +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/advanced.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/custom_editor.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/edge_editors.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/node_edge_instances.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/schema_types.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/simple.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/threejs_viewer.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/examples/threejs_viewer_instances.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/hatch_build.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/pyproject.toml +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/__init__.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/__version.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/dist/icons/gear.svg +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/py.typed +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/src/panel_reactflow/schema.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/tests/__init__.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/tests/conftest.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/tests/test_api.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/tests/ui/__init__.py +0 -0
- {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0b0}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: panel-reactflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0b0
|
|
4
4
|
Summary: A Panel wrapper for the Reactflow JS library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/panel-extensions/panel-reactflow
|
|
6
6
|
Project-URL: Source, https://github.com/panel-extensions/panel-reactflow
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Node Context Menus
|
|
2
|
+
|
|
3
|
+
Panel-ReactFlow supports per-node context menus that appear on right-click.
|
|
4
|
+
Define the menu content by overriding the `context_menu()` method on a `Node`
|
|
5
|
+
subclass. The method returns any Panel component, which is rendered as a
|
|
6
|
+
floating overlay at the click position.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Define a context menu
|
|
11
|
+
|
|
12
|
+
Override `context_menu()` on your `Node` subclass to return a Panel component.
|
|
13
|
+
The menu is dismissed automatically when the user clicks elsewhere on the
|
|
14
|
+
canvas.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import panel as pn
|
|
18
|
+
import panel_material_ui as pmui
|
|
19
|
+
from panel_reactflow import Node, ReactFlow
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskNode(Node):
|
|
23
|
+
def context_menu(self):
|
|
24
|
+
return pn.Column(
|
|
25
|
+
pmui.Button(name="Run", variant="text", size="small"),
|
|
26
|
+
pmui.Button(name="Delete", variant="text", color="error", size="small"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
flow = ReactFlow(nodes=[
|
|
31
|
+
TaskNode(id="t1", position={"x": 0, "y": 0}, label="My Task", data={}),
|
|
32
|
+
])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Access node state in the menu
|
|
38
|
+
|
|
39
|
+
The `context_menu()` method runs on the node instance, so you have access to
|
|
40
|
+
all its parameters and the parent flow via `self.flow`.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
class PipelineNode(Node):
|
|
44
|
+
status = param.Selector(
|
|
45
|
+
default="idle", objects=["idle", "running", "done"], precedence=0
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def context_menu(self):
|
|
49
|
+
def set_status(status):
|
|
50
|
+
self.flow.patch_node_data(self.id, {"status": status})
|
|
51
|
+
# Close the menu after action
|
|
52
|
+
self.flow._handle_msg({"type": "close_context_menu"})
|
|
53
|
+
|
|
54
|
+
return pn.Column(
|
|
55
|
+
pn.pane.Markdown(f"**{self.label}** ({self.status})"),
|
|
56
|
+
pmui.Button(
|
|
57
|
+
name="Start", variant="text", size="small",
|
|
58
|
+
on_click=lambda e: set_status("running"),
|
|
59
|
+
),
|
|
60
|
+
pmui.Button(
|
|
61
|
+
name="Delete", variant="text", color="error", size="small",
|
|
62
|
+
on_click=lambda e: self.flow.remove_node(self.id),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Close the menu programmatically
|
|
70
|
+
|
|
71
|
+
The context menu closes when the user clicks anywhere on the canvas pane.
|
|
72
|
+
To close it from a button callback (e.g. after performing an action), send
|
|
73
|
+
the close message:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
self.flow._handle_msg({"type": "close_context_menu"})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Listen for context menu events
|
|
82
|
+
|
|
83
|
+
You can also react to the right-click event without rendering a menu by
|
|
84
|
+
subscribing to the `"node_context_menu"` event:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
def on_context(payload, flow):
|
|
88
|
+
print(f"Right-clicked node {payload['node_id']} at {payload['position']}")
|
|
89
|
+
|
|
90
|
+
flow.on("node_context_menu", on_context)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The payload includes `node_id` and `position` (with `x` and `y` screen
|
|
94
|
+
coordinates).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Tips
|
|
99
|
+
|
|
100
|
+
- Return `None` from `context_menu()` to disable the menu for specific nodes
|
|
101
|
+
(this is the default behavior).
|
|
102
|
+
- Only `Node` subclass instances support context menus. Dict-based nodes do
|
|
103
|
+
not trigger a context menu on right-click.
|
|
104
|
+
- The menu overlay uses the `.rf-context-menu` CSS class for styling. Override
|
|
105
|
+
it in a custom stylesheet to change appearance.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Control Handle Connectivity
|
|
2
|
+
|
|
3
|
+
Restrict which connections users can create by configuring the connectable
|
|
4
|
+
flags on `NodeType`. This lets you enforce directional flow (sources, sinks,
|
|
5
|
+
transforms) and prevent invalid edges at the UI level.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Problem
|
|
12
|
+
|
|
13
|
+
By default, all handles are fully connectable — users can drag edges from or
|
|
14
|
+
to any handle on any node. But many graph types have directional semantics:
|
|
15
|
+
|
|
16
|
+
- **Data sources** should only output data, never accept input
|
|
17
|
+
- **Data sinks** should only accept input, never produce output
|
|
18
|
+
- **Monitor nodes** might accept input but only emit status (one direction)
|
|
19
|
+
- **Read-only nodes** might display data without allowing any new connections
|
|
20
|
+
|
|
21
|
+
Without restrictions, users can create semantically invalid edges that break
|
|
22
|
+
your application's logic.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Solution
|
|
27
|
+
|
|
28
|
+
Use the six `*_connectable*` flags on `NodeType` to control which drag
|
|
29
|
+
operations are allowed for input and output handles.
|
|
30
|
+
|
|
31
|
+
### Available flags
|
|
32
|
+
|
|
33
|
+
| Flag | Default | Controls |
|
|
34
|
+
|------|---------|----------|
|
|
35
|
+
| `input_connectable` | `True` | Whether input handles are connectable at all |
|
|
36
|
+
| `input_connectable_start` | `True` | Whether edges can **start from** input handles |
|
|
37
|
+
| `input_connectable_end` | `True` | Whether edges can **end at** input handles |
|
|
38
|
+
| `output_connectable` | `True` | Whether output handles are connectable at all |
|
|
39
|
+
| `output_connectable_start` | `True` | Whether edges can **start from** output handles |
|
|
40
|
+
| `output_connectable_end` | `True` | Whether edges can **end at** output handles |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Common patterns
|
|
45
|
+
|
|
46
|
+
### Data source (output only)
|
|
47
|
+
|
|
48
|
+
Produces data but cannot accept incoming connections:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from panel_reactflow import NodeType
|
|
52
|
+
|
|
53
|
+
source = NodeType(
|
|
54
|
+
type="source",
|
|
55
|
+
label="Data Source",
|
|
56
|
+
outputs=["data"],
|
|
57
|
+
output_connectable_start=True, # Can drag FROM output
|
|
58
|
+
output_connectable_end=False, # Cannot drag TO output
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Valid**: Drag from source output → another node's input ✓
|
|
63
|
+
**Invalid**: Drag from another node → source output ✗
|
|
64
|
+
|
|
65
|
+
### Data sink (input only)
|
|
66
|
+
|
|
67
|
+
Consumes data but cannot produce outgoing connections:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
sink = NodeType(
|
|
71
|
+
type="sink",
|
|
72
|
+
label="Data Sink",
|
|
73
|
+
inputs=["data"],
|
|
74
|
+
input_connectable_start=False, # Cannot drag FROM input
|
|
75
|
+
input_connectable_end=True, # Can drag TO input
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Valid**: Drag from another node's output → sink input ✓
|
|
80
|
+
**Invalid**: Drag from sink input → another node ✗
|
|
81
|
+
|
|
82
|
+
### Transform (bidirectional)
|
|
83
|
+
|
|
84
|
+
Full connectivity in both directions (default):
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
transform = NodeType(
|
|
88
|
+
type="transform",
|
|
89
|
+
label="Transform",
|
|
90
|
+
inputs=["in"],
|
|
91
|
+
outputs=["out"],
|
|
92
|
+
# All flags default to True — no need to specify
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Valid**: Any drag operation ✓
|
|
97
|
+
|
|
98
|
+
### Monitor node
|
|
99
|
+
|
|
100
|
+
Accepts input and emits status, but status cannot receive incoming edges:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
monitor = NodeType(
|
|
104
|
+
type="monitor",
|
|
105
|
+
label="Monitor",
|
|
106
|
+
inputs=["in"],
|
|
107
|
+
outputs=["status"],
|
|
108
|
+
input_connectable_start=False, # Cannot start edges from input
|
|
109
|
+
output_connectable_end=False, # Cannot end edges at output
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Valid**: Drag from another node → monitor input ✓
|
|
114
|
+
**Valid**: Drag from monitor output → another node ✓
|
|
115
|
+
**Invalid**: Drag from monitor input → another node ✗
|
|
116
|
+
**Invalid**: Drag from another node → monitor output ✗
|
|
117
|
+
|
|
118
|
+
### Read-only node
|
|
119
|
+
|
|
120
|
+
Displays data but allows no new connections:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
readonly = NodeType(
|
|
124
|
+
type="readonly",
|
|
125
|
+
label="Read Only",
|
|
126
|
+
inputs=["in"],
|
|
127
|
+
outputs=["out"],
|
|
128
|
+
input_connectable=False,
|
|
129
|
+
output_connectable=False,
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Invalid**: Any drag operation ✗
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Complete working example
|
|
138
|
+
|
|
139
|
+
This example builds a data pipeline with sources, transforms, and sinks:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import panel as pn
|
|
143
|
+
from panel_reactflow import NodeType, NodeSpec, EdgeSpec, ReactFlow
|
|
144
|
+
|
|
145
|
+
pn.extension("jsoneditor")
|
|
146
|
+
|
|
147
|
+
# Define node types
|
|
148
|
+
node_types = {
|
|
149
|
+
"source": NodeType(
|
|
150
|
+
type="source",
|
|
151
|
+
label="Data Source",
|
|
152
|
+
outputs=["data"],
|
|
153
|
+
output_connectable_start=True,
|
|
154
|
+
output_connectable_end=False,
|
|
155
|
+
),
|
|
156
|
+
"transform": NodeType(
|
|
157
|
+
type="transform",
|
|
158
|
+
label="Transform",
|
|
159
|
+
inputs=["in"],
|
|
160
|
+
outputs=["out"],
|
|
161
|
+
),
|
|
162
|
+
"monitor": NodeType(
|
|
163
|
+
type="monitor",
|
|
164
|
+
label="Monitor",
|
|
165
|
+
inputs=["in"],
|
|
166
|
+
outputs=["status"],
|
|
167
|
+
input_connectable_start=False,
|
|
168
|
+
output_connectable_end=False,
|
|
169
|
+
),
|
|
170
|
+
"sink": NodeType(
|
|
171
|
+
type="sink",
|
|
172
|
+
label="Data Sink",
|
|
173
|
+
inputs=["data"],
|
|
174
|
+
input_connectable_start=False,
|
|
175
|
+
input_connectable_end=True,
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Build pipeline
|
|
180
|
+
flow = ReactFlow(
|
|
181
|
+
nodes=[
|
|
182
|
+
NodeSpec(id="source1", type="source", position={"x": 100, "y": 150}, data={}).to_dict(),
|
|
183
|
+
NodeSpec(id="transform1", type="transform", position={"x": 350, "y": 150}, data={}).to_dict(),
|
|
184
|
+
NodeSpec(id="monitor1", type="monitor", position={"x": 600, "y": 150}, data={}).to_dict(),
|
|
185
|
+
NodeSpec(id="sink1", type="sink", position={"x": 850, "y": 150}, data={}).to_dict(),
|
|
186
|
+
],
|
|
187
|
+
edges=[
|
|
188
|
+
EdgeSpec(id="e1", source="source1", target="transform1", sourceHandle="data", targetHandle="in").to_dict(),
|
|
189
|
+
EdgeSpec(id="e2", source="transform1", target="monitor1", sourceHandle="out", targetHandle="in").to_dict(),
|
|
190
|
+
EdgeSpec(id="e3", source="monitor1", target="sink1", sourceHandle="status", targetHandle="data").to_dict(),
|
|
191
|
+
],
|
|
192
|
+
node_types=node_types,
|
|
193
|
+
width=1200,
|
|
194
|
+
height=400,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
flow.servable()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Try these interactions
|
|
201
|
+
|
|
202
|
+
1. **Valid**: Drag from source output → transform input ✓
|
|
203
|
+
2. **Valid**: Drag from transform output → monitor input ✓
|
|
204
|
+
3. **Valid**: Drag from monitor output → sink input ✓
|
|
205
|
+
4. **Invalid**: Try to drag TO source output ✗ (blocked)
|
|
206
|
+
5. **Invalid**: Try to drag FROM sink input ✗ (blocked)
|
|
207
|
+
6. **Invalid**: Try to drag TO monitor output ✗ (blocked)
|
|
208
|
+
|
|
209
|
+
The UI automatically prevents invalid connections — handles show different
|
|
210
|
+
cursor behavior and won't accept or initiate drag operations when restricted.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Multiple handles per side
|
|
215
|
+
|
|
216
|
+
Connectable flags apply to **all handles on a given side** (input or output).
|
|
217
|
+
If a node has multiple input handles, `input_connectable_start=False` affects
|
|
218
|
+
all of them:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
multi_input = NodeType(
|
|
222
|
+
type="multi",
|
|
223
|
+
label="Multi-Input",
|
|
224
|
+
inputs=["in1", "in2", "in3"], # Three input handles
|
|
225
|
+
outputs=["out"],
|
|
226
|
+
input_connectable_start=False, # Applies to all input handles
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
All three input handles will respect the same connectivity rules.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Programmatic edges vs UI restrictions
|
|
235
|
+
|
|
236
|
+
Connectable flags only affect **user drag interactions** in the UI. You can
|
|
237
|
+
still create edges programmatically regardless of the flags:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# This edge will be created even if handles are non-connectable
|
|
241
|
+
flow.add_edge(EdgeSpec(id="e1", source="source1", target="sink1"))
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Or by passing edges directly to `ReactFlow`:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
edges = [
|
|
248
|
+
EdgeSpec(id="e1", source="source1", target="sink1").to_dict()
|
|
249
|
+
]
|
|
250
|
+
flow = ReactFlow(nodes=nodes, edges=edges, node_types=node_types)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The flags control what users can do via drag-and-drop, not what your code
|
|
254
|
+
can create.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Flag independence
|
|
259
|
+
|
|
260
|
+
Each flag is independent. Setting one doesn't affect others:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# Only restrict starting edges from inputs
|
|
264
|
+
node = NodeType(
|
|
265
|
+
type="restricted",
|
|
266
|
+
inputs=["in"],
|
|
267
|
+
outputs=["out"],
|
|
268
|
+
input_connectable_start=False, # Only this flag changes
|
|
269
|
+
# input_connectable=True (default)
|
|
270
|
+
# input_connectable_end=True (default)
|
|
271
|
+
# All output_* flags also default to True
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Backwards compatibility
|
|
278
|
+
|
|
279
|
+
Existing code without connectable flags continues to work — all flags default
|
|
280
|
+
to `True`:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
# Old-style node type definition
|
|
284
|
+
legacy = NodeType(
|
|
285
|
+
type="task",
|
|
286
|
+
label="Task",
|
|
287
|
+
inputs=["in"],
|
|
288
|
+
outputs=["out"],
|
|
289
|
+
)
|
|
290
|
+
# Behaves exactly as before — all handles fully connectable
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Use cases
|
|
296
|
+
|
|
297
|
+
### ETL pipelines
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
extract = NodeType(type="extract", outputs=["data"], output_connectable_end=False)
|
|
301
|
+
transform = NodeType(type="transform", inputs=["in"], outputs=["out"])
|
|
302
|
+
load = NodeType(type="load", inputs=["data"], input_connectable_start=False)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### State machines
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
start = NodeType(type="start", outputs=["next"], output_connectable_end=False)
|
|
309
|
+
state = NodeType(type="state", inputs=["in"], outputs=["out"])
|
|
310
|
+
end = NodeType(type="end", inputs=["in"], input_connectable_start=False)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### DAG workflows
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
trigger = NodeType(type="trigger", outputs=["event"], output_connectable_end=False)
|
|
317
|
+
task = NodeType(type="task", inputs=["trigger"], outputs=["result"])
|
|
318
|
+
logger = NodeType(type="logger", inputs=["log"], input_connectable_start=False)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Tips
|
|
324
|
+
|
|
325
|
+
- **Start with defaults**: Don't add restrictions until you need them.
|
|
326
|
+
- **Test in UI**: Try dragging to confirm the restrictions work as expected.
|
|
327
|
+
- **Use patterns**: The source/sink/transform/monitor patterns cover most use cases.
|
|
328
|
+
- **Document intent**: Add comments explaining why specific flags are set.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Related
|
|
333
|
+
|
|
334
|
+
- [Declare Node & Edge Types](declare-types.md) — full type declaration guide
|
|
335
|
+
- [Define Nodes & Edges](define-nodes-edges.md) — node and edge structure
|
|
336
|
+
- [API Reference](../reference/panel_reactflow.md) — complete API documentation
|
|
@@ -155,11 +155,34 @@ edges = [
|
|
|
155
155
|
| `source` | yes | ID of the source node. |
|
|
156
156
|
| `target` | yes | ID of the target node. |
|
|
157
157
|
| `label` | no | Text rendered on the edge. |
|
|
158
|
-
| `type` | no | Edge type
|
|
158
|
+
| `type` | no | Edge type (see built-in types below, or a custom type name). |
|
|
159
159
|
| `data` | no | Arbitrary dict of payload data. |
|
|
160
160
|
| `sourceHandle` | no | Specific output handle on the source node. |
|
|
161
161
|
| `targetHandle` | no | Specific input handle on the target node. |
|
|
162
162
|
|
|
163
|
+
### Built-in edge types
|
|
164
|
+
|
|
165
|
+
| Type | Description |
|
|
166
|
+
|------------------|-------------|
|
|
167
|
+
| `"bezier"` | Smooth bezier curve (default). |
|
|
168
|
+
| `"straight"` | Straight line between nodes. |
|
|
169
|
+
| `"step"` | Orthogonal path with right angles. |
|
|
170
|
+
| `"smoothstep"` | Step path with rounded corners. |
|
|
171
|
+
| `"smart_bezier"` | Bezier curve that automatically routes around nodes. |
|
|
172
|
+
| `"smart_straight"`| Straight segments that automatically route around nodes. |
|
|
173
|
+
| `"smart_step"` | Step path that automatically routes around nodes. |
|
|
174
|
+
|
|
175
|
+
Smart edge types use pathfinding to avoid overlapping with other nodes in
|
|
176
|
+
the graph. They are useful when edges would otherwise pass through
|
|
177
|
+
intermediate nodes.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
edges = [
|
|
181
|
+
{"id": "e1", "source": "n1", "target": "n2", "type": "smoothstep"},
|
|
182
|
+
{"id": "e2", "source": "n1", "target": "n3", "type": "smart_bezier"},
|
|
183
|
+
]
|
|
184
|
+
```
|
|
185
|
+
|
|
163
186
|
---
|
|
164
187
|
|
|
165
188
|
## Define edges as classes
|
|
@@ -23,6 +23,7 @@ the `ReactFlow` instance as a second argument. You can also listen for
|
|
|
23
23
|
| `node_deleted` | A node is removed. | `node_id` |
|
|
24
24
|
| `node_moved` | A node is dragged to a new position. | `node_id`, `position` |
|
|
25
25
|
| `node_clicked` | A node is clicked (single click). | `node_id` |
|
|
26
|
+
| `node_context_menu` | A node is right-clicked. | `node_id`, `position` |
|
|
26
27
|
| `node_data_changed` | Node data is patched (via API, editor patch, or parameter-driven sync). | `node_id`, `patch` |
|
|
27
28
|
| `edge_added` | An edge is created (UI connect or API). | `edge` |
|
|
28
29
|
| `edge_deleted` | An edge is removed. | `edge_id` |
|
|
@@ -61,6 +61,7 @@ flow.servable()
|
|
|
61
61
|
|
|
62
62
|
- [Define Nodes & Edges](how-to/define-nodes-edges.md)
|
|
63
63
|
- [Declare Node & Edge Types](how-to/declare-types.md)
|
|
64
|
+
- [Control Handle Connectivity](how-to/control-handle-connectivity.md) — restrict connections
|
|
64
65
|
- [Define Editors](how-to/define-editors.md) — node *and* edge editors
|
|
65
66
|
- [Embed Views in Nodes](how-to/embed-views-in-nodes.md)
|
|
66
67
|
- [Style Nodes & Edges](how-to/style-nodes-edges.md)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Context menu example using Node subclasses.
|
|
2
|
+
|
|
3
|
+
Demonstrates:
|
|
4
|
+
- Per-node context menus via ``Node.context_menu()``
|
|
5
|
+
- Dynamic menu content based on node state
|
|
6
|
+
- Closing the menu on action (via ``flow.patch_node_data``)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import panel as pn
|
|
10
|
+
import panel_material_ui as pmui
|
|
11
|
+
import param
|
|
12
|
+
|
|
13
|
+
from panel_reactflow import Node, ReactFlow
|
|
14
|
+
|
|
15
|
+
pn.extension()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskNode(Node):
|
|
19
|
+
status = param.Selector(
|
|
20
|
+
default="idle", objects=["idle", "running", "done", "failed"], precedence=0
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def __init__(self, **params):
|
|
24
|
+
params.setdefault("type", "panel")
|
|
25
|
+
super().__init__(**params)
|
|
26
|
+
|
|
27
|
+
def __panel__(self):
|
|
28
|
+
return pn.pane.Markdown(
|
|
29
|
+
f"**{self.label}**\n\nStatus: `{self.status}`",
|
|
30
|
+
sizing_mode="stretch_width",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def context_menu(self):
|
|
34
|
+
def set_status(status):
|
|
35
|
+
self.flow.patch_node_data(self.id, {"status": status})
|
|
36
|
+
self.flow._handle_msg({"type": "close_context_menu"})
|
|
37
|
+
|
|
38
|
+
run_btn = pmui.Button(
|
|
39
|
+
name="Run", variant="text", size="small",
|
|
40
|
+
on_click=lambda e: set_status("running"),
|
|
41
|
+
)
|
|
42
|
+
done_btn = pmui.Button(
|
|
43
|
+
name="Mark Done", variant="text", size="small",
|
|
44
|
+
on_click=lambda e: set_status("done"),
|
|
45
|
+
)
|
|
46
|
+
reset_btn = pmui.Button(
|
|
47
|
+
name="Reset", variant="text", size="small",
|
|
48
|
+
on_click=lambda e: set_status("idle"),
|
|
49
|
+
)
|
|
50
|
+
delete_btn = pmui.Button(
|
|
51
|
+
name="Delete", variant="text", color="error", size="small",
|
|
52
|
+
on_click=lambda e: self.flow.remove_node(self.id),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return pn.Column(
|
|
56
|
+
pn.pane.Markdown(f"**{self.label}**", margin=(4, 8)),
|
|
57
|
+
run_btn, done_btn, reset_btn, delete_btn,
|
|
58
|
+
sizing_mode="stretch_width",
|
|
59
|
+
margin=0,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
nodes = [
|
|
64
|
+
TaskNode(id="extract", label="Extract", position={"x": 0, "y": 0}),
|
|
65
|
+
TaskNode(id="transform", label="Transform", position={"x": 300, "y": 80}),
|
|
66
|
+
TaskNode(id="load", label="Load", position={"x": 600, "y": 0}, status="done"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
flow = ReactFlow(
|
|
70
|
+
nodes=nodes,
|
|
71
|
+
edges=[
|
|
72
|
+
{"id": "e1", "source": "extract", "target": "transform"},
|
|
73
|
+
{"id": "e2", "source": "transform", "target": "load"},
|
|
74
|
+
],
|
|
75
|
+
sizing_mode="stretch_both",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
pn.Column(
|
|
79
|
+
pn.pane.Markdown("## Context Menu Demo\nRight-click any node to see its context menu."),
|
|
80
|
+
flow,
|
|
81
|
+
sizing_mode="stretch_both",
|
|
82
|
+
).servable()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comparison of standard edges vs smart edges.
|
|
3
|
+
|
|
4
|
+
This example demonstrates the difference between standard React Flow edges
|
|
5
|
+
and smart edges that automatically route around obstacles.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import panel as pn
|
|
9
|
+
from panel_reactflow import EdgeSpec, NodeSpec, ReactFlow
|
|
10
|
+
|
|
11
|
+
pn.extension()
|
|
12
|
+
|
|
13
|
+
# Create nodes arranged to show edge routing
|
|
14
|
+
nodes = [
|
|
15
|
+
# Left column - sources
|
|
16
|
+
NodeSpec(id="s1", position={"x": 0, "y": 0}, label="Source 1").to_dict(),
|
|
17
|
+
NodeSpec(id="s2", position={"x": 0, "y": 100}, label="Source 2").to_dict(),
|
|
18
|
+
NodeSpec(id="s3", position={"x": 0, "y": 200}, label="Source 3").to_dict(),
|
|
19
|
+
NodeSpec(id="s4", position={"x": 0, "y": 300}, label="Source 4").to_dict(),
|
|
20
|
+
# Middle obstacles
|
|
21
|
+
NodeSpec(id="obs1", position={"x": 150, "y": 50}, label="Obstacle 1").to_dict(),
|
|
22
|
+
NodeSpec(id="obs2", position={"x": 150, "y": 150}, label="Obstacle 2").to_dict(),
|
|
23
|
+
NodeSpec(id="obs3", position={"x": 150, "y": 250}, label="Obstacle 3").to_dict(),
|
|
24
|
+
# Right column - targets
|
|
25
|
+
NodeSpec(id="t1", position={"x": 300, "y": 0}, label="Target 1").to_dict(),
|
|
26
|
+
NodeSpec(id="t2", position={"x": 300, "y": 100}, label="Target 2").to_dict(),
|
|
27
|
+
NodeSpec(id="t3", position={"x": 300, "y": 200}, label="Target 3").to_dict(),
|
|
28
|
+
NodeSpec(id="t4", position={"x": 300, "y": 300}, label="Target 4").to_dict(),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Create edges with different types
|
|
32
|
+
edges = [
|
|
33
|
+
# Default edge (goes through obstacle)
|
|
34
|
+
EdgeSpec(
|
|
35
|
+
id="e1",
|
|
36
|
+
source="s1",
|
|
37
|
+
target="t2",
|
|
38
|
+
label="default",
|
|
39
|
+
type=None,
|
|
40
|
+
style={"stroke": "#999"},
|
|
41
|
+
).to_dict(),
|
|
42
|
+
# Smart bezier (routes around obstacle)
|
|
43
|
+
EdgeSpec(
|
|
44
|
+
id="e2",
|
|
45
|
+
source="s2",
|
|
46
|
+
target="t3",
|
|
47
|
+
label="smart_bezier",
|
|
48
|
+
type="smart_bezier",
|
|
49
|
+
style={"stroke": "#3b82f6"},
|
|
50
|
+
).to_dict(),
|
|
51
|
+
# Smart straight (routes around obstacle)
|
|
52
|
+
EdgeSpec(
|
|
53
|
+
id="e3",
|
|
54
|
+
source="s3",
|
|
55
|
+
target="t1",
|
|
56
|
+
label="smart_straight",
|
|
57
|
+
type="smart_straight",
|
|
58
|
+
style={"stroke": "#10b981"},
|
|
59
|
+
).to_dict(),
|
|
60
|
+
# Smart step (routes around obstacle)
|
|
61
|
+
EdgeSpec(
|
|
62
|
+
id="e4",
|
|
63
|
+
source="s4",
|
|
64
|
+
target="t4",
|
|
65
|
+
label="smart_step",
|
|
66
|
+
type="smart_step",
|
|
67
|
+
style={"stroke": "#f59e0b"},
|
|
68
|
+
).to_dict(),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Create the flow
|
|
72
|
+
flow = ReactFlow(
|
|
73
|
+
nodes=nodes,
|
|
74
|
+
edges=edges,
|
|
75
|
+
height=600,
|
|
76
|
+
width="100%",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Layout
|
|
80
|
+
pn.template.FastListTemplate(
|
|
81
|
+
title="Edge Types Comparison",
|
|
82
|
+
sidebar=[
|
|
83
|
+
pn.pane.Markdown(
|
|
84
|
+
"""
|
|
85
|
+
## Edge Types
|
|
86
|
+
|
|
87
|
+
This example compares different edge types:
|
|
88
|
+
|
|
89
|
+
- **Gray (default)**: Standard edge that goes straight through obstacles
|
|
90
|
+
- **Blue (smart_bezier)**: Curved edge that routes around obstacles
|
|
91
|
+
- **Green (smart_straight)**: Straight segments that avoid obstacles
|
|
92
|
+
- **Orange (smart_step)**: Step-style edge that avoids obstacles
|
|
93
|
+
|
|
94
|
+
### Try it:
|
|
95
|
+
- Drag the obstacle nodes around
|
|
96
|
+
- Watch how smart edges automatically reroute
|
|
97
|
+
- Notice the default edge doesn't avoid obstacles
|
|
98
|
+
""",
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
main=[flow],
|
|
102
|
+
).servable()
|