Skip to content

Commit 5ef0919

Browse files
authored
Add run as job functionality (#39)
1 parent 4b64b29 commit 5ef0919

File tree

10 files changed

+421
-11
lines changed

10 files changed

+421
-11
lines changed

.vscode/extensions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"recommendations": [
55
"dbaeumer.vscode-eslint",
66
"amodio.tsl-problem-matcher",
7-
"esbenp.prettier-vscode"
7+
"esbenp.prettier-vscode",
8+
"Tobermory.es6-string-html"
89
]
910
}

packages/databricks-vscode/package.json

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"onCommand:databricks.cluster.filterByAll",
4747
"onCommand:databricks.cluster.filterByMe",
4848
"onCommand:databricks.cluster.filterByRunning",
49+
"onCommand:databricks.run.getProgramName",
50+
"onCommand:databricks.run.runEditorContentsAsWorkflow",
4951
"onView:clusterManager",
5052
"onView:clusterList"
5153
],
@@ -95,6 +97,13 @@
9597
"command": "databricks.cluster.refresh",
9698
"icon": "$(refresh)",
9799
"title": "Refresh"
100+
},
101+
{
102+
"command": "databricks.run.runEditorContentsAsWorkflow",
103+
"title": "Run File as Workflow on Databricks",
104+
"category": "Databricks",
105+
"enablement": "!inDebugMode",
106+
"icon": "$(play)"
98107
}
99108
],
100109
"viewsContainers": {
@@ -169,6 +178,19 @@
169178
"when": "view == configurationView && viewItem == clusterStopped",
170179
"group": "inline@0"
171180
}
181+
],
182+
"editor/title/run": [
183+
{
184+
"command": "databricks.run.runEditorContentsAsWorkflow",
185+
"when": "resourceLangId == python",
186+
"group": "navigation@1"
187+
}
188+
],
189+
"commandPalette": [
190+
{
191+
"command": "databricks.run.runEditorContentsAsWorkflow",
192+
"when": "resourceLangId == python"
193+
}
172194
]
173195
},
174196
"submenus": [
@@ -193,11 +215,11 @@
193215
"test:lint": "eslint src --ext ts && prettier . -c",
194216
"test:compile": "tsc -p . --outDir out",
195217
"test:watch": "tsc -p . -w --outDir out",
196-
"test:unit": "node ./out/test/runTest.js",
218+
"test:unit": "yarn run test:compile && node ./out/test/runTest.js",
197219
"test:integ:prepare": "yarn run vscode:package && extest get-vscode --type ${VSCODE_TEST_VERSION:-stable} --storage /tmp/vscode-test-databricks && extest get-chromedriver --storage /tmp/vscode-test-databricks && extest install-vsix -f databricks-0.0.1.vsix --storage /tmp/vscode-test-databricks",
198220
"test:integ:run": "yarn run test:compile && ts-node src/test/e2e/scripts/e2e.ts --storage /tmp/vscode-test-databricks --code_settings src/test/e2e/settings.json 'out/**/*.e2e.js'",
199221
"test:integ": "yarn run test:integ:prepare && yarn run test:integ:run",
200-
"test": "yarn run test:compile && yarn run compile && yarn run test:lint && yarn run test:unit",
222+
"test": "yarn run compile && yarn run test:lint && yarn run test:unit",
201223
"build": "yarn run vscode:prepublish",
202224
"clean": "rm -rf node_modules out dist .vscode-test"
203225
},
@@ -227,4 +249,4 @@
227249
"webpack": "^5.73.0",
228250
"webpack-cli": "^4.10.0"
229251
}
230-
}
252+
}

packages/databricks-vscode/src/cli/CliWrapper.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import {execFile, ExecFileException, spawn} from "child_process";
1+
import {spawn} from "child_process";
22

