dash-control 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_control-0.1.1/.github/workflows/release.yml +265 -0
- dash_control-0.1.1/PKG-INFO +103 -0
- dash_control-0.1.1/README.md +73 -0
- dash_control-0.1.1/RELEASE_NOTES.md +3 -0
- dash_control-0.1.1/dashcontrol/__init__.py +6 -0
- dash_control-0.1.1/dashcontrol/config.py +62 -0
- dash_control-0.1.1/dashcontrol/formatters.py +161 -0
- dash_control-0.1.1/dashcontrol/runner.py +78 -0
- dash_control-0.1.1/dashcontrol/sql.py +489 -0
- dash_control-0.1.1/dashcontrol/ui.py +465 -0
- dash_control-0.1.1/docs/api/dashcontrol.html +838 -0
- dash_control-0.1.1/docs/api/index.html +7 -0
- dash_control-0.1.1/docs/api/search.js +46 -0
- dash_control-0.1.1/pyproject.toml +45 -0
- dash_control-0.1.1/tests/test_config.py +90 -0
- dash_control-0.1.1/tests/test_dashcontrol.py +24 -0
- dash_control-0.1.1/tests/test_formatters.py +161 -0
- dash_control-0.1.1/tests/test_runner.py +73 -0
- dash_control-0.1.1/tests/test_sql.py +256 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
name: Weekly Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 9 * * 1" # Every Monday 09:00 UTC
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
release_note:
|
|
9
|
+
description: "Optional release note (shown in GitHub release body)"
|
|
10
|
+
required: false
|
|
11
|
+
default: ""
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
# ── Gate: tests must pass ────────────────────────────────────────────────
|
|
15
|
+
test:
|
|
16
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: true
|
|
20
|
+
matrix:
|
|
21
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: pip install -e ".[dev]" pytest pytest-cov
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: pytest tests/ -v --cov=dashcontrol --cov-report=xml --cov-report=term-missing
|
|
34
|
+
|
|
35
|
+
- name: Upload coverage
|
|
36
|
+
uses: codecov/codecov-action@v4
|
|
37
|
+
with:
|
|
38
|
+
files: coverage.xml
|
|
39
|
+
fail_ci_if_error: false
|
|
40
|
+
|
|
41
|
+
# ── Generate docs ────────────────────────────────────────────────────────
|
|
42
|
+
docs:
|
|
43
|
+
name: Generate API docs
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
needs: test
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
|
|
49
|
+
- uses: actions/setup-python@v5
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.11"
|
|
52
|
+
|
|
53
|
+
- name: Install
|
|
54
|
+
run: pip install -e ".[dev]" pdoc
|
|
55
|
+
|
|
56
|
+
- name: Generate docs
|
|
57
|
+
run: |
|
|
58
|
+
pdoc dashcontrol --output-dir docs/api --docformat google
|
|
59
|
+
echo "Docs generated at $(date -u)" > docs/api/.generated
|
|
60
|
+
|
|
61
|
+
- name: Upload docs artifact
|
|
62
|
+
uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: api-docs
|
|
65
|
+
path: docs/api/
|
|
66
|
+
|
|
67
|
+
# ── Release: tag, GitHub release, commit docs ────────────────────────────
|
|
68
|
+
release:
|
|
69
|
+
name: Bump version & release
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
needs: [test, docs]
|
|
72
|
+
permissions:
|
|
73
|
+
contents: write
|
|
74
|
+
pull-requests: write
|
|
75
|
+
outputs:
|
|
76
|
+
version: ${{ steps.bump.outputs.version }}
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
with:
|
|
80
|
+
fetch-depth: 0
|
|
81
|
+
|
|
82
|
+
- uses: actions/setup-python@v5
|
|
83
|
+
with:
|
|
84
|
+
python-version: "3.11"
|
|
85
|
+
|
|
86
|
+
- name: Install build tools
|
|
87
|
+
run: pip install hatch pdoc
|
|
88
|
+
|
|
89
|
+
- name: Bump patch version
|
|
90
|
+
id: bump
|
|
91
|
+
run: |
|
|
92
|
+
current=$(hatch version)
|
|
93
|
+
hatch version patch
|
|
94
|
+
new=$(hatch version)
|
|
95
|
+
echo "version=$new" >> $GITHUB_OUTPUT
|
|
96
|
+
echo "prev_version=$current" >> $GITHUB_OUTPUT
|
|
97
|
+
echo "Bumped $current → $new"
|
|
98
|
+
|
|
99
|
+
- name: Regenerate docs into repo
|
|
100
|
+
run: |
|
|
101
|
+
pip install -e ".[dev]"
|
|
102
|
+
pdoc dashcontrol --output-dir docs/api --docformat google
|
|
103
|
+
|
|
104
|
+
- name: Build wheel + sdist
|
|
105
|
+
run: hatch build
|
|
106
|
+
|
|
107
|
+
- name: Write release notes
|
|
108
|
+
env:
|
|
109
|
+
VERSION: ${{ steps.bump.outputs.version }}
|
|
110
|
+
PREV_VERSION: ${{ steps.bump.outputs.prev_version }}
|
|
111
|
+
RELEASE_NOTE: ${{ github.event.inputs.release_note }}
|
|
112
|
+
run: |
|
|
113
|
+
cat > RELEASE_NOTES.md << EOF
|
|
114
|
+
## DashControl v${VERSION}
|
|
115
|
+
|
|
116
|
+
**Released:** $(date -u '+%Y-%m-%d')
|
|
117
|
+
**Previous:** v${PREV_VERSION}
|
|
118
|
+
|
|
119
|
+
$( [ -n "${RELEASE_NOTE}" ] && echo "### Notes" && echo "${RELEASE_NOTE}" || true )
|
|
120
|
+
|
|
121
|
+
### What's included
|
|
122
|
+
- All tests passing across Python 3.9, 3.10, 3.11, 3.12
|
|
123
|
+
- API documentation regenerated (see \`docs/api/\`)
|
|
124
|
+
- Published to PyPI and Databricks Marketplace
|
|
125
|
+
|
|
126
|
+
### Install
|
|
127
|
+
\`\`\`bash
|
|
128
|
+
pip install dash-control==${VERSION}
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
### Quick Start (Databricks notebook)
|
|
132
|
+
\`\`\`python
|
|
133
|
+
%pip install dash-control==${VERSION}
|
|
134
|
+
import dashcontrol
|
|
135
|
+
dashcontrol.launch()
|
|
136
|
+
\`\`\`
|
|
137
|
+
EOF
|
|
138
|
+
|
|
139
|
+
- name: Commit version bump + docs to a release branch
|
|
140
|
+
env:
|
|
141
|
+
VERSION: ${{ steps.bump.outputs.version }}
|
|
142
|
+
run: |
|
|
143
|
+
git config user.name "github-actions[bot]"
|
|
144
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
145
|
+
# Clean up any orphaned tag/branch from a previous partial run
|
|
146
|
+
git push origin --delete "refs/tags/v${VERSION}" 2>/dev/null || true
|
|
147
|
+
git push origin --delete "release/v${VERSION}" 2>/dev/null || true
|
|
148
|
+
git tag -d "v${VERSION}" 2>/dev/null || true
|
|
149
|
+
git checkout -b "release/v${VERSION}"
|
|
150
|
+
git add .
|
|
151
|
+
git commit -m "release: v${VERSION} — tests passed, docs updated"
|
|
152
|
+
git tag "v${VERSION}"
|
|
153
|
+
git push origin "release/v${VERSION}"
|
|
154
|
+
git push origin "v${VERSION}"
|
|
155
|
+
|
|
156
|
+
- name: Create GitHub Release
|
|
157
|
+
uses: softprops/action-gh-release@v2
|
|
158
|
+
with:
|
|
159
|
+
tag_name: "v${{ steps.bump.outputs.version }}"
|
|
160
|
+
body_path: RELEASE_NOTES.md
|
|
161
|
+
files: dist/*
|
|
162
|
+
|
|
163
|
+
- name: Upload dist artifact for PyPI job
|
|
164
|
+
uses: actions/upload-artifact@v4
|
|
165
|
+
with:
|
|
166
|
+
name: dist
|
|
167
|
+
path: dist/
|
|
168
|
+
|
|
169
|
+
- name: Open and auto-merge release PR
|
|
170
|
+
continue-on-error: true
|
|
171
|
+
env:
|
|
172
|
+
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
|
|
173
|
+
VERSION: ${{ steps.bump.outputs.version }}
|
|
174
|
+
run: |
|
|
175
|
+
gh pr create --base main --head "release/v${VERSION}" \
|
|
176
|
+
--title "release: v${VERSION}" \
|
|
177
|
+
--body "Automated release PR — tests passed, docs regenerated, version bumped to v${VERSION}." \
|
|
178
|
+
2>/dev/null || true
|
|
179
|
+
gh pr merge "release/v${VERSION}" --merge --admin --delete-branch
|
|
180
|
+
|
|
181
|
+
# ── Publish to PyPI (Trusted Publisher / OIDC — no token needed) ─────────
|
|
182
|
+
publish-pypi:
|
|
183
|
+
name: Publish to PyPI
|
|
184
|
+
runs-on: ubuntu-latest
|
|
185
|
+
needs: release
|
|
186
|
+
permissions:
|
|
187
|
+
id-token: write # required for OIDC trusted publisher
|
|
188
|
+
environment:
|
|
189
|
+
name: pypi
|
|
190
|
+
url: https://pypi.org/project/dash-control
|
|
191
|
+
steps:
|
|
192
|
+
- name: Download dist
|
|
193
|
+
uses: actions/download-artifact@v4
|
|
194
|
+
with:
|
|
195
|
+
name: dist
|
|
196
|
+
path: dist/
|
|
197
|
+
|
|
198
|
+
- name: Publish to PyPI
|
|
199
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
200
|
+
|
|
201
|
+
# ── Package for Databricks Marketplace ───────────────────────────────────
|
|
202
|
+
publish-databricks:
|
|
203
|
+
name: Package for Databricks Marketplace
|
|
204
|
+
runs-on: ubuntu-latest
|
|
205
|
+
needs: release
|
|
206
|
+
steps:
|
|
207
|
+
- uses: actions/checkout@v4
|
|
208
|
+
with:
|
|
209
|
+
ref: "v${{ needs.release.outputs.version }}"
|
|
210
|
+
|
|
211
|
+
- name: Download dist
|
|
212
|
+
uses: actions/download-artifact@v4
|
|
213
|
+
with:
|
|
214
|
+
name: dist
|
|
215
|
+
path: dist/
|
|
216
|
+
|
|
217
|
+
- name: Build Marketplace bundle
|
|
218
|
+
env:
|
|
219
|
+
VERSION: ${{ needs.release.outputs.version }}
|
|
220
|
+
run: |
|
|
221
|
+
mkdir -p marketplace-bundle/files
|
|
222
|
+
|
|
223
|
+
cp dist/*.whl marketplace-bundle/files/
|
|
224
|
+
|
|
225
|
+
cat > marketplace-bundle/files/DashControl_Quickstart.py << NBEOF
|
|
226
|
+
# Databricks notebook source
|
|
227
|
+
# MAGIC %md
|
|
228
|
+
# MAGIC # DashControl v${VERSION} — Databricks Control Center
|
|
229
|
+
# MAGIC Install and launch the interactive UI.
|
|
230
|
+
|
|
231
|
+
# COMMAND ----------
|
|
232
|
+
# MAGIC %pip install dash-control==${VERSION}
|
|
233
|
+
|
|
234
|
+
# COMMAND ----------
|
|
235
|
+
dbutils.library.restartPython()
|
|
236
|
+
|
|
237
|
+
# COMMAND ----------
|
|
238
|
+
import dashcontrol
|
|
239
|
+
dashcontrol.launch()
|
|
240
|
+
NBEOF
|
|
241
|
+
|
|
242
|
+
cat > marketplace-bundle/listing.json << LEOF
|
|
243
|
+
{
|
|
244
|
+
"listing_name": "DashControl — Databricks Control Center",
|
|
245
|
+
"version": "${VERSION}",
|
|
246
|
+
"short_description": "Live system-table dashboard for platform, governance, cost, users, jobs, and queries",
|
|
247
|
+
"long_description": "DashControl provides an ipywidgets UI inside Databricks notebooks to monitor table freshness, row-count volume, and schema changes, persisting results to a Delta history table. No external service to deploy.",
|
|
248
|
+
"categories": ["Data Engineering", "Developer Tools"],
|
|
249
|
+
"tags": ["data-observability", "databricks", "unity-catalog", "pyspark"],
|
|
250
|
+
"provider": "dash-libs",
|
|
251
|
+
"documentation_url": "https://github.com/dash-libs/dash-control",
|
|
252
|
+
"source_url": "https://github.com/dash-libs/dash-control",
|
|
253
|
+
"pypi_package": "dash-control==${VERSION}"
|
|
254
|
+
}
|
|
255
|
+
LEOF
|
|
256
|
+
|
|
257
|
+
cd marketplace-bundle && zip -r ../dashcontrol-marketplace-${VERSION}.zip .
|
|
258
|
+
echo "Bundle created: dashcontrol-marketplace-${VERSION}.zip"
|
|
259
|
+
|
|
260
|
+
- name: Upload Marketplace bundle artifact
|
|
261
|
+
uses: actions/upload-artifact@v4
|
|
262
|
+
with:
|
|
263
|
+
name: marketplace-bundle
|
|
264
|
+
path: dashcontrol-marketplace-*.zip
|
|
265
|
+
retention-days: 90
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dash-control
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Databricks Control Center — live system-table dashboard for platform, governance, cost, users, jobs, and queries
|
|
5
|
+
Project-URL: Homepage, https://github.com/dash-libs/dash-control
|
|
6
|
+
Author-email: Darshan Shah <darshan.innovation@gmail.com>
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Keywords: control-center,cost,dashboard,databricks,governance,observability,system-tables,unity-catalog
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: dash-uis==0.2.3
|
|
22
|
+
Requires-Dist: ipywidgets>=8.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
25
|
+
Requires-Dist: pdoc; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# dash-control
|
|
32
|
+
|
|
33
|
+
**Databricks Control Center** — a live, 8-panel system-table dashboard for platform, governance, cost, and operations teams. One `%pip install` + one `launch()` call gives you an always-on view of your entire Databricks workspace.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
%pip install dash-control
|
|
37
|
+
from dashcontrol import launch, ControlCenterConfig
|
|
38
|
+
|
|
39
|
+
config = ControlCenterConfig(
|
|
40
|
+
date_range_days=30,
|
|
41
|
+
catalogs=["main", "analytics"],
|
|
42
|
+
)
|
|
43
|
+
launch(config)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Panels
|
|
47
|
+
|
|
48
|
+
| Panel | What it shows |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Health** | DBU today, active users, failed jobs (last 24 h), total table count, 7-day DBU sparkline |
|
|
51
|
+
| **Cost** | Daily spend by SKU, top clusters, top jobs, top users by DBU, burn-rate projection |
|
|
52
|
+
| **Users** | Top users by query count & tables accessed, inactive users, permission changes |
|
|
53
|
+
| **Catalog** | Table inventory, tables by schema, stale tables, column count distribution, most-accessed tables |
|
|
54
|
+
| **Jobs** | Success rate, top failures, longest runs, daily run volume trend |
|
|
55
|
+
| **Queries** | Slowest queries, most expensive queries, top users, error summary |
|
|
56
|
+
| **Governance** | Tables without owners, PII columns, access anomalies, schema changes |
|
|
57
|
+
| **Custom** | Run any SQL against system tables; define custom panels in `ControlCenterConfig` |
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from dashcontrol import ControlCenterConfig, CustomPanel
|
|
63
|
+
|
|
64
|
+
config = ControlCenterConfig(
|
|
65
|
+
date_range_days=14, # default look-back window (also adjustable in the UI)
|
|
66
|
+
catalogs=["main"], # limit catalog scope; empty = all catalogs
|
|
67
|
+
panels=["health", "cost"], # show only these panels
|
|
68
|
+
row_limit=200, # max rows per table result
|
|
69
|
+
workspace_name="prod", # displayed in the header
|
|
70
|
+
custom_panels=[
|
|
71
|
+
CustomPanel(
|
|
72
|
+
name="My Query",
|
|
73
|
+
sql="SELECT user_name, COUNT(*) AS cnt FROM system.access.audit GROUP BY 1",
|
|
74
|
+
description="Custom audit rollup",
|
|
75
|
+
)
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
- Databricks Runtime 13.3 LTS or later (system tables must be enabled)
|
|
83
|
+
- Unity Catalog workspace
|
|
84
|
+
- `ipywidgets` (included as a dependency)
|
|
85
|
+
|
|
86
|
+
## Architecture
|
|
87
|
+
|
|
88
|
+
Every panel is **lazy-loaded** — clicking Load runs the query; nothing executes on `launch()`. Queries target Databricks system tables (`system.billing.*`, `system.access.audit`, `system.jobs.*`, `system.query.history`, `system.information_schema.*`). All queries degrade gracefully: if a system table isn't available on your workspace tier, the panel shows an info banner instead of crashing.
|
|
89
|
+
|
|
90
|
+
## Part of the DashLibs suite
|
|
91
|
+
|
|
92
|
+
| Package | Purpose |
|
|
93
|
+
|---|---|
|
|
94
|
+
| [dash-dq](https://pypi.org/project/dash-dq/) | 60+ data quality checks |
|
|
95
|
+
| [dash-synthetic](https://pypi.org/project/dash-synthetic/) | Synthetic data generation |
|
|
96
|
+
| [dash-observe](https://pypi.org/project/dash-observe/) | Freshness, volume & schema monitoring |
|
|
97
|
+
| [dash-gov](https://pypi.org/project/dash-gov/) | Table/column lineage + role classification |
|
|
98
|
+
| [dash-ontology](https://pypi.org/project/dash-ontology/) | Auto-inferred business ontology from lineage |
|
|
99
|
+
| **dash-control** | Control Center — this package |
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
Apache 2.0
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# dash-control
|
|
2
|
+
|
|
3
|
+
**Databricks Control Center** — a live, 8-panel system-table dashboard for platform, governance, cost, and operations teams. One `%pip install` + one `launch()` call gives you an always-on view of your entire Databricks workspace.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
%pip install dash-control
|
|
7
|
+
from dashcontrol import launch, ControlCenterConfig
|
|
8
|
+
|
|
9
|
+
config = ControlCenterConfig(
|
|
10
|
+
date_range_days=30,
|
|
11
|
+
catalogs=["main", "analytics"],
|
|
12
|
+
)
|
|
13
|
+
launch(config)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Panels
|
|
17
|
+
|
|
18
|
+
| Panel | What it shows |
|
|
19
|
+
|---|---|
|
|
20
|
+
| **Health** | DBU today, active users, failed jobs (last 24 h), total table count, 7-day DBU sparkline |
|
|
21
|
+
| **Cost** | Daily spend by SKU, top clusters, top jobs, top users by DBU, burn-rate projection |
|
|
22
|
+
| **Users** | Top users by query count & tables accessed, inactive users, permission changes |
|
|
23
|
+
| **Catalog** | Table inventory, tables by schema, stale tables, column count distribution, most-accessed tables |
|
|
24
|
+
| **Jobs** | Success rate, top failures, longest runs, daily run volume trend |
|
|
25
|
+
| **Queries** | Slowest queries, most expensive queries, top users, error summary |
|
|
26
|
+
| **Governance** | Tables without owners, PII columns, access anomalies, schema changes |
|
|
27
|
+
| **Custom** | Run any SQL against system tables; define custom panels in `ControlCenterConfig` |
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from dashcontrol import ControlCenterConfig, CustomPanel
|
|
33
|
+
|
|
34
|
+
config = ControlCenterConfig(
|
|
35
|
+
date_range_days=14, # default look-back window (also adjustable in the UI)
|
|
36
|
+
catalogs=["main"], # limit catalog scope; empty = all catalogs
|
|
37
|
+
panels=["health", "cost"], # show only these panels
|
|
38
|
+
row_limit=200, # max rows per table result
|
|
39
|
+
workspace_name="prod", # displayed in the header
|
|
40
|
+
custom_panels=[
|
|
41
|
+
CustomPanel(
|
|
42
|
+
name="My Query",
|
|
43
|
+
sql="SELECT user_name, COUNT(*) AS cnt FROM system.access.audit GROUP BY 1",
|
|
44
|
+
description="Custom audit rollup",
|
|
45
|
+
)
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Databricks Runtime 13.3 LTS or later (system tables must be enabled)
|
|
53
|
+
- Unity Catalog workspace
|
|
54
|
+
- `ipywidgets` (included as a dependency)
|
|
55
|
+
|
|
56
|
+
## Architecture
|
|
57
|
+
|
|
58
|
+
Every panel is **lazy-loaded** — clicking Load runs the query; nothing executes on `launch()`. Queries target Databricks system tables (`system.billing.*`, `system.access.audit`, `system.jobs.*`, `system.query.history`, `system.information_schema.*`). All queries degrade gracefully: if a system table isn't available on your workspace tier, the panel shows an info banner instead of crashing.
|
|
59
|
+
|
|
60
|
+
## Part of the DashLibs suite
|
|
61
|
+
|
|
62
|
+
| Package | Purpose |
|
|
63
|
+
|---|---|
|
|
64
|
+
| [dash-dq](https://pypi.org/project/dash-dq/) | 60+ data quality checks |
|
|
65
|
+
| [dash-synthetic](https://pypi.org/project/dash-synthetic/) | Synthetic data generation |
|
|
66
|
+
| [dash-observe](https://pypi.org/project/dash-observe/) | Freshness, volume & schema monitoring |
|
|
67
|
+
| [dash-gov](https://pypi.org/project/dash-gov/) | Table/column lineage + role classification |
|
|
68
|
+
| [dash-ontology](https://pypi.org/project/dash-ontology/) | Auto-inferred business ontology from lineage |
|
|
69
|
+
| **dash-control** | Control Center — this package |
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Apache 2.0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ControlCenterConfig — user-facing configuration for the Control Center.
|
|
3
|
+
|
|
4
|
+
All fields have sensible defaults so teams can call launch() with zero config,
|
|
5
|
+
or pre-wire a domain-specific dashboard by passing a config object.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
ALL_PANELS = ["health", "cost", "users", "catalog", "jobs", "queries", "governance"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CustomPanel:
|
|
15
|
+
title: str
|
|
16
|
+
sql: str
|
|
17
|
+
description: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ControlCenterConfig:
|
|
22
|
+
"""
|
|
23
|
+
Configuration for the Databricks Control Center.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
date_range_days
|
|
28
|
+
Default lookback window for all time-based queries (default: 30).
|
|
29
|
+
catalogs
|
|
30
|
+
List of catalogs to scope table/lineage queries to.
|
|
31
|
+
Empty list means all visible catalogs.
|
|
32
|
+
panels
|
|
33
|
+
Which built-in panels to show. Defaults to all.
|
|
34
|
+
custom_panels
|
|
35
|
+
User-defined SQL panels appended after the built-in tabs.
|
|
36
|
+
row_limit
|
|
37
|
+
Max rows returned per panel query (default: 500).
|
|
38
|
+
workspace_name
|
|
39
|
+
Optional display name shown in the dashboard header.
|
|
40
|
+
"""
|
|
41
|
+
date_range_days: int = 30
|
|
42
|
+
catalogs: list[str] = field(default_factory=list)
|
|
43
|
+
panels: list[str] = field(default_factory=lambda: list(ALL_PANELS))
|
|
44
|
+
custom_panels: list[CustomPanel] = field(default_factory=list)
|
|
45
|
+
row_limit: int = 500
|
|
46
|
+
workspace_name: str = ""
|
|
47
|
+
|
|
48
|
+
def __post_init__(self):
|
|
49
|
+
unknown = set(self.panels) - set(ALL_PANELS)
|
|
50
|
+
if unknown:
|
|
51
|
+
raise ValueError(f"Unknown panels: {unknown}. Valid: {ALL_PANELS}")
|
|
52
|
+
if self.date_range_days < 1 or self.date_range_days > 365:
|
|
53
|
+
raise ValueError("date_range_days must be between 1 and 365")
|
|
54
|
+
if self.row_limit < 1 or self.row_limit > 10_000:
|
|
55
|
+
raise ValueError("row_limit must be between 1 and 10,000")
|
|
56
|
+
|
|
57
|
+
def catalog_filter(self, col: str = "table_catalog") -> str:
|
|
58
|
+
"""SQL WHERE fragment to filter by configured catalogs."""
|
|
59
|
+
if not self.catalogs:
|
|
60
|
+
return ""
|
|
61
|
+
quoted = ", ".join(f"'{c}'" for c in self.catalogs)
|
|
62
|
+
return f"AND {col} IN ({quoted})"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pure-Python formatting utilities — no Spark dependency.
|
|
3
|
+
|
|
4
|
+
Converts query result rows (list[dict]) into styled HTML tables,
|
|
5
|
+
stat tiles, trend sparklines, and CSV exports for the Control Center UI.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# Colour palette
|
|
11
|
+
_TEAL = "#2A9D90"
|
|
12
|
+
_AMBER = "#F4A261"
|
|
13
|
+
_RED = "#E63946"
|
|
14
|
+
_GREEN = "#2DC653"
|
|
15
|
+
_GREY = "#6B7280"
|
|
16
|
+
_LIGHT = "#F9FAFB"
|
|
17
|
+
_BORDER = "#E5E7EB"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def stat_tile(label: str, value, color: str = _TEAL, unit: str = "") -> str:
|
|
21
|
+
"""Render a single KPI tile (label + big number)."""
|
|
22
|
+
return (
|
|
23
|
+
f"<div style='display:inline-block;padding:14px 20px;margin:6px;"
|
|
24
|
+
f"border-radius:8px;background:{_LIGHT};border:1px solid {_BORDER};"
|
|
25
|
+
f"min-width:140px;text-align:center'>"
|
|
26
|
+
f"<div style='font-size:26px;font-weight:700;color:{color}'>"
|
|
27
|
+
f"{value}{unit}</div>"
|
|
28
|
+
f"<div style='font-size:11px;color:{_GREY};margin-top:4px'>{label}</div>"
|
|
29
|
+
f"</div>"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stat_row(tiles: list[str]) -> str:
|
|
34
|
+
"""Wrap stat tiles in a flex row."""
|
|
35
|
+
inner = "".join(tiles)
|
|
36
|
+
return f"<div style='display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px'>{inner}</div>"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def html_table(
|
|
40
|
+
rows: list[dict],
|
|
41
|
+
highlight_col: Optional[str] = None,
|
|
42
|
+
max_rows: int = 200,
|
|
43
|
+
col_widths: Optional[dict] = None,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Convert a list of row-dicts to a styled HTML table.
|
|
47
|
+
|
|
48
|
+
highlight_col — column whose values determine row background colour
|
|
49
|
+
(high numeric value = amber, error strings = red)
|
|
50
|
+
"""
|
|
51
|
+
if not rows:
|
|
52
|
+
return "<div style='color:#9ca3af;font-size:12px;padding:8px'>No data</div>"
|
|
53
|
+
|
|
54
|
+
cols = list(rows[0].keys())
|
|
55
|
+
col_widths = col_widths or {}
|
|
56
|
+
|
|
57
|
+
def _header(col: str) -> str:
|
|
58
|
+
w = f"min-width:{col_widths[col]};" if col in col_widths else ""
|
|
59
|
+
label = col.replace("_", " ").title()
|
|
60
|
+
return (
|
|
61
|
+
f"<th style='padding:6px 10px;background:#F3F4F6;text-align:left;"
|
|
62
|
+
f"font-size:11px;color:{_GREY};font-weight:600;white-space:nowrap;{w}'>"
|
|
63
|
+
f"{label}</th>"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _cell(val, col: str) -> str:
|
|
67
|
+
text = "" if val is None else str(val)
|
|
68
|
+
# Truncate long strings
|
|
69
|
+
display = text[:80] + "…" if len(text) > 80 else text
|
|
70
|
+
return (
|
|
71
|
+
f"<td style='padding:5px 10px;font-size:12px;"
|
|
72
|
+
f"font-family:monospace;border-top:1px solid {_BORDER}'>"
|
|
73
|
+
f"{display}</td>"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _row_bg(row: dict) -> str:
|
|
77
|
+
if highlight_col and highlight_col in row:
|
|
78
|
+
v = row[highlight_col]
|
|
79
|
+
if isinstance(v, str) and any(k in v.upper() for k in ("FAIL", "ERROR", "DENIED")):
|
|
80
|
+
return "background:#FEF2F2;"
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
headers = "".join(_header(c) for c in cols)
|
|
84
|
+
body_rows = ""
|
|
85
|
+
for r in rows[:max_rows]:
|
|
86
|
+
bg = _row_bg(r)
|
|
87
|
+
cells = "".join(_cell(r.get(c), c) for c in cols)
|
|
88
|
+
body_rows += f"<tr style='{bg}'>{cells}</tr>"
|
|
89
|
+
|
|
90
|
+
footer = ""
|
|
91
|
+
if len(rows) > max_rows:
|
|
92
|
+
footer = (
|
|
93
|
+
f"<tr><td colspan='{len(cols)}' style='padding:6px 10px;"
|
|
94
|
+
f"font-size:11px;color:{_GREY};text-align:center'>"
|
|
95
|
+
f"Showing {max_rows} of {len(rows)} rows</td></tr>"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
f"<div style='overflow-x:auto'>"
|
|
100
|
+
f"<table style='border-collapse:collapse;width:100%;font-size:12px'>"
|
|
101
|
+
f"<thead><tr>{headers}</tr></thead>"
|
|
102
|
+
f"<tbody>{body_rows}{footer}</tbody>"
|
|
103
|
+
f"</table></div>"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def error_box(message: str) -> str:
|
|
108
|
+
"""Red alert box for query errors."""
|
|
109
|
+
return (
|
|
110
|
+
f"<div style='padding:10px 14px;background:#FEF2F2;border:1px solid #FCA5A5;"
|
|
111
|
+
f"border-radius:6px;color:{_RED};font-size:12px;font-family:monospace'>"
|
|
112
|
+
f"⚠ {message}</div>"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def info_box(message: str) -> str:
|
|
117
|
+
"""Blue info box."""
|
|
118
|
+
return (
|
|
119
|
+
f"<div style='padding:10px 14px;background:#EFF6FF;border:1px solid #BFDBFE;"
|
|
120
|
+
f"border-radius:6px;color:#1D4ED8;font-size:12px'>"
|
|
121
|
+
f"ℹ {message}</div>"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def section_header(title: str, subtitle: str = "") -> str:
|
|
126
|
+
sub = f"<span style='font-size:11px;color:{_GREY};margin-left:8px'>{subtitle}</span>" if subtitle else ""
|
|
127
|
+
return (
|
|
128
|
+
f"<div style='font-weight:600;font-size:13px;color:#374151;"
|
|
129
|
+
f"margin:12px 0 6px;padding-bottom:4px;border-bottom:1px solid {_BORDER}'>"
|
|
130
|
+
f"{title}{sub}</div>"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def sparkline_html(values: list[float], label: str = "") -> str:
|
|
135
|
+
"""
|
|
136
|
+
Minimal ASCII sparkline for trend data (no JS/SVG required).
|
|
137
|
+
Uses Unicode block characters: ▁▂▃▄▅▆▇█
|
|
138
|
+
"""
|
|
139
|
+
blocks = "▁▂▃▄▅▆▇█"
|
|
140
|
+
if not values or all(v == 0 for v in values):
|
|
141
|
+
return f"<span style='font-family:monospace;color:{_GREY}'>{'▁' * 10}</span>"
|
|
142
|
+
mn, mx = min(values), max(values)
|
|
143
|
+
rng = mx - mn or 1
|
|
144
|
+
chars = "".join(blocks[min(7, int((v - mn) / rng * 7))] for v in values)
|
|
145
|
+
return (
|
|
146
|
+
f"<span style='font-family:monospace;font-size:14px;color:{_TEAL}'>{chars}</span>"
|
|
147
|
+
+ (f"<span style='font-size:10px;color:{_GREY};margin-left:4px'>{label}</span>" if label else "")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def format_number(n) -> str:
|
|
152
|
+
"""Format large numbers with K/M suffixes."""
|
|
153
|
+
try:
|
|
154
|
+
n = float(n)
|
|
155
|
+
except (TypeError, ValueError):
|
|
156
|
+
return str(n)
|
|
157
|
+
if abs(n) >= 1_000_000:
|
|
158
|
+
return f"{n / 1_000_000:.1f}M"
|
|
159
|
+
if abs(n) >= 1_000:
|
|
160
|
+
return f"{n / 1_000:.1f}K"
|
|
161
|
+
return f"{n:,.0f}" if n == int(n) else f"{n:,.2f}"
|