Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions docs/how-to/embed-the-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Three params control what the user sees:
| Param | Default | Effect |
|-------|---------|--------|
| `mode` | `"wiring"` | `"wiring"` shows the ReactFlow canvas, `"dashboard"` the tile grid. |
| `editable` | `True` | When `False` the toolbar is hidden and the grid is locked, giving a pure dashboard view. |
| `editable` | `True` | When `False` the side panel is hidden and the grid is locked, giving a pure dashboard view. |
| `preview` | `False` | Locks the grid without leaving edit mode, to see the dashboard as an end user does. |

So a read-only dashboard viewer is just:
Expand All @@ -187,13 +187,12 @@ per-dashboard permissions.

## Fitting it into your own layout

The built-in toolbar can be hidden with `toolbar=False`, or extended with your
own controls through `toolbar_extra`:
The toolbar holds the mode toggle and the *Save*, *Download* and *Clear* actions. It floats at the top of the wiring canvas and sits above the tile grid in dashboard mode. Hide it with `toolbar=False`, leaving only the component palette, or extend it with your own controls through `toolbar_extra`, which places them between *Download* and *Clear*. Small text buttons match the built-in actions:

```python
import panel_material_ui as pmui

share = pmui.Button(icon="share", variant="outlined")
share = pmui.Button(label="Share", icon="share", variant="text", size="small")
editor = FlowDash(components, toolbar_extra=[share])
```

Expand Down
45 changes: 19 additions & 26 deletions docs/how-to/layout-and-sizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ nodes:
- **Dashboard mode** (`:material/dashboard:`) - a responsive tile grid that
renders each component's live view, the layout your users actually see.

Toggle between them with the mode switch in the editor toolbar. In dashboard
Toggle between them with the mode switch at the top of the editor's side panel. In dashboard
mode a **Preview** switch turns the drag/resize handles off so you can see the
dashboard exactly as it will appear when served.

![Dashboard mode showing tiles in the grid, sidebar filters, and the breakpoint toolbar](../assets/images/dashboard-mode.png)

The breakpoint toolbar (XS / SM / MD / AUTO) and the **Preview** switch appear at
the top of dashboard mode; the sidebar filters on the left are the components
The breakpoint toolbar (XS / SM / MD / AUTO) appears at the top of dashboard
mode and the **Preview** switch below the mode toggle; the sidebar filters on the left are the components
marked `sidebar=True`, described next.

---
Expand Down Expand Up @@ -75,26 +75,21 @@ the arrangement they settle on is what gets persisted.

## Responsive layouts

The tile grid is responsive. Its `breakpoints` are pixel-width thresholds that
divide the viewport into bands; the default is `[768, 1200]`, which yields three
bands (`sm` below 768px, `md` between, `lg` above 1200px). In edit mode a toolbar
lets you switch between these bands and arrange tiles independently for each one,
so a dashboard can stack into a single column on narrow screens while spreading
across the full width on a desktop.
Arrange the tiles once, at the width you are working at, and the grid adapts the arrangement to narrower screens. The grid records the width you arranged the tiles at as `reference_width`. On a narrower screen each tile may shrink to half its authored width; once it would shrink further, it wraps onto a new line instead. Tiles in a row that no longer fits are split into evenly balanced lines, and a tile alone on a line takes the full width. In the `complex_dataflow` example the chart and map share a row on a desktop and stack above the table on a laptop with both side panels open.

Each band's arrangement is captured in `responsive_layouts`, a mapping from band
label to a list of tile entries. A single entry records a tile's index, width
(as a percentage of the row), height in pixels, and visibility:
The grid's `breakpoints` divide the viewport into bands; the default `[768, 1200]` yields `xs` below 768px, `sm` between, and `md` above 1200px. In edit mode a toolbar lets you preview each band:

- The band containing `reference_width` is marked **base**. Editing while it's shown changes the authored layout.
- Other bands show the generated layout. Editing one saves a custom layout for that band, marked **custom**, and **Reset** discards it again.
- **AUTO** returns to the natural width.

