@@ -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+
616682TOPBAR = 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 ─────────────────────────────────────────────────────────────
757831WELCOME = 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" ),
0 commit comments