Skip to content

Commit 02e5ae7

Browse files
1 parent d4bae06 commit 02e5ae7

18 files changed

+288
-72
lines changed

packages/databricks-sdk-js/src/api-client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ export class ApiClientResponseError extends Error {
2828

2929
export class ApiClient {
3030
private agent: https.Agent;
31+
private _host?: URL;
32+
get host() {
33+
return (async () => {
34+
if (!this._host) {
35+
const credentials = await this.credentialProvider();
36+
this._host = credentials.host;
37+
}
38+
return this._host;
39+
})();
40+
}
3141

3242
constructor(
3343
private readonly product: string,

packages/databricks-sdk-js/src/decorators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {ApiClient} from "./api-client";
12
import {CancellationToken} from "./types";
23

34
/**

packages/databricks-sdk-js/src/services/Cluster.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ export class Cluster {
3939
return this.clusterDetails.cluster_name!;
4040
}
4141

42+
get url(): Promise<string> {
43+
return (async () =>
44+
`${(await this.client.host).host}/#setting/clusters/${
45+
this.id
46+
}/configuration`)();
47+
}
48+
4249
get memoryMb(): number | undefined {
4350
return this.clusterDetails.cluster_memory_mb;
4451
}

packages/databricks-sdk-js/src/services/Repos.integ.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
import {IntegrationTestSetup, sleep} from "../test/IntegrationTestSetup";
44
import * as assert from "node:assert";
5-
import {Repos} from "./Repos";
6-
import {ListReposResponse, RepoInfo, ReposService} from "../apis/repos";
5+
import {Repo} from "./Repos";
6+
import {RepoInfo, ReposService} from "../apis/repos";
77
import {randomUUID} from "node:crypto";
88
import {WorkspaceService} from "../apis/workspace";
9-
import path from "node:path";
109

1110
describe(__filename, function () {
1211
let integSetup: IntegrationTestSetup;
@@ -48,30 +47,27 @@ describe(__filename, function () {
4847
});
4948

5049
it("should list repos by prefix", async () => {
51-
let repos = new Repos(integSetup.client);
52-
let response = await repos.getRepos({
50+
let response = await Repo.list(integSetup.client, {
5351
path_prefix: repoDir,
5452
});
55-
assert.ok(response.repos!.length > 0);
53+
assert.ok(response.length > 0);
5654
});
5755

5856
// skip test as it takes too long to run
5957
it.skip("should list all repos", async () => {
60-
let repos = new Repos(integSetup.client);
61-
let response = await repos.getRepos({});
58+
let response = await Repo.list(integSetup.client, {});
6259

63-
assert.notEqual(response.repos, undefined);
64-
assert.ok(response.repos!.length > 0);
60+
assert.notEqual(response, undefined);
61+
assert.ok(response.length > 0);
6562
});
6663

6764
it("should cancel listing repos", async () => {
68-
let repos = new Repos(integSetup.client);
69-
7065
let token = {
7166
isCancellationRequested: false,
7267
};
7368

74-
let response = repos.getRepos(
69+
let response = Repo.list(
70+
integSetup.client,
7571
{
7672
path_prefix: repoDir,
7773
},
@@ -85,7 +81,7 @@ describe(__filename, function () {
8581
const start = Date.now();
8682
await response;
8783
assert.ok(Date.now() - start < 600);
88-
assert.notEqual((await response).repos, undefined);
89-
assert.ok((await response).repos!.length > 0);
84+
assert.notEqual(await response, undefined);
85+
assert.ok((await response).length > 0);
9086
});
9187
});
Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,83 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
12
import {ApiClient} from "../api-client";
2-
import {ListRequest, ListReposResponse, ReposService} from "../apis/repos";
3+
import {ListRequest, ReposService, RepoInfo} from "../apis/repos";
34
import {paginated} from "../decorators";
45
import {CancellationToken} from "../types";
56

7+
export interface RepoList {
8+
repos: Repo[];
9+
next_page_token: any;
10+
}
11+
12+
export class RepoError extends Error {}
13+
614
export class Repos {
715
constructor(private readonly client: ApiClient) {}
8-
9-
@paginated<ListRequest, ListReposResponse>("next_page_token", "repos")
10-
async getRepos(
16+
@paginated<ListRequest, RepoList>("next_page_token", "repos")
17+
async paginatedList(
1118
req: ListRequest,
1219
_token?: CancellationToken
13-
): Promise<ListReposResponse> {
20+
): Promise<RepoList> {
1421
const reposApi = new ReposService(this.client);
15-
return await reposApi.list(req);
22+
return {
23+
repos:
24+
(await reposApi.list(req)).repos?.map(
25+
(details) => new Repo(this.client, details)
26+
) ?? [],
27+
next_page_token: req["next_page_token"],
28+
};
29+
}
30+
}
31+
export class Repo {
32+
private readonly reposApi;
33+
34+
constructor(private readonly client: ApiClient, private details: RepoInfo) {
35+
this.reposApi = new ReposService(this.client);
36+
}
37+
38+
async refresh() {
39+
this.details = await this.reposApi.get({repo_id: this.id});
40+
return this.details;
41+
}
42+
43+
get id(): number {
44+
return this.details.id!;
45+
}
46+
47+
get path(): string {
48+
return this.details.path!;
49+
}
50+
51+
get url(): Promise<string> {
52+
return (async () =>
53+
`${(await this.client.host).host}#folder/${this.id}`)();
54+
}
55+
56+
static async list(
57+
client: ApiClient,
58+
req: ListRequest,
59+
_token?: CancellationToken
60+
) {
61+
return (await new Repos(client).paginatedList(req, _token)).repos;
62+
}
63+
64+
static async fromPath(
65+
client: ApiClient,
66+
path: string,
67+
_token?: CancellationToken
68+
) {
69+
const repos = await this.list(
70+
client,
71+
{
72+
path_prefix: path,
73+
},
74+
_token
75+
);
76+
77+
if (repos.length !== 1) {
78+
throw new RepoError(`${repos.length} repos match prefix ${path}`);
79+
}
80+
81+
return repos[0];
1682
}
1783
}

packages/databricks-vscode/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@
172172
"command": "databricks.cluster.stop",
173173
"title": "Stop Cluster",
174174
"icon": "$(debug-stop)"
175+
},
176+
{
177+
"command": "databricks.utils.openExternal",
178+
"title": "Open link externally",
179+
"icon": "$(link)"
175180
}
176181
],
177182
"viewsContainers": {
@@ -234,7 +239,7 @@
234239
{
235240
"command": "databricks.connection.attachCluster",
236241
"when": "view == clusterView",
237-
"group": "inline@0"
242+
"group": "inline@1"
238243
},
239244
{
240245
"command": "databricks.connection.configureProject",
@@ -280,6 +285,11 @@
280285
"command": "databricks.cluster.start",
281286
"when": "view == configurationView && viewItem == clusterStopped",
282287
"group": "inline@0"
288+
},
289+
{
290+
"command": "databricks.utils.openExternal",
291+
"when": "view == configurationView && viewItem =~ /^.*databricks-link.*$/ || view == clusterView && viewItem =~ /^.*databricks-link.*$/",
292+
"group": "inline@0"
283293
}
284294
],
285295
"editor/title/run": [

packages/databricks-vscode/src/cli/BricksTasks.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {ApiClient, Repo} from "@databricks/databricks-sdk";
12
import * as assert from "assert";
23
import {anything, instance, mock, when, verify} from "ts-mockito";
34
import {ProcessExecution, Uri} from "vscode";
@@ -33,19 +34,19 @@ describe(__filename, () => {
3334
assert.deepEqual(task.problemMatchers, ["$bricks-sync"]);
3435
});
3536

36-
it("should lazily create a process execution", () => {
37+
it("should lazily create a process execution", async () => {
3738
let connectionMock = mock(ConnectionManager);
39+
const testSyncDestination = new SyncDestination(
40+
instance(mock(Repo)),
41+
Uri.from({
42+
scheme: "dbws",
43+
path: "/Workspace/notebook-best-practices",
44+
}),
45+
Uri.file("/Desktop/notebook-best-practices")
46+
);
3847
when(connectionMock.profile).thenReturn("DEFAULT");
3948
when(connectionMock.me).thenReturn("fabian.jakobs@databricks.com");
40-
when(connectionMock.syncDestination).thenReturn(
41-
new SyncDestination(
42-
Uri.from({
43-
scheme: "dbws",
44-
path: "/Workspace/notebook-best-practices",
45-
}),
46-
Uri.file("/Desktop/notebook-best-practices")
47-
)
48-
);
49+
when(connectionMock.syncDestination).thenReturn(testSyncDestination);
4950

5051
let cliMock = mock(CliWrapper);
5152
when(cliMock.getSyncCommand(anything())).thenReturn({

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {promisify} from "node:util";
55
import {execFile as execFileCb} from "node:child_process";
66

77
import {CliWrapper} from "./CliWrapper";
8+
import {instance, mock} from "ts-mockito";
9+
import {ApiClient, Repo} from "@databricks/databricks-sdk";
810

911
const execFile = promisify(execFileCb);
1012

@@ -15,13 +17,14 @@ describe(__filename, () => {
1517
assert.ok(result.stdout.indexOf("bricks") > 0);
1618
});
1719

18-
it("should create sync command", () => {
20+
it("should create sync command", async () => {
1921
const cli = new CliWrapper({
2022
asAbsolutePath(path: string) {
2123
return path;
2224
},
2325
} as any);
2426
const mapper = new SyncDestination(
27+
instance(mock(Repo)),
2528
Uri.from({
2629
scheme: "dbws",
2730
path: "/Workspace/Repos/fabian.jakobs@databricks.com/notebook-best-practices",

packages/databricks-vscode/src/cluster/ClusterListDataProvider.test.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe(__filename, () => {
3333
let mockedClusterModel: ClusterModel;
3434
let disposables: Array<Disposable>;
3535
let onModelChangeListener: () => void;
36+
let mockApiClient: ApiClient;
3637

3738
beforeEach(() => {
3839
disposables = [];
@@ -44,12 +45,15 @@ describe(__filename, () => {
4445
dispose() {},
4546
};
4647
});
47-
let apiClient = instance(mock(ApiClient));
48+
mockApiClient = mock(ApiClient);
4849
when(mockedClusterModel.roots).thenReturn(
4950
mockListClustersResponse.clusters!.map(
50-
(m: any) => new Cluster(apiClient, m)
51+
(m: any) => new Cluster(instance(mockApiClient), m)
5152
)
5253
);
54+
when(mockApiClient.host).thenResolve(
55+
new URL("https://www.example.com")
56+
);
5357
});
5458

5559
afterEach(() => {
@@ -89,28 +93,36 @@ describe(__filename, () => {
8993
disposables.push(provider);
9094

9195
let cluster = new Cluster(
92-
instance(mock(ApiClient)),
96+
instance(mockApiClient),
9397
mockListClustersResponse.clusters![0]
9498
);
9599
let children = await resolveProviderResult(
96100
provider.getChildren(cluster)
97101
);
98102
assert(children);
99-
assert.equal(children.length, 4);
103+
assert.equal(children.length, 5);
100104
});
101105

102-
it("should get cluster tree node items", () => {
106+
it("should get cluster tree node items", async () => {
103107
let cluster = new Cluster(
104-
instance(mock(ApiClient)),
108+
instance(mockApiClient),
105109
mockListClustersResponse.clusters![0]
106110
);
107111

108-
let items = ClusterListDataProvider.clusterNodeToTreeItems(cluster);
112+
let items = await ClusterListDataProvider.clusterNodeToTreeItems(
113+
cluster
114+
);
109115
assert.deepEqual(items, [
110116
{
111117
description: "cluster-id-2",
112118
label: "Cluster ID:",
113119
},
120+
{
121+
contextValue: "databricks-link",
122+
description:
123+
"www.example.com/#setting/clusters/cluster-id-2/configuration",
124+
label: "URL:",
125+
},
114126
{
115127
description: "Spark 3.2.1",
116128
label: "Spark version:",

packages/databricks-vscode/src/cluster/ClusterListDataProvider.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,19 @@ export class ClusterListDataProvider
107107
* in the TreeItems match the information presented in cluster list
108108
* of the Databricks webapp.
109109
*/
110-
public static clusterNodeToTreeItems(element: Cluster): Array<TreeItem> {
111-
let children = [
110+
public static async clusterNodeToTreeItems(
111+
element: Cluster
112+
): Promise<Array<TreeItem>> {
113+
let children: TreeItem[] = [
112114
{
113115
label: "Cluster ID:",
114116
description: element.id,
115117
},
118+
{
119+
label: "URL:",
120+
description: await element.url,
121+
contextValue: "databricks-link",
122+
},
116123
];
117124
if (element.cores) {
118125
children.push({

0 commit comments

Comments
 (0)