This commit is contained in:
chlafreniere
2020-04-03 00:14:28 -07:00
351 changed files with 7194 additions and 4889 deletions

View File

@@ -113,6 +113,21 @@ steps:
displayName: Run unit tests displayName: Run unit tests
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
- script: |
set -e
APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin
APP_NAME="`ls $APP_ROOT | head -n 1`"
yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots"
displayName: Run smoke tests (Electron)
condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- script: |
set -e
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-web-darwin" \
yarn smoketest --web --headless --screenshots "$(build.artifactstagingdirectory)/smokeshots"
displayName: Run smoke tests (Browser)
condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- script: | - script: |
set -e set -e
pushd ../azuredatastudio-darwin pushd ../azuredatastudio-darwin

View File

@@ -6,7 +6,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
// @ts-ignore review
const { remote } = require('electron'); const { remote } = require('electron');
const dialog = remote.dialog; const dialog = remote.dialog;

View File

@@ -455,10 +455,8 @@ function createTscCompileTask(watch) {
// e.g. src/vs/base/common/strings.ts(663,5): error TS2322: Type '1234' is not assignable to type 'string'. // e.g. src/vs/base/common/strings.ts(663,5): error TS2322: Type '1234' is not assignable to type 'string'.
let fullpath = path.join(root, match[1]); let fullpath = path.join(root, match[1]);
let message = match[3]; let message = match[3];
// @ts-ignore
reporter(fullpath + message); reporter(fullpath + message);
} else { } else {
// @ts-ignore
reporter(str); reporter(str);
} }
} }

View File

@@ -36,10 +36,8 @@ const { compileBuildTask } = require('./gulpfile.compile');
const { compileExtensionsBuildTask } = require('./gulpfile.extensions'); const { compileExtensionsBuildTask } = require('./gulpfile.extensions');
const productionDependencies = deps.getProductionDependencies(path.dirname(__dirname)); const productionDependencies = deps.getProductionDependencies(path.dirname(__dirname));
const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n)); const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n));
// {{SQL CARBON EDIT}} const nodeModules = [ // {{SQL CARBON EDIT}}
const nodeModules = [
'electron', 'electron',
'original-fs', 'original-fs',
'rxjs/Observable', 'rxjs/Observable',

View File

@@ -92,9 +92,7 @@ function prepareDebPackage(arch) {
const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' })
.pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@ARCHITECTURE@@', debArch)) .pipe(replace('@@ARCHITECTURE@@', debArch))
// @ts-ignore JSON checking: quality is optional
.pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@'))
// @ts-ignore JSON checking: updateUrl is optional
.pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@'))
.pipe(rename('DEBIAN/postinst')); .pipe(rename('DEBIAN/postinst'));
@@ -169,9 +167,7 @@ function prepareRpmPackage(arch) {
.pipe(replace('@@RELEASE@@', linuxPackageRevision)) .pipe(replace('@@RELEASE@@', linuxPackageRevision))
.pipe(replace('@@ARCHITECTURE@@', rpmArch)) .pipe(replace('@@ARCHITECTURE@@', rpmArch))
.pipe(replace('@@LICENSE@@', product.licenseName)) .pipe(replace('@@LICENSE@@', product.licenseName))
// @ts-ignore JSON checking: quality is optional
.pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@'))
// @ts-ignore JSON checking: updateUrl is optional
.pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@'))
.pipe(replace('@@DEPENDENCIES@@', rpmDependencies[rpmArch].join(', '))) .pipe(replace('@@DEPENDENCIES@@', rpmDependencies[rpmArch].join(', ')))
.pipe(rename('SPECS/' + product.applicationName + '.spec')); .pipe(rename('SPECS/' + product.applicationName + '.spec'));

View File

@@ -25,7 +25,6 @@ function watch(root) {
var child = cp.spawn(watcherPath, [root]); var child = cp.spawn(watcherPath, [root]);
child.stdout.on('data', function (data) { child.stdout.on('data', function (data) {
// @ts-ignore
var lines = data.toString('utf8').split('\n'); var lines = data.toString('utf8').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim(); var line = lines[i].trim();
@@ -47,7 +46,6 @@ function watch(root) {
path: changePathFull, path: changePathFull,
base: root base: root
}); });
//@ts-ignore
file.event = toChangeType(changeType); file.event = toChangeType(changeType);
result.emit('data', file); result.emit('data', file);
} }
@@ -106,4 +104,4 @@ module.exports = function (pattern, options) {
}); });
})) }))
.pipe(rebase); .pipe(rebase);
}; };

View File

@@ -89,5 +89,85 @@
"prependLicenseText": [ "prependLicenseText": [
"Copyright (c) Microsoft Corporation. All rights reserved." "Copyright (c) Microsoft Corporation. All rights reserved."
] ]
},
{
// Reason: The license at https://github.com/reem/rust-unreachable/blob/master/LICENSE-MIT
// cannot be found by the OSS tool automatically.
"name": "reem/rust-unreachable",
"fullLicenseText": [
"Copyright (c) 2015 The rust-unreachable Developers",
"",
"Permission is hereby granted, free of charge, to any person obtaining a copy",
"of this software and associated documentation files (the \"Software\"), to deal",
"in the Software without restriction, including without limitation the rights",
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
"copies of the Software, and to permit persons to whom the Software is",
"furnished to do so, subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be included in all",
"copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
"SOFTWARE."
]
},
{
// Reason: The license at https://github.com/reem/rust-void/blob/master/LICENSE-MIT
// cannot be found by the OSS tool automatically.
"name": "reem/rust-void",
"fullLicenseText": [
"Copyright (c) 2015 The rust-void Developers",
"",
"Permission is hereby granted, free of charge, to any person obtaining a copy",
"of this software and associated documentation files (the \"Software\"), to deal",
"in the Software without restriction, including without limitation the rights",
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
"copies of the Software, and to permit persons to whom the Software is",
"furnished to do so, subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be included in all",
"copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
"SOFTWARE."
]
},
{
// Reason: The license at https://github.com/mrhooray/crc-rs/blob/master/LICENSE-MIT
// cannot be found by the OSS tool automatically.
"name": "mrhooray/crc-rs",
"fullLicenseText": [
"MIT License",
"",
"Copyright (c) 2017 crc-rs Developers",
"",
"Permission is hereby granted, free of charge, to any person obtaining a copy",
"of this software and associated documentation files (the \"Software\"), to deal",
"in the Software without restriction, including without limitation the rights",
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
"copies of the Software, and to permit persons to whom the Software is",
"furnished to do so, subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be included in all",
"copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
"SOFTWARE."
]
} }
] ]

View File

@@ -8,13 +8,30 @@
'use strict'; 'use strict';
const withDefaults = require('../shared.webpack.config'); const withDefaults = require('../shared.webpack.config');
const fs = require('fs');
const path = require('path');
const externals = {
'node-fetch': 'commonjs node-fetch',
'bufferutil': 'commonjs bufferutil',
'utf-8-validate': 'commonjs utf-8-validate',
'keytar': 'commonjs keytar',
};
// conditionally add ws if we are going to be running in a node environment
const yarnrcPath = path.join(__dirname, '.yarnrc');
if (fs.existsSync(yarnrcPath)) {
const yarnrc = fs.readFileSync(yarnrcPath).toString();
const properties = yarnrc.split(/\r?\n/).map(r => r.split(' '));
if (properties.find(r => r[0] === 'runtime')[1] === '"node"') {
externals['ws'] = 'commonjs ws';
}
}
module.exports = withDefaults({ module.exports = withDefaults({
context: __dirname, context: __dirname,
entry: { entry: {
extension: './src/extension.ts' extension: './src/extension.ts'
}, },
externals: { externals: externals
'keytar': 'commonjs keytar'
}
}); });

View File

