dashui

DashUI — shared ipywidgets component library for the Dashlibs suite. Import from here in any dash-* package instead of duplicating widget code.

 1"""
 2DashUI — shared ipywidgets component library for the Dashlibs suite.
 3Import from here in any dash-* package instead of duplicating widget code.
 4"""
 5from dashui.components import (
 6    EditableTable,
 7    EnvSetupPanel,
 8    SourceSelector,
 9    action_button,
10    card,
11    editable_table,
12    env_setup_panel,
13    header,
14    html,
15    output_panel,
16    running_list,
17    section,
18    source_selector,
19    status_line,
20)
21from dashui.persistence import (
22    clear_config_dir,
23    config_path,
24    get_config_dir,
25    load_config,
26    save_config,
27    set_config_dir,
28)
29from dashui.schema import list_columns, list_columns_safe
30from dashui.theme import (
31    ACCENT_BG,
32    ACCENT_FG,
33    BORDER,
34    BORDER_STRONG,
35    CARD,
36    DANGER,
37    FONT_MONO,
38    FONT_SANS,
39    INFO,
40    MUTED,
41    PRIMARY,
42    SUCCESS,
43    WARNING,
44    accent,
45)
46
47__version__ = "0.3.1"
48__all__ = [
49    "SourceSelector",
50    "EditableTable",
51    "EnvSetupPanel",
52    "action_button",
53    "card",
54    "editable_table",
55    "env_setup_panel",
56    "header",
57    "html",
58    "output_panel",
59    "running_list",
60    "section",
61    "source_selector",
62    "status_line",
63    "list_columns",
64    "list_columns_safe",
65    "get_config_dir",
66    "set_config_dir",
67    "clear_config_dir",
68    "config_path",
69    "load_config",
70    "save_config",
71    "accent",
72    "PRIMARY",
73    "SUCCESS",
74    "DANGER",
75    "WARNING",
76    "INFO",
77    "BORDER",
78    "BORDER_STRONG",
79    "CARD",
80    "MUTED",
81    "ACCENT_BG",
82    "ACCENT_FG",
83    "FONT_SANS",
84    "FONT_MONO",
85]
@dataclass
class SourceSelector:
234@dataclass
235class SourceSelector:
236    """
237    The UC Table / DataFrame variable / SQL Query picker used by every
238    Dashlibs UI that reads from Databricks.
239
240    Usage::
241        src = source_selector()
242        ui = card([src.toggle, src.box, ...])
243        kind, value = src.value()
244    """
245    toggle: object
246    box: object
247    table_input: object
248    df_input: object
249    sql_input: object
250
251    def value(self) -> tuple[str, str]:
252        """Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'."""
253        if self.toggle.value == "UC Table":
254            return "table", self.table_input.value.strip()
255        if self.toggle.value == "DataFrame variable":
256            return "dataframe", self.df_input.value.strip()
257        return "sql", self.sql_input.value.strip()
258
259    def resolve_df(self):
260        """Resolve the selected source to a Spark DataFrame (for direct use in core classes)."""
261        kind, value = self.value()
262        if kind == "dataframe":
263            import IPython
264            shell = IPython.get_ipython()
265            df = shell.user_ns.get(value) if shell else None
266            if df is None:
267                raise ValueError(f"Variable '{value}' not found")
268            return df
269        from pyspark.sql import SparkSession
270        spark = SparkSession.getActiveSession()
271        if kind == "table":
272            return spark.table(value)
273        return spark.sql(value)

The UC Table / DataFrame variable / SQL Query picker used by every Dashlibs UI that reads from Databricks.

Usage:: src = source_selector() ui = card([src.toggle, src.box, ...]) kind, value = src.value()

SourceSelector( toggle: object, box: object, table_input: object, df_input: object, sql_input: object)
toggle: object
box: object
table_input: object
df_input: object
sql_input: object
def value(self) -> tuple[str, str]:
251    def value(self) -> tuple[str, str]:
252        """Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'."""
253        if self.toggle.value == "UC Table":
254            return "table", self.table_input.value.strip()
255        if self.toggle.value == "DataFrame variable":
256            return "dataframe", self.df_input.value.strip()
257        return "sql", self.sql_input.value.strip()

Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'.

