From 1c5b2e0f77c91aea9a93e828aa1f1b41bb7f76c5 Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Thu, 15 Apr 2021 19:00:51 +0100 Subject: [PATCH] WIP --- package.json | 16 ++-- src/api/getServerSpec.ts | 43 ++++++++--- src/commands/managePasswords.ts | 8 +- src/extension.ts | 6 ++ src/ui/serverManagerView.ts | 127 ++++++++++++++++++++++---------- 5 files changed, 146 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 5290ca4..315108e 100644 --- a/package.json +++ b/package.json @@ -226,28 +226,29 @@ }, { "command": "intersystems-community.servermanager.addToStarred", - "category": "InterSystems Server Manager", "title": "Add to Starred", "icon": "$(star-full)" }, { "command": "intersystems-community.servermanager.removeFromStarred", - "category": "InterSystems Server Manager", "title": "Remove from Starred", "icon": "$(star-empty)" }, { "command": "intersystems-community.servermanager.openPortalExternal", - "category": "InterSystems Server Manager", "title": "Open Management Portal in External Browser", "icon": "$(link-external)" }, { "command": "intersystems-community.servermanager.openPortalTab", - "category": "InterSystems Server Manager", "title": "Open Management Portal in Tab", "icon": "$(tools)" }, + { + "command": "intersystems-community.servermanager.retryServer", + "title": "Reconnect", + "icon": "$(refresh)" + }, { "command": "intersystems-community.servermanager.editSettings", "category": "InterSystems Server Manager", @@ -268,7 +269,7 @@ { "command": "intersystems-community.servermanager.importServers", "category": "InterSystems Server Manager", - "title": "Import Servers from Registry" + "title": "Import Servers from Windows Registry" }, { "command": "intersystems-community.servermanager.setIconRed", @@ -401,6 +402,11 @@ "when": "view == intersystems-community_servermanager && viewItem == starred.server.starred", "group": "inline@10" }, + { + "command": "intersystems-community.servermanager.retryServer", + "when": "view == intersystems-community_servermanager && viewItem =~ /offline$/", + "group": "inline@10" + }, { "command": "intersystems-community.servermanager.editNamespace", "when": "view == intersystems-community_servermanager && viewItem =~ /namespace$/", diff --git a/src/api/getServerSpec.ts b/src/api/getServerSpec.ts index 1f18347..802a19f 100644 --- a/src/api/getServerSpec.ts +++ b/src/api/getServerSpec.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { filePassword } from '../commands/managePasswords'; import { ServerSpec } from '../extension'; import { Keychain } from '../keychain'; @@ -82,16 +83,38 @@ export async function getServerSpec(name: string, scope?: vscode.ConfigurationSc } if (server.username && !server.password) { - await vscode.window - .showInputBox({ - password: true, - placeHolder: `Password for user '${server.username}' on InterSystems server '${name}'`, - validateInput: (value => { - return value.length > 0 ? '' : 'Mandatory field'; - }), - ignoreFocusOut: true, - }) - .then((password) => { + const doInputBox = async (): Promise => { + return await new Promise((resolve, reject) => { + const inputBox = vscode.window.createInputBox(); + inputBox.password = true, + inputBox.title = `Password for InterSystems server '${name}'`, + inputBox.placeholder = `Password for user '${server?.username}' on '${name}'`, + inputBox.prompt = 'To store your password securely, submit it using the $(key) button', + inputBox.ignoreFocusOut = true, + inputBox.buttons = [{ iconPath: new vscode.ThemeIcon('key'), tooltip: 'Store Password in Keychain' }] + + async function done(store: boolean) { + // File the password and return it + if (store) { + await filePassword(name, inputBox.value) + } + // Resolve the promise and tidy up + resolve(inputBox.value); + inputBox.hide(); + inputBox.dispose(); + } + + inputBox.onDidTriggerButton((button) => { + // We only added one button + done(true); + }); + inputBox.onDidAccept(() => { + done(false); + }); + inputBox.show(); + }) + }; + await doInputBox().then((password) => { if (password && server) { server.password = password; credentialCache[name] = {username: server.username, password: password}; diff --git a/src/commands/managePasswords.ts b/src/commands/managePasswords.ts index 1f37296..3a6fef6 100644 --- a/src/commands/managePasswords.ts +++ b/src/commands/managePasswords.ts @@ -24,8 +24,7 @@ export async function storePassword(treeItem?: ServerTreeItem): Promise }) .then((password) => { if (password) { - credentialCache[name] = undefined; - new Keychain(name).setPassword(password).then(() => { + filePassword(name, password).then(() => { vscode.window.showInformationMessage(`Password for '${name}' stored in keychain.`); }); reply = name; @@ -35,6 +34,11 @@ export async function storePassword(treeItem?: ServerTreeItem): Promise return reply; } +export async function filePassword(serverName: string, password: string): Promise { + credentialCache[serverName] = undefined; + return new Keychain(serverName).setPassword(password).then(() => true, () => false); +} + export async function clearPassword(treeItem?: ServerTreeItem): Promise { if (treeItem && !getServerNames().some((value) => value.name === treeItem?.label)) { treeItem = undefined; diff --git a/src/extension.ts b/src/extension.ts index b9847f6..74e32cf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -187,6 +187,12 @@ export function activate(context: vscode.ExtensionContext) { }) ); + context.subscriptions.push( + vscode.commands.registerCommand(`${extensionId}.retryServer`, () => { + view.refreshTree(); + }) + ); + const addWorkspaceFolderAsync = async (readonly: boolean, namespaceTreeItem?: ServerTreeItem) => { if (namespaceTreeItem) { const pathParts = namespaceTreeItem.id?.split(':'); diff --git a/src/ui/serverManagerView.ts b/src/ui/serverManagerView.ts index ba2a07f..4de8d9d 100644 --- a/src/ui/serverManagerView.ts +++ b/src/ui/serverManagerView.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { getServerNames } from '../api/getServerNames'; -import { getServerSpec } from '../api/getServerSpec'; +import { credentialCache, getServerSpec } from '../api/getServerSpec'; import { getServerSummary } from '../api/getServerSummary'; import { ServerName } from '../extension'; import { makeRESTRequest } from '../makeRESTRequest'; @@ -23,6 +23,8 @@ export class ServerManagerView { private _globalState: vscode.Memento; + private _treeView: vscode.TreeView; + private _treeDataProvider: SMNodeProvider; constructor(context: vscode.ExtensionContext) { @@ -31,6 +33,7 @@ export class ServerManagerView { this._treeDataProvider = treeDataProvider; const treeView = vscode.window.createTreeView('intersystems-community_servermanager', { treeDataProvider, showCollapseAll: true }) + this._treeView = treeView; context.subscriptions.push(treeView); treeDataProvider.view = treeView; @@ -115,36 +118,39 @@ class SMNodeProvider implements vscode.TreeDataProvider { } getParent(element: SMTreeItem): SMTreeItem | undefined { + // This is a hack to allow reveal() to work on the first-level folders, + // so we can open one of them automatically at startup. + // TODO implement it properly for all items. return undefined; } - async getChildren(element?: SMTreeItem): Promise { + async getChildren(element?: SMTreeItem): Promise { const children: SMTreeItem[] = []; if (!element) { // Root folders let firstRevealId = favoritesMap.size > 0 ? 'starred' : recentsArray.length > 0 ? 'recent' : 'sorted'; if (vscode.workspace.workspaceFolders?.length || 0 > 0) { - children.push(new SMTreeItem({label: 'Current', id: 'current', tooltip: 'Servers referenced by current workspace', codiconName: 'home', getChildren: currentServers})); + children.push(new SMTreeItem({parent: element, label: 'Current', id: 'current', tooltip: 'Servers referenced by current workspace', codiconName: 'home', getChildren: currentServers})); firstRevealId = 'current'; this._firstRevealItem = children[children.length - 1]; } if (favoritesMap.size > 0) { - children.push(new SMTreeItem({label: 'Starred', id: 'starred', tooltip: 'Favorite servers', codiconName: 'star-full', getChildren: favoriteServers})); + children.push(new SMTreeItem({parent: element, label: 'Starred', id: 'starred', tooltip: 'Favorite servers', codiconName: 'star-full', getChildren: favoriteServers})); if (firstRevealId === 'starred') { this._firstRevealItem = children[children.length - 1]; } } - children.push(new SMTreeItem({label: 'Recent', id: 'recent', tooltip: 'Recently used servers', codiconName: 'history', getChildren: recentServers})); + children.push(new SMTreeItem({parent: element, label: 'Recent', id: 'recent', tooltip: 'Recently used servers', codiconName: 'history', getChildren: recentServers})); if (firstRevealId === 'recent') { this._firstRevealItem = children[children.length - 1]; } // TODO - use this when we can implement resequencing in the UI - // children.push(new SMTreeItem({label: 'Ordered', id: 'ordered', tooltip: 'All servers in settings.json order', codiconName: 'list-ordered', getChildren: allServers, params: {sorted: false}})); + // children.push(new SMTreeItem({parent: element, label: 'Ordered', id: 'ordered', tooltip: 'All servers in settings.json order', codiconName: 'list-ordered', getChildren: allServers, params: {sorted: false}})); - children.push(new SMTreeItem({label: 'All Servers', id: 'sorted', tooltip: 'All servers in alphabetical order', codiconName: 'server-environment', getChildren: allServers, params: {sorted: true}})); + children.push(new SMTreeItem({parent: element, label: 'All Servers', id: 'sorted', tooltip: 'All servers in alphabetical order', codiconName: 'server-environment', getChildren: allServers, params: {sorted: true}})); if (firstRevealId === 'sorted') { this._firstRevealItem = children[children.length - 1]; } @@ -168,12 +174,13 @@ class SMNodeProvider implements vscode.TreeDataProvider { interface SMItem { label: string, id: string, + parent: SMTreeItem | undefined, contextValue?: string, tooltip?: string | vscode.MarkdownString, description?: string, codiconName?: string, getChildren?: Function, - params?: any + params?: any, } class SMTreeItem extends vscode.TreeItem { @@ -181,11 +188,14 @@ class SMTreeItem extends vscode.TreeItem { private readonly _getChildren?: Function; private readonly _params?: any; + readonly parent: SMTreeItem | undefined; + constructor(item: SMItem) { const collapsibleState = item.getChildren ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; super(item.label, collapsibleState); this.id = item.id; + this.parent = item.parent; this.contextValue = item.contextValue; this.tooltip = item.tooltip; this.description = item.description; @@ -196,17 +206,17 @@ class SMTreeItem extends vscode.TreeItem { this._params = item.params; } - public async getChildren(): Promise { + public async getChildren(): Promise { if (this._getChildren) { - return await this._getChildren(this, this._params) || []; + return await this._getChildren(this, this._params); } else { - return []; + return; } } } -function allServers(element: SMTreeItem, params?: any): ServerTreeItem[] { +function allServers(treeItem: SMTreeItem, params?: any): ServerTreeItem[] { const children: ServerTreeItem[] = []; const getAllServers = (sorted?: boolean): ServerTreeItem[] => { let serverNames = getServerNames(); @@ -214,7 +224,7 @@ function allServers(element: SMTreeItem, params?: any): ServerTreeItem[] { serverNames = serverNames.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); } return serverNames.map((serverName) => { - return new ServerTreeItem(sorted ? 'sorted' : 'ordered', serverName); + return new ServerTreeItem({ label: serverName.name, id:serverName.name, parent: treeItem }, serverName); }) } @@ -230,7 +240,7 @@ function currentServers(element: SMTreeItem, params?: any): ServerTreeItem[] { if (['isfs', 'isfs-readonly'].includes(folder.uri.scheme)) { const serverSummary = getServerSummary(serverName); if (serverSummary) { - children.set(serverName, new ServerTreeItem('current', serverSummary)); + children.set(serverName, new ServerTreeItem({ parent: element, label: serverName, id: serverName }, serverSummary)); } } const conn = vscode.workspace.getConfiguration('objectscript.conn', folder); @@ -238,7 +248,7 @@ function currentServers(element: SMTreeItem, params?: any): ServerTreeItem[] { if (connServer) { const serverSummary = getServerSummary(connServer); if (serverSummary) { - children.set(connServer, new ServerTreeItem('current', serverSummary)); + children.set(connServer, new ServerTreeItem({ parent: element, label: serverName, id: serverName }, serverSummary)); } } }); @@ -252,7 +262,7 @@ function favoriteServers(element: SMTreeItem, params?: any): ServerTreeItem[] { favoritesMap.forEach((_, name) => { const serverSummary = getServerSummary(name); if (serverSummary) { - children.push(new ServerTreeItem('starred', serverSummary)); + children.push(new ServerTreeItem({ parent: element, label: name, id: name}, serverSummary)); } }); @@ -265,7 +275,7 @@ function recentServers(element: SMTreeItem, params?: any): ServerTreeItem[] { recentsArray.map((name) => { const serverSummary = getServerSummary(name); if (serverSummary) { - children.push(new ServerTreeItem('recent', serverSummary)); + children.push(new ServerTreeItem({ parent: element, label: name, id: name}, serverSummary)); } }); @@ -275,17 +285,20 @@ function recentServers(element: SMTreeItem, params?: any): ServerTreeItem[] { export class ServerTreeItem extends SMTreeItem { public readonly name: string; constructor( - parentFolderId: string, - serverName: ServerName + element: SMItem, + serverSummary: ServerName ) { + const parentFolderId = element.parent?.id || ""; // Wrap detail (a uri string) as a null link to prevent it from being linkified super({ - label: serverName.name, - id: parentFolderId + ':' + serverName.name, - tooltip: new vscode.MarkdownString(`[${serverName.detail}]()`).appendMarkdown(serverName.description ? `\n\n*${serverName.description}*` : ''), - getChildren: serverFeatures + parent: element.parent, + label: serverSummary.name, + id: parentFolderId + ':' + serverSummary.name, + tooltip: new vscode.MarkdownString(`[${serverSummary.detail}]()`).appendMarkdown(serverSummary.description ? `\n\n*${serverSummary.description}*` : ''), + getChildren: serverFeatures, + params: { serverSummary } }); - this.name = serverName.name; + this.name = serverSummary.name; //this.command = {command: 'intersystems-community.servermanager.openPortalTab', title: 'Open Management Portal in Simple Browser Tab', arguments: [this]}; this.contextValue = `${parentFolderId}.server.${favoritesMap.has(this.name) ? 'starred' : ''}`; const color = colorsMap.get(this.name); @@ -294,7 +307,7 @@ export class ServerTreeItem extends SMTreeItem { } /** - * getChildren function returning starred servers, + * getChildren function returning server features (the child nodes of a server), * * @param element parent * @param params (unused) @@ -303,21 +316,56 @@ export class ServerTreeItem extends SMTreeItem { async function serverFeatures(element: ServerTreeItem, params?: any): Promise { const children: FeatureTreeItem[] = []; - children.push(new NamespacesTreeItem(element.id || '', element.name)); - + if (params?.serverSummary) { + const name = params.serverSummary.name; + const serverSpec = await getServerSpec(name) + if (!serverSpec) { + return undefined + } + + const response = await makeRESTRequest("HEAD", serverSpec) + if (!response) { + children.push(new OfflineTreeItem({ parent: element, label: name, id: name }, element.name)); + credentialCache[name] = undefined; + } + else { + children.push(new NamespacesTreeItem({ parent: element, label: name, id: name }, element.name)); + } + } return children; } export class FeatureTreeItem extends SMTreeItem { } +export class OfflineTreeItem extends FeatureTreeItem { + public readonly name: string; + constructor( + element: SMItem, + serverName: string + ) { + const parentFolderId = element.parent?.id || ''; + super({ + parent: element.parent, + label: `Offline at ${new Date().toLocaleTimeString()}`, + id: parentFolderId + ':offline', + tooltip: `Server could not be reached`, + }); + this.name = 'offline'; + this.contextValue = 'offline'; + this.iconPath = new vscode.ThemeIcon('warning'); + } +} + export class NamespacesTreeItem extends FeatureTreeItem { public readonly name: string; constructor( - parentFolderId: string, + element: SMItem, serverName: string ) { + const parentFolderId = element.parent?.id || ''; super({ + parent: element.parent, label: 'Namespaces', id: parentFolderId + ':namespaces', tooltip: `Namespaces you can access`, @@ -341,17 +389,20 @@ export class NamespacesTreeItem extends FeatureTreeItem { const children: NamespaceTreeItem[] = []; if (params?.serverName) { - const server = await getServerSpec(params.serverName) - if (!server) { + const name: string = params.serverName; + const serverSpec = await getServerSpec(name) + if (!serverSpec) { return undefined } - const response = await makeRESTRequest("GET", server) - - if (response) { - - response.data.result.content.namespaces.map((name) => { - children.push(new NamespaceTreeItem(element.id || '', name, server.name)); + const response = await makeRESTRequest("GET", serverSpec) + if (!response) { + children.push(new OfflineTreeItem({ parent: element, label: name, id: name }, element.name)); + credentialCache[params.serverName] = undefined; + } + else { + response.data.result.content.namespaces.map((namespace) => { + children.push(new NamespaceTreeItem({ parent: element, label: name, id: name }, namespace, name)); }); } } @@ -362,12 +413,14 @@ export class NamespacesTreeItem extends FeatureTreeItem { export class NamespaceTreeItem extends SMTreeItem { public readonly name: string; constructor( - parentFolderId: string, + element: SMItem, name: string, serverName: string ) { + const parentFolderId = element.parent?.id || ''; const id = parentFolderId + ':' + name; super({ + parent: element.parent, label: name, id, tooltip: `${name} on ${serverName}`