@@ -143,6 +143,14 @@
"title": "%azure.resource.selectsubscriptions.title%", "title": "%azure.resource.selectsubscriptions.title%",
"icon": "$(filter)" "icon": "$(filter)"
}, },
{
"command": "azure.resource.startterminal",
"title": "%azure.resource.startterminal.title%",
"icon": {
"dark": "resources/dark/console.svg",
"light": "resources/light/console.svg"
}
},
{ {
"command": "azure.resource.connectsqlserver", "command": "azure.resource.connectsqlserver",
"title": "%azure.resource.connectsqlserver.title%", "title": "%azure.resource.connectsqlserver.title%",
@@ -217,15 +225,40 @@
"when": "viewItem == azure.resource.itemType.account", "when": "viewItem == azure.resource.itemType.account",
"group": "inline" "group": "inline"
}, },
{
"command": "azure.resource.selectsubscriptions",
"when": "viewItem == azure.resource.itemType.account",
"group": "azurecore"
},
{ {
"command": "azure.resource.refresh", "command": "azure.resource.refresh",
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/", "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
"group": "inline" "group": "inline"
}, },
{
"command": "azure.resource.refresh",
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
"group": "azurecore"
},
{ {
"command": "azure.resource.connectsqlserver", "command": "azure.resource.connectsqlserver",
"when": "viewItem == azure.resource.itemType.databaseServer || viewItem == azure.resource.itemType.database || viewItem == azure.resource.itemType.sqlInstance", "when": "viewItem == azure.resource.itemType.databaseServer || viewItem == azure.resource.itemType.database || viewItem == azure.resource.itemType.sqlInstance",
"group": "inline" "group": "inline"
},
{
"command": "azure.resource.connectsqlserver",
"when": "viewItem == azure.resource.itemType.databaseServer || viewItem == azure.resource.itemType.database || viewItem == azure.resource.itemType.sqlInstance",
"group": "azurecore"
},
{
"command": "azure.resource.startterminal",
"when": "viewItem == azure.resource.itemType.account",
"group": "inline"
},
{
"command": "azure.resource.startterminal",
"when": "viewItem == azure.resource.itemType.account",
"group": "azurecore"
} }
] ]
}, },
@@ -234,11 +267,10 @@
"dependencies": { "dependencies": {
"@azure/arm-resourcegraph": "^2.0.0", "@azure/arm-resourcegraph": "^2.0.0",
"@azure/arm-subscriptions": "1.0.0", "@azure/arm-subscriptions": "1.0.0",
"adal-node": "^0.2.1",
"axios": "^0.19.2", "axios": "^0.19.2",
"qs": "^6.9.1", "qs": "^6.9.1",
"request": "2.88.0", "vscode-nls": "^4.0.0",
"vscode-nls": "^4.0.0" "ws": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/keytar": "^4.4.2", "@types/keytar": "^4.4.2",
@@ -246,6 +278,7 @@
"@types/node": "^12.11.7", "@types/node": "^12.11.7",
"@types/qs": "^6.9.1", "@types/qs": "^6.9.1",
"@types/request": "^2.48.1", "@types/request": "^2.48.1",
"@types/ws": "^6.0.4",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",

View File

@@ -10,6 +10,7 @@
"azure.resource.refresh.title": "Refresh", "azure.resource.refresh.title": "Refresh",
"azure.resource.signin.title": "Azure: Sign In", "azure.resource.signin.title": "Azure: Sign In",
"azure.resource.selectsubscriptions.title": "Select Subscriptions", "azure.resource.selectsubscriptions.title": "Select Subscriptions",
"azure.resource.startterminal.title": "Start Cloud Shell",
"azure.resource.connectsqlserver.title": "Connect", "azure.resource.connectsqlserver.title": "Connect",
"azure.resource.connectsqldb.title": "Add to Servers", "azure.resource.connectsqldb.title": "Add to Servers",

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V15H1V1ZM2 14H14V2H2V14ZM4.00008 5.70709L4.70718 4.99999L8.24272 8.53552L7.53561 9.24263L7.53558 9.2426L4.70711 12.0711L4 11.364L6.82848 8.53549L4.00008 5.70709Z" fill="#C5C5C5"/>
</svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V15H1V1ZM2 14H14V2H2V14ZM4.00008 5.70709L4.70718 4.99999L8.24272 8.53552L7.53561 9.24263L7.53558 9.2426L4.70711 12.0711L4 11.364L6.82848 8.53549L4.00008 5.70709Z" fill="#424242"/>
</svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@@ -124,6 +124,7 @@ export abstract class AzureAuth implements vscode.Disposable {
this.metadata.settings.sqlResource, this.metadata.settings.sqlResource,
this.metadata.settings.graphResource, this.metadata.settings.graphResource,
this.metadata.settings.ossRdbmsResource, this.metadata.settings.ossRdbmsResource,
this.metadata.settings.microsoftResource,
this.metadata.settings.azureKeyVaultResource this.metadata.settings.azureKeyVaultResource
]; ];
@@ -137,32 +138,47 @@ export abstract class AzureAuth implements vscode.Disposable {
public dispose() { } public dispose() { }
public async refreshAccess(account: azdata.Account): Promise<azdata.Account> { public async refreshAccess(oldAccount: azdata.Account): Promise<azdata.Account> {
const response = await this.getCachedToken(account.key); const response = await this.getCachedToken(oldAccount.key);
if (!response) { if (!response) {
account.isStale = true; oldAccount.isStale = true;
return account; return oldAccount;
} }
const refreshToken = response.refreshToken; const refreshToken = response.refreshToken;
if (!refreshToken || !refreshToken.key) { if (!refreshToken || !refreshToken.key) {
account.isStale = true; oldAccount.isStale = true;
return account; return oldAccount;
} }
try { try {
await this.refreshAccessToken(account.key, refreshToken); // Refresh the access token
const tokenResponse = await this.refreshAccessToken(oldAccount.key, refreshToken);
const tenants = await this.getTenants(tokenResponse.accessToken);
// Recreate account object
const newAccount = this.createAccount(tokenResponse.tokenClaims, tokenResponse.accessToken.key, tenants);
const subscriptions = await this.getSubscriptions(newAccount);
newAccount.properties.subscriptions = subscriptions;
return newAccount;
} catch (ex) { } catch (ex) {
oldAccount.isStale = true;
if (ex.message) { if (ex.message) {
await vscode.window.showErrorMessage(ex.message); await vscode.window.showErrorMessage(ex.message);
} }
console.log(ex); console.log(ex);
} }
return account; return oldAccount;
} }
public async getSecurityToken(account: azdata.Account, azureResource: azdata.AzureResource): Promise<TokenResponse | undefined> { public async getSecurityToken(account: azdata.Account, azureResource: azdata.AzureResource): Promise<TokenResponse | undefined> {
if (account.isStale === true) {
return undefined;
}
const resource = this.resources.find(s => s.azureResourceId === azureResource); const resource = this.resources.find(s => s.azureResourceId === azureResource);
if (!resource) { if (!resource) {
return undefined; return undefined;
@@ -199,8 +215,13 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!baseToken) { if (!baseToken) {
return undefined; return undefined;
} }
try {
await this.refreshAccessToken(account.key, baseToken.refreshToken, tenant, resource);
} catch (ex) {
account.isStale = true;
return undefined;
}
await this.refreshAccessToken(account.key, baseToken.refreshToken, tenant, resource);
cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id); cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id);
if (!cachedTokens) { if (!cachedTokens) {
return undefined; return undefined;
@@ -349,8 +370,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return { accessToken, refreshToken, tokenClaims }; return { accessToken, refreshToken, tokenClaims };
} catch (err) { } catch (err) {
console.dir(err); const msg = localize('azure.noToken', "Retrieving the Azure token failed. Please sign in again.");
const msg = localize('azure.noToken', "Retrieving the token failed.");
vscode.window.showErrorMessage(msg); vscode.window.showErrorMessage(msg);
throw new Error(err); throw new Error(err);
} }
@@ -365,7 +385,7 @@ export abstract class AzureAuth implements vscode.Disposable {
} }
} }
private async refreshAccessToken(account: azdata.AccountKey, rt: RefreshToken, tenant?: Tenant, resource?: Resource): Promise<void> { private async refreshAccessToken(account: azdata.AccountKey, rt: RefreshToken, tenant?: Tenant, resource?: Resource): Promise<TokenRefreshResponse> {
const postData: { [key: string]: string } = { const postData: { [key: string]: string } = {
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: rt.token, refresh_token: rt.token,
@@ -377,7 +397,10 @@ export abstract class AzureAuth implements vscode.Disposable {
postData.resource = resource.endpoint; postData.resource = resource.endpoint;
} }
const { accessToken, refreshToken } = await this.getToken(postData, tenant?.id, resource?.id); const getTokenResponse = await this.getToken(postData, tenant?.id, resource?.id);
const accessToken = getTokenResponse?.accessToken;
const refreshToken = getTokenResponse?.refreshToken;
if (!accessToken || !refreshToken) { if (!accessToken || !refreshToken) {
console.log('Access or refresh token were undefined'); console.log('Access or refresh token were undefined');
@@ -385,7 +408,9 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new Error(msg); throw new Error(msg);
} }
return this.setCachedToken(account, accessToken, refreshToken, resource?.id, tenant?.id); await this.setCachedToken(account, accessToken, refreshToken, resource?.id, tenant?.id);
return getTokenResponse;
} }

View File

@@ -69,7 +69,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
serverPort = await this.server.startup(); serverPort = await this.server.startup();
} catch (err) { } catch (err) {
const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.'); const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.');
await vscode.window.showErrorMessage(msg); vscode.window.showErrorMessage(msg);
console.dir(err); console.dir(err);
return undefined; return undefined;
} }
@@ -181,7 +181,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
refreshToken = rt; refreshToken = rt;
} catch (ex) { } catch (ex) {
if (ex.msg) { if (ex.msg) {
await vscode.window.showErrorMessage(ex.msg); vscode.window.showErrorMessage(ex.msg);
} }
console.log(ex); console.log(ex);
} }
@@ -199,7 +199,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
} catch (ex) { } catch (ex) {
console.log(ex); console.log(ex);
if (ex.msg) { if (ex.msg) {
await vscode.window.showErrorMessage(ex.msg); vscode.window.showErrorMessage(ex.msg);
authCompleteDeferred.reject(ex); authCompleteDeferred.reject(ex);
} else { } else {
authCompleteDeferred.reject(new Error('There was an issue when storing the cache.')); authCompleteDeferred.reject(new Error('There was an issue when storing the cache.'));

View File

@@ -128,7 +128,7 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
if (this.authMappings.size === 0) { if (this.authMappings.size === 0) {
console.log('No auth method was enabled.'); console.log('No auth method was enabled.');
await vscode.window.showErrorMessage(noAuthAvailable); vscode.window.showErrorMessage(noAuthAvailable);
return { canceled: true }; return { canceled: true };
} }
@@ -145,7 +145,7 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
if (!pick) { if (!pick) {
console.log('No auth method was selected.'); console.log('No auth method was selected.');
await vscode.window.showErrorMessage(noAuthSelected); vscode.window.showErrorMessage(noAuthSelected);
return { canceled: true }; return { canceled: true };
} }

View File

@@ -69,6 +69,11 @@ interface Settings {
*/ */
signInResourceId?: string; signInResourceId?: string;
/**
* Information that describes the Microsoft resource management resource
*/
microsoftResource?: Resource
/** /**
* Information that describes the AAD graph resource * Information that describes the AAD graph resource
*/ */

View File

@@ -18,6 +18,11 @@ const publicAzureSettings: ProviderSettings = {
host: 'https://login.microsoftonline.com/', host: 'https://login.microsoftonline.com/',
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255', clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
signInResourceId: 'https://management.core.windows.net/', signInResourceId: 'https://management.core.windows.net/',
microsoftResource: {
id: 'marm',
endpoint: 'https://management.core.windows.net/',
azureResourceId: AzureResource.MicrosoftResourceManagement
},
graphResource: { graphResource: {
id: 'graph', id: 'graph',
endpoint: 'https://graph.microsoft.com', endpoint: 'https://graph.microsoft.com',
@@ -62,6 +67,11 @@ const usGovAzureSettings: ProviderSettings = {
host: 'https://login.microsoftonline.us/', host: 'https://login.microsoftonline.us/',
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255', clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
signInResourceId: 'https://management.core.usgovcloudapi.net/', signInResourceId: 'https://management.core.usgovcloudapi.net/',
microsoftResource: {
id: 'marm',
endpoint: 'https://management.core.usgovcloudapi.net/',
azureResourceId: AzureResource.MicrosoftResourceManagement
},
graphResource: { graphResource: {
id: 'graph', id: 'graph',
endpoint: 'https://graph.windows.net', endpoint: 'https://graph.windows.net',

View File

@@ -1,317 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as adal from 'adal-node';
import * as azdata from 'azdata';
import * as crypto from 'crypto';
import { promises as fs } from 'fs';
export default class TokenCache implements adal.TokenCache {
private static CipherAlgorithm = 'aes-256-cbc';
private static CipherAlgorithmIvLength = 16;
private static CipherKeyLength = 32;
private static FsOptions = { encoding: 'ascii' };
private _activeOperation: Thenable<any>;
constructor(
private _credentialProvider: azdata.CredentialProvider,
private _credentialServiceKey: string,
private _cacheSerializationPath: string
) {
}
// PUBLIC METHODS //////////////////////////////////////////////////////
public add(entries: adal.TokenResponse[], callback: (error: Error, result: boolean) => void): void {
let self = this;
this.doOperation(() => {
return self.readCache()
.then(cache => self.addToCache(cache, entries))
.then(updatedCache => self.writeCache(updatedCache))
.then(
() => callback(null, true),
(err) => callback(err, false)
);
});
}
/**
* Wrapper to make callback-based add method into a thenable method
* @param entries Entries to add into the cache
* @returns Promise to return the result of adding the tokens to the cache
* Rejected if an error was sent in the callback
*/
public addThenable(entries: adal.TokenResponse[]): Thenable<boolean> {
let self = this;
return new Promise<boolean>((resolve, reject) => {
self.add(entries, (error: Error, results: boolean) => {
if (error) {
reject(error);
} else {
resolve(results);
}
});
});
}
public async clear(): Promise<void> {
// 1) Delete encrypted serialization file
// If we got an 'ENOENT' response, the file doesn't exist, which is fine
// 3) Delete the encryption key
try {
await fs.unlink(this._cacheSerializationPath);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
await this._credentialProvider.deleteCredential(this._credentialServiceKey);
}
public find(query: any, callback: (error: Error, results: any[]) => void): void {
let self = this;
this.doOperation(async () => {
try {
const cache = await self.readCache();
const filtered = cache.filter(entry => {
return TokenCache.findByPartial(entry, query);
});
callback(null, filtered);
} catch (ex) {
console.log(ex);
callback(ex, null);
}
});
}
/**
* Wrapper to make callback-based find method into a thenable method
* @param query Partial object to use to look up tokens. Ideally should be partial of adal.TokenResponse
* @returns Promise to return the matching adal.TokenResponse objects.
* Rejected if an error was sent in the callback
*/
public findThenable(query: any): Thenable<any[]> {
let self = this;
return new Promise<any[]>((resolve, reject) => {
self.find(query, (error: Error, results: any[]) => {
if (error) {
reject(error);
} else {
resolve(results);
}
});
});
}
public remove(entries: adal.TokenResponse[], callback: (error: Error, result: null) => void): void {
let self = this;
this.doOperation(() => {
return this.readCache()
.then(cache => self.removeFromCache(cache, entries))
.then(updatedCache => self.writeCache(updatedCache))
.then(
() => callback(null, null),
(err) => callback(err, null)
);
});
}
/**
* Wrapper to make callback-based remove method into a thenable method
* @param entries Array of entries to remove from the token cache
* @returns Promise to remove the given tokens from the token cache
* Rejected if an error was sent in the callback
*/
public removeThenable(entries: adal.TokenResponse[]): Thenable<void> {
let self = this;
return new Promise<void>((resolve, reject) => {
self.remove(entries, (error: Error, result: null) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
// PRIVATE METHODS /////////////////////////////////////////////////////
private static findByKeyHelper(entry1: adal.TokenResponse, entry2: adal.TokenResponse): boolean {
return entry1._authority === entry2._authority
&& entry1._clientId === entry2._clientId
&& entry1.userId === entry2.userId
&& entry1.resource === entry2.resource;
}
private static findByPartial(entry: adal.TokenResponse, query: { [key: string]: any }): boolean {
for (let key in query) {
if (entry[key] === undefined || entry[key] !== query[key]) {
return false;
}
}
return true;
}
private doOperation<T>(op: () => Thenable<T>): void {
// Initialize the active operation to an empty promise if necessary
let activeOperation = this._activeOperation || Promise.resolve<any>(null);
// Chain the operation to perform to the end of the existing promise
activeOperation = activeOperation.then(op);
// Add a catch at the end to make sure we can continue after any errors
activeOperation = activeOperation.then(null, err => {
console.error(`Failed to perform token cache operation: ${err}`);
});
// Point the current active operation to this one
this._activeOperation = activeOperation;
}
private addToCache(cache: adal.TokenResponse[], entries: adal.TokenResponse[]): adal.TokenResponse[] {
// First remove entries from the db that are being updated
cache = this.removeFromCache(cache, entries);
// Then add the new entries to the cache
entries.forEach((entry: adal.TokenResponse) => {
cache.push(entry);
});
return cache;
}
private getOrCreateEncryptionParams(): Thenable<EncryptionParams> {
let self = this;
return this._credentialProvider.readCredential(this._credentialServiceKey)
.then(credential => {
if (credential.password) {
// We already have encryption params, deserialize them
let splitValues = credential.password.split('|');
if (splitValues.length === 2 && splitValues[0] && splitValues[1]) {
try {
return <EncryptionParams>{
key: Buffer.from(splitValues[0], 'hex'),
initializationVector: Buffer.from(splitValues[1], 'hex')
};
} catch (e) {
// Swallow the error and fall through to generate new params
console.warn('Failed to deserialize encryption params, new ones will be generated.');
}
}
}
// We haven't stored encryption values, so generate them
let encryptKey = crypto.randomBytes(TokenCache.CipherKeyLength);
let initializationVector = crypto.randomBytes(TokenCache.CipherAlgorithmIvLength);
// Serialize the values
let serializedValues = `${encryptKey.toString('hex')}|${initializationVector.toString('hex')}`;
return self._credentialProvider.saveCredential(self._credentialServiceKey, serializedValues)
.then(() => {
return <EncryptionParams>{
key: encryptKey,
initializationVector: initializationVector
};
});
});
}
private async readCache(): Promise<adal.TokenResponse[]> {
let self = this;
// NOTE: File system operations are performed synchronously to avoid annoying nested callbacks
// 1) Get the encryption key
// 2) Read the encrypted token cache file
// 3) Decrypt the file contents
// 4) Deserialize and return
return this.getOrCreateEncryptionParams()
.then(async encryptionParams => {
try {
return self.decryptCache('utf8', encryptionParams);
} catch (e) {
try {
// try to parse using 'binary' encoding and rewrite cache as UTF8
let response = await self.decryptCache('binary', encryptionParams);
self.writeCache(response);
return response;
} catch (e) {
throw e;
}
}
})
.then(null, err => {
// If reading the token cache fails, we'll just assume the tokens are garbage
console.warn(`Failed to read token cache: ${err}`);
return [];
});
}
private async decryptCache(encoding: crypto.Utf8AsciiBinaryEncoding, encryptionParams: EncryptionParams): Promise<adal.TokenResponse[]> {
let cacheCipher = await fs.readFile(this._cacheSerializationPath, TokenCache.FsOptions);
let decipher = crypto.createDecipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector);
let cacheJson = decipher.update(cacheCipher.toString(), 'hex', encoding);
cacheJson += decipher.final(encoding);
// Deserialize the JSON into the array of tokens
let cacheObj = <adal.TokenResponse[]>JSON.parse(cacheJson);
for (const obj of cacheObj) {
// Rehydrate Date objects since they will always serialize as a string
obj.expiresOn = new Date(<string>obj.expiresOn);
}
return cacheObj;
}
private removeFromCache(cache: adal.TokenResponse[], entries: adal.TokenResponse[]): adal.TokenResponse[] {
entries.forEach((entry: adal.TokenResponse) => {
// Check to see if the entry exists
let match = cache.findIndex(entry2 => TokenCache.findByKeyHelper(entry, entry2));
if (match >= 0) {
// Entry exists, remove it from cache
cache.splice(match, 1);
}
});
return cache;
}
private writeCache(cache: adal.TokenResponse[]): Thenable<void> {
let self = this;
// NOTE: File system operations are being done synchronously to avoid annoying callback nesting
// 1) Get (or generate) the encryption key
// 2) Stringify the token cache entries
// 4) Encrypt the JSON
// 3) Write to the file
return this.getOrCreateEncryptionParams()
.then(async encryptionParams => {
try {
let cacheJson = JSON.stringify(cache);
let cipher = crypto.createCipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector);
let cacheCipher = cipher.update(cacheJson, 'utf8', 'hex');
cacheCipher += cipher.final('hex');
await fs.writeFile(self._cacheSerializationPath, cacheCipher, TokenCache.FsOptions);
} catch (e) {
throw e;
}
});
}
}
interface EncryptionParams {
key: Buffer;
initializationVector: Buffer;
}

View File

@@ -16,13 +16,58 @@ import { TreeNode } from './treeNode';
import { AzureResourceCredentialError } from './errors'; import { AzureResourceCredentialError } from './errors';
import { AzureResourceTreeProvider } from './tree/treeProvider'; import { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode'; import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../azureResource/interfaces'; import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants'; import { AzureResourceServiceNames } from './constants';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService'; import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { GetSubscriptionsResult, GetResourceGroupsResult } from '../azurecore'; import { GetSubscriptionsResult, GetResourceGroupsResult } from '../azurecore';
import { isArray } from 'util'; import { isArray } from 'util';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void { export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void {
appContext.apiWrapper.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
try {
if (!node || !(node instanceof AzureResourceAccountTreeNode)) {
return;
}
const accountNode = node as AzureResourceAccountTreeNode;
const azureAccount = accountNode.account as AzureAccount;
const tokens = await appContext.apiWrapper.getSecurityToken(azureAccount, azdata.AzureResource.MicrosoftResourceManagement);
const terminalService = appContext.getService<IAzureTerminalService>(AzureResourceServiceNames.terminalService);
const listOfTenants = azureAccount.properties.tenants.map(t => t.displayName);
if (listOfTenants.length === 0) {
window.showErrorMessage(localize('azure.noTenants', "A tenant is required for this feature. Your Azure subscription seems to have no tenants."));
return;
}
let tenant: Tenant;
window.setStatusBarMessage(localize('azure.startingCloudShell', "Starting cloud shell…"), 5000);
if (listOfTenants.length === 1) {
// Don't show quickpick for a single option
tenant = azureAccount.properties.tenants[0];
} else {
const pickedTenant = await window.showQuickPick(listOfTenants, { canPickMany: false });
if (!pickedTenant) {
window.showErrorMessage(localize('azure.mustPickTenant', "You must select a tenant for this feature to work."));
return;
}
// The tenant the user picked
tenant = azureAccount.properties.tenants[listOfTenants.indexOf(pickedTenant)];
}
await terminalService.getOrCreateCloudConsole(azureAccount, tenant, tokens);
} catch (ex) {
console.error(ex);
window.showErrorMessage(ex);
}
});
// Resource Management commands // Resource Management commands
appContext.apiWrapper.registerCommand('azure.accounts.getSubscriptions', async (account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> => { appContext.apiWrapper.registerCommand('azure.accounts.getSubscriptions', async (account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> => {
@@ -98,6 +143,7 @@ export function registerAzureResourceCommands(appContext: AppContext, tree: Azur
}); });
// Resource Tree commands // Resource Tree commands
appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => { appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => {
if (!(node instanceof AzureResourceAccountTreeNode)) { if (!(node instanceof AzureResourceAccountTreeNode)) {
return; return;

View File

@@ -20,5 +20,6 @@ export enum AzureResourceServiceNames {
accountService = 'AzureResourceAccountService', accountService = 'AzureResourceAccountService',
subscriptionService = 'AzureResourceSubscriptionService', subscriptionService = 'AzureResourceSubscriptionService',
subscriptionFilterService = 'AzureResourceSubscriptionFilterService', subscriptionFilterService = 'AzureResourceSubscriptionFilterService',
tenantService = 'AzureResourceTenantService' tenantService = 'AzureResourceTenantService',
terminalService = 'AzureTerminalService',
} }

View File

@@ -9,6 +9,7 @@ import { Account, DidChangeAccountsParams } from 'azdata';
import { Event } from 'vscode'; import { Event } from 'vscode';
import { azureResource } from './azure-resource'; import { azureResource } from './azure-resource';
import { AzureAccount, AzureAccountSecurityToken, Tenant } from '../account-provider/interfaces';
export interface IAzureResourceAccountService { export interface IAzureResourceAccountService {
getAccounts(): Promise<Account[]>; getAccounts(): Promise<Account[]>;
@@ -24,6 +25,10 @@ export interface IAzureResourceSubscriptionFilterService {
saveSelectedSubscriptions(account: Account, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void>; saveSelectedSubscriptions(account: Account, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void>;
} }
export interface IAzureTerminalService {
getOrCreateCloudConsole(account: AzureAccount, tenant: Tenant, tokens: { [key: string]: AzureAccountSecurityToken }): Promise<void>;
}
export interface IAzureResourceCacheService { export interface IAzureResourceCacheService {
generateKey(id: string): string; generateKey(id: string): string;

View File

@@ -0,0 +1,196 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import axios, { AxiosRequestConfig } from 'axios';
import * as WS from 'ws';
import { IAzureTerminalService } from '../interfaces';
import { AzureAccount, AzureAccountSecurityToken, Tenant } from '../../account-provider/interfaces';
const localize = nls.loadMessageBundle();
export class AzureTerminalService implements IAzureTerminalService {
private readonly apiVersion = '?api-version=2018-10-01';
public constructor(context: vscode.ExtensionContext) {
}
public async getOrCreateCloudConsole(account: AzureAccount, tenant: Tenant, tokens: { [key: string]: AzureAccountSecurityToken }): Promise<void> {
const token = tokens[tenant.id].token;
const settings: AxiosRequestConfig = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
validateStatus: () => true
};
const metadata = account.properties.providerSettings;
const userSettingsUri = this.getConsoleUserSettingsUri(metadata.settings.armResource.endpoint);
const userSettingsResult = await axios.get(userSettingsUri, settings);
const preferredShell = userSettingsResult.data?.properties?.preferredShellType ?? 'bash';
const preferredLocation = userSettingsResult.data?.properties?.preferredLocation;
const consoleRequestUri = this.getConsoleRequestUri(metadata.settings.armResource.endpoint);
if (preferredLocation) {
settings.headers['x-ms-console-preferred-location'] = preferredLocation;
}
const provisionResult = await axios.put(consoleRequestUri, {}, settings);
if (provisionResult.data?.properties?.provisioningState !== 'Succeeded') {
throw new Error(provisionResult.data);
}
const consoleUri = provisionResult.data.properties.uri;
return this.createTerminal(consoleUri, token, account.displayInfo.displayName, preferredShell);
}
private async createTerminal(provisionedUri: string, token: string, accountDisplayName: string, preferredShell: string): Promise<void> {
class ShellType implements vscode.QuickPickItem {
constructor(public readonly label: string, public readonly value: string) {
}
}
const shells = [new ShellType('PowerShell', 'pwsh'), new ShellType('Bash', 'bash'),];
const idx = shells.findIndex(s => s.value === preferredShell);
const prefShell = shells.splice(idx, 1);
shells.unshift(prefShell[0]);
let shell = await vscode.window.showQuickPick(shells, {
canPickMany: false,
placeHolder: localize('azure.selectShellType', "Select Bash or PowerShell for Azure Cloud Shell")
});
if (!shell) {
vscode.window.showErrorMessage(localize('azure.shellTypeRequired', "You must pick a shell type"));
return;
}
const terminalName = localize('azure.cloudShell', "Azure Cloud Shell (Preview)") + ` ${shell} (${accountDisplayName})`;
const azureTerminal = new AzureTerminal(provisionedUri, token, shell.value);
const terminal = vscode.window.createTerminal({
name: terminalName,
pty: azureTerminal
});
terminal.show();
}
public getConsoleRequestUri(armEndpoint: string): string {
return `${armEndpoint}/providers/Microsoft.Portal/consoles/default${this.apiVersion}`;
}
public getConsoleUserSettingsUri(armEndpoint: string): string {
return `${armEndpoint}/providers/Microsoft.Portal/userSettings/cloudconsole${this.apiVersion}`;
}
}
class AzureTerminal implements vscode.Pseudoterminal {
private readonly writeEmitter: vscode.EventEmitter<string>;
public readonly onDidWrite: vscode.Event<string>;
private socket: WS;
private intervalTimer: NodeJS.Timer;
private terminalDimensions: vscode.TerminalDimensions;
constructor(private readonly consoleUri: string, private readonly token: string, private shell: string) {
this.writeEmitter = new vscode.EventEmitter<string>();
this.onDidWrite = this.writeEmitter.event;
}
handleInput(data: string): void {
this.socket?.send(data);
}
async open(initialDimensions: vscode.TerminalDimensions): Promise<void> {
return this.resetTerminalSize(initialDimensions);
}
close(): void {
if (!this.socket) { return; }
this.socket.removeAllListeners('open');
this.socket.removeAllListeners('message');
this.socket.removeAllListeners('close');
this.socket.terminate();
if (this.intervalTimer) {
clearInterval(this.intervalTimer);
}
}
async setDimensions(dimensions: vscode.TerminalDimensions): Promise<void> {
return this.resetTerminalSize(dimensions);
}
private async resetTerminalSize(dimensions: vscode.TerminalDimensions): Promise<void> {
try {
if (!this.terminalDimensions) { // first time
this.writeEmitter.fire(localize('azure.connectingShellTerminal', "Connecting terminal...\n"));
}
if (dimensions) {
this.terminalDimensions = dimensions;
}
// Close the shell before this and restablish a new connection
this.close();
const terminalUri = await this.establishTerminal(this.terminalDimensions);
this.socket = new WS(terminalUri);
this.socket.on('message', (data: WS.Data) => {
// Write to the console
this.writeEmitter.fire(data.toString());
});
this.socket.on('close', () => {
this.writeEmitter.fire(localize('azure.shellClosed', "Shell closed.\n"));
this.close();
});
// Keep alives
this.intervalTimer = setInterval(() => {
this.socket.ping();
}, 5000);
} catch (ex) {
console.log(ex);
}
}
private async establishTerminal(dimensions: vscode.TerminalDimensions): Promise<string> {
const terminalResult = await axios.post(`${this.consoleUri}/terminals?rows=${dimensions.rows}&cols=${dimensions.columns}&shell=${this.shell}`, undefined, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
}
});
const terminalUri = terminalResult.data?.socketUri;
if (terminalResult.data.error) {
vscode.window.showErrorMessage(terminalResult.data.error.message);
}
if (!terminalUri) {
console.log(terminalResult);
throw new Error(terminalResult.data);
}
return terminalUri;
}
}

View File

@@ -79,6 +79,16 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
if (subscriptions.length === 0) { if (subscriptions.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)]; return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
} else { } else {
// Filter out everything that we can't authenticate to.
subscriptions = subscriptions.filter(s => {
const token = tokens[s.id];
if (!token) {
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
return false;
}
return true;
});
let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => { let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
const token = tokens[subscription.id]; const token = tokens[subscription.id];
const tenantId = await this._tenantService.getTenantId(subscription, this.account, new TokenCredentials(token.token, token.tokenType)); const tenantId = await this._tenantService.getTenantId(subscription, this.account, new TokenCredentials(token.token, token.tokenType));

View File

@@ -17,7 +17,7 @@ import { AzureResourceDatabaseServerService } from './azureResource/providers/da
import { AzureResourceDatabaseProvider } from './azureResource/providers/database/databaseProvider'; import { AzureResourceDatabaseProvider } from './azureResource/providers/database/databaseProvider';
import { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService'; import { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService';
import { AzureResourceService } from './azureResource/resourceService'; import { AzureResourceService } from './azureResource/resourceService';
import { IAzureResourceCacheService, IAzureResourceAccountService, IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceTenantService } from './azureResource/interfaces'; import { IAzureResourceCacheService, IAzureResourceAccountService, IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceTenantService, IAzureTerminalService } from './azureResource/interfaces';
import { AzureResourceServiceNames } from './azureResource/constants'; import { AzureResourceServiceNames } from './azureResource/constants';
import { AzureResourceAccountService } from './azureResource/services/accountService'; import { AzureResourceAccountService } from './azureResource/services/accountService';
import { AzureResourceSubscriptionService } from './azureResource/services/subscriptionService'; import { AzureResourceSubscriptionService } from './azureResource/services/subscriptionService';
@@ -30,6 +30,7 @@ import { SqlInstanceResourceService } from './azureResource/providers/sqlinstanc
import { SqlInstanceProvider } from './azureResource/providers/sqlinstance/sqlInstanceProvider'; import { SqlInstanceProvider } from './azureResource/providers/sqlinstance/sqlInstanceProvider';
import { PostgresServerProvider } from './azureResource/providers/postgresServer/postgresServerProvider'; import { PostgresServerProvider } from './azureResource/providers/postgresServer/postgresServerProvider';
import { PostgresServerService } from './azureResource/providers/postgresServer/postgresServerService'; import { PostgresServerService } from './azureResource/providers/postgresServer/postgresServerService';
import { AzureTerminalService } from './azureResource/services/terminalService';
import { SqlInstanceArcProvider } from './azureResource/providers/sqlinstanceArc/sqlInstanceArcProvider'; import { SqlInstanceArcProvider } from './azureResource/providers/sqlinstanceArc/sqlInstanceArcProvider';
import { SqlInstanceArcResourceService } from './azureResource/providers/sqlinstanceArc/sqlInstanceArcService'; import { SqlInstanceArcResourceService } from './azureResource/providers/sqlinstanceArc/sqlInstanceArcService';
import { PostgresServerArcProvider } from './azureResource/providers/postgresArcServer/postgresServerProvider'; import { PostgresServerArcProvider } from './azureResource/providers/postgresArcServer/postgresServerProvider';
@@ -145,6 +146,7 @@ function registerAzureServices(appContext: AppContext): void {
appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService()); appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());
appContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext))); appContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext)));
appContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, new AzureResourceTenantService()); appContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, new AzureResourceTenantService());
appContext.registerService<IAzureTerminalService>(AzureResourceServiceNames.terminalService, new AzureTerminalService(extensionContext));
} }
async function onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent, apiWrapper: ApiWrapper): Promise<void> { async function onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent, apiWrapper: ApiWrapper): Promise<void> {

View File

@@ -1,36 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as os from 'os';
import * as adal from 'adal-node';
import * as path from 'path';
import 'mocha';
import CredentialServiceTokenCache from '../../account-provider/tokenCache';
import { CredentialsTestProvider } from '../stubs/credentialsTestProvider';
describe('AccountProvider.TokenCache', function (): void {
it('Can save and load tokens', async function (): Promise<void> {
const tokenResponse: adal.TokenResponse = {
tokenType: 'testTokenType',
expiresIn: 0,
expiresOn: new Date(),
resource: 'testResource',
accessToken: 'testAccessToken'
};
const tokenCacheKey = 'azureTokenCache-testkey';
const tokenCachePath = path.join(os.tmpdir(), tokenCacheKey);
const credentialProvider = new CredentialsTestProvider();
credentialProvider.saveCredential(tokenCacheKey, undefined);
const tokenCache = new CredentialServiceTokenCache(credentialProvider, tokenCacheKey, tokenCachePath);
const addResult = await tokenCache.addThenable([tokenResponse]);
should(addResult).true('TokenResponse not added correctly');
const results = await tokenCache.findThenable({ tokenType: 'testTokenType' });
should(results).deepEqual([tokenResponse]);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@ export class PreviewManager implements vscode.CustomEditorProvider {
) { } ) { }
public async openCustomDocument(uri: vscode.Uri) { public async openCustomDocument(uri: vscode.Uri) {
return new vscode.CustomDocument(PreviewManager.viewType, uri); return new vscode.CustomDocument(uri);
} }
public async resolveCustomEditor( public async resolveCustomEditor(

View File

@@ -9,7 +9,8 @@
"azdata": "*" "azdata": "*"
}, },
"activationEvents": [ "activationEvents": [
"*" "onFileSystem:memfs",
"onDebug"
], ],
"main": "./out/main", "main": "./out/main",
"extensionDependencies": [ "extensionDependencies": [
@@ -17,7 +18,8 @@
"Microsoft.import", "Microsoft.import",
"Microsoft.profiler", "Microsoft.profiler",
"Microsoft.mssql", "Microsoft.mssql",
"Microsoft.notebook" "Microsoft.notebook",
"Microsoft.azuredatastudio-postgresql"
], ],
"contributes": { "contributes": {
"configuration": { "configuration": {

View File

@@ -4,122 +4,11 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { normalize, join } from 'path';
import * as fs from 'fs';
const TEST_SETUP_COMPLETED_TEXT: string = 'Test Setup Completed';
const EXTENSION_LOADED_TEXT: string = 'Test Extension Loaded';
const ALL_EXTENSION_LOADED_TEXT: string = 'All Extensions Loaded';
let statusBarItemTimer: NodeJS.Timer;
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
let statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
vscode.commands.registerCommand('test.setupIntegrationTest', async () => {
let extensionInstallersFolder = normalize(join(__dirname, '../extensionInstallers'));
console.info(`extensionInstallersFolder=${extensionInstallersFolder}`);
// eslint-disable-next-line no-sync
let installers = fs.readdirSync(extensionInstallersFolder);
for (let i = 0; i < installers.length; i++) {
if (installers[i].endsWith('.vsix')) {
let installerFullPath = join(extensionInstallersFolder, installers[i]);
console.info(`installing extension at ${installerFullPath}`);
await azdata.extensions.install(installerFullPath);
console.info(`extension has been installed successfully. vsix: ${installers[i]}`);
}
}
await setConfiguration('workbench.enablePreviewFeatures', true);
await setConfiguration('workbench.showConnectDialogOnStartup', false);
await setConfiguration('test.testSetupCompleted', true);
showStatusBarItem(statusBarItem, TEST_SETUP_COMPLETED_TEXT);
});
vscode.commands.registerCommand('test.waitForExtensionsToLoad', async () => {
const expectedExtensions = ['Microsoft.agent', 'Microsoft.import', 'Microsoft.mssql', 'Microsoft.profiler', 'Microsoft.azuredatastudio-postgresql'];
const commonFeatures: azdata.DataProviderType[] = [
azdata.DataProviderType.AdminServicesProvider,
azdata.DataProviderType.BackupProvider,
azdata.DataProviderType.CapabilitiesProvider,
azdata.DataProviderType.ConnectionProvider,
azdata.DataProviderType.FileBrowserProvider,
azdata.DataProviderType.MetadataProvider,
azdata.DataProviderType.ObjectExplorerProvider,
azdata.DataProviderType.ProfilerProvider,
azdata.DataProviderType.QueryProvider,
azdata.DataProviderType.RestoreProvider,
azdata.DataProviderType.ScriptingProvider,
azdata.DataProviderType.TaskServicesProvider];
const features_mssql: azdata.DataProviderType[] = [
azdata.DataProviderType.AgentServicesProvider,
azdata.DataProviderType.IconProvider
];
features_mssql.push(...commonFeatures);
const providerFeatureMapping: { providerId: string, features: azdata.DataProviderType[] }[] = [
{
providerId: 'MSSQL',
features: features_mssql
}, {
providerId: 'PGSQL',
features: commonFeatures
}];
do {
let extensions = vscode.extensions.all.filter(ext => { return expectedExtensions.indexOf(ext.id) !== -1; });
const extensionsNotInReadyState: string[] = [];
extensions.forEach(extension => {
if (!extension.isActive) {
extensionsNotInReadyState.push(extension.id);
}
});
const providerTypesNotInReadyState: string[] = [];
if (extensionsNotInReadyState.length === 0) {
providerFeatureMapping.forEach(entry => {
entry.features.forEach(feature => {
const provider = azdata.dataprotocol.getProvider(entry.providerId, feature);
if (!provider) {
providerTypesNotInReadyState.push(`${entry.providerId}:${feature}`);
}
});
});
}
if (extensionsNotInReadyState.length === 0 && providerTypesNotInReadyState.length === 0) {
console.info('All extensions are ready');
showStatusBarItem(statusBarItem, ALL_EXTENSION_LOADED_TEXT);
break;
} else if (extensionsNotInReadyState.length !== 0) {
console.warn(`the following extensions are not ready: ${extensionsNotInReadyState.join(',')}`);
} else {
console.warn(`the following providers are not ready: ${providerTypesNotInReadyState.join(',')}`);
}
await new Promise(resolve => { setTimeout(resolve, 2000); });
}
while (true);
});
showStatusBarItem(statusBarItem, EXTENSION_LOADED_TEXT);
}
function showStatusBarItem(statusBarItem: vscode.StatusBarItem, text: string) {
statusBarItem.text = text;
statusBarItem.tooltip = text;
statusBarItem.show();
clearTimeout(statusBarItemTimer);
statusBarItemTimer = setTimeout(function () {
statusBarItem.hide();
}, 5000);
} }
// this method is called when your extension is deactivated // this method is called when your extension is deactivated
export function deactivate(): void { export function deactivate(): void {
} }
async function setConfiguration(name: string, value: any) {
await vscode.workspace.getConfiguration().update(name, value, true);
}

View File

@@ -1,35 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { isTestSetupCompleted } from './testContext';
import * as assert from 'assert';
import { getConfigValue, EnvironmentVariable_BDC_SERVER, EnvironmentVariable_BDC_USERNAME, EnvironmentVariable_BDC_PASSWORD, EnvironmentVariable_AZURE_PASSWORD, EnvironmentVariable_AZURE_SERVER, EnvironmentVariable_AZURE_USERNAME, EnvironmentVariable_STANDALONE_PASSWORD, EnvironmentVariable_STANDALONE_SERVER, EnvironmentVariable_STANDALONE_USERNAME, EnvironmentVariable_PYTHON_PATH } from './testConfig';
assert(getConfigValue(EnvironmentVariable_BDC_SERVER) !== undefined &&
getConfigValue(EnvironmentVariable_BDC_USERNAME) !== undefined &&
getConfigValue(EnvironmentVariable_BDC_PASSWORD) !== undefined &&
getConfigValue(EnvironmentVariable_AZURE_PASSWORD) !== undefined &&
getConfigValue(EnvironmentVariable_AZURE_SERVER) !== undefined &&
getConfigValue(EnvironmentVariable_AZURE_USERNAME) !== undefined &&
getConfigValue(EnvironmentVariable_STANDALONE_PASSWORD) !== undefined &&
getConfigValue(EnvironmentVariable_STANDALONE_SERVER) !== undefined &&
getConfigValue(EnvironmentVariable_STANDALONE_USERNAME) !== undefined &&
getConfigValue(EnvironmentVariable_PYTHON_PATH) !== undefined, 'Required environment variables are not set, if you see this error in the build pipeline, make sure the environment variables are set properly in the build definition, otherwise for local dev environment make sure you follow the instructions in the readme file.');
if (!isTestSetupCompleted()) {
suite('integration test setup', () => {
test('test setup', async function () {
this.timeout(5 * 60 * 1000);
// Prepare the environment and make it ready for testing
await vscode.commands.executeCommand('test.setupIntegrationTest');
// Wait for the extensions to load
await vscode.commands.executeCommand('test.waitForExtensionsToLoad');
// Reload the window, this is required for some changes made by the 'test.setupIntegrationTest' to work
await vscode.commands.executeCommand('workbench.action.reloadWindow');
});
});
}

View File

@@ -6,12 +6,12 @@
import 'mocha'; import 'mocha';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as mssql from '../../mssql'; import * as mssql from '../../../mssql';
import * as utils from './utils'; import * as utils from '../utils';
import * as uuid from './uuid'; import * as uuid from '../uuid';
import { isTestSetupCompleted } from './testContext'; import { isTestSetupCompleted } from '../testContext';
import assert = require('assert'); import assert = require('assert');
import { getStandaloneServer, TestServerProfile } from './testConfig'; import { getStandaloneServer, TestServerProfile } from '../testConfig';
let cmsService: mssql.ICmsService; let cmsService: mssql.ICmsService;
let server: TestServerProfile; let server: TestServerProfile;

View File

@@ -5,14 +5,14 @@
import 'mocha'; import 'mocha';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as utils from './utils'; import * as utils from '../utils';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as mssql from '../../mssql'; import * as mssql from '../../../mssql';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { isTestSetupCompleted } from './testContext'; import { isTestSetupCompleted } from '../testContext';
import { getStandaloneServer } from './testConfig'; import { getStandaloneServer } from '../testConfig';
import * as assert from 'assert'; import * as assert from 'assert';
import { promisify } from 'util'; import { promisify } from 'util';

View File

@@ -7,10 +7,10 @@ import 'mocha';
import * as assert from 'assert'; import * as assert from 'assert';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { isTestSetupCompleted } from './testContext'; import { isTestSetupCompleted } from '../testContext';
import { sqlNotebookContent, writeNotebookToFile, sqlKernelMetadata, getFileName, pySparkNotebookContent, pySparkKernelMetadata, pythonKernelMetadata, sqlNotebookMultipleCellsContent, notebookContentForCellLanguageTest, sqlKernelSpec, pythonKernelSpec, pySparkKernelSpec, CellTypes } from './notebook.util'; import { sqlNotebookContent, writeNotebookToFile, sqlKernelMetadata, getFileName, pySparkNotebookContent, pySparkKernelMetadata, pythonKernelMetadata, sqlNotebookMultipleCellsContent, notebookContentForCellLanguageTest, sqlKernelSpec, pythonKernelSpec, pySparkKernelSpec, CellTypes } from '../notebook.util';
import { getConfigValue, EnvironmentVariable_PYTHON_PATH, TestServerProfile, getStandaloneServer } from './testConfig'; import { getConfigValue, EnvironmentVariable_PYTHON_PATH, TestServerProfile, getStandaloneServer } from '../testConfig';
import { connectToServer, sleep, testServerProfileToIConnectionProfile } from './utils'; import { connectToServer, sleep, testServerProfileToIConnectionProfile } from '../utils';
import * as fs from 'fs'; import * as fs from 'fs';
import { stressify } from 'adstest'; import { stressify } from 'adstest';
import { isNullOrUndefined, promisify } from 'util'; import { isNullOrUndefined, promisify } from 'util';

View File

@@ -5,9 +5,9 @@
import 'mocha'; import 'mocha';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { isTestSetupCompleted } from './testContext'; import { isTestSetupCompleted } from '../testContext';
import { getBdcServer, TestServerProfile, getAzureServer, getStandaloneServer } from './testConfig'; import { getBdcServer, TestServerProfile, getAzureServer, getStandaloneServer } from '../testConfig';
import { connectToServer, createDB, deleteDB, DefaultConnectTimeoutInMs, asyncTimeout } from './utils'; import { connectToServer, createDB, deleteDB, DefaultConnectTimeoutInMs, asyncTimeout } from '../utils';
import * as assert from 'assert'; import * as assert from 'assert';
import { stressify } from 'adstest'; import { stressify } from 'adstest';

View File

@@ -6,14 +6,14 @@
import 'mocha'; import 'mocha';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as utils from './utils'; import * as utils from '../utils';
import * as mssql from '../../mssql'; import * as mssql from '../../../mssql';
import * as os from 'os'; import * as os from 'os';
import * as fs from 'fs'; import * as fs from 'fs';
const path = require('path'); const path = require('path');
import { isTestSetupCompleted } from './testContext'; import { isTestSetupCompleted } from '../testContext';
import * as assert from 'assert'; import * as assert from 'assert';
import { getStandaloneServer } from './testConfig'; import { getStandaloneServer } from '../testConfig';
import { stressify } from 'adstest'; import { stressify } from 'adstest';
import { promisify } from 'util'; import { promisify } from 'util';

View File

@@ -0,0 +1,4 @@
{
"workbench.enablePreviewFeatures": true,
"workbench.showConnectDialogOnStartup": false
}

View File

@@ -1,13 +1,9 @@
{ {
"sqlPackageManagement": { "sqlPackageManagement": {
"requiredPythonPackages": [ "requiredPythonPackages": [
{
"name": "pymssql",
"version": "2.1.4"
},
{ {
"name": "sqlmlutils", "name": "sqlmlutils",
"version": "" "version": "1.0.0"
} }
], ],
"requiredRPackages": [ "requiredRPackages": [

View File

@@ -37,7 +37,7 @@
}, },
"machineLearningServices.enableR": { "machineLearningServices.enableR": {
"type": "boolean", "type": "boolean",
"default": "true", "default": "false",
"description": "%mls.enableR.description%" "description": "%mls.enableR.description%"
}, },
"machineLearningServices.pythonPath": { "machineLearningServices.pythonPath": {

View File

@@ -47,7 +47,8 @@ export const msgYes = localize('msgYes', "Yes");
export const msgNo = localize('msgNo', "No"); export const msgNo = localize('msgNo', "No");
export const managePackageCommandError = localize('mls.managePackages.error', "Either no connection is available or the server does not have external script enabled."); export const managePackageCommandError = localize('mls.managePackages.error', "Either no connection is available or the server does not have external script enabled.");
export function taskFailedError(taskName: string, err: string): string { return localize('mls.taskFailedError.error', "Failed to complete task '{0}'. Error: {1}", taskName, err); } export function taskFailedError(taskName: string, err: string): string { return localize('mls.taskFailedError.error', "Failed to complete task '{0}'. Error: {1}", taskName, err); }
export const installDependenciesMsgTaskName = localize('mls.installDependencies.msgTaskName', "Installing Machine Learning extension dependencies"); export const installPackageMngDependenciesMsgTaskName = localize('mls.installPackageMngDependencies.msgTaskName', "Installing package management dependencies");
export const installModelMngDependenciesMsgTaskName = localize('mls.installModelMngDependencies.msgTaskName', "Installing model management dependencies");
export const noResultError = localize('mls.noResultError', "No Result returned"); export const noResultError = localize('mls.noResultError', "No Result returned");
export const requiredPackagesNotInstalled = localize('mls.requiredPackagesNotInstalled', "The required dependencies are not installed"); export const requiredPackagesNotInstalled = localize('mls.requiredPackagesNotInstalled', "The required dependencies are not installed");
export const confirmEnableExternalScripts = localize('mls.confirmEnableExternalScripts', "External script is required for package management. Are you sure you want to enable that."); export const confirmEnableExternalScripts = localize('mls.confirmEnableExternalScripts', "External script is required for package management. Are you sure you want to enable that.");

View File

@@ -7,22 +7,32 @@ import * as azdata from 'azdata';
import * as nbExtensionApis from '../typings/notebookServices'; import * as nbExtensionApis from '../typings/notebookServices';
import { ApiWrapper } from './apiWrapper'; import { ApiWrapper } from './apiWrapper';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import * as utils from '../common/utils';
const maxNumberOfRetries = 3; const maxNumberOfRetries = 2;
const listPythonPackagesQuery = ` const listPythonPackagesQuery = `
Declare @tablevar table(name NVARCHAR(MAX), version NVARCHAR(MAX))
insert into @tablevar(name, version)
EXEC sp_execute_external_script EXEC sp_execute_external_script
@language=N'Python', @language=N'Python',
@script=N'import pkg_resources @script=N'import pkg_resources
import pandas import pandas
OutputDataSet = pandas.DataFrame([(d.project_name, d.version) for d in pkg_resources.working_set])' OutputDataSet = pandas.DataFrame([(d.project_name, d.version) for d in pkg_resources.working_set])'
select e.name, version from sys.external_libraries e join @tablevar t on e.name = t.name
where [language] = 'PYTHON'
`; `;
const listRPackagesQuery = ` const listRPackagesQuery = `
Declare @tablevar table(name NVARCHAR(MAX), version NVARCHAR(MAX))
insert into @tablevar(name, version)
EXEC sp_execute_external_script EXEC sp_execute_external_script
@language=N'R', @language=N'R',
@script=N' @script=N'
OutputDataSet <- as.data.frame(installed.packages()[,c(1,3)])' OutputDataSet <- as.data.frame(installed.packages()[,c(1,3)])'
select e.name, version from sys.external_libraries e join @tablevar t on e.name = t.name
where [language] = 'R'
`; `;
const checkMlInstalledQuery = ` const checkMlInstalledQuery = `
@@ -63,24 +73,24 @@ export class QueryRunner {
* Returns python packages installed in SQL server instance * Returns python packages installed in SQL server instance
* @param connection SQL Connection * @param connection SQL Connection
*/ */
public async getPythonPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> { public async getPythonPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this.getPackages(connection, listPythonPackagesQuery); return this.getPackages(connection, databaseName, listPythonPackagesQuery);
} }
/** /**
* Returns python packages installed in SQL server instance * Returns python packages installed in SQL server instance
* @param connection SQL Connection * @param connection SQL Connection
*/ */
public async getRPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> { public async getRPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this.getPackages(connection, listRPackagesQuery); return this.getPackages(connection, databaseName, listRPackagesQuery);
} }
private async getPackages(connection: azdata.connection.ConnectionProfile, script: string): Promise<nbExtensionApis.IPackageDetails[]> { private async getPackages(connection: azdata.connection.ConnectionProfile, databaseName: string, script: string): Promise<nbExtensionApis.IPackageDetails[]> {
let packages: nbExtensionApis.IPackageDetails[] = []; let packages: nbExtensionApis.IPackageDetails[] = [];
let result: azdata.SimpleExecuteResult | undefined = undefined; let result: azdata.SimpleExecuteResult | undefined = undefined;
for (let index = 0; index < maxNumberOfRetries; index++) { for (let index = 0; index < maxNumberOfRetries; index++) {
result = await this.runQuery(connection, script); result = await this.runQuery(connection, utils.getScriptWithDBChange(connection.databaseName, databaseName, script));
if (result && result.rowCount > 0) { if (result && result.rowCount > 0) {
break; break;
} }

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as mssql from '../../../mssql/src/mssql'; import * as mssql from '../../../mssql';
import { ApiWrapper } from '../common/apiWrapper'; import { ApiWrapper } from '../common/apiWrapper';
/** /**

View File

@@ -40,7 +40,7 @@ export class ModelPythonClient {
* Installs dependencies for python client * Installs dependencies for python client
*/ */
private async installDependencies(): Promise<void> { private async installDependencies(): Promise<void> {
await utils.executeTasks(this._apiWrapper, constants.installDependenciesMsgTaskName, [ await utils.executeTasks(this._apiWrapper, constants.installModelMngDependenciesMsgTaskName, [
this._packageManager.installRequiredPythonPackages(this._config.modelsRequiredPythonPackages)], true); this._packageManager.installRequiredPythonPackages(this._config.modelsRequiredPythonPackages)], true);
} }

View File

@@ -5,7 +5,6 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { ApiWrapper } from '../common/apiWrapper'; import { ApiWrapper } from '../common/apiWrapper';
import * as constants from '../common/constants';
import * as nbExtensionApis from '../typings/notebookServices'; import * as nbExtensionApis from '../typings/notebookServices';
import * as utils from '../common/utils'; import * as utils from '../common/utils';
@@ -23,14 +22,17 @@ export abstract class SqlPackageManageProviderBase {
} }
/** /**
* Returns location title * Returns database names
*/ */
public async getLocationTitle(): Promise<string> { public async getLocations(): Promise<nbExtensionApis.IPackageLocation[]> {
let connection = await this.getCurrentConnection(); let connection = await this.getCurrentConnection();
if (connection) { if (connection) {
return `${connection.serverName} ${connection.databaseName ? connection.databaseName : ''}`; let databases = await this._apiWrapper.listDatabases(connection.connectionId);
return databases.map(x => {
return { displayName: x, name: x };
});
} }
return constants.noConnectionError; return [];
} }
protected async getCurrentConnection(): Promise<azdata.connection.ConnectionProfile> { protected async getCurrentConnection(): Promise<azdata.connection.ConnectionProfile> {
@@ -42,16 +44,16 @@ export abstract class SqlPackageManageProviderBase {
* @param packages Packages to install * @param packages Packages to install
* @param useMinVersion minimum version * @param useMinVersion minimum version
*/ */
public async installPackages(packages: nbExtensionApis.IPackageDetails[], useMinVersion: boolean): Promise<void> { public async installPackages(packages: nbExtensionApis.IPackageDetails[], useMinVersion: boolean, databaseName: string): Promise<void> {
if (packages) { if (packages) {
await Promise.all(packages.map(x => this.installPackage(x, useMinVersion))); await Promise.all(packages.map(x => this.installPackage(x, useMinVersion, databaseName)));
} }
//TODO: use useMinVersion //TODO: use useMinVersion
console.log(useMinVersion); console.log(useMinVersion);
} }
private async installPackage(packageDetail: nbExtensionApis.IPackageDetails, useMinVersion: boolean): Promise<void> { private async installPackage(packageDetail: nbExtensionApis.IPackageDetails, useMinVersion: boolean, databaseName: string): Promise<void> {
if (useMinVersion) { if (useMinVersion) {
let packageOverview = await this.getPackageOverview(packageDetail.name); let packageOverview = await this.getPackageOverview(packageDetail.name);
if (packageOverview && packageOverview.versions) { if (packageOverview && packageOverview.versions) {
@@ -60,16 +62,16 @@ export abstract class SqlPackageManageProviderBase {
} }
} }
await this.executeScripts(ScriptMode.Install, packageDetail); await this.executeScripts(ScriptMode.Install, packageDetail, databaseName);
} }
/** /**
* Uninstalls given packages * Uninstalls given packages
* @param packages Packages to uninstall * @param packages Packages to uninstall
*/ */
public async uninstallPackages(packages: nbExtensionApis.IPackageDetails[]): Promise<void> { public async uninstallPackages(packages: nbExtensionApis.IPackageDetails[], databaseName: string): Promise<void> {
if (packages) { if (packages) {
await Promise.all(packages.map(x => this.executeScripts(ScriptMode.Uninstall, x))); await Promise.all(packages.map(x => this.executeScripts(ScriptMode.Uninstall, x, databaseName)));
} }
} }
@@ -88,8 +90,8 @@ export abstract class SqlPackageManageProviderBase {
/** /**
* Returns list of packages * Returns list of packages
*/ */
public async listPackages(): Promise<nbExtensionApis.IPackageDetails[]> { public async listPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
let packages = await this.fetchPackages(); let packages = await this.fetchPackages(databaseName);
if (packages) { if (packages) {
packages = packages.sort((a, b) => this.comparePackages(a, b)); packages = packages.sort((a, b) => this.comparePackages(a, b));
} else { } else {
@@ -110,6 +112,6 @@ export abstract class SqlPackageManageProviderBase {
} }
protected abstract fetchPackage(packageName: string): Promise<nbExtensionApis.IPackageOverview>; protected abstract fetchPackage(packageName: string): Promise<nbExtensionApis.IPackageOverview>;
protected abstract fetchPackages(): Promise<nbExtensionApis.IPackageDetails[]>; protected abstract fetchPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]>;
protected abstract executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails): Promise<void>; protected abstract executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails, databaseName: string): Promise<void>;
} }

View File

@@ -103,15 +103,15 @@ export class PackageManagementService {
* Returns python packages installed in SQL server instance * Returns python packages installed in SQL server instance
* @param connection SQL Connection * @param connection SQL Connection
*/ */
public async getPythonPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> { public async getPythonPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this._queryRunner.getPythonPackages(connection); return this._queryRunner.getPythonPackages(connection, databaseName);
} }
/** /**
* Returns python packages installed in SQL server instance * Returns python packages installed in SQL server instance
* @param connection SQL Connection * @param connection SQL Connection
*/ */
public async getRPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> { public async getRPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this._queryRunner.getRPackages(connection); return this._queryRunner.getRPackages(connection, databaseName);
} }
} }

View File

@@ -93,7 +93,6 @@ export class PackageManager {
// Execute the command // Execute the command
// //
this._apiWrapper.executeCommand(constants.managePackagesCommand, { this._apiWrapper.executeCommand(constants.managePackagesCommand, {
multiLocations: false,
defaultLocation: defaultProvider.packageTarget.location, defaultLocation: defaultProvider.packageTarget.location,
defaultProviderId: defaultProvider.providerId defaultProviderId: defaultProvider.providerId
}); });
@@ -116,7 +115,7 @@ export class PackageManager {
* Installs dependencies for the extension * Installs dependencies for the extension
*/ */
public async installDependencies(): Promise<void> { public async installDependencies(): Promise<void> {
await utils.executeTasks(this._apiWrapper, constants.installDependenciesMsgTaskName, [ await utils.executeTasks(this._apiWrapper, constants.installPackageMngDependenciesMsgTaskName, [
this.installRequiredPythonPackages(this._config.requiredSqlPythonPackages), this.installRequiredPythonPackages(this._config.requiredSqlPythonPackages),
this.installRequiredRPackages()], true); this.installRequiredRPackages()], true);
} }
@@ -130,7 +129,7 @@ export class PackageManager {
} }
await utils.createFolder(utils.getRPackagesFolderPath(this._rootFolder)); await utils.createFolder(utils.getRPackagesFolderPath(this._rootFolder));
await Promise.all(this._config.requiredSqlPythonPackages.map(x => this.installRPackage(x))); await Promise.all(this._config.requiredSqlRPackages.map(x => this.installRPackage(x)));
} }
/** /**
@@ -151,7 +150,8 @@ export class PackageManager {
let fileContent = ''; let fileContent = '';
requiredPackages.forEach(packageDetails => { requiredPackages.forEach(packageDetails => {
let hasVersion = ('version' in packageDetails) && !isNullOrUndefined(packageDetails['version']) && packageDetails['version'].length > 0; let hasVersion = ('version' in packageDetails) && !isNullOrUndefined(packageDetails['version']) && packageDetails['version'].length > 0;
if (!installedPackages.find(x => x.name === packageDetails['name'] && (!hasVersion || packageDetails['version'] === x.version))) { if (!installedPackages.find(x => x.name === packageDetails['name']
&& (!hasVersion || utils.comparePackageVersions(packageDetails['version'] || '', x.version) <= 0))) {
let packageNameDetail = hasVersion ? `${packageDetails.name}==${packageDetails.version}` : `${packageDetails.name}`; let packageNameDetail = hasVersion ? `${packageDetails.name}==${packageDetails.version}` : `${packageDetails.name}`;
fileContent = `${fileContent}${packageNameDetail}\n`; fileContent = `${fileContent}${packageNameDetail}\n`;
} }
@@ -177,7 +177,7 @@ export class PackageManager {
private async getInstalledPipPackages(): Promise<nbExtensionApis.IPackageDetails[]> { private async getInstalledPipPackages(): Promise<nbExtensionApis.IPackageDetails[]> {
try { try {
let cmd = `"${this.pythonExecutable}" -m pip list --format=json`; let cmd = `"${this.pythonExecutable}" -m pip list --format=json`;
let packagesInfo = await this._processService.executeBufferedCommand(cmd, this._outputChannel); let packagesInfo = await this._processService.executeBufferedCommand(cmd, undefined);
let packagesResult: nbExtensionApis.IPackageDetails[] = []; let packagesResult: nbExtensionApis.IPackageDetails[] = [];
if (packagesInfo) { if (packagesInfo) {
packagesResult = <nbExtensionApis.IPackageDetails[]>JSON.parse(packagesInfo); packagesResult = <nbExtensionApis.IPackageDetails[]>JSON.parse(packagesInfo);

View File

@@ -9,7 +9,7 @@ import * as nbExtensionApis from '../typings/notebookServices';
import { ApiWrapper } from '../common/apiWrapper'; import { ApiWrapper } from '../common/apiWrapper';
import { ProcessService } from '../common/processService'; import { ProcessService } from '../common/processService';
import { Config } from '../configurations/config'; import { Config } from '../configurations/config';
import { SqlPackageManageProviderBase, ScriptMode } from './SqlPackageManageProviderBase'; import { SqlPackageManageProviderBase, ScriptMode } from './packageManageProviderBase';
import { HttpClient } from '../common/httpClient'; import { HttpClient } from '../common/httpClient';
import * as utils from '../common/utils'; import * as utils from '../common/utils';
import { PackageManagementService } from './packageManagementService'; import { PackageManagementService } from './packageManagementService';
@@ -50,8 +50,8 @@ export class SqlPythonPackageManageProvider extends SqlPackageManageProviderBase
/** /**
* Returns list of packages * Returns list of packages
*/ */
protected async fetchPackages(): Promise<nbExtensionApis.IPackageDetails[]> { protected async fetchPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return await this._service.getPythonPackages(await this.getCurrentConnection()); return await this._service.getPythonPackages(await this.getCurrentConnection(), databaseName);
} }
/** /**
@@ -59,14 +59,14 @@ export class SqlPythonPackageManageProvider extends SqlPackageManageProviderBase
* @param packageDetails Packages to install or uninstall * @param packageDetails Packages to install or uninstall
* @param scriptMode can be 'install' or 'uninstall' * @param scriptMode can be 'install' or 'uninstall'
*/ */
protected async executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails): Promise<void> { protected async executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails, databaseName: string): Promise<void> {
let connection = await this.getCurrentConnection(); let connection = await this.getCurrentConnection();
let credentials = await this._apiWrapper.getCredentials(connection.connectionId); let credentials = await this._apiWrapper.getCredentials(connection.connectionId);
if (connection) { if (connection) {
let port = '1433'; let port = '1433';
let server = connection.serverName; let server = connection.serverName;
let database = connection.databaseName ? `, database="${connection.databaseName}"` : ''; let database = databaseName ? `, database="${databaseName}"` : '';
let index = connection.serverName.indexOf(','); let index = connection.serverName.indexOf(',');
if (index > 0) { if (index > 0) {
port = connection.serverName.substring(index + 1); port = connection.serverName.substring(index + 1);

View File

@@ -10,7 +10,7 @@ import * as nbExtensionApis from '../typings/notebookServices';
import { ApiWrapper } from '../common/apiWrapper'; import { ApiWrapper } from '../common/apiWrapper';
import { ProcessService } from '../common/processService'; import { ProcessService } from '../common/processService';
import { Config } from '../configurations/config'; import { Config } from '../configurations/config';
import { SqlPackageManageProviderBase, ScriptMode } from './SqlPackageManageProviderBase'; import { SqlPackageManageProviderBase, ScriptMode } from './packageManageProviderBase';
import { HttpClient } from '../common/httpClient'; import { HttpClient } from '../common/httpClient';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import { PackageManagementService } from './packageManagementService'; import { PackageManagementService } from './packageManagementService';
@@ -54,8 +54,8 @@ export class SqlRPackageManageProvider extends SqlPackageManageProviderBase impl
/** /**
* Returns list of packages * Returns list of packages
*/ */
protected async fetchPackages(): Promise<nbExtensionApis.IPackageDetails[]> { protected async fetchPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return await this._service.getRPackages(await this.getCurrentConnection()); return await this._service.getRPackages(await this.getCurrentConnection(), databaseName);
} }
/** /**
@@ -63,12 +63,12 @@ export class SqlRPackageManageProvider extends SqlPackageManageProviderBase impl
* @param packageDetails Packages to install or uninstall * @param packageDetails Packages to install or uninstall
* @param scriptMode can be 'install' or 'uninstall' * @param scriptMode can be 'install' or 'uninstall'
*/ */
protected async executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails): Promise<void> { protected async executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails, databaseName: string): Promise<void> {
let connection = await this.getCurrentConnection(); let connection = await this.getCurrentConnection();
let credentials = await this._apiWrapper.getCredentials(connection.connectionId); let credentials = await this._apiWrapper.getCredentials(connection.connectionId);
if (connection) { if (connection) {
let database = connection.databaseName ? `, database="${connection.databaseName}"` : ''; let database = databaseName ? `, database="${databaseName}"` : '';
let connectionParts = `server="${connection.serverName}", uid="${connection.userName}", pwd="${credentials[azdata.ConnectionOptionSpecialType.password]}"${database}`; let connectionParts = `server="${connection.serverName}", uid="${connection.userName}", pwd="${credentials[azdata.ConnectionOptionSpecialType.password]}"${database}`;
let rCommandScript = scriptMode === ScriptMode.Install ? 'sql_install.packages' : 'sql_remove.packages'; let rCommandScript = scriptMode === ScriptMode.Install ? 'sql_install.packages' : 'sql_remove.packages';

View File

@@ -116,7 +116,7 @@ describe('Package Manager', () => {
it('installDependencies Should install packages that are not already installed', async function (): Promise<void> { it('installDependencies Should install packages that are not already installed', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
//let packagesInstalled = false; let packagesInstalled = false;
let installedPackages = `[ let installedPackages = `[
{"name":"pymssql","version":"2.1.4"} {"name":"pymssql","version":"2.1.4"}
]`; ]`;
@@ -128,15 +128,67 @@ describe('Package Manager', () => {
}); });
testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => { testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => {
if (command.indexOf('pip install') > 0) { if (command.indexOf('pip install') > 0) {
//packagesInstalled = true; packagesInstalled = true;
} }
return Promise.resolve(installedPackages); return Promise.resolve(installedPackages);
}); });
let packageManager = createPackageManager(testContext); let packageManager = createPackageManager(testContext);
await packageManager.installDependencies(); await packageManager.installDependencies();
//should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded); should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded);
//should.equal(packagesInstalled, true); should.equal(packagesInstalled, true);
});
it('installDependencies Should not install packages if runtime is disabled in setting', async function (): Promise<void> {
let testContext = createContext();
testContext.config.setup(x => x.rEnabled).returns(() => false);
testContext.config.setup(x => x.pythonEnabled).returns(() => false);
let packagesInstalled = false;
let installedPackages = `[
{"name":"pymssql","version":"2.1.4"}
]`;
testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
label: 'Yes'
}));
testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => {
operationInfo.operation(testContext.op);
});
testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => {
if (command.indexOf('pip install') > 0 || command.indexOf('install.packages') > 0) {
packagesInstalled = true;
}
return Promise.resolve(installedPackages);
});
let packageManager = createPackageManager(testContext);
await packageManager.installDependencies();
should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded);
should.equal(packagesInstalled, false);
});
it('installDependencies Should install packages that have older version installed', async function (): Promise<void> {
let testContext = createContext();
let packagesInstalled = false;
let installedPackages = `[
{"name":"sqlmlutils","version":"0.1.1"}
]`;
testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
label: 'Yes'
}));
testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => {
operationInfo.operation(testContext.op);
});
testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => {
if (command.indexOf('pip install') > 0) {
packagesInstalled = true;
}
return Promise.resolve(installedPackages);
});
let packageManager = createPackageManager(testContext);
await packageManager.installDependencies();
should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded);
should.equal(packagesInstalled, true);
}); });
it('installDependencies Should install packages if list packages fails', async function (): Promise<void> { it('installDependencies Should install packages if list packages fails', async function (): Promise<void> {
@@ -197,7 +249,7 @@ describe('Package Manager', () => {
{ name: 'pymssql', version: '2.1.4' }, { name: 'pymssql', version: '2.1.4' },
{ name: 'sqlmlutils', version: '' } { name: 'sqlmlutils', version: '' }
]); ]);
testContext.config.setup(x => x.requiredSqlPythonPackages).returns( () => [ testContext.config.setup(x => x.requiredSqlRPackages).returns( () => [
{ name: 'RODBCext', repository: 'https://cran.microsoft.com' }, { name: 'RODBCext', repository: 'https://cran.microsoft.com' },
{ name: 'sqlmlutils', fileName: 'sqlmlutils_0.7.1.zip', downloadUrl: 'https://github.com/microsoft/sqlmlutils/blob/master/R/dist/sqlmlutils_0.7.1.zip?raw=true'} { name: 'sqlmlutils', fileName: 'sqlmlutils_0.7.1.zip', downloadUrl: 'https://github.com/microsoft/sqlmlutils/blob/master/R/dist/sqlmlutils_0.7.1.zip?raw=true'}
]); ]);

View File

@@ -7,7 +7,6 @@ import * as azdata from 'azdata';
import * as should from 'should'; import * as should from 'should';
import 'mocha'; import 'mocha';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import * as constants from '../../common/constants';
import { SqlPythonPackageManageProvider } from '../../packageManagement/sqlPythonPackageManageProvider'; import { SqlPythonPackageManageProvider } from '../../packageManagement/sqlPythonPackageManageProvider';
import { createContext, TestContext } from './utils'; import { createContext, TestContext } from './utils';
import * as nbExtensionApis from '../../typings/notebookServices'; import * as nbExtensionApis from '../../typings/notebookServices';
@@ -40,10 +39,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(packages));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected = [ let expected = [
{ {
'name': 'a-name', 'name': 'a-name',
@@ -72,10 +71,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(packages));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected = [ let expected = [
{ {
'name': 'b-name', 'name': 'b-name',
@@ -95,10 +94,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
let packages: nbExtensionApis.IPackageDetails[]; let packages: nbExtensionApis.IPackageDetails[];
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(packages));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = []; let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected); should.deepEqual(actual, expected);
}); });
@@ -108,10 +107,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); testContext.serverConfigManager.setup(x => x.getPythonPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = []; let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected); should.deepEqual(actual, expected);
}); });
@@ -152,7 +151,7 @@ describe('SQL Python Package Manager', () => {
}); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.installPackages(packages, false); await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true); should.deepEqual(packagesUpdated, true);
}); });
@@ -192,7 +191,7 @@ describe('SQL Python Package Manager', () => {
}); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.uninstallPackages(packages); await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, true); should.deepEqual(packagesUpdated, true);
}); });
@@ -233,7 +232,7 @@ describe('SQL Python Package Manager', () => {
}); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.installPackages(packages, false); await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true); should.deepEqual(packagesUpdated, true);
}); });
@@ -255,7 +254,7 @@ describe('SQL Python Package Manager', () => {
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.installPackages(packages, false); await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, false); should.deepEqual(packagesUpdated, false);
}); });
@@ -277,7 +276,7 @@ describe('SQL Python Package Manager', () => {
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.uninstallPackages(packages); await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, false); should.deepEqual(packagesUpdated, false);
}); });
@@ -346,42 +345,44 @@ describe('SQL Python Package Manager', () => {
should.deepEqual(actual, packagePreview); should.deepEqual(actual, packagePreview);
}); });
it('getLocationTitle Should default string for no connection', async function (): Promise<void> { it('getLocations Should return empty array for no connection', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
let connection: azdata.connection.ConnectionProfile; let connection: azdata.connection.ConnectionProfile;
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.getLocationTitle(); let actual = await provider.getLocations();
should.deepEqual(actual, constants.noConnectionError); should.deepEqual(actual, []);
}); });
it('getLocationTitle Should return connection title string for valid connection', async function (): Promise<void> { it('getLocations Should return database names for valid connection', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName'; connection.serverName = 'serverName';
connection.databaseName = 'databaseName'; connection.databaseName = 'databaseName';
const databaseNames = [
'db1',
'db2'
];
const expected = [
{
displayName: 'db1',
name: 'db1'
},
{
displayName: 'db2',
name: 'db2'
}
];
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.listDatabases(connection.connectionId)).returns(() => { return Promise.resolve(databaseNames); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.getLocationTitle(); let actual = await provider.getLocations();
should.deepEqual(actual, `${connection.serverName} ${connection.databaseName}`); should.deepEqual(actual, expected);
});
it('getLocationTitle Should return server name as connection title if there is not database name', async function (): Promise<void> {
let testContext = createContext();
let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName';
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
let provider = createProvider(testContext);
let actual = await provider.getLocationTitle();
should.deepEqual(actual, `${connection.serverName} `);
}); });
function createProvider(testContext: TestContext): SqlPythonPackageManageProvider { function createProvider(testContext: TestContext): SqlPythonPackageManageProvider {

View File

@@ -7,7 +7,6 @@ import * as azdata from 'azdata';
import * as should from 'should'; import * as should from 'should';
import 'mocha'; import 'mocha';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import * as constants from '../../common/constants';
import { SqlRPackageManageProvider } from '../../packageManagement/sqlRPackageManageProvider'; import { SqlRPackageManageProvider } from '../../packageManagement/sqlRPackageManageProvider';
import { createContext, TestContext } from './utils'; import { createContext, TestContext } from './utils';
import * as nbExtensionApis from '../../typings/notebookServices'; import * as nbExtensionApis from '../../typings/notebookServices';
@@ -40,10 +39,10 @@ describe('SQL R Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getRPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); testContext.serverConfigManager.setup(x => x.getRPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(packages));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected = [ let expected = [
{ {
'name': 'a-name', 'name': 'a-name',
@@ -63,10 +62,10 @@ describe('SQL R Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
let packages: nbExtensionApis.IPackageDetails[]; let packages: nbExtensionApis.IPackageDetails[];
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getRPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); testContext.serverConfigManager.setup(x => x.getRPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(packages));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = []; let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected); should.deepEqual(actual, expected);
}); });
@@ -76,10 +75,10 @@ describe('SQL R Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.serverConfigManager.setup(x => x.getRPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); testContext.serverConfigManager.setup(x => x.getRPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.listPackages(); let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = []; let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected); should.deepEqual(actual, expected);
}); });
@@ -118,7 +117,7 @@ describe('SQL R Package Manager', () => {
}); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.installPackages(packages, false); await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true); should.deepEqual(packagesUpdated, true);
}); });
@@ -157,7 +156,7 @@ describe('SQL R Package Manager', () => {
}); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.uninstallPackages(packages); await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, true); should.deepEqual(packagesUpdated, true);
}); });
@@ -179,7 +178,7 @@ describe('SQL R Package Manager', () => {
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.installPackages(packages, false); await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, false); should.deepEqual(packagesUpdated, false);
}); });
@@ -201,7 +200,7 @@ describe('SQL R Package Manager', () => {
let provider = createProvider(testContext); let provider = createProvider(testContext);
await provider.uninstallPackages(packages); await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, false); should.deepEqual(packagesUpdated, false);
}); });
@@ -271,42 +270,44 @@ describe('SQL R Package Manager', () => {
should.deepEqual(actual, packagePreview); should.deepEqual(actual, packagePreview);
}); });
it('getLocationTitle Should default string for no connection', async function (): Promise<void> { it('getLocations Should return empty array for no connection', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
let connection: azdata.connection.ConnectionProfile; let connection: azdata.connection.ConnectionProfile;
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.getLocationTitle(); let actual = await provider.getLocations();
should.deepEqual(actual, constants.noConnectionError); should.deepEqual(actual, []);
}); });
it('getLocationTitle Should return connection title string for valid connection', async function (): Promise<void> { it('getLocations Should return database names for valid connection', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName'; connection.serverName = 'serverName';
connection.databaseName = 'databaseName'; connection.databaseName = 'databaseName';
const databaseNames = [
'db1',
'db2'
];
const expected = [
{
displayName: 'db1',
name: 'db1'
},
{
displayName: 'db2',
name: 'db2'
}
];
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.listDatabases(connection.connectionId)).returns(() => { return Promise.resolve(databaseNames); });
let provider = createProvider(testContext); let provider = createProvider(testContext);
let actual = await provider.getLocationTitle(); let actual = await provider.getLocations();
should.deepEqual(actual, `${connection.serverName} ${connection.databaseName}`); should.deepEqual(actual, expected);
});
it('getLocationTitle Should return server name as connection title if there is not database name', async function (): Promise<void> {
let testContext = createContext();
let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName';
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
let provider = createProvider(testContext);
let actual = await provider.getLocationTitle();
should.deepEqual(actual, `${connection.serverName} `);
}); });
function createProvider(testContext: TestContext): SqlRPackageManageProvider { function createProvider(testContext: TestContext): SqlRPackageManageProvider {

View File

@@ -59,7 +59,7 @@ describe('Query Runner', () => {
let queryProvider: azdata.QueryProvider; let queryProvider: azdata.QueryProvider;
testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => queryProvider); testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => queryProvider);
let actual = await queryRunner.getPythonPackages(connection); let actual = await queryRunner.getPythonPackages(connection, connection.databaseName);
should.deepEqual(actual, []); should.deepEqual(actual, []);
}); });
@@ -70,7 +70,7 @@ describe('Query Runner', () => {
testContext.queryProvider.runQueryAndReturn = () => { return Promise.reject(); }; testContext.queryProvider.runQueryAndReturn = () => { return Promise.reject(); };
testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider);
let actual = await queryRunner.getPythonPackages(connection); let actual = await queryRunner.getPythonPackages(connection, connection.databaseName);
should.deepEqual(actual, []); should.deepEqual(actual, []);
}); });
@@ -117,7 +117,7 @@ describe('Query Runner', () => {
testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); };
testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider);
let actual = await queryRunner.getPythonPackages(connection); let actual = await queryRunner.getPythonPackages(connection, connection.databaseName);
should.deepEqual(actual, expected); should.deepEqual(actual, expected);
}); });
@@ -138,7 +138,7 @@ describe('Query Runner', () => {
testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); };
testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); testContext.apiWrapper.setup(x => x.getProvider<azdata.QueryProvider>(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider);
let actual = await queryRunner.getPythonPackages(connection); let actual = await queryRunner.getPythonPackages(connection, connection.databaseName);
should.deepEqual(actual, expected); should.deepEqual(actual, expected);
}); });

