diff --git a/package-lock.json b/package-lock.json index 58a5ebe..4e6ed8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,19 @@ { "name": "servermanager", - "version": "2.0.0-SNAPSHOT", + "version": "2.0.0-SNAPSHOT.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "servermanager", - "version": "2.0.0-SNAPSHOT", + "version": "2.0.0-SNAPSHOT.1", "license": "MIT", "dependencies": { "@types/vscode": "^1.55.0", - "node-cmd": "^4.0.0" + "axios": "^0.21.1", + "axios-cookiejar-support": "^1.0.1", + "node-cmd": "^4.0.0", + "tough-cookie": "^4.0.0" }, "devDependencies": { "@types/glob": "^7.1.1", @@ -94,6 +97,12 @@ "integrity": "sha512-l+zSbvT8TPRaCxL1l9cwHCb0tSqGAGcjPJFItGGYat5oCTiq1uQQKYg5m7AF1mgnEBzFXGLJ2LRmNjtreRX76Q==", "dev": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", + "peer": true + }, "node_modules/@types/vscode": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.55.0.tgz", @@ -185,6 +194,31 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/axios-cookiejar-support": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz", + "integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==", + "dependencies": { + "is-redirect": "^1.0.0", + "pify": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@types/tough-cookie": ">=2.3.3", + "axios": ">=0.16.2", + "tough-cookie": ">=2.3.3" + } + }, "node_modules/azure-devops-node-api": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz", @@ -764,7 +798,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", - "dev": true, "engines": { "node": ">=4.0" } @@ -1098,6 +1131,14 @@ "node": ">=8" } }, + "node_modules/is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1791,6 +1832,17 @@ "node": ">=8.6" } }, + "node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prebuild-install": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.4.tgz", @@ -1832,6 +1884,11 @@ "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "dev": true }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -1842,6 +1899,14 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2133,6 +2198,19 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-loader": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.2.tgz", @@ -2265,6 +2343,14 @@ "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", "dev": true }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/url-join": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", @@ -2667,6 +2753,12 @@ "integrity": "sha512-l+zSbvT8TPRaCxL1l9cwHCb0tSqGAGcjPJFItGGYat5oCTiq1uQQKYg5m7AF1mgnEBzFXGLJ2LRmNjtreRX76Q==", "dev": true }, + "@types/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", + "peer": true + }, "@types/vscode": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.55.0.tgz", @@ -2743,6 +2835,23 @@ "sprintf-js": "~1.0.2" } }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "axios-cookiejar-support": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz", + "integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==", + "requires": { + "is-redirect": "^1.0.0", + "pify": "^5.0.0" + } + }, "azure-devops-node-api": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz", @@ -3228,8 +3337,7 @@ "follow-redirects": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", - "dev": true + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" }, "fs-constants": { "version": "1.0.0", @@ -3502,6 +3610,11 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -4063,6 +4176,11 @@ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, "prebuild-install": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.4.tgz", @@ -4098,6 +4216,11 @@ "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "dev": true }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4108,6 +4231,11 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4355,6 +4483,16 @@ "is-number": "^7.0.0" } }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, "ts-loader": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.2.tgz", @@ -4463,6 +4601,11 @@ "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", "dev": true }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "url-join": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", diff --git a/package.json b/package.json index 432bc79..caf2e1c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "servermanager", "displayName": "InterSystems Server Manager", - "version": "2.0.0-SNAPSHOT.1", + "version": "2.0.0-SNAPSHOT.2", "publisher": "intersystems-community", "description": "Helper extension for defining connections to InterSystems servers.", "repository": { @@ -41,7 +41,10 @@ }, "dependencies": { "@types/vscode": "^1.55.0", - "node-cmd": "^4.0.0" + "axios": "^0.21.1", + "axios-cookiejar-support": "^1.0.1", + "node-cmd": "^4.0.0", + "tough-cookie": "^4.0.0" }, "devDependencies": { "@types/glob": "^7.1.1", @@ -289,7 +292,7 @@ "command": "intersystems-community.servermanager.resetIconColor", "title": "default" } - ], + ], "submenus": [ { "id": "intersystems-community.servermanager.iconColor", @@ -388,6 +391,7 @@ }, { "submenu": "intersystems-community.servermanager.iconColor", + "when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./", "group": "color" }, { diff --git a/src/extension.ts b/src/extension.ts index 763553b..197fb95 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -191,15 +191,7 @@ export function activate(context: vscode.ExtensionContext) { // Expose our API let api = { async pickServer(scope?: vscode.ConfigurationScope, options: vscode.QuickPickOptions = {}): Promise { - const name = await pickServer(scope, options); - - /* - if (name) { - view.addToRecents(name); - } - */ - - return name; + return await pickServer(scope, options); }, getServerNames(scope?: vscode.ConfigurationScope): ServerName[] { return getServerNames(scope); @@ -209,10 +201,10 @@ export function activate(context: vscode.ExtensionContext) { return getServerSummary(name, scope); }, - async getServerSpec(name: string, scope?: vscode.ConfigurationScope, flushCredentialCache: boolean = false): Promise { + async getServerSpec(name: string, scope?: vscode.ConfigurationScope, flushCredentialCache: boolean = false, options?: { hideFromRecents?: boolean}): Promise { const spec = await getServerSpec(name, scope, flushCredentialCache); - if (spec) { - view.addToRecents(name); + if (spec && !options?.hideFromRecents) { + await view.addToRecents(name); } return spec; }, diff --git a/src/makeRESTRequest.ts b/src/makeRESTRequest.ts new file mode 100644 index 0000000..0bd6673 --- /dev/null +++ b/src/makeRESTRequest.ts @@ -0,0 +1,118 @@ +// Derived from https://github.com/intersystems/language-server/blob/bdeea88d1900a3aff35d5ac373436899f3904a7e/server/src/server.ts + +import axios, { AxiosResponse } from 'axios'; +import axiosCookieJarSupport from 'axios-cookiejar-support'; +import tough = require('tough-cookie'); +import { ServerSpec } from './extension'; + +axiosCookieJarSupport(axios); + +/** + * Cookie jar for REST requests to InterSystems servers. + */ +let cookieJar: tough.CookieJar = new tough.CookieJar(); + +export interface AtelierRESTEndpoint { + apiVersion: number, + namespace: string, + path: string +}; + +/** + * Make a REST request to an InterSystems server. + * + * @param method The REST method. + * @param server The server to send the request to. + * @param endpoint Optional endpoint object. If omitted the request will be to /api/atelier/ + * @param data Optional request data. Usually passed for POST requests. + */ + export async function makeRESTRequest(method: "GET"|"POST", server: ServerSpec, endpoint?: AtelierRESTEndpoint, data?: any): Promise { + + // Build the URL + var url = server.webServer.scheme + "://" + server.webServer.host + ":" + String(server.webServer.port); + const pathPrefix = server.webServer.pathPrefix; + if (pathPrefix && pathPrefix !== "") { + url = url.concat("/",pathPrefix) + } + url += "/api/atelier/"; + if (endpoint) { + url += "/api/atelier/v" + String(endpoint.apiVersion) + "/" + endpoint.namespace + endpoint.path; + } + + // Make the request (SASchema support removed) + try { + var respdata: AxiosResponse; + if (data !== undefined) { + // There is a data payload + respdata = await axios.request( + { + method: method, + url: encodeURI(url), + data: data, + headers: { + 'Content-Type': 'application/json' + }, + withCredentials: true, + jar: cookieJar, + validateStatus: function (status) { + return status < 500; + } + } + ); + if (respdata.status === 401) { + // Either we had no cookies or they expired, so resend the request with basic auth + + respdata = await axios.request( + { + method: method, + url: url, + data: data, + headers: { + 'Content-Type': 'application/json' + }, + auth: { + username: server.username || "", + password: server.password || "" + }, + withCredentials: true, + jar: cookieJar + } + ); + } + } + else { + // No data payload + respdata = await axios.request( + { + method: method, + url: url, + withCredentials: true, + jar: cookieJar, + validateStatus: function (status) { + return status < 500; + } + } + ); + if (respdata.status === 401) { + // Either we had no cookies or they expired, so resend the request with basic auth + + respdata = await axios.request( + { + method: method, + url: url, + auth: { + username: server.username || "", + password: server.password || "" + }, + withCredentials: true, + jar: cookieJar + } + ); + } + } + return respdata; + } catch (error) { + console.log(error); + return undefined; + } +}; diff --git a/src/ui/serverManagerView.ts b/src/ui/serverManagerView.ts index e340111..7fc0986 100644 --- a/src/ui/serverManagerView.ts +++ b/src/ui/serverManagerView.ts @@ -1,12 +1,15 @@ import * as vscode from 'vscode'; import { getServerNames } from '../api/getServerNames'; +import { getServerSpec } from '../api/getServerSpec'; import { getServerSummary } from '../api/getServerSummary'; import { ServerName } from '../extension'; +import { makeRESTRequest } from '../makeRESTRequest'; const SETTINGS_VERSION = 'v1'; namespace StorageIds { export const favorites = `tree.${SETTINGS_VERSION}.favorites`; + export const recents = `tree.${SETTINGS_VERSION}.recents`; export const iconColors = `tree.${SETTINGS_VERSION}.iconColors`; } @@ -22,7 +25,27 @@ export class ServerManagerView { private _treeDataProvider: SMNodeProvider; - addToRecents(name: string) { + constructor(context: vscode.ExtensionContext) { + this._globalState = context.globalState; + const treeDataProvider = new SMNodeProvider(); + this._treeDataProvider = treeDataProvider; + context.subscriptions.push( + vscode.window.createTreeView('intersystems-community_servermanager', { treeDataProvider, showCollapseAll: true }) + ); + + // load favoritesMap + const favorites = this._globalState.get(StorageIds.favorites) || []; + favorites.forEach((name) => favoritesMap.set(name, null)); + + // load recentsArray + recentsArray = this._globalState.get(StorageIds.recents) || []; + + // load colorsMap + const colors = this._globalState.get(StorageIds.iconColors) || []; + colors.forEach((pair) => colorsMap.set(pair[0], pair[1])); + } + + async addToRecents(name: string) { if (recentsArray[0] !== name) { recentsArray = recentsArray.filter((n) => n !== name); if (recentsArray.unshift(name) > 8) { @@ -31,6 +54,7 @@ export class ServerManagerView { // Delay the refresh to avoid startling the user by updating the tree the instant they click on a command button setTimeout(() => this.refreshTree(), 1000); + await this._globalState.update(StorageIds.recents, recentsArray); } } @@ -67,22 +91,6 @@ export class ServerManagerView { this._treeDataProvider.refresh(); } - constructor(context: vscode.ExtensionContext) { - this._globalState = context.globalState; - const treeDataProvider = new SMNodeProvider(); - this._treeDataProvider = treeDataProvider; - context.subscriptions.push( - vscode.window.createTreeView('intersystems-community_servermanager', { treeDataProvider, showCollapseAll: true }) - ); - - // load favoritesMap - const favorites = this._globalState.get(StorageIds.favorites) || []; - favorites.forEach((name) => favoritesMap.set(name, null)); - - // load colorsMap - const colors = this._globalState.get(StorageIds.iconColors) || []; - colors.forEach((pair) => colorsMap.set(pair[0], pair[1])); - } } class SMNodeProvider implements vscode.TreeDataProvider { @@ -101,28 +109,30 @@ class SMNodeProvider implements vscode.TreeDataProvider { return element; } - getChildren(element?: SMTreeItem): SMTreeItem[] { + async getChildren(element?: SMTreeItem): Promise { const children: SMTreeItem[] = []; if (!element) { + // Root folders if (vscode.workspace.workspaceFolders?.length || 0 > 0) { - children.push(new SMTreeItem({label: 'Current', tooltip: 'Servers used by current workspace', codiconName: 'home', getChildren: currentServers})); + children.push(new SMTreeItem({label: 'Current', id: 'current', tooltip: 'Servers used by current workspace', codiconName: 'home', getChildren: currentServers})); } if (favoritesMap.size > 0) { - children.push(new SMTreeItem({label: 'Starred', tooltip: 'Favorite servers', codiconName: 'star-full', getChildren: favoriteServers})); + children.push(new SMTreeItem({label: 'Starred', id: 'starred', tooltip: 'Favorite servers', codiconName: 'star-full', getChildren: favoriteServers})); } - children.push(new SMTreeItem({label: 'Recent', tooltip: 'Recently used servers', codiconName: 'history', getChildren: recentServers})); - children.push(new SMTreeItem({label: 'Ordered', tooltip: 'All servers in settings.json order', codiconName: 'list-ordered', getChildren: allServers, params: {sorted: false}})); - children.push(new SMTreeItem({label: 'Sorted', tooltip: 'All servers in alphabetical order', codiconName: 'triangle-down', getChildren: allServers, params: {sorted: true}})); + children.push(new SMTreeItem({label: 'Recent', id: 'recent', tooltip: 'Recently used servers', codiconName: 'history', getChildren: recentServers})); + 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({label: 'Sorted', id: 'sorted', tooltip: 'All servers in alphabetical order', codiconName: 'triangle-down', getChildren: allServers, params: {sorted: true}})); return children; } - else{ - return element.getChildren() + else { + return await element.getChildren(); } } } interface SMItem { label: string, + id: string, contextValue?: string, tooltip?: string | vscode.MarkdownString, description?: string, @@ -140,6 +150,7 @@ class SMTreeItem extends vscode.TreeItem { const collapsibleState = item.getChildren ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; super(item.label, collapsibleState); + this.id = item.id; this.contextValue = item.contextValue; this.tooltip = item.tooltip; this.description = item.description; @@ -150,9 +161,9 @@ class SMTreeItem extends vscode.TreeItem { this._params = item.params; } - public getChildren(): SMTreeItem[] { + public async getChildren(): Promise { if (this._getChildren) { - return this._getChildren(this, this._params); + return await this._getChildren(this, this._params) || []; } else { return []; @@ -160,7 +171,7 @@ class SMTreeItem extends vscode.TreeItem { } } -function allServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { +function allServers(element: SMTreeItem, params?: any): ServerTreeItem[] { const children: ServerTreeItem[] = []; const getAllServers = (sorted?: boolean): ServerTreeItem[] => { let serverNames = getServerNames(); @@ -168,7 +179,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(serverName, sorted ? 'sorted' : 'ordered'); + return new ServerTreeItem(sorted ? 'sorted' : 'ordered', serverName); }) } @@ -176,7 +187,7 @@ function allServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { return children; } -function currentServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { +function currentServers(element: SMTreeItem, params?: any): ServerTreeItem[] { const children = new Map(); vscode.workspace.workspaceFolders?.map((folder) => { @@ -184,7 +195,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(serverSummary, 'current')); + children.set(serverName, new ServerTreeItem('current', serverSummary)); } } const conn = vscode.workspace.getConfiguration('objectscript.conn', folder); @@ -192,7 +203,7 @@ function currentServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { if (connServer) { const serverSummary = getServerSummary(connServer); if (serverSummary) { - children.set(connServer, new ServerTreeItem(serverSummary, 'current')); + children.set(connServer, new ServerTreeItem('current', serverSummary)); } } }); @@ -200,26 +211,26 @@ function currentServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { return Array.from(children.values()).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); } -function favoriteServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { +function favoriteServers(element: SMTreeItem, params?: any): ServerTreeItem[] { const children: ServerTreeItem[] = []; favoritesMap.forEach((_, name) => { const serverSummary = getServerSummary(name); if (serverSummary) { - children.push(new ServerTreeItem(serverSummary, 'starred')); + children.push(new ServerTreeItem('starred', serverSummary)); } }); return children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); } -function recentServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { +function recentServers(element: SMTreeItem, params?: any): ServerTreeItem[] { const children: ServerTreeItem[] = []; recentsArray.map((name) => { const serverSummary = getServerSummary(name); if (serverSummary) { - children.push(new ServerTreeItem(serverSummary, 'recent')); + children.push(new ServerTreeItem('recent', serverSummary)); } }); @@ -229,11 +240,16 @@ function recentServers(element?: SMTreeItem, params?: any): ServerTreeItem[] { export class ServerTreeItem extends SMTreeItem { public readonly name: string; constructor( - serverName: ServerName, - parentFolderId: string + parentFolderId: string, + serverName: ServerName ) { // Wrap detail (a uri string) as a null link to prevent it from being linkified - super({label: serverName.name, tooltip: new vscode.MarkdownString(`[${serverName.detail}]()`).appendMarkdown(serverName.description ? `\n\n*${serverName.description}*` : '')}); + super({ + label: serverName.name, + id: parentFolderId + ':' + serverName.name, + tooltip: new vscode.MarkdownString(`[${serverName.detail}]()`).appendMarkdown(serverName.description ? `\n\n*${serverName.description}*` : ''), + getChildren: serverFeatures + }); this.name = serverName.name; //this.command = {command: 'intersystems-community.servermanager.openManagementPortalInSimpleBrowser', title: 'Open Management Portal in Simple Browser Tab', arguments: [this]}; this.contextValue = `${parentFolderId}.server.${favoritesMap.has(this.name) ? 'starred' : ''}`; @@ -241,3 +257,74 @@ export class ServerTreeItem extends SMTreeItem { this.iconPath = new vscode.ThemeIcon('server-environment', color ? new vscode.ThemeColor('charts.' + color) : undefined); } } + +async function serverFeatures(element: ServerTreeItem, params?: any): Promise { + const children: FeatureTreeItem[] = []; + + children.push(new NamespacesTreeItem(element.id || '', element.name)); + + return children; +} + +export class FeatureTreeItem extends SMTreeItem { +} + +export class NamespacesTreeItem extends FeatureTreeItem { + public readonly name: string; + constructor( + parentFolderId: string, + serverName: string + ) { + super({ + label: 'Namespaces', + id: parentFolderId + ':namespaces', + tooltip: `Namespaces you can access`, + getChildren: serverNamespaces, + params: { serverName } + }); + this.name = 'Namespaces'; + this.contextValue = 'namespaces'; + this.iconPath = new vscode.ThemeIcon('library'); + } +} + +async function serverNamespaces(element: ServerTreeItem, params?: any): Promise { + const children: NamespaceTreeItem[] = []; + + if (params?.serverName) { + const server = await getServerSpec(params.serverName) + if (!server) { + 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)); + }); + } + } + + return children; +} + +export class NamespaceTreeItem extends SMTreeItem { + public readonly name: string; + constructor( + parentFolderId: string, + name: string, + serverName: string + ) { + const id = parentFolderId + ':' + name; + super({ + label: name, + id, + tooltip: `${name} on ${serverName}` + }); + this.name = name; + this.contextValue = name === '%SYS' ? 'sysnamespace' : 'namespace'; + this.iconPath = new vscode.ThemeIcon('archive'); + } +}