|
90 | 90 | _DEFAULT_CONN = {"mode": "profile", "profile": DATABRICKS_PROFILE} |
91 | 91 |
|
92 | 92 |
|
| 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 | + |
93 | 178 | # ── JSON Syntax Highlighter ─────────────────────────────────────────────────── |
94 | 179 | # ── Pagination helpers ──────────────────────────────────────────────────────── |
95 | 180 | _SKIP_LIST_KEYS = frozenset({"schemas"}) |
@@ -1104,6 +1189,29 @@ def _theme_card(tid: str, t: dict) -> html.Div: |
1104 | 1189 | "Deploy API Explorer", |
1105 | 1190 | ])), |
1106 | 1191 | 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 | + |
1107 | 1215 | html.H6([html.I(className="bi bi-laptop me-2"), "Local Development"], className="deploy-section-title"), |
1108 | 1216 | html.P("Run the app locally with Databricks CLI authentication:", className="text-muted small mb-2"), |
1109 | 1217 | html.Pre( |
@@ -1250,12 +1358,22 @@ def _profile_section() -> html.Div: |
1250 | 1358 | # Options are populated dynamically via toggle_dropdown callback on each open |
1251 | 1359 | return html.Div([ |
1252 | 1360 | 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"), |
1259 | 1377 | html.Div(id="profile-auth-type", className="conn-hint mt-1"), |
1260 | 1378 | html.Button( |
1261 | 1379 | [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 |
1901 | 2019 | # Quick validation |
1902 | 2020 | r = make_api_call("GET", "/api/2.0/preview/scim/v2/Me", token, host) |
1903 | 2021 | 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 |
1905 | 2029 | 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 |
1906 | 2030 |
|
1907 | 2031 |
|
|
0 commit comments