33
/**
44
* Entrypoint for all wrapped CLI commands
5+
*
6+
* Righ now this is a placeholder for a future implementation
7+
* of the bricks CLI
58
*/
69
export class CliWrapper {
710
constructor() {}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
3+
import {mock, instance} from "ts-mockito";
4+
import {cluster} from "@databricks/databricks-sdk";
5+
import {ConnectionManager} from "./ConnectionManager";
6+
import {Disposable} from "vscode";
7+
import {CliWrapper} from "../cli/CliWrapper";
8+
9+
const me = "user-1";
10+
const mockListClustersResponse: cluster.ListClustersResponse = {
11+
clusters: [
12+
{
13+
cluster_id: "cluster-id-2",
14+
cluster_name: "cluster-name-2",
15+
cluster_source: "UI",
16+
creator_user_name: "user-2",
17+
state: "TERMINATED",
18+
},
19+
{
20+
cluster_id: "cluster-id-1",
21+
cluster_name: "cluster-name-1",
22+
cluster_source: "UI",
23+
creator_user_name: me,
24+
state: "RUNNING",
25+
},
26+
{
27+
cluster_id: "cluster-id-3",
28+
cluster_name: "cluster-name-3",
29+
cluster_source: "JOB",
30+
creator_user_name: "user-3",
31+
state: "RUNNING",
32+
},
33+
],
34+
};
35+
36+
describe(__filename, () => {
37+
let mockedCliWrapper: CliWrapper;
38+
let disposables: Array<Disposable>;
39+
40+
beforeEach(() => {
41+
disposables = [];
42+
mockedCliWrapper = mock(CliWrapper);
43+
});
44+
45+
afterEach(() => {
46+
disposables.forEach((d) => d.dispose());
47+
});
48+
49+
it("should create an instance", async () => {
50+
let manager = new ConnectionManager(instance(mockedCliWrapper));
51+
});
52+
53+
// login
54+
// logout
55+
// configure
56+
// attach cluster
57+
// detach cluster
58+
// attach workspace
59+
// detach workspace
60+
});

packages/databricks-vscode/src/configuration/ConnectionManager.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@ import {
44
fromConfigFile,
55
ScimService,
66
} from "@databricks/databricks-sdk";
7-
import {commands, EventEmitter, window, workspace} from "vscode";
7+
import {
8+
commands,
9+
EventEmitter,
10+
Uri,
11+
window,
12+
workspace as vscodeWorkspace,
13+
} from "vscode";
814
import {CliWrapper} from "../cli/CliWrapper";
15+
import {PathMapper} from "./PathMapper";
916
import {ProjectConfigFile} from "./ProjectConfigFile";
1017
import {selectProfile} from "./selectProfileWizard";
1118

@@ -21,6 +28,7 @@ export class ConnectionManager {
2128
private _state: ConnectionState = "DISCONNECTED";
2229
private _cluster?: Cluster;
2330
private _apiClient?: ApiClient;
31+
private _pathMapper?: PathMapper;
2432
private _projectConfigFile?: ProjectConfigFile;
2533
private _me?: string;
2634
private _profile?: string;
@@ -47,6 +55,10 @@ export class ConnectionManager {
4755
return this._state;
4856
}
4957

58+
get pathMapper(): PathMapper | undefined {
59+
return this._pathMapper;
60+
}
61+
5062
/**
5163
* Get a pre-configured APIClient. Do not hold on to references to this class as
5264
* it might be invalidated as the configuration changes. If you have to store a reference
@@ -64,14 +76,14 @@ export class ConnectionManager {
6476
let profile;
6577

6678
try {
67-
if (!workspace.rootPath) {
79+
if (!vscodeWorkspace.rootPath) {
6880
throw new Error(
6981
"Can't login to Databricks: Not in a VSCode workspace"
7082
);
7183
}
7284

7385
projectConfigFile = await ProjectConfigFile.load(
74-
workspace.rootPath
86+
vscodeWorkspace.rootPath
7587
);
7688

7789
profile = projectConfigFile.config.profile;
@@ -106,6 +118,12 @@ export class ConnectionManager {
106118
if (projectConfigFile.config.clusterId) {
107119
await this.attachCluster(projectConfigFile.config.clusterId);
108120
}
121+
122+
if (projectConfigFile.config.workspacePath) {
123+
await this.attachWorkspace(
124+
Uri.file(projectConfigFile.config.workspacePath)
125+
);
126+
}
109127
}
110128

111129
async logout() {
@@ -167,17 +185,20 @@ export class ConnectionManager {
167185
}
168186

169187
private async writeConfigFile(profile: string) {
170-
if (!workspace.rootPath) {
188+
if (!vscodeWorkspace.rootPath) {
171189
throw new Error("Not in a VSCode workspace");
172190
}
173191

174192
let projectConfigFile;
175193
try {
176194
projectConfigFile = await ProjectConfigFile.load(
177-
workspace.rootPath
195+
vscodeWorkspace.rootPath
178196
);
179197
} catch (e) {
180-
projectConfigFile = new ProjectConfigFile({}, workspace.rootPath);
198+
projectConfigFile = new ProjectConfigFile(
199+
{},
200+
vscodeWorkspace.rootPath
201+
);
181202
}
182203

183204
projectConfigFile.profile = profile;
@@ -224,6 +245,19 @@ export class ConnectionManager {
224245
this.updateCluster(undefined);
225246
}
226247

248+
async attachWorkspace(workspacePath: Uri): Promise<void> {
249+
if (
250+
!vscodeWorkspace.workspaceFolders ||
251+
!vscodeWorkspace.workspaceFolders.length
252+
) {
253+
// TODO how do we handle this?
254+
return;
255+
}
256+
257+
const wsUri = vscodeWorkspace.workspaceFolders[0].uri;
258+
this._pathMapper = new PathMapper(workspacePath, wsUri);
259+
}
260+
227261
private async getMe(apiClient: ApiClient): Promise<string> {
228262
let scimApi = new ScimService(apiClient);
229263
let response = await scimApi.me({});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import assert from "assert";
2+
import {Uri} from "vscode";
3+
import {PathMapper} from "./PathMapper";
4+
5+
describe(__filename, () => {
6+
it("should map a file", async () => {
7+
let mapper = new PathMapper(
8+
Uri.file(
9+
"/Workspace/Repos/fabian.jakobs@databricks.com/notebook-best-practices"
10+
),
11+
Uri.file("/Users/fabian.jakobs/Desktop/notebook-best-practices")
12+
);
13+
14+
assert.equal(
15+
mapper.localToRemote(
16+
Uri.file(
17+
"/Users/fabian.jakobs/Desktop/notebook-best-practices/hello.py"
18+
)
19+
),
20+
"/Workspace/Repos/fabian.jakobs@databricks.com/notebook-best-practices/hello.py"
21+
);
22+
});
23+
24+
it("should map a directory", async () => {
25+
let mapper = new PathMapper(
26+
Uri.file(
27+
"/Workspace/Repos/fabian.jakobs@databricks.com/notebook-best-practices"
28+
),
29+
Uri.file("/Users/fabian.jakobs/Desktop/notebook-best-practices")
30+
);
31+
32+
assert.equal(
33+
mapper.localToRemoteDir(
34+
Uri.file(
35+
"/Users/fabian.jakobs/Desktop/notebook-best-practices/jobs/hello.py"
36+
)
37+
),
38+
"/Workspace/Repos/fabian.jakobs@databricks.com/notebook-best-practices/jobs"
39+
);
40+
});
41+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import path = require("path");
2+
import {Uri} from "vscode";
3+
4+
/**
5+
* Class that maps paths between the local file system to the file systems
6+
* on the Databricks driver
7+
*/
8+
export class PathMapper {
9+
readonly repo: string;
10+
11+
constructor(readonly repoPath: Uri, readonly workspacePath: Uri) {
12+
this.repo = path.basename(repoPath.path);
13+
}
14+
15+
localToRemoteDir(localPath: Uri): string {
16+
return path.dirname(this.localToRemote(localPath));
17+
}
18+
19+
localToRemote(localPath: Uri): string {
20+
let relativePath = localPath.path.replace(this.workspacePath.path, "");
21+
return Uri.joinPath(this.repoPath, relativePath).path;
22+
}
23+
}

packages/databricks-vscode/src/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {ClusterListDataProvider} from "./cluster/ClusterListDataProvider";
66
import {ClusterModel} from "./cluster/ClusterModel";
77
import {ClusterCommands} from "./cluster/ClusterCommands";
88
import {ConfigurationDataProvider} from "./configuration/ConfigurationDataProvider";
9+
import {WorkflowCommands} from "./workflow/WorkflowCommands";
910

1011
export function activate(context: ExtensionContext) {
1112
let cli = new CliWrapper();
@@ -58,6 +59,16 @@ export function activate(context: ExtensionContext) {
5859
)
5960
);
6061

62+
// Workflow group
63+
const workflowCommands = new WorkflowCommands(connectionManager, context);
64+
context.subscriptions.push(
65+
commands.registerCommand(
66+
"databricks.run.runEditorContentsAsWorkflow",
67+
workflowCommands.runEditorContentsAsWorkflow(),
68+
workflowCommands
69+
)
70+
);
71+
6172
// Cluster group
6273
const clusterModel = new ClusterModel(connectionManager);
6374
const clusterTreeDataProvider = new ClusterListDataProvider(clusterModel);

0 commit comments

Comments
 (0)