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]
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()
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'.
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).
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"}, ...]
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.
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)
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": ...}
189def header(title: str, library: str = "default", emoji: str = "", subtitle: str = ""): 190 """Top banner used at the start of every launch() UI. `emoji` is kept for 191 API compatibility but not used by any dash-* package — Databricks' own 192 page headers are plain text, no decorative glyph.""" 193 color = accent(library) 194 prefix = f"{emoji} " if emoji else "" 195 sub = ( 196 f"<div style='font-size:12px;color:{MUTED_FOREGROUND};margin-top:2px;" 197 f"font-family:{FONT_SANS}'>{subtitle}</div>" 198 ) if subtitle else "" 199 return html( 200 f"<h2 style='color:{color};margin-bottom:0;font-weight:700;" 201 f"letter-spacing:-0.01em;font-family:{FONT_SANS}'>{prefix}{title}</h2>{sub}" 202 )
Top banner used at the start of every launch() UI. emoji is kept for
API compatibility but not used by any dash-* package — Databricks' own
page headers are plain text, no decorative glyph.
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.
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)
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.
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)
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.
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.
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.
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.
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.
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.
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.
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.
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').