def resolve_df(self):
259    def resolve_df(self):
260        """Resolve the selected source to a Spark DataFrame (for direct use in core classes)."""
261        kind, value = self.value()
262        if kind == "dataframe":
263            import IPython
264            shell = IPython.get_ipython()
265            df = shell.user_ns.get(value) if shell else None
266            if df is None:
267                raise ValueError(f"Variable '{value}' not found")
268            return df
269        from pyspark.sql import SparkSession
270        spark = SparkSession.getActiveSession()
271        if kind == "table":
272            return spark.table(value)
273        return spark.sql(value)

Resolve the selected source to a Spark DataFrame (for direct use in core classes).

@dataclass
class EditableTable:
339@dataclass
340class EditableTable:
341    """
342    An add/remove-row key-value grid — the pattern Databricks itself uses for
343    job parameters, cluster tags, and environment variables, instead of a
344    single free-text "key=value, key=value" field.
345
346    Usage::
347        tbl = editable_table(["Key", "Value"], placeholders={"Key": "AWS_REGION"})
348        ui = card([tbl.widget, ...])
349        rows = tbl.values()  # [{"Key": "AWS_REGION", "Value": "us-east-1"}, ...]
350    """
351    widget: object
352    add_row: object   # callable(b=None) -> None, wired as a Button.on_click handler too
353    values: object     # callable() -> list[dict[str, str]]

An add/remove-row key-value grid — the pattern Databricks itself uses for job parameters, cluster tags, and environment variables, instead of a single free-text "key=value, key=value" field.

Usage:: tbl = editable_table(["Key", "Value"], placeholders={"Key": "AWS_REGION"}) ui = card([tbl.widget, ...]) rows = tbl.values() # [{"Key": "AWS_REGION", "Value": "us-east-1"}, ...]

EditableTable(widget: object, add_row: object, values: object)
widget: object
add_row: object
values: object
@dataclass
class EnvSetupPanel:
480@dataclass
481class EnvSetupPanel:
482    widget: object
483    values: object  # callable() -> dict
EnvSetupPanel(widget: object, values: object)
widget: object
values: object
def action_button(text: str, style: str = 'primary', emoji: str = ''):
296def action_button(text: str, style: str = "primary", emoji: str = ""):
297    """style in primary|success|warning|danger|info — matches the Databricks
298    button variants. `emoji` is kept for API compatibility but not used by
299    any dash-* package — Databricks buttons are plain text, no glyph."""
300    w = _require_widgets()
301    label = f"{emoji} {text}".strip()
302    btn = w.Button(description=label, layout=w.Layout(height="32px", padding="0 14px", width="auto"))
303    btn.add_class(f"dashui-btn-{style or 'default'}")
304    return btn

style in primary|success|warning|danger|info — matches the Databricks button variants. emoji is kept for API compatibility but not used by any dash-* package — Databricks buttons are plain text, no glyph.

def card(children, padding: str = '16px'):
222def card(children, padding: str = "16px"):
223    """Bordered, shadowed VBox container — the outer shell for every launch() UI."""
224    w = _require_widgets()
225    global _STYLE_INJECTED
226    body = [_global_style(), *children] if not _STYLE_INJECTED else list(children)
227    _STYLE_INJECTED = True
228    box = w.VBox(body, layout=w.Layout(padding=padding))
229    box.add_class("dashui-card")
230    box.add_class("dashui-root")
231    return box

Bordered, shadowed VBox container — the outer shell for every launch() UI.

