dashcontrol

DashControl — Databricks Control Center.

1"""DashControl — Databricks Control Center."""
2from dashcontrol.config import ControlCenterConfig, CustomPanel
3from dashcontrol.ui import env_setup, launch
4
5__version__ = "0.1.5"
6__all__ = ["ControlCenterConfig", "CustomPanel", "env_setup", "launch"]
@dataclass
class ControlCenterConfig:
21@dataclass
22class ControlCenterConfig:
23    """
24    Configuration for the Databricks Control Center.
25
26    Parameters
27    ----------
28    date_range_days
29        Default lookback window for all time-based queries (default: 30).
30    catalogs
31        List of catalogs to scope table/lineage queries to.
32        Empty list means all visible catalogs.
33    panels
34        Which built-in panels to show. Defaults to all.
35    custom_panels
36        User-defined SQL panels appended after the built-in tabs.
37    row_limit
38        Max rows returned per panel query (default: 500).
39    workspace_name
40        Optional display name shown in the dashboard header.
41    """
42    date_range_days: int = 30
43    catalogs: list[str] = field(default_factory=list)
44    panels: list[str] = field(default_factory=lambda: list(ALL_PANELS))
45    custom_panels: list[CustomPanel] = field(default_factory=list)
46    row_limit: int = 500
47    workspace_name: str = ""
48
49    def __post_init__(self):
50        unknown = set(self.panels) - set(ALL_PANELS)
51        if unknown:
52            raise ValueError(f"Unknown panels: {unknown}. Valid: {ALL_PANELS}")
53        if self.date_range_days < 1 or self.date_range_days > 365:
54            raise ValueError("date_range_days must be between 1 and 365")
55        if self.row_limit < 1 or self.row_limit > 10_000:
56            raise ValueError("row_limit must be between 1 and 10,000")
57
58    def catalog_filter(self, col: str = "table_catalog") -> str:
59        """SQL WHERE fragment to filter by configured catalogs."""
60        if not self.catalogs:
61            return ""
62        quoted = ", ".join(f"'{c}'" for c in self.catalogs)
63        return f"AND {col} IN ({quoted})"

Configuration for the Databricks Control Center.

Parameters

date_range_days Default lookback window for all time-based queries (default: 30). catalogs List of catalogs to scope table/lineage queries to. Empty list means all visible catalogs. panels Which built-in panels to show. Defaults to all. custom_panels User-defined SQL panels appended after the built-in tabs. row_limit Max rows returned per panel query (default: 500). workspace_name Optional display name shown in the dashboard header.

ControlCenterConfig( date_range_days: int = 30, catalogs: list[str] = <factory>, panels: list[str] = <factory>, custom_panels: list[CustomPanel] = <factory>, row_limit: int = 500, workspace_name: str = '')
date_range_days: int = 30
catalogs: list[str]
panels: list[str]
custom_panels: list[CustomPanel]
row_limit: int = 500
workspace_name: str = ''
def catalog_filter(self, col: str = 'table_catalog') -> str:
58    def catalog_filter(self, col: str = "table_catalog") -> str:
59        """SQL WHERE fragment to filter by configured catalogs."""
60        if not self.catalogs:
61            return ""
62        quoted = ", ".join(f"'{c}'" for c in self.catalogs)
63        return f"AND {col} IN ({quoted})"

SQL WHERE fragment to filter by configured catalogs.

@dataclass
class CustomPanel:
14@dataclass
15class CustomPanel:
16    title: str
17    sql: str
18    description: str = ""
CustomPanel(title: str, sql: str, description: str = '')
title: str
sql: str
description: str = ''
def env_setup() -> None:
14def env_setup() -> None:
15    """Open the environment setup panel — where should dashcontrol read/write
16    its configs? Defaults to the notebook's current working directory if
17    never called."""
18    try:
19        import dashui
20        from IPython.display import display
21    except ImportError:
22        raise RuntimeError("ipywidgets required. Run: %pip install ipywidgets") from None
23
24    display(dashui.card([
25        dashui.header("Databricks Control Center — Environment Setup", library=_LIBRARY),
26        dashui.env_setup_panel(_LIBRARY).widget,
27    ]))

