Skip to content

Commit 99a38fb

Browse files
guidooswaldDBclaude
andcommitted
Add instant user dropdown, click-outside-to-close, and deploy modal
- Split user dropdown into clientside toggle (instant) + async data fetch - Add transparent overlay for click-outside-to-close on user panel - Make mode badge clickable to show deployment instructions modal - Modal covers local dev setup and Databricks App deployment options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c44541c commit 99a38fb

3 files changed

Lines changed: 183 additions & 18 deletions

File tree

app.py

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -607,12 +607,78 @@ def build_sidebar() -> html.Div:
607607

608608

609609
# ── Top Bar ───────────────────────────────────────────────────────────────────
610-
_MODE_BADGE = (
611-
dbc.Badge([html.I(className="bi bi-cloud-fill me-1"), "Databricks App"], color="info", className="mode-badge")
612-
if IS_DATABRICKS_APP else
613-
dbc.Badge([html.I(className="bi bi-laptop me-1"), "Local Mode"], color="warning", className="mode-badge")
610+
_MODE_BADGE = html.Button(
611+
dbc.Badge(
612+
[html.I(className="bi bi-cloud-fill me-1"), "Databricks App"] if IS_DATABRICKS_APP
613+
else [html.I(className="bi bi-laptop me-1"), "Local Mode"],
614+
color="info" if IS_DATABRICKS_APP else "warning",
615+
className="mode-badge",
616+
),
617+
id="mode-badge-btn", n_clicks=0,
618+
className="mode-badge-btn",
614619
)
615620

621+
_DEPLOY_MODAL = dbc.Modal([
622+
dbc.ModalHeader(dbc.ModalTitle([
623+
html.I(className="bi bi-rocket-takeoff me-2"),
624+
"Deploy API Explorer",
625+
])),
626+
dbc.ModalBody([
627+
html.H6([html.I(className="bi bi-laptop me-2"), "Local Development"], className="deploy-section-title"),
628+
html.P("Run the app locally with Databricks CLI authentication:", className="text-muted small mb-2"),
629+
html.Pre(
630+
"# Install dependencies\n"
631+
"pip install -r requirements.txt\n\n"
632+
"# Authenticate to your workspace\n"
633+
"databricks auth login --host https://<workspace-url>\n\n"
634+
"# Start the app\n"
635+
"python app.py\n\n"
636+
"# Open http://localhost:8050",
637+
className="deploy-code",
638+
),
639+
html.P([
640+
"The app reads CLI profiles from ",
641+
html.Code("~/.databrickscfg"),
642+
" and lets you switch between workspaces in the UI.",
643+
], className="text-muted small mb-4"),
644+
645+
html.Hr(className="divider"),
646+
647+
html.H6([html.I(className="bi bi-cloud-fill me-2"), "Databricks App Deployment"], className="deploy-section-title"),
648+
html.P("Deploy as a managed Databricks App with On-Behalf-Of (OBO) authentication:", className="text-muted small mb-2"),
649+
650+
html.Div([html.Span("Option A", className="deploy-option-label"), " — Asset Bundles (recommended)"], className="deploy-option-title"),
651+
html.Pre(
652+
"# Deploy via Databricks Asset Bundles\n"
653+
"databricks bundle deploy\n\n"
654+
"# Start the app\n"
655+
"databricks bundle run api_explorer",
656+
className="deploy-code",
657+
),
658+
659+
html.Div([html.Span("Option B", className="deploy-option-label"), " — Direct CLI"], className="deploy-option-title"),
660+
html.Pre(
661+
"# Deploy directly\n"
662+
"databricks apps deploy databricks-api-explorer \\\n"
663+
" --source-code-path . \\\n"
664+
" --profile <your-profile>",
665+
className="deploy-code",
666+
),
667+
668+
html.P([
669+
"When running as a Databricks App, authentication is automatic — "
670+
"the user's identity is forwarded via the ",
671+
html.Code("x-forwarded-access-token"),
672+
" header. No token configuration needed.",
673+
], className="text-muted small mb-2"),
674+
675+
html.Div([
676+
html.Span("View logs: ", className="text-muted small"),
677+
html.Code("databricks apps logs databricks-api-explorer --profile <your-profile>", className="small"),
678+
]),
679+
]),
680+
], id="deploy-modal", is_open=False, size="lg", centered=True, className="deploy-modal")
681+
616682
TOPBAR = dbc.Navbar(
617683
dbc.Container([
618684
html.Div([
@@ -752,6 +818,14 @@ def _custom_section():
752818

753819
], id="user-dropdown", style={"display": "none"}, className="user-dropdown")
754820

821+
# Transparent overlay behind dropdown — click to close
822+
_DROPDOWN_OVERLAY = html.Div(
823+
id="dropdown-overlay",
824+
n_clicks=0,
825+
style={"display": "none"},
826+
className="dropdown-overlay",
827+
)
828+
755829

756830
# ── Welcome Panel ─────────────────────────────────────────────────────────────
757831
WELCOME = html.Div([
@@ -795,8 +869,12 @@ def _custom_section():
795869
dcc.Interval(id="sso-poller", interval=1000, disabled=True, n_intervals=0),
796870
dcc.Interval(id="page-ticker", interval=500, disabled=False, n_intervals=0),
797871

872+
dcc.Store(id="dropdown-open", data=False), # tracks dropdown visibility
873+
798874
TOPBAR,
875+
_DROPDOWN_OVERLAY,
799876
USER_DROPDOWN, # fixed dropdown, outside normal flow
877+
_DEPLOY_MODAL,
800878

801879
html.Div([
802880
build_sidebar(),
@@ -853,9 +931,47 @@ def init_on_load(_, conn_config):
853931
return user_el, host_label, ws_name_el
854932

855933

856-
# 2. Toggle dropdown and populate identity info
934+
# 1b. Toggle deploy modal
857935
@app.callback(
936+
Output("deploy-modal", "is_open"),
937+
Input("mode-badge-btn", "n_clicks"),
938+
State("deploy-modal", "is_open"),
939+
prevent_initial_call=True,
940+
)
941+
def toggle_deploy_modal(n, is_open):
942+
return not is_open
943+
944+
945+
# 2. Toggle dropdown open/close — clientside for instant reactivity
946+
app.clientside_callback(
947+
"""
948+
function(btnClicks, overlayClicks, isOpen) {
949+
// Both inputs trigger a close or toggle
950+
var triggered = dash_clientside.callback_context.triggered;
951+
if (!triggered || !triggered.length) return [dash_clientside.no_update, dash_clientside.no_update, dash_clientside.no_update];
952+
var tid = triggered[0].prop_id;
953+
if (tid === "dropdown-overlay.n_clicks") {
954+
// Overlay click → always close
955+
return [false, {display: "none"}, {display: "none"}];
956+
}
957+
// user-btn click → toggle
958+
var newOpen = !isOpen;
959+
var style = newOpen ? {display: "block"} : {display: "none"};
960+
return [newOpen, style, style];
961+
}
962+
""",
963+
Output("dropdown-open", "data"),
858964
Output("user-dropdown", "style"),
965+
Output("dropdown-overlay", "style"),
966+
Input("user-btn", "n_clicks"),
967+
Input("dropdown-overlay", "n_clicks"),
968+
State("dropdown-open", "data"),
969+
prevent_initial_call=True,
970+
)
971+
972+
973+
# 2b. Populate identity info when dropdown opens (server-side, async)
974+
@app.callback(
859975
Output("popup-avatar", "children"),
860976
Output("popup-name", "children"),
861977
Output("popup-username", "children"),
@@ -865,15 +981,13 @@ def init_on_load(_, conn_config):
865981
Output("conn-mode-radio", "value"),
866982
Output("profile-select", "value"),
867983
Output("profile-select", "options"),
868-
Input("user-btn", "n_clicks"),
869-
State("user-dropdown", "style"),
984+
Input("dropdown-open", "data"),
870985
State("conn-config", "data"),
871986
prevent_initial_call=True,
872987
)
873-
def toggle_dropdown(n_clicks, current_style, conn_config):
874-
# Close if already open
875-
if current_style and current_style.get("display") != "none":
876-
return {"display": "none"}, *([no_update] * 9)
988+
def populate_dropdown(is_open, conn_config):
989+
if not is_open:
990+
return [no_update] * 9
877991

878992
conn_config = conn_config or _DEFAULT_CONN
879993
host, token = _resolve_conn(conn_config)
@@ -895,7 +1009,6 @@ def toggle_dropdown(n_clicks, current_style, conn_config):
8951009
username = scim.get("userName", "")
8961010
active = scim.get("active", True)
8971011
groups: List[Dict] = scim.get("groups", [])
898-
entitlements: List[Dict] = scim.get("entitlements", [])
8991012
emails: List[Dict] = scim.get("emails", [])
9001013
primary_email = next((e["value"] for e in emails if e.get("primary")), emails[0]["value"] if emails else "")
9011014
user_id = scim.get("id", "")
@@ -940,7 +1053,6 @@ def toggle_dropdown(n_clicks, current_style, conn_config):
9401053
)
9411054

9421055
return (
943-
{"display": "block"},
9441056
letter,
9451057
display_name,
9461058
username,
@@ -952,10 +1064,6 @@ def toggle_dropdown(n_clicks, current_style, conn_config):
9521064
profile_options,
9531065
)
9541066

955-
956-
# 3. Close dropdown when clicking user-btn again (handled above) or via ESC key not supported,
957-
# so provide close by clicking user-btn (toggle) — already handled in callback 2.
958-
9591067
# 4. Show/hide profile vs sso vs custom section
9601068
@app.callback(
9611069
Output("profile-section", "style"),

assets/style.css

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,55 @@ body {
718718
border-radius: 20px !important;
719719
letter-spacing: 0.02em;
720720
}
721+
.mode-badge-btn {
722+
background: none;
723+
border: none;
724+
padding: 0;
725+
cursor: pointer;
726+
transition: filter 0.15s;
727+
}
728+
.mode-badge-btn:hover { filter: brightness(1.3); }
729+
730+
/* ── Deploy Modal ────────────────────────────────────────────────── */
731+
.deploy-modal .modal-content {
732+
background: var(--card-bg, #1a1d23);
733+
border: 1px solid rgba(255,255,255,0.08);
734+
color: var(--text-primary, #e4e6eb);
735+
}
736+
.deploy-modal .modal-header {
737+
border-bottom: 1px solid rgba(255,255,255,0.06);
738+
}
739+
.deploy-modal .btn-close { filter: invert(1) grayscale(1) brightness(0.7); }
740+
.deploy-section-title {
741+
color: var(--accent-primary, #7f4bc4);
742+
margin-top: 0.5rem;
743+
margin-bottom: 0.5rem;
744+
}
745+
.deploy-code {
746+
background: rgba(0,0,0,0.35);
747+
border: 1px solid rgba(255,255,255,0.06);
748+
border-radius: 6px;
749+
padding: 12px 14px;
750+
font-size: 12px;
751+
color: #a8d8a8;
752+
overflow-x: auto;
753+
white-space: pre;
754+
margin-bottom: 12px;
755+
}
756+
.deploy-option-title {
757+
font-size: 13px;
758+
margin-bottom: 6px;
759+
color: var(--text-primary, #e4e6eb);
760+
}
761+
.deploy-option-label {
762+
background: rgba(127,75,196,0.15);
763+
color: var(--accent-primary, #7f4bc4);
764+
font-size: 11px;
765+
padding: 2px 8px;
766+
border-radius: 10px;
767+
margin-right: 6px;
768+
font-weight: 600;
769+
}
721770

722771
.workspace-info {
723772
display: flex;
@@ -799,6 +848,14 @@ body {
799848
box-shadow: 0 0 10px rgba(0,212,255,0.1);
800849
}
801850

851+
/* ── Dropdown Overlay (click-outside-to-close) ───────────────────── */
852+
.dropdown-overlay {
853+
position: fixed;
854+
top: 0; left: 0; right: 0; bottom: 0;
855+
z-index: 1049; /* just below user-dropdown */
856+
background: transparent;
857+
}
858+
802859
/* ── User Dropdown Panel ──────────────────────────────────────────── */
803860
.user-dropdown {
804861
position: fixed;

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
257
1+
268

0 commit comments

Comments
 (0)