Skip to content

Commit 824c003

Browse files
committed
Better support for server definitions defined at the workspace folder level
1 parent b83438b commit 824c003

File tree

5 files changed

+144
-71
lines changed

5 files changed

+144
-71
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@
334334
{
335335
"command": "vscode-objectscript.openISCDocument",
336336
"when": "vscode-objectscript.connectActive && workspaceFolderCount != 0"
337+
},
338+
{
339+
"command": "vscode-objectscript.connectFolderToServerNamespace",
340+
"when": "!vscode-objectscript.connectActive && vscode-objectscript.explorerRootCount == 0 && workspaceFolderCount != 0"
337341
}
338342
],
339343
"view/title": [

src/api/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,29 @@ export class AtelierAPI {
140140
this.namespace = namespace;
141141
}
142142

143+
/**
144+
* Manually set the connection spec for this object,
145+
* where `connSpec` is the return value of `getResolvedConnectionSpec()`.
146+
*/
147+
public setConnSpec(serverName: string, connSpec: any): void {
148+
const {
149+
webServer: { scheme, host, port, pathPrefix = "" },
150+
username,
151+
password,
152+
} = connSpec;
153+
this._config.username = username;
154+
this._config.password = password;
155+
this._config.https = scheme == "https";
156+
this._config.host = host;
157+
this._config.port = port;
158+
this._config.pathPrefix = pathPrefix;
159+
this._config.apiVersion = this._config.apiVersion || DEFAULT_API_VERSION;
160+
this._config.serverVersion = this._config.serverVersion || DEFAULT_SERVER_VERSION;
161+
this._config.docker = false;
162+
this._config.active = true;
163+
this._config.serverName = serverName;
164+
}
165+
143166
public get active(): boolean {
144167
const { host = "", port = 0 } = this.config;
145168
return !!this._config.active && host.length > 0 && port > 0;
@@ -185,7 +208,7 @@ export class AtelierAPI {
185208
this.configName = workspaceFolderName;
186209
const conn = config("conn", workspaceFolderName);
187210
let serverName = workspaceFolderName.toLowerCase();
188-
if (config("intersystems.servers").has(serverName)) {
211+
if (config("intersystems.servers", workspaceFolderName).has(serverName)) {
189212
this.externalServer = true;
190213
} else if (
191214
!(conn["docker-compose"] && extensionContext.extension.extensionKind !== vscode.ExtensionKind.Workspace) &&

src/commands/addServerNamespaceToWorkspace.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
FILESYSTEM_SCHEMA,
88
FILESYSTEM_READONLY_SCHEMA,
99
filesystemSchemas,
10-
smExtensionId,
10+
serverManagerApi,
1111
} from "../extension";
1212
import { cspAppsForUri, getWsFolder, handleError, notIsfs } from "../utils";
1313
import { pickProject } from "./project";
@@ -18,7 +18,6 @@ import { isfsConfig, IsfsUriParam } from "../utils/FileProviderUtil";
1818
* @returns An object containing `serverName` and `namespace`, or `undefined`.
1919
*/
2020
export async function pickServerAndNamespace(message?: string): Promise<{ serverName: string; namespace: string }> {
21-
const serverManagerApi = await getServerManagerApi();
2221
if (!serverManagerApi) {
2322
vscode.window.showErrorMessage(
2423
`${
@@ -168,22 +167,6 @@ export async function addServerNamespaceToWorkspace(resource?: vscode.Uri): Prom
168167
}
169168
}
170169

171-
export async function getServerManagerApi(): Promise<any> {
172-
const targetExtension = vscode.extensions.getExtension(smExtensionId);
173-
if (!targetExtension) {
174-
return undefined;
175-
}
176-
if (!targetExtension.isActive) {
177-
await targetExtension.activate();
178-
}
179-
const api = targetExtension.exports;
180-
181-
if (!api) {
182-
return undefined;
183-
}
184-
return api;
185-
}
186-
187170
/** Prompt the user to fill in the `path` and `query` of `uri`. */
188171
async function modifyWsFolderUri(uri: vscode.Uri): Promise<vscode.Uri | undefined> {
189172
if (notIsfs(uri)) return;
Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as vscode from "vscode";
22
import { AtelierAPI } from "../api";
3-
import { panel, resolveConnectionSpec, getResolvedConnectionSpec, smExtensionId } from "../extension";
4-
import { notIsfs } from "../utils";
3+
import {
4+
panel,
5+
resolveConnectionSpec,
6+
getResolvedConnectionSpec,
7+
serverManagerApi,
8+
resolveUsernameAndPassword,
9+
} from "../extension";
10+
import { handleError, isUnauthenticated, notIsfs } from "../utils";
511

612
interface ConnSettings {
713
server: string;
@@ -14,7 +20,6 @@ export async function connectFolderToServerNamespace(): Promise<void> {
1420
vscode.window.showErrorMessage("No folders in the workspace.", "Dismiss");
1521
return;
1622
}
17-
const serverManagerApi = await getServerManagerApi();
1823
if (!serverManagerApi) {
1924
vscode.window.showErrorMessage(
2025
"Connecting a folder to a server namespace requires the [InterSystems Server Manager extension](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) to be installed and enabled.",
@@ -31,42 +36,71 @@ export async function connectFolderToServerNamespace(): Promise<void> {
3136
return {
3237
label: folder.name,
3338
description: folder.uri.fsPath,
34-
detail: !conn.server ? undefined : `Currently connected to ${conn.ns} on ${conn.server}`,
39+
detail:
40+
!conn.server || !conn.active
41+
? "No active server connection"
42+
: `Currently connected to ${conn.ns} on ${conn.server}`,
3543
};
3644
});
3745
if (!items.length) {
3846
vscode.window.showErrorMessage("No local folders in the workspace.", "Dismiss");
3947
return;
4048
}
4149
const pick =
42-
items.length === 1 && !items[0].detail
50+
items.length == 1 && !items[0].detail.startsWith("Currently")
4351
? items[0]
4452
: await vscode.window.showQuickPick(items, { title: "Pick a folder" });
4553
const folder = vscode.workspace.workspaceFolders.find((el) => el.name === pick.label);
4654
// Get user's choice of server
4755
const options: vscode.QuickPickOptions = {};
48-
const serverName: string = await serverManagerApi.pickServer(undefined, options);
56+
const serverName: string = await serverManagerApi.pickServer(folder, options);
4957
if (!serverName) {
5058
return;
5159
}
52-
// Get its namespace list
53-
const uri = vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`);
54-
await resolveConnectionSpec(serverName);
60+
await resolveConnectionSpec(serverName, undefined, folder);
5561
// Prepare a displayable form of its connection spec as a hint to the user
5662
// This will never return the default value (second parameter) because we only just resolved the connection spec.
5763
const connSpec = getResolvedConnectionSpec(serverName, undefined);
5864
const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix}`;
5965
// Connect and fetch namespaces
60-
const api = new AtelierAPI(uri);
61-
const allNamespaces: string[] | undefined = await api
66+
const api = new AtelierAPI(vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`));
67+
const serverConf = vscode.workspace
68+
.getConfiguration("intersystems", folder)
69+
.inspect<{ [key: string]: any }>("servers");
70+
if (
71+
serverConf.workspaceFolderValue &&
72+
typeof serverConf.workspaceFolderValue[serverName] == "object" &&
73+
!(serverConf.workspaceValue && typeof serverConf.workspaceValue[serverName] == "object")
74+
) {
75+
// Need to manually set connection info if the server is defined at the workspace folder level
76+
api.setConnSpec(serverName, connSpec);
77+
}
78+
const allNamespaces: string[] = await api
6279
.serverInfo(false)
6380
.then((data) => data.result.content.namespaces)
64-
.catch((reason) => {
65-
// Notify user about serverInfo failure
66-
vscode.window.showErrorMessage(
67-
reason.message || `Failed to fetch namespace list from server at ${connDisplayString}`,
68-
"Dismiss"
69-
);
81+
.catch(async (error) => {
82+
if (error?.statusCode == 401 && isUnauthenticated(api.config.username)) {
83+
// Attempt to resolve username and password and try again
84+
const newSpec = await resolveUsernameAndPassword(api.config.serverName, connSpec);
85+
if (newSpec) {
86+
// We were able to resolve credentials, so try again
87+
api.setConnSpec(api.config.serverName, newSpec);
88+
return api
89+
.serverInfo(false)
90+
.then((data) => data.result.content.namespaces)
91+
.catch(async (err) => {
92+
handleError(err, `Failed to fetch namespace list from server at ${connDisplayString}.`);
93+
return undefined;
94+
});
95+
} else {
96+
handleError(
97+
`Unauthenticated access rejected by '${api.serverId}'.`,
98+
`Failed to fetch namespace list from server at ${connDisplayString}.`
99+
);
100+
return undefined;
101+
}
102+
}
103+
handleError(error, `Failed to fetch namespace list from server at ${connDisplayString}.`);
70104
return undefined;
71105
});
72106
// Clear the panel entry created by the connection
@@ -90,22 +124,34 @@ export async function connectFolderToServerNamespace(): Promise<void> {
90124
}
91125
// Update folder's config object
92126
const config = vscode.workspace.getConfiguration("objectscript", folder);
93-
const conn: any = config.inspect("conn").workspaceFolderValue;
94-
await config.update("conn", { ...conn, server: serverName, ns: namespace, active: true });
95-
}
96-
97-
async function getServerManagerApi(): Promise<any> {
98-
const targetExtension = vscode.extensions.getExtension(smExtensionId);
99-
if (!targetExtension) {
100-
return undefined;
101-
}
102-
if (!targetExtension.isActive) {
103-
await targetExtension.activate();
104-
}
105-
const api = targetExtension.exports;
106-
107-
if (!api) {
108-
return undefined;
127+
if (vscode.workspace.workspaceFile && items.length == 1) {
128+
// Ask the user if they want to enable the connection at the workspace or folder level.
129+
// Only allow this when there is a single client-side folder in the workspace because
130+
// the server may be configured at the workspace folder level.
131+
const answer = await vscode.window.showQuickPick(
132+
[
133+
{ label: `Workspace Folder ${folder.name}`, detail: folder.uri.toString(true) },
134+
{ label: "Workspace File", detail: vscode.workspace.workspaceFile.toString(true) },
135+
],
136+
{ title: "Store the server connection at the workspace or folder level?" }
137+
);
138+
if (!answer) return;
139+
if (answer.label == "Workspace File") {
140+
// Enable the connection at the workspace level
141+
const conn: any = config.inspect("conn").workspaceValue;
142+
await config.update(
143+
"conn",
144+
{ ...conn, server: serverName, ns: namespace, active: true },
145+
vscode.ConfigurationTarget.Workspace
146+
);
147+
return;
148+
}
109149
}
110-
return api;
150+
// Enable the connection at the workspace folder level
151+
const conn: any = config.inspect("conn").workspaceFolderValue;
152+
await config.update(
153+
"conn",
154+
{ ...conn, server: serverName, ns: namespace, active: true },
155+
vscode.ConfigurationTarget.WorkspaceFolder
156+
);
111157
}

src/extension.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ let reporter: TelemetryReporter;
216216

217217
export let checkingConnection = false;
218218

219-
let serverManagerApi: serverManager.ServerManagerAPI;
219+
export let serverManagerApi: serverManager.ServerManagerAPI;
220220

221221
/** Map of the intersystems.server connection specs we have resolved via the API to that extension */
222222
const resolvedConnSpecs = new Map<string, any>();
@@ -227,22 +227,26 @@ const resolvedConnSpecs = new Map<string, any>();
227227
* @param serverName authority element of an isfs uri, or `objectscript.conn.server` property, or the name of a root folder with an `objectscript.conn.docker-compose` property object
228228
* @param uri if passed, re-check the `objectscript.conn.docker-compose` case in case servermanager API couldn't do that because we're still running our own `activate` method.
229229
*/
230-
export async function resolveConnectionSpec(serverName: string, uri?: vscode.Uri): Promise<void> {
230+
export async function resolveConnectionSpec(
231+
serverName: string,
232+
uri?: vscode.Uri,
233+
scope?: vscode.ConfigurationScope
234+
): Promise<void> {
231235
if (!serverManagerApi || !serverManagerApi.getServerSpec || serverName === "") {
232236
return;
233237
}
234238
if (resolvedConnSpecs.has(serverName)) {
235239
// Already resolved
236240
return;
237241
}
238-
if (!vscode.workspace.getConfiguration("intersystems.servers", null).has(serverName)) {
242+
if (!vscode.workspace.getConfiguration("intersystems.servers", scope).has(serverName)) {
239243
// When not a defined server see it already resolved as a foldername that matches case-insensitively
240244
if (getResolvedConnectionSpec(serverName, undefined)) {
241245
return;
242246
}
243247
}
244248

245-
let connSpec = await serverManagerApi.getServerSpec(serverName);
249+
let connSpec = await serverManagerApi.getServerSpec(serverName, scope);
246250

247251
if (!connSpec && uri) {
248252
// Caller passed uri as a signal to process any docker-compose settings
@@ -305,6 +309,24 @@ async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promi
305309
}
306310
}
307311

312+
/** Resolve credentials for `serverName` and returned the complete connection spec if successful */
313+
export async function resolveUsernameAndPassword(serverName: string, oldSpec: any): Promise<any> {
314+
const newSpec: { name: string; username?: string; password?: string } = {
315+
name: serverName,
316+
username: oldSpec?.username,
317+
};
318+
await resolvePassword(newSpec, true);
319+
if (newSpec.password) {
320+
// Update the connection spec
321+
resolvedConnSpecs.set(serverName, {
322+
...oldSpec,
323+
username: newSpec.username,
324+
password: newSpec.password,
325+
});
326+
return resolvedConnSpecs.get(serverName);
327+
}
328+
}
329+
308330
/** Accessor for the cache of resolved connection specs */
309331
export function getResolvedConnectionSpec(key: string, dflt: any): any {
310332
let spec = resolvedConnSpecs.get(key);
@@ -462,25 +484,20 @@ export async function checkConnection(
462484
if (isUnauthenticated(username)) {
463485
vscode.window.showErrorMessage(
464486
`Unauthenticated access rejected by '${api.serverId}'.${
465-
!api.externalServer ? " Connection has been disabled." : ""
487+
!api.config.serverName ? " Connection has been disabled." : ""
466488
}`,
467489
"Dismiss"
468490
);
469-
if (api.externalServer) {
491+
if (api.config.serverName) {
470492
// Attempt to resolve a username and password
471-
const newSpec: { name: string; username?: string; password?: string } = {
472-
name: api.config.serverName,
473-
username,
474-
};
475-
await resolvePassword(newSpec, true);
476-
if (newSpec.password) {
477-
// Update the connection spec and try again
493+
const oldSpec = getResolvedConnectionSpec(
494+
api.config.serverName,
495+
vscode.workspace.getConfiguration("intersystems.servers", uri).get(api.config.serverName)
496+
);
497+
const newSpec = await resolveUsernameAndPassword(api.config.serverName, oldSpec);
498+
if (newSpec) {
499+
// We were able to resolve credentials, so try again
478500
await workspaceState.update(wsKey + ":password", newSpec.password);
479-
resolvedConnSpecs.set(api.config.serverName, {
480-
...resolvedConnSpecs.get(api.config.serverName),
481-
username: newSpec.username,
482-
password: newSpec.password,
483-
});
484501
api = new AtelierAPI(apiTarget, false);
485502
await api
486503
.serverInfo(true, serverInfoTimeout)

0 commit comments

Comments
 (0)