def editable_table( columns: list[str], placeholders: dict[str, str] | None = None, initial_rows: int = 1) -> EditableTable:
356def editable_table(columns: list[str], placeholders: dict[str, str] | None = None, initial_rows: int = 1) -> EditableTable:
357    w = _require_widgets()
358    placeholders = placeholders or {}
359    row_entries: list[tuple[object, dict]] = []  # (row_box, {col: Text widget})
360
361    header_row = w.HBox(
362        [w.HTML(f"<div class='dashui-table-header'>{col}</div>") for col in columns] + [w.HTML("", layout=w.Layout(width="32px"))]
363    )
364    rows_box = w.VBox([])
365
366    def _make_row():
367        cells = {col: w.Text(placeholder=placeholders.get(col, ""), layout=w.Layout(width="auto", flex="1")) for col in columns}
368        remove_btn = w.Button(description="✕", layout=w.Layout(width="32px", height="28px"), tooltip="Remove row")
369        remove_btn.add_class("dashui-btn-info")
370        row_box = w.HBox([cells[c] for c in columns] + [remove_btn])
371        row_box.add_class("dashui-table-row")
372
373        def on_remove(_b):
374            row_entries[:] = [(rb, c) for rb, c in row_entries if rb is not row_box]
375            rows_box.children = tuple(rb for rb, _ in row_entries)
376
377        remove_btn.on_click(on_remove)
378        return row_box, cells
379
380    def add_row(_b=None):
381        row_box, cells = _make_row()
382        row_entries.append((row_box, cells))
383        rows_box.children = tuple(rb for rb, _ in row_entries)
384
385    for _ in range(initial_rows):
386        add_row()
387
388    add_btn = action_button("Add row", style="info", emoji="+")
389    add_btn.on_click(add_row)
390
391    def values() -> list[dict]:
392        return [
393            {col: cells[col].value.strip() for col in columns}
394            for _, cells in row_entries
395            if any(cells[col].value.strip() for col in columns)
396        ]
397
398    table = w.VBox([header_row, rows_box, add_btn])
399    table.add_class("dashui-table")
400    return EditableTable(widget=table, add_row=add_row, values=values)
def env_setup_panel(library: str, extra_fields: dict | None = None):
403def env_setup_panel(library: str, extra_fields: dict | None = None):
404    """
405    A ready-to-embed "Environment Setup" panel: where should this package's
406    configs live? Defaults to the notebook's current working directory;
407    Save remembers a different directory (e.g. a Workspace path or Volume)
408    for every future session, across notebooks.
409
410    `extra_fields` is `{label: placeholder}` for any library-specific
411    defaults (e.g. {"Default catalog": "main"}) — saved alongside the
412    directory choice and available via the returned `values()`.
413
414    Usage::
415        env = dashui.env_setup_panel("dashingest", extra_fields={"Default catalog": "main"})
416        ui = card([..., env.widget, ...])
417        settings = env.values()  # {"config_dir": ..., "Default catalog": ...}
418    """
419    from dashui.persistence import get_config_dir, load_config, save_config, set_config_dir
420
421    w = _require_widgets()
422    extra_fields = extra_fields or {}
423    saved = load_config(library, name="env")
424
425    dir_input = w.Text(
426        description="Config directory:",
427        value=saved.get("config_dir", get_config_dir(library)),
428        placeholder=get_config_dir(library),
429        layout=w.Layout(width="420px"),
430    )
431    extra_inputs = {
432        label: w.Text(description=f"{label}:", value=saved.get(label, ""), placeholder=placeholder)
433        for label, placeholder in extra_fields.items()
434    }
435
436    save_btn = action_button("Save", style="primary")
437    reload_btn = action_button("Reload", style="info")
438    status = html(f"<span style='font-size:12px;color:{MUTED_FOREGROUND}'>Currently using: <code>{get_config_dir(library)}</code></span>")
439
440    def _collect() -> dict:
441        return {"config_dir": dir_input.value.strip() or get_config_dir(library),
442                **{label: field.value for label, field in extra_inputs.items()}}
443
444    def _on_save(_b):
445        config = _collect()
446        set_config_dir(library, config["config_dir"])
447        path = save_config(library, config, name="env")
448        status.value = f"<span style='font-size:12px;color:{SUCCESS}'>Saved — settings will be read from <code>{path}</code> in future sessions.</span>"
449
450    def _on_reload(_b):
451        current = load_config(library, name="env")
452        dir_input.value = current.get("config_dir", get_config_dir(library))
453        for label, field in extra_inputs.items():
454            field.value = current.get(label, "")
455        status.value = f"<span style='font-size:12px;color:{MUTED_FOREGROUND}'>Reloaded from <code>{config_path_display(library)}</code>.</span>"
456
457    save_btn.on_click(_on_save)
458    reload_btn.on_click(_on_reload)
459
460    panel = w.VBox([
461        html(
462            f"<div style='font-size:12px;color:{MUTED_FOREGROUND};margin-bottom:4px'>"
463            "Where should this package's configs be read/written? Leave as-is to use "
464            "the notebook's current working directory — nothing here is required."
465            "</div>"
466        ),
467        dir_input,
468        *extra_inputs.values(),
469        w.HBox([save_btn, reload_btn]),
470        status,
471    ])
472    return EnvSetupPanel(widget=panel, values=_collect)

