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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,16 @@ See `examples/demo.py` for a full runnable notebook flow.

## Examples

- `examples/demo.py` — end-to-end browser + editor + result rendering flow.
- `examples/demo.py` — tabbed explorer with workspace selection, connection health, recent results (selectable table), run history, and a native `mo.sql` cell.

Run:
Run locally (single-user machine):

```bash
uv run marimo edit examples/demo.py --no-token
```

On a **shared or networked host**, omit `--no-token` and use the access token printed in the terminal URL. Without it, anyone who can reach the Marimo port can run queries against your Hotdata workspace.

## Layout

This repo is intentionally thin: **API client, env helpers, and result models** live in **hotdata-runtime**; **hotdata-marimo** only adds Marimo widgets (`sql_editor`, `table_browser`, `display` for tables/status/history, `workspace_selector`). Import `HotdataClient` / `QueryResult` / `from_env` from **`hotdata_marimo`** or directly from **`hotdata_runtime`**.
Expand Down
56 changes: 23 additions & 33 deletions examples/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,49 +35,39 @@ def _(hm, mo, os):
@app.cell
def _(hm, workspace):
client = workspace.client
status = hm.connection_status(client)
browser = hm.table_browser(client)
editor = hm.sql_editor(
client,
default_sql="SELECT 1 AS ok",
)
status = hm.connections_panel(client)
recent = hm.recent_results(client, limit=20)
history = hm.run_history(client, limit=10)
return browser, client, editor, history, recent, status, workspace
return client, history, recent, status


@app.cell
def _(browser, editor, mo, recent, status, workspace):
return mo.vstack(
[
workspace.ui,
status,
browser.ui,
editor.ui,
recent.ui,
],
gap=2,
)


@app.cell
def _(history):
return history
def _(mo):
mo.md(r"""
## HotData explorer
Use the tabs below to switch between workspaces, connection status, recent results, and run history.

On a shared or networked host, run Marimo **without** `--no-token` and open the printed URL
with its access token so only you can use this notebook.
""")
return


@app.cell
def _(editor, hm):
# Explicitly touch nested widget values so Marimo reruns this cell on clicks.
_run = editor.run.value
_rerun = editor.rerun.value
_clear = editor.clear.value
return hm.query_result(editor.result), _clear, _rerun, _run
def _(recent):
recent_tab = recent.tab_ui
return (recent_tab,)


@app.cell
def _(hm, recent):
_selected = recent.pick.value
return hm.query_result(recent.result, label="Recent result"), _selected
def _(history, mo, recent_tab, status, workspace):
mo.ui.tabs({
"Workspaces": workspace.ui,
"Connections": status,
"Recent results": recent_tab,
"Run history": history,
})
return


@app.cell
Expand All @@ -86,7 +76,7 @@ def _(client, mo):
"""
SELECT 1 AS example_value
""",
engine=client,
engine=client
)
return