Custom layouts are stored in `responsive_layouts`, a mapping from band label to a list of tile entries. Each entry records a tile's index, width (as a percentage of the row), height in pixels, and visibility:

```json
{
"sm": [
{"index": 0, "width": 98.7, "height": 837.0, "visible": true},
{"index": 1, "width": 98.7, "height": 669.4, "visible": true}
],
"lg": [
{"index": 0, "width": 44.4, "height": 502.8, "visible": true},
{"index": 1, "width": 54.7, "height": 502.8, "visible": true}
"xs": [
{"index": 0, "width": 100, "height": 440, "visible": true},
{"index": 1, "width": 100, "height": 450, "visible": true}
]
}
```
Expand All @@ -103,16 +98,14 @@ label to a list of tile entries. A single entry records a tile's index, width

## What gets persisted

When you save a dashboard, the current grid arrangement and responsive settings
are stored alongside the nodes and edges:
When you save a dashboard, the grid arrangement and responsive settings are stored alongside the nodes and edges:

- `tile_layout` - the arrangement for the current (default) band.
- `tile_layout` - the authored arrangement.
- `reference_width` - the grid width in pixels the arrangement was authored at.
- `breakpoints` - the pixel thresholds in use.
- `responsive_layouts` - the per-band arrangements described above.
- `responsive_layouts` - the custom per-band arrangements described above.

On load these are restored so the dashboard reopens with the same layout at every
breakpoint. See [Persist dashboards](persist-dashboards.md) for the storage model
and CRUD API.
On load these are restored so the dashboard reopens with the same layout at every width. Dashboards saved before `reference_width` existed are treated as authored at the largest breakpoint. See [Persist dashboards](persist-dashboards.md) for the storage model and CRUD API.

---

Expand Down
4 changes: 2 additions & 2 deletions docs/how-to/manage-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ identity may not see from the launcher and navigation menu.
## Restricting dashboards

Saved dashboards start out accessible only to their owner (the user who created
them). To share one, open it in the editor and click the **share** button in the
toolbar next to *Save*. The button is only shown when you are allowed to
them). To share one, open it in the editor and click **Share** in the
editor's side panel next to *Save*. The button is only shown when you are allowed to
administer that dashboard.