A ready-to-embed "Environment Setup" panel: where should this package's configs live? Defaults to the notebook's current working directory; Save remembers a different directory (e.g. a Workspace path or Volume) for every future session, across notebooks.

extra_fields is {label: placeholder} for any library-specific defaults (e.g. {"Default catalog": "main"}) — saved alongside the directory choice and available via the returned values().

Usage:: env = dashui.env_setup_panel("dashingest", extra_fields={"Default catalog": "main"}) ui = card([..., env.widget, ...]) settings = env.values() # {"config_dir": ..., "Default catalog": ...}

def html(text: str):
57def html(text: str):
58    w = _require_widgets()
59    return w.HTML(text)
def output_panel():
307def output_panel():
308    """Standard scrollable output area for run/profile results and errors."""
309    w = _require_widgets()
310    out = w.Output(layout=w.Layout(padding="12px"))
311    out.add_class("dashui-output")
312    return out

Standard scrollable output area for run/profile results and errors.

def running_list(formatter):
315def running_list(formatter):
316    """
317    A live-updating numbered list display, the pattern used for 'added entities'
318    / 'added relationships' style accumulators.
319
320    Usage::
321        items = []
322        out, render = running_list(lambda i, item: f"{i}. {item['name']}")
323        items.append({"name": "Customer"})
324        render(items)
325    """
326    w = _require_widgets()
327    out = w.Output(layout=w.Layout(padding="8px 12px"))
328    out.add_class("dashui-output")
329
330    def render(items: list):
331        with out:
332            out.clear_output()
333            for i, item in enumerate(items, 1):
334                print(formatter(i, item))
335
336    return out, render

A live-updating numbered list display, the pattern used for 'added entities' / 'added relationships' style accumulators.

Usage:: items = [] out, render = running_list(lambda i, item: f"{i}. {item['name']}") items.append({"name": "Customer"}) render(items)

def section(title: str):
205def section(title: str):
206    """Step/section divider, styled like the datapal-access card label convention."""
207    return html(f"<div class='dashui-section'>{title}</div>")

Step/section divider, styled like the datapal-access card label convention.

def source_selector(label: str = 'Source:') -> SourceSelector:
276def source_selector(label: str = "Source:") -> SourceSelector:
277    w = _require_widgets()
278    toggle = w.ToggleButtons(options=["UC Table", "DataFrame variable", "SQL Query"], description=label)
279    table_input = w.Text(placeholder="catalog.schema.table", description="Table:")
280    df_input = w.Text(placeholder="df", description="Variable:")
281    sql_input = w.Textarea(placeholder="SELECT * FROM ...", description="SQL:", rows=3)
282    box = w.VBox([table_input])
283
284    def on_change(change):
285        if change["new"] == "UC Table":
286            box.children = [table_input]
287        elif change["new"] == "DataFrame variable":
288            box.children = [df_input]
289        else:
290            box.children = [sql_input]
291
292    toggle.observe(on_change, names="value")
293    return SourceSelector(toggle, box, table_input, df_input, sql_input)
def status_line(text: str, kind: str = 'info'):
210def status_line(text: str, kind: str = "info"):
211    """One-line status message: kind in success|error|warning|info. A small
212    solid dot carries the color instead of a colorful emoji — closer to how
213    Databricks' own job/cluster status indicators read."""
214    color = {"success": SUCCESS, "error": DANGER, "warning": WARNING, "info": MUTED_FOREGROUND}.get(kind, MUTED_FOREGROUND)
215    return html(
216        f"<span style='font-family:{FONT_SANS};color:#1B3139'>"
217        f"<span style='display:inline-block;width:6px;height:6px;border-radius:50%;"
218        f"background:{color};margin-right:7px'></span>{text}</span>"
219    )

One-line status message: kind in success|error|warning|info. A small solid dot carries the color instead of a colorful emoji — closer to how Databricks' own job/cluster status indicators read.

