Skip to content

Commit 798077b

Browse files
authored
Merge pull request #3 from hotdata-dev/refactor/dedupe-marimo-ui-helpers
refactor: dedupe marimo dropdown and connection helpers
2 parents 87deab3 + a060ade commit 798077b

10 files changed

Lines changed: 438 additions & 191 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,16 @@ See `examples/demo.py` for a full runnable notebook flow.
8181

8282
## Examples
8383

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

86-
Run:
86+
Run locally (single-user machine):
8787

8888
```bash
8989
uv run marimo edit examples/demo.py --no-token
9090
```
9191

92+
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.
93+
9294
## Layout
9395

9496
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`**.

examples/demo.py

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -35,49 +35,39 @@ def _(hm, mo, os):
3535
@app.cell
3636
def _(hm, workspace):
3737
client = workspace.client
38-
status = hm.connection_status(client)
39-
browser = hm.table_browser(client)
40-
editor = hm.sql_editor(
41-
client,
42-
default_sql="SELECT 1 AS ok",
43-
)
38+
status = hm.connections_panel(client)
4439
recent = hm.recent_results(client, limit=20)
4540
history = hm.run_history(client, limit=10)
46-
return browser, client, editor, history, recent, status, workspace
41+
return client, history, recent, status
4742

4843

4944
@app.cell
50-
def _(browser, editor, mo, recent, status, workspace):
51-
return mo.vstack(
52-
[
53-
workspace.ui,
54-
status,
55-
browser.ui,
56-
editor.ui,
57-
recent.ui,
58-
],
59-
gap=2,
60-
)
61-
62-
63-
@app.cell
64-
def _(history):
65-
return history
45+
def _(mo):
46+
mo.md(r"""
47+
## HotData explorer
48+
Use the tabs below to switch between workspaces, connection status, recent results, and run history.
49+
50+
On a shared or networked host, run Marimo **without** `--no-token` and open the printed URL
51+
with its access token so only you can use this notebook.
52+
""")
53+
return
6654

6755

6856
@app.cell
69-
def _(editor, hm):
70-
# Explicitly touch nested widget values so Marimo reruns this cell on clicks.
71-
_run = editor.run.value
72-
_rerun = editor.rerun.value
73-
_clear = editor.clear.value
74-
return hm.query_result(editor.result), _clear, _rerun, _run
57+
def _(recent):
58+
recent_tab = recent.tab_ui
59+
return (recent_tab,)
7560

7661

7762
@app.cell
78-
def _(hm, recent):
79-
_selected = recent.pick.value
80-
return hm.query_result(recent.result, label="Recent result"), _selected
63+
def _(history, mo, recent_tab, status, workspace):
64+
mo.ui.tabs({
65+
"Workspaces": workspace.ui,
66+
"Connections": status,
67+
"Recent results": recent_tab,
68+
"Run history": history,
69+
})
70+
return
8171

8272

8373
@app.cell
@@ -86,7 +76,7 @@ def _(client, mo):
8676
"""
8777
SELECT 1 AS example_value
8878
""",
89-
engine=client,
79+
engine=client
9080
)
9181
return
9282

hotdata_marimo/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from hotdata_marimo.display import (
1313
RecentResults,
1414
connection_status,
15+
connections_panel,
1516
query_result,
1617
recent_results,
1718
run_history,
@@ -36,6 +37,7 @@
3637
"WorkspaceSelector",
3738
"connection_picker",
3839
"connection_status",
40+
"connections_panel",
3941
"from_env",
4042
"hotdata_connection_picker",
4143
"hotdata_query_result",

hotdata_marimo/_options.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Shared dropdown option helpers for Marimo UI widgets."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from typing import Any
7+
8+
import marimo as mo
9+
10+
from hotdata_runtime import HotdataClient
11+
12+
13+
def unique_label_options(
14+
pairs: list[tuple[str, str]],
15+
*,
16+
disambiguate: Callable[[str, str, int], str] | None = None,
17+
) -> dict[str, str]:
18+
"""Build a label→value map, suffixing repeated labels when needed."""
19+
counts: dict[str, int] = {}
20+
options: dict[str, str] = {}
21+
for label, value in pairs:
22+
count = counts.get(label, 0)
23+
counts[label] = count + 1
24+
if count == 0:
25+
key = label
26+
elif disambiguate is not None:
27+
key = disambiguate(label, value, count)
28+
else:
29+
key = f"{label} ({count + 1})"
30+
options[key] = value
31+
return options
32+
33+
34+
def empty_dropdown(
35+
*,
36+
label: str,
37+
message: str,
38+
full_width: bool = True,
39+
):
40+
return mo.ui.dropdown(
41+
options={message: ""},
42+
label=label,
43+
full_width=full_width,
44+
)
45+
46+
47+
def connection_options(conns: list[Any]) -> dict[str, str]:
48+
pairs = [(str(c.name), str(c.id)) for c in conns]
49+
return unique_label_options(
50+
pairs,
51+
disambiguate=lambda label, value, count: f"{label} ({value})",
52+
)
53+
54+
55+
def connection_picker_from_connections(
56+
conns: list[Any],
57+
*,
58+
label: str = "Connection",
59+
full_width: bool = True,
60+
):
61+
if not conns:
62+
return empty_dropdown(
63+
label=label,
64+
message="(no connections)",
65+
full_width=full_width,
66+
)
67+
return mo.ui.dropdown(
68+
options=connection_options(conns),
69+
label=label,
70+
full_width=full_width,
71+
)
72+
73+
74+
def connection_picker(
75+
client: HotdataClient,
76+
*,
77+
label: str = "Connection",
78+
full_width: bool = True,
79+
):
80+
conns = client.connections().list_connections().connections
81+
return connection_picker_from_connections(
82+
conns,
83+
label=label,
84+
full_width=full_width,
85+
)
86+
87+
88+
def resolve_connection_picker(
89+
client: HotdataClient,
90+
*,
91+
label: str = "Connection",
92+
full_width: bool = True,
93+
) -> tuple[Any | None, str | None]:
94+
"""Return ``(dropdown_or_none, implicit_connection_id)`` for table browsers."""
95+
conns = client.connections().list_connections().connections
96+
if not conns:
97+
return None, ""
98+
if len(conns) == 1:
99+
return None, conns[0].id
100+
return (
101+
connection_picker_from_connections(
102+
conns,
103+
label=label,
104+
full_width=full_width,
105+
),
106+
None,
107+
)

hotdata_marimo/display.py

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,6 @@
77
from hotdata_runtime import HotdataClient, QueryResult, workspace_health_lines
88

99

10-
def _option_map_with_unique_labels(
11-
pairs: list[tuple[str, str]],
12-
) -> dict[str, str]:
13-
counts: dict[str, int] = {}
14-
options: dict[str, str] = {}
15-
for label, value in pairs:
16-
count = counts.get(label, 0)
17-
counts[label] = count + 1
18-
key = label if count == 0 else f"{label} ({count + 1})"
19-
options[key] = value
20-
return options
21-
22-
2310
def query_result(
2411
result: QueryResult,
2512
*,
@@ -72,32 +59,71 @@ class RecentResults:
7259
def __init__(self, client: HotdataClient, *, limit: int = 50) -> None:
7360
self._client = client
7461
self._results = client.list_recent_results(limit=limit, offset=0)
75-
option_pairs = [
76-
(f"{r.created_at} · {r.status} · {r.result_id}", r.result_id)
62+
self._rows: list[dict[str, object]] = [
63+
{
64+
"created_at": r.created_at,
65+
"status": r.status,
66+
"result_id": r.result_id,
67+
}
7768
for r in self._results
7869
]
79-
options = _option_map_with_unique_labels(option_pairs)
80-
self.pick = mo.ui.dropdown(
81-
options=options or {"(no results)": ""},
82-
label="Recent results",
83-
full_width=True,
70+
self.table = (
71+
mo.ui.table(
72+
self._rows,
73+
label="Recent results",
74+
pagination=True,
75+
page_size=min(10, limit),
76+
selection="single",
77+
max_height=320,
78+
)
79+
if self._rows
80+
else None
8481
)
8582

8683
@property
8784
def selected_result_id(self) -> str | None:
88-
v = self.pick.value
89-
return v if v else None
85+
if self.table is None:
86+
return None
87+
selected = self.table.value
88+
if not selected:
89+
return None
90+
row = selected[0]
91+
if not isinstance(row, dict):
92+
return None
93+
rid = row.get("result_id")
94+
return rid if rid else None
9095

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

102+
@property
103+
def result_panel(self):
104+
rid = self.selected_result_id
105+
if rid is None:
106+
return mo.md("_Select a result row to load._")
107+
return query_result(self._client.get_result(rid), label="Recent result")
108+
109+
@property
110+
def tab_ui(self):
111+
if self.table is not None:
112+
_ = self.table.value
113+
return mo.vstack([self.ui, self.result_panel], gap=2)
114+
97115
@property
98116
def ui(self):
99-
_ = self.pick.value
100-
return mo.vstack([self.pick], gap=1)
117+
if self.table is None:
118+
return mo.md("_No recent results._")
119+
_ = self.table.value
120+
return mo.vstack(
121+
[
122+
mo.md("### Recent results"),
123+
self.table,
124+
],
125+
gap=1,
126+
)
101127

102128

103129
def recent_results(client: HotdataClient, *, limit: int = 50) -> RecentResults:
@@ -150,3 +176,34 @@ def connection_status(client: HotdataClient):
150176
mo.md(f"**API** error — {parts[0]}"),
151177
kind="danger",
152178
)
179+
180+
181+
def connections_panel(client: HotdataClient):
182+
"""Workspace health callout plus a table of configured connections."""
183+
status = connection_status(client)
184+
conns = client.connections().list_connections().connections
185+
if not conns:
186+
return mo.vstack([status, mo.md("_No connections in this workspace._")], gap=1)
187+
rows: list[dict[str, object]] = []
188+
for c in conns:
189+
rows.append(
190+
{
191+
"name": c.name,
192+
"id": c.id,
193+
"source_type": getattr(c, "source_type", None),
194+
}
195+
)
196+
return mo.vstack(
197+
[
198+
status,
199+
mo.ui.table(
200+
rows,
201+
label="Connections",
202+
pagination=True,
203+
page_size=min(10, len(rows)),
204+
selection=None,
205+
max_height=320,
206+
),
207+
],
208+
gap=1,
209+
)

0 commit comments

Comments
 (0)