The share dialog exposes the same four rule fields as allow/deny lists of groups
Expand Down
18 changes: 18 additions & 0 deletions docs/how-to/persist-dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ model = DashboardModel(

The `tile_layout` list stores the grid positions and sizes for the rendered
dashboard view (separate from the node editor canvas positions in items).
`reference_width`, `breakpoints` and `responsive_layouts` store how that layout adapts to other screen sizes; see [Layout & sizing](layout-and-sizing.md#responsive-layouts).

---

Expand Down Expand Up @@ -131,3 +132,20 @@ store.delete_dashboard("user1", "abc123")
```

Returns `True` if a row was deleted, `False` if not found.

---

## Export and import dashboard files

In edit mode, **Download** in the side panel saves the canvas as a JSON file named after the dashboard. Dropping that file onto the wiring canvas recreates its components, connections and tile layout. If the canvas already has components, the editor asks before replacing them. Components the editor doesn't offer are skipped with a warning.

The file holds the dashboard contents but not its `dashboard_id`, `user_id` or `permission`. An imported file takes the identity of the dashboard currently loaded, so saving writes the imported contents to that dashboard and cannot overwrite another one or change who it is shared with.

The same round trip is available from Python:

```python
data = editor.export_dashboard() # a JSON-serializable dict
other_editor.import_dashboard(data) # also accepts the JSON string
```

`import_dashboard` raises `ValueError` for anything that isn't a dashboard export and marks the canvas dirty.
2 changes: 2 additions & 0 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ Initial release of panel-flowdash.
- `DataflowGraph` engine with cycle detection, type checking, single-source validation
- Runtime validation via `param.watch` with error callbacks
- SQLite persistence (`DashboardStore`, `DashboardModel`)
- Responsive tile grid that wraps the authored layout on narrower screens, with optional per-breakpoint custom layouts; `DashboardModel.reference_width` records the width the layout was authored at
- Download a dashboard as JSON from the editor and drop the file onto the canvas to recreate it; `FlowDash.export_dashboard()` and `FlowDash.import_dashboard()` do the same from Python
- CLI: `flowdash serve <project-dir>` with Panel-compatible options
4 changes: 3 additions & 1 deletion examples/complex_dataflow/Components/capacity_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from panel_flowdash import register


@register(component=True, title="Capacity by Year", config=["title", "color_scheme"])
@register(
component=True, title="Capacity by Year", config=["title", "color_scheme"], extensions=["vega"]
)
class app(pn.viewable.Viewer):
"""Bar chart showing total installed capacity per year using Vega-Lite.

Expand Down
4 changes: 3 additions & 1 deletion examples/complex_dataflow/Components/scatter_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from panel_flowdash import register


@register(component=True, title="Location Map", config=["zoom", "radius_scale"])
@register(
component=True, title="Location Map", config=["zoom", "radius_scale"], extensions=["deckgl"]
)
class app(pn.viewable.Viewer):
"""Scatter plot of turbine lat/lon colored by capacity using DeckGL.

Expand Down
4 changes: 3 additions & 1 deletion examples/complex_dataflow/Components/table_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from panel_flowdash import register


@register(component=True, title="Data Table", config=["title", "page_size"])
@register(
component=True, title="Data Table", config=["title", "page_size"], extensions=["tabulator"]
)
class app(pn.viewable.Viewer):
"""Renders a DataFrame as an interactive Tabulator table.

Expand Down
10 changes: 6 additions & 4 deletions pixi.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
name = "panel-flowdash"
channels = ["conda-forge"]
channels = ["conda-forge", "pyviz"]
platforms = ["osx-arm64", "linux-64", "win-64"]
preview = ["pixi-build"]
requires-pixi = ">=0.68.1"
Expand All @@ -20,15 +20,17 @@ hatch-vcs = "*"
python = ">=3.10"
python-gil = '*'
panel = ">=1.9.0"
panel-material-ui = ">=0.14.0"
panel-material-ui = ">=0.16.0"

[dependencies]
panel_flowdash = { path = "." }
# TODO: Drop the channel pin once conda-forge has panel-material-ui >=0.16.0
panel-material-ui = { version = ">=0.16.0", channel = "pyviz" }

[pypi-dependencies]
# TODO: Add these to conda-forge / pyviz
panel_reactflow = ">=0.5.0b0"
panel_tiles = "*"
panel_reactflow = ">=0.5.1"
panel_tiles = ">=0.4.0"

[feature.test.dependencies]
pytest = ">=6"
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ classifiers = [
dependencies = [
"packaging",
"panel >=1.9.3",
"panel-material-ui >=0.14.0",
"panel-reactflow >=0.5.0b0",
"panel-tiles >=0.3.0",
"panel-material-ui >=0.16.0",
"panel-reactflow >=0.5.1",
"panel-tiles >=0.4.0",
]

[project.scripts]
Expand Down Expand Up @@ -114,6 +114,7 @@ convention = "numpy"
"tests/**" = ["D"]
"*_test.py" = ["D"]
"examples/**" = ["D"]
"manual_tests/**" = ["D", "T201"]

[tool.ruff.lint.isort]
known-first-party = ["panel_flowdash"]
Expand Down
7 changes: 6 additions & 1 deletion src/panel_flowdash/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,12 @@ def __init__(self, registry: dict[str, RegistryEntry] | None = None, **params):
self._user_id = self._resolve_user_id()
self._sidebar_container = pn.Column(sizing_mode="stretch_width")
self._share_button = pmui.Button(
icon="share", color="primary", variant="outlined", visible=False
label="Share",
icon="share",
variant="text",
size="small",
margin=(5, 2),
visible=False,
)
self._share_button.on_click(lambda _event: self._share_current_dashboard())
# The editor owns the canvas, tile grid and persistence; this class adds
Expand Down
14 changes: 12 additions & 2 deletions src/panel_flowdash/dashboard_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class DashboardModel:
tile_layout: list[dict[str, Any]] = field(default_factory=list)
breakpoints: list[int] = field(default_factory=list)
responsive_layouts: dict[str, list[dict[str, Any]]] = field(default_factory=dict)
# Container width the tile layout was authored at; narrower screens derive from it.
reference_width: int | None = None
permission: Permission = field(default_factory=Permission)

@property
Expand All @@ -107,6 +109,7 @@ def to_dict(self) -> dict[str, Any]:
"tile_layout": self.tile_layout,
"breakpoints": self.breakpoints,
"responsive_layouts": self.responsive_layouts,
"reference_width": self.reference_width,
"permission": self.permission.to_dict(),
}

Expand All @@ -122,6 +125,7 @@ def from_dict(cls, data: dict[str, Any]) -> DashboardModel:
tile_layout=data.get("tile_layout", []),
breakpoints=data.get("breakpoints", []),
responsive_layouts=data.get("responsive_layouts", {}),
reference_width=data.get("reference_width"),
permission=Permission.from_dict(data.get("permission")),
)

Expand Down Expand Up @@ -353,6 +357,7 @@ def _init_db(self):
("breakpoints_json", "'[]'"),
("responsive_layouts_json", "'{}'"),
("permission_json", "'{}'"),
("reference_width_json", "'null'"),
]
for col, default in migrations:
try:
Expand Down Expand Up @@ -447,12 +452,13 @@ def save_dashboard(self, dashboard: DashboardModel) -> None:
tile_layout_json = json.dumps(dashboard.tile_layout)
breakpoints_json = json.dumps(dashboard.breakpoints)
responsive_layouts_json = json.dumps(dashboard.responsive_layouts)
reference_width_json = json.dumps(dashboard.reference_width)
permission_json = json.dumps(dashboard.permission.to_dict())
with self._get_conn() as conn:
conn.execute(
"""
INSERT INTO dashboards (dashboard_id, user_id, title, version, items_json, edges_json, tile_layout_json, breakpoints_json, responsive_layouts_json, permission_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
INSERT INTO dashboards (dashboard_id, user_id, title, version, items_json, edges_json, tile_layout_json, breakpoints_json, responsive_layouts_json, reference_width_json, permission_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(dashboard_id) DO UPDATE SET
title = excluded.title,
version = excluded.version,
Expand All @@ -461,6 +467,7 @@ def save_dashboard(self, dashboard: DashboardModel) -> None:
tile_layout_json = excluded.tile_layout_json,
breakpoints_json = excluded.breakpoints_json,
responsive_layouts_json = excluded.responsive_layouts_json,
reference_width_json = excluded.reference_width_json,
permission_json = excluded.permission_json,
updated_at = datetime('now')
""",
Expand All @@ -474,6 +481,7 @@ def save_dashboard(self, dashboard: DashboardModel) -> None:
tile_layout_json,
breakpoints_json,
responsive_layouts_json,
reference_width_json,
permission_json,
),
)
Expand Down Expand Up @@ -504,6 +512,7 @@ def _row_to_model(self, row: sqlite3.Row) -> DashboardModel:
row["responsive_layouts_json"] if "responsive_layouts_json" in keys else "{}"
)
permission_raw = row["permission_json"] if "permission_json" in keys else "{}"
reference_raw = row["reference_width_json"] if "reference_width_json" in keys else "null"
edges = json.loads(edges_raw)
tile_layout = json.loads(tile_layout_raw)
breakpoints = json.loads(breakpoints_raw)
Expand All @@ -519,5 +528,6 @@ def _row_to_model(self, row: sqlite3.Row) -> DashboardModel:
tile_layout=tile_layout,
breakpoints=breakpoints,
responsive_layouts=responsive_layouts,
reference_width=json.loads(reference_raw),
permission=permission,
)
Loading
Loading