def list_columns(table: str) -> list[str]:
 6def list_columns(table: str) -> list[str]:
 7    """Return column names for a UC table, without loading any data."""
 8    from pyspark.sql import SparkSession
 9    spark = SparkSession.getActiveSession()
10    return [f.name for f in spark.table(table).schema.fields]

Return column names for a UC table, without loading any data.

def list_columns_safe(table: str) -> list[str]:
13def list_columns_safe(table: str) -> list[str]:
14    """Like list_columns, but returns [] instead of raising — for UI dropdowns."""
15    try:
16        return list_columns(table)
17    except Exception:
18        return []

Like list_columns, but returns [] instead of raising — for UI dropdowns.

def get_config_dir(library: str) -> str:
29def get_config_dir(library: str) -> str:
30    """The directory configs for `library` should be read/written from:
31    the env_setup()-configured directory if one was set, else cwd."""
32    pointer = _pointer_path(library)
33    if pointer.exists():
34        try:
35            configured = json.loads(pointer.read_text()).get("config_dir")
36            if configured:
37                return configured
38        except Exception:
39            pass
40    return os.environ.get("DASHLIBS_CONFIG_DIR", os.getcwd())

The directory configs for library should be read/written from: the env_setup()-configured directory if one was set, else cwd.

def set_config_dir(library: str, path: str) -> None:
43def set_config_dir(library: str, path: str) -> None:
44    """Remember `path` as this library's config directory for future sessions."""
45    pointer = _pointer_path(library)
46    pointer.parent.mkdir(parents=True, exist_ok=True)
47    pointer.write_text(json.dumps({"config_dir": path}, indent=2))

Remember path as this library's config directory for future sessions.

def clear_config_dir(library: str) -> None:
50def clear_config_dir(library: str) -> None:
51    """Forget the configured directory — future calls fall back to cwd again."""
52    pointer = _pointer_path(library)
53    if pointer.exists():
54        pointer.unlink()

Forget the configured directory — future calls fall back to cwd again.

def config_path(library: str, name: str = 'config') -> str:
57def config_path(library: str, name: str = "config") -> str:
58    return os.path.join(get_config_dir(library), f"{library}_{name}.json")
def load_config(library: str, name: str = 'config', defaults: dict | None = None) -> dict:
61def load_config(library: str, name: str = "config", defaults: dict | None = None) -> dict:
62    """Read `<config_dir>/<library>_<name>.json`, merged over `defaults`.
63    Returns `defaults` (or {}) unchanged if the file doesn't exist or is invalid."""
64    path = config_path(library, name)
65    if os.path.exists(path):
66        try:
67            with open(path) as f:
68                return {**(defaults or {}), **json.load(f)}
69        except Exception:
70            pass
71    return dict(defaults or {})

Read <config_dir>/<library>_<name>.json, merged over defaults. Returns defaults (or {}) unchanged if the file doesn't exist or is invalid.

def save_config(library: str, config: dict, name: str = 'config') -> str:
74def save_config(library: str, config: dict, name: str = "config") -> str:
75    """Write `config` to `<config_dir>/<library>_<name>.json`. Returns the path written."""
76    path = config_path(library, name)
77    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
78    with open(path, "w") as f:
79        json.dump(config, f, indent=2)
80    return path

Write config to <config_dir>/<library>_<name>.json. Returns the path written.

def accent(library: str) -> str:
72def accent(library: str) -> str:
73    """Look up the accent color for a Dashlibs package name (e.g. 'dashsynthetic')."""
74    return ACCENTS.get(library, ACCENTS["default"])

Look up the accent color for a Dashlibs package name (e.g. 'dashsynthetic').

PRIMARY = '#FF3621'
SUCCESS = '#2E7D32'
DANGER = '#C62828'
WARNING = '#B36B00'
INFO = '#0E6BA8'
BORDER = '#DCE0E2'
BORDER_STRONG = '#C7CCD1'
CARD = '#FFFFFF'
MUTED = '#F3F4F5'
ACCENT_BG = '#FFF1EC'
ACCENT_FG = '#B33B1E'
FONT_SANS = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
FONT_MONO = "'Roboto Mono', 'SFMono-Regular', Consolas, monospace"