View File

@@ -8,7 +8,7 @@ import 'mocha';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { createContext } from './utils'; import { createContext } from './utils';
import { LanguageController } from '../../../views/externalLanguages/languageController'; import { LanguageController } from '../../../views/externalLanguages/languageController';
import * as mssql from '../../../../../mssql/src/mssql'; import * as mssql from '../../../../../mssql';
describe('External Languages Controller', () => { describe('External Languages Controller', () => {
it('Should open dialog for manage languages successfully ', async function (): Promise<void> { it('Should open dialog for manage languages successfully ', async function (): Promise<void> {

View File

@@ -6,7 +6,7 @@
import * as should from 'should'; import * as should from 'should';
import 'mocha'; import 'mocha';
import { createContext } from './utils'; import { createContext } from './utils';
import * as mssql from '../../../../../mssql/src/mssql'; import * as mssql from '../../../../../mssql';
import { LanguageService } from '../../../externalLanguage/languageService'; import { LanguageService } from '../../../externalLanguage/languageService';
describe('External Languages Dialog Model', () => { describe('External Languages Dialog Model', () => {

View File

@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { ApiWrapper } from '../../../common/apiWrapper'; import { ApiWrapper } from '../../../common/apiWrapper';
import { LanguageViewBase } from '../../../views/externalLanguages/languageViewBase'; import { LanguageViewBase } from '../../../views/externalLanguages/languageViewBase';
import * as mssql from '../../../../../mssql/src/mssql'; import * as mssql from '../../../../../mssql';
import { LanguageService } from '../../../externalLanguage/languageService'; import { LanguageService } from '../../../externalLanguage/languageService';
import { createViewContext } from '../utils'; import { createViewContext } from '../utils';

View File

@@ -51,13 +51,56 @@ export interface IPackageOverview {
summary: string; summary: string;
} }
export interface IPackageManageProvider { export interface IPackageLocation {
providerId: string; name: string;
packageTarget: IPackageTarget; displayName: string;
listPackages(): Promise<IPackageDetails[]> }
installPackages(package: IPackageDetails[], useMinVersion: boolean): Promise<void>;
uninstallPackages(package: IPackageDetails[]): Promise<void>; /**
canUseProvider(): Promise<boolean>; * Package manage provider interface
getLocationTitle(): Promise<string>; */
getPackageOverview(packageName: string): Promise<IPackageOverview> export interface IPackageManageProvider {
/**
* Provider id
*/
providerId: string;
/**
* package target
*/
packageTarget: IPackageTarget;
/**
* Returns list of installed packages
*/
listPackages(location?: string): Promise<IPackageDetails[]>;
/**
* Installs give packages
* @param package Packages to install
* @param useMinVersion if true, minimal version will be used
*/
installPackages(package: IPackageDetails[], useMinVersion: boolean, location?: string): Promise<void>;
/**
* Uninstalls given packages
* @param package package to uninstall
*/
uninstallPackages(package: IPackageDetails[], location?: string): Promise<void>;
/**
* Returns true if the provider can be used in current context
*/
canUseProvider(): Promise<boolean>;
/**
* Returns location title
*/
getLocations(): Promise<IPackageLocation[]>;
/**
* Returns Package Overview
* @param packageName package name
*/
getPackageOverview(packageName: string): Promise<IPackageOverview>;
} }

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as mssql from '../../../../mssql/src/mssql'; import * as mssql from '../../../../mssql';
import { LanguageViewBase } from './languageViewBase'; import { LanguageViewBase } from './languageViewBase';
import * as constants from '../../common/constants'; import * as constants from '../../common/constants';
import { ApiWrapper } from '../../common/apiWrapper'; import { ApiWrapper } from '../../common/apiWrapper';

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as mssql from '../../../../mssql/src/mssql'; import * as mssql from '../../../../mssql';
import { ApiWrapper } from '../../common/apiWrapper'; import { ApiWrapper } from '../../common/apiWrapper';
import { LanguageService } from '../../externalLanguage/languageService'; import { LanguageService } from '../../externalLanguage/languageService';
import { LanguagesDialog } from './languagesDialog'; import { LanguagesDialog } from './languagesDialog';

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as constants from '../../common/constants'; import * as constants from '../../common/constants';
import { ApiWrapper } from '../../common/apiWrapper'; import { ApiWrapper } from '../../common/apiWrapper';
import * as mssql from '../../../../mssql/src/mssql'; import * as mssql from '../../../../mssql';
import * as path from 'path'; import * as path from 'path';
export interface LanguageUpdateModel { export interface LanguageUpdateModel {

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as constants from '../../common/constants'; import * as constants from '../../common/constants';
import * as mssql from '../../../../mssql/src/mssql'; import * as mssql from '../../../../mssql';
import { LanguageViewBase } from './languageViewBase'; import { LanguageViewBase } from './languageViewBase';
import { ApiWrapper } from '../../common/apiWrapper'; import { ApiWrapper } from '../../common/apiWrapper';

View File

@@ -151,7 +151,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
} }
public async openCustomDocument(uri: vscode.Uri) { public async openCustomDocument(uri: vscode.Uri) {
return new vscode.CustomDocument(this.customEditorViewType, uri); return new vscode.CustomDocument(uri);
} }
public async resolveCustomTextEditor( public async resolveCustomTextEditor(

View File

@@ -485,6 +485,32 @@
} }
] ]
} }
},
{
"id": "mssql-databases",
"description": "%mssql.tabs.databases%",
"provider": "*",
"title": "%mssql.tabs.databases%",
"when": "dashboardContext == 'server'",
"group": "home",
"icon": {
"light": "resources/light/database.svg",
"dark": "resources/dark/database_inverse.svg"
},
"container": {
"widgets-container": [
{
"name": "%explorer-widget-title%",
"gridItemConfig": {
"sizex": 3,
"sizey": 3
},
"widget": {
"explorer-widget": {}
}
}
]
}
} }
], ],
"connectionProvider": { "connectionProvider": {

View File

@@ -140,5 +140,8 @@
"mssql.connectionOptions.packetSize.displayName": "Packet size", "mssql.connectionOptions.packetSize.displayName": "Packet size",
"mssql.connectionOptions.packetSize.description": "Size in bytes of the network packets used to communicate with an instance of SQL Server", "mssql.connectionOptions.packetSize.description": "Size in bytes of the network packets used to communicate with an instance of SQL Server",
"mssql.connectionOptions.typeSystemVersion.displayName": "Type system version", "mssql.connectionOptions.typeSystemVersion.displayName": "Type system version",
"mssql.connectionOptions.typeSystemVersion.description": "Indicates which server type system then provider will expose through the DataReader" "mssql.connectionOptions.typeSystemVersion.description": "Indicates which server type system the provider will expose through the DataReader",
"mssql.tabs.databases": "Databases",
"explorer-widget-title": "Search"
} }

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.07 15.91"><defs><style>.cls-1{fill:#fff;}</style></defs><title>Database_Inverse@2x</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M6.53,0C4.13,0,.11.45,0,2.12V13.66c0,1.77,4.1,2.25,6.53,2.25s6.54-.48,6.54-2.25V2.12C13,.45,8.94,0,6.53,0ZM12,13.66c-.14.39-2.18,1.16-5.45,1.16S1.2,14,1.09,13.66V3.49a14.55,14.55,0,0,0,5.44.87A14.57,14.57,0,0,0,12,3.49ZM12,2.18c-.18.38-2.19,1.09-5.45,1.09S1.32,2.58,1.09,2.19c.23-.39,2.22-1.09,5.44-1.09s5.26.72,5.45,1.06h0Z"/><polygon class="cls-1" points="11.99 2.17 11.98 2.18 11.98 2.15 11.99 2.17"/></g></g></svg> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:none;}</style></defs><title>bv</title><path class="cls-1" d="M7.5,1q.563,0,1.246.062a10.962,10.962,0,0,1,1.367.215,7.406,7.406,0,0,1,1.3.418,3.558,3.558,0,0,1,1.039.664,1.991,1.991,0,0,1,.4.516A1.327,1.327,0,0,1,13,3.5v9a1.292,1.292,0,0,1-.152.629,2.2,2.2,0,0,1-.4.512,3.24,3.24,0,0,1-1.027.66,7.741,7.741,0,0,1-1.313.418,10.706,10.706,0,0,1-1.379.219Q8.047,15,7.5,15q-.562,0-1.246-.062a10.962,10.962,0,0,1-1.367-.215,7.323,7.323,0,0,1-1.3-.418,3.538,3.538,0,0,1-1.039-.664,1.958,1.958,0,0,1-.395-.516A1.322,1.322,0,0,1,2,12.5v-9a1.3,1.3,0,0,1,.152-.629,2.217,2.217,0,0,1,.4-.512A3.257,3.257,0,0,1,3.574,1.7a7.924,7.924,0,0,1,1.313-.418,10.883,10.883,0,0,1,1.379-.219Q6.953,1,7.5,1Zm0,1c-.266,0-.543.01-.832.027s-.579.05-.871.094-.577.1-.856.172a6.4,6.4,0,0,0-.793.254c-.078.031-.182.08-.312.144a3.808,3.808,0,0,0-.383.223,1.645,1.645,0,0,0-.32.277A.47.47,0,0,0,3,3.5a.3.3,0,0,0,.023.117.618.618,0,0,0,.063.11,1.445,1.445,0,0,0,.473.437,3.925,3.925,0,0,0,.695.328,6.414,6.414,0,0,0,.824.238q.438.1.871.157t.836.086Q7.189,5,7.5,5t.715-.027q.4-.027.836-.086t.871-.157a6.414,6.414,0,0,0,.824-.238,3.827,3.827,0,0,0,.692-.328,1.542,1.542,0,0,0,.476-.437.618.618,0,0,0,.063-.11A.3.3,0,0,0,12,3.5a.47.47,0,0,0-.133-.309,1.645,1.645,0,0,0-.32-.277,3.808,3.808,0,0,0-.383-.223c-.13-.064-.234-.113-.312-.144a6.4,6.4,0,0,0-.793-.254q-.418-.105-.856-.172t-.871-.094C8.043,2.01,7.766,2,7.5,2Zm0,12q.4,0,.832-.027t.871-.094q.438-.066.856-.168a5.182,5.182,0,0,0,.793-.258q.117-.046.312-.144a3.461,3.461,0,0,0,.383-.223,1.612,1.612,0,0,0,.32-.277A.47.47,0,0,0,12,12.5V4.984a4.572,4.572,0,0,1-1.008.485,8.312,8.312,0,0,1-1.168.312,11.149,11.149,0,0,1-1.207.168Q8.015,6,7.5,6T6.379,5.949a11.047,11.047,0,0,1-1.207-.168,8.368,8.368,0,0,1-1.164-.312A4.572,4.572,0,0,1,3,4.984V12.5a.47.47,0,0,0,.133.309,1.612,1.612,0,0,0,.32.277,3.461,3.461,0,0,0,.383.223q.2.1.312.144a5.182,5.182,0,0,0,.793.258q.418.1.856.168t.871.094Q7.1,14,7.5,14Z"/><rect class="cls-2" width="16" height="16"/></svg>

Before

Width:  |  Height:  |  Size: 661 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.07 15.91"><defs><style>.cls-1{fill:#231f20;}</style></defs><title>Database@2x</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M6.53,0C4.13,0,.11.45,0,2.12V13.66c0,1.77,4.1,2.25,6.53,2.25s6.54-.48,6.54-2.25V2.12C13,.45,8.94,0,6.53,0ZM12,13.66c-.14.39-2.18,1.16-5.45,1.16S1.2,14,1.09,13.66V3.49a14.55,14.55,0,0,0,5.44.87A14.57,14.57,0,0,0,12,3.49ZM12,2.18c-.18.38-2.19,1.09-5.45,1.09S1.32,2.58,1.09,2.19c.23-.39,2.22-1.09,5.44-1.09s5.26.72,5.45,1.06h0Z"/><polygon class="cls-1" points="11.99 2.17 11.98 2.18 11.98 2.15 11.99 2.17"/></g></g></svg> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}</style></defs><title>bv</title><path d="M7.5,1q.563,0,1.246.062a10.962,10.962,0,0,1,1.367.215,7.406,7.406,0,0,1,1.3.418,3.558,3.558,0,0,1,1.039.664,1.991,1.991,0,0,1,.4.516A1.327,1.327,0,0,1,13,3.5v9a1.292,1.292,0,0,1-.152.629,2.2,2.2,0,0,1-.4.512,3.24,3.24,0,0,1-1.027.66,7.741,7.741,0,0,1-1.313.418,10.706,10.706,0,0,1-1.379.219Q8.047,15,7.5,15q-.562,0-1.246-.062a10.962,10.962,0,0,1-1.367-.215,7.323,7.323,0,0,1-1.3-.418,3.538,3.538,0,0,1-1.039-.664,1.958,1.958,0,0,1-.395-.516A1.322,1.322,0,0,1,2,12.5v-9a1.3,1.3,0,0,1,.152-.629,2.217,2.217,0,0,1,.4-.512A3.257,3.257,0,0,1,3.574,1.7a7.924,7.924,0,0,1,1.313-.418,10.883,10.883,0,0,1,1.379-.219Q6.953,1,7.5,1Zm0,1c-.266,0-.543.01-.832.027s-.579.05-.871.094-.577.1-.856.172a6.4,6.4,0,0,0-.793.254c-.078.031-.182.08-.312.144a3.808,3.808,0,0,0-.383.223,1.645,1.645,0,0,0-.32.277A.47.47,0,0,0,3,3.5a.3.3,0,0,0,.023.117.618.618,0,0,0,.063.11,1.445,1.445,0,0,0,.473.437,3.925,3.925,0,0,0,.695.328,6.414,6.414,0,0,0,.824.238q.438.1.871.157t.836.086Q7.189,5,7.5,5t.715-.027q.4-.027.836-.086t.871-.157a6.414,6.414,0,0,0,.824-.238,3.827,3.827,0,0,0,.692-.328,1.542,1.542,0,0,0,.476-.437.618.618,0,0,0,.063-.11A.3.3,0,0,0,12,3.5a.47.47,0,0,0-.133-.309,1.645,1.645,0,0,0-.32-.277,3.808,3.808,0,0,0-.383-.223c-.13-.064-.234-.113-.312-.144a6.4,6.4,0,0,0-.793-.254q-.418-.105-.856-.172t-.871-.094C8.043,2.01,7.766,2,7.5,2Zm0,12q.4,0,.832-.027t.871-.094q.438-.066.856-.168a5.182,5.182,0,0,0,.793-.258q.117-.046.312-.144a3.461,3.461,0,0,0,.383-.223,1.612,1.612,0,0,0,.32-.277A.47.47,0,0,0,12,12.5V4.984a4.572,4.572,0,0,1-1.008.485,8.312,8.312,0,0,1-1.168.312,11.149,11.149,0,0,1-1.207.168Q8.015,6,7.5,6T6.379,5.949a11.047,11.047,0,0,1-1.207-.168,8.368,8.368,0,0,1-1.164-.312A4.572,4.572,0,0,1,3,4.984V12.5a.47.47,0,0,0,.133.309,1.612,1.612,0,0,0,.32.277,3.461,3.461,0,0,0,.383.223q.2.1.312.144a5.182,5.182,0,0,0,.793.258q.418.1.856.168t.871.094Q7.1,14,7.5,14Z"/><rect class="cls-1" width="16" height="16"/></svg>

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -147,7 +147,7 @@ export class AddNewPackageTab {
let pipPackage: PipPackageOverview; let pipPackage: PipPackageOverview;
pipPackage = await this.dialog.model.getPackageOverview(packageName); pipPackage = await this.dialog.model.getPackageOverview(packageName);
if (!pipPackage.versions || pipPackage.versions.length === 0) { if (!pipPackage?.versions || pipPackage.versions.length === 0) {
this.dialog.showErrorMessage( this.dialog.showErrorMessage(
localize('managePackages.noVersionsFound', localize('managePackages.noVersionsFound',
"Could not find any valid versions for the specified package")); "Could not find any valid versions for the specified package"));

View File

@@ -20,11 +20,13 @@ export class InstalledPackagesTab {
private installedPkgTab: azdata.window.DialogTab; private installedPkgTab: azdata.window.DialogTab;
private packageTypeDropdown: azdata.DropDownComponent; private packageTypeDropdown: azdata.DropDownComponent;
private locationComponent: azdata.TextComponent; private locationComponent: azdata.Component;
private installedPackageCount: azdata.TextComponent; private installedPackageCount: azdata.TextComponent;
private installedPackagesTable: azdata.TableComponent; private installedPackagesTable: azdata.TableComponent;
private installedPackagesLoader: azdata.LoadingComponent; private installedPackagesLoader: azdata.LoadingComponent;
private uninstallPackageButton: azdata.ButtonComponent; private uninstallPackageButton: azdata.ButtonComponent;
private view: azdata.ModelView | undefined;
private formBuilder: azdata.FormBuilder;
constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) { constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) {
this.prompter = new CodeAdapter(); this.prompter = new CodeAdapter();
@@ -32,14 +34,7 @@ export class InstalledPackagesTab {
this.installedPkgTab = azdata.window.createTab(localize('managePackages.installedTabTitle', "Installed")); this.installedPkgTab = azdata.window.createTab(localize('managePackages.installedTabTitle', "Installed"));
this.installedPkgTab.registerContent(async view => { this.installedPkgTab.registerContent(async view => {
this.view = view;
// TODO: only supporting single location for now. We should add a drop down for multi locations mode
//
let locationTitle = await this.dialog.model.getLocationTitle();
this.locationComponent = view.modelBuilder.text().withProperties({
value: locationTitle
}).component();
let dropdownValues = this.dialog.model.getPackageTypes().map(x => { let dropdownValues = this.dialog.model.getPackageTypes().map(x => {
return { return {
name: x.providerId, name: x.providerId,
@@ -52,11 +47,17 @@ export class InstalledPackagesTab {
value: defaultPackageType value: defaultPackageType
}).component(); }).component();
this.dialog.changeProvider(defaultPackageType.providerId); this.dialog.changeProvider(defaultPackageType.providerId);
this.packageTypeDropdown.onValueChanged(() => { this.packageTypeDropdown.onValueChanged(async () => {
this.dialog.resetPages((<azdata.CategoryValue>this.packageTypeDropdown.value).name) this.dialog.changeProvider((<azdata.CategoryValue>this.packageTypeDropdown.value).name);
.catch(err => { try {
this.dialog.showErrorMessage(utils.getErrorMessage(err)); await this.resetLocations();
}); await this.dialog.resetPages();
}
catch (err) {
this.dialog.showErrorMessage(utils.getErrorMessage(err));
}
}); });
this.installedPackageCount = view.modelBuilder.text().withProperties({ this.installedPackageCount = view.modelBuilder.text().withProperties({
@@ -81,11 +82,8 @@ export class InstalledPackagesTab {
}).component(); }).component();
this.uninstallPackageButton.onDidClick(() => this.doUninstallPackage()); this.uninstallPackageButton.onDidClick(() => this.doUninstallPackage());
let formModel = view.modelBuilder.formContainer() this.formBuilder = view.modelBuilder.formContainer()
.withFormItems([{ .withFormItems([{
component: this.locationComponent,
title: localize('managePackages.location', "Location")
}, {
component: this.packageTypeDropdown, component: this.packageTypeDropdown,
title: localize('managePackages.packageType', "Package Type") title: localize('managePackages.packageType', "Package Type")
}, { }, {
@@ -97,10 +95,11 @@ export class InstalledPackagesTab {
}, { }, {
component: this.uninstallPackageButton, component: this.uninstallPackageButton,
title: '' title: ''
}]).component(); }]);
await this.resetLocations();
this.installedPackagesLoader = view.modelBuilder.loadingComponent() this.installedPackagesLoader = view.modelBuilder.loadingComponent()
.withItem(formModel) .withItem(this.formBuilder.component())
.withProperties({ .withProperties({
loading: true loading: true
}).component(); }).component();
@@ -112,6 +111,68 @@ export class InstalledPackagesTab {
}); });
} }
private async resetLocations(): Promise<void> {
if (this.view) {
if (this.locationComponent) {
this.formBuilder.removeFormItem({
component: this.locationComponent,
title: localize('managePackages.location', "Location")
});
}
this.locationComponent = await InstalledPackagesTab.getLocationComponent(this.view, this.dialog);
this.formBuilder.insertFormItem({
component: this.locationComponent,
title: localize('managePackages.location', "Location")
}, 1);
}
}
/**
* Creates a component for package locations
* @param view Model view
* @param dialog Manage package dialog
*/
public static async getLocationComponent(view: azdata.ModelView, dialog: ManagePackagesDialog): Promise<azdata.Component> {
const locations = await dialog.model.getLocations();
let component: azdata.Component;
if (locations && locations.length === 1) {
component = view.modelBuilder.text().withProperties({
value: locations[0].displayName
}).component();
} else if (locations) {
let dropdownValues = locations.map(x => {
return {
name: x.name,
displayName: x.displayName
};
});
let locationDropDown = view.modelBuilder.dropDown().withProperties({
values: dropdownValues,
value: dropdownValues[0]
}).component();
locationDropDown.onValueChanged(async () => {
dialog.changeLocation((<azdata.CategoryValue>locationDropDown.value).name);
try {
await dialog.resetPages();
}
catch (err) {
dialog.showErrorMessage(utils.getErrorMessage(err));
}
});
component = locationDropDown;
} else {
component = view.modelBuilder.text().withProperties({
}).component();
}
if (locations && locations.length > 0) {
dialog.changeLocation(locations[0].name);
}
return component;
}
public get tab(): azdata.window.DialogTab { public get tab(): azdata.window.DialogTab {
return this.installedPkgTab; return this.installedPkgTab;
} }

View File

@@ -67,14 +67,17 @@ export class ManagePackagesDialog {
} }
/** /**
* Resets the tabs for given provider Id * Changes the current location
* @param providerId Package Management Provider Id * @param location location name
*/ */
public async resetPages(providerId: string): Promise<void> { public changeLocation(location: string): void {
this.model.changeLocation(location);
}
// Change the provider in the model /**
// * Resets the tabs for given provider Id
this.changeProvider(providerId); */
public async resetPages(): Promise<void> {
// Load packages for given provider // Load packages for given provider
// //

View File

@@ -4,10 +4,9 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
import { IPackageManageProvider, IPackageDetails, IPackageOverview } from '../../types'; import { IPackageManageProvider, IPackageDetails, IPackageOverview, IPackageLocation } from '../../types';
export interface ManagePackageDialogOptions { export interface ManagePackageDialogOptions {
multiLocations: boolean;
defaultLocation?: string; defaultLocation?: string;
defaultProviderId?: string; defaultProviderId?: string;
} }
@@ -23,11 +22,12 @@ export interface ProviderPackageType {
export class ManagePackagesDialogModel { export class ManagePackagesDialogModel {
private _currentProvider: string; private _currentProvider: string;
private _currentLocation: string;
/** /**
* A set for locations * A set for locations
*/ */
private _locations: Set<string> = new Set<string>(); private _locationTypes: Set<string> = new Set<string>();
/** /**
* Map of locations to providers * Map of locations to providers
@@ -77,15 +77,10 @@ export class ManagePackagesDialogModel {
if (this._options.defaultProviderId && !this._packageManageProviders.has(this._options.defaultProviderId)) { if (this._options.defaultProviderId && !this._packageManageProviders.has(this._options.defaultProviderId)) {
throw new Error(`Invalid default provider id '${this._options.defaultProviderId}`); throw new Error(`Invalid default provider id '${this._options.defaultProviderId}`);
} }
if (!this._options.multiLocations && !this.defaultLocation) {
throw new Error('Default location not specified for single location mode');
}
} }
private get defaultOptions(): ManagePackageDialogOptions { private get defaultOptions(): ManagePackageDialogOptions {
return { return {
multiLocations: true,
defaultLocation: undefined, defaultLocation: undefined,
defaultProviderId: undefined defaultProviderId: undefined
}; };
@@ -120,13 +115,6 @@ export class ManagePackagesDialogModel {
return undefined; return undefined;
} }
/**
* Returns true if multi locations mode is enabled
*/
public get multiLocationMode(): boolean {
return this.options.multiLocations;
}
/** /**
* Returns options * Returns options
*/ */
@@ -135,17 +123,17 @@ export class ManagePackagesDialogModel {
} }
/** /**
* returns the array of target locations * returns the array of target location types
*/ */
public get targetLocations(): string[] { public get targetLocationTypes(): string[] {
return Array.from(this._locations.keys()); return Array.from(this._locationTypes.keys());
} }
/** /**
* Returns the default location * Returns the default location
*/ */
public get defaultLocation(): string { public get defaultLocation(): string {
return this.options.defaultLocation || this.targetLocations[0]; return this.options.defaultLocation || this.targetLocationTypes[0];
} }
/** /**
@@ -164,8 +152,8 @@ export class ManagePackagesDialogModel {
for (let index = 0; index < keyArray.length; index++) { for (let index = 0; index < keyArray.length; index++) {
const element = this.packageManageProviders.get(keyArray[index]); const element = this.packageManageProviders.get(keyArray[index]);
if (await element.canUseProvider()) { if (await element.canUseProvider()) {
if (!this._locations.has(element.packageTarget.location)) { if (!this._locationTypes.has(element.packageTarget.location)) {
this._locations.add(element.packageTarget.location); this._locationTypes.add(element.packageTarget.location);
} }
if (!this._packageTypes.has(element.packageTarget.location)) { if (!this._packageTypes.has(element.packageTarget.location)) {
this._packageTypes.set(element.packageTarget.location, []); this._packageTypes.set(element.packageTarget.location, []);
@@ -205,7 +193,7 @@ export class ManagePackagesDialogModel {
public async listPackages(): Promise<IPackageDetails[]> { public async listPackages(): Promise<IPackageDetails[]> {
let provider = this.currentPackageManageProvider; let provider = this.currentPackageManageProvider;
if (provider) { if (provider) {
return await provider.listPackages(); return await provider.listPackages(this._currentLocation);
} else { } else {
throw new Error('Current Provider is not set'); throw new Error('Current Provider is not set');
} }
@@ -222,6 +210,13 @@ export class ManagePackagesDialogModel {
} }
} }
/**
* Changes the current location
*/
public changeLocation(location: string): void {
this._currentLocation = location;
}
/** /**
* Installs given packages using current provider * Installs given packages using current provider
* @param packages Packages to install * @param packages Packages to install
@@ -229,7 +224,7 @@ export class ManagePackagesDialogModel {
public async installPackages(packages: IPackageDetails[]): Promise<void> { public async installPackages(packages: IPackageDetails[]): Promise<void> {
let provider = this.currentPackageManageProvider; let provider = this.currentPackageManageProvider;
if (provider) { if (provider) {
await provider.installPackages(packages, false); await provider.installPackages(packages, false, this._currentLocation);
} else { } else {
throw new Error('Current Provider is not set'); throw new Error('Current Provider is not set');
} }
@@ -238,10 +233,10 @@ export class ManagePackagesDialogModel {
/** /**
* Returns the location title for current provider * Returns the location title for current provider
*/ */
public async getLocationTitle(): Promise<string | undefined> { public async getLocations(): Promise<IPackageLocation[] | undefined> {
let provider = this.currentPackageManageProvider; let provider = this.currentPackageManageProvider;
if (provider) { if (provider) {
return await provider.getLocationTitle(); return await provider.getLocations();
} }
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
@@ -253,7 +248,7 @@ export class ManagePackagesDialogModel {
public async uninstallPackages(packages: IPackageDetails[]): Promise<void> { public async uninstallPackages(packages: IPackageDetails[]): Promise<void> {
let provider = this.currentPackageManageProvider; let provider = this.currentPackageManageProvider;
if (provider) { if (provider) {
await provider.uninstallPackages(packages); await provider.uninstallPackages(packages, this._currentLocation);
} else { } else {
throw new Error('Current Provider is not set'); throw new Error('Current Provider is not set');
} }

View File

@@ -207,7 +207,6 @@ export class JupyterController implements vscode.Disposable {
try { try {
if (!options) { if (!options) {
options = { options = {
multiLocations: false,
defaultLocation: constants.localhostName, defaultLocation: constants.localhostName,
defaultProviderId: LocalPipPackageManageProvider.ProviderId defaultProviderId: LocalPipPackageManageProvider.ProviderId
}; };

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IPackageManageProvider, IPackageDetails, IPackageTarget, IPackageOverview } from '../types'; import { IPackageManageProvider, IPackageDetails, IPackageTarget, IPackageOverview, IPackageLocation } from '../types';
import { IJupyterServerInstallation } from './jupyterServerInstallation'; import { IJupyterServerInstallation } from './jupyterServerInstallation';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import * as utils from '../common/utils'; import * as utils from '../common/utils';
@@ -35,7 +35,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
/** /**
* Returns list of packages * Returns list of packages
*/ */
public async listPackages(): Promise<IPackageDetails[]> { public async listPackages(location?: string): Promise<IPackageDetails[]> {
return await this.jupyterInstallation.getInstalledCondaPackages(); return await this.jupyterInstallation.getInstalledCondaPackages();
} }
@@ -44,7 +44,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
* @param packages Packages to install * @param packages Packages to install
* @param useMinVersion minimum version * @param useMinVersion minimum version
*/ */
installPackages(packages: IPackageDetails[], useMinVersion: boolean): Promise<void> { installPackages(packages: IPackageDetails[], useMinVersion: boolean, location?: string): Promise<void> {
return this.jupyterInstallation.installCondaPackages(packages, useMinVersion); return this.jupyterInstallation.installCondaPackages(packages, useMinVersion);
} }
@@ -52,7 +52,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
* Uninstalls given packages * Uninstalls given packages
* @param packages Packages to uninstall * @param packages Packages to uninstall
*/ */
uninstallPackages(packages: IPackageDetails[]): Promise<void> { uninstallPackages(packages: IPackageDetails[], location?: string): Promise<void> {
return this.jupyterInstallation.uninstallCondaPackages(packages); return this.jupyterInstallation.uninstallCondaPackages(packages);
} }
@@ -66,8 +66,8 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
/** /**
* Returns location title * Returns location title
*/ */
getLocationTitle(): Promise<string> { getLocations(): Promise<IPackageLocation[]> {
return Promise.resolve(constants.localhostTitle); return Promise.resolve([{ displayName: constants.localhostTitle, name: constants.localhostName }]);
} }
/** /**

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IPackageManageProvider, IPackageDetails, IPackageTarget, IPackageOverview } from '../types'; import { IPackageManageProvider, IPackageDetails, IPackageTarget, IPackageOverview, IPackageLocation } from '../types';
import { IJupyterServerInstallation } from './jupyterServerInstallation'; import { IJupyterServerInstallation } from './jupyterServerInstallation';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import * as utils from '../common/utils'; import * as utils from '../common/utils';
@@ -38,7 +38,7 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
/** /**
* Returns list of packages * Returns list of packages
*/ */
public async listPackages(): Promise<IPackageDetails[]> { public async listPackages(location?: string): Promise<IPackageDetails[]> {
return await this.jupyterInstallation.getInstalledPipPackages(); return await this.jupyterInstallation.getInstalledPipPackages();
} }
@@ -47,7 +47,7 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
* @param packages Packages to install * @param packages Packages to install
* @param useMinVersion minimum version * @param useMinVersion minimum version
*/ */
installPackages(packages: IPackageDetails[], useMinVersion: boolean): Promise<void> { installPackages(packages: IPackageDetails[], useMinVersion: boolean, location?: string): Promise<void> {
return this.jupyterInstallation.installPipPackages(packages, useMinVersion); return this.jupyterInstallation.installPipPackages(packages, useMinVersion);
} }
@@ -55,7 +55,7 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
* Uninstalls given packages * Uninstalls given packages
* @param packages Packages to uninstall * @param packages Packages to uninstall
*/ */
uninstallPackages(packages: IPackageDetails[]): Promise<void> { uninstallPackages(packages: IPackageDetails[], location?: string): Promise<void> {
return this.jupyterInstallation.uninstallPipPackages(packages); return this.jupyterInstallation.uninstallPipPackages(packages);
} }
@@ -69,8 +69,8 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
/** /**
* Returns location title * Returns location title
*/ */
getLocationTitle(): Promise<string> { getLocations(): Promise<IPackageLocation[]> {
return Promise.resolve(constants.localhostTitle); return Promise.resolve([{ displayName: constants.localhostTitle, name: constants.localhostName }]);
} }
/** /**

View File

@@ -0,0 +1,295 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq';
import { ManagePackagesDialog } from '../../dialog/managePackages/managePackagesDialog';
import { ManagePackagesDialogModel } from '../../dialog/managePackages/managePackagesDialogModel';
import { IPackageManageProvider, IPackageLocation } from '../../types';
import { LocalCondaPackageManageProvider } from '../../jupyter/localCondaPackageManageProvider';
import { InstalledPackagesTab } from '../../dialog/managePackages/installedPackagesTab';
import should = require('should');
interface TestContext {
view: azdata.ModelView;
onClick: vscode.EventEmitter<any>;
dialog: TypeMoq.IMock<ManagePackagesDialog>;
model: TypeMoq.IMock<ManagePackagesDialogModel>;
}
describe('Manage Package Dialog', () => {
it('getLocationComponent should create text component for one location', async function (): Promise<void> {
let testContext = createViewContext();
let locations = [
{
displayName: 'dl1',
name: 'nl1'
}
];
testContext.model.setup(x => x.getLocations()).returns(() => Promise.resolve(locations));
testContext.model.setup(x => x.changeLocation('nl1'));
testContext.dialog.setup(x => x.changeLocation('nl1'));
let actual = await InstalledPackagesTab.getLocationComponent(testContext.view, testContext.dialog.object);
should.equal('onTextChanged' in actual, true);
testContext.dialog.verify(x => x.changeLocation('nl1'), TypeMoq.Times.once());
});
it('getLocationComponent should create text component for undefined location', async function (): Promise<void> {
let testContext = createViewContext();
let locations: IPackageLocation[] | undefined = undefined;
testContext.model.setup(x => x.getLocations()).returns(() => Promise.resolve(locations));
let actual = await InstalledPackagesTab.getLocationComponent(testContext.view, testContext.dialog.object);
should.equal('onTextChanged' in actual, true);
});
it('getLocationComponent should create drop down component for more than one location', async function (): Promise<void> {
let testContext = createViewContext();
let locations = [
{
displayName: 'dl1',
name: 'nl1'
},
{
displayName: 'dl2',
name: 'nl2'
}
];
testContext.model.setup(x => x.getLocations()).returns(() => Promise.resolve(locations));
testContext.dialog.setup(x => x.changeLocation('nl1'));
testContext.dialog.setup(x => x.resetPages()).returns(() => Promise.resolve());
let actual = await InstalledPackagesTab.getLocationComponent(testContext.view, testContext.dialog.object);
should.equal('onValueChanged' in actual, true);
testContext.dialog.verify(x => x.changeLocation('nl1'), TypeMoq.Times.once());
(<azdata.DropDownComponent>actual).value = {
displayName: 'dl2',
name: 'nl2'
};
testContext.onClick.fire();
testContext.dialog.verify(x => x.changeLocation('nl2'), TypeMoq.Times.once());
testContext.dialog.verify(x => x.resetPages(), TypeMoq.Times.once());
});
it('getLocationComponent should show error if reset pages fails', async function (): Promise<void> {
let testContext = createViewContext();
let locations = [
{
displayName: 'dl1',
name: 'nl1'
},
{
displayName: 'dl2',
name: 'nl2'
}
];
testContext.model.setup(x => x.getLocations()).returns(() => Promise.resolve(locations));
testContext.dialog.setup(x => x.changeLocation('nl1'));
testContext.dialog.setup(x => x.resetPages()).throws(new Error('failed'));
testContext.dialog.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve());
let actual = await InstalledPackagesTab.getLocationComponent(testContext.view, testContext.dialog.object);
should.equal('onValueChanged' in actual, true);
testContext.dialog.verify(x => x.changeLocation('nl1'), TypeMoq.Times.once());
(<azdata.DropDownComponent>actual).value = {
displayName: 'dl2',
name: 'nl2'
};
testContext.onClick.fire();
testContext.dialog.verify(x => x.changeLocation('nl2'), TypeMoq.Times.once());
testContext.dialog.verify(x => x.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
function createViewContext(): TestContext {
let packageManageProviders = new Map<string, IPackageManageProvider>();
packageManageProviders.set(LocalCondaPackageManageProvider.ProviderId, new LocalCondaPackageManageProvider(undefined));
let model = TypeMoq.Mock.ofInstance(new ManagePackagesDialogModel(undefined, packageManageProviders));
let dialog = TypeMoq.Mock.ofInstance(new ManagePackagesDialog(model.object));
dialog.setup(x => x.model).returns(() => model.object);
let onClick: vscode.EventEmitter<any> = new vscode.EventEmitter<any>();
let componentBase: azdata.Component = {
id: '',
updateProperties: () => Promise.resolve(),
updateProperty: () => Promise.resolve(),
updateCssStyles: undefined!,
onValidityChanged: undefined!,
valid: true,
validate: undefined!,
focus: undefined!
};
let button: azdata.ButtonComponent = Object.assign({}, componentBase, {
onDidClick: onClick.event
});
let radioButton: azdata.RadioButtonComponent = Object.assign({}, componentBase, {
onDidClick: onClick.event
});
const components: azdata.Component[] = [];
let container = {
clearItems: () => { },
addItems: () => { },
addItem: () => { },
removeItem: () => true,
insertItem: () => { },
items: components,
setLayout: () => { }
};
let form: azdata.FormContainer = Object.assign({}, componentBase, container, {
});
let flex: azdata.FlexContainer = Object.assign({}, componentBase, container, {
});
let buttonBuilder: azdata.ComponentBuilder<azdata.ButtonComponent> = {
component: () => button,
withProperties: () => buttonBuilder,
withValidation: () => buttonBuilder
};
let radioButtonBuilder: azdata.ComponentBuilder<azdata.ButtonComponent> = {
component: () => radioButton,
withProperties: () => radioButtonBuilder,
withValidation: () => radioButtonBuilder
};
let inputBox: () => azdata.InputBoxComponent = () => Object.assign({}, componentBase, {
onTextChanged: undefined!,
onEnterKeyPressed: undefined!,
value: ''
});
let image: () => azdata.ImageComponent = () => Object.assign({}, componentBase, {
});
let dropdown: () => azdata.DropDownComponent = () => Object.assign({}, componentBase, {
onValueChanged: onClick.event,
value: {
name: '',
displayName: ''
},
values: []
});
let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, {
onDataChanged: undefined!,
data: [],
columns: []
});
let loadingComponent: () => azdata.LoadingComponent = () => Object.assign({}, componentBase, {
loading: false,
component: undefined!
});
let declarativeTableBuilder: azdata.ComponentBuilder<azdata.DeclarativeTableComponent> = {
component: () => declarativeTable(),
withProperties: () => declarativeTableBuilder,
withValidation: () => declarativeTableBuilder
};
let loadingBuilder: azdata.LoadingComponentBuilder = {
component: () => loadingComponent(),
withProperties: () => loadingBuilder,
withValidation: () => loadingBuilder,
withItem: () => loadingBuilder
};
let formBuilder: azdata.FormBuilder = Object.assign({}, {
component: () => form,
addFormItem: () => { },
insertFormItem: () => { },
removeFormItem: () => true,
addFormItems: () => { },
withFormItems: () => formBuilder,
withProperties: () => formBuilder,
withValidation: () => formBuilder,
withItems: () => formBuilder,
withLayout: () => formBuilder
});
let flexBuilder: azdata.FlexBuilder = Object.assign({}, {
component: () => flex,
withProperties: () => flexBuilder,
withValidation: () => flexBuilder,
withItems: () => flexBuilder,
withLayout: () => flexBuilder
});
let inputBoxBuilder: azdata.ComponentBuilder<azdata.InputBoxComponent> = {
component: () => {
let r = inputBox();
return r;
},
withProperties: () => inputBoxBuilder,
withValidation: () => inputBoxBuilder
};
let imageBuilder: azdata.ComponentBuilder<azdata.ImageComponent> = {
component: () => {
let r = image();
return r;
},
withProperties: () => imageBuilder,
withValidation: () => imageBuilder
};
let dropdownBuilder: azdata.ComponentBuilder<azdata.DropDownComponent> = {
component: () => {
let r = dropdown();
return r;
},
withProperties: () => dropdownBuilder,
withValidation: () => dropdownBuilder
};
let view: azdata.ModelView = {
onClosed: undefined!,
connection: undefined!,
serverInfo: undefined!,
valid: true,
onValidityChanged: undefined!,
validate: undefined!,
initializeModel: () => { return Promise.resolve(); },
modelBuilder: {
radioCardGroup: undefined!,
navContainer: undefined!,
divContainer: undefined!,
flexContainer: () => flexBuilder,
splitViewContainer: undefined!,
dom: undefined!,
card: undefined!,
inputBox: () => inputBoxBuilder,
checkBox: undefined!,
radioButton: () => radioButtonBuilder,
webView: undefined!,
editor: undefined!,
diffeditor: undefined!,
text: () => inputBoxBuilder,
image: () => imageBuilder,
button: () => buttonBuilder,
dropDown: () => dropdownBuilder,
tree: undefined!,
listBox: undefined!,
table: undefined!,
declarativeTable: () => declarativeTableBuilder,
dashboardWidget: undefined!,
dashboardWebview: undefined!,
formContainer: () => formBuilder,
groupContainer: undefined!,
toolbarContainer: undefined!,
loadingComponent: () => loadingBuilder,
fileBrowserTree: undefined!,
hyperlink: undefined!,
tabbedPanel: undefined!,
separator: undefined!
}
};
return {
dialog: dialog,
model: model,
view: view,
onClick: onClick,
};
}
});

View File

@@ -50,7 +50,6 @@ describe('Manage Packages', () => {
providers.set(provider.providerId, provider); providers.set(provider.providerId, provider);
let options = { let options = {
multiLocations: true,
defaultLocation: 'invalid location' defaultLocation: 'invalid location'
}; };
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options); let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
@@ -64,29 +63,12 @@ describe('Manage Packages', () => {
providers.set(provider.providerId, provider); providers.set(provider.providerId, provider);
let options = { let options = {
multiLocations: true,
defaultProviderId: 'invalid provider' defaultProviderId: 'invalid provider'
}; };
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options); let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
await should(model.init()).rejectedWith(`Invalid default provider id '${options.defaultProviderId}`); await should(model.init()).rejectedWith(`Invalid default provider id '${options.defaultProviderId}`);
}); });
/* Test disabled. Tracking issue: https://github.com/microsoft/azuredatastudio/issues/8877
it('Init should throw exception not given valid default location for single location mode', async function (): Promise<void> {
let testContext = createContext();
let provider = createProvider(testContext);
let providers = new Map<string, IPackageManageProvider>();
providers.set(provider.providerId, provider);
let options = {
multiLocations: false
};
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
await should(model.init()).rejectedWith(`Default location not specified for single location mode`);
});
*/
it('Init should set default options given undefined', async function (): Promise<void> { it('Init should set default options given undefined', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
let provider = createProvider(testContext); let provider = createProvider(testContext);
@@ -96,7 +78,6 @@ describe('Manage Packages', () => {
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, undefined); let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, undefined);
await model.init(); await model.init();
should.equal(model.multiLocationMode, true);
should.equal(model.defaultLocation, provider.packageTarget.location); should.equal(model.defaultLocation, provider.packageTarget.location);
should.equal(model.defaultProviderId, provider.providerId); should.equal(model.defaultProviderId, provider.providerId);
}); });
@@ -119,14 +100,12 @@ describe('Manage Packages', () => {
providers.set(testContext1.provider.providerId, createProvider(testContext1)); providers.set(testContext1.provider.providerId, createProvider(testContext1));
providers.set(testContext2.provider.providerId, createProvider(testContext2)); providers.set(testContext2.provider.providerId, createProvider(testContext2));
let options = { let options = {
multiLocations: false,
defaultLocation: testContext2.provider.packageTarget.location, defaultLocation: testContext2.provider.packageTarget.location,
defaultProviderId: testContext2.provider.providerId defaultProviderId: testContext2.provider.providerId
}; };
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options); let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
await model.init(); await model.init();
should.equal(model.multiLocationMode, false);
should.equal(model.defaultLocation, testContext2.provider.packageTarget.location); should.equal(model.defaultLocation, testContext2.provider.packageTarget.location);
should.equal(model.defaultProviderId, testContext2.provider.providerId); should.equal(model.defaultProviderId, testContext2.provider.providerId);
}); });
@@ -195,7 +174,7 @@ describe('Manage Packages', () => {
it('changeProvider should change current provider successfully', async function (): Promise<void> { it('changeProvider should change current provider successfully', async function (): Promise<void> {
let testContext1 = createContext(); let testContext1 = createContext();
testContext1.provider.providerId = 'providerId1'; testContext1.provider.providerId = 'providerId1';
testContext1.provider.getLocationTitle = () => Promise.resolve('location title 1'); testContext1.provider.getLocations = () => Promise.resolve([{displayName: 'location title 1', name: 'location1'}]);
testContext1.provider.packageTarget = { testContext1.provider.packageTarget = {
location: 'location1', location: 'location1',
packageType: 'package-type1' packageType: 'package-type1'
@@ -203,7 +182,7 @@ describe('Manage Packages', () => {
let testContext2 = createContext(); let testContext2 = createContext();
testContext2.provider.providerId = 'providerId2'; testContext2.provider.providerId = 'providerId2';
testContext2.provider.getLocationTitle = () => Promise.resolve('location title 2'); testContext2.provider.getLocations = () => Promise.resolve([{displayName: 'location title 2', name: 'location2'}]);
testContext2.provider.packageTarget = { testContext2.provider.packageTarget = {
location: 'location2', location: 'location2',
packageType: 'package-type2' packageType: 'package-type2'
@@ -217,7 +196,7 @@ describe('Manage Packages', () => {
await model.init(); await model.init();
model.changeProvider('providerId2'); model.changeProvider('providerId2');
should.deepEqual(await model.getLocationTitle(), 'location title 2'); should.deepEqual(await model.getLocations(), [{displayName: 'location title 2', name: 'location2'}]);
}); });
it('changeProvider should throw exception given invalid provider', async function (): Promise<void> { it('changeProvider should throw exception given invalid provider', async function (): Promise<void> {
@@ -283,7 +262,7 @@ describe('Manage Packages', () => {
let testContext2 = createContext(); let testContext2 = createContext();
testContext2.provider.providerId = 'providerId2'; testContext2.provider.providerId = 'providerId2';
testContext2.provider.getLocationTitle = () => Promise.resolve('location title 2'); testContext2.provider.getLocations = () => Promise.resolve([{displayName: 'location title 2', name: 'location2'}]);
testContext2.provider.packageTarget = { testContext2.provider.packageTarget = {
location: 'location2', location: 'location2',
packageType: 'package-type2' packageType: 'package-type2'
@@ -301,6 +280,12 @@ describe('Manage Packages', () => {
testContext2.provider.listPackages = () => { testContext2.provider.listPackages = () => {
return Promise.resolve(packages); return Promise.resolve(packages);
}; };
testContext1.provider.listPackages = () => {
return Promise.resolve([{
name: 'p3',
version: '1.1.1.3'
}]);
};
let providers = new Map<string, IPackageManageProvider>(); let providers = new Map<string, IPackageManageProvider>();
providers.set(testContext1.provider.providerId, createProvider(testContext1)); providers.set(testContext1.provider.providerId, createProvider(testContext1));
@@ -315,7 +300,50 @@ describe('Manage Packages', () => {
await should(model.installPackages(packages)).resolved(); await should(model.installPackages(packages)).resolved();
await should(model.uninstallPackages(packages)).resolved(); await should(model.uninstallPackages(packages)).resolved();
await should(model.getPackageOverview('p1')).resolved(); await should(model.getPackageOverview('p1')).resolved();
await should(model.getLocationTitle()).resolvedWith('location title 2'); await should(model.getLocations()).resolvedWith([{displayName: 'location title 2', name: 'location2'}]);
});
it('listPackages should return packages for current location', async function (): Promise<void> {
let testContext = createContext();
testContext.provider.providerId = 'providerId1';
testContext.provider.packageTarget = {
location: 'location1',
packageType: 'package-type1'
};
let packages1 = [
{
name: 'p1',
version: '1.1.1.1'
},
{
name: 'p2',
version: '1.1.1.2'
}
];
let packages2 = [{
name: 'p3',
version: '1.1.1.3'
}];
testContext.provider.listPackages = (location) => {
if (location === 'location1') {
return Promise.resolve(packages1);
} else {
return Promise.resolve(packages2);
}
};
let providers = new Map<string, IPackageManageProvider>();
providers.set(testContext.provider.providerId, createProvider(testContext));
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, undefined);
await model.init();
model.changeProvider('providerId1');
model.changeLocation('location2');
await should(model.listPackages()).resolvedWith(packages2);
}); });
function createContext(): TestContext { function createContext(): TestContext {
@@ -327,7 +355,7 @@ describe('Manage Packages', () => {
packageType: 'package-type' packageType: 'package-type'
}, },
canUseProvider: () => { return Promise.resolve(true); }, canUseProvider: () => { return Promise.resolve(true); },
getLocationTitle: () => { return Promise.resolve('location-title'); }, getLocations: () => { return Promise.resolve([{displayName: 'location-title', name: 'location'}]); },
installPackages:() => { return Promise.resolve(); }, installPackages:() => { return Promise.resolve(); },
uninstallPackages: (packages: IPackageDetails[]) => { return Promise.resolve(); }, uninstallPackages: (packages: IPackageDetails[]) => { return Promise.resolve(); },
listPackages: () => { return Promise.resolve([]); }, listPackages: () => { return Promise.resolve([]); },
@@ -339,10 +367,10 @@ describe('Manage Packages', () => {
function createProvider(testContext: TestContext): IPackageManageProvider { function createProvider(testContext: TestContext): IPackageManageProvider {
let mockProvider = TypeMoq.Mock.ofType(LocalPipPackageManageProvider); let mockProvider = TypeMoq.Mock.ofType(LocalPipPackageManageProvider);
mockProvider.setup(x => x.canUseProvider()).returns(() => testContext.provider.canUseProvider()); mockProvider.setup(x => x.canUseProvider()).returns(() => testContext.provider.canUseProvider());
mockProvider.setup(x => x.getLocationTitle()).returns(() => testContext.provider.getLocationTitle()); mockProvider.setup(x => x.getLocations()).returns(() => testContext.provider.getLocations());
mockProvider.setup(x => x.installPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((packages, useMinVersion) => testContext.provider.installPackages(packages, useMinVersion)); mockProvider.setup(x => x.installPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((packages, useMinVersion) => testContext.provider.installPackages(packages, useMinVersion));
mockProvider.setup(x => x.uninstallPackages(TypeMoq.It.isAny())).returns((packages) => testContext.provider.uninstallPackages(packages)); mockProvider.setup(x => x.uninstallPackages(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((packages) => testContext.provider.uninstallPackages(packages));
mockProvider.setup(x => x.listPackages()).returns(() => testContext.provider.listPackages()); mockProvider.setup(x => x.listPackages(TypeMoq.It.isAny())).returns(() => testContext.provider.listPackages());
mockProvider.setup(x => x.getPackageOverview(TypeMoq.It.isAny())).returns((name) => testContext.provider.getPackageOverview(name)); mockProvider.setup(x => x.getPackageOverview(TypeMoq.It.isAny())).returns((name) => testContext.provider.getPackageOverview(name));
mockProvider.setup(x => x.packageTarget).returns(() => testContext.provider.packageTarget); mockProvider.setup(x => x.packageTarget).returns(() => testContext.provider.packageTarget);
mockProvider.setup(x => x.providerId).returns(() => testContext.provider.providerId); mockProvider.setup(x => x.providerId).returns(() => testContext.provider.providerId);

View File

@@ -65,6 +65,14 @@ export interface IPackageDetails {
version: string; version: string;
} }
/**
* Package location
*/
export interface IPackageLocation {
name: string;
displayName: string;
}
/** /**
* Package target interface * Package target interface
*/ */
@@ -99,20 +107,22 @@ export interface IPackageManageProvider {
/** /**
* Returns list of installed packages * Returns list of installed packages
*/ */
listPackages(): Promise<IPackageDetails[]>; listPackages(location?: string): Promise<IPackageDetails[]>;
/** /**
* Installs give packages * Installs give packages
* @param package Packages to install * @param package Packages to install
* @param useMinVersion if true, minimal version will be used * @param useMinVersion if true, minimal version will be used
* @param location package location
*/ */
installPackages(package: IPackageDetails[], useMinVersion: boolean): Promise<void>; installPackages(package: IPackageDetails[], useMinVersion: boolean, location?: string): Promise<void>;
/** /**
* Uninstalls given packages * Uninstalls given packages
* @param package package to uninstall * @param package package to uninstall
* @param location package location
*/ */
uninstallPackages(package: IPackageDetails[]): Promise<void>; uninstallPackages(package: IPackageDetails[], location?: string): Promise<void>;
/** /**
* Returns true if the provider can be used in current context * Returns true if the provider can be used in current context
@@ -122,7 +132,7 @@ export interface IPackageManageProvider {
/** /**
* Returns location title * Returns location title
*/ */
getLocationTitle(): Promise<string>; getLocations(): Promise<IPackageLocation[]>;
/** /**
* Returns Package Overview * Returns Package Overview

View File

@@ -103,7 +103,7 @@ export class SchemaCompareDialog {
endpointType: mssql.SchemaCompareEndpointType.Database, endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: (this.sourceServerDropdown.value as ConnectionDropdownValue).displayName, serverDisplayName: (this.sourceServerDropdown.value as ConnectionDropdownValue).displayName,
serverName: (this.sourceServerDropdown.value as ConnectionDropdownValue).name, serverName: (this.sourceServerDropdown.value as ConnectionDropdownValue).name,
databaseName: (<azdata.CategoryValue>this.sourceDatabaseDropdown.value).name, databaseName: this.sourceDatabaseDropdown.value.toString(),
ownerUri: ownerUri, ownerUri: ownerUri,
packageFilePath: '', packageFilePath: '',
connectionDetails: undefined connectionDetails: undefined
@@ -127,7 +127,7 @@ export class SchemaCompareDialog {
endpointType: mssql.SchemaCompareEndpointType.Database, endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: (this.targetServerDropdown.value as ConnectionDropdownValue).displayName, serverDisplayName: (this.targetServerDropdown.value as ConnectionDropdownValue).displayName,
serverName: (this.targetServerDropdown.value as ConnectionDropdownValue).name, serverName: (this.targetServerDropdown.value as ConnectionDropdownValue).name,
databaseName: (<azdata.CategoryValue>this.targetDatabaseDropdown.value).name, databaseName: this.targetDatabaseDropdown.value.toString(),
ownerUri: ownerUri, ownerUri: ownerUri,
packageFilePath: '', packageFilePath: '',
connectionDetails: undefined connectionDetails: undefined
@@ -204,7 +204,7 @@ export class SchemaCompareDialog {
this.sourceDatabaseComponent = await this.createSourceDatabaseDropdown(view); this.sourceDatabaseComponent = await this.createSourceDatabaseDropdown(view);
if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) { if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) {
await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId, false); await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection, false);
} }
this.targetServerComponent = await this.createTargetServerDropdown(view); this.targetServerComponent = await this.createTargetServerDropdown(view);
@@ -212,7 +212,7 @@ export class SchemaCompareDialog {
this.targetDatabaseComponent = await this.createTargetDatabaseDropdown(view); this.targetDatabaseComponent = await this.createTargetDatabaseDropdown(view);
if ((this.targetServerDropdown.value as ConnectionDropdownValue)) { if ((this.targetServerDropdown.value as ConnectionDropdownValue)) {
await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId, true); await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection, true);
} }
this.sourceDacpacComponent = await this.createFileBrowser(view, false, this.schemaCompareResult.sourceEndpointInfo); this.sourceDacpacComponent = await this.createFileBrowser(view, false, this.schemaCompareResult.sourceEndpointInfo);
@@ -477,7 +477,7 @@ export class SchemaCompareDialog {
}); });
} }
else { else {
await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId, false); await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection, false);
} }
}); });
@@ -503,7 +503,7 @@ export class SchemaCompareDialog {
}); });
} }
else { else {
await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId, true); await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection, true);
} }
}); });
@@ -627,11 +627,18 @@ export class SchemaCompareDialog {
return listValue.displayName === value || listValue === value; return listValue.displayName === value || listValue === value;
} }
protected async populateDatabaseDropdown(connectionId: string, isTarget: boolean): Promise<void> { protected async populateDatabaseDropdown(connectionProfile: azdata.connection.ConnectionProfile, isTarget: boolean): Promise<void> {
let currentDropdown = isTarget ? this.targetDatabaseDropdown : this.sourceDatabaseDropdown; let currentDropdown = isTarget ? this.targetDatabaseDropdown : this.sourceDatabaseDropdown;
currentDropdown.updateProperties({ values: [], value: null }); currentDropdown.updateProperties({ values: [], value: null });
let values = await this.getDatabaseValues(connectionId, isTarget); let values = [];
try {
values = await this.getDatabaseValues(connectionProfile.connectionId, isTarget);
} catch (e) {
// if the user doesn't have access to master, just set the database of the connection profile
values = [connectionProfile.databaseName];
console.warn(e);
}
if (values && values.length > 0) { if (values && values.length > 0) {
currentDropdown.updateProperties({ currentDropdown.updateProperties({
values: values, values: values,
@@ -640,7 +647,7 @@ export class SchemaCompareDialog {
} }
} }
protected async getDatabaseValues(connectionId: string, isTarget: boolean): Promise<{ displayName, name }[]> { protected async getDatabaseValues(connectionId: string, isTarget: boolean): Promise<string[]> {
let endpointInfo = isTarget ? this.schemaCompareResult.targetEndpointInfo : this.schemaCompareResult.sourceEndpointInfo; let endpointInfo = isTarget ? this.schemaCompareResult.targetEndpointInfo : this.schemaCompareResult.sourceEndpointInfo;
let idx = -1; let idx = -1;
@@ -654,10 +661,7 @@ export class SchemaCompareDialog {
idx = count; idx = count;
} }
return { return db;
displayName: db,
name: db
};
}); });
if (idx >= 0) { if (idx >= 0) {

View File

@@ -69,7 +69,7 @@ module.exports = function withDefaults(/**@type WebpackConfig*/extConfig) {
// yes, really source maps // yes, really source maps
devtool: 'source-map', devtool: 'source-map',
plugins: [ plugins: [
// @ts-ignore // @ts-expect-error
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{ from: 'src', to: '.', ignore: ['**/test/**', '*.ts'] } { from: 'src', to: '.', ignore: ['**/test/**', '*.ts'] }
]), ]),

View File

@@ -27,7 +27,20 @@
"title": "%signOut%", "title": "%signOut%",
"category": "%displayName%" "category": "%displayName%"
} }
] ],
"configuration": {
"title": "Microsoft Account",
"properties": {
"microsoftAccount.logLevel": {
"type": "string",
"enum": [
"info",
"trace"
],
"default": "info"
}
}
}
}, },
"scripts": { "scripts": {
"vscode:prepublish": "npm run compile", "vscode:prepublish": "npm run compile",

View File

@@ -45,6 +45,7 @@ export class Keychain {
async setToken(token: string): Promise<void> { async setToken(token: string): Promise<void> {
try { try {
Logger.trace('Writing to keychain', token);
return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token);
} catch (e) { } catch (e) {
// Ignore // Ignore
@@ -59,7 +60,9 @@ export class Keychain {
async getToken(): Promise<string | null | undefined> { async getToken(): Promise<string | null | undefined> {
try { try {
return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); const result = await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID);
Logger.trace('Reading from keychain', result);
return result;
} catch (e) { } catch (e) {
// Ignore // Ignore
Logger.error(`Getting token failed: ${e}`); Logger.error(`Getting token failed: ${e}`);

View File

@@ -7,11 +7,23 @@ import * as vscode from 'vscode';
type LogLevel = 'Trace' | 'Info' | 'Error'; type LogLevel = 'Trace' | 'Info' | 'Error';
enum Level {
Trace = 'trace',
Info = 'Info'
}
class Log { class Log {
private output: vscode.OutputChannel; private output: vscode.OutputChannel;
private level: Level;
constructor() { constructor() {
this.output = vscode.window.createOutputChannel('Account'); this.output = vscode.window.createOutputChannel('Account');
this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info;
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('microsoftAccount.logLevel')) {
this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info;
}
});
} }
private data2String(data: any): string { private data2String(data: any): string {
@@ -32,6 +44,12 @@ class Log {
this.logLevel('Error', message, data); this.logLevel('Error', message, data);
} }
public trace(message: string, data?: any): void {
if (this.level === Level.Trace) {
this.logLevel('Trace', message, data);
}
}
public logLevel(level: LogLevel, message: string, data?: any): void { public logLevel(level: LogLevel, message: string, data?: any): void {
this.output.appendLine(`[${level} - ${this.now()}] ${message}`); this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
if (data) { if (data) {

View File

@@ -1,68 +1,59 @@
{ {
"name": "vscode-colorize-tests", "name": "vscode-colorize-tests",
"description": "Colorize tests for VS Code", "description": "Colorize tests for VS Code",
"version": "0.0.1", "version": "0.0.1",
"publisher": "vscode", "publisher": "vscode",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"activationEvents": [ "activationEvents": [
"onLanguage:json" "onLanguage:json"
], ],
"main": "./out/colorizerTestMain", "main": "./out/colorizerTestMain",
"enableProposedApi": true, "enableProposedApi": true,
"engines": { "engines": {
"vscode": "*" "vscode": "*"
}, },
"scripts": { "scripts": {
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json" "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json"
}, },
"dependencies": { "dependencies": {
"jsonc-parser": "2.2.1" "jsonc-parser": "2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.11.7", "@types/node": "^12.11.7",
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"vscode": "1.1.5" "vscode": "1.1.5"
}, },
"contributes": { "contributes": {
"semanticTokenTypes": [ "semanticTokenTypes": [
{ {
"id": "testToken", "id": "testToken",
"description": "A test token" "description": "A test token"
} }
], ],
"semanticTokenModifiers": [ "semanticTokenModifiers": [
{ {
"id": "testModifier", "id": "testModifier",
"description": "A test modifier" "description": "A test modifier"
} }
], ],
"semanticTokenStyleDefaults": [ "semanticTokenScopes": [
{ {
"selector": "testToken", "scopes": {
"scope": [ "entity.name.function.special" ] "testToken": [
}, "entity.name.function.special"
{ ]
"selector": "*.testModifier", }
"light": { }
"fontStyle": "bold" ],
}, "productIconThemes": [
"dark": { {
"fontStyle": "bold" "id": "Test Product Icons",
}, "label": "The Test Product Icon Theme",
"highContrast": { "path": "./producticons/test-product-icon-theme.json",
"fontStyle": "bold" "_watch": true
} }
} ]
], }
"productIconThemes": [
{
"id": "Test Product Icons",
"label": "The Test Product Icon Theme",
"path": "./producticons/test-product-icon-theme.json",
"_watch": true
}
]
}
} }

View File

@@ -56,7 +56,7 @@ export function activate(context: vscode.ExtensionContext): any {
}; };
jsoncParser.visit(document.getText(), visitor); jsoncParser.visit(document.getText(), visitor);
return new vscode.SemanticTokens(builder.build()); return builder.build();
} }
}; };

View File

@@ -57,6 +57,7 @@
"jquery": "3.4.0", "jquery": "3.4.0",
"jschardet": "2.1.1", "jschardet": "2.1.1",
"keytar": "^4.11.0", "keytar": "^4.11.0",
"minimist": "^1.2.5",
"native-is-elevated": "0.4.1", "native-is-elevated": "0.4.1",
"native-keymap": "2.1.1", "native-keymap": "2.1.1",
"native-watchdog": "1.3.0", "native-watchdog": "1.3.0",
@@ -72,7 +73,6 @@
"spdlog": "^0.11.1", "spdlog": "^0.11.1",
"sudo-prompt": "9.1.1", "sudo-prompt": "9.1.1",
"v8-inspect-profiler": "^0.0.20", "v8-inspect-profiler": "^0.0.20",
"vscode-minimist": "^1.2.2",
"vscode-nsfw": "1.2.8", "vscode-nsfw": "1.2.8",
"vscode-proxy-agent": "^0.5.2", "vscode-proxy-agent": "^0.5.2",
"vscode-ripgrep": "^1.5.8", "vscode-ripgrep": "^1.5.8",
@@ -98,6 +98,7 @@
"@types/http-proxy-agent": "^2.0.1", "@types/http-proxy-agent": "^2.0.1",
"@types/iconv-lite": "0.0.1", "@types/iconv-lite": "0.0.1",
"@types/keytar": "^4.4.0", "@types/keytar": "^4.4.0",
"@types/minimist": "^1.2.0",
"@types/mocha": "2.2.39", "@types/mocha": "2.2.39",
"@types/node": "^12.11.7", "@types/node": "^12.11.7",
"@types/plotly.js": "^1.44.9", "@types/plotly.js": "^1.44.9",

View File

@@ -23,6 +23,7 @@
"iconv-lite": "0.5.0", "iconv-lite": "0.5.0",
"jquery": "3.4.0", "jquery": "3.4.0",
"jschardet": "2.1.1", "jschardet": "2.1.1",
"minimist": "^1.2.5",
"native-watchdog": "1.3.0", "native-watchdog": "1.3.0",
"ng2-charts": "^1.6.0", "ng2-charts": "^1.6.0",
"node-pty": "^0.10.0-beta2", "node-pty": "^0.10.0-beta2",
@@ -33,7 +34,6 @@
"semver-umd": "^5.5.5", "semver-umd": "^5.5.5",
"slickgrid": "github:anthonydresser/SlickGrid#2.3.32", "slickgrid": "github:anthonydresser/SlickGrid#2.3.32",
"spdlog": "^0.11.1", "spdlog": "^0.11.1",
"vscode-minimist": "^1.2.2",
"vscode-nsfw": "1.2.8", "vscode-nsfw": "1.2.8",
"vscode-proxy-agent": "^0.5.2", "vscode-proxy-agent": "^0.5.2",
"vscode-ripgrep": "^1.5.8", "vscode-ripgrep": "^1.5.8",

View File

@@ -454,6 +454,11 @@ minimist@0.0.8:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mkdirp@^0.5.1: mkdirp@^0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@@ -695,11 +700,6 @@ util-deprecate@^1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
vscode-minimist@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/vscode-minimist/-/vscode-minimist-1.2.2.tgz#65403f44f0c6010d259b2271d36eb5c6f4ad8aab"
integrity sha512-DXMNG2QgrXn1jOP12LzjVfvxVkzxv/0Qa27JrMBj/XP2esj+fJ/wP2T4YUH5derj73Lc96dC8F25WyfDUbTpxQ==
vscode-nsfw@1.2.8: vscode-nsfw@1.2.8:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-1.2.8.tgz#1bf452e72ff1304934de63692870d039a2d972af" resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-1.2.8.tgz#1bf452e72ff1304934de63692870d039a2d972af"

View File

@@ -13,7 +13,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const util = require('util'); const util = require('util');
const opn = require('opn'); const opn = require('opn');
const minimist = require('vscode-minimist'); const minimist = require('minimist');
const APP_ROOT = path.dirname(__dirname); const APP_ROOT = path.dirname(__dirname);
const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions');

View File

@@ -55,6 +55,9 @@ function code() {
function code-wsl() function code-wsl()
{ {
HOST_IP=$(powershell.exe -Command "& {(Get-NetIPAddress | Where-Object {\$_.InterfaceAlias -like '*WSL*' -and \$_.AddressFamily -eq 'IPv4'}).IPAddress | Write-Host -NoNewline}")
export DISPLAY="$HOST_IP:0"
# in a wsl shell # in a wsl shell
ELECTRON="$ROOT/.build/electron/Code - OSS.exe" ELECTRON="$ROOT/.build/electron/Code - OSS.exe"
if [ -f "$ELECTRON" ]; then if [ -f "$ELECTRON" ]; then

View File

@@ -47,7 +47,7 @@ if "%SKIP_PYTHON_INSTALL_TEST%" == "1" (
) )
call %INTEGRATION_TEST_ELECTRON_PATH% --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 ^ call %INTEGRATION_TEST_ELECTRON_PATH% --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 ^
--extensionDevelopmentPath=%~dp0\..\extensions\integration-tests --extensionTestsPath=%~dp0\..\extensions\integration-tests\out --disable-telemetry --disable-crash-reporter --disable-updates -nogpu --extensionDevelopmentPath=%~dp0\..\extensions\integration-tests --extensionTestsPath=%~dp0\..\extensions\integration-tests\out\tests --disable-telemetry --disable-crash-reporter --disable-updates -nogpu
rmdir /s /q %VSCODEUSERDATADIR% rmdir /s /q %VSCODEUSERDATADIR%
rmdir /s /q %VSCODEEXTENSIONSDIR% rmdir /s /q %VSCODEEXTENSIONSDIR%

View File

@@ -60,7 +60,7 @@ fi
--extensionDevelopmentPath=$ROOT/extensions/profiler \ --extensionDevelopmentPath=$ROOT/extensions/profiler \
--extensionDevelopmentPath=$ROOT/extensions/resource-deployment \ --extensionDevelopmentPath=$ROOT/extensions/resource-deployment \
--extensionDevelopmentPath=$ROOT/extensions/schema-compare \ --extensionDevelopmentPath=$ROOT/extensions/schema-compare \
--extensionTestsPath=$ROOT/extensions/integration-tests/out \ --extensionTestsPath=$ROOT/extensions/integration-tests/out/tests \
--user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR \ --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR \
--disable-telemetry --disable-crash-reporter --disable-updates --skip-getting-started --disable-inspect --disable-telemetry --disable-crash-reporter --disable-updates --skip-getting-started --disable-inspect

View File

@@ -142,13 +142,11 @@ function pipeLoggingToParent() {
function handleExceptions() { function handleExceptions() {
// Handle uncaught exceptions // Handle uncaught exceptions
// @ts-ignore
process.on('uncaughtException', function (err) { process.on('uncaughtException', function (err) {
console.error('Uncaught Exception: ', err); console.error('Uncaught Exception: ', err);
}); });
// Handle unhandled promise rejections // Handle unhandled promise rejections
// @ts-ignore
process.on('unhandledRejection', function (reason) { process.on('unhandledRejection', function (reason) {
console.error('Unhandled Promise Rejection: ', reason); console.error('Unhandled Promise Rejection: ', reason);
}); });

View File

@@ -25,7 +25,6 @@ exports.assign = function assign(destination, source) {
*/ */
exports.load = function (modulePaths, resultCallback, options) { exports.load = function (modulePaths, resultCallback, options) {
// @ts-ignore
const webFrame = require('electron').webFrame; const webFrame = require('electron').webFrame;
const path = require('path'); const path = require('path');
@@ -49,7 +48,6 @@ exports.load = function (modulePaths, resultCallback, options) {
} }
// Error handler // Error handler
// @ts-ignore
process.on('uncaughtException', function (error) { process.on('uncaughtException', function (error) {
onUnexpectedError(error, enableDeveloperTools); onUnexpectedError(error, enableDeveloperTools);
}); });
@@ -184,7 +182,6 @@ function parseURLQueryArgs() {
*/ */
function registerDeveloperKeybindings(disallowReloadKeybinding) { function registerDeveloperKeybindings(disallowReloadKeybinding) {
// @ts-ignore
const ipc = require('electron').ipcRenderer; const ipc = require('electron').ipcRenderer;
const extractKey = function (e) { const extractKey = function (e) {
@@ -223,7 +220,6 @@ function registerDeveloperKeybindings(disallowReloadKeybinding) {
function onUnexpectedError(error, enableDeveloperTools) { function onUnexpectedError(error, enableDeveloperTools) {
// @ts-ignore
const ipc = require('electron').ipcRenderer; const ipc = require('electron').ipcRenderer;
if (enableDeveloperTools) { if (enableDeveloperTools) {

View File

@@ -323,7 +323,7 @@ function getUserDataPath(cliArgs) {
* @returns {ParsedArgs} * @returns {ParsedArgs}
*/ */
function parseCLIArgs() { function parseCLIArgs() {
const minimist = require('vscode-minimist'); const minimist = require('minimist');
return minimist(process.argv, { return minimist(process.argv, {
string: [ string: [

View File

@@ -6,7 +6,7 @@
//@ts-check //@ts-check
'use strict'; 'use strict';
// @ts-ignore // @ts-expect-error
// const pkg = require('../package.json'); // const pkg = require('../package.json');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
@@ -34,4 +34,4 @@ function getDefaultUserDataPath(platform) {
} }
exports.getAppDataPath = getAppDataPath; exports.getAppDataPath = getAppDataPath;
exports.getDefaultUserDataPath = getDefaultUserDataPath; exports.getDefaultUserDataPath = getDefaultUserDataPath;

3
src/sql/azdata.d.ts vendored
View File

@@ -2212,7 +2212,8 @@ declare module 'azdata' {
Sql = 1, Sql = 1,
OssRdbms = 2, OssRdbms = 2,
AzureKeyVault = 3, AzureKeyVault = 3,
Graph = 4 Graph = 4,
MicrosoftResourceManagement = 5
} }
export interface DidChangeAccountsParams { export interface DidChangeAccountsParams {

View File

@@ -225,6 +225,7 @@ declare module 'azdata' {
*/ */
export interface TabbedPanelLayout { export interface TabbedPanelLayout {
orientation: TabOrientation; orientation: TabOrientation;
showIcon: boolean;
} }
/** /**
@@ -245,6 +246,11 @@ declare module 'azdata' {
* Id of the tab * Id of the tab
*/ */
id: string; id: string;
/**
* Icon of the tab
*/
icon?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
} }
/** /**
@@ -287,5 +293,33 @@ declare module 'azdata' {
*/ */
export const onDidChangeActiveNotebookEditor: vscode.Event<NotebookEditor>; export const onDidChangeActiveNotebookEditor: vscode.Event<NotebookEditor>;
} }
export namespace window {
export interface ModelViewDashboard {
registerTabs(handler: (view: ModelView) => Thenable<(DashboardTab | DashboardTabGroup)[]>): void;
open(): Thenable<void>;
}
export function createModelViewDashboard(title: string): ModelViewDashboard;
}
export interface DashboardTab extends Tab {
/**
* Toolbar of the tab, optional.
*/
toolbar?: ToolbarContainer;
}
export interface DashboardTabGroup {
/**
* * Title of the tab group
*/
title: string;
/**
* children of the tab group
*/
tabs: DashboardTab[];
}
} }

View File

@@ -24,9 +24,16 @@ panel {
position: relative; position: relative;
} }
.tabbedPanel.vertical>.title {
flex: 0 0 auto;
flex-direction: column;
height: 100%;
}
.tabbedPanel .tabContainer { .tabbedPanel .tabContainer {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
height: 100%;
} }
.tabbedPanel .tabList { .tabbedPanel .tabList {
@@ -45,31 +52,34 @@ panel {
margin: auto; margin: auto;
} }
.tabbedPanel.horizontal .tabList .tab .tabLabel {
font-size: 12px;
font-weight: normal;
}
.tabbedPanel.vertical .tabList .tab .tabLabel {
font-size: 12px;
padding-bottom: 0px;
font-weight: normal;
}
.tabbedPanel .tabList .tab .tabLabel { .tabbedPanel .tabList .tab .tabLabel {
font-size: 13px; font-size: 13px;
padding-bottom: 4px; padding-bottom: 4px;
font-weight: 600; font-weight: 600;
} }
.tabbedPanel.vertical .tabList .tab .tabLabel {
font-size: 11px;
}
.tabbedPanel .tabList .tab-header { .tabbedPanel .tabList .tab-header {
display: flex; display: flex;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
cursor: pointer; min-width: 65px;
} }
.tabbedPanel.vertical .tabList .tab-header { .tabbedPanel.vertical > .title .tabList .tab-header {
display: block; display: block;
text-transform: none; min-width: 150px;
text-overflow: ellipsis; line-height: 35px;
overflow: hidden;
width: auto;
height: 50px;
line-height: 45px;
} }
.tabbedPanel .tabList .tab .tabIcon.codicon { .tabbedPanel .tabList .tab .tabIcon.codicon {
@@ -85,8 +95,6 @@ panel {
.tabbedPanel .composite.title .title-actions .action-label { .tabbedPanel .composite.title .title-actions .action-label {
display: block; display: block;
height: 35px;
line-height: 35px;
min-width: 28px; min-width: 28px;
background-size: 16px; background-size: 16px;
background-position: center center; background-position: center center;
@@ -110,22 +118,22 @@ panel {
flex-direction: row; flex-direction: row;
} }
.tabbedPanel.vertical > .title { .tabbedPanel.vertical>.title {
flex: 0 0 auto; flex: 0 0 auto;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.tabbedPanel > .tab-content { .tabbedPanel>.tab-content {
flex: 1; flex: 1 1 auto;
position: relative; position: relative;
} }
.tabbedPanel.vertical > .title > .tabContainer > .monaco-scrollable-element > .tabList { .tabbedPanel.vertical>.title > .tabContainer .tabList {
flex-flow: column; flex-flow: column;
} }
.tabbedPanel.horizontal > .title > .tabContainer > .monaco-scrollable-element > .tabList { .tabbedPanel.horizontal > .title > .tabContainer .tabList {
flex-flow: row; flex-flow: row;
} }
@@ -153,9 +161,11 @@ panel {
border-color: rgb(214, 214, 214); border-color: rgb(214, 214, 214);
} }
.tabbedPanel .action-container { .tabbedPanel .vertical-tab-action-container {
display: flex; display: flex;
flex-flow: row-reverse; flex-flow: row-reverse;
height: 35px;
padding: 0px 5px;
} }
.tabbedPanel .tab-action { .tabbedPanel .tab-action {
@@ -164,12 +174,13 @@ panel {
padding: 0px; padding: 0px;
border: 0px; border: 0px;
background-color: transparent; background-color: transparent;
background-position: 2px center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 11px 11px; background-size: 9px 9px;
align-self: center;
} }
.vs .tabbedPanel .tab-action.collapse{ .vs .tabbedPanel .tab-action.collapse {
background-image: url("collapse.svg"); background-image: url("collapse.svg");
} }

View File

@@ -52,16 +52,16 @@ let idPool = 0;
<div class="tabbedPanel fullsize" [ngClass]="options.layout === NavigationBarLayout.vertical ? 'vertical' : 'horizontal'"> <div class="tabbedPanel fullsize" [ngClass]="options.layout === NavigationBarLayout.vertical ? 'vertical' : 'horizontal'">
<div *ngIf="!options.showTabsWhenOne ? _tabs.length !== 1 : true" class="composite title"> <div *ngIf="!options.showTabsWhenOne ? _tabs.length !== 1 : true" class="composite title">
<div class="tabContainer"> <div class="tabContainer">
<div *ngIf="options.layout === NavigationBarLayout.vertical" class="action-container"> <div *ngIf="options.layout === NavigationBarLayout.vertical" class="vertical-tab-action-container">
<button [attr.aria-expanded]="_tabExpanded" [title]="toggleTabPanelButtonAriaLabel" [attr.aria-label]="toggleTabPanelButtonAriaLabel" [ngClass]="toggleTabPanelButtonCssClass" tabindex="0" (click)="toggleTabPanel()"></button> <button [attr.aria-expanded]="_tabExpanded" [title]="toggleTabPanelButtonAriaLabel" [attr.aria-label]="toggleTabPanelButtonAriaLabel" [ngClass]="toggleTabPanelButtonCssClass" tabindex="0" (click)="toggleTabPanel()"></button>
</div> </div>
<div *ngIf="_tabExpanded" class="tabList" role="tablist" scrollable [horizontalScroll]="AutoScrollbarVisibility" [verticalScroll]="HiddenScrollbarVisibility" [scrollYToX]="true" (keydown)="onKey($event)"> <div [style.display]="_tabExpanded ? 'flex': 'none'" [attr.aria-hidden]="_tabExpanded ? 'false': 'true'" class="tabList" role="tablist" scrollable [horizontalScroll]="AutoScrollbarVisibility" [verticalScroll]="HiddenScrollbarVisibility" [scrollYToX]="true" (keydown)="onKey($event)">
<div role="presentation" *ngFor="let tab of _tabs"> <div role="presentation" *ngFor="let tab of _tabs">
<ng-container *ngIf="tab.type!=='group-header'"> <ng-container *ngIf="tab.type!=='group-header'">
<tab-header role="presentation" [active]="_activeTab === tab" [tab]="tab" [showIcon]="options.showIcon" (onSelectTab)='selectTab($event)' (onCloseTab)='closeTab($event)'></tab-header> <tab-header role="presentation" [active]="_activeTab === tab" [tab]="tab" [showIcon]="options.showIcon" (onSelectTab)='selectTab($event)' (onCloseTab)='closeTab($event)'></tab-header>
</ng-container> </ng-container>
<ng-container *ngIf="tab.type==='group-header' && options.layout === NavigationBarLayout.vertical"> <ng-container *ngIf="tab.type==='group-header' && options.layout === NavigationBarLayout.vertical">
<div class="tab-group-header" *ngIf="_tabExpanded"> <div class="tab-group-header">
<span>{{tab.title}}</span> <span>{{tab.title}}</span>
</div> </div>
</ng-container > </ng-container >

View File

@@ -69,7 +69,7 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit,
const tabLabelContainer = this._tabLabelRef.nativeElement as HTMLElement; const tabLabelContainer = this._tabLabelRef.nativeElement as HTMLElement;
if (this.showIcon && this.tab.iconClass) { if (this.showIcon && this.tab.iconClass) {
const tabIconContainer = this._tabIconRef.nativeElement as HTMLElement; const tabIconContainer = this._tabIconRef.nativeElement as HTMLElement;
tabIconContainer.className = 'tabIcon codicon'; tabIconContainer.className = 'tabIcon codicon icon';
tabIconContainer.classList.add(this.tab.iconClass); tabIconContainer.classList.add(this.tab.iconClass);
} }

View File

@@ -5,7 +5,7 @@
import 'vs/css!./media/selectBox'; import 'vs/css!./media/selectBox';
import { SelectBox as vsSelectBox, ISelectBoxStyles as vsISelectBoxStyles, ISelectBoxOptions, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; import { SelectBox as vsSelectBox, ISelectBoxStyles as vsISelectBoxStyles, ISelectBoxOptions, ISelectOptionItem, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
import { Color } from 'vs/base/common/color'; import { Color } from 'vs/base/common/color';
import { IContextViewProvider, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IContextViewProvider, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import * as dom from 'vs/base/browser/dom'; import * as dom from 'vs/base/browser/dom';
@@ -65,29 +65,18 @@ export class SelectBox extends vsSelectBox {
private element?: HTMLElement; private element?: HTMLElement;
constructor(options: SelectOptionItemSQL[] | string[], selectedOption: string, contextViewProvider: IContextViewProvider, container?: HTMLElement, selectBoxOptions?: ISelectBoxOptions) { constructor(options: SelectOptionItemSQL[] | string[], selectedOption: string, contextViewProvider: IContextViewProvider, container?: HTMLElement, selectBoxOptions?: ISelectBoxOptions) {
let optionItems: SelectOptionItemSQL[]; let optionItems: SelectOptionItemSQL[] = SelectBox.createOptions(options);
if (Array.isArray<string>(options)) {
optionItems = (options as string[]).map(o => {
return { text: o, value: o } as SelectOptionItemSQL;
});
} else {
optionItems = options;
}
super(optionItems, 0, contextViewProvider, undefined, selectBoxOptions); super(optionItems, 0, contextViewProvider, undefined, selectBoxOptions);
this._optionsDictionary = new Map<string, number>();
for (let i = 0; i < options.length; i++) { this.populateOptionsDictionary(optionItems);
this._optionsDictionary.set(optionItems[i].value, i);
}
const option = this._optionsDictionary.get(selectedOption); const option = this._optionsDictionary.get(selectedOption);
if (option) { if (option) {
super.select(option); super.select(option);
} }
this._selectedOption = selectedOption; this._selectedOption = selectedOption;
this._dialogOptions = optionItems; this._register(this.onDidSelect(newSelect => {
this._register(this.onDidSelect(newInput => { this.onSelect(newSelect);
const selected = optionItems[newInput.index];
this._selectedOption = selected.value;
})); }));
this.enabledSelectBackground = this.selectBackground; this.enabledSelectBackground = this.selectBackground;
@@ -130,6 +119,38 @@ export class SelectBox extends vsSelectBox {
} }
} }
public onSelect(newInput: ISelectData) {
const selected = this._dialogOptions[newInput.index];
this._selectedOption = selected.value;
}
private static createOptions(options: SelectOptionItemSQL[] | string[] | ISelectOptionItem[]): SelectOptionItemSQL[] {
let selectOptions: SelectOptionItemSQL[];
if (Array.isArray<string>(options) && typeof (options[0]) === 'string') {
selectOptions = options.map(o => {
return { text: o, value: o } as SelectOptionItemSQL;
});
} else { // Handle both SelectOptionItemSql and ISelectOptionItem
const temp = (options as SelectOptionItemSQL[]);
selectOptions = temp.map(opt => {
if (opt.value === undefined) {
opt.value = opt.text;
}
return opt;
});
}
return selectOptions;
}
public populateOptionsDictionary(options: SelectOptionItemSQL[]) {
this._optionsDictionary = new Map<string, number>();
for (let i = 0; i < options.length; i++) {
this._optionsDictionary.set(options[i].value, i);
}
this._dialogOptions = options;
}
public style(styles: ISelectBoxStyles): void { public style(styles: ISelectBoxStyles): void {
super.style(styles); super.style(styles);
this.enabledSelectBackground = this.selectBackground; this.enabledSelectBackground = this.selectBackground;
@@ -165,18 +186,10 @@ export class SelectBox extends vsSelectBox {
} }
} }
public setOptions(options: string[] | ISelectOptionItem[], selected?: number): void {
let selectOptions: SelectOptionItemSQL[]; public setOptions(options: string[] | SelectOptionItemSQL[] | ISelectOptionItem[], selected?: number): void {
if (options.length > 0 && typeof options[0] !== 'string') { let selectOptions: SelectOptionItemSQL[] = SelectBox.createOptions(options);
selectOptions = options as SelectOptionItemSQL[]; this.populateOptionsDictionary(selectOptions);
} else {
selectOptions = (options as string[]).map(o => { return { text: o, value: o } as SelectOptionItemSQL; });
}
this._optionsDictionary = new Map<string, number>();
for (let i = 0; i < selectOptions.length; i++) {
this._optionsDictionary.set(selectOptions[i].value, i);
}
this._dialogOptions = selectOptions;
super.setOptions(selectOptions, selected); super.setOptions(selectOptions, selected);
} }

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { SelectBox, SelectOptionItemSQL } from 'sql/base/browser/ui/selectBox/selectBox';
import { deepClone, equals } from 'vs/base/common/objects';
const options: SelectOptionItemSQL[] = [
{ text: 't1', value: 'v1' },
{ text: 't2', value: 'v2' }
];
suite('Select Box tests', () => {
test('default value', () => {
const sb = new SelectBox(options, options[1].value, undefined, undefined, undefined);
assert(sb.value === options[1].value);
});
test('values change', () => {
const sb = new SelectBox(options, options[1].value, undefined, undefined, undefined);
const newOptions = deepClone(options);
{
const moreOptions: SelectOptionItemSQL[] = [
{ text: 't3', value: 'v3' },
{ text: 't4', value: 'v4' }
];
newOptions.push(...moreOptions);
}
sb.setOptions(newOptions);
assert(equals(sb.values, newOptions.map(s => s.value)));
});
test('the selected option changes', () => {
const sb = new SelectBox(options, options[1].value, undefined, undefined, undefined);
sb.onSelect({
index: 0,
selected: options[0].value
});
assert(sb.value === options[0].value);
});
test('values get auto populated', () => {
const newOptions = deepClone(options).map(s => { return { text: s.text, value: undefined }; });
const sb = new SelectBox(newOptions, undefined, undefined, undefined, undefined);
assert(equals(sb.values, newOptions.map(s => s.text)));
});
});

Some files were not shown because too many files have changed in this diff Show More