Skip to content

Commit 6b5762e

Browse files
Bundle selection ux (#1519)
## Changes When you land on a workspace without dabs config in the root, you can now select a sub-folder as the active bundle folder (without changing the workspace root, and without reloading the whole IDE). <img width="500" alt="Screenshot 2025-01-16 at 10 40 16" src="https://github.com/user-attachments/assets/5348c29a-1e8c-4ad3-b54e-bc3c59347922" /> The above is for the case when we can't find any sub-projects ourselves. Clicking on the top button opens an OS folder selection dialog. When we can detect sub projects, we show the "select sub-folder" button on top: <img width="500" alt="Screenshot 2025-01-16 at 10 39 44" src="https://github.com/user-attachments/assets/f96a0953-7e03-4281-b9a5-d80a1907bbe8" /> When you select the sub-folder and initialise the extension, we now show additional "Local Folder" UI at the top of the configuration: <img width="497" alt="Screenshot 2025-01-14 at 10 11 09" src="https://github.com/user-attachments/assets/17270619-c4f0-453f-9d9e-a85574ab3bd4" /> <img width="637" alt="Screenshot 2025-01-14 at 10 17 39" src="https://github.com/user-attachments/assets/af326541-8765-4583-a908-5fbd5cffd8b7" /> The UI for selecting sub-folders: <img width="880" alt="Screenshot 2025-01-14 at 10 11 52" src="https://github.com/user-attachments/assets/1eee2b12-8747-42da-ab72-89eb688ab4db" /> "Select another folder" opens a OS-level selection dialog. If you select a folder that's part of the workspace, we don't reload the IDE and just point our extension to it. When the selected folder is outside of the workspace, we reload the IDE with the folder being the new workspace root. After you select some sub-folder it's saved to workspace-scoped vscode storage, so the next time this workspace is opened our extension already knows what sub-folder to use. After the extension is initialised, you can now easily change the active bundle folder if you have multiple bundles in different sub-folders. ## Tests Manually and with a few new unit tests, e2e tests will be in the follow up PR --------- Co-authored-by: Julia Crawford (Databricks) <julia.crawford@databricks.com>
1 parent df60b3d commit 6b5762e

34 files changed

+519
-409
lines changed

packages/databricks-vscode/package.json

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@
203203
"enablement": "databricks.context.activated",
204204
"category": "Databricks"
205205
},
206+
{
207+
"command": "databricks.bundle.selectActiveProjectFolder",
208+
"icon": "$(gear)",
209+
"title": "Select a Databricks project folder",
210+
"enablement": "databricks.context.activated && databricks.context.bundle.deploymentState == idle",
211+
"category": "Databricks"
212+
},
206213
{
207214
"command": "databricks.bundle.refreshRemoteState",
208215
"icon": "$(refresh)",
@@ -444,17 +451,22 @@
444451
},
445452
{
446453
"view": "configurationView",
447-
"contents": "There are multiple Databricks projects in the folder:\n[Open existing Databricks Project](command:databricks.bundle.openSubProject)",
454+
"contents": "Detected multiple Databricks projects in the VSCode workspace:\n[Select a project](command:databricks.bundle.selectActiveProjectFolder)",
448455
"when": "workspaceFolderCount > 0 && databricks.context.initialized && databricks.context.subProjectsAvailable"
449456
},
450457
{
451458
"view": "configurationView",
452-
"contents": "Migrate current folder to a Databricks Project: \n[Migrate current folder to a Databricks Project](command:databricks.bundle.startManualMigration)",
459+
"contents": "No Databricks project configuration detected in the root of the VSCode workspace:\n[Create configuration](command:databricks.bundle.startManualMigration)",
453460
"when": "workspaceFolderCount > 0 && databricks.context.initialized && databricks.context.pendingManualMigration"
454461
},
455462
{
456463
"view": "configurationView",
457-
"contents": "[Create a new Databricks Project](command:databricks.bundle.initNewProject)",
464+
"contents": "No Databricks projects detected in the VSCode workspace:\n[Select a project manually](command:databricks.bundle.selectActiveProjectFolder)",
465+
"when": "workspaceFolderCount > 0 && databricks.context.initialized && !databricks.context.isBundleProject && !databricks.context.subProjectsAvailable"
466+
},
467+
{
468+
"view": "configurationView",
469+
"contents": "Chose a new parent folder and create a Databricks project based on a [template](https://docs.databricks.com/en/dev-tools/bundles/templates.html#databricks-asset-bundle-project-templates):\n[Create a new project](command:databricks.bundle.initNewProject)",
458470
"when": "workspaceFolderCount > 0 && databricks.context.initialized && !databricks.context.isBundleProject"
459471
},
460472
{
@@ -464,7 +476,12 @@
464476
},
465477
{
466478
"view": "configurationView",
467-
"contents": "This folder is empty.\n[Create a new Databricks Project](command:databricks.bundle.initNewProject)",
479+
"contents": "This folder is empty.\n[Open folder](command:vscode.openFolder)",
480+
"when": "workspaceFolderCount == 0"
481+
},
482+
{
483+
"view": "configurationView",
484+
"contents": "[Create a new Databricks project](command:databricks.bundle.initNewProject)",
468485
"when": "workspaceFolderCount == 0"
469486
},
470487
{
@@ -559,6 +576,16 @@
559576
"when": "viewItem =~ /^databricks.*\\.(has-url).*$/ && databricks.context.bundle.deploymentState == idle",
560577
"group": "navigation_2@0"
561578
},
579+
{
580+
"command": "databricks.bundle.selectActiveProjectFolder",
581+
"when": "viewItem =~ /^databricks.configuration.activeProjectFolder/ && databricks.context.bundle.deploymentState == idle",
582+
"group": "navigation_2@0"
583+
},
584+
{
585+
"command": "databricks.bundle.selectActiveProjectFolder",
586+
"when": "viewItem =~ /^databricks.configuration.activeProjectFolder/ && databricks.context.bundle.deploymentState == idle",
587+
"group": "inline@2"
588+
},
562589
{
563590
"command": "databricks.connection.attachCluster",
564591
"when": "view == clusterView && databricks.context.bundle.deploymentState == idle",

packages/databricks-vscode/resources/python/dbconnect-bootstrap.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,19 @@
33
import logging
44
from runpy import run_path
55

6-
# Load environment variables from .databricks/.databricks.env
7-
# We only look for the folder in the current working directory
8-
# since for all commands laucnhed from root workspace
9-
def load_env_file_from_cwd(path: str):
10-
if not os.path.isdir(path):
11-
return
12-
13-
env_file_path = os.path.join(path, ".databricks", ".databricks.env")
14-
if not os.path.exists(os.path.dirname(env_file_path)):
15-
return
16-
17-
with open(env_file_path, "r") as f:
18-
for line in f.readlines():
19-
key, value = line.strip().split("=", 1)
20-
os.environ[key] = value
21-
return
22-
6+
def load_env_from_leaf(path: str) -> bool:
7+
curdir = path if os.path.isdir(path) else os.path.dirname(path)
8+
env_file_path = os.path.join(curdir, ".databricks", ".databricks.env")
9+
if os.path.exists(env_file_path):
10+
with open(env_file_path, "r") as f:
11+
for line in f.readlines():
12+
key, value = line.strip().split("=", 1)
13+
os.environ[key] = value
14+
return curdir
15+
parent = os.path.dirname(curdir)
16+
if parent == curdir:
17+
return curdir
18+
return load_env_from_leaf(parent)
2319

2420
script = sys.argv[1]
2521
sys.argv = sys.argv[1:]
@@ -35,8 +31,7 @@ def load_env_file_from_cwd(path: str):
3531
# Suppress grpc warnings coming from databricks-connect with newer version of grpcio lib
3632
os.environ["GRPC_VERBOSITY"] = "NONE"
3733

38-
root_dir = os.getcwd()
39-
load_env_file_from_cwd(root_dir)
34+
project_dir = load_env_from_leaf(cur_dir)
4035

4136
log_level = os.environ.get("DATABRICKS_VSCODE_LOG_LEVEL")
4237
log_level = log_level if log_level is not None else "WARN"
@@ -68,7 +63,7 @@ def getArgument(*args, **kwargs):
6863

6964
db_globals['getArgument'] = getArgument
7065

71-
sys.path.insert(0, root_dir)
66+
sys.path.insert(0, project_dir)
7267
sys.path.insert(0, cur_dir)
7368

7469
run_path(script, init_globals=db_globals, run_name="__main__")

packages/databricks-vscode/src/bundle/BundleFileSet.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ describe(__filename, async function () {
2323
function getWorkspaceFolderManagerMock() {
2424
const mockWorkspaceFolderManager = mock<WorkspaceFolderManager>();
2525
const mockWorkspaceFolder = mock<WorkspaceFolder>();
26-
when(mockWorkspaceFolder.uri).thenReturn(Uri.file(tmpdir.path));
26+
const uri = Uri.file(tmpdir.path);
27+
when(mockWorkspaceFolder.uri).thenReturn(uri);
2728
when(mockWorkspaceFolderManager.activeWorkspaceFolder).thenReturn(
2829
instance(mockWorkspaceFolder)
2930
);
31+
when(mockWorkspaceFolderManager.activeProjectUri).thenReturn(uri);
3032
return instance(mockWorkspaceFolderManager);
3133
}
3234

packages/databricks-vscode/src/bundle/BundleFileSet.ts

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,21 @@ export class BundleFileSet {
6262
return bundle as BundleSchema;
6363
});
6464

65-
private get workspaceRoot() {
66-
return this.workspaceFolderManager.activeWorkspaceFolder.uri;
65+
private get projectRoot() {
66+
return this.workspaceFolderManager.activeProjectUri;
6767
}
6868

6969
constructor(
7070
private readonly workspaceFolderManager: WorkspaceFolderManager
7171
) {
72-
workspaceFolderManager.onDidChangeActiveWorkspaceFolder(() => {
72+
workspaceFolderManager.onDidChangeActiveProjectFolder(() => {
7373
this.bundleDataCache.invalidate();
7474
});
7575
}
7676

7777
async getRootFile() {
7878
const rootFile = await glob.glob(
79-
getAbsoluteGlobPath(rootFilePattern, this.workspaceRoot),
79+
getAbsoluteGlobPath(rootFilePattern, this.projectRoot),
8080
{nocase: process.platform === "win32"}
8181
);
8282
if (rootFile.length !== 1) {
@@ -85,33 +85,6 @@ export class BundleFileSet {
8585
return Uri.file(rootFile[0]);
8686
}
8787

88-
async getSubProjects(
89-
root?: Uri
90-
): Promise<{relative: Uri; absolute: Uri}[]> {
91-
const subProjectRoots = await glob.glob(
92-
getAbsoluteGlobPath(
93-
subProjectFilePattern,
94-
root || this.workspaceRoot
95-
),
96-
{nocase: process.platform === "win32"}
97-
);
98-
const normalizedRoot = path.normalize(
99-
root?.fsPath ?? this.workspaceRoot.fsPath
100-
);
101-
return subProjectRoots
102-
.map((rootFile) => {
103-
const dirname = path.dirname(path.normalize(rootFile));
104-
const absolute = Uri.file(dirname);
105-
const relative = Uri.file(
106-
absolute.fsPath.replace(normalizedRoot, "")
107-
);
108-
return {absolute, relative};
109-
})
110-
.filter(({absolute}) => {
111-
return absolute.fsPath !== normalizedRoot;
112-
});
113-
}
114-
11588
async getIncludedFilesGlob() {
11689
const rootFile = await this.getRootFile();
11790
if (rootFile === undefined) {
@@ -133,7 +106,7 @@ export class BundleFileSet {
133106
return (
134107
await glob.glob(
135108
toGlobPath(
136-
path.join(this.workspaceRoot.fsPath, includedFilesGlob)
109+
path.join(this.projectRoot.fsPath, includedFilesGlob)
137110
),
138111
{nocase: process.platform === "win32"}
139112
)
@@ -171,7 +144,7 @@ export class BundleFileSet {
171144
isRootBundleFile(e: Uri) {
172145
return minimatch(
173146
e.fsPath,
174-
getAbsoluteGlobPath(rootFilePattern, this.workspaceRoot)
147+
getAbsoluteGlobPath(rootFilePattern, this.projectRoot)
175148
);
176149
}
177150

@@ -182,7 +155,7 @@ export class BundleFileSet {
182155
}
183156
includedFilesGlob = getAbsoluteGlobPath(
184157
includedFilesGlob,
185-
this.workspaceRoot
158+
this.projectRoot
186159
);
187160
return minimatch(e.fsPath, toGlobPath(includedFilesGlob));
188161
}

packages/databricks-vscode/src/bundle/BundleInitWizard.ts

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,9 @@ import {getSubProjects} from "./BundleFileSet";
1515
import {tmpdir} from "os";
1616
import {ShellUtils} from "../utils";
1717
import {Events, Telemetry} from "../telemetry";
18-
import {OverrideableConfigModel} from "../configuration/models/OverrideableConfigModel";
19-
import {writeFile, mkdir} from "fs/promises";
20-
import path from "path";
2118
import {escapePathArgument} from "../utils/shellUtils";
22-
23-
export async function promptToOpenSubProjects(
24-
projects: {absolute: Uri; relative: string}[],
25-
authProvider?: AuthProvider
26-
) {
27-
type OpenProjectItem = QuickPickItem & {uri?: Uri};
28-
const items: OpenProjectItem[] = projects.map((project) => {
29-
return {
30-
uri: project.absolute,
31-
label: project.relative,
32-
detail: project.absolute.fsPath,
33-
};
34-
});
35-
items.push(
36-
{label: "", kind: QuickPickItemKind.Separator},
37-
{label: "Choose another folder"}
38-
);
39-
const options = {
40-
title: "Select the project you want to open",
41-
};
42-
const item = await window.showQuickPick<OpenProjectItem>(items, options);
43-
if (!item?.uri) {
44-
return;
45-
}
46-
47-
if (authProvider?.authType === "profile") {
48-
const rootOverrideFilePath =
49-
OverrideableConfigModel.getRootOverrideFile(item.uri);
50-
await mkdir(path.dirname(rootOverrideFilePath.fsPath), {
51-
recursive: true,
52-
});
53-
await writeFile(
54-
rootOverrideFilePath.fsPath,
55-
JSON.stringify({authProfile: authProvider.toJSON().profile})
56-
);
57-
}
58-
await commands.executeCommand("vscode.openFolder", item.uri);
59-
}
19+
import {promptToSelectActiveProjectFolder} from "./activeBundleUtils";
20+
import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager";
6021

6122
export class BundleInitWizard {
6223
private logger = logging.NamedLogger.getOrCreate(Loggers.Extension);
@@ -68,7 +29,8 @@ export class BundleInitWizard {
6829

6930
public async initNewProject(
7031
workspaceUri?: Uri,
71-
existingAuthProvider?: AuthProvider
32+
existingAuthProvider?: AuthProvider,
33+
workspaceFolderManager?: WorkspaceFolderManager
7234
) {
7335
const recordEvent = this.telemetry.start(Events.BUNDLE_INIT);
7436
try {
@@ -97,7 +59,11 @@ export class BundleInitWizard {
9759
this.logger.debug(
9860
`Detected ${projects.length} sub projects after the init wizard, prompting to open one`
9961
);
100-
await promptToOpenSubProjects(projects, authProvider);
62+
await promptToSelectActiveProjectFolder(
63+
projects,
64+
authProvider,
65+
workspaceFolderManager
66+
);
10167
} else {
10268
this.logger.debug(
10369
`No projects detected after the init wizard, showing notification to open a folder manually`

packages/databricks-vscode/src/bundle/BundleProjectManager.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {ProfileAuthProvider} from "../configuration/auth/AuthProvider";
1616
import {ProjectConfigFile} from "../file-managers/ProjectConfigFile";
1717
import {randomUUID} from "crypto";
1818
import {onError} from "../utils/onErrorDecorator";
19-
import {BundleInitWizard, promptToOpenSubProjects} from "./BundleInitWizard";
19+
import {BundleInitWizard} from "./BundleInitWizard";
2020
import {EventReporter, Events, Telemetry} from "../telemetry";
2121
import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager";
22+
import {promptToSelectActiveProjectFolder} from "./activeBundleUtils";
2223

2324
export class BundleProjectManager {
2425
private logger = logging.NamedLogger.getOrCreate(Loggers.Extension);
@@ -52,7 +53,7 @@ export class BundleProjectManager {
5253
private telemetry: Telemetry
5354
) {
5455
this.disposables.push(
55-
this.workspaceFolderManager.onDidChangeActiveWorkspaceFolder(
56+
this.workspaceFolderManager.onDidChangeActiveProjectFolder(
5657
async () => {
5758
await this.isBundleProjectCache.refresh();
5859
}
@@ -162,10 +163,18 @@ export class BundleProjectManager {
162163
});
163164
}
164165

165-
public async openSubProjects() {
166-
if (this.subProjects && this.subProjects.length > 0) {
167-
return promptToOpenSubProjects(this.subProjects);
168-
}
166+
public async selectActiveProjectFolder() {
167+
return window.withProgress(
168+
{location: {viewId: "configurationView"}},
169+
async () => {
170+
await this.detectSubProjects();
171+
return promptToSelectActiveProjectFolder(
172+
this.subProjects ?? [],
173+
undefined,
174+
this.workspaceFolderManager
175+
);
176+
}
177+
);
169178
}
170179

171180
private setPendingManualMigration() {
@@ -328,7 +337,8 @@ export class BundleProjectManager {
328337
this.connectionManager.databricksWorkspace?.authProvider;
329338
const parentFolder = await bundleInitWizard.initNewProject(
330339
this.workspaceUri,
331-
authProvider
340+
authProvider,
341+
this.workspaceFolderManager
332342
);
333343
if (parentFolder) {
334344
await this.isBundleProjectCache.refresh();

packages/databricks-vscode/src/bundle/BundleWatcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class BundleWatcher implements Disposable {
2525
) {
2626
this.initCleanup = this.init();
2727
this.disposables.push(
28-
this.workspaceFolderManager.onDidChangeActiveWorkspaceFolder(() => {
28+
this.workspaceFolderManager.onDidChangeActiveProjectFolder(() => {
2929
this.initCleanup.dispose();
3030
this.initCleanup = this.init();
3131
this.bundleFileSet.bundleDataCache.invalidate();
@@ -37,7 +37,7 @@ export class BundleWatcher implements Disposable {
3737
const yamlWatcher = workspace.createFileSystemWatcher(
3838
getAbsoluteGlobPath(
3939
path.join("**", "*.{yaml,yml}"),
40-
this.workspaceFolderManager.activeWorkspaceFolder.uri
40+
this.workspaceFolderManager.activeProjectUri
4141
)
4242
);
4343

0 commit comments

Comments
 (0)