panel-reactflow 0.5.0a1__tar.gz → 0.5.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.5.0a1 → panel_reactflow-0.5.0b0}/PKG-INFO +29 -1
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/README.md +28 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/declare-types.md +56 -6
- panel_reactflow-0.5.0b0/docs/how-to/validate-connections.md +82 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/index.md +1 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/releases.md +62 -9
- panel_reactflow-0.5.0b0/examples/connection_validation.py +89 -0
- panel_reactflow-0.5.0b0/examples/port_value_inspection.py +123 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/pixi.lock +4033 -4003
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/base.py +185 -8
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/dist/css/reactflow.css +40 -0
- panel_reactflow-0.5.0b0/src/panel_reactflow/dist/panel-reactflow.bundle.js +88 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/models/reactflow.jsx +465 -36
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/test_api.py +259 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/test_core.py +2 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/ui/test_ui.py +237 -0
- panel_reactflow-0.5.0b0/tests/ui/test_value_popup.py +239 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/zensical.toml +1 -0
- panel_reactflow-0.5.0a1/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -88
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.copier-answers.yml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.gitattributes +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.github/CODEOWNERS +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.github/dependabot.yml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.github/workflows/build.yml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.github/workflows/docs.yml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.github/workflows/test.yml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.gitignore +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.pre-commit-config.yaml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/.prettierrc +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/LICENSE.txt +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/MANIFEST.in +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/logo.svg +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/declare-types.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/define-editors-edge.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/define-editors-node.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/define-nodes-edges.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/advanced.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/custom_editor.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/edge_editors.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/schema_types.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/simple.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/quickstart.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/react-to-events.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/assets/screenshots/style-nodes-edges.png +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/advanced.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/custom-editor.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/edge-editors.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/index.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/node-edge-instances.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/schema-types.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/simple.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/threejs-viewer-instances.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/examples/threejs-viewer.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/context-menu.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/control-handle-connectivity.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/define-editors.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/define-nodes-edges.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/embed-views-in-nodes.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/react-to-events.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/recover-from-errors.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/how-to/style-nodes-edges.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/quickstart.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/docs/reference/panel_reactflow.md +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/advanced.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/context_menu.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/custom_editor.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/edge_editors.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/edge_types_comparison.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/node_edge_instances.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/schema_types.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/simple.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/smart_edges_example.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/threejs_viewer.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/examples/threejs_viewer_instances.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/hatch_build.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/pixi.toml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/pyproject.toml +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/__init__.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/__version.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/dist/icons/gear.svg +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/py.typed +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/src/panel_reactflow/schema.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/__init__.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/conftest.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/test_connectable_handles.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/test_connectable_integration.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/test_error_recovery.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/ui/__init__.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/ui/test_context_menu.py +0 -0
- {panel_reactflow-0.5.0a1 → panel_reactflow-0.5.0b0}/tests/ui/test_error_recovery.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: panel-reactflow
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -114,6 +114,34 @@ flow
|
|
|
114
114
|
|
|
115
115
|
For property schemas and richer editors, provide `node_types`/`edge_types` with `PropertySpec` and handle changes via `ReactFlow.on(...)`. `.on` callbacks receive the event payload as the first argument and can optionally accept the `ReactFlow` instance as a second argument.
|
|
116
116
|
|
|
117
|
+
## Connection validation
|
|
118
|
+
|
|
119
|
+
Connection checks are opt-in. For frontend-only validation, set `connection_validation` with any of `direction`, `types`, `capacity`, `duplicates`, or `cycles` set to `True`. `types` compares declared handle types case-insensitively and permits unknown types; `capacity` uses `maxConnections` on input handle dictionaries, with no limit when omitted. Existing handle connectability flags still apply.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from panel_reactflow import NodeType, ReactFlow
|
|
123
|
+
|
|
124
|
+
flow = ReactFlow(
|
|
125
|
+
node_types={
|
|
126
|
+
"source": NodeType(type="source", outputs=[{"id": "value", "type": "str"}]),
|
|
127
|
+
"sink": NodeType(type="sink", inputs=[{"id": "value", "type": "str", "maxConnections": 1}]),
|
|
128
|
+
},
|
|
129
|
+
connection_validation={"direction": True, "types": True, "capacity": True, "cycles": True},
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For application rules, register a Python validator. On each drag start, ReactFlow requests results for every candidate port and shows the returned reasons while dragging. The callback receives an edge-shaped payload with `source`, `target`, `sourceHandle`, and `targetHandle`; return `None` to allow or a string to reject. Hooks and frontend checks can be used together. Validate again in your `edge_added` handler before accepting the connection, since the graph may have changed after drag start.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
def validate_connection(edge, flow):
|
|
137
|
+
if edge["source"] == edge["target"]:
|
|
138
|
+
return "A node cannot connect to itself."
|
|
139
|
+
|
|
140
|
+
flow.add_connection_validator(validate_connection)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Run the [connection validation demo](examples/connection_validation.py) with `PYTHONPATH=src pixi run panel serve examples/connection_validation.py --show` from the repository root.
|
|
144
|
+
|
|
117
145
|
## Development
|
|
118
146
|
|
|
119
147
|
```bash
|
|
@@ -71,6 +71,34 @@ flow
|
|
|
71
71
|
|
|
72
72
|
For property schemas and richer editors, provide `node_types`/`edge_types` with `PropertySpec` and handle changes via `ReactFlow.on(...)`. `.on` callbacks receive the event payload as the first argument and can optionally accept the `ReactFlow` instance as a second argument.
|
|
73
73
|
|
|
74
|
+
## Connection validation
|
|
75
|
+
|
|
76
|
+
Connection checks are opt-in. For frontend-only validation, set `connection_validation` with any of `direction`, `types`, `capacity`, `duplicates`, or `cycles` set to `True`. `types` compares declared handle types case-insensitively and permits unknown types; `capacity` uses `maxConnections` on input handle dictionaries, with no limit when omitted. Existing handle connectability flags still apply.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from panel_reactflow import NodeType, ReactFlow
|
|
80
|
+
|
|
81
|
+
flow = ReactFlow(
|
|
82
|
+
node_types={
|
|
83
|
+
"source": NodeType(type="source", outputs=[{"id": "value", "type": "str"}]),
|
|
84
|
+
"sink": NodeType(type="sink", inputs=[{"id": "value", "type": "str", "maxConnections": 1}]),
|
|
85
|
+
},
|
|
86
|
+
connection_validation={"direction": True, "types": True, "capacity": True, "cycles": True},
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For application rules, register a Python validator. On each drag start, ReactFlow requests results for every candidate port and shows the returned reasons while dragging. The callback receives an edge-shaped payload with `source`, `target`, `sourceHandle`, and `targetHandle`; return `None` to allow or a string to reject. Hooks and frontend checks can be used together. Validate again in your `edge_added` handler before accepting the connection, since the graph may have changed after drag start.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
def validate_connection(edge, flow):
|
|
94
|
+
if edge["source"] == edge["target"]:
|
|
95
|
+
return "A node cannot connect to itself."
|
|
96
|
+
|
|
97
|
+
flow.add_connection_validator(validate_connection)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run the [connection validation demo](examples/connection_validation.py) with `PYTHONPATH=src pixi run panel serve examples/connection_validation.py --show` from the repository root.
|
|
101
|
+
|
|
74
102
|
## Development
|
|
75
103
|
|
|
76
104
|
```bash
|
|
@@ -244,7 +244,8 @@ validation or auto-generated forms.
|
|
|
244
244
|
## Handle tooltips
|
|
245
245
|
|
|
246
246
|
By default, handles are plain connection points. You can add a tooltip (shown
|
|
247
|
-
on hover) by passing a dict with `"id"` and `"label"`
|
|
247
|
+
on hover) by passing a dict with `"id"` and `"label"` and/or `"type"` instead
|
|
248
|
+
of a plain string:
|
|
248
249
|
|
|
249
250
|
```python
|
|
250
251
|
from panel_reactflow import NodeType
|
|
@@ -253,19 +254,68 @@ node_types = {
|
|
|
253
254
|
"transform": NodeType(
|
|
254
255
|
type="transform",
|
|
255
256
|
label="Transform",
|
|
256
|
-
inputs=[{"id": "in", "label": "Data Input"}],
|
|
257
|
+
inputs=[{"id": "in", "label": "Data Input", "type": "DataFrame"}],
|
|
257
258
|
outputs=[
|
|
258
|
-
{"id": "success", "label": "Successful results"},
|
|
259
|
-
{"id": "error", "label": "Failed records"},
|
|
259
|
+
{"id": "success", "label": "Successful results", "type": "DataFrame"},
|
|
260
|
+
{"id": "error", "label": "Failed records", "type": "list"},
|
|
260
261
|
],
|
|
261
262
|
),
|
|
262
263
|
}
|
|
263
264
|
```
|
|
264
265
|
|
|
265
|
-
|
|
266
|
+
The tooltip combines both when present (`"Data Input (DataFrame)"`), or falls
|
|
267
|
+
back to whichever one is given. Plain strings and dicts can be mixed freely in
|
|
268
|
+
the same list:
|
|
266
269
|
|
|
267
270
|
```python
|
|
268
|
-
inputs=["simple_port", {"id": "documented_port", "label": "Hover to see this"}]
|
|
271
|
+
inputs=["simple_port", {"id": "documented_port", "label": "Hover to see this", "type": "int"}]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Show a port's current value on click
|
|
277
|
+
|
|
278
|
+
Clicking a handle emits a `"handle_clicked"` event with the node id, handle
|
|
279
|
+
id, direction (`"input"`/`"output"`) and a screen position. Clicking an edge
|
|
280
|
+
emits `"edge_clicked"` with the edge id and position. Use
|
|
281
|
+
`ReactFlow.show_popup(content, position)` in the handler to display whatever
|
|
282
|
+
you consider the "current value" for that port or connection:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
def on_handle_clicked(payload, flow):
|
|
286
|
+
node_id, handle_id = payload["node_id"], payload["handle_id"]
|
|
287
|
+
value = live_values.get(node_id, {}).get(handle_id)
|
|
288
|
+
flow.show_popup(pn.pane.Markdown(f"**{handle_id}**: {value!r}"), payload["position"])
|
|
289
|
+
|
|
290
|
+
def on_edge_clicked(payload, flow):
|
|
291
|
+
source_id, source_port = edge_sources[payload["edge_id"]]
|
|
292
|
+
value = live_values.get(source_id, {}).get(source_port)
|
|
293
|
+
flow.show_popup(pn.pane.Markdown(f"**Value:** {value!r}"), payload["position"])
|
|
294
|
+
|
|
295
|
+
flow.on("handle_clicked", on_handle_clicked)
|
|
296
|
+
flow.on("edge_clicked", on_edge_clicked)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
For `popup_trigger="hover"`, register `handle_hovered` and `edge_hovered` to
|
|
300
|
+
show the popup and their corresponding `handle_unhovered` and `edge_unhovered`
|
|
301
|
+
events to call `flow.close_popup()`. Match the unhovered target against the
|
|
302
|
+
currently displayed one so a delayed leave event does not dismiss a newer
|
|
303
|
+
popup. See the runnable example below for both pairs of callbacks.
|
|
304
|
+
|
|
305
|
+
The default `popup_hover_delay` is 500 ms. After opening, an unhover event is
|
|
306
|
+
emitted when the pointer moves more than 10% of the shorter canvas dimension
|
|
307
|
+
from the anchor (at least 48 pixels) and is not over the popup. Click-opened
|
|
308
|
+
popups dismiss on outside click; either mode can dismiss programmatically via
|
|
309
|
+
`flow.close_popup()`. Use
|
|
310
|
+
`popup_trigger="none"` to disable the built-in inspection events.
|
|
311
|
+
`panel-reactflow` only provides the interaction events and overlay; looking up
|
|
312
|
+
"the current value" for a port is application-specific.
|
|
313
|
+
|
|
314
|
+
For a complete runnable graph with typed port hover tooltips and popups for
|
|
315
|
+
both handles and edges, run:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
panel serve examples/port_value_inspection.py --show
|
|
269
319
|
```
|
|
270
320
|
|
|
271
321
|
---
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Validate Connections
|
|
2
|
+
|
|
3
|
+
Use `connection_validation` to reject invalid connections as users drag between handles. The policies run in the browser; a Python validator can add application-specific rules. Both are opt-in and can be used together.
|
|
4
|
+
|
|
5
|
+
## Configure browser policies
|
|
6
|
+
|
|
7
|
+
Set only the checks your graph needs:
|
|
8
|
+
|
|
9
|
+
| Policy | Rejects |
|
|
10
|
+
|--------|---------|
|
|
11
|
+
| `direction` | Connections that do not run from a declared output to a declared input. Users can still start a drag at either end. |
|
|
12
|
+
| `types` | Connections between handles with different declared `type` values (case-insensitive). If either type is missing, the connection is allowed. |
|
|
13
|
+
| `capacity` | Connections to an input handle that has reached its positive integer `maxConnections` limit. Inputs without a limit are unrestricted. |
|
|
14
|
+
| `duplicates` | A second edge with the same source, target, source handle, and target handle. Different handle pairs can still connect the same nodes. |
|
|
15
|
+
| `cycles` | Connections that would create a cycle, including a self-connection. |
|
|
16
|
+
|
|
17
|
+
Policies default to off. Existing `NodeType` connectable flags still control whether a handle can start or receive a drag; see [Control Handle Connectivity](control-handle-connectivity.md).
|
|
18
|
+
|
|
19
|
+
The following app uses handle types and capacity limits alongside a Python rule. Save it as `validation_app.py` and run `panel serve validation_app.py --show`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import panel as pn
|
|
23
|
+
|
|
24
|
+
from panel_reactflow import NodeSpec, NodeType, ReactFlow
|
|
25
|
+
|
|
26
|
+
pn.extension("jsoneditor")
|
|
27
|
+
|
|
28
|
+
flow = ReactFlow(
|
|
29
|
+
nodes=[
|
|
30
|
+
NodeSpec(id="source", type="source", label="Source", position={"x": 0, "y": 0}),
|
|
31
|
+
NodeSpec(id="transform", type="transform", label="Transform", position={"x": 260, "y": 0}),
|
|
32
|
+
NodeSpec(id="publish", type="publish", label="Publish", position={"x": 520, "y": 0}),
|
|
33
|
+
],
|
|
34
|
+
node_types={
|
|
35
|
+
"source": NodeType(type="source", outputs=[{"id": "text", "type": "Text"}]),
|
|
36
|
+
"transform": NodeType(
|
|
37
|
+
type="transform",
|
|
38
|
+
inputs=[{"id": "text", "type": "text", "maxConnections": 1}],
|
|
39
|
+
outputs=[{"id": "cleaned", "type": "Text"}],
|
|
40
|
+
),
|
|
41
|
+
"publish": NodeType(type="publish", inputs=[{"id": "text", "type": "Text", "maxConnections": 1}]),
|
|
42
|
+
},
|
|
43
|
+
connection_validation={
|
|
44
|
+
"direction": True,
|
|
45
|
+
"types": True,
|
|
46
|
+
"capacity": True,
|
|
47
|
+
"duplicates": True,
|
|
48
|
+
"cycles": True,
|
|
49
|
+
},
|
|
50
|
+
height=350,
|
|
51
|
+
width=800,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_connection(edge, flow):
|
|
56
|
+
if edge["source"] == "source" and edge["target"] == "publish":
|
|
57
|
+
return "Publish requires the Transform output."
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
flow.add_connection_validator(validate_connection)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def on_edge_added(event, flow):
|
|
65
|
+
edge = event["edge"]
|
|
66
|
+
if validate_connection(edge, flow):
|
|
67
|
+
flow.remove_edge(edge["id"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
flow.on("edge_added", on_edge_added)
|
|
71
|
+
flow.servable()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Drag from **Source.text** to **Transform.text**, then from **Transform.cleaned** to **Publish.text**. A direct connection from Source to Publish is rejected by Python. Try connecting Source.text to Transform.cleaned (two outputs) to see the direction check, or connect an output to an already occupied input to see the capacity check.
|
|
75
|
+
|
|
76
|
+
## Add application rules
|
|
77
|
+
|
|
78
|
+
Register callbacks with `flow.add_connection_validator(callback)` and unregister them with `flow.remove_connection_validator(callback)`. Each callback accepts either `edge` or `(edge, flow)` and returns `None` to allow the connection or a reason string to reject it. The payload has `source`, `target`, `sourceHandle`, and `targetHandle` keys; handles without an explicit ID use `None`.
|
|
79
|
+
|
|
80
|
+
When a user starts dragging from an output or input, Python checks every candidate handle on the opposite side. Validators run in registration order until one rejects a candidate. Browser policies and Python reasons are shown on handles while dragging; the browser prevents a rejected connection. Python results must arrive before the connection is dropped: pending requests and requests taking longer than three seconds block the connection. Exceptions also reject the candidate and are logged on the server. Keep validators fast.
|
|
81
|
+
|
|
82
|
+
Validation is for interactive drags, not a constraint on `flow.edges` or `flow.add_edge()`. Python validators run at drag start, not when the edge is added. If the rule depends on graph state that may change during the drag, check it again in an `edge_added` handler before keeping or persisting the edge. The example above removes an edge if its application rule no longer holds.
|
|
@@ -62,6 +62,7 @@ flow.servable()
|
|
|
62
62
|
- [Define Nodes & Edges](how-to/define-nodes-edges.md)
|
|
63
63
|
- [Declare Node & Edge Types](how-to/declare-types.md)
|
|
64
64
|
- [Control Handle Connectivity](how-to/control-handle-connectivity.md) — restrict connections
|
|
65
|
+
- [Validate Connections](how-to/validate-connections.md) — check types, capacity, cycles, and application rules
|
|
65
66
|
- [Define Editors](how-to/define-editors.md) — node *and* edge editors
|
|
66
67
|
- [Embed Views in Nodes](how-to/embed-views-in-nodes.md)
|
|
67
68
|
- [Style Nodes & Edges](how-to/style-nodes-edges.md)
|
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Version 0.5.0
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This release adds port and edge value inspection and connection checks
|
|
6
|
+
for interactive graph editing. `Node` and `Edge` properties now sync to
|
|
7
|
+
the browser in place, batches of changes render as a single update, and
|
|
8
|
+
an error boundary keeps a malformed graph from blanking the canvas.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
10
|
+
### Highlights
|
|
11
|
+
|
|
12
|
+
- **Port and edge inspection**: handle and edge click events can open a
|
|
13
|
+
Panel popup with `flow.show_popup(content, position)`. Set
|
|
14
|
+
`popup_trigger="hover"` to use hover events instead; use
|
|
15
|
+
`flow.close_popup()` to dismiss it. Applications supply the value to
|
|
16
|
+
display, and `examples/port_value_inspection.py` shows both interactions
|
|
17
|
+
([#74](https://github.com/panel-extensions/panel-reactflow/pull/74)).
|
|
18
|
+
|
|
19
|
+
- **Connection validation**: opt-in browser policies reject connections
|
|
20
|
+
with the wrong direction or handle type, full inputs, duplicate edges,
|
|
21
|
+
and cycles. Register Python rules with `add_connection_validator()` to
|
|
22
|
+
show rejection reasons on candidate handles during a drag. Python
|
|
23
|
+
validators check interactive drags only; they do not restrict
|
|
24
|
+
`flow.add_edge()` or direct changes to `flow.edges`. See
|
|
25
|
+
[Validate Connections](how-to/validate-connections.md) and
|
|
26
|
+
`examples/connection_validation.py`
|
|
27
|
+
([#75](https://github.com/panel-extensions/panel-reactflow/pull/75)).
|
|
28
|
+
|
|
29
|
+
- **Error recovery** — the canvas is now wrapped in an error boundary,
|
|
30
|
+
controlled by the new `error_recovery` parameter (`"auto"` by default,
|
|
31
|
+
or `"manual"` / `"off"`). In `"auto"` mode a React render error
|
|
32
|
+
remounts the canvas, then remounts again in a view-only safe mode that
|
|
33
|
+
repairs or hides elements it cannot render (invalid positions, unknown
|
|
34
|
+
node/edge types, dangling edges, duplicate or missing ids) without
|
|
35
|
+
mutating the server-side graph. If retries are exhausted a recovery
|
|
36
|
+
panel offers *Try again*, *Reload page* and *Copy details*. Every
|
|
37
|
+
error is logged to the `panel.reactflow` logger and emitted as a
|
|
38
|
+
`client_error` event, so browser-side failures are no longer invisible
|
|
39
|
+
to the server
|
|
40
|
+
([#70](https://github.com/panel-extensions/panel-reactflow/pull/70)).
|
|
41
|
+
|
|
42
|
+
- **Base property sync** — the top-level React Flow fields on `Node` and
|
|
43
|
+
`Edge` (`label`, `type`, `style`, `className`, `draggable`,
|
|
44
|
+
`connectable`, `deletable`, ...) are now synced to the frontend, so
|
|
45
|
+
assigning `node.label = "Start (running)"` patches the browser in place
|
|
46
|
+
instead of requiring `flow.nodes` to be replaced. Parameters declared
|
|
47
|
+
on a subclass continue to sync into `data`. New `patch_node_props()`
|
|
48
|
+
and `patch_edge_props()` methods do the same for dict-based nodes and
|
|
49
|
+
edges, where passing `None` clears a field back to the CSS/theme
|
|
50
|
+
default, and new `on_props_change` hooks fire on `Node`/`Edge`
|
|
51
|
+
subclasses when the frontend changes a property. `position` and
|
|
52
|
+
`selected` remain browser-owned during drag and selection and are only
|
|
53
|
+
pushed via `patch_node_props()`
|
|
54
|
+
([#71](https://github.com/panel-extensions/panel-reactflow/pull/71)).
|
|
13
55
|
|
|
14
56
|
### Enhancements
|
|
15
57
|
|
|
@@ -18,7 +60,18 @@
|
|
|
18
60
|
(`flow.remove_node("n1", "n2")`) or as a sequence
|
|
19
61
|
(`flow.remove_node(["n1", "n2"])`), and remove them in a single update.
|
|
20
62
|
For any other batch of changes made from Python, wrap them in
|
|
21
|
-
`pn.io.hold()` to render them at once
|
|
63
|
+
`pn.io.hold()` to render them at once
|
|
64
|
+
([#72](https://github.com/panel-extensions/panel-reactflow/pull/72)).
|
|
65
|
+
|
|
66
|
+
### Bug fixes
|
|
67
|
+
|
|
68
|
+
- **Progressive re-render when deleting multiple elements** — deleting a
|
|
69
|
+
multi-node selection removed the nodes one at a time, syncing an
|
|
70
|
+
intermediate graph to the browser per node so the nodes visibly
|
|
71
|
+
disappeared one by one. Updates triggered by a frontend message are now
|
|
72
|
+
held and combined into a single patch, and node/edge deletion assigns
|
|
73
|
+
`nodes` and `edges` once
|
|
74
|
+
([#72](https://github.com/panel-extensions/panel-reactflow/pull/72)).
|
|
22
75
|
|
|
23
76
|
## Version 0.4.1
|
|
24
77
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Try frontend rules and Python connection hooks in one graph.
|
|
2
|
+
|
|
3
|
+
Run from the repository root with:
|
|
4
|
+
|
|
5
|
+
PYTHONPATH=src pixi run panel serve examples/connection_validation.py --show
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import panel as pn
|
|
9
|
+
import panel_material_ui as pmui
|
|
10
|
+
|
|
11
|
+
from panel_reactflow import NodeSpec, NodeType, ReactFlow
|
|
12
|
+
|
|
13
|
+
pn.extension()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConnectionValidationDemo(pn.viewable.Viewer):
|
|
17
|
+
def __init__(self, **params):
|
|
18
|
+
super().__init__(**params)
|
|
19
|
+
self._status = pn.pane.Markdown("No connections yet.")
|
|
20
|
+
self._flow = ReactFlow(
|
|
21
|
+
nodes=[
|
|
22
|
+
NodeSpec(id="source", type="source", label="Source", position={"x": 0, "y": 60}).to_dict(),
|
|
23
|
+
NodeSpec(id="number", type="number", label="Number", position={"x": 0, "y": 280}).to_dict(),
|
|
24
|
+
NodeSpec(id="transform", type="transform", label="Transform", position={"x": 290, "y": 60}).to_dict(),
|
|
25
|
+
NodeSpec(id="publish", type="publish", label="Publish", position={"x": 610, "y": 60}).to_dict(),
|
|
26
|
+
NodeSpec(id="monitor", type="monitor", label="Monitor", position={"x": 610, "y": 280}).to_dict(),
|
|
27
|
+
],
|
|
28
|
+
node_types={
|
|
29
|
+
"source": NodeType(type="source", outputs=[{"id": "text", "type": "Text"}]),
|
|
30
|
+
"number": NodeType(type="number", outputs=[{"id": "value", "type": "Number"}]),
|
|
31
|
+
"transform": NodeType(
|
|
32
|
+
type="transform",
|
|
33
|
+
inputs=[{"id": "text", "type": "Text", "maxConnections": 1}],
|
|
34
|
+
outputs=[{"id": "cleaned", "type": "Text"}],
|
|
35
|
+
),
|
|
36
|
+
"publish": NodeType(type="publish", inputs=[{"id": "text", "type": "Text", "maxConnections": 1}]),
|
|
37
|
+
"monitor": NodeType(type="monitor", inputs=[{"id": "text", "type": "Text"}]),
|
|
38
|
+
},
|
|
39
|
+
connection_validation={
|
|
40
|
+
"direction": True,
|
|
41
|
+
"types": True,
|
|
42
|
+
"capacity": True,
|
|
43
|
+
"duplicates": True,
|
|
44
|
+
"cycles": True,
|
|
45
|
+
},
|
|
46
|
+
height=510,
|
|
47
|
+
sizing_mode="stretch_width",
|
|
48
|
+
)
|
|
49
|
+
self._flow.add_connection_validator(self._validate_connection)
|
|
50
|
+
self._flow.on("edge_added", self._on_edge_added)
|
|
51
|
+
self._page = pmui.Page(
|
|
52
|
+
title="Connection validation",
|
|
53
|
+
main=[
|
|
54
|
+
pmui.Container(
|
|
55
|
+
pmui.Column(
|
|
56
|
+
pn.pane.Markdown(
|
|
57
|
+
"Drag **Source.text** to **Transform.text**, then **Transform.cleaned** to **Publish.text**. "
|
|
58
|
+
"**Number.value** to **Transform.text** fails the frontend type check; "
|
|
59
|
+
"**Source.text** to **Publish.text** is rejected by Python. "
|
|
60
|
+
"Try connecting both text outputs to **Monitor.text**, or creating a cycle."
|
|
61
|
+
),
|
|
62
|
+
self._flow,
|
|
63
|
+
self._status,
|
|
64
|
+
sizing_mode="stretch_width",
|
|
65
|
+
),
|
|
66
|
+
width_option="lg",
|
|
67
|
+
),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _validate_connection(self, edge, flow):
|
|
72
|
+
if edge["source"] == "source" and edge["target"] == "publish":
|
|
73
|
+
return "Publish requires text from Transform, not Source."
|
|
74
|
+
|
|
75
|
+
def _on_edge_added(self, event, flow):
|
|
76
|
+
edge = event["edge"]
|
|
77
|
+
reason = self._validate_connection(edge, flow)
|
|
78
|
+
if reason:
|
|
79
|
+
flow.remove_edge(edge["id"])
|
|
80
|
+
self._status.object = f"Rejected: {reason}"
|
|
81
|
+
else:
|
|
82
|
+
self._status.object = f"Connected: {edge['source']}.{edge['sourceHandle']} to {edge['target']}.{edge['targetHandle']}"
|
|
83
|
+
|
|
84
|
+
def __panel__(self):
|
|
85
|
+
return self._page
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
demo = ConnectionValidationDemo()
|
|
89
|
+
demo.servable()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Inspect typed port values by hovering over handles and edges.
|
|
2
|
+
|
|
3
|
+
Run with:
|
|
4
|
+
|
|
5
|
+
panel serve examples/port_value_inspection.py --show
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import panel as pn
|
|
9
|
+
|
|
10
|
+
from panel_reactflow import EdgeSpec, NodeSpec, NodeType, ReactFlow
|
|
11
|
+
|
|
12
|
+
pn.extension("jsoneditor")
|
|
13
|
+
|
|
14
|
+
node_types = {
|
|
15
|
+
"source": NodeType(
|
|
16
|
+
type="source",
|
|
17
|
+
label="Data Source",
|
|
18
|
+
outputs=[{"id": "records", "label": "Output records", "type": "list[dict]"}],
|
|
19
|
+
),
|
|
20
|
+
"transform": NodeType(
|
|
21
|
+
type="transform",
|
|
22
|
+
label="Transform",
|
|
23
|
+
inputs=[{"id": "records", "label": "Input records", "type": "list[dict]"}],
|
|
24
|
+
outputs=[{"id": "summary", "label": "Summary", "type": "dict[str, int]"}],
|
|
25
|
+
),
|
|
26
|
+
"sink": NodeType(
|
|
27
|
+
type="sink",
|
|
28
|
+
label="Sink",
|
|
29
|
+
inputs=[{"id": "summary", "label": "Input summary", "type": "dict[str, int]"}],
|
|
30
|
+
),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
nodes = [
|
|
34
|
+
NodeSpec(id="source", type="source", position={"x": 0, "y": 100}, data={}).to_dict(),
|
|
35
|
+
NodeSpec(id="transform", type="transform", position={"x": 300, "y": 100}, data={}).to_dict(),
|
|
36
|
+
NodeSpec(id="sink", type="sink", position={"x": 600, "y": 100}, data={}).to_dict(),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
edges = [
|
|
40
|
+
EdgeSpec(id="records", source="source", target="transform", sourceHandle="records", targetHandle="records").to_dict(),
|
|
41
|
+
EdgeSpec(id="summary", source="transform", target="sink", sourceHandle="summary", targetHandle="summary").to_dict(),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# In an application this mapping would be updated by the code that executes
|
|
45
|
+
# the graph. It is intentionally separate from ReactFlow's graph metadata.
|
|
46
|
+
live_values = {
|
|
47
|
+
"source": {"records": [{"city": "Berlin", "sales": 12}, {"city": "Oslo", "sales": 8}]},
|
|
48
|
+
"transform": {"summary": {"record_count": 2, "total_sales": 20}},
|
|
49
|
+
"sink": {"summary": {"record_count": 2, "total_sales": 20}},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
edge_sources = {
|
|
53
|
+
"records": ("source", "records"),
|
|
54
|
+
"summary": ("transform", "summary"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
flow = ReactFlow(
|
|
58
|
+
nodes=nodes,
|
|
59
|
+
edges=edges,
|
|
60
|
+
node_types=node_types,
|
|
61
|
+
popup_trigger="hover", # Change to "click" to inspect on click instead.
|
|
62
|
+
popup_hover_delay=500,
|
|
63
|
+
sizing_mode="stretch_both",
|
|
64
|
+
min_height=450,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
active_target = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def show_value(title, value, position, target):
|
|
72
|
+
global active_target
|
|
73
|
+
active_target = target
|
|
74
|
+
flow.show_popup(
|
|
75
|
+
pn.Column(
|
|
76
|
+
pn.pane.Markdown(f"**{title}**", margin=(0, 0, 6, 0)),
|
|
77
|
+
pn.pane.JSON(value, depth=3, sizing_mode="stretch_width"),
|
|
78
|
+
sizing_mode="stretch_width",
|
|
79
|
+
),
|
|
80
|
+
position,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def on_handle_inspected(payload, flow):
|
|
85
|
+
node_id = payload["node_id"]
|
|
86
|
+
handle_id = payload["handle_id"]
|
|
87
|
+
value = live_values.get(node_id, {}).get(handle_id)
|
|
88
|
+
show_value(f"{node_id}.{handle_id}", value, payload["position"], ("handle", node_id, handle_id, payload["direction"]))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def on_edge_inspected(payload, flow):
|
|
92
|
+
node_id, handle_id = edge_sources[payload["edge_id"]]
|
|
93
|
+
value = live_values[node_id][handle_id]
|
|
94
|
+
show_value(f"{node_id}.{handle_id}", value, payload["position"], ("edge", payload["edge_id"]))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def on_handle_unhovered(payload, flow):
|
|
98
|
+
global active_target
|
|
99
|
+
if active_target == ("handle", payload["node_id"], payload["handle_id"], payload["direction"]):
|
|
100
|
+
active_target = None
|
|
101
|
+
flow.close_popup()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def on_edge_unhovered(payload, flow):
|
|
105
|
+
global active_target
|
|
106
|
+
if active_target == ("edge", payload["edge_id"]):
|
|
107
|
+
active_target = None
|
|
108
|
+
flow.close_popup()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
flow.on("handle_clicked", on_handle_inspected)
|
|
112
|
+
flow.on("edge_clicked", on_edge_inspected)
|
|
113
|
+
flow.on("handle_hovered", on_handle_inspected)
|
|
114
|
+
flow.on("edge_hovered", on_edge_inspected)
|
|
115
|
+
flow.on("handle_unhovered", on_handle_unhovered)
|
|
116
|
+
flow.on("edge_unhovered", on_edge_unhovered)
|
|
117
|
+
|
|
118
|
+
pn.Column(
|
|
119
|
+
"# Port value inspection",
|
|
120
|
+
"Hover a port to see its type and pause over a port or edge to inspect its current value.",
|
|
121
|
+
flow,
|
|
122
|
+
sizing_mode="stretch_both",
|
|
123
|
+
).servable()
|