From 79a341a237c58023b560cd1e62abc0a441959a7b Mon Sep 17 00:00:00 2001 From: John Murray Date: Tue, 17 Feb 2026 13:21:25 +0000 Subject: [PATCH] Use new Integrated Browser (1.109+) to open Management Portal in a VS Code tab (#310) * Use new Integrated Browser (1.109+) * Bump vscode version requirement to 1.109 * Simplify code now that we require vscode 1.109 minimum * Remove remnants of Simple Browser use * Delete obsolete comment * Add Integrated Browser option to `...` menu of namespaces * Offer openPortalTab as alt for openPortalExternal button on server node * Cleanup * Remove code to provide CSPCHD on Portal uris * Prefer Integrated Browser tab when opening Portal * Remove commands to open Portal direct in external browser Still available from ... menu of Integrated Browser --- README.md | 17 ------ package-lock.json | 23 ++++---- package.json | 30 +++-------- src/api/getPortalUri.ts | 24 +++++++++ src/api/getPortalUriWithToken.ts | 93 -------------------------------- src/commonActivate.ts | 36 ++++++------- src/ui/serverManagerView.ts | 4 -- 7 files changed, 59 insertions(+), 168 deletions(-) create mode 100644 src/api/getPortalUri.ts delete mode 100644 src/api/getPortalUriWithToken.ts diff --git a/README.md b/README.md index 127e4e40..d99897d8 100644 --- a/README.md +++ b/README.md @@ -132,23 +132,6 @@ Learn more about `isfs` and `isfs-readonly` folders in the [documentation](https When you have a folder or a workspace (including a multi-root one) open in VS Code, Server Manager displays a 'Current' node at the start of its tree if your workspace references any server defined in Server Manager. The linking happens automatically if you added workspace folders from Server Manager as described above. If you are using the client-side mode of working, your `objectscript.conn` setting needs to use the `server` property. -## Launching Management Portal - -When you hover over a server entry in the tree two command buttons let you launch InterSystems Management Portal. - -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. - -### 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 2020.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). diff --git a/package-lock.json b/package-lock.json index babb8e74..65f590f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@types/glob": "^7.1.1", "@types/mocha": "^9.0.0", "@types/node": "^20.14.0", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.109.0", "@vscode/test-electron": "^2.3.8", "@vscode/test-web": "^0.0.71", "glob": "^7.1.6", @@ -30,7 +30,7 @@ "webpack-cli": "^6.0.1" }, "engines": { - "vscode": "^1.93.0" + "vscode": "^1.109.0" } }, "node_modules/@babel/code-frame": { @@ -298,9 +298,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.95.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz", - "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==", + "version": "1.109.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", + "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", "dev": true, "license": "MIT" }, @@ -1331,9 +1331,9 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4100,10 +4100,11 @@ } }, "node_modules/tslint/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } diff --git a/package.json b/package.json index f483ca2a..33e23506 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "multi-root ready" ], "engines": { - "vscode": "^1.93.0" + "vscode": "^1.109.0" }, "icon": "images/logo.png", "categories": [ @@ -57,7 +57,7 @@ "@types/glob": "^7.1.1", "@types/mocha": "^9.0.0", "@types/node": "^20.14.0", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.109.0", "@vscode/test-electron": "^2.3.8", "@vscode/test-web": "^0.0.71", "glob": "^7.1.6", @@ -277,19 +277,14 @@ "command": "intersystems-community.servermanager.removeFromRecent", "title": "Remove from Recent" }, - { - "command": "intersystems-community.servermanager.openPortalExternal", - "title": "Open Management Portal in External Browser", - "icon": "$(link-external)" - }, { "command": "intersystems-community.servermanager.openPortalTab", "title": "Open Management Portal in Tab", - "icon": "$(tools)" + "icon": "$(link-external)" }, { - "command": "intersystems-community.servermanager.openPortalExplorerExternal", - "title": "Open Management Portal Here in External Browser", + "command": "intersystems-community.servermanager.openPortalExplorerTab", + "title": "Open Management Portal Here in Tab", "icon": "$(link-external)" }, { @@ -396,12 +391,7 @@ "menus": { "intersystems-community.servermanager.moreActions": [ { - "command": "intersystems-community.servermanager.openPortalTab", - "when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./", - "group": "1_builtin@10" - }, - { - "command": "intersystems-community.servermanager.openPortalExplorerExternal", + "command": "intersystems-community.servermanager.openPortalExplorerTab", "when": "view == intersystems-community_servermanager && viewItem =~ /namespace$/", "group": "1_builtin@10" } @@ -453,16 +443,12 @@ "command": "intersystems-community.servermanager.removeFromRecent", "when": "false" }, - { - "command": "intersystems-community.servermanager.openPortalExternal", - "when": "false" - }, { "command": "intersystems-community.servermanager.openPortalTab", "when": "false" }, { - "command": "intersystems-community.servermanager.openPortalExplorerExternal", + "command": "intersystems-community.servermanager.openPortalExplorerTab", "when": "false" }, { @@ -611,7 +597,7 @@ "group": "inline@20" }, { - "command": "intersystems-community.servermanager.openPortalExternal", + "command": "intersystems-community.servermanager.openPortalTab", "when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./", "group": "inline@90" }, diff --git a/src/api/getPortalUri.ts b/src/api/getPortalUri.ts new file mode 100644 index 00000000..86ab9792 --- /dev/null +++ b/src/api/getPortalUri.ts @@ -0,0 +1,24 @@ +import * as vscode from "vscode"; +import { Uri } from "vscode"; +import { IServerSpec } from "@intersystems-community/intersystems-servermanager"; +import { extensionId } from "../commonActivate"; + +export async function getPortalUri( + name: string, + page = "/csp/sys/UtilHome.csp", + namespace = "%SYS", + scope?: vscode.ConfigurationScope, +): Promise { + + // Use our own API so that the Recent folder updates with our activity + const myApi = vscode.extensions.getExtension(extensionId)?.exports; + + const spec: IServerSpec | undefined = await myApi.getServerSpec(name, scope); + if (typeof spec !== "undefined") { + + const webServer = spec.webServer; + const queryString = `$NAMESPACE=${encodeURIComponent(namespace)}`; + + return vscode.Uri.parse(`${webServer.scheme}://${webServer.host}:${webServer.port}${webServer.pathPrefix}${page}?${queryString}`, true); + } +} diff --git a/src/api/getPortalUriWithToken.ts b/src/api/getPortalUriWithToken.ts deleted file mode 100644 index 07d3f80a..00000000 --- a/src/api/getPortalUriWithToken.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as vscode from "vscode"; -import { Uri } from "vscode"; -import { IServerSpec } from "@intersystems-community/intersystems-servermanager"; -import { extensionId } from "../commonActivate"; -import { makeRESTRequest } from "../makeRESTRequest"; - -export enum BrowserTarget { - SIMPLE = 0, - EXTERNAL = 1, -} - -const allTokens = [new Map(), new Map()]; - -const simpleBrowserCompatible = new Map(); - -export async function getPortalUriWithToken( - target: BrowserTarget, - name: string, - page = "/csp/sys/UtilHome.csp", - namespace = "%SYS", - scope?: vscode.ConfigurationScope, -): Promise { - - // Use our own API so that the Recent folder updates with our activity - const myApi = vscode.extensions.getExtension(extensionId)?.exports; - - const spec: IServerSpec | undefined = await myApi.getServerSpec(name, scope); - if (typeof spec !== "undefined") { - - if (target === BrowserTarget.SIMPLE && !simpleBrowserCompatible.has(name)) { - // Check that the portal webapps have all been altered so they don't require session cookie support, which Simple Browser cannot provide - const response = await makeRESTRequest( - "POST", - spec, - { apiVersion: 1, namespace: "%SYS", path: "/action/query" }, - { - query: - "SELECT Name FROM Security.Applications WHERE {fn CONCAT(Name, '/')} %STARTSWITH '/csp/sys/' AND UseCookies = 2 " + - "UNION SELECT $PIECE($PIECE($PIECE($ZVERSION,') ',2),' '),'.') AS Name" - }, - ).catch((errorStr) => { - vscode.window.showErrorMessage(`Error checking the Portal web apps for compatibility with Simple Browser${errorStr ? `: ${errorStr}` : "."}`, "Dismiss"); - }); - if (response) { - const appsRequiringCookie = (response.data?.result?.content as any[]).map((row) => { - return row.Name as string; - }); - if (appsRequiringCookie.length && parseInt(appsRequiringCookie[appsRequiringCookie.length - 1], 10) >= 2024) { - // SMP in 2024.1+ can't be embedded in a cross-origin iframe - vscode.window.showWarningMessage(`The Portal cannot be opened in the Simple Browser for IRIS versions 2024.1+.`, "Dismiss"); - simpleBrowserCompatible.set(name, false); - } - else if (appsRequiringCookie.length > 1) { - vscode.window.showWarningMessage(`Portal web apps cannot be used in the Simple Browser tab if their 'UseCookies' property is set to 'Always' (the default). To resolve this, use Portal's security section to change it to 'Autodetect' in these apps: ${appsRequiringCookie.slice(0, -1).join(", ")}`, { modal: true }); - } - else { - simpleBrowserCompatible.set(name, true); - } - } - if (!simpleBrowserCompatible.get(name)) return; - } - - if (target === BrowserTarget.SIMPLE && simpleBrowserCompatible.has(name) && !simpleBrowserCompatible.get(name)) { - vscode.window.showWarningMessage(`The Portal cannot be opened in the Simple Browser for IRIS versions 2024.1+.`, "Dismiss"); - return; - } - - // Retrieve previously cached token - let token = allTokens[target].get(name) || ""; - - // Revalidate and extend existing token, or obtain a new one - const response = await makeRESTRequest( - "POST", - spec, - { apiVersion: 1, namespace, path: "/action/query" }, - { query: "select %Atelier_v1_Utils.General_GetCSPToken(?, ?) token", parameters: [page, token] }, - ).catch(() => { /* Swallow errors */ }); - - if (!response) { - // User will have to enter credentials - token = ""; - allTokens[target].delete(name); - } else { - token = response.data?.result?.content[0]?.token || ""; - allTokens[target].set(name, token); - } - - const webServer = spec.webServer; - const queryString = `$NAMESPACE=${encodeURIComponent(namespace)}${token ? `&CSPCHD=${encodeURIComponent(token)}` : ""}`; - - return vscode.Uri.parse(`${webServer.scheme}://${webServer.host}:${webServer.port}${webServer.pathPrefix}${page}?${queryString}`, true); - } -} diff --git a/src/commonActivate.ts b/src/commonActivate.ts index 2aa6253a..e953d29f 100644 --- a/src/commonActivate.ts +++ b/src/commonActivate.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { IServerName, IServerSpec } from "@intersystems-community/intersystems-servermanager"; import { addServer } from "./api/addServer"; -import { BrowserTarget, getPortalUriWithToken } from "./api/getPortalUriWithToken"; +import { getPortalUri } from "./api/getPortalUri"; import { getServerNames } from "./api/getServerNames"; import { getServerSpec } from "./api/getServerSpec"; import { getServerSummary } from "./api/getServerSummary"; @@ -167,36 +167,30 @@ export function commonActivate(context: vscode.ExtensionContext, view: ServerMan await view.removeFromRecents(server.name); } }), - vscode.commands.registerCommand(`${extensionId}.openPortalExternal`, (server?: ServerTreeItem) => { + vscode.commands.registerCommand(`${extensionId}.openPortalTab`, async (server?: ServerTreeItem) => { if (server?.contextValue?.match(/\.server\./) && server.name) { - getPortalUriWithToken(BrowserTarget.EXTERNAL, server.name, undefined, undefined, server?.params?.serverSummary?.scope).then((uriWithToken) => { - if (uriWithToken) { - vscode.env.openExternal(uriWithToken); + // It is essential to pass skipEncoding=true when converting the uri to a string, + // otherwise the querystring's & and = get encoded. + // Use Integrated Browser which arrived in 1.109 + getPortalUri(server.name, undefined, undefined, server?.params?.serverSummary?.scope).then((uri) => { + if (uri) { + vscode.commands.executeCommand("workbench.action.browser.open", uri.toString(true)); } }); } }), - vscode.commands.registerCommand(`${extensionId}.openPortalTab`, (server?: ServerTreeItem) => { - if (server?.contextValue?.match(/\.server\./) && server.name) { - getPortalUriWithToken(BrowserTarget.SIMPLE, server.name, undefined, undefined, server?.params?.serverSummary?.scope).then((uriWithToken) => { - if (uriWithToken) { - // - // It is essential to pass skipEncoding=true when converting the uri to a string, - // otherwise the querystring's & and = get encoded. - vscode.commands.executeCommand("simpleBrowser.show", uriWithToken.toString(true)); - } - }); - } - }), - vscode.commands.registerCommand(`${extensionId}.openPortalExplorerExternal`, (namespaceTreeItem?: NamespaceTreeItem) => { + vscode.commands.registerCommand(`${extensionId}.openPortalExplorerTab`, (namespaceTreeItem?: NamespaceTreeItem) => { if (namespaceTreeItem) { const pathParts = namespaceTreeItem.id?.split(":"); if (pathParts && pathParts.length === 4) { const serverName = pathParts[1]; const namespace = pathParts[3]; - getPortalUriWithToken(BrowserTarget.EXTERNAL, serverName, "/csp/sys/exp/%25CSP.UI.Portal.ClassList.zen", namespace, namespaceTreeItem.parent?.parent?.params?.serverSummary?.scope).then((uriWithToken) => { - if (uriWithToken) { - vscode.env.openExternal(uriWithToken); + getPortalUri(serverName, "/csp/sys/exp/%2525CSP.UI.Portal.ClassList.zen", namespace, namespaceTreeItem.parent?.parent?.params?.serverSummary?.scope).then((uri) => { + if (uri) { + // It is essential to pass skipEncoding=true when converting the uri to a string, + // otherwise the querystring's & and = get encoded. + // Use Integrated Browser which arrived in 1.109 + vscode.commands.executeCommand("workbench.action.browser.open", uri.toString(true)); } }); } diff --git a/src/ui/serverManagerView.ts b/src/ui/serverManagerView.ts index a914cae7..cb931174 100644 --- a/src/ui/serverManagerView.ts +++ b/src/ui/serverManagerView.ts @@ -400,10 +400,6 @@ export class ServerTreeItem extends SMTreeItem { "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]}; } }