dashcontrol

DashControl — Databricks Control Center.

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