Skip to content

Commit a6cd83c

Browse files
committed
Add profile config viewer, CLI install instructions, and HTML auth detection
- Profile selector now has an edit button that opens ~/.databrickscfg in a syntax-highlighted browser tab with an "Open in editor" button - Deploy modal includes Databricks CLI install instructions for macOS, Linux/WSL, and Windows - API calls that receive HTML sign-in pages instead of JSON now return a clear 401 auth error instead of dumping raw HTML - Improved custom URL+token connection error messages Co-authored-by: Isaac
1 parent c9f9897 commit a6cd83c

3 files changed

Lines changed: 172 additions & 7 deletions

File tree

app.py

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,91 @@
9090
_DEFAULT_CONN = {"mode": "profile", "profile": DATABRICKS_PROFILE}
9191

9292

93+
# ── Flask route: serve ~/.databrickscfg in the browser ────────────────────────
94+
@server.route("/open-databrickscfg")
95+
def _serve_databrickscfg():
96+
"""Serve ``~/.databrickscfg`` as a syntax-highlighted HTML page."""
97+
import html as _html
98+
cfg_path = os.path.expanduser("~/.databrickscfg")
99+
try:
100+
with open(cfg_path, encoding="utf-8") as f:
101+
content = f.read()
102+
except FileNotFoundError:
103+
content = "# File not found: ~/.databrickscfg"
104+
escaped = _html.escape(content)
105+
highlighted = _highlight_ini(escaped)
106+
page = (
107+
"<!DOCTYPE html><html><head><title>~/.databrickscfg</title>"
108+
"<style>"
109+
"body{background:#0d1117;color:#c9d1d9;font-family:'JetBrains Mono',monospace;"
110+
"padding:24px;margin:0;font-size:13px;line-height:1.6}"
111+
"pre{white-space:pre-wrap;word-break:break-all;margin:0}"
112+
".comment{color:#6a737d}.section{color:#79c0ff;font-weight:600}"
113+
".key{color:#d2a8ff}.val{color:#a5d6ff}"
114+
".toolbar{display:flex;align-items:center;justify-content:space-between;"
115+
"margin-bottom:16px}"
116+
"h1{font-size:16px;color:#58a6ff;margin:0;font-weight:500}"
117+
".edit-btn{background:rgba(88,166,255,0.12);color:#58a6ff;border:1px solid "
118+
"rgba(88,166,255,0.3);border-radius:6px;padding:6px 14px;font-size:12px;"
119+
"font-family:inherit;cursor:pointer;text-decoration:none;display:inline-flex;"
120+
"align-items:center;gap:6px;transition:all 0.15s}"
121+
".edit-btn:hover{background:rgba(88,166,255,0.22);border-color:#58a6ff;"
122+
"color:#79c0ff}"
123+
".edit-btn svg{width:14px;height:14px;fill:currentColor}"
124+
"</style></head><body>"
125+
'<div class="toolbar">'
126+
"<h1>~/.databrickscfg</h1>"
127+
'<a class="edit-btn" href="/open-databrickscfg-editor" target="_self">'
128+
'<svg viewBox="0 0 16 16"><path d="M12.854.146a.5.5 0 00-.707 0L10.5 1.793 '
129+
"14.207 5.5l1.647-1.646a.5.5 0 000-.708l-3-3zM13.5 6.207L9.793 2.5 "
130+
"3.622 8.671a.5.5 0 00-.121.196l-1.47 4.413a.5.5 0 00.633.633l4.413-1.47"
131+
'a.5.5 0 00.196-.121L13.5 6.207z"/></svg>'
132+
"Open in editor</a>"
133+
"</div>"
134+
"<pre>" + highlighted + "</pre>"
135+
"</body></html>"
136+
)
137+
from flask import Response
138+
return Response(page, status=200, content_type="text/html")
139+
140+
141+
@server.route("/open-databrickscfg-editor")
142+
def _open_in_editor():
143+
"""Open ``~/.databrickscfg`` in the user's default text editor."""
144+
import platform
145+
import subprocess as _sp
146+
cfg_path = os.path.expanduser("~/.databrickscfg")
147+
system = platform.system()
148+
try:
149+
if system == "Darwin":
150+
_sp.Popen(["open", cfg_path])
151+
elif system == "Windows":
152+
os.startfile(cfg_path)
153+
else:
154+
_sp.Popen(["xdg-open", cfg_path])
155+
except Exception:
156+
pass
157+
from flask import redirect
158+
return redirect("/open-databrickscfg")
159+
160+
161+
def _highlight_ini(text: str) -> str:
162+
"""Add syntax highlighting spans to escaped INI content."""
163+
lines = []
164+
for line in text.split("\n"):
165+
stripped = line.lstrip()
166+
if stripped.startswith("#") or stripped.startswith(";"):
167+
lines.append(f'<span class="comment">{line}</span>')
168+
elif stripped.startswith("["):
169+
lines.append(f'<span class="section">{line}</span>')
170+
elif "=" in line:
171+
key, _, val = line.partition("=")
172+
lines.append(f'<span class="key">{key}</span>=<span class="val">{val}</span>')
173+
else:
174+
lines.append(line)
175+
return "\n".join(lines)
176+
177+
93178
# ── JSON Syntax Highlighter ───────────────────────────────────────────────────
94179
# ── Pagination helpers ────────────────────────────────────────────────────────
95180
_SKIP_LIST_KEYS = frozenset({"schemas"})
@@ -1104,6 +1189,29 @@ def _theme_card(tid: str, t: dict) -> html.Div:
11041189
"Deploy API Explorer",
11051190
])),
11061191
dbc.ModalBody([
1192+
html.H6([html.I(className="bi bi-terminal me-2"), "Install Databricks CLI"], className="deploy-section-title"),
1193+
html.P("The Databricks CLI is required for profile-based authentication and deployment:", className="text-muted small mb-2"),
1194+
1195+
html.Div([html.Span("macOS", className="deploy-option-label"), " (Homebrew)"], className="deploy-option-title"),
1196+
html.Pre("brew tap databricks/tap\nbrew install databricks", className="deploy-code"),
1197+
1198+
html.Div([html.Span("Linux / WSL", className="deploy-option-label"), " (curl)"], className="deploy-option-title"),
1199+
html.Pre('curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh', className="deploy-code"),
1200+
1201+
html.Div([html.Span("Windows", className="deploy-option-label"), " (winget)"], className="deploy-option-title"),
1202+
html.Pre("winget install Databricks.DatabricksCLI", className="deploy-code"),
1203+
1204+
html.P([
1205+
"Verify installation: ",
1206+
html.Code("databricks --version"),
1207+
". See ",
1208+
html.A("docs.databricks.com/dev-tools/cli", href="https://docs.databricks.com/dev-tools/cli/index.html",
1209+
target="_blank", rel="noopener noreferrer", className="about-link"),
1210+
" for details.",
1211+
], className="text-muted small mb-4"),
1212+
1213+
html.Hr(className="divider"),
1214+
11071215
html.H6([html.I(className="bi bi-laptop me-2"), "Local Development"], className="deploy-section-title"),
11081216
html.P("Run the app locally with Databricks CLI authentication:", className="text-muted small mb-2"),
11091217
html.Pre(
@@ -1250,12 +1358,22 @@ def _profile_section() -> html.Div:
12501358
# Options are populated dynamically via toggle_dropdown callback on each open
12511359
return html.Div([
12521360
html.Label("Profile", className="conn-label"),
1253-
dbc.Select(
1254-
id="profile-select",
1255-
options=[],
1256-
value="",
1257-
className="conn-select",
1258-
),
1361+
html.Div([
1362+
dbc.Select(
1363+
id="profile-select",
1364+
options=[],
1365+
value="",
1366+
className="conn-select",
1367+
),
1368+
html.A(
1369+
html.I(className="bi bi-pencil-square"),
1370+
id="open-databrickscfg-btn",
1371+
href="/open-databrickscfg",
1372+
target="_blank",
1373+
className="open-cfg-btn",
1374+
title="Open ~/.databrickscfg",
1375+
),
1376+
], className="profile-select-row"),
12591377
html.Div(id="profile-auth-type", className="conn-hint mt-1"),
12601378
html.Button(
12611379
[html.I(className="bi bi-arrow-repeat me-2"), "Re-authenticate with SSO"],
@@ -1901,7 +2019,13 @@ def apply_connection(n_clicks, mode, profile, sso_host, custom_host, custom_toke
19012019
# Quick validation
19022020
r = make_api_call("GET", "/api/2.0/preview/scim/v2/Me", token, host)
19032021
if not r["success"]:
1904-
return no_update, html.Span(f"Connection failed: {r['status_code']} {r.get('error','')}", className="text-danger"), no_update
2022+
if r["status_code"] == 401:
2023+
msg = "Authentication failed — check that the token is valid and not expired."
2024+
elif r["status_code"] == 0:
2025+
msg = f"Could not reach {host} — check the workspace URL."
2026+
else:
2027+
msg = f"Connection failed: {r['status_code']} {r.get('error', '')}"
2028+
return no_update, html.Span([html.I(className="bi bi-x-circle-fill me-1"), msg], className="text-danger"), no_update
19052029
return {"mode": "custom", "host": host, "token": token}, html.Span([html.I(className="bi bi-check-circle-fill me-1 text-success"), f"Connected to {host.replace('https://','')}"]), no_update
19062030

19072031

assets/style.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,31 @@ body {
11511151
.conn-radio-label { font-size: 12px; color: var(--text-2); cursor: pointer; }
11521152

11531153
/* Connection form elements */
1154+
.profile-select-row {
1155+
display: flex;
1156+
align-items: center;
1157+
gap: 6px;
1158+
}
1159+
.profile-select-row .conn-select { flex: 1; min-width: 0; }
1160+
.open-cfg-btn {
1161+
display: flex;
1162+
align-items: center;
1163+
justify-content: center;
1164+
color: var(--text-3);
1165+
font-size: 14px;
1166+
text-decoration: none;
1167+
width: 32px;
1168+
height: 32px;
1169+
flex-shrink: 0;
1170+
border: 1px solid var(--border);
1171+
border-radius: var(--r-sm);
1172+
transition: color 0.15s, background 0.15s, border-color 0.15s;
1173+
}
1174+
.open-cfg-btn:hover {
1175+
color: var(--cyan);
1176+
border-color: var(--cyan);
1177+
background: rgba(0,212,255,0.08);
1178+
}
11541179
.conn-label {
11551180
display: block;
11561181
font-size: 10px;

auth.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,22 @@ def make_api_call(
417417
timeout=timeout,
418418
)
419419
elapsed_ms = int((time.perf_counter() - t0) * 1000)
420+
421+
# Detect HTML login/sign-in pages returned instead of JSON —
422+
# this happens when the token is invalid, expired, or the host
423+
# requires browser-based authentication.
424+
content_type = response.headers.get("Content-Type", "")
425+
if "text/html" in content_type:
426+
return {
427+
"status_code": 401,
428+
"elapsed_ms": elapsed_ms,
429+
"data": {"error": "Authentication failed — the server returned a sign-in page instead of JSON. "
430+
"Check that your token is valid and has not expired."},
431+
"success": False,
432+
"error": "Authentication required",
433+
"url": url,
434+
}
435+
420436
try:
421437
data = response.json()
422438
except Exception:

0 commit comments

Comments
 (0)