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
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: |
set -e
pushd ../azuredatastudio-darwin

View File

@@ -6,7 +6,6 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
// @ts-ignore review
const { remote } = require('electron');
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'.
let fullpath = path.join(root, match[1]);
let message = match[3];
// @ts-ignore
reporter(fullpath + message);
} else {
// @ts-ignore
reporter(str);
}
}

View File

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

View File

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

View File

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

View File

@@ -89,5 +89,85 @@
"prependLicenseText": [
"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';
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({
context: __dirname,
entry: {
extension: './src/extension.ts'
},
externals: {
'keytar': 'commonjs keytar'
}
externals: externals
});

View File

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

View File

@@ -10,6 +10,7 @@
"azure.resource.refresh.title": "Refresh",
"azure.resource.signin.title": "Azure: Sign In",
"azure.resource.selectsubscriptions.title": "Select Subscriptions",
"azure.resource.startterminal.title": "Start Cloud Shell",
"azure.resource.connectsqlserver.title": "Connect",
"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.graphResource,
this.metadata.settings.ossRdbmsResource,
this.metadata.settings.microsoftResource,
this.metadata.settings.azureKeyVaultResource
];
@@ -137,32 +138,47 @@ export abstract class AzureAuth implements vscode.Disposable {
public dispose() { }
public async refreshAccess(account: azdata.Account): Promise<azdata.Account> {
const response = await this.getCachedToken(account.key);
public async refreshAccess(oldAccount: azdata.Account): Promise<azdata.Account> {
const response = await this.getCachedToken(oldAccount.key);
if (!response) {
account.isStale = true;
return account;
oldAccount.isStale = true;
return oldAccount;
}
const refreshToken = response.refreshToken;
if (!refreshToken || !refreshToken.key) {
account.isStale = true;
return account;
oldAccount.isStale = true;
return oldAccount;
}
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) {
oldAccount.isStale = true;
if (ex.message) {
await vscode.window.showErrorMessage(ex.message);
}
console.log(ex);
}
return account;
return oldAccount;
}
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);
if (!resource) {
return undefined;
@@ -199,8 +215,13 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!baseToken) {
return undefined;
}
try {
await this.refreshAccessToken(account.key, baseToken.refreshToken, tenant, resource);
} catch (ex) {
account.isStale = true;
return undefined;
}
cachedTokens = await this.getCachedToken(account.key, resource.id, tenant.id);
if (!cachedTokens) {
return undefined;
@@ -349,8 +370,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return { accessToken, refreshToken, tokenClaims };
} catch (err) {
console.dir(err);
const msg = localize('azure.noToken', "Retrieving the token failed.");
const msg = localize('azure.noToken', "Retrieving the Azure token failed. Please sign in again.");
vscode.window.showErrorMessage(msg);
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 } = {
grant_type: 'refresh_token',
refresh_token: rt.token,
@@ -377,7 +397,10 @@ export abstract class AzureAuth implements vscode.Disposable {
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) {
console.log('Access or refresh token were undefined');
@@ -385,7 +408,9 @@ export abstract class AzureAuth implements vscode.Disposable {
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();
} 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.');
await vscode.window.showErrorMessage(msg);
vscode.window.showErrorMessage(msg);
console.dir(err);
return undefined;
}
@@ -181,7 +181,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
refreshToken = rt;
} catch (ex) {
if (ex.msg) {
await vscode.window.showErrorMessage(ex.msg);
vscode.window.showErrorMessage(ex.msg);
}
console.log(ex);
}
@@ -199,7 +199,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
} catch (ex) {
console.log(ex);
if (ex.msg) {
await vscode.window.showErrorMessage(ex.msg);
vscode.window.showErrorMessage(ex.msg);
authCompleteDeferred.reject(ex);
} else {
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) {
console.log('No auth method was enabled.');
await vscode.window.showErrorMessage(noAuthAvailable);
vscode.window.showErrorMessage(noAuthAvailable);
return { canceled: true };
}
@@ -145,7 +145,7 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
if (!pick) {
console.log('No auth method was selected.');
await vscode.window.showErrorMessage(noAuthSelected);
vscode.window.showErrorMessage(noAuthSelected);
return { canceled: true };
}

View File

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

View File

@@ -18,6 +18,11 @@ const publicAzureSettings: ProviderSettings = {
host: 'https://login.microsoftonline.com/',
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
signInResourceId: 'https://management.core.windows.net/',
microsoftResource: {
id: 'marm',
endpoint: 'https://management.core.windows.net/',
azureResourceId: AzureResource.MicrosoftResourceManagement
},
graphResource: {
id: 'graph',
endpoint: 'https://graph.microsoft.com',
@@ -62,6 +67,11 @@ const usGovAzureSettings: ProviderSettings = {
host: 'https://login.microsoftonline.us/',
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
signInResourceId: 'https://management.core.usgovcloudapi.net/',
microsoftResource: {
id: 'marm',
endpoint: 'https://management.core.usgovcloudapi.net/',
azureResourceId: AzureResource.MicrosoftResourceManagement
},
graphResource: {
id: 'graph',
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 { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../azureResource/interfaces';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { GetSubscriptionsResult, GetResourceGroupsResult } from '../azurecore';
import { isArray } from 'util';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
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
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
appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => {
if (!(node instanceof AzureResourceAccountTreeNode)) {
return;

View File

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

View File

@@ -9,6 +9,7 @@ import { Account, DidChangeAccountsParams } from 'azdata';
import { Event } from 'vscode';
import { azureResource } from './azure-resource';
import { AzureAccount, AzureAccountSecurityToken, Tenant } from '../account-provider/interfaces';
export interface IAzureResourceAccountService {
getAccounts(): Promise<Account[]>;
@@ -24,6 +25,10 @@ export interface IAzureResourceSubscriptionFilterService {
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 {
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) {
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
} 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) => {
const token = tokens[subscription.id];
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 { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService';
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 { AzureResourceAccountService } from './azureResource/services/accountService';
import { AzureResourceSubscriptionService } from './azureResource/services/subscriptionService';
@@ -30,6 +30,7 @@ import { SqlInstanceResourceService } from './azureResource/providers/sqlinstanc
import { SqlInstanceProvider } from './azureResource/providers/sqlinstance/sqlInstanceProvider';
import { PostgresServerProvider } from './azureResource/providers/postgresServer/postgresServerProvider';
import { PostgresServerService } from './azureResource/providers/postgresServer/postgresServerService';
import { AzureTerminalService } from './azureResource/services/terminalService';
import { SqlInstanceArcProvider } from './azureResource/providers/sqlinstanceArc/sqlInstanceArcProvider';
import { SqlInstanceArcResourceService } from './azureResource/providers/sqlinstanceArc/sqlInstanceArcService';
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<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext)));
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> {

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) {
return new vscode.CustomDocument(PreviewManager.viewType, uri);
return new vscode.CustomDocument(uri);
}
public async resolveCustomEditor(

View File

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

View File

@@ -4,122 +4,11 @@
*--------------------------------------------------------------------------------------------*/
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) {
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
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 * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as mssql from '../../mssql';
import * as utils from './utils';
import * as uuid from './uuid';
import { isTestSetupCompleted } from './testContext';
import * as mssql from '../../../mssql';
import * as utils from '../utils';
import * as uuid from '../uuid';
import { isTestSetupCompleted } from '../testContext';
import assert = require('assert');
import { getStandaloneServer, TestServerProfile } from './testConfig';
import { getStandaloneServer, TestServerProfile } from '../testConfig';
let cmsService: mssql.ICmsService;
let server: TestServerProfile;

View File

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

View File

@@ -7,10 +7,10 @@ import 'mocha';
import * as assert from 'assert';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { isTestSetupCompleted } from './testContext';
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 { connectToServer, sleep, testServerProfileToIConnectionProfile } from './utils';
import { isTestSetupCompleted } from '../testContext';
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 { connectToServer, sleep, testServerProfileToIConnectionProfile } from '../utils';
import * as fs from 'fs';
import { stressify } from 'adstest';
import { isNullOrUndefined, promisify } from 'util';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,8 @@ export const msgYes = localize('msgYes', "Yes");
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 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 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.");

View File

@@ -7,22 +7,32 @@ import * as azdata from 'azdata';
import * as nbExtensionApis from '../typings/notebookServices';
import { ApiWrapper } from './apiWrapper';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
const maxNumberOfRetries = 3;
const maxNumberOfRetries = 2;
const listPythonPackagesQuery = `
Declare @tablevar table(name NVARCHAR(MAX), version NVARCHAR(MAX))
insert into @tablevar(name, version)
EXEC sp_execute_external_script
@language=N'Python',
@script=N'import pkg_resources
import pandas
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 = `
Declare @tablevar table(name NVARCHAR(MAX), version NVARCHAR(MAX))
insert into @tablevar(name, version)
EXEC sp_execute_external_script
@language=N'R',
@script=N'
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 = `
@@ -63,24 +73,24 @@ export class QueryRunner {
* Returns python packages installed in SQL server instance
* @param connection SQL Connection
*/
public async getPythonPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> {
return this.getPackages(connection, listPythonPackagesQuery);
public async getPythonPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this.getPackages(connection, databaseName, listPythonPackagesQuery);
}
/**
* Returns python packages installed in SQL server instance
* @param connection SQL Connection
*/
public async getRPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> {
return this.getPackages(connection, listRPackagesQuery);
public async getRPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
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 result: azdata.SimpleExecuteResult | undefined = undefined;
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) {
break;
}

View File

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

View File

@@ -40,7 +40,7 @@ export class ModelPythonClient {
* Installs dependencies for python client
*/
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);
}

View File

@@ -5,7 +5,6 @@
import * as azdata from 'azdata';
import { ApiWrapper } from '../common/apiWrapper';
import * as constants from '../common/constants';
import * as nbExtensionApis from '../typings/notebookServices';
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();
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> {
@@ -42,16 +44,16 @@ export abstract class SqlPackageManageProviderBase {
* @param packages Packages to install
* @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) {
await Promise.all(packages.map(x => this.installPackage(x, useMinVersion)));
await Promise.all(packages.map(x => this.installPackage(x, useMinVersion, databaseName)));
}
//TODO: use 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) {
let packageOverview = await this.getPackageOverview(packageDetail.name);
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
* @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) {
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
*/
public async listPackages(): Promise<nbExtensionApis.IPackageDetails[]> {
let packages = await this.fetchPackages();
public async listPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
let packages = await this.fetchPackages(databaseName);
if (packages) {
packages = packages.sort((a, b) => this.comparePackages(a, b));
} else {
@@ -110,6 +112,6 @@ export abstract class SqlPackageManageProviderBase {
}
protected abstract fetchPackage(packageName: string): Promise<nbExtensionApis.IPackageOverview>;
protected abstract fetchPackages(): Promise<nbExtensionApis.IPackageDetails[]>;
protected abstract executeScripts(scriptMode: ScriptMode, packageDetails: nbExtensionApis.IPackageDetails): Promise<void>;
protected abstract fetchPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]>;
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
* @param connection SQL Connection
*/
public async getPythonPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> {
return this._queryRunner.getPythonPackages(connection);
public async getPythonPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this._queryRunner.getPythonPackages(connection, databaseName);
}
/**
* Returns python packages installed in SQL server instance
* @param connection SQL Connection
*/
public async getRPackages(connection: azdata.connection.ConnectionProfile): Promise<nbExtensionApis.IPackageDetails[]> {
return this._queryRunner.getRPackages(connection);
public async getRPackages(connection: azdata.connection.ConnectionProfile, databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
return this._queryRunner.getRPackages(connection, databaseName);
}
}

View File

@@ -93,7 +93,6 @@ export class PackageManager {
// Execute the command
//
this._apiWrapper.executeCommand(constants.managePackagesCommand, {
multiLocations: false,
defaultLocation: defaultProvider.packageTarget.location,
defaultProviderId: defaultProvider.providerId
});
@@ -116,7 +115,7 @@ export class PackageManager {
* Installs dependencies for the extension
*/
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.installRequiredRPackages()], true);
}
@@ -130,7 +129,7 @@ export class PackageManager {
}
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 = '';
requiredPackages.forEach(packageDetails => {
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}`;
fileContent = `${fileContent}${packageNameDetail}\n`;
}
@@ -177,7 +177,7 @@ export class PackageManager {
private async getInstalledPipPackages(): Promise<nbExtensionApis.IPackageDetails[]> {
try {
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[] = [];
if (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 { ProcessService } from '../common/processService';
import { Config } from '../configurations/config';
import { SqlPackageManageProviderBase, ScriptMode } from './SqlPackageManageProviderBase';
import { SqlPackageManageProviderBase, ScriptMode } from './packageManageProviderBase';
import { HttpClient } from '../common/httpClient';
import * as utils from '../common/utils';
import { PackageManagementService } from './packageManagementService';
@@ -50,8 +50,8 @@ export class SqlPythonPackageManageProvider extends SqlPackageManageProviderBase
/**
* Returns list of packages
*/
protected async fetchPackages(): Promise<nbExtensionApis.IPackageDetails[]> {
return await this._service.getPythonPackages(await this.getCurrentConnection());
protected async fetchPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
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 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 credentials = await this._apiWrapper.getCredentials(connection.connectionId);
if (connection) {
let port = '1433';
let server = connection.serverName;
let database = connection.databaseName ? `, database="${connection.databaseName}"` : '';
let database = databaseName ? `, database="${databaseName}"` : '';
let index = connection.serverName.indexOf(',');
if (index > 0) {
port = connection.serverName.substring(index + 1);

View File

@@ -10,7 +10,7 @@ import * as nbExtensionApis from '../typings/notebookServices';
import { ApiWrapper } from '../common/apiWrapper';
import { ProcessService } from '../common/processService';
import { Config } from '../configurations/config';
import { SqlPackageManageProviderBase, ScriptMode } from './SqlPackageManageProviderBase';
import { SqlPackageManageProviderBase, ScriptMode } from './packageManageProviderBase';
import { HttpClient } from '../common/httpClient';
import * as constants from '../common/constants';
import { PackageManagementService } from './packageManagementService';
@@ -54,8 +54,8 @@ export class SqlRPackageManageProvider extends SqlPackageManageProviderBase impl
/**
* Returns list of packages
*/
protected async fetchPackages(): Promise<nbExtensionApis.IPackageDetails[]> {
return await this._service.getRPackages(await this.getCurrentConnection());
protected async fetchPackages(databaseName: string): Promise<nbExtensionApis.IPackageDetails[]> {
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 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 credentials = await this._apiWrapper.getCredentials(connection.connectionId);
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 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> {
let testContext = createContext();
//let packagesInstalled = false;
let packagesInstalled = false;
let installedPackages = `[
{"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) => {
if (command.indexOf('pip install') > 0) {
//packagesInstalled = true;
packagesInstalled = true;
}
return Promise.resolve(installedPackages);
});
let packageManager = createPackageManager(testContext);
await packageManager.installDependencies();
//should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded);
//should.equal(packagesInstalled, true);
should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded);
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> {
@@ -197,7 +249,7 @@ describe('Package Manager', () => {
{ name: 'pymssql', version: '2.1.4' },
{ 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: '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 'mocha';
import * as TypeMoq from 'typemoq';
import * as constants from '../../common/constants';
import { SqlPythonPackageManageProvider } from '../../packageManagement/sqlPythonPackageManageProvider';
import { createContext, TestContext } from './utils';
import * as nbExtensionApis from '../../typings/notebookServices';
@@ -40,10 +39,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected = [
{
'name': 'a-name',
@@ -72,10 +71,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected = [
{
'name': 'b-name',
@@ -95,10 +94,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
let packages: nbExtensionApis.IPackageDetails[];
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected);
});
@@ -108,10 +107,10 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected);
});
@@ -152,7 +151,7 @@ describe('SQL Python Package Manager', () => {
});
let provider = createProvider(testContext);
await provider.installPackages(packages, false);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
@@ -192,7 +191,7 @@ describe('SQL Python Package Manager', () => {
});
let provider = createProvider(testContext);
await provider.uninstallPackages(packages);
await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
@@ -233,7 +232,7 @@ describe('SQL Python Package Manager', () => {
});
let provider = createProvider(testContext);
await provider.installPackages(packages, false);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
@@ -255,7 +254,7 @@ describe('SQL Python Package Manager', () => {
let provider = createProvider(testContext);
await provider.installPackages(packages, false);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, false);
});
@@ -277,7 +276,7 @@ describe('SQL Python Package Manager', () => {
let provider = createProvider(testContext);
await provider.uninstallPackages(packages);
await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, false);
});
@@ -346,42 +345,44 @@ describe('SQL Python Package Manager', () => {
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 connection: azdata.connection.ConnectionProfile;
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
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 connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName';
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.listDatabases(connection.connectionId)).returns(() => { return Promise.resolve(databaseNames); });
let provider = createProvider(testContext);
let actual = await provider.getLocationTitle();
let actual = await provider.getLocations();
should.deepEqual(actual, `${connection.serverName} ${connection.databaseName}`);
});
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} `);
should.deepEqual(actual, expected);
});
function createProvider(testContext: TestContext): SqlPythonPackageManageProvider {

View File

@@ -7,7 +7,6 @@ import * as azdata from 'azdata';
import * as should from 'should';
import 'mocha';
import * as TypeMoq from 'typemoq';
import * as constants from '../../common/constants';
import { SqlRPackageManageProvider } from '../../packageManagement/sqlRPackageManageProvider';
import { createContext, TestContext } from './utils';
import * as nbExtensionApis from '../../typings/notebookServices';
@@ -40,10 +39,10 @@ describe('SQL R Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected = [
{
'name': 'a-name',
@@ -63,10 +62,10 @@ describe('SQL R Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
let packages: nbExtensionApis.IPackageDetails[];
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected);
});
@@ -76,10 +75,10 @@ describe('SQL R Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile();
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 actual = await provider.listPackages();
let actual = await provider.listPackages(connection.databaseName);
let expected: nbExtensionApis.IPackageDetails[] = [];
should.deepEqual(actual, expected);
});
@@ -118,7 +117,7 @@ describe('SQL R Package Manager', () => {
});
let provider = createProvider(testContext);
await provider.installPackages(packages, false);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
@@ -157,7 +156,7 @@ describe('SQL R Package Manager', () => {
});
let provider = createProvider(testContext);
await provider.uninstallPackages(packages);
await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
@@ -179,7 +178,7 @@ describe('SQL R Package Manager', () => {
let provider = createProvider(testContext);
await provider.installPackages(packages, false);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, false);
});
@@ -201,7 +200,7 @@ describe('SQL R Package Manager', () => {
let provider = createProvider(testContext);
await provider.uninstallPackages(packages);
await provider.uninstallPackages(packages, connection.databaseName);
should.deepEqual(packagesUpdated, false);
});
@@ -271,42 +270,44 @@ describe('SQL R Package Manager', () => {
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 connection: azdata.connection.ConnectionProfile;
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
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 connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName';
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.listDatabases(connection.connectionId)).returns(() => { return Promise.resolve(databaseNames); });
let provider = createProvider(testContext);
let actual = await provider.getLocationTitle();
let actual = await provider.getLocations();
should.deepEqual(actual, `${connection.serverName} ${connection.databaseName}`);
});
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} `);
should.deepEqual(actual, expected);
});
function createProvider(testContext: TestContext): SqlRPackageManageProvider {

View File

@@ -59,7 +59,7 @@ describe('Query Runner', () => {
let queryProvider: azdata.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, []);
});
@@ -70,7 +70,7 @@ describe('Query Runner', () => {
testContext.queryProvider.runQueryAndReturn = () => { return Promise.reject(); };
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, []);
});
@@ -117,7 +117,7 @@ describe('Query Runner', () => {
testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); };
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);
});
@@ -138,7 +138,7 @@ describe('Query Runner', () => {
testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); };
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);
});

View File

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

View File

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

View File

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

View File

@@ -51,13 +51,56 @@ export interface IPackageOverview {
summary: string;
}
export interface IPackageManageProvider {
providerId: string;
packageTarget: IPackageTarget;
listPackages(): Promise<IPackageDetails[]>
installPackages(package: IPackageDetails[], useMinVersion: boolean): Promise<void>;
uninstallPackages(package: IPackageDetails[]): Promise<void>;
canUseProvider(): Promise<boolean>;
getLocationTitle(): Promise<string>;
getPackageOverview(packageName: string): Promise<IPackageOverview>
export interface IPackageLocation {
name: string;
displayName: string;
}
/**
* Package manage provider interface
*/
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 mssql from '../../../../mssql/src/mssql';
import * as mssql from '../../../../mssql';
import { LanguageViewBase } from './languageViewBase';
import * as constants from '../../common/constants';
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.
*--------------------------------------------------------------------------------------------*/
import * as mssql from '../../../../mssql/src/mssql';
import * as mssql from '../../../../mssql';
import { ApiWrapper } from '../../common/apiWrapper';
import { LanguageService } from '../../externalLanguage/languageService';
import { LanguagesDialog } from './languagesDialog';

View File

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

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import * as constants from '../../common/constants';
import * as mssql from '../../../../mssql/src/mssql';
import * as mssql from '../../../../mssql';
import { LanguageViewBase } from './languageViewBase';
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) {
return new vscode.CustomDocument(this.customEditorViewType, uri);
return new vscode.CustomDocument(uri);
}
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": {

View File

@@ -140,5 +140,8 @@
"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.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;
pipPackage = await this.dialog.model.getPackageOverview(packageName);
if (!pipPackage.versions || pipPackage.versions.length === 0) {
if (!pipPackage?.versions || pipPackage.versions.length === 0) {
this.dialog.showErrorMessage(
localize('managePackages.noVersionsFound',
"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 packageTypeDropdown: azdata.DropDownComponent;
private locationComponent: azdata.TextComponent;
private locationComponent: azdata.Component;
private installedPackageCount: azdata.TextComponent;
private installedPackagesTable: azdata.TableComponent;
private installedPackagesLoader: azdata.LoadingComponent;
private uninstallPackageButton: azdata.ButtonComponent;
private view: azdata.ModelView | undefined;
private formBuilder: azdata.FormBuilder;
constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) {
this.prompter = new CodeAdapter();
@@ -32,14 +34,7 @@ export class InstalledPackagesTab {
this.installedPkgTab = azdata.window.createTab(localize('managePackages.installedTabTitle', "Installed"));
this.installedPkgTab.registerContent(async 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();
this.view = view;
let dropdownValues = this.dialog.model.getPackageTypes().map(x => {
return {
name: x.providerId,
@@ -52,11 +47,17 @@ export class InstalledPackagesTab {
value: defaultPackageType
}).component();
this.dialog.changeProvider(defaultPackageType.providerId);
this.packageTypeDropdown.onValueChanged(() => {
this.dialog.resetPages((<azdata.CategoryValue>this.packageTypeDropdown.value).name)
.catch(err => {
this.packageTypeDropdown.onValueChanged(async () => {
this.dialog.changeProvider((<azdata.CategoryValue>this.packageTypeDropdown.value).name);
try {
await this.resetLocations();
await this.dialog.resetPages();
}
catch (err) {
this.dialog.showErrorMessage(utils.getErrorMessage(err));
});
}
});
this.installedPackageCount = view.modelBuilder.text().withProperties({
@@ -81,11 +82,8 @@ export class InstalledPackagesTab {
}).component();
this.uninstallPackageButton.onDidClick(() => this.doUninstallPackage());
let formModel = view.modelBuilder.formContainer()
this.formBuilder = view.modelBuilder.formContainer()
.withFormItems([{
component: this.locationComponent,
title: localize('managePackages.location', "Location")
}, {
component: this.packageTypeDropdown,
title: localize('managePackages.packageType', "Package Type")
}, {
@@ -97,10 +95,11 @@ export class InstalledPackagesTab {
}, {
component: this.uninstallPackageButton,
title: ''
}]).component();
}]);
await this.resetLocations();
this.installedPackagesLoader = view.modelBuilder.loadingComponent()
.withItem(formModel)
.withItem(this.formBuilder.component())
.withProperties({
loading: true
}).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 {
return this.installedPkgTab;
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
* 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 * as constants from '../common/constants';
import * as utils from '../common/utils';
@@ -35,7 +35,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
/**
* Returns list of packages
*/
public async listPackages(): Promise<IPackageDetails[]> {
public async listPackages(location?: string): Promise<IPackageDetails[]> {
return await this.jupyterInstallation.getInstalledCondaPackages();
}
@@ -44,7 +44,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
* @param packages Packages to install
* @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);
}
@@ -52,7 +52,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
* Uninstalls given packages
* @param packages Packages to uninstall
*/
uninstallPackages(packages: IPackageDetails[]): Promise<void> {
uninstallPackages(packages: IPackageDetails[], location?: string): Promise<void> {
return this.jupyterInstallation.uninstallCondaPackages(packages);
}
@@ -66,8 +66,8 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
/**
* Returns location title
*/
getLocationTitle(): Promise<string> {
return Promise.resolve(constants.localhostTitle);
getLocations(): Promise<IPackageLocation[]> {
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.
*--------------------------------------------------------------------------------------------*/
import { IPackageManageProvider, IPackageDetails, IPackageTarget, IPackageOverview } from '../types';
import { IPackageManageProvider, IPackageDetails, IPackageTarget, IPackageOverview, IPackageLocation } from '../types';
import { IJupyterServerInstallation } from './jupyterServerInstallation';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
@@ -38,7 +38,7 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
/**
* Returns list of packages
*/
public async listPackages(): Promise<IPackageDetails[]> {
public async listPackages(location?: string): Promise<IPackageDetails[]> {
return await this.jupyterInstallation.getInstalledPipPackages();
}
@@ -47,7 +47,7 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
* @param packages Packages to install
* @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);
}
@@ -55,7 +55,7 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
* Uninstalls given packages
* @param packages Packages to uninstall
*/
uninstallPackages(packages: IPackageDetails[]): Promise<void> {
uninstallPackages(packages: IPackageDetails[], location?: string): Promise<void> {
return this.jupyterInstallation.uninstallPipPackages(packages);
}
@@ -69,8 +69,8 @@ export class LocalPipPackageManageProvider implements IPackageManageProvider {
/**
* Returns location title
*/
getLocationTitle(): Promise<string> {
return Promise.resolve(constants.localhostTitle);
getLocations(): Promise<IPackageLocation[]> {
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);
let options = {
multiLocations: true,
defaultLocation: 'invalid location'
};
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
@@ -64,29 +63,12 @@ describe('Manage Packages', () => {
providers.set(provider.providerId, provider);
let options = {
multiLocations: true,
defaultProviderId: 'invalid provider'
};
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
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> {
let testContext = createContext();
let provider = createProvider(testContext);
@@ -96,7 +78,6 @@ describe('Manage Packages', () => {
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, undefined);
await model.init();
should.equal(model.multiLocationMode, true);
should.equal(model.defaultLocation, provider.packageTarget.location);
should.equal(model.defaultProviderId, provider.providerId);
});
@@ -119,14 +100,12 @@ describe('Manage Packages', () => {
providers.set(testContext1.provider.providerId, createProvider(testContext1));
providers.set(testContext2.provider.providerId, createProvider(testContext2));
let options = {
multiLocations: false,
defaultLocation: testContext2.provider.packageTarget.location,
defaultProviderId: testContext2.provider.providerId
};
let model = new ManagePackagesDialogModel(jupyterServerInstallation, providers, options);
await model.init();
should.equal(model.multiLocationMode, false);
should.equal(model.defaultLocation, testContext2.provider.packageTarget.location);
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> {
let testContext1 = createContext();
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 = {
location: 'location1',
packageType: 'package-type1'
@@ -203,7 +182,7 @@ describe('Manage Packages', () => {
let testContext2 = createContext();
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 = {
location: 'location2',
packageType: 'package-type2'
@@ -217,7 +196,7 @@ describe('Manage Packages', () => {
await model.init();
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> {
@@ -283,7 +262,7 @@ describe('Manage Packages', () => {
let testContext2 = createContext();
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 = {
location: 'location2',
packageType: 'package-type2'
@@ -301,6 +280,12 @@ describe('Manage Packages', () => {
testContext2.provider.listPackages = () => {
return Promise.resolve(packages);
};
testContext1.provider.listPackages = () => {
return Promise.resolve([{
name: 'p3',
version: '1.1.1.3'
}]);
};
let providers = new Map<string, IPackageManageProvider>();
providers.set(testContext1.provider.providerId, createProvider(testContext1));
@@ -315,7 +300,50 @@ describe('Manage Packages', () => {
await should(model.installPackages(packages)).resolved();
await should(model.uninstallPackages(packages)).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 {
@@ -327,7 +355,7 @@ describe('Manage Packages', () => {
packageType: 'package-type'
},
canUseProvider: () => { return Promise.resolve(true); },
getLocationTitle: () => { return Promise.resolve('location-title'); },
getLocations: () => { return Promise.resolve([{displayName: 'location-title', name: 'location'}]); },
installPackages:() => { return Promise.resolve(); },
uninstallPackages: (packages: IPackageDetails[]) => { return Promise.resolve(); },
listPackages: () => { return Promise.resolve([]); },
@@ -339,10 +367,10 @@ describe('Manage Packages', () => {
function createProvider(testContext: TestContext): IPackageManageProvider {
let mockProvider = TypeMoq.Mock.ofType(LocalPipPackageManageProvider);
mockProvider.setup(x => x.canUseProvider()).returns(() => testContext.provider.canUseProvider());
mockProvider.setup(x => x.getLocationTitle()).returns(() => testContext.provider.getLocationTitle());
mockProvider.setup(x => x.installPackages(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.listPackages()).returns(() => testContext.provider.listPackages());
mockProvider.setup(x => x.getLocations()).returns(() => testContext.provider.getLocations());
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(), TypeMoq.It.isAny())).returns((packages) => testContext.provider.uninstallPackages(packages));
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.packageTarget).returns(() => testContext.provider.packageTarget);
mockProvider.setup(x => x.providerId).returns(() => testContext.provider.providerId);

View File

@@ -65,6 +65,14 @@ export interface IPackageDetails {
version: string;
}
/**
* Package location
*/
export interface IPackageLocation {
name: string;
displayName: string;
}
/**
* Package target interface
*/
@@ -99,20 +107,22 @@ export interface IPackageManageProvider {
/**
* Returns list of installed packages
*/
listPackages(): Promise<IPackageDetails[]>;
listPackages(location?: string): Promise<IPackageDetails[]>;
/**
* Installs give packages
* @param package Packages to install
* @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
* @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
@@ -122,7 +132,7 @@ export interface IPackageManageProvider {
/**
* Returns location title
*/
getLocationTitle(): Promise<string>;
getLocations(): Promise<IPackageLocation[]>;
/**
* Returns Package Overview

View File

@@ -103,7 +103,7 @@ export class SchemaCompareDialog {
endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: (this.sourceServerDropdown.value as ConnectionDropdownValue).displayName,
serverName: (this.sourceServerDropdown.value as ConnectionDropdownValue).name,
databaseName: (<azdata.CategoryValue>this.sourceDatabaseDropdown.value).name,
databaseName: this.sourceDatabaseDropdown.value.toString(),
ownerUri: ownerUri,
packageFilePath: '',
connectionDetails: undefined
@@ -127,7 +127,7 @@ export class SchemaCompareDialog {
endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: (this.targetServerDropdown.value as ConnectionDropdownValue).displayName,
serverName: (this.targetServerDropdown.value as ConnectionDropdownValue).name,
databaseName: (<azdata.CategoryValue>this.targetDatabaseDropdown.value).name,
databaseName: this.targetDatabaseDropdown.value.toString(),
ownerUri: ownerUri,
packageFilePath: '',
connectionDetails: undefined
@@ -204,7 +204,7 @@ export class SchemaCompareDialog {
this.sourceDatabaseComponent = await this.createSourceDatabaseDropdown(view);
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);
@@ -212,7 +212,7 @@ export class SchemaCompareDialog {
this.targetDatabaseComponent = await this.createTargetDatabaseDropdown(view);
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);
@@ -477,7 +477,7 @@ export class SchemaCompareDialog {
});
}
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 {
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;
}
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;
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) {
currentDropdown.updateProperties({
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 idx = -1;
@@ -654,10 +661,7 @@ export class SchemaCompareDialog {
idx = count;
}
return {
displayName: db,
name: db
};
return db;
});
if (idx >= 0) {

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,23 @@ import * as vscode from 'vscode';
type LogLevel = 'Trace' | 'Info' | 'Error';
enum Level {
Trace = 'trace',
Info = 'Info'
}
class Log {
private output: vscode.OutputChannel;
private level: Level;
constructor() {
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 {
@@ -32,6 +44,12 @@ class Log {
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 {
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
if (data) {

View File

@@ -38,21 +38,12 @@
"description": "A test modifier"
}
],
"semanticTokenStyleDefaults": [
"semanticTokenScopes": [
{
"selector": "testToken",
"scope": [ "entity.name.function.special" ]
},
{
"selector": "*.testModifier",
"light": {
"fontStyle": "bold"
},
"dark": {
"fontStyle": "bold"
},
"highContrast": {
"fontStyle": "bold"
"scopes": {
"testToken": [
"entity.name.function.special"
]
}
}
],

View File

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

View File

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

View File

@@ -23,6 +23,7 @@
"iconv-lite": "0.5.0",
"jquery": "3.4.0",
"jschardet": "2.1.1",
"minimist": "^1.2.5",
"native-watchdog": "1.3.0",
"ng2-charts": "^1.6.0",
"node-pty": "^0.10.0-beta2",
@@ -33,7 +34,6 @@
"semver-umd": "^5.5.5",
"slickgrid": "github:anthonydresser/SlickGrid#2.3.32",
"spdlog": "^0.11.1",
"vscode-minimist": "^1.2.2",
"vscode-nsfw": "1.2.8",
"vscode-proxy-agent": "^0.5.2",
"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"
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:
version "0.5.1"
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"
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:
version "1.2.8"
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 util = require('util');
const opn = require('opn');
const minimist = require('vscode-minimist');
const minimist = require('minimist');
const APP_ROOT = path.dirname(__dirname);
const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions');

View File

@@ -55,6 +55,9 @@ function code() {
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
ELECTRON="$ROOT/.build/electron/Code - OSS.exe"
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 ^
--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 %VSCODEEXTENSIONSDIR%

View File

@@ -60,7 +60,7 @@ fi
--extensionDevelopmentPath=$ROOT/extensions/profiler \
--extensionDevelopmentPath=$ROOT/extensions/resource-deployment \
--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 \
--disable-telemetry --disable-crash-reporter --disable-updates --skip-getting-started --disable-inspect

View File

@@ -142,13 +142,11 @@ function pipeLoggingToParent() {
function handleExceptions() {
// Handle uncaught exceptions
// @ts-ignore
process.on('uncaughtException', function (err) {
console.error('Uncaught Exception: ', err);
});
// Handle unhandled promise rejections
// @ts-ignore
process.on('unhandledRejection', function (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) {
// @ts-ignore
const webFrame = require('electron').webFrame;
const path = require('path');
@@ -49,7 +48,6 @@ exports.load = function (modulePaths, resultCallback, options) {
}
// Error handler
// @ts-ignore
process.on('uncaughtException', function (error) {
onUnexpectedError(error, enableDeveloperTools);
});
@@ -184,7 +182,6 @@ function parseURLQueryArgs() {
*/
function registerDeveloperKeybindings(disallowReloadKeybinding) {
// @ts-ignore
const ipc = require('electron').ipcRenderer;
const extractKey = function (e) {
@@ -223,7 +220,6 @@ function registerDeveloperKeybindings(disallowReloadKeybinding) {
function onUnexpectedError(error, enableDeveloperTools) {
// @ts-ignore
const ipc = require('electron').ipcRenderer;
if (enableDeveloperTools) {

View File

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

View File

@@ -6,7 +6,7 @@
//@ts-check
'use strict';
// @ts-ignore
// @ts-expect-error
// const pkg = require('../package.json');
const path = require('path');
const os = require('os');

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

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

View File

@@ -225,6 +225,7 @@ declare module 'azdata' {
*/
export interface TabbedPanelLayout {
orientation: TabOrientation;
showIcon: boolean;
}
/**
@@ -245,6 +246,11 @@ declare module 'azdata' {
* Id of the tab
*/
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 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;
}
.tabbedPanel.vertical>.title {
flex: 0 0 auto;
flex-direction: column;
height: 100%;
}
.tabbedPanel .tabContainer {
flex: 1 1 auto;
overflow: hidden;
height: 100%;
}
.tabbedPanel .tabList {
@@ -45,31 +52,34 @@ panel {
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 {
font-size: 13px;
padding-bottom: 4px;
font-weight: 600;
}
.tabbedPanel.vertical .tabList .tab .tabLabel {
font-size: 11px;
}
.tabbedPanel .tabList .tab-header {
display: flex;
padding-left: 5px;
padding-right: 5px;
cursor: pointer;
min-width: 65px;
}
.tabbedPanel.vertical .tabList .tab-header {
.tabbedPanel.vertical > .title .tabList .tab-header {
display: block;
text-transform: none;
text-overflow: ellipsis;
overflow: hidden;
width: auto;
height: 50px;
line-height: 45px;
min-width: 150px;
line-height: 35px;
}
.tabbedPanel .tabList .tab .tabIcon.codicon {
@@ -85,8 +95,6 @@ panel {
.tabbedPanel .composite.title .title-actions .action-label {
display: block;
height: 35px;
line-height: 35px;
min-width: 28px;
background-size: 16px;
background-position: center center;
@@ -117,15 +125,15 @@ panel {
}
.tabbedPanel>.tab-content {
flex: 1;
flex: 1 1 auto;
position: relative;
}
.tabbedPanel.vertical > .title > .tabContainer > .monaco-scrollable-element > .tabList {
.tabbedPanel.vertical>.title > .tabContainer .tabList {
flex-flow: column;
}
.tabbedPanel.horizontal > .title > .tabContainer > .monaco-scrollable-element > .tabList {
.tabbedPanel.horizontal > .title > .tabContainer .tabList {
flex-flow: row;
}
@@ -153,9 +161,11 @@ panel {
border-color: rgb(214, 214, 214);
}
.tabbedPanel .action-container {
.tabbedPanel .vertical-tab-action-container {
display: flex;
flex-flow: row-reverse;
height: 35px;
padding: 0px 5px;
}
.tabbedPanel .tab-action {
@@ -164,9 +174,10 @@ panel {
padding: 0px;
border: 0px;
background-color: transparent;
background-position: 2px center;
background-position: center;
background-repeat: no-repeat;
background-size: 11px 11px;
background-size: 9px 9px;
align-self: center;
}
.vs .tabbedPanel .tab-action.collapse {

View File

@@ -52,16 +52,16 @@ let idPool = 0;
<div class="tabbedPanel fullsize" [ngClass]="options.layout === NavigationBarLayout.vertical ? 'vertical' : 'horizontal'">
<div *ngIf="!options.showTabsWhenOne ? _tabs.length !== 1 : true" class="composite title">
<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>
</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">
<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>
</ng-container>
<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>
</div>
</ng-container >

View File

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

View File

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