Open the environment setup panel — where should dashcontrol read/write its configs? Defaults to the notebook's current working directory if never called.

def launch(config: ControlCenterConfig = None):
 30def launch(config: ControlCenterConfig = None):
 31    try:
 32        import ipywidgets as w
 33        from IPython.display import display
 34    except ImportError:
 35        raise RuntimeError("ipywidgets required. Run: %pip install ipywidgets")
 36
 37    import dashui
 38    from dashcontrol.runner import run_query_safe
 39    from dashcontrol import sql as Q
 40    from dashcontrol.formatters import (
 41        html_table, stat_tile, stat_row, error_box, info_box,
 42        section_header, sparkline_html, format_number, _TEAL, _RED, _AMBER, _GREEN,
 43    )
 44
 45    if config is None:
 46        saved = dashui.load_config(_LIBRARY)
 47        if saved:
 48            from dashcontrol.config import CustomPanel
 49            saved["custom_panels"] = [CustomPanel(**p) for p in saved.get("custom_panels", [])]
 50            config = ControlCenterConfig(**saved)
 51    cfg = config or ControlCenterConfig()
 52
 53    # ── Global controls ───────────────────────────────────────────────────────
 54    days_slider = w.IntSlider(
 55        value=cfg.date_range_days, min=1, max=90, step=1,
 56        description="Days:", style={"description_width": "40px"},
 57        layout=w.Layout(width="320px"),
 58    )
 59    catalog_text = w.Text(
 60        value=", ".join(cfg.catalogs),
 61        description="Catalogs:",
 62        placeholder="main, hive_metastore (blank = all)",
 63        style={"description_width": "70px"},
 64        layout=w.Layout(width="360px"),
 65    )
 66    workspace_label = w.HTML(
 67        value=f"<div style='font-size:11px;color:#6b7280;padding:4px 0'>"
 68              f"Workspace: <b>{cfg.workspace_name or 'auto-detected'}</b></div>"
 69    )
 70
 71    def _days() -> int:
 72        return days_slider.value
 73
 74    def _cat_filter() -> str:
 75        cats = [c.strip() for c in catalog_text.value.split(",") if c.strip()]
 76        if not cats:
 77            return ""
 78        quoted = ", ".join(f"'{c}'" for c in cats)
 79        return f"AND table_catalog IN ({quoted})"
 80
 81    # ── Panel builder helper ──────────────────────────────────────────────────
 82    def _save_state() -> None:
 83        try:
 84            from dataclasses import asdict
 85            state = asdict(cfg)
 86            state["date_range_days"] = days_slider.value
 87            state["catalogs"] = [c.strip() for c in catalog_text.value.split(",") if c.strip()]
 88            dashui.save_config(_LIBRARY, state)
 89        except Exception:
 90            pass  # persistence is a convenience, never block the actual operation on it
 91
 92    def _panel(sections_fn) -> tuple:
 93        """Create a (load_btn, output) pair and wire the click."""
 94        out = dashui.output_panel()
 95        btn = dashui.action_button("Load", style="info")
 96
 97        def _on_click(b):
 98            btn.disabled = True
 99            btn.description = "Loading…"
