diff --git a/CHANGELOG.md b/CHANGELOG.md index f60a817..11344c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.0.0 (16-Apr-2021) +* Add tree view interface. + ## 1.0.5 (02-Feb-2021) * Fix publication problem (#69). diff --git a/README.md b/README.md index c47b44e..4a8a273 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,75 @@ # InterSystems Server Manager -This is a VS Code helper extension for defining connections to [InterSystems®](https://www.intersystems.com/) servers. These connection definitions can used by other VS Code extensions when they make connections. One example is the [ObjectScript extension](https://github.com/intersystems-community/vscode-objectscript). + +InterSystems Server Manager is a Visual Studio Code extension for defining connections to [InterSystems](https://www.intersystems.com/) servers. These definitions can used by other VS Code extensions when they make connections. One example is the [ObjectScript extension](https://github.com/intersystems-community/vscode-objectscript) for code editing. + +# New in 2.0 - April 2021 + +> We are pleased to publish version 2.0 of this extension, adding a tree-style user interface. This significant new release is competing in the April 2021 InterSystems Programming Contest for Developer Tools. If you like the new 2.0 features please visit the [contest page](https://openexchange.intersystems.com/contest/13) no later than April 25 and vote for us. + +> Thanks to [George James Software](https://georgejames.com) for backing this development effort. + +## The Server Tree + +Server Manager displays connection definitions as a tree on an InterSystems Tools view: + +![Server Manager tree](images/README/tree.png) + +In this tree you can: + +- Launch the InterSystems Management Portal, either in a VS Code tab or in your default browser. +- List namespaces. +- Add namespaces to your VS Code workspace for viewing or editing code on the server with the [ObjectScript extension](https://github.com/intersystems-community/vscode-objectscript). +- Tag favorite servers. +- Set icon colors. +- Focus on recently used connections. +- Manage stored passwords. +- Add new servers, and edit existing ones. In common with the rest of VS Code, Server Manager stores your connection settings in JSON files. VS Code settings are arranged in a hierarchy that you can learn more about [here](https://code.visualstudio.com/docs/getstarted/settings). -Using Server Manager you can store connection passwords in the native keystore of your workstation's operating system instead of as plaintext in JSON files. +Server Manager can store connection passwords in the native keystore of your workstation's operating system. This is a more secure alternative to you putting them as plaintext in your JSON files. + +On Windows, Server Manager can create connection entries for all connections you previously defined with the original Windows app called InterSystems Server Manager. This action is available from the '`...`' menu at the top right corner of Server Manager's tree. + +## Defining a New Server + +1. Click the '`+`' button on Server Manager's title bar. +2. Complete the sequence of prompts. +3. Expand `All Servers` to see your new entry in the tree. + +The server definition is added to your [user-level](https://code.visualstudio.com/docs/getstarted/settings) `settings.json` file. + +Optionally use its context menu to store the password for the username you entered when defining the server. You can also set the color of the server icon. -On Windows you can run `Import Servers from Registry` from [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) to create connection entries for all connections you previously defined with the InterSystems Server Manager. +The 'star' button that appears when you hover over the row lets you add the server to the `Favorites` list at the top of the tree. -## Defining a new connection +## Launching Management Portal -1. Open the VS Code [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) by typing Ctrl+Shift+P (Cmd+Shift+P on macOS) or F1. -2. Start typing "Server Manager" to locate `InterSystems Server Manager: Store Password in Keychain`. -3. Type the '+' character into the quickpick input field. Alternatively click the '+' button on the top right of the quickpick. -4. Complete the prompts. By the time you reach the password prompt your connection definition has already been saved in **user-level** JSON. If you prefer to enter your password whenever a VS Code extension connects via Server Manager for the first time in a session, press Esc here. +When you hover over a server entry in the tree, two command buttons let you launch InterSystems Management Portal. -## Amending and removing definitions +The first button uses VS Code's Simple Browser feature, which creates a tab alongside any documents you may have open. The second button opens Portal in your workstation's default web browser. -To manage your definitions, [edit the relevant JSON file](https://code.visualstudio.com/docs/getstarted/settings). VS Code offers several routes to these files. One way is to type "json" into the Command Palette. +### Notes About Simple Browser +- There is only ever a single Simple Browser tab. Launching another server's Management Portal in it will replace the previous one. +- If the server version is InterSystems IRIS 2021.1.1 or later you will need to change a setting on the suite of web applications that implement Management Portal. This is a consequence of change [SGM031 - Support SameSite for CSP session and user cookies](https://docs.intersystems.com/iris20201/csp/docbook/relnotes/index.html#SGM031). Simple Browser will not be permitted to store Portal's session management cookies, so Portal must be willing to fall back to using the CSPCHD query parameter mechanism. + - Locate the five web applications whose path begins with `/csp/sys` + ![Portal web app list](images/README/portalWebApps.png) + + - Alter the `Use Cookie for Session` setting on each of them so it is `Autodetect` instead of `Always`. + ![Portal web app detail](images/README/portalWebAppSetting.png) + Remember to save the change. The change is not thought to have any adverse effects on the usage of Portal from ordinary browsers, which will continue to use session cookies. +- When a 2020.1.1+ Portal has resorted to using CSPCHD (see above) a few inter-page links fail because they don't add the CSPCHD queryparam. One specific case is the breadcrumb links. Pending the arrival of an InterSystems correction (JIRA DP-404817) these links will take you to the login page. Either enter your credentials to proceed, or launch Simple Browser again from the Server Manager tree. + +## Amending and Removing Servers + +To manage your server definitions, including changing the username it connects with, [edit the relevant JSON file](https://code.visualstudio.com/docs/getstarted/settings). + +1. From a server's context menu, or from Server Manager's top-right '`...`' menu, choose `Edit Settings`. This opens VS Code's Settings Editor and filters its contents. + +![Edit Settings](images/README/editSettings.png) + +2. Click the `Edit in settings.json` link. In this example two connections have been defined: @@ -48,25 +100,37 @@ In this example two connections have been defined: The JSON editor offers the usual [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense) as you work in this structure. -Notice how you can add a `description` property to each connection. This will be shown alongside its entry in the server quickpick. +Notice how you can add a `description` property to each connection. This will be shown in the hover in Server Manager's tree, and alongside the entry if a server quickpick is used. Servers are displayed in the quickpick in the order they are defined in the JSON file. The exception is that if a server name is set as the value of the `/default` property (see example above) it will be shown first in the list. -A set of embedded servers with names beginning `default~` will appear at the end of the quickpick unless you add the property `"/hideEmbeddedEntries": true` to your `intersystems.server` object (see above). +A set of embedded servers with names beginning `default~` will appear at the end of the lists unless you add the property `"/hideEmbeddedEntries": true` to your `intersystems.server` object to hide them (see above). + +## Removing a Stored Password + +Use the server's context menu. Alternatively, run `InterSystems Server Manager: Clear Password from Keychain` from Command Palette. + +--- + +## Technical Notes + +### Colors, Favorites and Recents + +These features use VS Code's extension-private global state storage. Data is not present in your `settings.json` file. + +### The 'All Servers' Folder -## Removing a stored password +The `All Servers` tree respects the optional `/default` and `/hideEmbeddedEntries` settings in the `intersystems.servers` JSON. -The command `InterSystems Server Manager: Clear Password from Keychain` removes a stored password. +If a server has been named in `/default` it is promoted to the top of the list, which is otherwise presented in alphabetical order. -**Usage:** +Embedded entries (built-in default ones) are demoted to the end of the list, or omitted completely if `/hideEmbeddedEntries` is true. -1. Bring up the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) by typing Ctrl+Shift+P (Cmd+Shift+P on Mac) or F1. -2. Start typing "Server Manager" to locate the `InterSystems Server Manager: Clear Password from Keychain` command. -3. Pick a server. +--- -## For VS Code Extension Developers: Use By Other Extensions +## Information for VS Code Extension Developers - How To Leverage Server Manager -An extension XYZ needing to connect to InterSystems servers can define this extension as a dependency in its `package.json` +An extension XYZ needing to connect to InterSystems servers can define Server Manager as a dependency in its `package.json` like this: ```json "extensionDependencies": [ @@ -74,7 +138,7 @@ An extension XYZ needing to connect to InterSystems servers can define this exte ], ``` -Alternatively the `activate` method of XYZ can detect if the extension is already available, then offer to install it if necessary: +Alternatively the `activate` method of XYZ can detect whether the extension is already available, then offer to install it if necessary: ```ts const extId = "intersystems-community.servermanager"; diff --git a/images/README/editSettings.png b/images/README/editSettings.png new file mode 100644 index 0000000..79cff41 Binary files /dev/null and b/images/README/editSettings.png differ diff --git a/images/README/portalWebAppSetting.png b/images/README/portalWebAppSetting.png new file mode 100644 index 0000000..bfcf4f8 Binary files /dev/null and b/images/README/portalWebAppSetting.png differ diff --git a/images/README/portalWebApps.png b/images/README/portalWebApps.png new file mode 100644 index 0000000..153af91 Binary files /dev/null and b/images/README/portalWebApps.png differ diff --git a/images/README/tree.png b/images/README/tree.png new file mode 100644 index 0000000..49255eb Binary files /dev/null and b/images/README/tree.png differ diff --git a/package.json b/package.json index d537777..510efb3 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "servermanager", "displayName": "InterSystems Server Manager", - "version": "2.0.0-SNAPSHOT.4", + "version": "2.0.0-SNAPSHOT", "publisher": "intersystems-community", - "description": "Helper extension for defining connections to InterSystems servers.", + "description": "Define connections to InterSystems servers. Browse and manage those servers.", "repository": { "type": "git", "url": "https://github.com/intersystems-community/intersystems-servermanager" @@ -89,6 +89,13 @@ } ] }, + "viewsWelcome": [ + { + "view": "explorer", + "contents": "Use the 'InterSystems Tools: Servers' view to work with your servers.\n[Manage Servers $(tools)](command:workbench.view.extension.intersystems-community_servermanager)" + } + ] + , "configuration": { "title": "InterSystems Server Manager", "properties": { @@ -392,6 +399,11 @@ } ], "view/item/context": [ + { + "command": "intersystems-community.servermanager.addServer", + "when": "view == intersystems-community_servermanager && viewItem == sorted", + "group": "inline@10" + }, { "command": "intersystems-community.servermanager.addToStarred", "when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\.$/", diff --git a/src/api/addServer.ts b/src/api/addServer.ts index 11f5852..735fad8 100644 --- a/src/api/addServer.ts +++ b/src/api/addServer.ts @@ -27,69 +27,78 @@ export async function addServer( .then( async (name): Promise => { if (name) { - const host = await vscode.window.showInputBox({ - placeHolder: "Hostname or IP address of web server", - validateInput: (value) => { - return value.trim().length ? undefined : "Required"; - }, + const description = await vscode.window.showInputBox({ + placeHolder: "Optional description", ignoreFocusOut: true }); - if (host) { - spec.webServer.host = host.trim(); - const portString = await vscode.window.showInputBox({ - placeHolder: "Port of web server", + if (typeof description !== 'undefined') { + if (description) { + spec.description = description.trim(); + } + const host = await vscode.window.showInputBox({ + placeHolder: "Hostname or IP address of web server", validateInput: (value) => { - const port = +value; - return value.match(/\d+/) && - port.toString() === value && - port > 0 && - port < 65536 - ? undefined - : "Required, 1-65535"; + return value.trim().length ? undefined : "Required"; }, ignoreFocusOut: true }); - if (portString) { - spec.webServer.port = +portString; - const username = await vscode.window.showInputBox({ - placeHolder: - "Username", - prompt: - "Leave empty to be prompted when connecting.", + if (host) { + spec.webServer.host = host.trim(); + const portString = await vscode.window.showInputBox({ + placeHolder: "Port of web server", + validateInput: (value) => { + const port = +value; + return value.match(/\d+/) && + port.toString() === value && + port > 0 && + port < 65536 + ? undefined + : "Required, 1-65535"; + }, ignoreFocusOut: true - }); - if (typeof username !== 'undefined') { - const usernameTrimmed = username.trim(); - if (usernameTrimmed !== "") { - spec.username = usernameTrimmed; - } - const scheme = await vscode.window.showQuickPick( - ["http", "https"], - { - placeHolder: - "Confirm connection type, then the definition will be stored in your User Settings. 'Escape' to cancel.", - ignoreFocusOut: true + }); + if (portString) { + spec.webServer.port = +portString; + const username = await vscode.window.showInputBox({ + placeHolder: + "Username", + prompt: + "Leave empty to be prompted when connecting.", + ignoreFocusOut: true + }); + if (typeof username !== 'undefined') { + const usernameTrimmed = username.trim(); + if (usernameTrimmed !== "") { + spec.username = usernameTrimmed; + } + const scheme = await vscode.window.showQuickPick( + ["http", "https"], + { + placeHolder: + "Confirm connection type, then the definition will be stored in your User Settings. 'Escape' to cancel.", + ignoreFocusOut: true + } + ); + if (scheme) { + spec.webServer.scheme = scheme; + try { + const config = vscode.workspace.getConfiguration( + "intersystems", + scope + ); + // For simplicity we always add to the user-level (aka Global) settings + const servers: any = + config.inspect("servers")?.globalValue || {}; + servers[name] = spec; + await config.update("servers", servers, true); + vscode.window.showInformationMessage(`Server '${name}' stored in user-level settings.`); + return name; + } catch (error) { + vscode.window.showErrorMessage( + "Failed to store server '${name}' definition." + ); + return undefined; } - ); - if (scheme) { - spec.webServer.scheme = scheme; - try { - const config = vscode.workspace.getConfiguration( - "intersystems", - scope - ); - // For simplicity we always add to the user-level (aka Global) settings - const servers: any = - config.inspect("servers")?.globalValue || {}; - servers[name] = spec; - await config.update("servers", servers, true); - vscode.window.showInformationMessage(`Server '${name}' stored in user-level settings.`); - return name; - } catch (error) { - vscode.window.showErrorMessage( - "Failed to store server '${name}' definition." - ); - return undefined; } } } diff --git a/src/ui/serverManagerView.ts b/src/ui/serverManagerView.ts index 45e8ca3..943e2e5 100644 --- a/src/ui/serverManagerView.ts +++ b/src/ui/serverManagerView.ts @@ -128,26 +128,26 @@ class SMNodeProvider implements vscode.TreeDataProvider { let firstRevealId = favoritesMap.size > 0 ? 'starred' : recentsArray.length > 0 ? 'recent' : 'sorted'; if (vscode.workspace.workspaceFolders?.length || 0 > 0) { - children.push(new SMTreeItem({parent: element, label: 'Current', id: 'current', tooltip: 'Servers referenced by current workspace', codiconName: 'home', getChildren: currentServers})); + children.push(new SMTreeItem({parent: element, label: 'Current', id: 'current', contextValue: '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({parent: element, label: 'Starred', id: 'starred', tooltip: 'Favorite servers', codiconName: 'star-full', getChildren: favoriteServers})); + children.push(new SMTreeItem({parent: element, label: 'Favorites', id: 'starred', contextValue: 'starred', tooltip: 'Favorite servers', codiconName: 'star-full', getChildren: favoriteServers})); if (firstRevealId === 'starred') { this._firstRevealItem = children[children.length - 1]; } } - children.push(new SMTreeItem({parent: element, label: 'Recent', id: 'recent', tooltip: 'Recently used servers', codiconName: 'history', getChildren: recentServers})); + children.push(new SMTreeItem({parent: element, label: 'Recent', id: 'recent', contextValue: '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({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({parent: element, label: 'Ordered', id: 'ordered', contextValue: 'ordered', tooltip: 'All servers in settings.json order', codiconName: 'list-ordered', getChildren: allServers, params: {sorted: false}})); - 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}})); + children.push(new SMTreeItem({parent: element, label: 'All Servers', id: 'sorted', contextValue: 'sorted', tooltip: 'All servers in alphabetical order', codiconName: 'server-environment', getChildren: allServers, params: {sorted: true}})); if (firstRevealId === 'sorted') { this._firstRevealItem = children[children.length - 1]; } @@ -286,20 +286,30 @@ export class ServerTreeItem extends SMTreeItem { serverSummary: ServerName ) { const parentFolderId = element.parent?.id || ""; + // Convert linebreaks etc, escape Markdown characters, truncate + const escapedDescription = serverSummary.description + .replace(/[\n\t]/g, ' ') + .replace(/[\r\f\b]/g, '') + .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + .substr(0,90) + .trim(); // Wrap detail (a uri string) as a null link to prevent it from being linkified + const wrappedDetail = `[${serverSummary.detail}]()`; super({ parent: element.parent, label: serverSummary.name, id: parentFolderId + ':' + serverSummary.name, - tooltip: new vscode.MarkdownString(`[${serverSummary.detail}]()`).appendMarkdown(serverSummary.description ? `\n\n*${serverSummary.description}*` : ''), + tooltip: new vscode.MarkdownString(wrappedDetail).appendMarkdown(escapedDescription ? `\n\n*${escapedDescription}*` : ''), getChildren: serverFeatures, params: { serverSummary } }); 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); this.iconPath = new vscode.ThemeIcon('server-environment', color ? new vscode.ThemeColor('charts.' + color) : undefined); + + // TODO If single click on server item should open Portal tab + // this.command = {command: 'intersystems-community.servermanager.openPortalTab', title: 'Open Management Portal in Simple Browser Tab', arguments: [this]}; } }