Expand Down
2 changes: 2 additions & 0 deletions hotdata_marimo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from hotdata_marimo.display import (
RecentResults,
connection_status,
connections_panel,
query_result,
recent_results,
run_history,
Expand All @@ -36,6 +37,7 @@
"WorkspaceSelector",
"connection_picker",
"connection_status",
"connections_panel",
"from_env",
"hotdata_connection_picker",
"hotdata_query_result",
Expand Down
107 changes: 107 additions & 0 deletions hotdata_marimo/_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Shared dropdown option helpers for Marimo UI widgets."""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

import marimo as mo

from hotdata_runtime import HotdataClient


def unique_label_options(
pairs: list[tuple[str, str]],
*,
disambiguate: Callable[[str, str, int], str] | None = None,
) -> dict[str, str]:
"""Build a label→value map, suffixing repeated labels when needed."""
counts: dict[str, int] = {}
options: dict[str, str] = {}
for label, value in pairs:
count = counts.get(label, 0)
counts[label] = count + 1
if count == 0:
key = label
elif disambiguate is not None:
key = disambiguate(label, value, count)
else:
key = f"{label} ({count + 1})"
options[key] = value
return options


def empty_dropdown(
*,
label: str,
message: str,
full_width: bool = True,
):
return mo.ui.dropdown(
options={message: ""},
label=label,
full_width=full_width,
)


def connection_options(conns: list[Any]) -> dict[str, str]:
pairs = [(str(c.name), str(c.id)) for c in conns]
return unique_label_options(
pairs,
disambiguate=lambda label, value, count: f"{label} ({value})",
)


def connection_picker_from_connections(
conns: list[Any],
*,
label: str = "Connection",
full_width: bool = True,
):
if not conns:
return empty_dropdown(
label=label,
message="(no connections)",
full_width=full_width,
)
return mo.ui.dropdown(
options=connection_options(conns),
label=label,
full_width=full_width,
)


def connection_picker(
client: HotdataClient,
*,
label: str = "Connection",
full_width: bool = True,
):
conns = client.connections().list_connections().connections
return connection_picker_from_connections(
conns,
label=label,
full_width=full_width,
)


def resolve_connection_picker(
client: HotdataClient,
*,
label: str = "Connection",
full_width: bool = True,
) -> tuple[Any | None, str | None]:
"""Return ``(dropdown_or_none, implicit_connection_id)`` for table browsers."""
conns = client.connections().list_connections().connections
if not conns:
return None, ""
if len(conns) == 1:
return None, conns[0].id
return (
connection_picker_from_connections(
conns,
label=label,
full_width=full_width,
),
None,
)
107 changes: 82 additions & 25 deletions hotdata_marimo/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@
from hotdata_runtime import HotdataClient, QueryResult, workspace_health_lines


def _option_map_with_unique_labels(
pairs: list[tuple[str, str]],
) -> dict[str, str]:
counts: dict[str, int] = {}
options: dict[str, str] = {}
for label, value in pairs:
count = counts.get(label, 0)
counts[label] = count + 1
key = label if count == 0 else f"{label} ({count + 1})"
options[key] = value
return options


def query_result(
result: QueryResult,
*,
Expand Down Expand Up @@ -72,32 +59,71 @@ class RecentResults:
def __init__(self, client: HotdataClient, *, limit: int = 50) -> None:
self._client = client
self._results = client.list_recent_results(limit=limit, offset=0)
option_pairs = [
(f"{r.created_at} · {r.status} · {r.result_id}", r.result_id)
self._rows: list[dict[str, object]] = [
{
"created_at": r.created_at,
"status": r.status,
"result_id": r.result_id,
}
for r in self._results
]
options = _option_map_with_unique_labels(option_pairs)
self.pick = mo.ui.dropdown(
options=options or {"(no results)": ""},
label="Recent results",
full_width=True,
self.table = (
mo.ui.table(
self._rows,
label="Recent results",
pagination=True,
page_size=min(10, limit),
selection="single",
max_height=320,
)
if self._rows
else None
)

@property
def selected_result_id(self) -> str | None:
v = self.pick.value
return v if v else None
if self.table is None:
return None
selected = self.table.value
if not selected:
return None
row = selected[0]
if not isinstance(row, dict):
return None
rid = row.get("result_id")
return rid if rid else None

@property
def result(self) -> QueryResult:
rid = self.selected_result_id
mo.stop(rid is None, mo.md("Pick a result id to load."))
mo.stop(rid is None, mo.md("Select a result row to load."))
return self._client.get_result(rid or "")

@property
def result_panel(self):
rid = self.selected_result_id
if rid is None:
return mo.md("_Select a result row to load._")
return query_result(self._client.get_result(rid), label="Recent result")

@property
def tab_ui(self):
if self.table is not None:
_ = self.table.value
return mo.vstack([self.ui, self.result_panel], gap=2)

@property
def ui(self):
_ = self.pick.value
return mo.vstack([self.pick], gap=1)
if self.table is None:
return mo.md("_No recent results._")
_ = self.table.value
return mo.vstack(
[
mo.md("### Recent results"),
self.table,
],
gap=1,
)


def recent_results(client: HotdataClient, *, limit: int = 50) -> RecentResults:
Expand Down Expand Up @@ -150,3 +176,34 @@ def connection_status(client: HotdataClient):
mo.md(f"**API** error — {parts[0]}"),
kind="danger",
)


def connections_panel(client: HotdataClient):
"""Workspace health callout plus a table of configured connections."""
status = connection_status(client)
conns = client.connections().list_connections().connections
if not conns:
return mo.vstack([status, mo.md("_No connections in this workspace._")], gap=1)
rows: list[dict[str, object]] = []
for c in conns:
rows.append(
{
"name": c.name,
"id": c.id,
"source_type": getattr(c, "source_type", None),
}
)
return mo.vstack(
[
status,
mo.ui.table(
rows,
label="Connections",
pagination=True,
page_size=min(10, len(rows)),
selection=None,
max_height=320,
),
],
gap=1,
)
Loading
Loading