100            _save_state()
101            with out:
102                out.clear_output()
103                try:
104                    sections_fn(out)
105                except Exception as e:
106                    import ipywidgets as _w
107                    from IPython.display import display as _d
108                    _d(_w.HTML(error_box(str(e))))
109            btn.disabled = False
110            btn.description = "Refresh"
111
112        btn.on_click(_on_click)
113        return btn, out
114
115    # ─────────────────────────────────────────────────────────────────────────
116    # TAB: HEALTH
117    # ─────────────────────────────────────────────────────────────────────────
118    def _health(out):
119        from IPython.display import display as _d
120        import ipywidgets as _w
121
122        d = _days()
123        dbu   = run_query_safe(Q.health_dbu_today(), 1)
124        users = run_query_safe(Q.health_active_users(d), 1)
125        jobs  = run_query_safe(Q.health_failed_jobs(24), 1)
126        tbls  = run_query_safe(Q.health_table_count(_cat_filter()), 1)
127        trend = run_query_safe(Q.health_dbu_trend(min(d, 14)))
128
129        dbu_val   = format_number(dbu.first_value("dbu_today", 0))
130        user_val  = format_number(users.first_value("active_users", 0))
131        job_val   = jobs.first_value("failed_jobs", 0)
132        tbl_val   = format_number(tbls.first_value("total_tables", 0))
133        job_color = _RED if (job_val or 0) > 0 else _GREEN
134
135        tiles = stat_row([
136            stat_tile("DBU Today",              dbu_val,  _TEAL),
137            stat_tile(f"Active Users ({d}d)",   user_val, _TEAL),
138            stat_tile("Failed Jobs (24h)",       job_val,  job_color),
139            stat_tile("Total Tables",            tbl_val,  _TEAL),
140        ])
141
142        sparkline = ""
143        if trend.ok and trend.rows:
144            vals = [float(r.get("dbu", 0) or 0) for r in trend.rows]
145            dates = [str(r.get("usage_date", ""))[-5:] for r in trend.rows]
146            label = f"{dates[0]}{dates[-1]}" if dates else ""
147            sparkline = (
148                section_header("DBU Trend") +
149                f"<div style='padding:8px 0'>{sparkline_html(vals, label)}</div>"
150            )
151
152        _d(_w.HTML(tiles + sparkline))
153
154    health_btn, health_out = _panel(_health)
155
156    # ─────────────────────────────────────────────────────────────────────────
157    # TAB: COST
158    # ─────────────────────────────────────────────────────────────────────────
159    def _cost(out):
160        from IPython.display import display as _d
161        import ipywidgets as _w
162
163        d = _days()
164        burn  = run_query_safe(Q.cost_burn_rate(d), 1)
165        skus  = run_query_safe(Q.cost_daily_by_sku(d), cfg.row_limit)
166        clust = run_query_safe(Q.cost_top_clusters(d), 10)
167        jobs  = run_query_safe(Q.cost_top_jobs(d), 10)
168        users = run_query_safe(Q.cost_top_users_by_dbu(d), 10)
169
170        total  = format_number(burn.first_value("total_dbu", 0))
171        daily  = format_number(burn.first_value("avg_daily_dbu", 0))
172        proj   = format_number(burn.first_value("projected_monthly_dbu", 0))
173
174        tiles = stat_row([
175            stat_tile(f"Total DBU ({d}d)",        total, _TEAL),
176            stat_tile("Avg Daily DBU",             daily, _TEAL),
177            stat_tile("Projected Monthly DBU",     proj,  _AMBER),
178        ])
179
180        html = tiles
181        if skus.ok:
182            html += section_header("Daily DBU by SKU")
183            html += html_table(skus.rows[:30], highlight_col="dbu")
184        if clust.ok:
185            html += section_header("Top Clusters by DBU")
186            html += html_table(clust.rows)
187        if jobs.ok:
188            html += section_header("Top Jobs by DBU")
189            html += html_table(jobs.rows)
190        if users.ok:
191            html += section_header("Top Users by DBU")
192            html += html_table(users.rows)
193
194        for res in [skus, clust, jobs, users]:
195            if not res.ok:
196                html += error_box(res.error)
197
198        _d(_w.HTML(html))
199
200    cost_btn, cost_out = _panel(_cost)
201
202    # ─────────────────────────────────────────────────────────────────────────
203    # TAB: USERS
204    # ─────────────────────────────────────────────────────────────────────────
205    def _users(out):
206        from IPython.display import display as _d
207        import ipywidgets as _w
208
209        d = _days()
210        top_q  = run_query_safe(Q.users_top_by_queries(d), 20)
211        top_t  = run_query_safe(Q.users_top_by_tables_accessed(d), 20)
212        inact  = run_query_safe(Q.users_inactive(d, min(d * 3, 90)), 20)
213        perms  = run_query_safe(Q.users_permission_changes(d), 30)
214
215        html = ""
216        if top_q.ok:
217            html += section_header("Top Users by Activity", f"last {d} days")
218            html += html_table(top_q.rows)
219        if top_t.ok:
220            html += section_header("Top Users by Tables Accessed")
221            html += html_table(top_t.rows)
222        if inact.ok and inact.rows:
223            html += section_header("Inactive Users", f"not seen in {d}d")
224            html += html_table(inact.rows, highlight_col="days_inactive")
225        if perms.ok and perms.rows:
226            html += section_header("Recent Permission Changes")
227            html += html_table(perms.rows, highlight_col="action_name")
228
229        for res in [top_q, top_t, inact, perms]:
230            if not res.ok:
231                html += error_box(res.error)
232
233        _d(_w.HTML(html))
234
235    users_btn, users_out = _panel(_users)
236
237    # ─────────────────────────────────────────────────────────────────────────
238    # TAB: CATALOG
239    # ─────────────────────────────────────────────────────────────────────────
240    def _catalog(out):
241        from IPython.display import display as _d
242        import ipywidgets as _w
243
244        cf = _cat_filter()
245        d  = _days()
246        inv    = run_query_safe(Q.catalog_tables_by_schema(cf), 100)
247        stale  = run_query_safe(Q.catalog_stale_tables(90, cf), 50)
248        hot    = run_query_safe(Q.catalog_most_accessed(d, 20))
249        cols   = run_query_safe(Q.catalog_column_count(cf), 30)
250
251        html = ""
252        if inv.ok:
253            total = sum(r.get("table_count", 0) or 0 for r in inv.rows)
254            html += stat_row([stat_tile("Total Tables", format_number(total), _TEAL)])
255            html += section_header("Tables by Schema")
256            html += html_table(inv.rows)
257        if stale.ok and stale.rows:
258            html += section_header("Stale Tables (90+ days unmodified)")
259            html += html_table(stale.rows, highlight_col="days_stale")
260        if hot.ok:
261            html += section_header(f"Most Accessed Tables (last {d}d)")
262            html += html_table(hot.rows)
263        if cols.ok:
264            html += section_header("Widest Tables (by column count)")
265            html += html_table(cols.rows)
266
267        for res in [inv, stale, hot, cols]:
268            if not res.ok:
269                html += error_box(res.error)
270
271        _d(_w.HTML(html))
272
273    catalog_btn, catalog_out = _panel(_catalog)
274
275    # ─────────────────────────────────────────────────────────────────────────
276    # TAB: JOBS
277    # ─────────────────────────────────────────────────────────────────────────
278    def _jobs(out):
279        from IPython.display import display as _d
280        import ipywidgets as _w
281
282        d = _days()
283        rates  = run_query_safe(Q.jobs_success_rate(d))
284        fails  = run_query_safe(Q.jobs_top_failures(d), 20)
285        slow   = run_query_safe(Q.jobs_longest_runs(d), 20)
286        volume = run_query_safe(Q.jobs_daily_run_volume(d))
287
288        html = ""
289        if rates.ok and rates.rows:
290            total = sum(r.get("run_count", 0) or 0 for r in rates.rows)
291            success = next((r["run_count"] for r in rates.rows if r.get("result_state") == "SUCCEEDED"), 0)
292            rate_pct = round(success / total * 100, 1) if total else 0
293            color = _GREEN if rate_pct >= 95 else (_AMBER if rate_pct >= 80 else _RED)
294            html += stat_row([
295                stat_tile(f"Success Rate ({d}d)", f"{rate_pct}%", color),
296                stat_tile("Total Runs", format_number(total), _TEAL),
297            ])
298            html += section_header("Run State Breakdown")
299            html += html_table(rates.rows, highlight_col="result_state")
300        if fails.ok and fails.rows:
301            html += section_header("Top Failing Jobs")
302            html += html_table(fails.rows, highlight_col="failure_count")
303        if slow.ok:
304            html += section_header("Longest Running Jobs")
305            html += html_table(slow.rows, highlight_col="duration_min")
306        if volume.ok and volume.rows:
307            html += section_header("Daily Run Volume")
308            html += html_table(volume.rows)
309
310        for res in [rates, fails, slow, volume]:
311            if not res.ok:
312                html += error_box(res.error)
313
314        _d(_w.HTML(html))
315
316    jobs_btn, jobs_out = _panel(_jobs)
317
318    # ─────────────────────────────────────────────────────────────────────────
319    # TAB: QUERIES
320    # ─────────────────────────────────────────────────────────────────────────
321    def _queries(out):
322        from IPython.display import display as _d
323        import ipywidgets as _w
324
325        d = min(_days(), 7)  # query.history can be large; cap at 7d default
326        slow  = run_query_safe(Q.queries_slowest(d), 20)
327        exp   = run_query_safe(Q.queries_most_expensive(d), 20)
328        top   = run_query_safe(Q.queries_top_users(d), 20)
329        errs  = run_query_safe(Q.queries_error_summary(d), 20)
330
331        html = ""
332        if slow.ok:
333            html += section_header(f"Slowest Queries (last {d}d)")
334            html += html_table(slow.rows, highlight_col="duration_sec")
335        if exp.ok:
336            html += section_header("Most Expensive Queries (by task time)")
337            html += html_table(exp.rows, highlight_col="task_sec")
338        if top.ok:
339            html += section_header("Top Query Authors")
340            html += html_table(top.rows)
341        if errs.ok and errs.rows:
342            html += section_header("Most Common Query Errors")
343            html += html_table(errs.rows, highlight_col="occurrences")
344
345        for res in [slow, exp, top, errs]:
346            if not res.ok:
347                html += error_box(res.error)
348
349        _d(_w.HTML(html))
350
351    queries_btn, queries_out = _panel(_queries)
352
353    # ─────────────────────────────────────────────────────────────────────────
354    # TAB: GOVERNANCE
355    # ─────────────────────────────────────────────────────────────────────────
356    def _governance(out):
357        from IPython.display import display as _d
358        import ipywidgets as _w
359
360        cf = _cat_filter()
361        d  = _days()
362        noown  = run_query_safe(Q.governance_tables_without_owners(cf), 50)
363        pii    = run_query_safe(Q.governance_pii_columns(cf), 100)
364        anomal = run_query_safe(Q.governance_access_anomalies(d), 30)
365        schema = run_query_safe(Q.governance_schema_changes(d), 30)
366
367        html = ""
368        noown_n = len(noown.rows) if noown.ok else "?"
369        pii_n   = len(pii.rows)   if pii.ok   else "?"
370        anomal_n= len(anomal.rows)if anomal.ok else "?"
371
372        noown_color  = _RED if noown_n and noown_n != "?" and noown_n > 0  else _GREEN
373        anomal_color = _RED if anomal_n and anomal_n != "?" and anomal_n > 0 else _GREEN
374
375        html += stat_row([
376            stat_tile("Tables Without Owners", noown_n,  noown_color),
377            stat_tile("PII Columns Detected",  pii_n,    _AMBER),
378            stat_tile(f"Access Denials ({d}d)", anomal_n, anomal_color),
379        ])
380
381        if noown.ok and noown.rows:
382            html += section_header("Tables Without Owners")
383            html += html_table(noown.rows)
384        elif noown.ok:
385            html += section_header("Tables Without Owners")
386            html += info_box("All tables have owners. Good.")
387
388        if pii.ok and pii.rows:
389            html += section_header("Potential PII Columns (pattern-matched)")
390            html += html_table(pii.rows)
391
392        if anomal.ok and anomal.rows:
393            html += section_header(f"Access Denials / Anomalies (last {d}d)")
394            html += html_table(anomal.rows, highlight_col="action_name")
395
396        if schema.ok and schema.rows:
397            html += section_header(f"Schema Changes (last {d}d)")
398            html += html_table(schema.rows, highlight_col="action_name")
399
400        for res in [noown, pii, anomal, schema]:
401            if not res.ok:
402                html += error_box(res.error)
403
404        _d(_w.HTML(html))
405
406    gov_btn, gov_out = _panel(_governance)
407
408    # ─────────────────────────────────────────────────────────────────────────
409    # TAB: CUSTOM
410    # ─────────────────────────────────────────────────────────────────────────
411    custom_title = w.Text(description="Title:", placeholder="My panel title")
412    custom_sql   = w.Textarea(
413        description="SQL:",
414        placeholder="SELECT * FROM system.access.audit LIMIT 20",
415        layout=w.Layout(width="100%", height="100px"),
416    )
417    custom_btn = dashui.action_button("Run Custom Query", style="warning")
418    custom_out = dashui.output_panel()
419
420    # Pre-wire any custom panels from config
421    _config_custom_widgets = []
422    for cp in cfg.custom_panels:
423        cp_out = dashui.output_panel()
424        cp_btn = dashui.action_button(f"Load: {cp.title}", style="info")
425        _sql_closure = cp.sql
426
427        def _make_handler(s, o):
428            def _h(b):
429                from IPython.display import display as _d
430                import ipywidgets as _w
431                r = run_query_safe(s, cfg.row_limit)
432                with o:
433                    o.clear_output()
434                    _d(_w.HTML(
435                        html_table(r.rows) if r.ok else error_box(r.error)
436                    ))
437            return _h
438
439        cp_btn.on_click(_make_handler(_sql_closure, cp_out))
440        _config_custom_widgets.extend([cp_btn, cp_out])
441
442    def on_custom(b):
443        sql = custom_sql.value.strip()
444        if not sql:
445            return
446        with custom_out:
447            custom_out.clear_output()
448            from IPython.display import display as _d
449            import ipywidgets as _w
450            r = run_query_safe(sql, cfg.row_limit)
451            _d(_w.HTML(
452                html_table(r.rows) if r.ok else error_box(r.error)
453            ))
454
455    custom_btn.on_click(on_custom)
456
457    # ── Assemble tabs ─────────────────────────────────────────────────────────
458    panel_map = {
459        "health":     ("Health",     w.VBox([health_btn, health_out])),
460        "cost":       ("Cost",       w.VBox([cost_btn, cost_out])),
461        "users":      ("Users",      w.VBox([users_btn, users_out])),
462        "catalog":    ("Catalog",    w.VBox([catalog_btn, catalog_out])),
463        "jobs":       ("Jobs",       w.VBox([jobs_btn, jobs_out])),
464        "queries":    ("Queries",    w.VBox([queries_btn, queries_out])),
465        "governance": ("Governance", w.VBox([gov_btn, gov_out])),
466    }
467
468    tab = w.Tab()
469    children, titles = [], []
470    for panel_id in cfg.panels:
471        if panel_id in panel_map:
472            title, content = panel_map[panel_id]
473            children.append(content)
474            titles.append(title)
475
476    # Custom tab always last
477    custom_content = w.VBox(
478        _config_custom_widgets + [custom_title, custom_sql, custom_btn, custom_out]
479    )
480    children.append(custom_content)
481    titles.append("Custom")
482
483    tab.children = children
484    for i, t in enumerate(titles):
485        tab.set_title(i, t)
486
487    env_accordion = w.Accordion(children=[dashui.env_setup_panel(_LIBRARY).widget])
488    env_accordion.set_title(0, "Environment setup")
489    env_accordion.selected_index = None
490
491    ui = dashui.card([
492        dashui.header(
493            f"Databricks Control Center{' — ' + cfg.workspace_name if cfg.workspace_name else ''}",
494            library="dashcontrol",
495        ),
496        env_accordion,
497        dashui.html(
498            "<div style='font-size:11px;color:#6b7280;margin-bottom:4px'>"
499            "Click a tab then <b>Load</b> to query system tables. "
500            "Results are lazy-loaded and cached until you Refresh.</div>"
501        ),
502        w.HBox([days_slider, catalog_text]),
503        workspace_label,
504        tab,
505    ])
506    display(ui)