Compare commits

...

31 Commits

Author SHA1 Message Date
Karl Burtram
2299a76973 Adding the mirrored parallelism icon (#19391) (#19428)
Co-authored-by: Aasim Khan <aaskhan@microsoft.com>
2022-05-18 20:22:45 -07:00
Aasim Khan
85bb8e3948 Fixing zoomed badges (#19178) (#19430) 2022-05-18 20:21:28 -07:00
Karl Burtram
71c8849199 Fixing zoomed badges (#19178) (#19429)
Co-authored-by: Aasim Khan <aasimkhan30@gmail.com>
2022-05-18 20:20:50 -07:00
Karl Burtram
201e53d091 Fix connection issue with wrong resource endpoint (#19387) (#19427)
* Fix connection bug accessing PBI resource

* Address CR feedback

* Fix null check
2022-05-18 20:20:43 -07:00
Karl Burtram
99b40b448a Bump version to 1.36.2 for hotfix (#19426) 2022-05-18 20:12:24 -07:00
Benjin Dubishar
b77aa3dc77 [Port] //Build features for VSCode database projects extension release only (#19422)
* add SDK option to create project from db quickpick (#19100)

* Add SDK option to create project from db quickpick

* cleanup

* New UI for deploying SQL project to a new Azure server (#18833)

* SQL Project Deploy to docker container - Adding a UI for user to select docker image tag (#19297)

* add docker image with telemetry for publish to container (#19360)

* add docker info image to telemetry for publish to container

* change name

* merge issue

* version bump

Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com>
Co-authored-by: Leila Lali <llali@microsoft.com>
2022-05-18 20:11:41 -07:00
Alan Ren
e56e1d931b increase nps survey probability (#19175) (#19177) 2022-04-21 13:57:15 -07:00
Karl Burtram
4bd07a00c0 Update SQL Tools Service in release/1.36 for OE icon revert (#19173)
* Update SQL Tools Service in release/1.36 for OE icon revert

* Remove 'v'
2022-04-21 13:31:51 -07:00
Karl Burtram
4ef3706cf2 Bump version to 1.36.1 for April hotfix. (#19172) 2022-04-21 12:42:47 -07:00
erpett
0704471e6b Updating readme and change log to reflect major changes in 1.36 (#19138) 2022-04-18 15:57:09 -07:00
Vasu Bhog
f5aebda7de Fix SQL Binding when creating new project (#19118) (#19124)
* Fix SQL Binding when creating new project

* Use sql binding templates

* fix openDialog to use select

Co-authored-by: Vasu Bhog <vabhog@microsoft.com>

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
2022-04-15 14:27:37 -07:00
Lewis Sanchez
9400c56c0b Telemetry for Query Execution Plans (#19039) (#19104)
* Adds telemetry around how the properties window is opened

* Adds telemetry around accessing execution plan top operations

* Adds key for viewing top operations

* Adds telemetry around using the open query button and context menu item

* Adds telemetry around execution plan zoom in, out, to fit, and custom

* Adds telemetry around searching for nodes in execution plans

* Reduces telemetry additional properties to 1.

* Code review changes

* Removes unnecessary export
2022-04-14 16:56:09 -07:00
Vasu Bhog
aeaf0ef473 remove extra error message (#19110) (#19113) 2022-04-14 16:55:52 -07:00
Aasim Khan
fd0a8c0ef4 Updating the cache element and not an unlinked reference to it. (#19057) (#19095) 2022-04-14 13:45:59 -07:00
Aasim Khan
a7361002cb Fixing broken action label logic. (#19086) (#19093)
* Fixing broken action label logic.

* Cleaning up some code
2022-04-14 13:44:51 -07:00
Aasim Khan
af42325121 Fixing parallism icon not showing up (#19085) (#19092) 2022-04-14 13:43:27 -07:00
Cory Rivera
85983140cc Port cell hover fix and new Interactive extension metadata additions to release branch. (#19096)
* Only update hover execution state if cell is not active. (#19073)

* Add .NET Interactive extension info to product.json. (#19069)
2022-04-14 13:09:06 -07:00
Barbara Valdez
58d6b22263 fix cell nav (#19082) (#19091) 2022-04-14 11:27:10 -07:00
Udeesha Gautam
09a74aadd5 fix for sensitive text in output message (#19071) (#19088) 2022-04-14 10:46:16 -07:00
Aasim Khan
26eeb78018 Adding caching to execution plan and refactoring code and some other fixes (#18913) (#19061)
* Making ep code modular for easy swithcing in and out

* Changing to innerText

* Fixing renames

* Fixing var name in one file
2022-04-14 10:20:54 -07:00
Vasu Bhog
9d610d17ba Fix connectionSettingName for create azure function with sql binding (#19008) (#19072)
* fix connectionSettingName for create azure function with sql binding

* add sql binding user enters connection string manually

* address comments + fix test

* final comments
2022-04-13 16:31:28 -07:00
Alan Ren
8bed834226 vbump sts (#19053) (#19059) 2022-04-13 13:58:56 -07:00
Benjin Dubishar
835a644e7d updated svg (#19034) (#19048) 2022-04-13 12:51:37 -07:00
Cory Rivera
f0f83d005b Enable cell cancellation for Interactive notebooks (#19005) (#19045)
* Also added a check to prevent multiple Interactive cells from executing simultaneously.
2022-04-13 11:47:07 -07:00
Kim Santiago
89ee54ab8f update schema compare version to 1.13.1 (#19026) (#19029) 2022-04-12 19:00:09 -07:00
Lewis Sanchez
07ed6abfd5 Updates azdataGraph package version to 0.0.20 (#19009) (#19021) 2022-04-12 18:57:39 -07:00
Aasim Khan
b1b0b9e7af Adding link support to infobox. (#18876) (#19016) 2022-04-12 18:57:20 -07:00
Aasim Khan
691d46a0d8 Adding badge icons to execution plan (#19004) (#19015)
* Adding badge icons to executionplan

* Fixing doc comment

* Fixing doc comments

* Making enum value more readable

* Changing default to undefined.

* Fixing some icon names
2022-04-12 18:57:01 -07:00
Alan Ren
b8d47cc97e bring in fixes for a few table designer issues (#19020) (#19024) 2022-04-12 18:56:39 -07:00
Vasu Bhog
5050111a42 suppress create function prompt (#18962) (#19017) 2022-04-12 12:45:24 -07:00
Kim Santiago
0bd8450cf6 hide convert to sdk-style from project context menu (#19002) (#19014)
* hide convert to sdk-style from project context menu

* bump version since 0.16.0 was released in insiders
2022-04-12 10:35:01 -07:00
90 changed files with 4473 additions and 1679 deletions

View File

@@ -1,11 +1,14 @@
# Change Log
## Version 1.35.1
* Release date: March 17, 2022
## Version 1.36.0
* Release date: April 20, 2022
* Release status: General Availability
## Hotfix release
- Fix for [Excel number format #18615](https://github.com/microsoft/azuredatastudio/issues/18615)
- Fix for [Geometry Data Type Returned as Unknown Charset in Results Grid #18630](https://github.com/microsoft/azuredatastudio/issues/18630)
## What's new in this version
- General Availability of the Azure SQL Migration Extension for ADS
- Support for .NET Interactive Notebooks Extension
- New Table Designer Features including support for System Versioned, Graph and Memory Optomized Tables
- Query Plan Viewer Updates includign warning and parallelism icons, the option to disable tooltips and support for opening .sqlplan files
- Improvements in SQL Projects and Schema Compare
| Platform |
| --------------------------------------- |
@@ -17,13 +20,20 @@
| [Linux RPM][linux-rpm] |
| [Linux DEB][linux-deb] |
[win-user]: https://go.microsoft.com/fwlink/?linkid=2187459
[win-system]: https://go.microsoft.com/fwlink/?linkid=2187520
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2187460
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2187461
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2187462
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2187521
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2187522
[win-user]: https://go.microsoft.com/fwlink/?linkid=2193235
[win-system]: https://go.microsoft.com/fwlink/?linkid=2193326
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2193236
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2192971
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2193237
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2193238
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2193327
## Version 1.35.1
* Release date: March 17, 2022
* Release status: General Availability
## Hotfix release
- Fix for [Excel number format #18615](https://github.com/microsoft/azuredatastudio/issues/18615)
- Fix for [Geometry Data Type Returned as Unknown Charset in Results Grid #18630](https://github.com/microsoft/azuredatastudio/issues/18630)
## Version 1.35.0
* Release date: February 24, 2022

View File

@@ -131,10 +131,10 @@ Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [Source EULA](LICENSE.txt).
[win-user]: https://go.microsoft.com/fwlink/?linkid=2187459
[win-system]: https://go.microsoft.com/fwlink/?linkid=2187520
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2187460
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2187461
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2187462
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2187521
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2187522
[win-user]: https://go.microsoft.com/fwlink/?linkid=2193235
[win-system]: https://go.microsoft.com/fwlink/?linkid=2193326
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2193236
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2192971
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2193237
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2193238
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2193327

View File

@@ -81,6 +81,10 @@ export abstract class AzureAuth implements vscode.Disposable {
this.resources = this.resources.concat(this.metadata.settings.azureKustoResource);
}
if (this.metadata.settings.powerBiResource) {
this.resources = this.resources.concat(this.metadata.settings.powerBiResource);
}
this.scopes = [...this.metadata.settings.scopes];
this.scopesString = this.scopes.join(' ');
}

View File

@@ -20,7 +20,8 @@ const enum SettingIds {
ado = 'ado',
ala = 'ala',
storage = 'storage',
kusto = 'kusto'
kusto = 'kusto',
powerbi = 'powerbi'
}
const publicAzureSettings: ProviderSettings = {
@@ -87,6 +88,11 @@ const publicAzureSettings: ProviderSettings = {
endpoint: 'https://kusto.kusto.windows.net',
azureResourceId: AzureResource.AzureKusto,
},
powerBiResource: {
id: SettingIds.powerbi,
endpoint: 'https://analysis.windows.net/powerbi/api',
azureResourceId: AzureResource.PowerBi
},
redirectUri: 'https://vscode-redirect.azurewebsites.net/',
scopes: [
'openid', 'email', 'profile', 'offline_access',
@@ -147,6 +153,11 @@ const usGovAzureSettings: ProviderSettings = {
endpointSuffix: '.core.usgovcloudapi.net',
azureResourceId: AzureResource.AzureStorage
},
powerBiResource: {
id: SettingIds.powerbi,
endpoint: 'https://analysis.windows.net/powerbi/api',
azureResourceId: AzureResource.PowerBi
},
redirectUri: 'https://vscode-redirect.azurewebsites.net/',
scopes: [
'openid', 'email', 'profile', 'offline_access',
@@ -251,6 +262,11 @@ const germanyAzureSettings: ProviderSettings = {
endpointSuffix: '.core.cloudapi.de',
azureResourceId: AzureResource.AzureStorage
},
powerBiResource: {
id: SettingIds.powerbi,
endpoint: 'https://analysis.windows.net/powerbi/api',
azureResourceId: AzureResource.PowerBi
},
redirectUri: 'https://vscode-redirect.azurewebsites.net/',
scopes: [
'openid', 'email', 'profile', 'offline_access',
@@ -310,6 +326,11 @@ const chinaAzureSettings: ProviderSettings = {
endpointSuffix: '.core.chinacloudapi.cn',
azureResourceId: AzureResource.AzureStorage
},
powerBiResource: {
id: SettingIds.powerbi,
endpoint: 'https://analysis.windows.net/powerbi/api',
azureResourceId: AzureResource.PowerBi
},
redirectUri: 'https://vscode-redirect.azurewebsites.net/',
scopes: [
'openid', 'email', 'profile', 'offline_access',

View File

@@ -129,10 +129,15 @@ declare module 'azurecore' {
azureLogAnalyticsResource?: Resource;
/**
* Information that describes the Azure Storage resourceI
* Information that describes the Azure Storage resource
*/
azureStorageResource?: Resource;
/**
* Information that describes the Power BI resource
*/
powerBiResource?: Resource;
/**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "3.0.0-release.229",
"version": "3.0.0-release.238",
"downloadFileNames": {
"Windows_86": "win-x86-net6.0.zip",
"Windows_64": "win-x64-net6.0.zip",

View File

@@ -2,7 +2,7 @@
"name": "schema-compare",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.13.0",
"version": "1.13.1",
"publisher": "Microsoft",
"preview": false,
"engines": {

View File

@@ -51,7 +51,7 @@
},
{
"command": "sqlBindings.createAzureFunction",
"when": "view == objectExplorer && viewItem == Table && !azdataAvailable",
"when": "!azdataAvailable",
"group": "zAzure_Function@1"
}
],

View File

@@ -44,7 +44,6 @@ export async function getLocalSettingsJson(localSettingsPath: string): Promise<I
throw new Error(utils.formatString(constants.failedToParse(error.message), constants.azureFunctionLocalSettingsFileName, error.message));
}
}
return {
IsEncrypted: false // Include this by default otherwise the func cli assumes settings are encrypted and fails to run
};
@@ -75,8 +74,7 @@ export async function setLocalAppSetting(projectFolder: string, key: string, val
}
settings.Values[key] = value;
void fs.promises.writeFile(localSettingsPath, JSON.stringify(settings, undefined, 2));
await fs.promises.writeFile(localSettingsPath, JSON.stringify(settings, undefined, 2));
return true;
}
@@ -323,8 +321,10 @@ export async function promptForObjectName(bindingType: BindingType): Promise<str
/**
* Prompts the user to enter connection setting and updates it from AF project
* @param projectUri Azure Function project uri
* @param connectionInfo connection info from the user to update the connection string
* @returns connection string setting name to be used for the createFunction API
*/
export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined): Promise<string | undefined> {
export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined, connectionInfo?: IConnectionInfo): Promise<string | undefined> {
let connectionStringSettingName: string | undefined;
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
@@ -390,6 +390,7 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.
'WEBSITE_VNET_ROUTE_ALL'
];
// setup connetion string setting quickpick
let connectionStringSettings: (vscode.QuickPickItem)[] = [];
if (settings?.Values) {
connectionStringSettings = Object.keys(settings.Values).filter(setting => !knownSettings.includes(setting)).map(setting => { return { label: setting }; });
@@ -426,104 +427,65 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.
// show the connection string methods (user input and connection profile options)
const listOfConnectionStringMethods = [constants.connectionProfile, constants.userConnectionString];
let selectedConnectionStringMethod: string | undefined;
let connectionString: string | undefined = '';
while (true) {
const selectedConnectionStringMethod = await vscode.window.showQuickPick(listOfConnectionStringMethods, {
canPickMany: false,
title: constants.selectConnectionString,
ignoreFocusOut: true
});
if (!selectedConnectionStringMethod) {
// User cancelled
return;
}
try {
const projectFolder: string = path.dirname(projectUri.fsPath);
const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName);
let connectionString: string = '';
let includePassword: string | undefined;
let connectionInfo: IConnectionInfo | undefined;
let connectionDetails: ConnectionDetails;
if (selectedConnectionStringMethod === constants.userConnectionString) {
// User chooses to enter connection string manually
connectionString = await vscode.window.showInputBox(
{
title: constants.enterConnectionString,
ignoreFocusOut: true,
value: 'Server=localhost;Initial Catalog={db_name};User ID=sa;Password={your_password};Persist Security Info=False',
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
}
) ?? '';
} else {
// Let user choose from existing connections to create connection string from
connectionInfo = await vscodeMssqlApi.promptForConnection(true);
if (!connectionInfo) {
// User cancelled return to selectedConnectionStringMethod prompt
continue;
}
connectionDetails = { options: connectionInfo };
try {
// Prompt to include password in connection string if authentication type is SqlLogin and connection has password saved
if (connectionInfo.authenticationType === 'SqlLogin' && connectionInfo.password) {
includePassword = await vscode.window.showQuickPick([constants.yesString, constants.noString], {
title: constants.includePassword,
canPickMany: false,
ignoreFocusOut: true
});
if (includePassword === constants.yesString) {
// set connection string to include password
connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, true, false);
}
// show the connection string methods (user input and connection profile options)
selectedConnectionStringMethod = await vscode.window.showQuickPick(listOfConnectionStringMethods, {
canPickMany: false,
title: constants.selectConnectionString,
ignoreFocusOut: true
});
if (!selectedConnectionStringMethod) {
// User cancelled
return;
}
// set connection string to not include the password if connection info does not include password, or user chooses to not include password, or authentication type is not sql login
if (includePassword !== constants.yesString) {
connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false);
}
} catch (e) {
// failed to get connection string for selected connection and will go back to prompt for connection string methods
console.warn(e);
void vscode.window.showErrorMessage(constants.failedToGetConnectionString);
continue;
}
}
if (connectionString) {
try {
const projectFolder: string = path.dirname(projectUri.fsPath);
const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName);
let userPassword: string | undefined;
// Ask user to enter password if auth type is sql login and password is not saved
if (connectionInfo?.authenticationType === 'SqlLogin' && !connectionInfo?.password) {
userPassword = await vscode.window.showInputBox({
prompt: constants.enterPasswordPrompt,
placeHolder: constants.enterPasswordManually,
ignoreFocusOut: true,
password: true,
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
});
if (userPassword) {
// if user enters password replace password placeholder with user entered password
connectionString = connectionString.replace(constants.passwordPlaceholder, userPassword);
}
}
if (includePassword !== constants.yesString && !userPassword && connectionInfo?.authenticationType === 'SqlLogin') {
// if user does not want to include password or user does not enter password, show warning message that they will have to enter it manually later in local.settings.json
void vscode.window.showWarningMessage(constants.userPasswordLater, constants.openFile, constants.closeButton).then(async (result) => {
if (result === constants.openFile) {
// open local.settings.json file
void vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(localSettingsPath));
if (selectedConnectionStringMethod === constants.userConnectionString) {
// User chooses to enter connection string manually
connectionString = await vscode.window.showInputBox(
{
title: constants.enterConnectionString,
ignoreFocusOut: true,
value: 'Server=localhost;Initial Catalog={db_name};User ID=sa;Password={your_password};Persist Security Info=False',
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
}
});
}
const success = await setLocalAppSetting(projectFolder, newConnectionStringSettingName, connectionString);
if (success) {
// exit both loops and insert binding
connectionStringSettingName = newConnectionStringSettingName;
break;
) ?? '';
} else {
void vscode.window.showErrorMessage(constants.selectConnectionError());
// Let user choose from existing connections to create connection string from
connectionInfo = await vscodeMssqlApi.promptForConnection(true);
}
} catch (e) {
// display error message and show select setting quickpick again
void vscode.window.showErrorMessage(constants.selectConnectionError(e));
continue;
}
if (selectedConnectionStringMethod !== constants.userConnectionString) {
if (!connectionInfo) {
// User cancelled return to selectedConnectionStringMethod prompt
continue;
}
// get the connection string including prompts for password if needed
connectionString = await promptConnectionStringPasswordAndUpdateConnectionString(connectionInfo, localSettingsPath);
}
if (!connectionString) {
// user cancelled the prompts
return;
}
const success = await setLocalAppSetting(projectFolder, newConnectionStringSettingName, connectionString);
if (success) {
// exit both loops and insert binding
connectionStringSettingName = newConnectionStringSettingName;
break;
} else {
void vscode.window.showErrorMessage(constants.selectConnectionError());
}
} catch (e) {
// display error message and show select setting quickpick again
void vscode.window.showErrorMessage(constants.selectConnectionError(e));
continue;
}
}
} else {
@@ -545,3 +507,70 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.
}
return connectionStringSettingName;
}
/**
* Prompts the user to include password in the connection string and updates the connection string based on user input
* @param connectionInfo connection info from the connection profile user selected
* @param localSettingsPath path to the local.settings.json file
* @returns the updated connection string based on password prompts
*/
export async function promptConnectionStringPasswordAndUpdateConnectionString(connectionInfo: IConnectionInfo, localSettingsPath: string): Promise<string | undefined> {
let includePassword: string | undefined;
let connectionString: string = '';
let connectionDetails: ConnectionDetails;
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
connectionDetails = { options: connectionInfo };
try {
// Prompt to include password in connection string if authentication type is SqlLogin and connection has password saved
if (connectionInfo.authenticationType === 'SqlLogin' && connectionInfo.password) {
includePassword = await vscode.window.showQuickPick([constants.yesString, constants.noString], {
title: constants.includePassword,
canPickMany: false,
ignoreFocusOut: true
});
if (includePassword === constants.yesString) {
// set connection string to include password
connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, true, false);
}
}
// set connection string to not include the password if connection info does not include password, or user chooses to not include password, or authentication type is not sql login
let userPassword: string | undefined;
if (includePassword !== constants.yesString) {
connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false);
// Ask user to enter password if auth type is sql login and password is not saved
if (connectionInfo.authenticationType === 'SqlLogin' && connectionInfo.password) {
userPassword = await vscode.window.showInputBox({
prompt: constants.enterPasswordPrompt,
placeHolder: constants.enterPasswordManually,
ignoreFocusOut: true,
password: true,
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
});
if (userPassword) {
// if user enters password replace password placeholder with user entered password
connectionString = connectionString.replace(constants.passwordPlaceholder, userPassword);
}
}
}
if (includePassword !== constants.yesString && !userPassword && connectionInfo?.authenticationType === 'SqlLogin') {
// if user does not want to include password or user does not enter password, show warning message that they will have to enter it manually later in local.settings.json
void vscode.window.showWarningMessage(constants.userPasswordLater, constants.openFile, constants.closeButton).then(async (result) => {
if (result === constants.openFile) {
// open local.settings.json file
void vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(localSettingsPath));
}
});
}
return connectionString;
} catch (e) {
// failed to get connection string for selected connection and will go back to prompt for connection string methods
console.warn(e);
void vscode.window.showErrorMessage(constants.failedToGetConnectionString);
return undefined;
}
}

View File

@@ -34,6 +34,8 @@ export const azureFunctionsExtensionNotInstalled = localize('azureFunctionsExten
export const azureFunctionsProjectMustBeOpened = localize('azureFunctionsProjectMustBeOpened', 'A C# Azure Functions project must be present in order to create a new Azure Function for this table.');
export const needConnection = localize('needConnection', 'A connection is required to use Azure Function with SQL Binding');
export const selectDatabase = localize('selectDatabase', 'Select Database');
export const browseEllipsisWithIcon = `$(folder) ${localize('browseEllipsis', "Browse...")}`;
export const selectButton = localize('selectButton', 'Select');
// Insert SQL binding
export const hostFileName = 'host.json';

View File

@@ -78,7 +78,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
}
} catch (e) {
void vscode.window.showErrorMessage(utils.getErrorMessage(e));
propertyBag.quickPickStep = quickPickStep;
exitReason = 'error';
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, utils.getErrorType(e))
@@ -114,8 +113,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
objectName = utils.generateQuotedFullName(node.metadata.schema, node.metadata.name);
}
const connectionDetails = vscodeMssqlApi.createConnectionDetails(connectionInfo);
const connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false);
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding)
.withConnectionInfo(connectionInfo).send();
@@ -151,10 +148,40 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
// start the create azure function project flow
try {
// First prompt user for project location. We need to do this ourselves due to an issue
// in the AF extension : https://github.com/microsoft/vscode-azurefunctions/issues/3115
const browseProjectLocation = await vscode.window.showQuickPick(
[constants.browseEllipsisWithIcon],
{ title: constants.selectAzureFunctionProjFolder, ignoreFocusOut: true });
if (!browseProjectLocation) {
// User cancelled
return undefined;
}
const projectFolders = (await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: constants.selectButton
}));
if (!projectFolders) {
// User cancelled
return;
}
const templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID;
// because of an AF extension API issue, we have to get the newly created file by adding a watcher
// issue: https://github.com/microsoft/vscode-azurefunctions/issues/3052
newHostProjectFile = await azureFunctionsUtils.waitForNewHostFile();
await azureFunctionApi.createFunction({ language: 'C#', targetFramework: 'netcoreapp3.1' });
newHostProjectFile = azureFunctionsUtils.waitForNewHostFile();
await azureFunctionApi.createFunction({
language: 'C#',
targetFramework: 'netcoreapp3.1',
templateId: templateId,
suppressCreateProjectPrompt: true,
folderPath: projectFolders[0].fsPath,
functionSettings: {
...(selectedBindingType === BindingType.input && { object: objectName }),
...(selectedBindingType === BindingType.output && { table: objectName })
},
});
const timeoutForHostFile = utils.timeoutPromise(constants.timeoutProjectError);
hostFile = await Promise.race([newHostProjectFile.filePromise, timeoutForHostFile]);
if (hostFile) {
@@ -220,13 +247,18 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
// issue https://github.com/microsoft/azuredatastudio/issues/18780
await azureFunctionsUtils.setLocalAppSetting(path.dirname(projectFile), constants.azureWebJobsStorageSetting, constants.azureWebJobsStoragePlaceholder);
// prompt for connection string setting name and set connection string in local.settings.json
quickPickStep = 'getConnectionStringSettingName';
let connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo);
// create C# Azure Function with SQL Binding
await azureFunctionApi.createFunction({
language: 'C#',
templateId: templateId,
functionName: functionName,
targetFramework: 'netcoreapp3.1',
functionSettings: {
connectionStringSetting: constants.sqlConnectionStringSetting,
connectionStringSetting: connectionStringSettingName,
...(selectedBindingType === BindingType.input && { object: objectName }),
...(selectedBindingType === BindingType.output && { table: objectName })
},
@@ -265,7 +297,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
.withAdditionalProperties(propertyBag).send();
newFunctionFileObject.watcherDisposable.dispose();
}
await azureFunctionsUtils.addConnectionStringToConfig(connectionString, projectFile);
} else {
TelemetryReporter.sendErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.finishCreateAzureFunctionWithSqlBinding);
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import * as fs from 'fs';
import * as should from 'should';
import * as sinon from 'sinon';
import * as constants from '../../common/constants';
import * as azureFunctionsUtils from '../../common/azureFunctionsUtils';
import { EOL } from 'os';
let rootFolderPath = 'test';
let localSettingsPath: string = `${rootFolderPath}/local.settings.json`;
let projectFilePath: string = `${rootFolderPath}//projectFilePath.csproj`;
describe('Tests to verify Azure Functions Utils functions', function (): void {
it('Should correctly parse local.settings.json', async () => {
sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true);
sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns(
`{"IsEncrypted": false,
"Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}`
);
let settings = await azureFunctionsUtils.getLocalSettingsJson(localSettingsPath);
should(settings.IsEncrypted).equals(false);
should(Object.keys(settings.Values!).length).equals(3);
});
it('setLocalAppSetting can update settings.json with new setting value', async () => {
sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true);
sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns(
`{"IsEncrypted": false,
"Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}`
);
let writeFileStub = sinon.stub(fs.promises, 'writeFile');
await azureFunctionsUtils.setLocalAppSetting(path.dirname(localSettingsPath), 'test4', 'test4');
should(writeFileStub.calledWithExactly(localSettingsPath, `{${EOL} "IsEncrypted": false,${EOL} "Values": {${EOL} "test1": "test1",${EOL} "test2": "test2",${EOL} "test3": "test3",${EOL} "test4": "test4"${EOL} }${EOL}}`)).equals(true);
});
it('Should not overwrite setting if value already exists in local.settings.json', async () => {
sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true);
sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns(
`{"IsEncrypted": false,
"Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}`
);
let warningMsg = constants.settingAlreadyExists('test1');
const spy = sinon.stub(vscode.window, 'showWarningMessage').resolves({ title: constants.settingAlreadyExists('test1') });
await azureFunctionsUtils.setLocalAppSetting(path.dirname(localSettingsPath), 'test1', 'newValue');
should(spy.calledOnce).be.true('showWarningMessage should have been called exactly once');
should(spy.calledWith(warningMsg)).be.true(`showWarningMessage not called with expected message '${warningMsg}' Actual '${spy.getCall(0).args[0]}'`);
});
it('Should get settings file given project file', async () => {
const settingsFile = await azureFunctionsUtils.getSettingsFile(projectFilePath);
should(settingsFile).equals(localSettingsPath);
});
it('Should add connection string to local.settings.json', async () => {
sinon.stub(fs, 'existsSync').withArgs(localSettingsPath).returns(true);
sinon.stub(fs, 'readFileSync').withArgs(localSettingsPath).returns(
`{"IsEncrypted": false,
"Values": {"test1": "test1", "test2": "test2", "test3":"test3"}}`
);
const connectionString = 'testConnectionString';
let writeFileStub = sinon.stub(fs.promises, 'writeFile');
await azureFunctionsUtils.addConnectionStringToConfig(connectionString, projectFilePath);
should(writeFileStub.calledWithExactly(localSettingsPath, `{${EOL} "IsEncrypted": false,${EOL} "Values": {${EOL} "test1": "test1",${EOL} "test2": "test2",${EOL} "test3": "test3",${EOL} "SqlConnectionString": "testConnectionString"${EOL} }${EOL}}`)).equals(true);
});
afterEach(async function (): Promise<void> {
sinon.restore();
});
});

View File

@@ -117,8 +117,8 @@ describe('Add SQL Binding quick pick', () => {
await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri'));
// should go back to the select connection string methods
should(quickpickStub.callCount === 5);
should(quickpickStub.getCall(4).args).deepEqual([
should(quickpickStub.callCount === 4);
should(quickpickStub.getCall(3).args).deepEqual([
[constants.connectionProfile, constants.userConnectionString],
{
canPickMany: false,

View File

@@ -23,11 +23,13 @@ export class MockVscodeMssqlIExtension implements vscodeMssql.IExtension {
dacFx: vscodeMssql.IDacFxService;
schemaCompare: vscodeMssql.ISchemaCompareService;
azureAccountService: vscodeMssql.IAzureAccountService;
azureResourceService: vscodeMssql.IAzureResourceService;
constructor() {
this.dacFx = TypeMoq.Mock.ofType<vscodeMssql.IDacFxService>().object;
this.schemaCompare = TypeMoq.Mock.ofType<vscodeMssql.ISchemaCompareService>().object;
this.azureAccountService = TypeMoq.Mock.ofType<vscodeMssql.IAzureAccountService>().object;
this.azureResourceService = TypeMoq.Mock.ofType<vscodeMssql.IAzureResourceService>().object;
}
promptForFirewallRule(_: string, __: vscodeMssql.IConnectionInfo): Promise<boolean> {

View File

@@ -1,6 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="28" width="28" version="1.1" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="#0072C6" d="M11.423,44.326l23.623-4.156L22.894,25.748l6.328-17.346L50,44.33L11.423,44.326z M27.566,5.67L11.469,40.109v-0.034H0l12.717-21.975L27.566,5.67z" />
</g>
<svg width="150" height="150" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="e399c19f-b68f-429d-b176-18c2117ff73c" x1="-1032.172" x2="-1059.213" y1="145.312" y2="65.426" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#114a8b"/>
<stop offset="1" stop-color="#0669bc"/>
</linearGradient>
<linearGradient id="ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15" x1="-1023.725" x2="-1029.98" y1="108.083" y2="105.968" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-opacity=".3"/>
<stop offset=".071" stop-opacity=".2"/>
<stop offset=".321" stop-opacity=".1"/>
<stop offset=".623" stop-opacity=".05"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="a7fee970-a784-4bb1-af8d-63d18e5f7db9" x1="-1027.165" x2="-997.482" y1="147.642" y2="68.561" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3ccbf4"/>
<stop offset="1" stop-color="#2892df"/>
</linearGradient>
</defs>
<path fill="url(#e399c19f-b68f-429d-b176-18c2117ff73c)" d="M33.338 6.544h26.038l-27.03 80.087a4.152 4.152 0 0 1-3.933 2.824H8.149a4.145 4.145 0 0 1-3.928-5.47L29.404 9.368a4.152 4.152 0 0 1 3.934-2.825z"/>
<path fill="#0078d4" d="M71.175 60.261h-41.29a1.911 1.911 0 0 0-1.305 3.309l26.532 24.764a4.171 4.171 0 0 0 2.846 1.121h23.38z"/>
<path fill="url(#ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15)" d="M33.338 6.544a4.118 4.118 0 0 0-3.943 2.879L4.252 83.917a4.14 4.14 0 0 0 3.908 5.538h20.787a4.443 4.443 0 0 0 3.41-2.9l5.014-14.777 17.91 16.705a4.237 4.237 0 0 0 2.666.972H81.24L71.024 60.261l-29.781.007L59.47 6.544z"/>
<path fill="url(#a7fee970-a784-4bb1-af8d-63d18e5f7db9)" d="M66.595 9.364a4.145 4.145 0 0 0-3.928-2.82H33.648a4.146 4.146 0 0 1 3.928 2.82l25.184 74.62a4.146 4.146 0 0 1-3.928 5.472h29.02a4.146 4.146 0 0 0 3.927-5.472z"/>
</svg>

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -2,7 +2,7 @@
"name": "sql-database-projects",
"displayName": "SQL Database Projects",
"description": "Enables users to develop and publish database schemas for MSSQL Databases",
"version": "0.16.0",
"version": "0.16.2",
"publisher": "Microsoft",
"preview": true,
"engines": {
@@ -398,11 +398,6 @@
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/ || viewItem == databaseProject.itemType.reference",
"group": "9_dbProjectsLast@2"
},
{
"command": "sqlDatabaseProjects.convertToSdkStyleProject",
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.legacyProject",
"group": "9_dbProjectsLast@5"
},
{
"command": "sqlDatabaseProjects.changeTargetPlatform",
"when": "view == dataworkspace.views.main && viewItem =~ /^(databaseProject.itemType.project|databaseProject.itemType.legacyProject)$/",
@@ -461,6 +456,7 @@
}
},
"dependencies": {
"axios": "^0.27.2",
"@microsoft/ads-extension-telemetry": "^1.1.5",
"fast-glob": "^3.2.7",
"fs-extra": "^5.0.0",
@@ -469,7 +465,7 @@
"vscode-languageclient": "^5.3.0-next.1",
"vscode-nls": "^4.1.2",
"which": "^2.0.2",
"xml-formatter": "^2.1.0",
"xml-formatter": "2.1.0",
"xmldom": "^0.6.0"
},
"devDependencies": {

View File

@@ -149,9 +149,22 @@ export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not
// Deploy
export const SqlServerName = 'SQL server';
export const AzureSqlServerName = 'Azure SQL server';
export const SqlServerDockerImageName = 'Microsoft SQL Server';
export const AzureSqlDbFullDockerImageName = 'Azure SQL Database emulator Full';
export const AzureSqlDbLiteDockerImageName = 'Azure SQL Database emulator Lite';
export const AzureSqlLogicalServerName = 'Azure SQL logical server';
export const selectPublishOption = localize('selectPublishOption', "Select where to publish the project to");
export const defaultQuickPickItem = localize('defaultQuickPickItem', "Default - image defined as default in the container registry");
export function dockerImagesPlaceHolder(name: string) { return localize('dockerImagesPlaceHolder', 'Use {0} on local arm64/Apple Silicon', name); }
export function publishToExistingServer(name: string) { return localize('publishToExistingServer', "Publish to an existing {0}", name); }
export function publishToDockerContainer(name: string) { return localize('publishToDockerContainer', "Publish to new {0} local development container", name); }
export const publishToAzureEmulator = localize('publishToAzureEmulator', "Publish to new Azure SQL Database emulator");
export const publishToNewAzureServer = localize('publishToNewAzureServer', "Publish to new Azure SQL logical server");
export const azureServerName = localize('azureServerName', "Azure SQL server name");
export const azureSubscription = localize('azureSubscription', "Azure subscription");
export const resourceGroup = localize('resourceGroup', "Resource group");
export const azureLocation = localize('location', "Location");
export const azureAccounts = localize('azureAccounts', "Azure accounts");
export function enterPortNumber(name: string) { return localize('enterPortNumber', "Enter {0} port number or press enter to use the default value", name); }
export function serverPortNumber(name: string) { return localize('serverPortNumber', "{0} port number", name); }
export function serverPassword(name: string) { return localize('serverPassword', "{0} admin password", name); }
@@ -160,15 +173,18 @@ export function baseDockerImage(name: string) { return localize('baseDockerImage
export const publishTo = localize('publishTo', "Publish Target");
export const enterConnectionStringEnvName = localize('enterConnectionStringEnvName', "Enter connection string environment variable name");
export const enterConnectionStringTemplate = localize('enterConnectionStringTemplate', "Enter connection string template");
export function enterUser(name: string) { return localize('enterUser', "Enter {0} admin user name", name); }
export function enterPassword(name: string) { return localize('enterPassword', "Enter {0} admin password", name); }
export function confirmPassword(name: string) { return localize('confirmPassword', "Confirm {0} admin password", name); }
export function selectBaseImage(name: string) { return localize('selectBaseImage', "Select the base {0} docker image", name); }
export function selectImageTag(name: string) { return localize('selectImageTag', "Select the image tag or press enter to use the default value", name); }
export function invalidSQLPasswordMessage(name: string) { return localize('invalidSQLPassword', "{0} password doesn't meet the password complexity requirement. For more information see https://docs.microsoft.com/sql/relational-databases/security/password-policy", name); }
export function passwordNotMatch(name: string) { return localize('passwordNotMatch', "{0} password doesn't match the confirmation password", name); }
export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be number");
export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty");
export const dockerImageLabelPrefix = 'source=sqldbproject';
export const dockerImageNamePrefix = 'sqldbproject';
export const dockerImageDefaultTag = 'latest';
// Publish to Container
export const eulaAgreementTemplate = localize({ key: 'eulaAgreementTemplate', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}.");
@@ -205,12 +221,16 @@ export const runningDockerMessage = localize('runningDockerMessage', "Running th
export function dockerNotRunningError(error: string) { return localize('dockerNotRunningError', "Failed to verify docker. Please make sure docker is installed and running. Error: '{0}'", error || ''); }
export const dockerContainerNotRunningErrorMessage = localize('dockerContainerNotRunningErrorMessage', "Docker container is not running");
export const dockerContainerFailedToRunErrorMessage = localize('dockerContainerFailedToRunErrorMessage', "Failed to run the docker container");
export const connectingToSqlServerMessage = localize('connectingToSqlServerOnDockerMessage', "Connecting to SQL Server");
export const connectingToSqlServerMessage = localize('connectingToSqlServerMessage', "Connecting to SQL Server");
export const serverCreated = localize('serverCreated', "Server created");
export const deployProjectFailedMessage = localize('deployProjectFailedMessage', "Failed to open a connection to the deployed database'");
export const containerAlreadyExistForProject = localize('containerAlreadyExistForProject', "Containers already exist for this project. Do you want to delete them before deploying a new one?");
export const checkoutOutputMessage = localize('checkoutOutputMessage', "Check output pane for more details");
export function creatingAzureSqlServer(name: string): string { return localize('creatingAzureSqlServer', "Creating Azure SQL Server '{0}' ...", name); }
export function azureSqlServerCreated(name: string): string { return localize('azureSqlServerCreated', "Azure SQL Server '{0}' created", name); }
export function taskFailedError(taskName: string, err: string): string { return localize('taskFailedError.error', "Failed to complete task '{0}'. Error: {1}", taskName, err); }
export function publishToContainerFailed(errorMessage: string) { return localize('publishToContainerFailed', "Failed to publish to container. {0}", errorMessage); }
export function publishToNewAzureServerFailed(errorMessage: string) { return localize('publishToNewAzureServerFailed', "Failed to publish to new Azure SQL server. {0}", errorMessage); }
export function deployAppSettingUpdateFailed(appSetting: string) { return localize('deployAppSettingUpdateFailed', "Failed to update app setting '{0}'", appSetting); }
export function deployAppSettingUpdating(appSetting: string) { return localize('deployAppSettingUpdating', "Updating app setting: '{0}'", appSetting); }
export function connectionFailedError(error: string) { return localize('connectionFailedError', "Connection failed error: '{0}'", error); }
@@ -220,7 +240,7 @@ export function retryWaitMessage(numberOfSeconds: number, name: string) { return
export function retryRunMessage(attemptNumber: number, numberOfAttempts: number, name: string) { return localize('retryRunMessage', "Running operation '{2}' Attempt {0} of {1}", attemptNumber, numberOfAttempts, name); }
export function retrySucceedMessage(name: string, result: string) { return localize('retrySucceedMessage', "Operation '{0}' completed successfully. Result: {1}", name, result); }
export function retryFailedMessage(name: string, result: string, error: string) { return localize('retryFailedMessage', "Operation '{0}' failed. Re-trying... Current Result: {1}. Error: '{2}'", name, result, error); }
export function retryMessage(name: string, error: string) { return localize('retryMessage', "Operation '{0}' failed. Re-trying... Error: '{1}'", name, error || ''); }
export function retryMessage(name: string, error: string) { return localize('retryMessage', "Operation '{0}' failed. Re-trying... Error: '{1}' ", name, error); }
// Add Database Reference dialog strings
@@ -269,6 +289,8 @@ export const WorkspaceFileExtension = '.code-workspace';
export const browseEllipsisWithIcon = `$(folder) ${localize('browseEllipsis', "Browse...")}`;
export const selectProjectLocation = localize('selectProjectLocation', "Select project location");
export const sdkStyleProject = localize('sdkStyleProject', 'SDK-style project (Preview)');
export const YesRecommended = localize('yesRecommended', "Yes (Recommended)");
export const SdkLearnMorePlaceholder = localize('sdkLearnMorePlaceholder', "Click \"Learn More\" button for more information about SDK-style projects");
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); };
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };
@@ -480,6 +502,8 @@ export const integratedAuth = 'Integrated';
export const azureMfaAuth = 'AzureMFA';
export const sqlAuth = 'SqlAuth';
export const azureAddAccount = localize('azureAddAccount', "Add an Account...");
// Tree item types
export enum DatabaseProjectItemType {
project = 'databaseProject.itemType.project',
@@ -548,4 +572,5 @@ export function getTargetPlatformFromVersion(version: string): string {
export enum PublishTargetType {
existingServer = 'existingServer',
docker = 'docker',
newAzureServer = 'newAzureServer'
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import axios, { AxiosRequestConfig } from 'axios';
/**
* Class includes method for making http request
*/
export class HttpClient {
private static cache: Map<string, any> = new Map();
/**
* Makes http GET request to the given url. If useCache is set to true, returns the result from cache if exists
* @param url url to make http GET request against
* @param useCache if true and result is already cached the cached value will be returned
* @returns result of http GET request
*/
public static async getRequest(url: string, useCache = false): Promise<any> {
if (useCache) {
if (HttpClient.cache.has(url)) {
return HttpClient.cache.get(url);
}
}
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json'
},
validateStatus: () => true // Never throw
};
const response = await axios.get(url, config);
if (response.status !== 200) {
let errorMessage: string[] = [];
errorMessage.push(response.status.toString());
errorMessage.push(response.statusText);
if (response.data?.error) {
errorMessage.push(`${response.data?.error?.code} : ${response.data?.error?.message}`);
}
throw new Error(errorMessage.join(os.EOL));
}
if (useCache) {
HttpClient.cache.set(url, response.data);
}
return response.data;
}
}

View File

@@ -38,5 +38,7 @@ export enum TelemetryActions {
finishAddSqlBinding = 'finishAddSqlBinding',
createProjectFromDatabase = 'createProjectFromDatabase',
updateProjectFromDatabase = 'updateProjectFromDatabase',
publishToContainer = 'publishToContainer'
publishToContainer = 'publishToContainer',
publishToNewAzureServer = 'publishToNewAzureServer',
generateProjectFromOpenApiSpec = 'generateProjectFromOpenApiSpec'
}

View File

@@ -307,6 +307,18 @@ export async function getVscodeMssqlApi(): Promise<vscodeMssql.IExtension> {
return ext.activate();
}
export type AzureResourceServiceFactory = () => Promise<vscodeMssql.IAzureResourceService>;
export async function defaultAzureResourceServiceFactory(): Promise<vscodeMssql.IAzureResourceService> {
const vscodeMssqlApi = await getVscodeMssqlApi();
return vscodeMssqlApi.azureResourceService;
}
export type AzureAccountServiceFactory = () => Promise<vscodeMssql.IAzureAccountService>;
export async function defaultAzureAccountServiceFactory(): Promise<vscodeMssql.IAzureAccountService> {
const vscodeMssqlApi = await getVscodeMssqlApi();
return vscodeMssqlApi.azureAccountService;
}
/*
* Returns the default deployment options from DacFx, filtered to appropriate options for the given project.
*/
@@ -441,7 +453,7 @@ export async function retry<T>(
}
} catch (err) {
outputChannel.appendLine(constants.retryMessage(name, err));
outputChannel.appendLine(constants.retryMessage(name, getErrorMessage(err)));
}
}
@@ -599,3 +611,47 @@ export function getFoldersAlongPath(startFolder: string, endFolder: string): str
return folders;
}
/**
* Returns SQL version number from docker image name which is in the beginning of the image name
* @param imageName docker image name
* @returns SQL server version
*/
export function findSqlVersionInImageName(imageName: string): number | undefined {
// Regex to find the version in the beginning of the image name
// e.g. 2017-CU16-ubuntu, 2019-latest
const regex = new RegExp('^([0-9]+)[-].+$');
if (regex.test(imageName)) {
const finds = regex.exec(imageName);
if (finds) {
// 0 is the full match and 1 is the number with pattern inside the first ()
return +finds[1];
}
}
return undefined;
}
/**
* Returns SQL version number from target platform name
* @param targetPlatform target platform
* @returns SQL server version
*/
export function findSqlVersionInTargetPlatform(targetPlatform: string): number | undefined {
// Regex to find the version in target platform
// e.g. SQL Server 2019
const regex = new RegExp('([0-9]+)$');
if (regex.test(targetPlatform)) {
const finds = regex.exec(targetPlatform);
if (finds) {
// 0 is the full match and 1 is the number with pattern inside the first ()
return +finds[1];
}
}
return undefined;
}

View File

@@ -38,16 +38,17 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t
import { IconPathHelper } from '../common/iconHelper';
import { DashboardData, PublishData, Status } from '../models/dashboardData/dashboardData';
import { getPublishDatabaseSettings, launchPublishTargetOption } from '../dialogs/publishDatabaseQuickpick';
import { launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick';
import { launchCreateAzureServerQuickPick, launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick';
import { DeployService } from '../models/deploy/deployService';
import { GenerateProjectFromOpenApiSpecOptions, SqlTargetPlatform } from 'sqldbproj';
import { AutorestHelper } from '../tools/autorestHelper';
import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick';
import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick';
import { IDeployProfile } from '../models/deploy/deployProfile';
import { ILocalDbDeployProfile, ISqlDbDeployProfile } from '../models/deploy/deployProfile';
import { EntryType, FileProjectEntry, IDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/projectEntry';
import { UpdateProjectAction, UpdateProjectDataModel } from '../models/api/updateProject';
import { targetPlatformToAssets } from '../projectProvider/projectAssets';
import { AzureSqlClient } from '../models/deploy/azureSqlClient';
const maxTableLength = 10;
@@ -75,6 +76,7 @@ export class ProjectsController {
private buildInfo: DashboardData[] = [];
private publishInfo: PublishData[] = [];
private deployService: DeployService;
private azureSqlClient: AzureSqlClient;
private autorestHelper: AutorestHelper;
projFileWatchers = new Map<string, vscode.FileSystemWatcher>();
@@ -82,7 +84,8 @@ export class ProjectsController {
constructor(private _outputChannel: vscode.OutputChannel) {
this.netCoreTool = new NetCoreTool(this._outputChannel);
this.buildHelper = new BuildHelper();
this.deployService = new DeployService(this._outputChannel);
this.azureSqlClient = new AzureSqlClient();
this.deployService = new DeployService(this.azureSqlClient, this._outputChannel);
this.autorestHelper = new AutorestHelper(this._outputChannel);
}
@@ -285,21 +288,58 @@ export class ProjectsController {
}
}
/**
* Publishes a project to a new Azure server
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project or the Project itself
* @param deployProfile deploy profile
*/
public async publishToNewAzureServer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: ISqlDbDeployProfile): Promise<void> {
try {
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishToNewAzureServer);
const project: Project = this.getProjectFromContext(context);
if (deployProfile?.deploySettings && deployProfile?.sqlDbSetting) {
void utils.showInfoMessageWithOutputChannel(constants.creatingAzureSqlServer(deployProfile?.sqlDbSetting?.serverName), this._outputChannel);
const connectionUri = await this.deployService.createNewAzureSqlServer(deployProfile);
if (connectionUri) {
deployProfile.deploySettings.connectionUri = connectionUri;
const publishResult = await this.publishOrScriptProject(project, deployProfile.deploySettings, true);
if (publishResult && publishResult.success) {
if (deployProfile.sqlDbSetting) {
// Connecting to the deployed db to add the profile to connection viewlet
await this.deployService.getConnection(deployProfile.sqlDbSetting, true, deployProfile.sqlDbSetting.dbName);
}
void vscode.window.showInformationMessage(constants.publishProjectSucceed);
} else {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, publishResult?.errorMessage || '', this._outputChannel);
}
} else {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, constants.deployProjectFailedMessage, this._outputChannel);
}
}
} catch (error) {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, error, this._outputChannel);
TelemetryReporter.sendErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishToNewAzureServer);
}
}
/**
* Publishes a project to docker container
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project or the Project itself
* @param deployProfile
*/
public async publishToDockerContainer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: IDeployProfile): Promise<void> {
public async publishToDockerContainer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: ILocalDbDeployProfile): Promise<void> {
const project: Project = this.getProjectFromContext(context);
try {
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer)
.withAdditionalProperties({ dockerBaseImage: deployProfile.localDbSetting!.dockerBaseImage })
.send();
if (deployProfile && deployProfile.deploySettings) {
let connectionUri: string | undefined;
if (deployProfile.localDbSetting) {
void utils.showInfoMessageWithOutputChannel(constants.publishingProjectMessage, this._outputChannel);
connectionUri = await this.deployService.deploy(deployProfile, project);
connectionUri = await this.deployService.deployToContainer(deployProfile, project);
if (connectionUri) {
deployProfile.deploySettings.connectionUri = connectionUri;
}
@@ -321,7 +361,9 @@ export class ProjectsController {
}
} catch (error) {
void utils.showErrorMessageWithOutputChannel(constants.publishToContainerFailed, error, this._outputChannel);
TelemetryReporter.sendErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer);
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer)
.withAdditionalProperties({ dockerBaseImage: deployProfile.localDbSetting!.dockerBaseImage })
.send();
}
return;
}
@@ -367,9 +409,19 @@ export class ProjectsController {
if (publishTarget === constants.PublishTargetType.docker) {
const deployProfile = await launchPublishToDockerContainerQuickpick(project);
if (deployProfile?.deploySettings) {
if (deployProfile?.deploySettings && deployProfile?.localDbSetting) {
await this.publishToDockerContainer(project, deployProfile);
}
} else if (publishTarget === constants.PublishTargetType.newAzureServer) {
try {
const settings = await launchCreateAzureServerQuickPick(project, this.azureSqlClient);
if (settings?.deploySettings && settings?.sqlDbSetting) {
await this.publishToNewAzureServer(project, settings);
}
} catch (error) {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, error, this._outputChannel);
}
} else {
let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project);

View File

@@ -133,6 +133,48 @@ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?:
return undefined;
}
// 5. SDK-style project or not
let sdkStyle;
const sdkLearnMoreButton: vscode.QuickInputButton = {
iconPath: new vscode.ThemeIcon('link-external'),
tooltip: constants.learnMore
};
const quickPick = vscode.window.createQuickPick();
quickPick.items = [{ label: constants.YesRecommended }, { label: constants.noString }];
quickPick.title = constants.sdkStyleProject;
quickPick.ignoreFocusOut = true;
const disposables: vscode.Disposable[] = [];
try {
quickPick.buttons = [sdkLearnMoreButton];
quickPick.placeholder = constants.SdkLearnMorePlaceholder;
const sdkStylePromise = new Promise<boolean | undefined>((resolve) => {
disposables.push(
quickPick.onDidHide(() => {
resolve(undefined);
}),
quickPick.onDidChangeSelection((item) => {
resolve(item[0].label === constants.YesRecommended);
}));
disposables.push(quickPick.onDidTriggerButton(async () => {
await vscode.env.openExternal(vscode.Uri.parse(constants.sdkLearnMoreUrl!));
}));
});
quickPick.show();
sdkStyle = await sdkStylePromise;
quickPick.hide();
} finally {
disposables.forEach(d => d.dispose());
}
if (sdkStyle === undefined) {
// User cancelled
return;
}
return {
connectionUri: connectionUri,
database: selectedDatabase,
@@ -140,6 +182,6 @@ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?:
filePath: projectLocation,
version: '1.0.0.0',
extractTarget: mapExtractTargetEnum(folderStructure),
sdkStyle: false // todo: add sdkstyle option to quickpick
sdkStyle: sdkStyle
};
}

View File

@@ -7,11 +7,14 @@ import * as vscode from 'vscode';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import * as uiUtils from './utils';
import { AppSettingType, DockerImageInfo, IDeployAppIntegrationProfile, IDeployProfile, ILocalDbSetting } from '../models/deploy/deployProfile';
import { AppSettingType, DockerImageInfo, IDeployAppIntegrationProfile, ISqlDbDeployProfile, ILocalDbDeployProfile, ILocalDbSetting } from '../models/deploy/deployProfile';
import { Project } from '../models/project';
import { getPublishDatabaseSettings } from './publishDatabaseQuickpick';
import * as path from 'path';
import * as fse from 'fs-extra';
import { AzureSqlClient } from '../models/deploy/azureSqlClient';
import { IDeploySettings } from '../models/IDeploySettings';
import { IAccount } from 'vscode-mssql';
/**
* Create flow for Deploying a database using only VS Code-native APIs such as QuickPick
@@ -112,12 +115,168 @@ async function launchEulaQuickPick(imageInfo: DockerImageInfo | undefined): Prom
return false;
}
export async function launchCreateAzureServerQuickPick(project: Project, azureSqlClient: AzureSqlClient): Promise<ISqlDbDeployProfile | undefined> {
const name = uiUtils.getPublishServerName(project.getProjectTargetVersion());
const accounts = await azureSqlClient.getAccounts();
const accountOptions = accounts.map(x => x.displayInfo?.displayName || '');
accountOptions.unshift(constants.azureAddAccount);
let account: IAccount | undefined;
let accountOption = await vscode.window.showQuickPick(
accountOptions,
{ title: constants.azureAccounts, ignoreFocusOut: true });
// Return when user hits escape
if (!accountOption) {
return undefined;
}
if (accountOption === constants.azureAddAccount) {
account = await azureSqlClient.getAccount();
} else {
account = accounts.find(x => x.displayInfo.displayName === accountOption);
}
if (!account) {
return undefined;
}
const sessions = await azureSqlClient.getSessions(account);
const subscriptionName = await vscode.window.showQuickPick(
sessions.map(x => x.subscription.displayName || ''),
{ title: constants.azureSubscription, ignoreFocusOut: true });
// Return when user hits escape
if (!subscriptionName) {
return undefined;
}
const session = sessions.find(x => x.subscription.displayName === subscriptionName);
if (!session?.subscription?.subscriptionId) {
return undefined;
}
const resourceGroups = await azureSqlClient.getResourceGroups(session);
const resourceGroupName = await vscode.window.showQuickPick(
resourceGroups.map(x => x.name || ''),
{ title: constants.resourceGroup, ignoreFocusOut: true });
// Return when user hits escape
if (!resourceGroupName) {
return undefined;
}
const resourceGroup = resourceGroups.find(x => x.name === resourceGroupName);
// Return resource group is invalid
if (!resourceGroup) {
return undefined;
}
let locations = await azureSqlClient.getLocations(session);
if (resourceGroup.location) {
const defaultLocation = locations.find(x => x.name === resourceGroup.location);
if (defaultLocation) {
locations = locations.filter(x => x.name !== defaultLocation.name);
locations.unshift(defaultLocation);
}
}
let locationName = await vscode.window.showQuickPick(
locations.map(x => x.name || ''),
{ title: constants.azureLocation, ignoreFocusOut: true, placeHolder: resourceGroup?.location });
// Return when user hits escape
if (!locationName) {
return undefined;
}
let serverName: string | undefined = '';
serverName = await vscode.window.showInputBox({
title: constants.azureServerName,
ignoreFocusOut: true,
value: serverName,
password: false
}
);
// Return when user hits escape
if (!serverName) {
return undefined;
}
let user: string | undefined = '';
user = await vscode.window.showInputBox({
title: constants.enterUser(name),
ignoreFocusOut: true,
value: user,
password: false
}
);
// Return when user hits escape
if (!user) {
return undefined;
}
let password: string | undefined = '';
password = await vscode.window.showInputBox({
title: constants.enterPassword(name),
ignoreFocusOut: true,
value: password,
validateInput: input => !utils.isValidSQLPassword(input) ? constants.invalidSQLPasswordMessage(name) : undefined,
password: true
}
);
// Return when user hits escape
if (!password) {
return undefined;
}
let confirmPassword: string | undefined = '';
confirmPassword = await vscode.window.showInputBox({
title: constants.confirmPassword(name),
ignoreFocusOut: true,
value: confirmPassword,
validateInput: input => input !== password ? constants.passwordNotMatch(name) : undefined,
password: true
}
);
// Return when user hits escape
if (!confirmPassword) {
return undefined;
}
let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project, false);
return {
// TODO add tenant
deploySettings: settings, sqlDbSetting: {
tenantId: session.tenantId,
accountId: session.account.key.id,
serverName: serverName,
userName: user,
password: password,
port: 1433,
dbName: '',
session: session,
resourceGroupName: resourceGroup.name || '',
location: locationName
}
};
}
/**
* Create flow for publishing a database to docker container using only VS Code-native APIs such as QuickPick
*/
export async function launchPublishToDockerContainerQuickpick(project: Project): Promise<IDeployProfile | undefined> {
const name = uiUtils.getPublishServerName(project.getProjectTargetVersion());
export async function launchPublishToDockerContainerQuickpick(project: Project): Promise<ILocalDbDeployProfile | undefined> {
const target = project.getProjectTargetVersion();
const name = uiUtils.getPublishServerName(target);
let localDbSetting: ILocalDbSetting | undefined;
// Deploy to docker selected
let portNumber = await vscode.window.showInputBox({
@@ -163,10 +322,10 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
return undefined;
}
const baseImages = uiUtils.getDockerBaseImages(project.getProjectTargetVersion());
const baseImages = uiUtils.getDockerBaseImages(target);
const baseImage = await vscode.window.showQuickPick(
baseImages.map(x => x.displayName),
{ title: constants.selectBaseImage(name), ignoreFocusOut: true });
{ title: constants.selectBaseImage(name), ignoreFocusOut: true, placeHolder: uiUtils.getDockerImagePlaceHolder(target) });
// Return when user hits escape
if (!baseImage) {
@@ -174,19 +333,50 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
}
const imageInfo = baseImages.find(x => x.displayName === baseImage);
if (!imageInfo) {
return undefined;
}
const eulaAccepted = await launchEulaQuickPick(imageInfo);
if (!eulaAccepted) {
return undefined;
}
let imageTags = await uiUtils.getImageTags(imageInfo, target);
let imageTagsItems: vscode.QuickPickItem[] = imageTags.map(tag => { return { label: tag }; });
if (imageInfo.defaultTag) {
// move the default to be the first one in the list
const defaultIndex = imageTagsItems.findIndex(i => i.label === imageInfo.defaultTag);
if (defaultIndex > -1) {
imageTagsItems.splice(defaultIndex, 1);
}
// add default next to the default value
imageTagsItems.unshift({ label: imageInfo.defaultTag, description: constants.defaultQuickPickItem });
}
const imageTag = await vscode.window.showQuickPick(
imageTagsItems,
{ title: constants.selectImageTag(name), ignoreFocusOut: true });
if (!imageTag) {
return undefined;
}
// Add the image tag if it's not the latest
let imageName = imageInfo.name;
if (imageTag && imageTag.label !== constants.dockerImageDefaultTag) {
imageName = `${imageName}:${imageTag.label}`;
}
localDbSetting = {
serverName: constants.defaultLocalServerName,
userName: constants.defaultLocalServerAdminName,
dbName: project.projectFileName,
password: password,
port: +portNumber,
dockerBaseImage: imageInfo?.name || '',
dockerBaseImageEula: imageInfo?.agreementInfo?.link?.url || ''
dockerBaseImage: imageName,
dockerBaseImageEula: imageInfo.agreementInfo.link.url
};
let deploySettings = await getPublishDatabaseSettings(project, false);
@@ -202,7 +392,6 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
// Get the database name from deploy settings
localDbSetting.dbName = deploySettings.databaseName;
return {
localDbSetting: localDbSetting,
deploySettings: deploySettings,

View File

@@ -16,7 +16,7 @@ import { IconPathHelper } from '../common/iconHelper';
import { cssStyles } from '../common/uiConstants';
import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPublishServerName } from './utils';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { IDeployProfile } from '../models/deploy/deployProfile';
import { ILocalDbDeployProfile } from '../models/deploy/deployProfile';
import { Deferred } from '../common/promise';
interface DataSourceDropdownValue extends azdataType.CategoryValue {
@@ -62,7 +62,7 @@ export class PublishDatabaseDialog {
private toDispose: vscode.Disposable[] = [];
public publish: ((proj: Project, profile: IDeploySettings) => any) | undefined;
public publishToContainer: ((proj: Project, profile: IDeployProfile) => any) | undefined;
public publishToContainer: ((proj: Project, profile: ILocalDbDeployProfile) => any) | undefined;
public generateScript: ((proj: Project, profile: IDeploySettings) => any) | undefined;
public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined;
@@ -232,7 +232,7 @@ export class PublishDatabaseDialog {
const dockerBaseImage = this.getBaseDockerImageName();
const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion());
const imageInfo = baseImages.find(x => x.name === dockerBaseImage);
const settings: IDeployProfile = {
const settings: ILocalDbDeployProfile = {
localDbSetting: {
dbName: this.targetDatabaseName,
dockerBaseImage: dockerBaseImage,

View File

@@ -12,6 +12,7 @@ import { getDefaultPublishDeploymentOptions, getVscodeMssqlApi } from '../common
import { IConnectionInfo } from 'vscode-mssql';
import { IDeploySettings } from '../models/IDeploySettings';
import { getPublishServerName } from './utils';
import { SqlTargetPlatform } from 'sqldbproj';
/**
* Create flow for Publishing a database using only VS Code-native APIs such as QuickPick
@@ -209,9 +210,18 @@ export async function getPublishDatabaseSettings(project: Project, promptForConn
export async function launchPublishTargetOption(project: Project): Promise<constants.PublishTargetType | undefined> {
// Show options to user for deploy to existing server or docker
const name = getPublishServerName(project.getProjectTargetVersion());
const target = project.getProjectTargetVersion();
const name = getPublishServerName(target);
const logicalServerName = target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ? constants.AzureSqlLogicalServerName : constants.SqlServerName;
// Options list based on target
const options = target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ?
[constants.publishToAzureEmulator, constants.publishToNewAzureServer, constants.publishToExistingServer(logicalServerName)] :
[constants.publishToDockerContainer(name), constants.publishToExistingServer(logicalServerName)];
// Show the options to the user
const publishOption = await vscode.window.showQuickPick(
[constants.publishToExistingServer(name), constants.publishToDockerContainer(name)],
options,
{ title: constants.selectPublishOption, ignoreFocusOut: true });
// Return when user hits escape
@@ -219,11 +229,16 @@ export async function launchPublishTargetOption(project: Project): Promise<const
return undefined;
}
// Map the title to the publish option type
switch (publishOption) {
case constants.publishToExistingServer(name):
return constants.PublishTargetType.existingServer;
case constants.publishToDockerContainer(name):
return constants.PublishTargetType.docker;
case constants.publishToAzureEmulator:
return constants.PublishTargetType.docker;
case constants.publishToNewAzureServer:
return constants.PublishTargetType.newAzureServer;
default:
return constants.PublishTargetType.existingServer;
}

View File

@@ -5,6 +5,8 @@
import { SqlTargetPlatform } from 'sqldbproj';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import { HttpClient } from '../common/httpClient';
import { AgreementInfo, DockerImageInfo } from '../models/deploy/deployProfile';
/**
@@ -38,58 +40,117 @@ export function getPublishServerName(target: string): string {
return target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ? constants.AzureSqlServerName : constants.SqlServerName;
}
/**
* Returns the docker image place holder based on the target version
*/
export function getDockerImagePlaceHolder(target: string): string {
return target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ?
constants.dockerImagesPlaceHolder(constants.AzureSqlDbLiteDockerImageName) :
constants.dockerImagesPlaceHolder(SqlTargetPlatform.sqlEdge);
}
/**
* Returns the list of image tags for given target
* @param imageInfo docker image info
* @param target project target version
* @returns image tags
*/
export async function getImageTags(imageInfo: DockerImageInfo, target: string): Promise<string[]> {
let imageTags: string[] | undefined = [];
const versionToImageTags: Map<number, string[]> = new Map<number, string[]>();
try {
const imageTagsFromUrl = await HttpClient.getRequest(imageInfo?.tagsUrl, true);
if (imageTagsFromUrl?.tags) {
// Create a map for version and tags and find the max version in the list
let defaultVersion: number = 0;
let maxVersionNumber: number = defaultVersion;
(imageTagsFromUrl.tags as string[]).forEach(imageTag => {
const version = utils.findSqlVersionInImageName(imageTag) || defaultVersion;
let tags = versionToImageTags.has(version) ? versionToImageTags.get(version) : [];
tags = tags ?? [];
tags = tags?.concat(imageTag);
versionToImageTags.set(version, tags);
maxVersionNumber = version && version > maxVersionNumber ? version : maxVersionNumber;
});
// Find the version maps to the target framework and default to max version in the tags
const targetVersion = utils.findSqlVersionInTargetPlatform(constants.getTargetPlatformFromVersion(target)) || maxVersionNumber;
// Get the image tags with no version of the one that matches project platform
versionToImageTags.forEach((tags: string[], version: number) => {
if (version === defaultVersion || version >= targetVersion) {
imageTags = imageTags?.concat(tags);
}
});
imageTags = imageTags ?? [];
imageTags = imageTags.sort((a, b) => a.indexOf(constants.dockerImageDefaultTag) > 0 ? -1 : a.localeCompare(b));
}
} catch (err) {
// Ignore the error. If http request fails, we just use the default tag
console.debug(`Failed to get docker image tags ${err}`);
}
return imageTags;
}
/**
* Returns the list of base images for given target version
* @param target
* @returns list of image info
*/
export function getDockerBaseImages(target: string): DockerImageInfo[] {
if (target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)) {
return [{
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`,
displayName: SqlTargetPlatform.sqlServer2019,
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}`,
displayName: constants.AzureSqlDbFullDockerImageName,
agreementInfo: {
link: {
text: constants.eulaAgreementTitle,
url: constants.sqlServerEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.sqlServerDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
}, {
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest`,
displayName: SqlTargetPlatform.sqlEdge,
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}`,
displayName: constants.AzureSqlDbLiteDockerImageName,
agreementInfo: {
link: {
text: constants.edgeEulaAgreementTitle,
url: constants.sqlServerEdgeEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.azureSqlEdgeDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
}];
} else {
return [
{
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`,
displayName: SqlTargetPlatform.sqlServer2017,
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}`,
displayName: constants.SqlServerDockerImageName,
agreementInfo: {
link: {
text: constants.eulaAgreementTitle,
url: constants.sqlServerEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.sqlServerDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
},
{
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`,
displayName: SqlTargetPlatform.sqlServer2019,
agreementInfo: {
link: {
text: constants.eulaAgreementTitle,
url: constants.sqlServerEulaLink,
}
}
},
{
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest`,
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}`,
displayName: SqlTargetPlatform.sqlEdge,
agreementInfo: {
link: {
text: constants.edgeEulaAgreementTitle,
url: constants.sqlServerEdgeEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.azureSqlEdgeDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
},
];
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as utils from '../../common/utils';
import { IAccount, IAzureAccountSession, azure } from 'vscode-mssql';
/**
* Client module to call Azure APIs for getting or creating resources
*/
export class AzureSqlClient {
constructor(
private _azureAccountServiceFactory: utils.AzureAccountServiceFactory = utils.defaultAzureAccountServiceFactory,
private _azureResourceServiceFactory: utils.AzureResourceServiceFactory = utils.defaultAzureResourceServiceFactory
) { }
/**
* Returns existing Azure accounts
*/
public async getAccounts(): Promise<IAccount[]> {
const azureAccountService = await this._azureAccountServiceFactory();
return await azureAccountService.getAccounts();
}
/**
* Prompt user to login to Azure and returns the account
* @returns Azure account that user logged in to
*/
public async getAccount(): Promise<IAccount> {
const azureAccountService = await this._azureAccountServiceFactory();
return await azureAccountService.addAccount();
}
/**
* Returns Azure locations for given subscription
*/
public async getLocations(session: IAzureAccountSession): Promise<azure.subscription.Location[]> {
const azureResourceService = await this._azureResourceServiceFactory();
return await azureResourceService.getLocations(session);
}
/**
* Returns Azure sessions with subscription, tenant and token for given account
*/
public async getSessions(account: IAccount): Promise<IAzureAccountSession[]> {
const azureAccountService = await this._azureAccountServiceFactory();
return await azureAccountService.getAccountSessions(account);
}
/**
* Creates a new Azure SQL server for given subscription, resource group and location
*/
public async createOrUpdateServer(session: IAzureAccountSession, resourceGroupName: string, serverName: string, parameters: azure.sql.Server): Promise<string | undefined> {
const azureResourceService = await this._azureResourceServiceFactory();
return await azureResourceService.createOrUpdateServer(session, resourceGroupName, serverName, parameters);
}
/**
* Returns Azure resource groups for given subscription
*/
public async getResourceGroups(session: IAzureAccountSession): Promise<Array<azure.resources.ResourceGroup> | []> {
const azureResourceService = await this._azureResourceServiceFactory();
return await azureResourceService.getResourceGroups(session);
}
}

View File

@@ -5,39 +5,59 @@
import { IDeploySettings } from '../IDeploySettings';
import type * as azdataType from 'azdata';
import { IAzureAccountSession } from 'vscode-mssql';
export enum AppSettingType {
None,
AzureFunction
}
export interface IDeployProfile {
export interface ILocalDbDeployProfile {
localDbSetting?: ILocalDbSetting;
deploySettings?: IDeploySettings;
}
export interface ISqlDbDeployProfile {
sqlDbSetting?: ISqlDbSetting;
deploySettings?: IDeploySettings;
}
export interface IDeployAppIntegrationProfile {
envVariableName?: string;
appSettingFile?: string;
appSettingType: AppSettingType;
}
export interface ILocalDbSetting {
serverName: string,
port: number,
userName: string,
password: string,
dbName: string,
export interface ISqlDbSetting extends ISqlConnectionProperties {
session: IAzureAccountSession
resourceGroupName: string,
location: string
}
export interface ILocalDbSetting extends ISqlConnectionProperties {
dockerBaseImage: string,
dockerBaseImageEula: string,
connectionRetryTimeout?: number,
profileName?: string
}
export interface ISqlConnectionProperties {
tenantId?: string,
accountId?: string
serverName: string,
userName: string,
password: string,
port: number,
dbName: string,
profileName?: string,
connectionRetryTimeout?: number
}
export interface DockerImageInfo {
name: string,
displayName: string,
agreementInfo: AgreementInfo
agreementInfo: AgreementInfo,
tagsUrl: string,
defaultTag: string
}
export interface AgreementInfo {
link: azdataType.LinkArea;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AppSettingType, IDeployAppIntegrationProfile, IDeployProfile, ILocalDbSetting } from './deployProfile';
import { AppSettingType, IDeployAppIntegrationProfile, ILocalDbDeployProfile, ILocalDbSetting, ISqlConnectionProperties, ISqlDbDeployProfile } from './deployProfile';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import { Project } from '../project';
import * as constants from '../../common/constants';
@@ -13,6 +13,8 @@ import * as vscode from 'vscode';
import { ConnectionResult } from 'azdata';
import * as templates from '../../templates/templates';
import { ShellExecutionHelper } from '../../tools/shellExecutionHelper';
import { AzureSqlClient } from './azureSqlClient';
import { IFireWallRuleError } from 'vscode-mssql';
interface DockerImageSpec {
label: string;
@@ -21,7 +23,7 @@ interface DockerImageSpec {
}
export class DeployService {
constructor(private _outputChannel: vscode.OutputChannel, shellExecutionHelper: ShellExecutionHelper | undefined = undefined) {
constructor(private _azureSqlClient = new AzureSqlClient(), private _outputChannel: vscode.OutputChannel, shellExecutionHelper: ShellExecutionHelper | undefined = undefined) {
this._shellExecutionHelper = shellExecutionHelper ?? new ShellExecutionHelper(this._outputChannel);
}
@@ -50,7 +52,7 @@ export class DeployService {
return undefined;
}
public async updateAppSettings(profile: IDeployAppIntegrationProfile, deployProfile: IDeployProfile | undefined): Promise<void> {
public async updateAppSettings(profile: IDeployAppIntegrationProfile, deployProfile: ILocalDbDeployProfile | undefined): Promise<void> {
// Update app settings
//
if (!profile.appSettingFile) {
@@ -122,7 +124,37 @@ export class DeployService {
return { label: imageLabel, tag: imageTag, containerName: dockerName };
}
public async deploy(profile: IDeployProfile, project: Project): Promise<string | undefined> {
/**
* Creates a new Azure Sql server and tries to connect to the new server. If connection fails because of firewall rule, it prompts user to add firewall rule settings
* @param profile Azure Sql server settings
* @returns connection url for the new server
*/
public async createNewAzureSqlServer(profile: ISqlDbDeployProfile | undefined): Promise<string | undefined> {
if (!profile?.sqlDbSetting) {
return undefined;
}
this.logToOutput(constants.creatingAzureSqlServer(profile?.sqlDbSetting?.serverName));
// Create the server
const server = await this._azureSqlClient.createOrUpdateServer(profile.sqlDbSetting.session, profile?.sqlDbSetting.resourceGroupName, profile?.sqlDbSetting.serverName, {
location: profile?.sqlDbSetting?.location,
administratorLogin: profile?.sqlDbSetting.userName,
administratorLoginPassword: profile?.sqlDbSetting.password
});
if (server) {
this._outputChannel.appendLine(constants.serverCreated);
profile.sqlDbSetting.serverName = server;
this.logToOutput(constants.azureSqlServerCreated(profile?.sqlDbSetting?.serverName));
// Connect to the server
return await this.getConnection(profile.sqlDbSetting, false, constants.master);
}
return undefined;
}
public async deployToContainer(profile: ILocalDbDeployProfile, project: Project): Promise<string | undefined> {
return await this.executeTask(constants.deployDbTaskName, async () => {
if (!profile.localDbSetting) {
return undefined;
@@ -218,7 +250,7 @@ export class DeployService {
}
// Connects to a database
private async connectToDatabase(profile: ILocalDbSetting, saveConnectionAndPassword: boolean, database: string): Promise<ConnectionResult | string | undefined> {
private async connectToDatabase(profile: ISqlConnectionProperties, saveConnectionAndPassword: boolean, database: string): Promise<ConnectionResult | string | undefined> {
const getAzdataApi = await utils.getAzdataApi();
const vscodeMssqlApi = getAzdataApi ? undefined : await utils.getVscodeMssqlApi();
if (getAzdataApi) {
@@ -248,7 +280,7 @@ export class DeployService {
encrypt: false,
connectTimeout: 30,
applicationName: 'SQL Database Project',
accountId: undefined,
accountId: profile.accountId,
azureAccountToken: undefined,
applicationIntent: undefined,
attachDbFilename: undefined,
@@ -272,9 +304,19 @@ export class DeployService {
workstationId: undefined,
profileName: profile.profileName,
expiresOn: undefined,
tenantId: undefined
tenantId: profile.tenantId
};
let connectionUrl = await vscodeMssqlApi.connect(connectionProfile, saveConnectionAndPassword);
let connectionUrl = '';
try {
connectionUrl = await vscodeMssqlApi.connect(connectionProfile, saveConnectionAndPassword);
} catch (err) {
const firewallRuleError = <IFireWallRuleError>err;
if (firewallRuleError?.connectionUri) {
await vscodeMssqlApi.promptForFirewallRule(err.connectionUri, connectionProfile);
} else {
throw err;
}
}
return connectionUrl;
} else {
return undefined;
@@ -307,7 +349,7 @@ export class DeployService {
return connectionResult ? connectionResult.connectionId : <string>connection;
}
public async getConnection(profile: ILocalDbSetting, saveConnectionAndPassword: boolean, database: string): Promise<string | undefined> {
public async getConnection(profile: ISqlConnectionProperties, saveConnectionAndPassword: boolean, database: string): Promise<string | undefined> {
const getAzdataApi = await utils.getAzdataApi();
let connection = await utils.retry(
constants.connectingToSqlServerMessage,

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* 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 { AzureSqlClient } from '../../models/deploy/azureSqlClient';
import { IAccount, IAzureAccountService, IAzureAccountSession, IAzureResourceService, azure } from 'vscode-mssql';
export interface TestContext {
azureAccountService: IAzureAccountService;
azureResourceService: IAzureResourceService;
accounts: IAccount[];
session: IAzureAccountSession;
subscriptions: azure.subscription.Subscription[];
locations: azure.subscription.Location[];
groups: azure.resources.ResourceGroup[];
}
export function createContext(): TestContext {
const accounts = [{
key: undefined!,
displayInfo: undefined!,
properties: {
tenants: [{
id: '',
displayName: ''
}]
},
isStale: false,
isSignedIn: true
}];
const subscriptions: azure.subscription.Subscription[] = [{ subscriptionId: 'id1' }, { subscriptionId: 'id2' }];
const locations: azure.subscription.Location[] = [{ id: 'id1' }, { id: 'id2' }];
const groups: azure.resources.ResourceGroup[] = [{ id: 'id1', location: 'l1' }, { id: 'id2', location: 'l2' }];
const session: IAzureAccountSession = {
account: accounts[0],
subscription: subscriptions[0],
tenantId: 'tenantId',
token: {
key: '',
token: '',
tokenType: '',
}
};
return {
groups: groups,
locations: locations,
subscriptions: subscriptions,
session: session,
accounts: accounts,
azureAccountService: {
addAccount: () => Promise.resolve(accounts[0]),
getAccounts: () => Promise.resolve(accounts),
getAccountSecurityToken: () => Promise.resolve({
key: '',
token: '',
tokenType: ''
}),
getAccountSessions: () => Promise.resolve([session])
},
azureResourceService: {
getLocations: () => Promise.resolve(locations),
getResourceGroups: () => Promise.resolve(groups),
createOrUpdateServer: () => Promise.resolve('new_server')
}
};
}
describe('Azure SQL client', function (): void {
it('Should return accounts successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService));
const accounts = await azureSqlClient.getAccounts();
should(accounts.length).equal(testContext.accounts.length);
});
it('Should create and return new account successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService));
const account = await azureSqlClient.getAccount();
should(account.key).equal(testContext.accounts[0].key);
});
it('Should return subscriptions successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService));
const result = await azureSqlClient.getSessions(testContext.accounts[0]);
should(result[0].subscription.id).deepEqual(testContext.subscriptions[0].id);
});
it('Should return locations successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService), () => Promise.resolve(testContext.azureResourceService));
const result = await azureSqlClient.getLocations(testContext.session);
should(result.length).deepEqual(testContext.locations.length);
});
it('Should return resource groups successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService), () => Promise.resolve(testContext.azureResourceService));
const result = await azureSqlClient.getResourceGroups(testContext.session);
should(result.length).deepEqual(testContext.groups.length);
should(result[0].location).deepEqual(testContext.groups[0].location);
});
});

View File

@@ -11,16 +11,18 @@ import { DeployService } from '../../models/deploy/deployService';
import { Project } from '../../models/project';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { AppSettingType, IDeployProfile } from '../../models/deploy/deployProfile';
import { AppSettingType, ILocalDbDeployProfile, ISqlDbDeployProfile } from '../../models/deploy/deployProfile';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as fse from 'fs-extra';
import * as path from 'path';
import * as constants from '../../common/constants';
import { ShellExecutionHelper } from '../../tools/shellExecutionHelper';
import * as TypeMoq from 'typemoq';
import { AzureSqlClient } from '../../models/deploy/azureSqlClient';
export interface TestContext {
outputChannel: vscode.OutputChannel;
azureSqlClient: TypeMoq.IMock<AzureSqlClient>;
}
export const mockConnectionResult: azdata.ConnectionResult = {
@@ -47,7 +49,8 @@ export function createContext(): TestContext {
show: () => { },
hide: () => { },
dispose: () => { }
}
},
azureSqlClient: TypeMoq.Mock.ofType(AzureSqlClient)
};
}
@@ -68,7 +71,7 @@ describe('deploy service', function (): void {
it('Should deploy a database to docker container successfully', async function (): Promise<void> {
const testContext = createContext();
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
@@ -84,21 +87,21 @@ describe('deploy service', function (): void {
const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath);
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
sandbox.stub(azdata.connection, 'connect').returns(Promise.resolve(mockConnectionResult));
sandbox.stub(azdata.connection, 'getUriForConnection').returns(Promise.resolve('connection'));
sandbox.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.yesString));
sandbox.stub(azdata.tasks, 'startBackgroundOperation').callThrough();
let connection = await deployService.deploy(deployProfile, project1);
let connection = await deployService.deployToContainer(deployProfile, project1);
should(connection).equals('connection');
});
it('Should fail the deploy if docker is not running', async function (): Promise<void> {
const testContext = createContext();
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
@@ -114,11 +117,11 @@ describe('deploy service', function (): void {
const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath);
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject('error'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject('error'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
sandbox.stub(azdata.tasks, 'startBackgroundOperation').callThrough();
await should(deployService.deploy(deployProfile, project1)).rejected();
await should(deployService.deployToContainer(deployProfile, project1)).rejected();
});
it('Should retry connecting to the server', async function (): Promise<void> {
@@ -136,8 +139,8 @@ describe('deploy service', function (): void {
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
let connectionStub = sandbox.stub(azdata.connection, 'connect');
connectionStub.onFirstCall().returns(Promise.resolve(mockFailedConnectionResult));
connectionStub.onSecondCall().returns(Promise.resolve(mockConnectionResult));
@@ -173,7 +176,7 @@ describe('deploy service', function (): void {
const filePath = path.join(project1.projectFolderPath, 'local.settings.json');
await fse.writeFile(filePath, settingContent);
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
@@ -194,8 +197,8 @@ describe('deploy service', function (): void {
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
await deployService.updateAppSettings(appInteg, deployProfile);
let newContent = JSON.parse(fse.readFileSync(filePath, 'utf8'));
@@ -228,7 +231,7 @@ describe('deploy service', function (): void {
const filePath = path.join(project1.projectFolderPath, 'local.settings.json');
await fse.writeFile(filePath, settingContent);
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
deploySettings: {
connectionUri: 'connection',
@@ -245,8 +248,8 @@ describe('deploy service', function (): void {
};
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
let connection = new azdata.connection.ConnectionProfile();
sandbox.stub(azdata.connection, 'getConnection').returns(Promise.resolve(connection));
@@ -260,10 +263,10 @@ describe('deploy service', function (): void {
const testContext = createContext();
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(`id
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(`id
id2
id3`));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
const ids = await deployService.getCurrentDockerContainer('label');
await deployService.cleanDockerObjects(ids, ['docker stop', 'docker rm']);
shellExecutionHelper.verify(x => x.runStreamedCommand(TypeMoq.It.isAny(), undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(7));
@@ -271,7 +274,7 @@ describe('deploy service', function (): void {
it('Should create docker image info correctly', () => {
const testContext = createContext();
const deployService = new DeployService(testContext.outputChannel);
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel);
const id = UUID.generateUuid().toLocaleLowerCase();
const baseImage = 'baseImage:latest';
const tag = baseImage.replace(':', '-').replace(constants.sqlServerDockerRegistry, '').replace(/[^a-zA-Z0-9_,\-]/g, '').toLocaleLowerCase();
@@ -311,4 +314,53 @@ describe('deploy service', function (): void {
tag: `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}`
});
});
it('Should create a new Azure SQL server successfully', async function (): Promise<void> {
const testContext = createContext();
const deployProfile: ISqlDbDeployProfile = {
sqlDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
port: 1433,
serverName: 'localhost',
userName: 'sa',
connectionRetryTimeout: 1,
resourceGroupName: 'resourceGroups',
session: {
subscription: {
subscriptionId: 'subscriptionId',
},token: {
key: '',
token: '',
tokenType: '',
},
tenantId: '',
account: undefined!
},
location: 'location'
}
};
const fullyQualifiedDomainName = 'servername';
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const session = deployProfile?.sqlDbSetting?.session;
if (deployProfile?.sqlDbSetting?.session && session) {
testContext.azureSqlClient.setup(x => x.createOrUpdateServer(
session,
deployProfile.sqlDbSetting?.resourceGroupName || '',
deployProfile.sqlDbSetting?.serverName || '',
{
location: deployProfile?.sqlDbSetting?.location || '',
administratorLogin: deployProfile?.sqlDbSetting?.userName,
administratorLoginPassword: deployProfile?.sqlDbSetting?.password
})).returns(() => Promise.resolve(fullyQualifiedDomainName));
}
sandbox.stub(azdata.connection, 'connect').returns(Promise.resolve(mockConnectionResult));
sandbox.stub(azdata.connection, 'getUriForConnection').returns(Promise.resolve('connection'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
let connection = await deployService.createNewAzureSqlServer(deployProfile);
should(deployProfile.sqlDbSetting?.serverName).equal(fullyQualifiedDomainName);
should(connection).equals('connection');
});
});

View File

@@ -18,7 +18,7 @@ import { ProjectsController } from '../../controllers/projectController';
import { IDeploySettings } from '../../models/IDeploySettings';
import { emptySqlDatabaseProjectTypeId } from '../../common/constants';
import { createContext, mockDacFxOptionsResult, TestContext } from '../testContext';
import { IDeployProfile } from '../../models/deploy/deployProfile';
import { ILocalDbDeployProfile } from '../../models/deploy/deployProfile';
let testContext: TestContext;
describe('Publish Database Dialog', () => {
@@ -112,7 +112,7 @@ describe('Publish Database Dialog', () => {
should(profile).deepEqual(expectedGenScript);
const expectedContainerPublishProfile: IDeployProfile = {
const expectedContainerPublishProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'MockDatabaseName',
dockerBaseImage: '',
@@ -136,7 +136,7 @@ describe('Publish Database Dialog', () => {
}
};
dialog.object.publishToExistingServer = false;
let deployProfile: IDeployProfile | undefined;
let deployProfile: ILocalDbDeployProfile | undefined;
dialog.object.publishToContainer = (_, prof) => { deployProfile = prof; };
await dialog.object.publishClick();

View File

@@ -7,7 +7,7 @@ import * as should from 'should';
import * as path from 'path';
import * as os from 'os';
import { createDummyFileStructure } from './testUtils';
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation, isValidSQLPassword } from '../common/utils';
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation, isValidSQLPassword, findSqlVersionInImageName, findSqlVersionInTargetPlatform } from '../common/utils';
import { Uri } from 'vscode';
describe('Tests to verify utils functions', function (): void {
@@ -123,5 +123,20 @@ describe('Tests to verify utils functions', function (): void {
should(isValidSQLPassword('dFdf65$530')).equals(true, 'dF65$530 is valid password');
should(isValidSQLPassword('av1fgh533@')).equals(true, 'dF65$530 is valid password');
});
it('findSqlVersionInImageName should return the version correctly', () => {
should(findSqlVersionInImageName('2017-CU1-ubuntu')).equals(2017, 'invalid number returned for 2017-CU1-ubuntu');
should(findSqlVersionInImageName('2019-latest')).equals(2019, 'invalid number returned for 2019-latest');
should(findSqlVersionInImageName('latest')).equals(undefined, 'invalid number returned for latest');
should(findSqlVersionInImageName('latest-ubuntu')).equals(undefined, 'invalid number returned for latest-ubuntu');
should(findSqlVersionInImageName('2017-CU20-ubuntu-16.04')).equals(2017, 'invalid number returned for 2017-CU20-ubuntu-16.04');
});
it('findSqlVersionInTargetPlatform should return the version correctly', () => {
should(findSqlVersionInTargetPlatform('SQL Server 2012')).equals(2012, 'invalid number returned for SQL Server 2012');
should(findSqlVersionInTargetPlatform('SQL Server 2019')).equals(2019, 'invalid number returned for SQL Server 2019');
should(findSqlVersionInTargetPlatform('Azure SQL Database')).equals(undefined, 'invalid number returned for Azure SQL Database');
should(findSqlVersionInTargetPlatform('Azure Synapse Dedicated SQL Pool')).equals(undefined, 'invalid number returned for Azure Synapse Dedicated SQL Pool');
});
});

View File

@@ -50,9 +50,9 @@ export class ShellExecutionHelper {
// Add listeners to print stdout and stderr and exit code
void child.on('exit', (code: number | null, signal: string | null) => {
if (code !== null) {
this._outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithCode', " >>> {0} … exited with code: {1}", command, code));
this._outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithCode', " >>> {0} … exited with code: {1}", cmdOutputMessage, code));
} else {
this._outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithSignal', " >>> {0} … exited with signal: {1}", command, signal));
this._outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithSignal', " >>> {0} … exited with signal: {1}", cmdOutputMessage, signal));
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,11 @@ declare module 'vscode-mssql' {
*/
readonly azureAccountService: IAzureAccountService;
/**
* Service for accessing Azure Resources functionality
*/
readonly azureResourceService: IAzureResourceService;
/**
* Prompts the user to select an existing connection or create a new one, and then returns the result
* @param ignoreFocusOut Whether the quickpick prompt ignores focus out (default false)
@@ -399,6 +404,13 @@ declare module 'vscode-mssql' {
isSignedIn?: boolean;
}
export interface IAzureAccountSession {
subscription: azure.subscription.Subscription,
tenantId: string,
account: IAccount,
token: Token
}
export interface TokenKey {
/**
* Account Key - uniquely identifies an account
@@ -437,6 +449,38 @@ declare module 'vscode-mssql' {
* Returns an access token for given user and tenant
*/
getAccountSecurityToken(account: IAccount, tenantId: string | undefined): Promise<Token>;
/**
* Returns Azure subscriptions with tenant and token for each given account
*/
getAccountSessions(account: IAccount): Promise<IAzureAccountSession[]>;
}
export interface IAzureResourceService {
/**
* Returns Azure resource groups for given subscription
* @param session Azure session
* @returns List of resource groups
*/
getResourceGroups(session: IAzureAccountSession): Promise<azure.resources.ResourceGroup[]>;
/**
* Creates or updates a Azure SQL server for given subscription, resource group and location
* @param session Azure session
* @param resourceGroupName resource group name
* @param serverName SQL server name
* @param parameters parameters for the SQL server
* @returns name of the SQL server
*/
createOrUpdateServer(session: IAzureAccountSession, resourceGroupName: string, serverName: string, parameters: azure.sql.Server): Promise<string | undefined>;
/**
* Returns Azure locations for given session
* @param session Azure session
* @returns List of locations
*/
getLocations(session: IAzureAccountSession): Promise<azure.subscription.Location[]>;
}
export const enum TaskExecutionMode {
@@ -725,4 +769,462 @@ declare module 'vscode-mssql' {
options: { [name: string]: any };
}
/**
* Namespace for Azure APIs
*/
export namespace azure {
/**
* Namespace for Azure Subscriptions. Types from @azure/arm-subscriptions module
*/
export namespace subscription {
/** Location information. */
interface Location {
/**
* The fully qualified ID of the location. For example, /subscriptions/00000000-0000-0000-0000-000000000000/locations/westus.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly id?: string;
/**
* The subscription ID.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly subscriptionId?: string;
/**
* The location name.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly name?: string;
/**
* The display name of the location.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly displayName?: string;
/**
* The latitude of the location.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly latitude?: string;
/**
* The longitude of the location.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly longitude?: string;
}
/** Subscription information. */
export interface Subscription {
/**
* The fully qualified ID for the subscription. For example, /subscriptions/00000000-0000-0000-0000-000000000000.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly id?: string;
/**
* The subscription ID.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly subscriptionId?: string;
/**
* The subscription display name.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly displayName?: string;
/**
* The subscription state. Possible values are Enabled, Warned, PastDue, Disabled, and Deleted.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly state?: SubscriptionState;
/** The subscription policies. */
subscriptionPolicies?: SubscriptionPolicies;
/** The authorization source of the request. Valid values are one or more combinations of Legacy, RoleBased, Bypassed, Direct and Management. For example, 'Legacy, RoleBased'. */
authorizationSource?: string;
}
/** Defines values for SubscriptionState. */
export type SubscriptionState = 'Enabled' | 'Warned' | 'PastDue' | 'Disabled' | 'Deleted';
/** Subscription policies. */
export interface SubscriptionPolicies {
/**
* The subscription location placement ID. The ID indicates which regions are visible for a subscription. For example, a subscription with a location placement Id of Public_2014-09-01 has access to Azure public regions.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly locationPlacementId?: string;
/**
* The subscription quota ID.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly quotaId?: string;
/**
* The subscription spending limit.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly spendingLimit?: SpendingLimit;
}
/** Defines values for SpendingLimit. */
export type SpendingLimit = 'On' | 'Off' | 'CurrentPeriodOff';
}
/**
* Namespace for Azure resources. Types from @azure/arm-resources module
*/
export namespace resources {
export interface ResourceGroup {
/**
* The ID of the resource group.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly id?: string;
/**
* The name of the resource group.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly name?: string;
/**
* The type of the resource group.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly type?: string;
/** The resource group properties. */
properties?: ResourceGroupProperties;
/** The location of the resource group. It cannot be changed after the resource group has been created. It must be one of the supported Azure locations. */
location: string;
/** The ID of the resource that manages this resource group. */
managedBy?: string;
/** The tags attached to the resource group. */
tags?: {
[propertyName: string]: string;
};
}
/** The resource group properties. */
export interface ResourceGroupProperties {
/**
* The provisioning state.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly provisioningState?: string;
}
export interface ResourceGroup {
/**
* The ID of the resource group.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly id?: string;
/**
* The name of the resource group.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly name?: string;
/**
* The type of the resource group.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly type?: string;
/** The resource group properties. */
properties?: ResourceGroupProperties;
/** The location of the resource group. It cannot be changed after the resource group has been created. It must be one of the supported Azure locations. */
location: string;
/** The ID of the resource that manages this resource group. */
managedBy?: string;
/** The tags attached to the resource group. */
tags?: {
[propertyName: string]: string;
};
}
/** The resource group properties. */
export interface ResourceGroupProperties {
/**
* The provisioning state.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly provisioningState?: string;
}
}
/**
* Namespace for Azure SQL APIs. Types from @azure/arm-sql module
*/
export namespace sql {
/** ARM resource. */
export interface Resource {
/**
* Resource ID.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly id?: string;
/**
* Resource name.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly name?: string;
/**
* Resource type.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly type?: string;
}
/** Azure Active Directory identity configuration for a resource. */
export interface UserIdentity {
/**
* The Azure Active Directory principal id.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly principalId?: string;
/**
* The Azure Active Directory client id.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly clientId?: string;
}
/**
* Defines values for IdentityType. \
* {@link KnownIdentityType} can be used interchangeably with IdentityType,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **None** \
* **SystemAssigned** \
* **UserAssigned** \
* **SystemAssigned,UserAssigned**
*/
export type IdentityType = string;
/** Azure Active Directory identity configuration for a resource. */
export interface ResourceIdentity {
/** The resource ids of the user assigned identities to use */
userAssignedIdentities?: {
[propertyName: string]: UserIdentity;
};
/**
* The Azure Active Directory principal id.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly principalId?: string;
/** The identity type. Set this to 'SystemAssigned' in order to automatically create and assign an Azure Active Directory principal for the resource. */
type?: IdentityType;
/**
* The Azure Active Directory tenant id.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly tenantId?: string;
}
/** ARM tracked top level resource. */
export type TrackedResource = Resource & {
/** Resource location. */
location: string;
/** Resource tags. */
tags?: {
[propertyName: string]: string;
};
};
/** An Azure SQL Database server. */
export type Server = TrackedResource & {
/** The Azure Active Directory identity of the server. */
identity?: ResourceIdentity;
/**
* Kind of sql server. This is metadata used for the Azure portal experience.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly kind?: string;
/** Administrator username for the server. Once created it cannot be changed. */
administratorLogin?: string;
/** The administrator login password (required for server creation). */
administratorLoginPassword?: string;
/** The version of the server. */
version?: string;
/**
* The state of the server.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly state?: string;
/**
* The fully qualified domain name of the server.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly fullyQualifiedDomainName?: string;
/**
* List of private endpoint connections on a server
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly privateEndpointConnections?: ServerPrivateEndpointConnection[];
/** Minimal TLS version. Allowed values: '1.0', '1.1', '1.2' */
minimalTlsVersion?: string;
/** Whether or not public endpoint access is allowed for this server. Value is optional but if passed in, must be 'Enabled' or 'Disabled' */
publicNetworkAccess?: ServerNetworkAccessFlag;
/**
* Whether or not existing server has a workspace created and if it allows connection from workspace
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly workspaceFeature?: ServerWorkspaceFeature;
/** The resource id of a user assigned identity to be used by default. */
primaryUserAssignedIdentityId?: string;
/** The Client id used for cross tenant CMK scenario */
federatedClientId?: string;
/** A CMK URI of the key to use for encryption. */
keyId?: string;
/** The Azure Active Directory identity of the server. */
administrators?: ServerExternalAdministrator;
/** Whether or not to restrict outbound network access for this server. Value is optional but if passed in, must be 'Enabled' or 'Disabled' */
restrictOutboundNetworkAccess?: ServerNetworkAccessFlag;
};
/** A private endpoint connection under a server */
export interface ServerPrivateEndpointConnection {
/**
* Resource ID.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly id?: string;
/**
* Private endpoint connection properties
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly properties?: PrivateEndpointConnectionProperties;
}
/**
* Defines values for ServerNetworkAccessFlag. \
* {@link KnownServerNetworkAccessFlag} can be used interchangeably with ServerNetworkAccessFlag,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **Enabled** \
* **Disabled**
*/
export type ServerNetworkAccessFlag = string;
/**
* Defines values for ServerWorkspaceFeature. \
* {@link KnownServerWorkspaceFeature} can be used interchangeably with ServerWorkspaceFeature,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **Connected** \
* **Disconnected**
*/
export type ServerWorkspaceFeature = string;
/** Properties of a active directory administrator. */
export interface ServerExternalAdministrator {
/** Type of the sever administrator. */
administratorType?: AdministratorType;
/** Principal Type of the sever administrator. */
principalType?: PrincipalType;
/** Login name of the server administrator. */
login?: string;
/** SID (object ID) of the server administrator. */
sid?: string;
/** Tenant ID of the administrator. */
tenantId?: string;
/** Azure Active Directory only Authentication enabled. */
azureADOnlyAuthentication?: boolean;
}
/** Properties of a private endpoint connection. */
export interface PrivateEndpointConnectionProperties {
/** Private endpoint which the connection belongs to. */
privateEndpoint?: PrivateEndpointProperty;
/** Connection state of the private endpoint connection. */
privateLinkServiceConnectionState?: PrivateLinkServiceConnectionStateProperty;
/**
* State of the private endpoint connection.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly provisioningState?: PrivateEndpointProvisioningState;
}
/**
* Defines values for AdministratorType. \
* {@link KnownAdministratorType} can be used interchangeably with AdministratorType,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **ActiveDirectory**
*/
export type AdministratorType = string;
/**
* Defines values for PrincipalType. \
* {@link KnownPrincipalType} can be used interchangeably with PrincipalType,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **User** \
* **Group** \
* **Application**
*/
export type PrincipalType = string;
export interface PrivateEndpointProperty {
/** Resource id of the private endpoint. */
id?: string;
}
export interface PrivateLinkServiceConnectionStateProperty {
/** The private link service connection status. */
status: PrivateLinkServiceConnectionStateStatus;
/** The private link service connection description. */
description: string;
/**
* The actions required for private link service connection.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly actionsRequired?: PrivateLinkServiceConnectionStateActionsRequire;
}
/**
* Defines values for PrivateEndpointProvisioningState. \
* {@link KnownPrivateEndpointProvisioningState} can be used interchangeably with PrivateEndpointProvisioningState,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **Approving** \
* **Ready** \
* **Dropping** \
* **Failed** \
* **Rejecting**
*/
export type PrivateEndpointProvisioningState = string;
/**
* Defines values for PrivateLinkServiceConnectionStateStatus. \
* {@link KnownPrivateLinkServiceConnectionStateStatus} can be used interchangeably with PrivateLinkServiceConnectionStateStatus,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **Approved** \
* **Pending** \
* **Rejected** \
* **Disconnected**
*/
export type PrivateLinkServiceConnectionStateStatus = string;
/**
* Defines values for PrivateLinkServiceConnectionStateActionsRequire. \
* {@link KnownPrivateLinkServiceConnectionStateActionsRequire} can be used interchangeably with PrivateLinkServiceConnectionStateActionsRequire,
* this enum contains the known values that the service supports.
* ### Known values supported by the service
* **None**
*/
export type PrivateLinkServiceConnectionStateActionsRequire = string;
export interface PrivateLinkServiceConnectionStateProperty {
/** The private link service connection status. */
status: PrivateLinkServiceConnectionStateStatus;
/** The private link service connection description. */
description: string;
/**
* The actions required for private link service connection.
* NOTE: This property will not be serialized. It can only be populated by the server.
*/
readonly actionsRequired?: PrivateLinkServiceConnectionStateActionsRequire;
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "azuredatastudio",
"version": "1.36.0",
"version": "1.36.2",
"distro": "8f4d839fbcb98eaa7aa3fd8bf9f84ab9bae899d9",
"author": {
"name": "Microsoft Corporation"
@@ -75,7 +75,7 @@
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"applicationinsights": "1.0.8",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.19",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
"chart.js": "^2.9.4",
"chokidar": "3.5.2",
"graceful-fs": "4.2.6",

View File

@@ -61,6 +61,7 @@
"Microsoft.sql-database-projects",
"Microsoft.sql-migration",
"Microsoft.machine-learning",
"Microsoft.dotnet-interactive-vscode",
"Redgate.sql-prompt",
"Redgate.sql-search",
"SentryOne.plan-explorer"
@@ -76,7 +77,9 @@
"ms-vscode.remotehub",
"ms-vscode.remotehub-insiders",
"GitHub.remotehub",
"GitHub.remotehub-insiders"
"GitHub.remotehub-insiders",
"ms-dotnettools.dotnet-interactive-vscode",
"ms-toolsai.jupyter"
],
"extensionsGallery": {
"version": "0.0.76",

View File

@@ -16,7 +16,7 @@
"applicationinsights": "1.0.8",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.19",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
"chart.js": "^2.9.4",
"chokidar": "3.5.2",
"cookie": "^0.4.0",

View File

@@ -15,7 +15,7 @@
"@vscode/vscode-languagedetection": "1.0.18",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.19",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
"chart.js": "^2.9.4",
"gridstack": "^3.1.3",
"kburtram-query-plan": "2.6.1",

View File

@@ -150,9 +150,9 @@ array-uniq@^1.0.2:
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
"azdataGraph@github:Microsoft/azdataGraph#0.0.19":
version "0.0.19"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/4f5e3ef88b997c0190d2bd688a586b993f9fed67"
"azdataGraph@github:Microsoft/azdataGraph#0.0.21":
version "0.0.21"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
chalk@^2.3.0, chalk@^2.4.1:
version "2.4.2"

View File

@@ -198,9 +198,9 @@ array-uniq@^1.0.2:
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
"azdataGraph@github:Microsoft/azdataGraph#0.0.19":
version "0.0.19"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/4f5e3ef88b997c0190d2bd688a586b993f9fed67"
"azdataGraph@github:Microsoft/azdataGraph#0.0.21":
version "0.0.21"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
binary-extensions@^2.0.0:
version "2.0.0"

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

@@ -2354,7 +2354,11 @@ declare module 'azdata' {
/**
* Kusto
*/
AzureKusto = 10
AzureKusto = 10,
/**
* Power BI
*/
PowerBi = 11
}
export interface DidChangeAccountsParams {

View File

@@ -1264,6 +1264,27 @@ declare module 'azdata' {
* Edges corresponding to the children.
*/
edges: ExecutionPlanEdge[];
/**
* Warning/parallelism badges applicable to the current node
*/
badges: ExecutionPlanBadge[];
}
export interface ExecutionPlanBadge {
/**
* Type of the node overlay. This determines the icon that is displayed for it
*/
type: BadgeType;
/**
* Text to display for the overlay tooltip
*/
tooltip: string;
}
export enum BadgeType {
Warning = 0,
CriticalWarning = 1,
Parallelism = 2
}
export interface ExecutionPlanEdge {
@@ -1392,9 +1413,13 @@ declare module 'azdata' {
*/
export interface InfoBoxComponent extends Component, InfoBoxComponentProperties {
/**
* An event called when the InfoBox is clicked
* An event fired when the InfoBox is clicked
*/
onDidClick: vscode.Event<void>;
/**
* An event fired when the Infobox link is clicked
*/
onLinkClick: vscode.Event<InfoBoxLinkClickEventArgs>;
}
export interface InfoBoxComponentProperties {
@@ -1408,5 +1433,27 @@ declare module 'azdata' {
* Sets the ariaLabel for the right arrow button that shows up in clickable infoboxes
*/
clickableButtonAriaLabel?: string;
/**
* List of links to embed within the text. If links are specified there must be placeholder
* values in the value indicating where the links should be placed, in the format {i}
*
* e.g. "Click {0} for more information!""
*/
links?: LinkArea[];
}
/**
* Event argument for infobox link click event.
*/
export interface InfoBoxLinkClickEventArgs {
/**
* Index of the link selected
*/
index: number;
/**
* Link that is clicked
*/
link: LinkArea;
}
}

View File

@@ -22,7 +22,8 @@ export enum ComponentEventType {
onCellAction,
onEnterKeyPressed,
onInput,
onComponentLoaded
onComponentLoaded,
onChildClick
}
/**

View File

@@ -38,6 +38,7 @@ export const enum TelemetryView {
AgentNotebookHistory = 'AgentNotebookHistory',
AgentNotebooks = 'AgentNotebooks',
ConnectionDialog = 'ConnectionDialog',
ExecutionPlan = 'ExecutionPlan',
ExtensionHost = 'ExtensionHost',
ExtensionRecommendationDialog = 'ExtensionRecommendationDialog',
Notebook = 'Notebook',
@@ -55,6 +56,7 @@ export const enum TelemetryAction {
AddServerGroup = 'AddServerGroup',
adsCommandExecuted = 'adsCommandExecuted',
ConnectToServer = 'ConnectToServer',
CustomZoom = 'CustomZoom',
BackupCreated = 'BackupCreated',
DashboardNavigated = 'DashboardNavigated',
DatabaseConnected = 'DatabaseConnected',
@@ -69,6 +71,7 @@ export const enum TelemetryAction {
CancelQuery = 'CancelQuery',
ChartCreated = 'ChartCreated',
Click = 'Click',
FindNode = 'FindNode',
FirewallRuleRequested = 'FirewallRuleCreated',
GenerateScript = 'GenerateScript',
GeneratePreviewReport = 'GeneratePreviewReport',
@@ -82,6 +85,8 @@ export const enum TelemetryAction {
NewQuery = 'NewQuery',
ObjectExplorerExpand = 'ObjectExplorerExpand',
Open = 'Open',
OpenQuery = 'OpenQuery',
OpenExecutionPlanProperties = 'OpenExecutionPlanProperties',
PublishChanges = 'PublishChanges',
RestoreRequested = 'RestoreRequested',
RunAgentJob = 'RunAgentJob',
@@ -90,9 +95,13 @@ export const enum TelemetryAction {
RunQueryString = 'RunQueryString',
ShowChart = 'ShowChart',
StopAgentJob = 'StopAgentJob',
ViewTopOperations = 'ViewTopOperations',
WizardPagesNavigation = 'WizardPagesNavigation',
SearchStarted = 'SearchStarted',
SearchCompleted = 'SearchCompleted'
SearchCompleted = 'SearchCompleted',
ZoomIn = 'ZoomIn',
ZoomOut = 'ZoomOut',
ZoomToFit = 'ZoomToFIt'
}
export const enum NbTelemetryAction {

View File

@@ -2085,7 +2085,8 @@ class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBox
constructor(proxy: MainThreadModelViewShape, handle: number, id: string, logService: ILogService) {
super(proxy, handle, ModelComponentTypes.InfoBox, id, logService);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<void>());
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
this._emitterMap.set(ComponentEventType.onChildClick, new Emitter<any>());
}
public get style(): azdata.InfoBoxStyle {
@@ -2104,6 +2105,14 @@ class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBox
this.setProperty('text', v);
}
public get links(): azdata.LinkArea[] {
return this.properties['links'];
}
public set links(v: azdata.LinkArea[]) {
this.setProperty('links', v);
}
public get announceText(): boolean {
return this.properties['announceText'];
}
@@ -2128,10 +2137,15 @@ class InfoBoxComponentWrapper extends ComponentWrapper implements azdata.InfoBox
this.setProperty('clickableButtonAriaLabel', v);
}
public get onDidClick(): vscode.Event<any> {
public get onDidClick(): vscode.Event<void> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
public get onLinkClick(): vscode.Event<azdata.InfoBoxLinkClickEventArgs> {
let emitter = this._emitterMap.get(ComponentEventType.onChildClick);
return emitter && emitter.event;
}
}
class SliderComponentWrapper extends ComponentWrapper implements azdata.SliderComponent {

View File

@@ -173,6 +173,10 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
}
$requestExecute(kernelId: number, content: azdata.nb.IExecuteRequest, disposeOnDone?: boolean): Thenable<INotebookFutureDetails> {
// Revive request's URIs to restore functions
content.notebookUri = URI.revive(content.notebookUri);
content.cellUri = URI.revive(content.cellUri);
let kernel = this._getAdapter<azdata.nb.IKernel>(kernelId);
let future = kernel.requestExecute(content, disposeOnDone);
let futureId = this._addNewAdapter(future);
@@ -259,9 +263,17 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
return this.registerSerializationProvider(serializationProvider);
}
createNotebookController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler?: (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable<void>, rendererScripts?: vscode.NotebookRendererScript[]): vscode.NotebookController {
createNotebookController(
extension: IExtensionDescription,
id: string,
viewType: string,
label: string,
getDocHandler: (notebookUri: URI) => azdata.nb.NotebookDocument,
execHandler?: (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable<void>,
rendererScripts?: vscode.NotebookRendererScript[]
): vscode.NotebookController {
let languagesHandler = (languages: string[]) => this._proxy.$updateKernelLanguages(viewType, viewType, languages);
let controller = new ADSNotebookController(extension, id, viewType, label, this._extHostNotebookDocumentsAndEditors, languagesHandler, handler, extension.enableProposedApi ? rendererScripts : undefined);
let controller = new ADSNotebookController(extension, id, viewType, label, this._extHostNotebookDocumentsAndEditors, languagesHandler, getDocHandler, execHandler, extension.enableProposedApi ? rendererScripts : undefined);
let newKernel: azdata.nb.IStandardKernel = {
name: viewType,
displayName: controller.label,

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import type * as azdata from 'azdata';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { INotebookKernelDto2 } from 'vs/workbench/api/common/extHost.protocol';
import { Emitter, Event } from 'vs/base/common/event';
@@ -14,13 +15,14 @@ import { URI } from 'vs/base/common/uri';
import { NotebookCellExecutionTaskState } from 'vs/workbench/api/common/extHostNotebookKernels';
import { asArray } from 'vs/base/common/arrays';
import { convertToADSCellOutput } from 'sql/workbench/api/common/notebooks/notebookUtils';
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
type SelectionChangedEvent = { selected: boolean, notebook: vscode.NotebookDocument; };
type MessageReceivedEvent = { editor: vscode.NotebookEditor, message: any; };
type ExecutionHandler = (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable<void>;
type LanguagesHandler = (languages: string[]) => void;
type InterruptHandler = (notebook: vscode.NotebookDocument) => void | Promise<void>;
type GetDocHandler = (notebookUri: URI) => azdata.nb.NotebookDocument;
/**
* A VS Code Notebook Controller that is used as part of converting VS Code notebook extension APIs into ADS equivalents.
@@ -35,6 +37,8 @@ export class ADSNotebookController implements vscode.NotebookController {
private readonly _languagesAdded = new Deferred<void>();
private readonly _executionHandlerAdded = new Deferred<void>();
private readonly _execMap: Map<string, ADSNotebookCellExecution> = new Map();
constructor(
private _extension: IExtensionDescription,
private _id: string,
@@ -42,7 +46,8 @@ export class ADSNotebookController implements vscode.NotebookController {
private _label: string,
private _extHostNotebookDocumentsAndEditors: ExtHostNotebookDocumentsAndEditors,
private _languagesHandler: LanguagesHandler,
private _handler?: ExecutionHandler,
private _getDocHandler: GetDocHandler,
private _execHandler?: ExecutionHandler,
preloads?: vscode.NotebookRendererScript[]
) {
this._kernelData = {
@@ -53,7 +58,7 @@ export class ADSNotebookController implements vscode.NotebookController {
label: this._label || this._extension.identifier.value,
preloads: preloads ? preloads.map(extHostTypeConverters.NotebookRendererScript.from) : []
};
if (this._handler) {
if (this._execHandler) {
this._executionHandlerAdded.resolve();
}
}
@@ -125,11 +130,11 @@ export class ADSNotebookController implements vscode.NotebookController {
}
public get executeHandler(): ExecutionHandler {
return this._handler;
return this._execHandler;
}
public set executeHandler(value: ExecutionHandler) {
this._handler = value;
this._execHandler = value;
this._executionHandlerAdded.resolve();
}
@@ -142,8 +147,22 @@ export class ADSNotebookController implements vscode.NotebookController {
this._kernelData.supportsInterrupt = Boolean(value);
}
public getCellExecution(cellUri: URI): ADSNotebookCellExecution | undefined {
return this._execMap.get(cellUri.toString());
}
public removeCellExecution(cellUri: URI): void {
this._execMap.delete(cellUri.toString());
}
public getNotebookDocument(notebookUri: URI): azdata.nb.NotebookDocument {
return this._getDocHandler(notebookUri);
}
public createNotebookCellExecution(cell: vscode.NotebookCell): vscode.NotebookCellExecution {
return new ADSNotebookCellExecution(cell, this._extHostNotebookDocumentsAndEditors);
let exec = new ADSNotebookCellExecution(cell, this._extHostNotebookDocumentsAndEditors);
this._execMap.set(cell.document.uri.toString(), exec);
return exec;
}
public dispose(): void {
@@ -166,6 +185,7 @@ export class ADSNotebookController implements vscode.NotebookController {
class ADSNotebookCellExecution implements vscode.NotebookCellExecution {
private _executionOrder: number;
private _state = NotebookCellExecutionTaskState.Init;
private _cancellationSource = new CancellationTokenSource();
constructor(private readonly _cell: vscode.NotebookCell, private readonly _extHostNotebookDocumentsAndEditors: ExtHostNotebookDocumentsAndEditors) {
this._executionOrder = this._cell.executionSummary?.executionOrder ?? -1;
}
@@ -174,8 +194,12 @@ class ADSNotebookCellExecution implements vscode.NotebookCellExecution {
return this._cell;
}
public get tokenSource(): vscode.CancellationTokenSource {
return this._cancellationSource;
}
public get token(): vscode.CancellationToken {
return CancellationToken.None;
return this._cancellationSource.token;
}
public get executionOrder(): number {

View File

@@ -9,6 +9,8 @@ import { ADSNotebookController } from 'sql/workbench/api/common/notebooks/adsNot
import * as nls from 'vs/nls';
import { convertToVSCodeNotebookCell } from 'sql/workbench/api/common/notebooks/notebookUtils';
import { CellTypes } from 'sql/workbench/services/notebook/common/contracts';
import { VSCodeNotebookDocument } from 'sql/workbench/api/common/notebooks/vscodeNotebookDocument';
import { URI } from 'vs/base/common/uri';
class VSCodeFuture implements azdata.nb.IFuture {
private _inProgress = true;
@@ -71,6 +73,7 @@ class VSCodeKernel implements azdata.nb.IKernel {
private readonly _name: string;
private readonly _info: azdata.nb.IInfoReply;
private readonly _kernelSpec: azdata.nb.IKernelSpec;
private _activeRequest: azdata.nb.IExecuteRequest;
constructor(private readonly _controller: ADSNotebookController, private readonly _options: azdata.nb.ISessionOptions) {
this._id = this._options.kernelId ?? (VSCodeKernel.kernelId++).toString();
@@ -136,11 +139,20 @@ class VSCodeKernel implements azdata.nb.IKernel {
return Promise.resolve(this.spec);
}
private cleanUpActiveExecution(cellUri: URI) {
this._activeRequest = undefined;
this._controller.removeCellExecution(cellUri);
}
requestExecute(content: azdata.nb.IExecuteRequest, disposeOnDone?: boolean): azdata.nb.IFuture {
if (this._activeRequest) {
throw new Error(nls.localize('notebookMultipleRequestsError', "Cannot execute code cell. Another cell is currently being executed."));
}
let executePromise: Promise<void>;
if (this._controller.executeHandler) {
let cell = convertToVSCodeNotebookCell(CellTypes.Code, content.cellIndex, content.cellUri, content.notebookUri, content.language ?? this._kernelSpec.language, content.code);
executePromise = Promise.resolve(this._controller.executeHandler([cell], cell.notebook, this._controller));
this._activeRequest = content;
executePromise = Promise.resolve(this._controller.executeHandler([cell], cell.notebook, this._controller)).then(() => this.cleanUpActiveExecution(content.cellUri));
} else {
executePromise = Promise.resolve();
}
@@ -154,7 +166,16 @@ class VSCodeKernel implements azdata.nb.IKernel {
}
public async interrupt(): Promise<void> {
return;
if (this._activeRequest) {
if (this._controller.interruptHandler) {
let doc = this._controller.getNotebookDocument(this._activeRequest.notebookUri);
await this._controller.interruptHandler.call(this._controller, new VSCodeNotebookDocument(doc));
} else {
let exec = this._controller.getCellExecution(this._activeRequest.cellUri);
exec?.tokenSource.cancel();
}
this.cleanUpActiveExecution(this._activeRequest.cellUri);
}
}
public async restart(): Promise<void> {

View File

@@ -593,6 +593,10 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
}
};
const executionPlan: typeof azdata.executionPlan = {
BadgeType: sqlExtHostTypes.executionPlan.BadgeType
};
return {
version: initData.version,
accounts,
@@ -644,7 +648,8 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
TabOrientation: sqlExtHostTypes.TabOrientation,
sqlAssessment,
TextType: sqlExtHostTypes.TextType,
designers: designers
designers: designers,
executionPlan: executionPlan
};
},
extHostNotebook: extHostNotebook,

View File

@@ -246,7 +246,8 @@ export enum ComponentEventType {
onCellAction,
onEnterKeyPressed,
onInput,
onComponentLoaded
onComponentLoaded,
onChildClick
}
export interface IComponentEventArgs {
@@ -442,7 +443,8 @@ export enum AzureResource {
MsGraph = 7,
AzureLogAnalytics = 8,
AzureStorage = 9,
AzureKusto = 10
AzureKusto = 10,
PowerBi = 11
}
export class TreeItem extends vsExtTypes.TreeItem {
@@ -1031,3 +1033,11 @@ export namespace designers {
GraphNode = 'GraphNode'
}
}
export namespace executionPlan {
export enum BadgeType {
Warning = 0,
CriticalWarning = 1,
Parallelism = 2
}
}

View File

@@ -11,9 +11,10 @@ import * as azdata from 'azdata';
import { ComponentEventType, IComponent, IComponentDescriptor, IModelStore } from 'sql/platform/dashboard/browser/interfaces';
import { ILogService } from 'vs/platform/log/common/log';
import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase';
import { InfoBox, InfoBoxStyle } from 'sql/base/browser/ui/infoBox/infoBox';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { attachInfoBoxStyler } from 'sql/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InfoBox, InfoBoxStyle } from 'sql/workbench/browser/ui/infoBox/infoBox';
@Component({
selector: 'modelview-infobox',
@@ -31,7 +32,8 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IWorkbenchThemeService) private _themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(ILogService) logService: ILogService) {
super(changeRef, el, logService);
}
@@ -39,14 +41,20 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
ngAfterViewInit(): void {
this.baseInit();
if (this._container) {
this._infoBox = new InfoBox(this._container.nativeElement);
this._register(attachInfoBoxStyler(this._infoBox, this.themeService));
this._infoBox = this._instantiationService.createInstance(InfoBox, this._container.nativeElement, undefined);
this._register(attachInfoBoxStyler(this._infoBox, this._themeService));
this._infoBox.onDidClick(e => {
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: e
});
});
this._infoBox.onLinkClick(e => {
this.fireEvent({
eventType: ComponentEventType.onChildClick,
args: e
});
});
this.updateInfoBox();
}
}
@@ -70,6 +78,7 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
this._container.nativeElement.style.height = this.getHeight();
this._infoBox.announceText = this.announceText;
this._infoBox.infoBoxStyle = this.style;
this._infoBox.links = this.links;
this._infoBox.text = this.text;
this._infoBox.isClickable = this.isClickable;
this._infoBox.clickableButtonAriaLabel = this.clickableButtonAriaLabel;
@@ -95,4 +104,8 @@ export default class InfoBoxComponent extends ComponentBase<azdata.InfoBoxCompon
public get clickableButtonAriaLabel(): string {
return this.getPropertyOrDefault<string>((props) => props.clickableButtonAriaLabel, '');
}
public get links(): azdata.LinkArea[] {
return this.getPropertyOrDefault<azdata.LinkArea[]>((props) => props.links, []);
}
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/infoBox';
import * as azdata from 'azdata';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { alert, status } from 'vs/base/browser/ui/aria/aria';
import { IThemable } from 'vs/base/common/styler';
@@ -13,6 +14,8 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Codicon } from 'vs/base/common/codicons';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ILogService } from 'vs/platform/log/common/log';
export interface IInfoBoxStyles {
informationBackground?: Color;
@@ -25,6 +28,7 @@ export type InfoBoxStyle = 'information' | 'warning' | 'error' | 'success';
export interface InfoBoxOptions {
text: string;
links?: azdata.LinkArea[];
style: InfoBoxStyle;
announceText?: boolean;
isClickable?: boolean;
@@ -37,6 +41,7 @@ export class InfoBox extends Disposable implements IThemable {
private _infoBoxElement: HTMLDivElement;
private _clickableIndicator: HTMLDivElement;
private _text = '';
private _links: azdata.LinkArea[] = [];
private _infoBoxStyle: InfoBoxStyle = 'information';
private _styles: IInfoBoxStyles;
private _announceText: boolean = false;
@@ -47,7 +52,16 @@ export class InfoBox extends Disposable implements IThemable {
private _onDidClick: Emitter<void> = this._register(new Emitter<void>());
get onDidClick(): Event<void> { return this._onDidClick.event; }
constructor(container: HTMLElement, options?: InfoBoxOptions) {
private _linkListenersDisposableStore = new DisposableStore();
private _onLinkClick: Emitter<azdata.InfoBoxLinkClickEventArgs> = this._register(new Emitter<azdata.InfoBoxLinkClickEventArgs>());
get onLinkClick(): Event<azdata.InfoBoxLinkClickEventArgs> { return this._onLinkClick.event; }
constructor(
container: HTMLElement,
options: InfoBoxOptions | undefined,
@IOpenerService private _openerService: IOpenerService,
@ILogService private _logService: ILogService
) {
super();
this._infoBoxElement = document.createElement('div');
this._imageElement = document.createElement('div');
@@ -63,6 +77,7 @@ export class InfoBox extends Disposable implements IThemable {
if (options) {
this.infoBoxStyle = options.style;
this.links = options.links;
this.text = options.text;
this._announceText = (options.announceText === true);
this.isClickable = (options.isClickable === true);
@@ -98,6 +113,15 @@ export class InfoBox extends Disposable implements IThemable {
this.updateStyle();
}
public get links(): azdata.LinkArea[] {
return this._links;
}
public set links(v: azdata.LinkArea[]) {
this._links = v ?? [];
this.createTextWithHyperlinks();
}
public get text(): string {
return this._text;
}
@@ -105,16 +129,96 @@ export class InfoBox extends Disposable implements IThemable {
public set text(text: string) {
if (this._text !== text) {
this._text = text;
this._textElement.innerText = text;
if (this.announceText) {
if (this.infoBoxStyle === 'warning' || this.infoBoxStyle === 'error') {
alert(text);
}
else {
status(text);
this.createTextWithHyperlinks();
}
}
public createTextWithHyperlinks() {
let text = this._text;
DOM.clearNode(this._textElement);
this._linkListenersDisposableStore.clear();
for (let i = 0; i < this._links.length; i++) {
const placeholderIndex = text.indexOf(`{${i}}`);
if (placeholderIndex < 0) {
this._logService.warn(`Could not find placeholder text {${i}} in text ${text}`);
// Just continue on so we at least show the rest of the text if just one was missed or something
continue;
}
// First insert any text from the start of the current string fragment up to the placeholder
let curText = text.slice(0, placeholderIndex);
if (curText) {
const span = DOM.$('span');
span.innerText = text.slice(0, placeholderIndex);
this._textElement.appendChild(span);
}
// Now insert the link element
const link = this._links[i];
/**
* If the url is empty, electron displays the link as visited.
* TODO: Investigate why it happens and fix the issue iin electron/vsbase.
*/
const linkElement = DOM.$('a', {
href: link.url === '' ? ' ' : link.url
});
linkElement.innerText = link.text;
if (link.accessibilityInformation) {
linkElement.setAttribute('aria-label', link.accessibilityInformation.label);
if (link.accessibilityInformation.role) {
linkElement.setAttribute('role', link.accessibilityInformation.role);
}
}
this._linkListenersDisposableStore.add(DOM.addDisposableListener(linkElement, DOM.EventType.CLICK, e => {
this._onLinkClick.fire({
index: i,
link: link
});
if (link.url) {
this.openLink(link.url);
}
e.stopPropagation();
}));
this._linkListenersDisposableStore.add(DOM.addDisposableListener(linkElement, DOM.EventType.KEY_PRESS, e => {
const event = new StandardKeyboardEvent(e);
if (this._isClickable && (event.equals(KeyCode.Enter) || !event.equals(KeyCode.Space))) {
this._onLinkClick.fire({
index: i,
link: link
});
if (link.url) {
this.openLink(link.url);
}
e.stopPropagation();
}
}));
this._textElement.appendChild(linkElement);
text = text.slice(placeholderIndex + 3);
}
if (text) {
const span = DOM.$('span');
span.innerText = text;
this._textElement.appendChild(span);
}
if (this.announceText) {
if (this.infoBoxStyle === 'warning' || this.infoBoxStyle === 'error') {
alert(text);
}
else {
status(text);
}
}
}
private openLink(href: string): void {
this._openerService.open(href);
}
public get isClickable(): boolean {

View File

Before

Width:  |  Height:  |  Size: 414 B

After

Width:  |  Height:  |  Size: 414 B

View File

Before

Width:  |  Height:  |  Size: 625 B

After

Width:  |  Height:  |  Size: 625 B

View File

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 911 B

View File

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -5,9 +5,20 @@
import type * as azdata from 'azdata';
/**
* This class holds the view and the graphs of the execution plans
* displayed in the results tab of a query editor
*/
export class ExecutionPlanState {
graphs: azdata.executionPlan.ExecutionPlanGraph[] = [];
clearExecutionPlanState() {
this.graphs = [];
private _graphs: azdata.executionPlan.ExecutionPlanGraph[] = [];
public executionPlanFileViewUUID: string;
public get graphs(): azdata.executionPlan.ExecutionPlanGraph[] {
return this._graphs;
}
public set graphs(v: azdata.executionPlan.ExecutionPlanGraph[]) {
this._graphs = v;
}
}

View File

@@ -29,7 +29,6 @@ export class ResultsViewState {
this.gridPanelState.dispose();
this.chartState.dispose();
this.queryPlanState.dispose();
this.executionPlanState.clearExecutionPlanState();
this.dynamicModelViewTabsState.forEach((state: QueryModelViewState, identifier: string) => {
state.dispose();
});

View File

@@ -0,0 +1,520 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataGraphModule from 'azdataGraph';
import type * as azdata from 'azdata';
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { isString } from 'vs/base/common/types';
import { badgeIconPaths, executionPlanNodeIconPaths } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
const azdataGraph = azdataGraphModule();
/**
* This view holds the azdataGraph diagram and provides different
* methods to manipulate the azdataGraph
*/
export class AzdataGraphView {
private _diagram: any;
private _diagramModel: AzDataGraphCell;
private _uniqueElementId: number = -1;
private _graphElementPropertiesSet: Set<string> = new Set();
private _onElementSelectedEmitter: Emitter<InternalExecutionPlanElement> = new Emitter<InternalExecutionPlanElement>();
public onElementSelected: Event<InternalExecutionPlanElement>;
constructor(
private _parentContainer: HTMLElement,
private _executionPlan: azdata.executionPlan.ExecutionPlanGraph,
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,
) {
this._diagramModel = this.populate(this._executionPlan.root);
this._diagram = new azdataGraph.azdataQueryPlan(this._parentContainer, this._diagramModel, executionPlanNodeIconPaths, badgeIconPaths);
this.setGraphProperties();
this.initializeGraphEvents();
}
private setGraphProperties(): void {
this._diagram.graph.setCellsMovable(false); // preventing drag and drop of graph nodes.
this._diagram.graph.setCellsDisconnectable(false); // preventing graph edges to be disconnected from source and target nodes.
this._diagram.graph.tooltipHandler.delay = 700; // increasing delay for tooltips
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const iconBackground = theme.getColor(editorBackground);
if (iconBackground) {
this._diagram.setIconBackgroundColor(iconBackground);
}
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this._diagram.setTextFontColor(iconLabelColor);
this._diagram.setEdgeColor(iconLabelColor);
}
});
}
private initializeGraphEvents(): void {
this.onElementSelected = this._onElementSelectedEmitter.event;
this._diagram.graph.addListener('click', (sender, evt) => {
// Updating properties view table on node clicks
const cell = evt.properties['cell'];
let selectedGraphElement: InternalExecutionPlanElement;
if (cell) {
selectedGraphElement = this.getElementById(cell.id);
this.selectElement(cell.id);
} else if (!this.getSelectedElement()) {
selectedGraphElement = this._executionPlan.root;
this.selectElement(undefined);
}
this._onElementSelectedEmitter.fire(selectedGraphElement ?? this.getSelectedElement());
evt.consume();
});
}
/**
* Selects an execution plan node/edge in the graph diagram.
* @param element Element to be selected
* @param bringToCenter Check if the selected element has to be brought into the center of this view
*/
public selectElement(element: InternalExecutionPlanElement | undefined, bringToCenter: boolean = false): void {
let cell;
if (element) {
cell = this._diagram.graph.model.getCell(element.id);
} else {
cell = this._diagram.graph.model.getCell((<InternalExecutionPlanNode>this._executionPlan.root).id);
}
this._diagram.graph.getSelectionModel().setCell(cell);
if (bringToCenter) {
this.centerElement(element);
}
}
/**
* returns the currently selected graph element.
*/
public getSelectedElement(): InternalExecutionPlanElement | undefined {
const cell = this._diagram.graph.getSelectionCell();
if (cell?.id) {
return this.getElementById(cell.id);
}
return undefined;
}
/**
* Zooms in to the diagram.
*/
public zoomIn(): void {
this._diagram.zoomIn();
}
/**
* Zooms out of the diagram
*/
public zoomOut(): void {
this._diagram.zoomOut();
}
/**
* Fits the diagram into the parent container size.
*/
public zoomToFit(): void {
this._diagram.zoomToFit();
}
/**
* Gets the current zoom level of the diagram.
*/
public getZoomLevel(): number {
return this._diagram.graph.view.getScale() * 100;
}
/**
* Sets the zoom level of the diagram
* @param level The scale factor to be be applied to the diagram.
*/
public setZoomLevel(level: number): void {
if (level < 1) {
throw new Error(localize('invalidExecutionPlanZoomError', "Zoom level cannot be 0 or negative"));
}
this._diagram.zoomTo(level);
}
/**
* Get the diagram element by its id
* @param id id of the diagram element
*/
public getElementById(id: string): InternalExecutionPlanElement | undefined {
const nodeStack: InternalExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
const currentNode = nodeStack.pop();
if (currentNode.id === id) {
return currentNode;
}
if (currentNode.edges) {
for (let i = 0; i < currentNode.edges.length; i++) {
if ((<InternalExecutionPlanEdge>currentNode.edges[i]).id === id) {
return currentNode.edges[i];
}
}
}
nodeStack.push(...currentNode.children);
}
return undefined;
}
/**
* Searches the diagram nodes based on the search query provided.
*/
public searchNodes(searchQuery: SearchQuery): InternalExecutionPlanNode[] {
const resultNodes: InternalExecutionPlanNode[] = [];
const nodeStack: InternalExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
const currentNode = nodeStack.pop();
const matchingProp = currentNode.properties.find(e => e.name === searchQuery.propertyName);
let matchFound = false;
// Searching only properties with string value.
if (isString(matchingProp?.value)) {
// If the search type is '=' we look for exact match and for 'contains' we look search string occurrences in prop value
switch (searchQuery.searchType) {
case SearchType.Equals:
matchFound = matchingProp.value === searchQuery.value;
break;
case SearchType.Contains:
matchFound = matchingProp.value.includes(searchQuery.value);
break;
case SearchType.GreaterThan:
matchFound = matchingProp.value > searchQuery.value;
break;
case SearchType.LesserThan:
matchFound = matchingProp.value < searchQuery.value;
break;
case SearchType.GreaterThanEqualTo:
matchFound = matchingProp.value >= searchQuery.value;
break;
case SearchType.LesserThanEqualTo:
matchFound = matchingProp.value <= searchQuery.value;
break;
case SearchType.LesserAndGreaterThan:
matchFound = matchingProp.value < searchQuery.value || matchingProp.value > searchQuery.value;
break;
}
if (matchFound) {
resultNodes.push(currentNode);
}
}
nodeStack.push(...currentNode.children);
}
return resultNodes;
}
/**
* Brings a graph element to the center of the parent view.
* @param node Node to be brought into the center
*/
public centerElement(node: InternalExecutionPlanElement): void {
/**
* The selected graph node might be hidden/partially visible if the graph is overflowing the parent container.
* Apart from the obvious problems in aesthetics, user do not get a proper feedback of the search result.
* To solve this problem, we will have to scroll the node into view. (preferably into the center of the view)
* Steps for that:
* 1. Get the bounding rect of the node on graph.
* 2. Get the midpoint of the node's bounding rect.
* 3. Find the dimensions of the parent container.
* 4. Since, we are trying to position the node into center, we set the left top corner position of parent to
* below x and y.
* x = node's x midpoint - half the width of parent container
* y = node's y midpoint - half the height of parent container
* 5. If the x and y are negative, we set them 0 as that is the minimum possible scroll position.
* 6. Smoothly scroll to the left top x and y calculated in step 4, 5.
*/
if (!node) {
return;
}
const cell = this._diagram.graph.model.getCell(node.id);
if (!cell) {
return;
}
this._diagram.graph.setSelectionCell(cell);
const cellRect = this._diagram.graph.getCellBounds(cell);
const cellMidPoint: Point = {
x: cellRect.x + cellRect.width / 2,
y: cellRect.y + cellRect.height / 2
};
const graphContainer = <HTMLElement>this._diagram.graph.container;
const diagramContainerRect = graphContainer.getBoundingClientRect();
const leftTopScrollPoint: Point = {
x: cellMidPoint.x - diagramContainerRect.width / 2,
y: cellMidPoint.y - diagramContainerRect.height / 2
};
leftTopScrollPoint.x = leftTopScrollPoint.x < 0 ? 0 : leftTopScrollPoint.x;
leftTopScrollPoint.y = leftTopScrollPoint.y < 0 ? 0 : leftTopScrollPoint.y;
graphContainer.scrollTo({
left: leftTopScrollPoint.x,
top: leftTopScrollPoint.y,
behavior: 'smooth'
});
}
private populate(node: InternalExecutionPlanNode): AzDataGraphCell {
let diagramNode: AzDataGraphCell = <AzDataGraphCell>{};
diagramNode.label = node.subtext.join(this.textResourcePropertiesService.getEOL(undefined));
diagramNode.tooltipTitle = node.name;
const nodeId = this.createGraphElementId();
diagramNode.id = nodeId;
node.id = nodeId;
if (node.type) {
diagramNode.icon = node.type;
}
if (node.properties) {
diagramNode.metrics = this.populateProperties(node.properties);
}
if (node.badges) {
diagramNode.badges = [];
for (let i = 0; i < node.badges.length; i++) {
diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type));
}
}
if (node.edges) {
diagramNode.edges = this.populateEdges(node.edges);
}
if (node.children) {
diagramNode.children = [];
for (let i = 0; i < node.children.length; ++i) {
diagramNode.children.push(this.populate(node.children[i]));
}
}
if (node.description) {
diagramNode.description = node.description;
}
return diagramNode;
}
private getBadgeTypeString(badgeType: sqlExtHostType.executionPlan.BadgeType): {
type: string,
tooltip: string
} | undefined {
/**
* TODO: Need to figure out if tooltip have to be removed. For now, they are empty
*/
switch (badgeType) {
case sqlExtHostType.executionPlan.BadgeType.Warning:
return {
type: 'warning',
tooltip: ''
};
case sqlExtHostType.executionPlan.BadgeType.CriticalWarning:
return {
type: 'criticalWarning',
tooltip: ''
};
case sqlExtHostType.executionPlan.BadgeType.Parallelism:
return {
type: 'parallelism',
tooltip: ''
};
default:
return undefined;
}
}
private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): AzDataGraphCellMetric[] {
props.forEach(p => {
this._graphElementPropertiesSet.add(p.name);
});
return props.filter(e => isString(e.displayValue) && e.showInTooltip)
.sort((a, b) => a.displayOrder - b.displayOrder)
.map(e => {
return {
name: e.name,
value: e.displayValue,
isLongString: e.positionAtBottom
};
});
}
private populateEdges(edges: InternalExecutionPlanEdge[]): AzDataGraphCellEdge[] {
return edges.map(e => {
e.id = this.createGraphElementId();
return {
id: e.id,
metrics: this.populateProperties(e.properties),
weight: Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(e.rowCount), 6)),
label: ''
};
});
}
private createGraphElementId(): string {
this._uniqueElementId += 1;
return `element-${this._uniqueElementId}`;
}
/**
* Gets a list of unique properties of the graph elements.
*/
public getUniqueElementProperties(): string[] {
return [...this._graphElementPropertiesSet].sort();
}
/**
* Enables/Disables the graph tooltips
* @returns state of the tooltip after toggling
*/
public toggleTooltip(): boolean {
if (this._diagram.graph.tooltipHandler.enabled) {
this._diagram.graph.tooltipHandler.setEnabled(false);
} else {
this._diagram.graph.tooltipHandler.setEnabled(true);
}
return this._diagram.graph.tooltipHandler.enabled;
}
}
export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode {
/**
* Unique internal id given to graph node by ADS.
*/
id?: string;
}
export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge {
/**
* Unique internal id given to graph edge by ADS.
*/
id?: string;
}
export type InternalExecutionPlanElement = InternalExecutionPlanEdge | InternalExecutionPlanNode;
export interface AzDataGraphCell {
/**
* Label for the azdata cell
*/
label: string;
/**
* unique identifier for the cell
*/
id: string;
/**
* icon for the cell
*/
icon: string;
/**
* title for the cell hover tooltip
*/
tooltipTitle: string;
/**
* metrics to be shown in the tooltip
*/
metrics: AzDataGraphCellMetric[];
/**
* cell edges
*/
edges: AzDataGraphCellEdge[];
/**
* child cells
*/
children: AzDataGraphCell[];
/**
* Description to be displayed in the cell tooltip
*/
description: string;
badges: AzDataGraphNodeBadge[];
}
export interface AzDataGraphNodeBadge {
type: string;
tooltip: string;
}
export interface AzDataGraphCellMetric {
/**
* name of the metric
*/
name: string;
/**
* display value of the metric
*/
value: string;
/**
* flag that indicates if the display property is a long string
* long strings will be displayed at the bottom
*/
isLongString: boolean;
}
export interface AzDataGraphCellEdge {
/**
* Label for the edge
*/
label: string;
/**
* Unique identifier for the edge
*/
id: string;
/**
* weight of the edge. This value determines the edge thickness
*/
weight: number;
/**
* metrics to be shown in the edge tooltip
*/
metrics: AzDataGraphCellMetric[];
}
interface Point {
x: number;
y: number;
}
export enum SearchType {
Equals,
Contains,
LesserThan,
GreaterThan,
GreaterThanEqualTo,
LesserThanEqualTo,
LesserAndGreaterThan
}
export interface SearchQuery {
/**
* property name to be searched
*/
propertyName: string,
/**
* expected value of the property
*/
value: string,
/**
* Type of search to be performed
*/
searchType: SearchType
}

View File

@@ -249,17 +249,27 @@ export let executionPlanNodeIconPaths =
unionAll: imageBasePath + 'union_all.png'
};
const parentContainer = 'qps-container';
export const savePlanIconClassNames = [parentContainer, 'save-plan-icon'].join(' ');
export const openPropertiesIconClassNames = [parentContainer, 'open-properties-icon'].join(' ');
export const openQueryIconClassNames = [parentContainer, 'open-query-icon'].join(' ');
export const openPlanFileIconClassNames = [parentContainer, 'open-plan-file-icon'].join(' ');
export const saveIconClassNames = [parentContainer, 'save-icon'].join(' ');
export const searchIconClassNames = [parentContainer, 'search-icon'].join(' ');
export const sortAlphabeticallyIconClassNames = [parentContainer, 'sort-alphabetically-icon'].join(' ');
export const sortByDisplayOrderIconClassNames = [parentContainer, 'sort-display-order-icon'].join(' ');
export const zoomInIconClassNames = [parentContainer, 'zoom-in-icon'].join(' ');
export const zoomOutIconClassNames = [parentContainer, 'zoom-out-icon'].join(' ');
export const customZoomIconClassNames = [parentContainer, 'custom-zoom-icon'].join(' ');
export const zoomToFitIconClassNames = [parentContainer, 'zoom-to-fit-icon'].join(' ');
export const zoomIconClassNames = [parentContainer, 'zoom-icon'].join(' ');
export const badgeIconPaths = {
warning: imageBasePath + 'badge_warning.svg',
parallelism: imageBasePath + 'badge_parallelism.svg',
criticalWarning: imageBasePath + 'badge_critical_warning.svg'
};
export const savePlanIconClassNames = 'ep-save-plan-icon';
export const openPropertiesIconClassNames = 'ep-open-properties-icon';
export const openQueryIconClassNames = 'ep-open-query-icon';
export const openPlanFileIconClassNames = 'ep-open-plan-file-icon';
export const saveIconClassNames = 'ep-save-icon';
export const searchIconClassNames = 'ep-search-icon';
export const sortAlphabeticallyIconClassNames = 'ep-sort-alphabetically-icon';
export const sortReverseAlphabeticallyIconClassNames = 'ep-sort-reverse-alphabetically-icon';
export const sortByDisplayOrderIconClassNames = 'ep-sort-display-order-icon';
export const zoomInIconClassNames = 'ep-zoom-in-icon';
export const zoomOutIconClassNames = 'ep-zoom-out-icon';
export const customZoomIconClassNames = 'ep-custom-zoom-icon';
export const zoomToFitIconClassNames = 'ep-zoom-to-fit-icon';
export const zoomIconClassNames = 'ep-zoom-icon';
export const enableTooltipIconClassName = 'ep-enable-tooltip-icon';
export const disableTooltipIconClassName = 'ep-disable-tooltip-icon';

View File

@@ -1,708 +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 'vs/css!./media/executionPlan';
import type * as azdata from 'azdata';
import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { localize } from 'vs/nls';
import { dispose } from 'vs/base/common/lifecycle';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import * as azdataGraphModule from 'azdataGraph';
import { customZoomIconClassNames, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, executionPlanNodeIconPaths, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { isString } from 'vs/base/common/types';
import { PlanHeader } from 'sql/workbench/contrib/executionPlan/browser/planHeader';
import { ExecutionPlanPropertiesView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView';
import { Action } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { contrastBorder, editorBackground, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { InfoBox } from 'sql/base/browser/ui/infoBox/infoBox';
let azdataGraph = azdataGraphModule();
export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode {
/**
* Unique internal id given to graph node by ADS.
*/
id?: string;
}
export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge {
/**
* Unique internal id given to graph edge by ADS.
*/
id?: string;
}
export class ExecutionPlanTab implements IPanelTab {
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
public readonly identifier = 'ExecutionPlan2Tab';
public readonly view: ExecutionPlanView;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
) {
this.view = instantiationService.createInstance(ExecutionPlanView);
}
public dispose() {
dispose(this.view);
}
public clear() {
this.view.clear();
}
}
export class ExecutionPlanView implements IPanelView {
private _loadingSpinner: LoadingSpinner;
private _loadingErrorInfoBox: InfoBox;
private _eps?: ExecutionPlan[] = [];
private _graphs?: azdata.executionPlan.ExecutionPlanGraph[] = [];
private _container = DOM.$('.eps-container');
private _planCache: Map<string, azdata.executionPlan.ExecutionPlanGraph[]> = new Map();
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@IExecutionPlanService private executionPlanService: IExecutionPlanService
) {
}
public render(parent: HTMLElement): void {
parent.appendChild(this._container);
}
dispose() {
this._container.remove();
delete this._eps;
delete this._graphs;
}
public layout(dimension: DOM.Dimension): void {
}
public clear() {
this._eps = [];
this._graphs = [];
DOM.clearNode(this._container);
}
/**
* Adds executionPlanGraph to the graph controller.
* @param newGraphs ExecutionPlanGraphs to be added.
*/
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
if (newGraphs) {
newGraphs.forEach(g => {
const ep = this.instantiationService.createInstance(ExecutionPlan, this._container, this._eps.length + 1);
ep.graphModel = g;
this._eps.push(ep);
this._graphs.push(g);
this.updateRelativeCosts();
});
}
}
/**
* Loads the graph file by converting the file to generic executionPlan graphs.
* This feature requires the right providers to be registered that can handle
* the graphFileType in the graphFile
* Please note: this method clears the existing graph in the graph control
* @param graphFile graph file to be loaded.
* @returns
*/
public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) {
this.clear();
this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true });
this._loadingSpinner.loadingMessage = localize('loadingExecutionPlanFile', "Generating execution plans");
try {
this._loadingSpinner.loading = true;
if (this._planCache.has(graphFile.graphFileContent)) {
this.addGraphs(this._planCache.get(graphFile.graphFileContent));
return;
} else {
const graphs = (await this.executionPlanService.getExecutionPlan({
graphFileContent: graphFile.graphFileContent,
graphFileType: graphFile.graphFileType
})).graphs;
this.addGraphs(graphs);
this._planCache.set(graphFile.graphFileContent, graphs);
}
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated");
} catch (e) {
this._loadingErrorInfoBox = new InfoBox(this._container, {
text: e.toString(),
style: 'error',
isClickable: false
});
this._loadingErrorInfoBox.isClickable = false;
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan");
} finally {
this._loadingSpinner.loading = false;
}
}
private updateRelativeCosts() {
const sum = this._graphs.reduce((prevCost: number, cg) => {
return prevCost += cg.root.subTreeCost + cg.root.cost;
}, 0);
if (sum > 0) {
this._eps.forEach(ep => {
ep.planHeader.relativeCost = ((ep.graphModel.root.subTreeCost + ep.graphModel.root.cost) / sum) * 100;
});
}
}
}
export class ExecutionPlan implements ISashLayoutProvider {
private _graphModel?: azdata.executionPlan.ExecutionPlanGraph;
private _container: HTMLElement;
private _actionBarContainer: HTMLElement;
private _actionBar: ActionBar;
public planHeader: PlanHeader;
private _planContainer: HTMLElement;
private _planHeaderContainer: HTMLElement;
public propertiesView: ExecutionPlanPropertiesView;
private _propContainer: HTMLElement;
private _planActionContainer: HTMLElement;
public planActionView: ExecutionPlanWidgetController;
public azdataGraphDiagram: any;
public graphElementPropertiesSet: Set<string> = new Set();
private uniqueElementId: number = -1;
constructor(
private _parent: HTMLElement,
private _graphIndex: number,
@IInstantiationService public readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IContextViewService public readonly contextViewService: IContextViewService,
@IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService,
@IEditorService private readonly editorService: IEditorService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IFileDialogService public fileDialogService: IFileDialogService,
@IFileService public fileService: IFileService,
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,
) {
// parent container for query plan.
this._container = DOM.$('.execution-plan');
this._parent.appendChild(this._container);
const sashContainer = DOM.$('.execution-plan-sash');
this._parent.appendChild(sashContainer);
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL });
let originalHeight = this._container.offsetHeight;
let originalTableHeight = 0;
let change = 0;
sash.onDidStart((e: ISashEvent) => {
originalHeight = this._container.offsetHeight;
originalTableHeight = this.propertiesView.tableHeight;
});
/**
* Using onDidChange for the smooth resizing of the graph diagram
*/
sash.onDidChange((evt: ISashEvent) => {
change = evt.startY - evt.currentY;
const newHeight = originalHeight - change;
if (newHeight < 200) {
return;
}
/**
* Since the parent container is flex, we will have
* to change the flex-basis property to change the height.
*/
this._container.style.minHeight = '200px';
this._container.style.flex = `0 0 ${newHeight}px`;
});
/**
* Resizing properties window table only once at the end as it is a heavy operation and worsens the smooth resizing experience
*/
sash.onDidEnd(() => {
this.propertiesView.tableHeight = originalTableHeight - change;
});
this._planContainer = DOM.$('.plan');
this._container.appendChild(this._planContainer);
// container that holds plan header info
this._planHeaderContainer = DOM.$('.header');
// Styling header text like the query editor
this._planHeaderContainer.style.fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
this._planHeaderContainer.style.fontSize = EDITOR_FONT_DEFAULTS.fontSize.toString();
this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight;
this._planContainer.appendChild(this._planHeaderContainer);
this.planHeader = this._instantiationService.createInstance(PlanHeader, this._planHeaderContainer, {
planIndex: this._graphIndex,
});
// container properties
this._propContainer = DOM.$('.properties');
this._container.appendChild(this._propContainer);
this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService);
this._planActionContainer = DOM.$('.plan-action-container');
this._planContainer.appendChild(this._planActionContainer);
this.planActionView = new ExecutionPlanWidgetController(this._planActionContainer);
// container that holds actionbar icons
this._actionBarContainer = DOM.$('.action-bar-container');
this._container.appendChild(this._actionBarContainer);
this._actionBar = new ActionBar(this._actionBarContainer, {
orientation: ActionsOrientation.VERTICAL, context: this
});
const actions = [
new SavePlanFile(),
new OpenPlanFile(),
new OpenQueryAction(),
new SearchNodeAction(),
new ZoomInAction(),
new ZoomOutAction(),
new ZoomToFitAction(),
new CustomZoomAction(),
new PropertiesAction(),
];
this._actionBar.pushAction(actions, { icon: true, label: false });
// Setting up context menu
const self = this;
this._container.oncontextmenu = (e: MouseEvent) => {
if (actions) {
this._contextMenuService.showContextMenu({
getAnchor: () => {
return {
x: e.x,
y: e.y
};
},
getActions: () => actions,
getActionsContext: () => (self)
});
}
};
}
getHorizontalSashTop(sash: Sash): number {
return 0;
}
getHorizontalSashLeft?(sash: Sash): number {
return 0;
}
getHorizontalSashWidth?(sash: Sash): number {
return this._container.clientWidth;
}
private populate(node: InternalExecutionPlanNode, diagramNode: any): any {
diagramNode.label = node.subtext.join(this.textResourcePropertiesService.getEOL(undefined));
diagramNode.tooltipTitle = node.name;
const nodeId = this.createGraphElementId();
diagramNode.id = nodeId;
node.id = nodeId;
if (node.properties && node.properties.length > 0) {
diagramNode.metrics = this.populateProperties(node.properties);
}
if (node.type) {
diagramNode.icon = node.type;
}
if (node.edges) {
diagramNode.edges = [];
for (let i = 0; i < node.edges.length; i++) {
diagramNode.edges.push(this.populateEdges(node.edges[i], new Object()));
}
}
if (node.children) {
diagramNode.children = [];
for (let i = 0; i < node.children.length; ++i) {
diagramNode.children.push(this.populate(node.children[i], new Object()));
}
}
if (node.description) {
diagramNode.description = node.description;
}
return diagramNode;
}
private populateEdges(edge: InternalExecutionPlanEdge, diagramEdge: any) {
diagramEdge.label = '';
const edgeId = this.createGraphElementId();
diagramEdge.id = edgeId;
edge.id = edgeId;
diagramEdge.metrics = this.populateProperties(edge.properties);
diagramEdge.weight = Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(edge.rowCount), 6));
return diagramEdge;
}
private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]) {
return props.filter(e => isString(e.displayValue) && e.showInTooltip)
.sort((a, b) => a.displayOrder - b.displayOrder)
.map(e => {
this.graphElementPropertiesSet.add(e.name);
return {
name: e.name,
value: e.displayValue,
isLongString: e.positionAtBottom
};
});
}
private createGraphElementId(): string {
this.uniqueElementId += 1;
return `element-${this.uniqueElementId}`;
}
private createPlanDiagram(container: HTMLElement) {
let diagramRoot: any = new Object();
let graphRoot: azdata.executionPlan.ExecutionPlanNode = this._graphModel.root;
this.populate(graphRoot, diagramRoot);
this.azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, executionPlanNodeIconPaths);
this.azdataGraphDiagram.graph.setCellsMovable(false); // preventing drag and drop of graph nodes.
this.azdataGraphDiagram.graph.setCellsDisconnectable(false); // preventing graph edges to be disconnected from source and target nodes.
this.azdataGraphDiagram.graph.addListener('click', (sender, evt) => {
// Updating properties view table on node clicks
const cell = evt.properties['cell'];
if (cell) {
this.propertiesView.graphElement = this.searchNodes(cell.id);
} else if (!this.azdataGraphDiagram.graph.getSelectionCell()) {
const root = this.azdataGraphDiagram.graph.model.getCell(diagramRoot.id);
this.azdataGraphDiagram.graph.getSelectionModel().setCell(root);
this.propertiesView.graphElement = this.searchNodes(diagramRoot.id);
evt.consume();
} else {
evt.consume();
}
});
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const iconBackground = theme.getColor(editorBackground);
if (iconBackground) {
this.azdataGraphDiagram.setIconBackgroundColor(iconBackground);
}
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this.azdataGraphDiagram.setTextFontColor(iconLabelColor);
this.azdataGraphDiagram.setEdgeColor(iconLabelColor);
}
});
}
public set graphModel(graph: azdata.executionPlan.ExecutionPlanGraph | undefined) {
this._graphModel = graph;
if (this._graphModel) {
this.planHeader.graphIndex = this._graphIndex;
this.planHeader.query = graph.query;
if (graph.recommendations) {
this.planHeader.recommendations = graph.recommendations;
}
let diagramContainer = DOM.$('.diagram');
this.createPlanDiagram(diagramContainer);
/**
* We do not want to scroll the diagram through mouse wheel.
* Instead, we pass this event to parent control. So, when user
* uses the scroll wheel, they scroll through graphs present in
* the graph control. To scroll the individual graphs, users should
* use the scroll bars.
*/
diagramContainer.addEventListener('wheel', e => {
this._parent.scrollTop += e.deltaY;
//Hiding all tooltips when we scroll.
const element = document.getElementsByClassName('mxTooltip');
for (let i = 0; i < element.length; i++) {
(<HTMLElement>element[i]).style.visibility = 'hidden';
}
e.preventDefault();
e.stopPropagation();
});
this._planContainer.appendChild(diagramContainer);
this.propertiesView.graphElement = this._graphModel.root;
}
}
public get graphModel(): azdata.executionPlan.ExecutionPlanGraph | undefined {
return this._graphModel;
}
public openQuery() {
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.graphModel.query, RunQueryOnConnectionMode.none).then();
}
public async openGraphFile() {
const input = this._untitledEditorService.create({ mode: this.graphModel.graphFile.graphFileType, initialValue: this.graphModel.graphFile.graphFileContent });
await input.resolve();
await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None);
input.setDirty(false);
this.editorService.openEditor(input);
}
public searchNodes(searchId: string): InternalExecutionPlanNode | InternalExecutionPlanEdge | undefined {
let stack: InternalExecutionPlanNode[] = [];
stack.push(this._graphModel.root);
while (stack.length !== 0) {
const currentNode = stack.pop();
if (currentNode.id === searchId) {
return currentNode;
}
stack.push(...currentNode.children);
const resultEdge = currentNode.edges.find(e => (<InternalExecutionPlanEdge>e).id === searchId);
if (resultEdge) {
return resultEdge;
}
}
return undefined;
}
}
class OpenQueryAction extends Action {
public static ID = 'ep.OpenQueryAction';
public static LABEL = localize('openQueryAction', "Open Query");
constructor() {
super(OpenQueryAction.ID, OpenQueryAction.LABEL, openQueryIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.openQuery();
}
}
class PropertiesAction extends Action {
public static ID = 'ep.propertiesAction';
public static LABEL = localize('executionPlanPropertiesActionLabel', "Properties");
constructor() {
super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.propertiesView.toggleVisibility();
}
}
class ZoomInAction extends Action {
public static ID = 'ep.ZoomInAction';
public static LABEL = localize('executionPlanZoomInActionLabel', "Zoom In");
constructor() {
super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.azdataGraphDiagram.graph.zoomIn();
}
}
class ZoomOutAction extends Action {
public static ID = 'ep.ZoomOutAction';
public static LABEL = localize('executionPlanZoomOutActionLabel', "Zoom Out");
constructor() {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.azdataGraphDiagram.graph.zoomOut();
}
}
class ZoomToFitAction extends Action {
public static ID = 'ep.FitGraph';
public static LABEL = localize('executionPlanFitGraphLabel', "Zoom to fit");
constructor() {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.azdataGraphDiagram.graph.fit();
context.azdataGraphDiagram.graph.view.rendering = true;
context.azdataGraphDiagram.graph.refresh();
}
}
class SavePlanFile extends Action {
public static ID = 'ep.saveXML';
public static LABEL = localize('executionPlanSavePlanXML', "Save Plan File");
constructor() {
super(SavePlanFile.ID, SavePlanFile.LABEL, savePlanIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
const workspaceFolders = await context.workspaceContextService.getWorkspace().folders;
const defaultFileName = 'plan';
let currentWorkSpaceFolder: URI;
if (workspaceFolders.length !== 0) {
currentWorkSpaceFolder = workspaceFolders[0].uri;
currentWorkSpaceFolder = URI.joinPath(currentWorkSpaceFolder, defaultFileName); //appending default file name to workspace uri
} else {
currentWorkSpaceFolder = URI.parse(defaultFileName); // giving default name
}
const saveFileUri = await context.fileDialogService.showSaveDialog({
filters: [
{
extensions: ['sqlplan'], //TODO: Get this extension from provider
name: localize('executionPlan.SaveFileDescription', 'Execution Plan Files') //TODO: Get the names from providers.
}
],
defaultUri: currentWorkSpaceFolder // If no workspaces are opened this will be undefined
});
if (saveFileUri) {
await context.fileService.writeFile(saveFileUri, VSBuffer.fromString(context.graphModel.graphFile.graphFileContent));
}
}
}
class CustomZoomAction extends Action {
public static ID = 'ep.customZoom';
public static LABEL = localize('executionPlanCustomZoom', "Custom Zoom");
constructor() {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, customZoomIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.planActionView.toggleWidget(context._instantiationService.createInstance(CustomZoomWidget, context));
}
}
class SearchNodeAction extends Action {
public static ID = 'ep.searchNode';
public static LABEL = localize('executionPlanSearchNodeAction', "Find Node");
constructor() {
super(SearchNodeAction.ID, SearchNodeAction.LABEL, searchIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.planActionView.toggleWidget(context._instantiationService.createInstance(NodeSearchWidget, context));
}
}
class OpenPlanFile extends Action {
public static ID = 'ep.openGraphFile';
public static Label = localize('executionPlanOpenGraphFile', "Show Query Plan XML"); //TODO: add a contribution point for providers to set this text
constructor() {
super(OpenPlanFile.ID, OpenPlanFile.Label, openPlanFileIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
await context.openGraphFile();
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const recommendationsColor = theme.getColor(textLinkForeground);
if (recommendationsColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header .recommendations {
color: ${recommendationsColor};
}
`);
}
const shadow = theme.getColor(widgetShadow);
if (shadow) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child {
box-shadow: 0 0 8px 2px ${shadow};
}
`);
}
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar {
background-color: ${menuBackgroundColor};
}
`);
}
const widgetBackgroundColor = theme.getColor(editorWidgetBackground);
if (widgetBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.mxTooltip {
background-color: ${widgetBackgroundColor};
}
`);
}
const widgetBorderColor = theme.getColor(contrastBorder);
if (widgetBorderColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar,
.mxTooltip {
border: 1px solid ${widgetBorderColor};
}
`);
}
const textColor = theme.getColor(foreground);
if (textColor) {
collector.addRule(`
.mxTooltip {
color: ${textColor};
}
`);
}
});

View File

@@ -10,55 +10,74 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { ExecutionPlanView } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
import { generateUuid } from 'vs/base/common/uuid';
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
export class ExecutionPlanEditor extends EditorPane {
public static ID: string = 'workbench.editor.executionplan';
public static LABEL: string = localize('executionPlanEditor', "Query Execution Plan Editor");
private view: ExecutionPlanView;
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
private _parentContainer: HTMLElement;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService private _instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
) {
super(ExecutionPlanEditor.ID, telemetryService, themeService, storageService);
this.view = this._register(instantiationService.createInstance(ExecutionPlanView));
}
/**
* Called to create the editor in the parent element.
*/
public createEditor(parent: HTMLElement): void {
this._parentContainer = parent;
//Enable scrollbars when drawing area is larger than viewport
parent.style.overflow = 'auto';
this.view.render(parent);
}
/**
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
* To be called when the container of this editor changes size.
*/
public layout(dimension: DOM.Dimension): void {
this.view.layout(dimension);
}
public override async setInput(input: ExecutionPlanInput, options: IEditorOptions, context: IEditorOpenContext): Promise<void> {
if (this.input instanceof ExecutionPlanInput && this.input.matches(input)) {
return Promise.resolve(undefined);
public override async setInput(newInput: ExecutionPlanInput, options: IEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
const oldInput = this.input as ExecutionPlanInput;
// returning if the new input is same as old input
if (oldInput && newInput.matches(oldInput)) {
return Promise.resolve();
}
super.setInput(newInput, options, context, token);
// clearing old input view if present in the editor
if (oldInput?._executionPlanFileViewUUID) {
const oldView = this._viewCache.executionPlanFileViewMap.get(oldInput._executionPlanFileViewUUID);
oldView.onHide(this._parentContainer);
}
// if new input already has a view we are just making it visible here.
let newView = this._viewCache.executionPlanFileViewMap.get(newInput.executionPlanFileViewUUID);
if (newView) {
newView.onShow(this._parentContainer);
} else {
// creating a new view for the new input
newInput._executionPlanFileViewUUID = generateUuid();
newView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView));
newView.onShow(this._parentContainer);
newView.loadGraphFile({
graphFileContent: await newInput.content(),
graphFileType: newInput.getFileExtension().replace('.', '')
});
this._viewCache.executionPlanFileViewMap.set(newInput._executionPlanFileViewUUID, newView);
}
await input.resolve();
await super.setInput(input, options, context, CancellationToken.None);
this.view.loadGraphFile({
graphFileContent: input.content,
graphFileType: input.getFileExtension().replace('.', '')
});
}
}

View File

@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as azdata from 'azdata';
import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { ExecutionPlanView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanView';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
export class ExecutionPlanFileView {
private _parent: HTMLElement;
private _loadingSpinner: LoadingSpinner;
private _loadingErrorInfoBox: InfoBox;
private _executionPlanViews: ExecutionPlanView[] = [];
private _graphs?: azdata.executionPlan.ExecutionPlanGraph[] = [];
private _container = DOM.$('.eps-container');
private _planCache: Map<string, azdata.executionPlan.ExecutionPlanGraph[]> = new Map();
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@IExecutionPlanService private executionPlanService: IExecutionPlanService
) {
}
public render(parent: HTMLElement): void {
this._parent = parent;
this._parent.appendChild(this._container);
}
public onShow(parentContainer: HTMLElement): void {
this._parent = parentContainer;
this._parent.appendChild(this._container);
}
public onHide(parentContainer: HTMLElement): void {
if (parentContainer === this._parent && parentContainer.contains(this._container)) {
this._parent.removeChild(this._container);
}
}
dispose() {
}
/**
* Adds executionPlanGraph to the graph controller.
* @param newGraphs ExecutionPlanGraphs to be added.
*/
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
if (newGraphs) {
newGraphs.forEach(g => {
const ep = this.instantiationService.createInstance(ExecutionPlanView, this._container, this._executionPlanViews.length + 1);
ep.model = g;
this._executionPlanViews.push(ep);
this._graphs.push(g);
this.updateRelativeCosts();
});
}
}
/**
* Loads the graph file by converting the file to generic executionPlan graphs.
* This feature requires the right providers to be registered that can handle
* the graphFileType in the graphFile
* Please note: this method clears the existing graph in the graph control
* @param graphFile graph file to be loaded.
* @returns
*/
public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) {
this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true });
this._loadingSpinner.loadingMessage = localize('loadingExecutionPlanFile', "Generating execution plans");
try {
this._loadingSpinner.loading = true;
if (this._planCache.has(graphFile.graphFileContent)) {
this.addGraphs(this._planCache.get(graphFile.graphFileContent));
return;
} else {
const graphs = (await this.executionPlanService.getExecutionPlan({
graphFileContent: graphFile.graphFileContent,
graphFileType: graphFile.graphFileType
})).graphs;
this.addGraphs(graphs);
this._planCache.set(graphFile.graphFileContent, graphs);
}
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated");
} catch (e) {
this._loadingErrorInfoBox = this.instantiationService.createInstance(InfoBox, this._container, {
text: e.toString(),
style: 'error',
isClickable: false
});
this._loadingErrorInfoBox.isClickable = false;
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan");
} finally {
this._loadingSpinner.loading = false;
}
}
private updateRelativeCosts() {
const sum = this._graphs.reduce((prevCost: number, cg) => {
return prevCost += cg.root.subTreeCost + cg.root.cost;
}, 0);
if (sum > 0) {
this._executionPlanViews.forEach(ep => {
ep.planHeader.relativeCost = ((ep.model.root.subTreeCost + ep.model.root.cost) / sum) * 100;
});
}
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const recommendationsColor = theme.getColor(textLinkForeground);
if (recommendationsColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header .recommendations {
color: ${recommendationsColor};
}
`);
}
const shadow = theme.getColor(widgetShadow);
if (shadow) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child {
box-shadow: 0 0 8px 2px ${shadow};
}
`);
}
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar {
background-color: ${menuBackgroundColor};
}
`);
}
const widgetBackgroundColor = theme.getColor(editorWidgetBackground);
if (widgetBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.mxTooltip {
background-color: ${widgetBackgroundColor};
}
`);
}
const widgetBorderColor = theme.getColor(contrastBorder);
if (widgetBorderColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar,
.mxTooltip {
border: 1px solid ${widgetBorderColor};
}
`);
}
const textColor = theme.getColor(foreground);
if (textColor) {
collector.addRule(`
.mxTooltip {
color: ${textColor};
}
`);
}
});

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
export class ExecutionPlanFileViewCache {
private static instance: ExecutionPlanFileViewCache;
public executionPlanFileViewMap: Map<string, ExecutionPlanFileView> = new Map();
private constructor() { }
public static getInstance(): ExecutionPlanFileViewCache {
if (!ExecutionPlanFileViewCache.instance) {
ExecutionPlanFileViewCache.instance = new ExecutionPlanFileViewCache();
}
return ExecutionPlanFileViewCache.instance;
}
}

View File

@@ -6,89 +6,84 @@
import * as DOM from 'vs/base/browser/dom';
import type * as azdata from 'azdata';
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { Table } from 'sql/base/browser/ui/table/table';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { removeLineBreaks } from 'sql/base/common/strings';
import { isString } from 'vs/base/common/types';
import { sortAlphabeticallyIconClassNames, sortByDisplayOrderIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { textFormatter } from 'sql/base/browser/ui/table/formatters';
import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase';
export class ExecutionPlanPropertiesView {
// Title bar with close button action
private _propertiesTitle!: HTMLElement;
private _titleText!: HTMLElement;
private _titleActionBarContainer!: HTMLElement;
private _titleActionBar: ActionBar;
export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase {
// Div that holds the name of the element selected
private _operationName!: HTMLElement;
// Action bar that contains sorting option for the table
private _tableActionBarContainer!: HTMLElement;
private _tableActionBar!: ActionBar;
// Properties table
private _table: Table<Slick.SlickData>;
private _dataView: TableDataView<Slick.SlickData>;
private _data: { [key: string]: string }[];
private _tableContainer!: HTMLElement;
private _actualTable!: HTMLElement;
// Table dimensions.
private _tableWidth = 485;
private _tableHeight;
private _model: ExecutionPlanPropertiesViewModel;
public constructor(
private _parentContainer: HTMLElement,
private _themeService: IThemeService,
private _model: GraphElementPropertyViewData = <GraphElementPropertyViewData>{}
parentContainer: HTMLElement,
themeService: IThemeService
) {
this._parentContainer.style.display = 'none';
this._propertiesTitle = DOM.$('.title');
this._parentContainer.appendChild(this._propertiesTitle);
this._titleText = DOM.$('h3');
this._titleText.classList.add('text');
this._titleText.innerText = localize('nodePropertyViewTitle', "Properties");
this._propertiesTitle.appendChild(this._titleText);
this._titleActionBarContainer = DOM.$('.action-bar');
this._propertiesTitle.appendChild(this._titleActionBarContainer);
this._titleActionBar = new ActionBar(this._titleActionBarContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._titleActionBar.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
super(parentContainer, themeService);
this._model = <ExecutionPlanPropertiesView>{};
this._operationName = DOM.$('h3');
this._operationName.classList.add('operation-name');
this._parentContainer.appendChild(this._operationName);
this.setHeader(this._operationName);
this._tableActionBarContainer = DOM.$('.table-action-bar');
this._parentContainer.appendChild(this._tableActionBarContainer);
this._tableActionBar = new ActionBar(this._tableActionBarContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
this._parentContainer.style.display = 'none';
}
public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) {
this._model.graphElement = element;
this.renderView();
}
public sortPropertiesAlphabetically(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
return props.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
this._tableActionBar.pushAction([new SortPropertiesByDisplayOrderAction(), new SortPropertiesAlphabeticallyAction()], { icon: true, label: false });
}
public sortPropertiesReverseAlphabetically(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
return props.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return b.name.localeCompare(a.name);
}
});
}
this._tableContainer = DOM.$('.table-container');
this._parentContainer.appendChild(this._tableContainer);
public sortPropertiesByImportance(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
return props.sort((a, b) => {
if (!a?.displayOrder && !b?.displayOrder) {
return 0;
} else if (!a?.displayOrder) {
return -1;
} else if (!b?.displayOrder) {
return 1;
} else {
return a.displayOrder - b.displayOrder;
}
});
}
this._actualTable = DOM.$('.table');
this._tableContainer.appendChild(this._actualTable);
this._dataView = new TableDataView();
this._data = [];
public renderView(): void {
if (this._model.graphElement) {
const nodeName = (<azdata.executionPlan.ExecutionPlanNode>this._model.graphElement).name;
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
}
const columns: Slick.Column<Slick.SlickData>[] = [
{
@@ -111,100 +106,26 @@ export class ExecutionPlanPropertiesView {
}
];
this._table = new Table(this._actualTable, {
dataProvider: this._dataView, columns: columns
}, {
rowHeight: RESULTS_GRID_DEFAULTS.rowHeight,
forceFitColumns: true,
defaultColumnWidth: 120
});
new ResizeObserver((e) => {
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - 80);
}).observe(this._parentContainer);
attachTableStyler(this._table, this._themeService);
this.populateTable(columns, this.convertModelToTableRows(this._model.graphElement.properties, -1, 0));
}
public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) {
this._model.graphElement = element;
this.sortPropertiesByImportance();
this.renderView();
}
public sortPropertiesAlphabetically(): void {
this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
this.renderView();
}
public sortPropertiesByImportance(): void {
this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => {
if (!a?.displayOrder && !b?.displayOrder) {
return 0;
} else if (!a?.displayOrder) {
return -1;
} else if (!b?.displayOrder) {
return 1;
} else {
return a.displayOrder - b.displayOrder;
}
});
this.renderView();
}
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
this.renderView();
}
}
public get tableHeight(): number {
return this._tableHeight;
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
this.renderView();
}
}
public get tableWidth(): number {
return this._tableWidth;
}
private renderView(): void {
if (this._model.graphElement) {
const nodeName = (<azdata.executionPlan.ExecutionPlanNode>this._model.graphElement).name;
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
}
this._tableContainer.scrollTo(0, 0);
this._dataView.clear();
this._data = this.convertPropertiesToTableRows(this._model.graphElement.properties, -1, 0);
this._dataView.push(this._data);
this._table.setData(this._dataView);
this._table.autosizeColumns();
this._table.updateRowCount();
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - 80); //80px is the space taken by the title and toolbar
this._table.layout(new DOM.Dimension(this._tableWidth, this._tableHeight));
this._table.resizeCanvas();
}
private convertPropertiesToTableRows(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
private convertModelToTableRows(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
if (!props) {
return rows;
}
switch (this.sortType) {
case PropertiesSortType.DisplayOrder:
props = this.sortPropertiesByImportance(props);
break;
case PropertiesSortType.Alphabetical:
props = this.sortPropertiesAlphabetically(props);
break;
case PropertiesSortType.ReverseAlphabetical:
props = this.sortPropertiesReverseAlphabetically(props);
break;
}
props.forEach((p, i) => {
let row = {};
rows.push(row);
@@ -212,7 +133,7 @@ export class ExecutionPlanPropertiesView {
row['parent'] = parentIndex;
if (!isString(p.value)) {
row['value'] = removeLineBreaks(p.displayValue, ' ');
this.convertPropertiesToTableRows(p.value, rows.length - 1, indent + 2, rows);
this.convertModelToTableRows(p.value, rows.length - 1, indent + 2, rows);
} else {
row['value'] = removeLineBreaks(p.value, ' ');
row['tooltip'] = p.value;
@@ -220,52 +141,8 @@ export class ExecutionPlanPropertiesView {
});
return rows;
}
public toggleVisibility(): void {
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
this.renderView();
}
}
export interface GraphElementPropertyViewData {
export interface ExecutionPlanPropertiesViewModel {
graphElement: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge;
}
export class ClosePropertyViewAction extends Action {
public static ID = 'ep.propertiesView.close';
public static LABEL = localize('executionPlanPropertyViewClose', "Close");
constructor() {
super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames);
}
public override async run(context: ExecutionPlanPropertiesView): Promise<void> {
context.toggleVisibility();
}
}
export class SortPropertiesAlphabeticallyAction extends Action {
public static ID = 'ep.propertiesView.sortByAlphabet';
public static LABEL = localize('executionPlanPropertyViewSortAlphabetically', "Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, sortAlphabeticallyIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesView): Promise<void> {
context.sortPropertiesAlphabetically();
}
}
export class SortPropertiesByDisplayOrderAction extends Action {
public static ID = 'ep.propertiesView.sortByDisplayOrder';
public static LABEL = localize('executionPlanPropertyViewSortByDisplayOrder', "Categorized");
constructor() {
super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, sortByDisplayOrderIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesView): Promise<void> {
context.sortPropertiesByImportance();
}
}

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { sortAlphabeticallyIconClassNames, sortByDisplayOrderIconClassNames, sortReverseAlphabeticallyIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { contrastBorder, listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
export abstract class ExecutionPlanPropertiesViewBase {
// Title bar with close button action
private _titleBarContainer!: HTMLElement;
private _titleBarTextContainer!: HTMLElement;
private _titleBarActionsContainer!: HTMLElement;
private _titleActions: ActionBar;
// Header container
private _headerContainer: HTMLElement;
// Properties actions
private _headerActionsContainer!: HTMLElement;
private _headerActions: ActionBar;
// Properties table
private _tableComponent: Table<Slick.SlickData>;
private _tableComponentDataView: TableDataView<Slick.SlickData>;
private _tableComponentDataModel: { [key: string]: string }[];
private _tableContainer!: HTMLElement;
private _tableWidth;
private _tableHeight;
public sortType: PropertiesSortType = PropertiesSortType.DisplayOrder;
constructor(
public _parentContainer: HTMLElement,
private _themeService: IThemeService
) {
const sashContainer = DOM.$('.properties-sash');
this._parentContainer.appendChild(sashContainer);
this._titleBarContainer = DOM.$('.title');
this._parentContainer.appendChild(this._titleBarContainer);
this._titleBarTextContainer = DOM.$('h3');
this._titleBarTextContainer.classList.add('text');
this._titleBarTextContainer.innerText = localize('nodePropertyViewTitle', "Properties");
this._titleBarContainer.appendChild(this._titleBarTextContainer);
this._titleBarActionsContainer = DOM.$('.action-bar');
this._titleBarContainer.appendChild(this._titleBarActionsContainer);
this._titleActions = new ActionBar(this._titleBarActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._titleActions.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
this._headerContainer = DOM.$('.header');
this._parentContainer.appendChild(this._headerContainer);
this._headerActionsContainer = DOM.$('.table-action-bar');
this._parentContainer.appendChild(this._headerActionsContainer);
this._headerActions = new ActionBar(this._headerActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._headerActions.pushAction([new SortPropertiesByDisplayOrderAction(), new SortPropertiesAlphabeticallyAction(), new SortPropertiesReverseAlphabeticallyAction()], { icon: true, label: false });
this._tableContainer = DOM.$('.table-container');
this._parentContainer.appendChild(this._tableContainer);
const table = DOM.$('.table');
this._tableContainer.appendChild(table);
this._tableComponentDataView = new TableDataView();
this._tableComponentDataModel = [];
this._tableComponent = new Table(table, {
dataProvider: this._tableComponentDataView, columns: []
}, {
rowHeight: RESULTS_GRID_DEFAULTS.rowHeight,
forceFitColumns: true,
defaultColumnWidth: 120
});
attachTableStyler(this._tableComponent, this._themeService);
new ResizeObserver((e) => {
this.resizeTable();
}).observe(this._parentContainer);
}
public setTitle(v: string): void {
this._titleBarTextContainer.innerText = v;
}
public setHeader(c: HTMLElement): void {
this._headerContainer.appendChild(c);
}
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
this.renderView();
}
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
this.renderView();
}
}
public get tableWidth(): number {
return this._tableWidth;
}
public get tableHeight(): number {
return this._tableHeight;
}
public abstract renderView();
public toggleVisibility(): void {
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
this.renderView();
}
public populateTable(columns: Slick.Column<Slick.SlickData>[], data: { [key: string]: string }[]) {
this._tableComponent.columns = columns;
this._tableContainer.scrollTo(0, 0);
this._tableComponentDataView.clear();
this._tableComponentDataModel = data;
this._tableComponentDataView.push(this._tableComponentDataModel);
this._tableComponent.setData(this._tableComponentDataView);
this._tableComponent.autosizeColumns();
this._tableComponent.updateRowCount();
this.resizeTable();
}
private resizeTable(): void {
const spaceOccupied = (this._titleBarContainer.getBoundingClientRect().height
+ this._headerContainer.getBoundingClientRect().height
+ this._headerActionsContainer.getBoundingClientRect().height);
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - spaceOccupied - 15);
this.tableWidth = (this._parentContainer.getBoundingClientRect().width - 15);
this._tableComponent.layout(new DOM.Dimension(this._tableWidth, this._tableHeight));
this._tableComponent.resizeCanvas();
}
}
export class ClosePropertyViewAction extends Action {
public static ID = 'ep.propertiesView.close';
public static LABEL = localize('executionPlanPropertyViewClose', "Close");
constructor() {
super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.toggleVisibility();
}
}
export class SortPropertiesAlphabeticallyAction extends Action {
public static ID = 'ep.propertiesView.sortByAlphabet';
public static LABEL = localize('executionPlanPropertyViewSortAlphabetically', "Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, sortAlphabeticallyIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.Alphabetical;
context.renderView();
}
}
export class SortPropertiesReverseAlphabeticallyAction extends Action {
public static ID = 'ep.propertiesView.sortByAlphabet';
public static LABEL = localize('executionPlanPropertyViewSortReverseAlphabetically', "Reverse Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, sortReverseAlphabeticallyIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.ReverseAlphabetical;
context.renderView();
}
}
export class SortPropertiesByDisplayOrderAction extends Action {
public static ID = 'ep.propertiesView.sortByDisplayOrder';
public static LABEL = localize('executionPlanPropertyViewSortByDisplayOrder', "Categorized");
constructor() {
super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, sortByDisplayOrderIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.DisplayOrder;
context.renderView();
}
}
export enum PropertiesSortType {
DisplayOrder,
Alphabetical,
ReverseAlphabetical
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.properties .title,
.properties .table-action-bar {
background-color: ${menuBackgroundColor};
}
`);
}
const widgetBorderColor = theme.getColor(contrastBorder);
if (widgetBorderColor) {
collector.addRule(`
.properties .title,
.properties .table-action-bar,
.mxTooltip {
border: 1px solid ${widgetBorderColor};
}
`);
}
});

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/executionPlan';
import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ExecutionPlanState } from 'sql/workbench/common/editor/query/executionPlanState';
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
import { generateUuid } from 'vs/base/common/uuid';
export class ExecutionPlanTab implements IPanelTab {
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
public readonly identifier = 'ExecutionPlan2Tab';
public readonly view: ExecutionPlanTabView;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
) {
this.view = instantiationService.createInstance(ExecutionPlanTabView);
}
public dispose() {
}
public clear() {
this.view.clear();
}
}
export class ExecutionPlanTabView implements IPanelView {
private _container: HTMLElement = DOM.$('.execution-plan-tab');
private _input: ExecutionPlanState;
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
) {
}
public set state(newInput: ExecutionPlanState) {
const oldInput = this._input;
// clearing old input view
if (oldInput?.executionPlanFileViewUUID) {
const oldView = this._viewCache.executionPlanFileViewMap.get(oldInput.executionPlanFileViewUUID);
oldView.onHide(this._container);
}
// if new input already has a view we are just making it visible here.
let newView = this._viewCache.executionPlanFileViewMap.get(newInput.executionPlanFileViewUUID);
if (newView) {
newView.onShow(this._container);
} else {
// creating a new view for the new input
newInput.executionPlanFileViewUUID = generateUuid();
newView = this._instantiationService.createInstance(ExecutionPlanFileView);
newView.onShow(this._container);
newView.addGraphs(
newInput.graphs
);
this._viewCache.executionPlanFileViewMap.set(newInput.executionPlanFileViewUUID, newView);
}
this._input = newInput;
}
public render(parent: HTMLElement): void {
parent.appendChild(this._container);
}
public layout(dimension: DOM.Dimension): void {
this._container.style.width = dimension.width + 'px';
this._container.style.height = dimension.height + 'px';
}
public clearPlans(): void {
let currentView = this._viewCache.executionPlanFileViewMap.get(this._input.executionPlanFileViewUUID);
if (currentView) {
currentView.onHide(this._container);
this._input.graphs = [];
currentView = this._instantiationService.createInstance(ExecutionPlanFileView);
this._viewCache.executionPlanFileViewMap.set(this._input.executionPlanFileViewUUID, currentView);
currentView.render(this._container);
}
}
public clear() {
}
}

View File

@@ -0,0 +1,506 @@
/*---------------------------------------------------------------------------------------------
* 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 DOM from 'vs/base/browser/dom';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { ExecutionPlanPropertiesView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
import { ExecutionPlanViewHeader } from 'sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader';
import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Action } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget';
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
export class ExecutionPlanView implements ISashLayoutProvider {
// Underlying execution plan displayed in the view
private _model?: azdata.executionPlan.ExecutionPlanGraph;
// container for the view
private _container: HTMLElement;
// action bar for the view
private _actionBarContainer: HTMLElement;
private _actionBar: ActionBar;
// plan header section
public planHeader: ExecutionPlanViewHeader;
private _planContainer: HTMLElement;
private _planHeaderContainer: HTMLElement;
// properties view
public propertiesView: ExecutionPlanPropertiesView;
private _propContainer: HTMLElement;
// plan widgets
private _widgetContainer: HTMLElement;
public widgetController: ExecutionPlanWidgetController;
// plan diagram
public executionPlanDiagram: AzdataGraphView;
public actionBarToggleTopTip: Action;
public contextMenuToggleTooltipAction: Action;
constructor(
private _parent: HTMLElement,
private _graphIndex: number,
@IInstantiationService public readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IContextViewService public readonly contextViewService: IContextViewService,
@IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService,
@IEditorService private readonly editorService: IEditorService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IFileDialogService public fileDialogService: IFileDialogService,
@IFileService public fileService: IFileService,
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
) {
// parent container for query plan.
this._container = DOM.$('.execution-plan');
this._parent.appendChild(this._container);
const sashContainer = DOM.$('.execution-plan-sash');
this._parent.appendChild(sashContainer);
// resizing sash for the query plan.
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL, size: 3 });
let originalHeight = this._container.offsetHeight;
let originalTableHeight = 0;
let change = 0;
sash.onDidStart((e: ISashEvent) => {
originalHeight = this._container.offsetHeight;
originalTableHeight = this.propertiesView.tableHeight;
});
/**
* Using onDidChange for the smooth resizing of the graph diagram
*/
sash.onDidChange((evt: ISashEvent) => {
change = evt.startY - evt.currentY;
const newHeight = originalHeight - change;
if (newHeight < 200) {
return;
}
/**
* Since the parent container is flex, we will have
* to change the flex-basis property to change the height.
*/
this._container.style.minHeight = '200px';
this._container.style.flex = `0 0 ${newHeight}px`;
});
/**
* Resizing properties window table only once at the end as it is a heavy operation and worsens the smooth resizing experience
*/
sash.onDidEnd(() => {
this.propertiesView.tableHeight = originalTableHeight - change;
});
this._planContainer = DOM.$('.plan');
this._container.appendChild(this._planContainer);
// container that holds plan header info
this._planHeaderContainer = DOM.$('.header');
// Styling header text like the query editor
this._planHeaderContainer.style.fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
this._planHeaderContainer.style.fontSize = EDITOR_FONT_DEFAULTS.fontSize.toString();
this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight;
this._planContainer.appendChild(this._planHeaderContainer);
this.planHeader = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._planHeaderContainer, {
planIndex: this._graphIndex,
});
// container properties
this._propContainer = DOM.$('.properties');
this._container.appendChild(this._propContainer);
this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService);
this._widgetContainer = DOM.$('.plan-action-container');
this._planContainer.appendChild(this._widgetContainer);
this.widgetController = new ExecutionPlanWidgetController(this._widgetContainer);
// container that holds action bar icons
this._actionBarContainer = DOM.$('.action-bar-container');
this._container.appendChild(this._actionBarContainer);
this._actionBar = new ActionBar(this._actionBarContainer, {
orientation: ActionsOrientation.VERTICAL, context: this
});
this.actionBarToggleTopTip = new ActionBarToggleTooltip();
const actionBarActions = [
new SavePlanFile(),
new OpenPlanFile(),
this._instantiationService.createInstance(OpenQueryAction, 'ActionBar'),
this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'),
this._instantiationService.createInstance(ZoomInAction, 'ActionBar'),
this._instantiationService.createInstance(ZoomOutAction, 'ActionBar'),
this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'),
this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'),
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'),
this.actionBarToggleTopTip
];
this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
// Setting up context menu
this.contextMenuToggleTooltipAction = new ContextMenuTooltipToggle();
const contextMenuAction = [
new SavePlanFile(),
new OpenPlanFile(),
this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu'),
this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'),
this._instantiationService.createInstance(ZoomInAction, 'ContextMenu'),
this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu'),
this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'),
this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'),
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'),
this.contextMenuToggleTooltipAction
];
const self = this;
this._container.oncontextmenu = (e: MouseEvent) => {
if (contextMenuAction) {
this._contextMenuService.showContextMenu({
getAnchor: () => {
return {
x: e.x,
y: e.y
};
},
getActions: () => contextMenuAction,
getActionsContext: () => (self)
});
}
};
}
getHorizontalSashTop(sash: Sash): number {
return 0;
}
getHorizontalSashLeft?(sash: Sash): number {
return 0;
}
getHorizontalSashWidth?(sash: Sash): number {
return this._container.clientWidth;
}
private createPlanDiagram(container: HTMLElement) {
this.executionPlanDiagram = this._instantiationService.createInstance(AzdataGraphView, container, this._model);
this.executionPlanDiagram.onElementSelected(e => {
this.propertiesView.graphElement = e;
});
}
public set model(graph: azdata.executionPlan.ExecutionPlanGraph | undefined) {
this._model = graph;
if (this._model) {
this.planHeader.graphIndex = this._graphIndex;
this.planHeader.query = graph.query;
if (graph.recommendations) {
this.planHeader.recommendations = graph.recommendations;
}
let diagramContainer = DOM.$('.diagram');
this.createPlanDiagram(diagramContainer);
/**
* We do not want to scroll the diagram through mouse wheel.
* Instead, we pass this event to parent control. So, when user
* uses the scroll wheel, they scroll through graphs present in
* the graph control. To scroll the individual graphs, users should
* use the scroll bars.
*/
diagramContainer.addEventListener('wheel', e => {
this._parent.scrollTop += e.deltaY;
//Hiding all tooltips when we scroll.
const element = document.getElementsByClassName('mxTooltip');
for (let i = 0; i < element.length; i++) {
(<HTMLElement>element[i]).style.visibility = 'hidden';
}
e.preventDefault();
e.stopPropagation();
});
this._planContainer.appendChild(diagramContainer);
this.propertiesView.graphElement = this._model.root;
}
}
public get model(): azdata.executionPlan.ExecutionPlanGraph | undefined {
return this._model;
}
public openQuery() {
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.model.query, RunQueryOnConnectionMode.none).then();
}
public async openGraphFile() {
const input = this._untitledEditorService.create({ mode: this.model.graphFile.graphFileType, initialValue: this.model.graphFile.graphFileContent });
await input.resolve();
await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None);
input.setDirty(false);
this.editorService.openEditor(input);
}
public hideActionBar() {
this._actionBarContainer.style.display = 'none';
}
}
type ExecutionPlanActionSource = 'ContextMenu' | 'ActionBar';
export class OpenQueryAction extends Action {
public static ID = 'ep.OpenQueryAction';
public static LABEL = localize('openQueryAction', "Open Query");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(OpenQueryAction.ID, OpenQueryAction.LABEL, openQueryIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.OpenQuery)
.withAdditionalProperties({ source: this.source })
.send();
context.openQuery();
}
}
export class PropertiesAction extends Action {
public static ID = 'ep.propertiesAction';
public static LABEL = localize('executionPlanPropertiesActionLabel', "Properties");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.OpenExecutionPlanProperties)
.withAdditionalProperties({ source: this.source })
.send();
context.propertiesView.toggleVisibility();
}
}
export class ZoomInAction extends Action {
public static ID = 'ep.ZoomInAction';
public static LABEL = localize('executionPlanZoomInActionLabel', "Zoom In");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.ZoomIn)
.withAdditionalProperties({ source: this.source })
.send();
context.executionPlanDiagram.zoomIn();
}
}
export class ZoomOutAction extends Action {
public static ID = 'ep.ZoomOutAction';
public static LABEL = localize('executionPlanZoomOutActionLabel', "Zoom Out");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.ZoomOut)
.withAdditionalProperties({ source: this.source })
.send();
context.executionPlanDiagram.zoomOut();
}
}
export class ZoomToFitAction extends Action {
public static ID = 'ep.FitGraph';
public static LABEL = localize('executionPlanFitGraphLabel', "Zoom to fit");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.ZoomToFit)
.withAdditionalProperties({ source: this.source })
.send();
context.executionPlanDiagram.zoomToFit();
}
}
export class SavePlanFile extends Action {
public static ID = 'ep.saveXML';
public static LABEL = localize('executionPlanSavePlanXML', "Save Plan File");
constructor() {
super(SavePlanFile.ID, SavePlanFile.LABEL, savePlanIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const workspaceFolders = await context.workspaceContextService.getWorkspace().folders;
const defaultFileName = 'plan';
let currentWorkSpaceFolder: URI;
if (workspaceFolders.length !== 0) {
currentWorkSpaceFolder = workspaceFolders[0].uri;
currentWorkSpaceFolder = URI.joinPath(currentWorkSpaceFolder, defaultFileName); //appending default file name to workspace uri
} else {
currentWorkSpaceFolder = URI.parse(defaultFileName); // giving default name
}
const saveFileUri = await context.fileDialogService.showSaveDialog({
filters: [
{
extensions: ['sqlplan'], //TODO: Get this extension from provider
name: localize('executionPlan.SaveFileDescription', 'Execution Plan Files') //TODO: Get the names from providers.
}
],
defaultUri: currentWorkSpaceFolder // If no workspaces are opened this will be undefined
});
if (saveFileUri) {
await context.fileService.writeFile(saveFileUri, VSBuffer.fromString(context.model.graphFile.graphFileContent));
}
}
}
export class CustomZoomAction extends Action {
public static ID = 'ep.customZoom';
public static LABEL = localize('executionPlanCustomZoom', "Custom Zoom");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, customZoomIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.CustomZoom)
.withAdditionalProperties({ source: this.source })
.send();
context.widgetController.toggleWidget(context._instantiationService.createInstance(CustomZoomWidget, context.widgetController, context.executionPlanDiagram));
}
}
export class SearchNodeAction extends Action {
public static ID = 'ep.searchNode';
public static LABEL = localize('executionPlanSearchNodeAction', "Find Node");
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(SearchNodeAction.ID, SearchNodeAction.LABEL, searchIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.FindNode)
.withAdditionalProperties({ source: this.source })
.send();
context.widgetController.toggleWidget(context._instantiationService.createInstance(NodeSearchWidget, context.widgetController, context.executionPlanDiagram));
}
}
export class OpenPlanFile extends Action {
public static ID = 'ep.openGraphFile';
public static Label = localize('executionPlanOpenGraphFile', "Show Query Plan XML"); //TODO: add a contribution point for providers to set this text
constructor() {
super(OpenPlanFile.ID, OpenPlanFile.Label, openPlanFileIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
await context.openGraphFile();
}
}
export class ActionBarToggleTooltip extends Action {
public static ID = 'ep.tooltipToggleActionBar';
public static WHEN_TOOLTIPS_ENABLED_LABEL = localize('executionPlanEnableTooltip', "Tooltips enabled");
public static WHEN_TOOLTIPS_DISABLED_LABEL = localize('executionPlanDisableTooltip', "Tooltips disabled");
constructor() {
super(ActionBarToggleTooltip.ID, ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL, enableTooltipIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const state = context.executionPlanDiagram.toggleTooltip();
if (!state) {
this.class = disableTooltipIconClassName;
this.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_DISABLED_LABEL;
context.contextMenuToggleTooltipAction.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_DISABLED_LABEL;
} else {
this.class = enableTooltipIconClassName;
this.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL;
context.contextMenuToggleTooltipAction.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL;
}
}
}
export class ContextMenuTooltipToggle extends Action {
public static ID = 'ep.tooltipToggleContextMenu';
public static WHEN_TOOLTIPS_ENABLED_LABEL = localize('executionPlanContextMenuDisableTooltip', "Disable Tooltips");
public static WHEN_TOOLTIPS_DISABLED_LABEL = localize('executionPlanContextMenuEnableTooltip', "Enable Tooltips");
constructor() {
super(ContextMenuTooltipToggle.ID, ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL, enableTooltipIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const state = context.executionPlanDiagram.toggleTooltip();
if (!state) {
this.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_DISABLED_LABEL;
context.actionBarToggleTopTip.class = disableTooltipIconClassName;
context.actionBarToggleTopTip.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_DISABLED_LABEL;
} else {
this.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL;
context.actionBarToggleTopTip.class = enableTooltipIconClassName;
context.actionBarToggleTopTip.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL;
}
}
}

View File

@@ -12,7 +12,7 @@ import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connect
import { Button } from 'sql/base/browser/ui/button/button';
import { removeLineBreaks } from 'sql/base/common/strings';
export class PlanHeader {
export class ExecutionPlanViewHeader {
private _graphIndex: number; // Index of the graph in the view
private _relativeCost: number; // Relative cost of the graph to the script

View File

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>
.st0 {
opacity: 0
}
.st0,
.st1 {
fill: #f6f6f6
}
.st2 {
fill: #424242
}
.st3 {
fill: #f0eff1
}
.icon-vs-action-red {
fill: #a1260d
}
</style>
<g id="outline">
<path class="st0" d="M0 0h16v16H0z" />
<path class="st1" d="M6.771 5L1.382 0H0v10.825l2.337-2.566L3 9.755V16h13V5z" />
</g>
<g id="icon_x5F_bg">
<path class="st2"
d="M8.4 12h2.2l.4 1h1l-2-5H9l-2 5h1l.4-1zm1.1-2.75l.7 1.75H8.8l.7-1.75zM4.955 8.463L3.869 5.998h2.518L1 1v7.231l1.629-1.789 1.137 2.562z" />
<path class="st2" d="M7.849 6l1.077 1H14v7H5V9.551l-1 .454V15h11V6z" />
</g>
<g id="icon_x5F_fg">
<path class="st3" d="M9.5 9.25L8.8 11h1.4z" />
<path class="st3"
d="M8.926 7l.008.008H5.402l.866 1.966L5 9.551V14h9V7H8.926zM11 13l-.4-1H8.4L8 13H7l2-5h1l2 5h-1z" />
</g>
<path class="icon-vs-action-red"
d="M13.03 12.03L15 14l-1.061 1.061-1.97-1.97L10 15.061 8.939 14l1.97-1.97-1.97-1.97L10 9l1.97 1.97L13.939 9 15 10.061l-1.97 1.969z"
id="colorAction" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>
.st0 {
opacity: 0
}
.st0,
.st1 {
fill: #f6f6f6
}
.st2 {
fill: #424242
}
.st3 {
fill: #f0eff1
}
.icon-vs-green {
fill: #393
}
</style>
<g id="outline">
<path class="st0" d="M0 0h16v16H0z" />
<path class="st1" d="M6.771 5L1.382 0H0v10.825l2.337-2.566L3 9.755V16h13V5z" />
</g>
<g id="icon_x5F_bg">
<path class="st2"
d="M8.4 12h2.2l.4 1h1l-2-5H9l-2 5h1l.4-1zm1.1-2.75l.7 1.75H8.8l.7-1.75zM4.955 8.463L3.869 5.998h2.518L1 1v7.231l1.629-1.789 1.137 2.562z" />
<path class="st2" d="M7.849 6l1.077 1H14v7H5V9.551l-1 .454V15h11V6z" />
</g>
<g id="icon_x5F_fg">
<path class="st3" d="M9.5 9.25L8.8 11h1.4z" />
<path class="st3"
d="M8.926 7l.008.008H5.402l.866 1.966L5 9.551V14h9V7H8.926zM11 13l-.4-1H8.4L8 13H7l2-5h1l2 5h-1z" />
</g>
<g id="colorImportance">
<path class="icon-vs-green" d="M8.558 15l-3.442-3.441.884-.885 2.558 2.558 5.557-5.558.885.884z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1001 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}.icon-vs-fg{fill:#f0eff1;}.icon-vs-action-blue{fill:#00539c;}</style></defs><title>SortDescending_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline"><path class="icon-vs-out" d="M16,9.4v.536l-4.484,4.5L9,11.923V16H0V7H1V4.586L2.586,3H1V0H8V3H7.462L6.449,4H8V7H9v.682l1,1V3h3V8.7l1.148-1.144Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M7,6H2V5L5,2H2V1H7V2H6.952L7,2.051,4.013,5H7ZM3.8,12H5.2l-.7-1.75ZM8,8v7H1V8ZM7,14,5,9H4L2,14H3l.4-1H5.6L6,14Z"/></g><g id="iconFg"><path class="icon-vs-fg" d="M5,9H4L2,14H3l.4-1H5.6L6,14H7ZM3.8,12l.7-1.75L5.2,12Z"/></g><g id="colorAction"><path class="icon-vs-action-blue" d="M14.855,9.671l-3.34,3.352L8.163,9.671l.707-.707L11,11.086V4h1v7.1l2.148-2.14Z"/></g></svg>

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-red{fill:#e51400;}.icon-white{fill:#fff;}</style></defs><title>StatusCriticalError_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline"><path class="icon-vs-out" d="M16,8A8,8,0,1,1,8,0,8,8,0,0,1,16,8Z"/></g><g id="iconBg"><path class="icon-vs-red" d="M8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Zm4.414,10L11,12.414l-3-3-3,3L3.586,11l3-3-3-3L5,3.586l3,3,3-3L12.414,5l-3,3Z"/><path class="icon-white" d="M9.414,8l3,3L11,12.414l-3-3-3,3L3.586,11l3-3-3-3L5,3.586l3,3,3-3L12.414,5Z"/></g></svg>

After

Width:  |  Height:  |  Size: 699 B

View File

@@ -0,0 +1,8 @@
<svg id="fd8a72cd-0f76-430d-b563-1cfd12e162a7" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path id="f8e19413-ab25-4f75-80be-8af00a2b851f" data-name="canvas" d="M16,16H0V0H16Z" fill="#f6f6f6" fill-opacity="0"/>
<circle cx="8" cy="8" r="8" fill="#f6f6f6"/>
<circle cx="8" cy="8" r="7" fill="#dcb67a"/>
<g id="aeca0109-615d-41d0-81c7-d18f9815e720" data-name="iconBg">
<path d="M13,10.5A1.538,1.538,0,0,1,11.5,12a1.412,1.412,0,0,1-1.4-1H4.9l.9.9-.7.7L3,10.5,5.1,8.4l.7.7-.9.9h5.2a1.412,1.412,0,0,1,1.4-1A1.538,1.538,0,0,1,13,10.5ZM5.8,6.9,4.9,6h5.2a1.412,1.412,0,0,0,1.4,1A1.538,1.538,0,0,0,13,5.5,1.538,1.538,0,0,0,11.5,4a1.412,1.412,0,0,0-1.4,1H4.9l.9-.9-.7-.7L3,5.5,5.1,7.6Z" fill="#424242"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-yellow{fill:#fc0;}</style></defs><title>StatusWarning_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,14l-2,2H2L0,14,7,0H9Z"/><path class="icon-vs-yellow" d="M8.382,1H7.618l-6.4,12.8L2.5,15h11l1.283-1.2ZM9,13H7V11H9Zm0-3H7V5H9Z"/><path d="M7,11H9v2H7ZM7,5v5H9V5Z"/></svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@@ -106,6 +106,7 @@ However we always want it to be the width of the container it is resizing.
width: 100%;
height: 100%;
overflow: scroll;
position: relative;
}
/* Properties view in execution plan */
@@ -226,87 +227,107 @@ However we always want it to be the width of the container it is resizing.
z-index: 3;
}
.eps-container .save-plan-icon {
.ep-save-plan-icon {
background-image: url(../images/actionIcons/save.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .open-properties-icon {
.ep-open-properties-icon {
background-image: url(../images/actionIcons/openProperties.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .open-query-icon {
.ep-open-query-icon {
background-image: url(../images/actionIcons/openQuery.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .open-plan-file-icon {
.ep-open-plan-file-icon {
background-image: url(../images/actionIcons/openPlanFile.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .search-icon {
.ep-search-icon {
background-image: url(../images/actionIcons/search.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .sort-alphabetically-icon {
.ep-sort-alphabetically-icon {
background-image: url(../images/actionIcons/sortAlphabetically.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .sort-display-order-icon {
.ep-sort-reverse-alphabetically-icon {
background-image: url(../images/actionIcons/sortReverseAlphabetically.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-sort-display-order-icon {
background-image: url(../images/actionIcons/sortByDisplayOrder.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-in-icon {
.ep-zoom-in-icon {
background-image: url(../images/actionIcons/zoomIn.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-out-icon {
.ep-zoom-out-icon {
background-image: url(../images/actionIcons/zoomOut.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .custom-zoom-icon {
.ep-custom-zoom-icon {
background-image: url(../images/actionIcons/customZoom.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-to-fit-icon {
.ep-zoom-to-fit-icon {
background-image: url(../images/actionIcons/zoomToFit.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-icon {
.ep-zoom-icon {
background-image: url(../images/actionIcons/zoom.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-enable-tooltip-icon {
background-image: url(../images/actionIcons/enableTooltip.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-disable-tooltip-icon {
background-image: url(../images/actionIcons/disableTooltip.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}

View File

@@ -7,7 +7,6 @@ import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { ExecutionPlanWidgetBase } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase';
import { ExecutionPlan } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import * as DOM from 'vs/base/browser/dom';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
@@ -17,13 +16,16 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { zoomIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { Button } from 'sql/base/browser/ui/button/button';
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
export class CustomZoomWidget extends ExecutionPlanWidgetBase {
private _actionBar: ActionBar;
public customZoomInputBox: InputBox;
constructor(
public readonly executionPlanView: ExecutionPlan,
public readonly widgetController: ExecutionPlanWidgetController,
public readonly executionPlanDiagram: AzdataGraphView,
@IContextViewService public readonly contextViewService: IContextViewService,
@IThemeService public readonly themeService: IThemeService,
@INotificationService public readonly notificationService: INotificationService
@@ -39,7 +41,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
});
attachInputBoxStyler(this.customZoomInputBox, this.themeService);
const currentZoom = executionPlanView.azdataGraphDiagram.graph.view.getScale() * 100;
const currentZoom = this.executionPlanDiagram.getZoomLevel();
// Setting initial value to graph's current zoom
this.customZoomInputBox.value = Math.round(currentZoom).toString();
@@ -50,7 +52,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
if (ev.key === 'Enter') {
await new CustomZoomAction().run(self);
} else if (ev.key === 'Escape') {
executionPlanView.planActionView.removeWidget(self);
this.widgetController.removeWidget(self);
}
};
@@ -87,8 +89,8 @@ export class CustomZoomAction extends Action {
public override async run(context: CustomZoomWidget): Promise<void> {
const newValue = parseInt(context.customZoomInputBox.value);
if (newValue <= 200 && newValue >= 1) { // Getting max and min zoom values from SSMS
context.executionPlanView.azdataGraphDiagram.graph.view.setScale(newValue / 100);
context.executionPlanView.planActionView.removeWidget(context);
context.executionPlanDiagram.setZoomLevel(newValue);
context.widgetController.removeWidget(context);
} else {
context.notificationService.error(
localize('invalidCustomZoomError', "Select a zoom value between 1 to 200")
@@ -106,7 +108,7 @@ export class CancelZoom extends Action {
}
public override async run(context: CustomZoomWidget): Promise<void> {
context.executionPlanView.planActionView.removeWidget(context);
context.widgetController.removeWidget(context);
}
}

View File

@@ -8,17 +8,22 @@ import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { Codicon } from 'vs/base/common/codicons';
import { InternalExecutionPlanNode, ExecutionPlan } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachInputBoxStyler, attachSelectBoxStyler } from 'sql/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Action } from 'vs/base/common/actions';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { isString } from 'vs/base/common/types';
import { AzdataGraphView, InternalExecutionPlanNode, SearchType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
const CONTAINS_DISPLAY_STRING = localize("executionPlanSearchTypeContains", 'Contains');
const EQUALS_DISPLAY_STRING = localize("executionPlanSearchTypeEquals", 'Equals');
const GREATER_DISPLAY_STRING = '>';
const LESSER_DISPLAY_STRING = '<';
const GREATER_EQUAL_DISPLAY_STRING = '>=';
const LESSER_EQUAL_DISPLAY_STRING = '<=';
const LESSER_AND_GREATER_DISPLAY_STRING = '<>';
export class NodeSearchWidget extends ExecutionPlanWidgetBase {
@@ -27,16 +32,18 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
private _searchTypeSelectBoxContainer: HTMLElement;
private _searchTypeSelectBox: SelectBox;
private _selectedSearchType: SearchType = SearchType.Equals;
private _searchTextInputBox: InputBox;
private _searchResults: string[] = [];
private _searchResults: InternalExecutionPlanNode[] = [];
private _currentSearchResultIndex = 0;
private _usePreviousSearchResult: boolean = false;
private _actionBar: ActionBar;
constructor(
public readonly executionPlanView: ExecutionPlan,
public readonly planActionView: ExecutionPlanWidgetController,
public readonly executionPlanDiagram: AzdataGraphView,
@IContextViewService public readonly contextViewService: IContextViewService,
@IThemeService public readonly themeService: IThemeService
@@ -46,7 +53,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
// property name dropdown
this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container');
this.container.appendChild(this._propertyNameSelectBoxContainer);
const propDropdownOptions = [...executionPlanView.graphElementPropertiesSet].sort();
const propDropdownOptions = executionPlanDiagram.getUniqueElementProperties();
this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer);
attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService);
this._propertyNameSelectBoxContainer.style.width = '150px';
@@ -60,13 +67,40 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this.container.appendChild(this._searchTypeSelectBoxContainer);
this._searchTypeSelectBox = new SelectBox([
EQUALS_DISPLAY_STRING,
CONTAINS_DISPLAY_STRING
CONTAINS_DISPLAY_STRING,
GREATER_DISPLAY_STRING,
LESSER_DISPLAY_STRING,
GREATER_EQUAL_DISPLAY_STRING,
LESSER_EQUAL_DISPLAY_STRING,
LESSER_AND_GREATER_DISPLAY_STRING
], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer);
this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer);
attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService);
this._searchTypeSelectBoxContainer.style.width = '100px';
this._searchTypeSelectBox.onDidSelect(e => {
this._usePreviousSearchResult = false;
switch (e.selected) {
case EQUALS_DISPLAY_STRING:
this._selectedSearchType = SearchType.Equals;
break;
case CONTAINS_DISPLAY_STRING:
this._selectedSearchType = SearchType.Contains;
break;
case GREATER_DISPLAY_STRING:
this._selectedSearchType = SearchType.GreaterThan;
break;
case LESSER_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserThan;
break;
case GREATER_EQUAL_DISPLAY_STRING:
this._selectedSearchType = SearchType.GreaterThanEqualTo;
break;
case LESSER_EQUAL_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserThanEqualTo;
break;
case LESSER_AND_GREATER_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserAndGreaterThan;
}
});
// search text input box
@@ -103,108 +137,39 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this._searchTextInputBox.focus();
}
public searchNode(returnPreviousResult: boolean): void {
// Searching again as the input params have changed
if (!this._usePreviousSearchResult) {
this._searchResults = [];
this._currentSearchResultIndex = 0; //Resetting search Index to 0;
this._usePreviousSearchResult = true;
// Doing depth first search in the graphModel to find nodes with matching prop values.
const graphModel = this.executionPlanView.graphModel;
const stack: InternalExecutionPlanNode[] = [];
stack.push(graphModel.root);
while (stack.length !== 0) {
const currentNode = stack.pop();
const matchingProp = currentNode.properties.find(e => e.name === this._propertyNameSelectBox.value);
// Searching only properties with string value.
if (isString(matchingProp?.value)) {
// If the search type is '=' we look for exact match and for 'contains' we look search string occurance in prop value
if (
this._searchTypeSelectBox.value === EQUALS_DISPLAY_STRING && matchingProp.value === this._searchTextInputBox.value ||
this._searchTypeSelectBox.value === CONTAINS_DISPLAY_STRING && matchingProp.value.includes(this._searchTextInputBox.value)
) {
this._searchResults.push(currentNode.id);
}
}
stack.push(...currentNode.children);
}
}
// Returning if no results found.
if (this._searchResults.length === 0) {
return;
}
// Getting the node at search index
const resultCell = this.executionPlanView.azdataGraphDiagram.graph.model.getCell(this._searchResults[this._currentSearchResultIndex]);
// Selecting the node on graph diagram
this.executionPlanView.azdataGraphDiagram.graph.setSelectionCell(resultCell);
this.executionPlanView.propertiesView.graphElement = this.executionPlanView.searchNodes(resultCell.id);
/**
* The selected graph node might be hidden/partially visible if the graph is overflowing the parent container.
* Apart from the obvious problems in aesthetics, user do not get a proper feedback of the search result.
* To solve this problem, we will have to scroll the node into view. (preferably into the center of the view)
* Steps for that:
* 1. Get the bounding rect of the node on graph.
* 2. Get the midpoint of the node's bounding rect.
* 3. Find the dimensions of the parent container.
* 4. Since, we are trying to position the node into center, we set the left top corner position of parent to
* below x and y.
* x = node's x midpoint - half the width of parent container
* y = node's y midpoint - half the height of parent container
* 5. If the x and y are negative, we set them 0 as that is the minimum possible scroll position.
* 6. Smoothly scroll to the left top x and y calculated in step 4, 5.
*/
const cellRect = this.executionPlanView.azdataGraphDiagram.graph.getCellBounds(resultCell);
const cellMidPoint: Point = {
x: cellRect.x + cellRect.width / 2,
y: cellRect.y + cellRect.height / 2,
};
const graphContainer = <HTMLElement>this.executionPlanView.azdataGraphDiagram.container;
const containerBoundingRect = graphContainer.getBoundingClientRect();
const leftTopScrollPoint: Point = {
x: cellMidPoint.x - containerBoundingRect.width / 2,
y: cellMidPoint.y - containerBoundingRect.height / 2
};
leftTopScrollPoint.x = leftTopScrollPoint.x < 0 ? 0 : leftTopScrollPoint.x;
leftTopScrollPoint.y = leftTopScrollPoint.y < 0 ? 0 : leftTopScrollPoint.y;
graphContainer.scrollTo({
left: leftTopScrollPoint.x,
top: leftTopScrollPoint.y,
behavior: 'smooth'
public searchNodes(): void {
this._currentSearchResultIndex = 0;
this._searchResults = this.executionPlanDiagram.searchNodes({
propertyName: this._propertyNameSelectBox.value,
value: this._searchTextInputBox.value,
searchType: this._selectedSearchType
});
// Updating search result index based on prev flag
if (returnPreviousResult) {
// going to the end of list if the index is 0 on prev
this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ?
this._currentSearchResultIndex = this._searchResults.length - 1 :
this._currentSearchResultIndex = --this._currentSearchResultIndex;
} else {
// going to the front of list if we are at the last element
this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ?
this._currentSearchResultIndex = 0 :
this._currentSearchResultIndex = ++this._currentSearchResultIndex;
}
this._usePreviousSearchResult = true;
}
}
interface Point {
x: number;
y: number;
public next(): void {
if (!this._usePreviousSearchResult) {
this.searchNodes();
}
this.executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]);
this.executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]);
this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ?
this._currentSearchResultIndex = 0 :
this._currentSearchResultIndex = ++this._currentSearchResultIndex;
}
public previous(): void {
if (!this._usePreviousSearchResult) {
this.searchNodes();
}
this.executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]);
this.executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]);
this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ?
this._currentSearchResultIndex = this._searchResults.length - 1 :
this._currentSearchResultIndex = --this._currentSearchResultIndex;
}
}
export class GoToNextMatchAction extends Action {
@@ -216,7 +181,7 @@ export class GoToNextMatchAction extends Action {
}
public override async run(context: NodeSearchWidget): Promise<void> {
context.searchNode(false);
context.next();
}
}
@@ -229,7 +194,7 @@ export class GoToPreviousMatchAction extends Action {
}
public override async run(context: NodeSearchWidget): Promise<void> {
context.searchNode(true);
context.previous();
}
}
@@ -242,6 +207,6 @@ export class CancelSearch extends Action {
}
public override async run(context: NodeSearchWidget): Promise<void> {
context.executionPlanView.planActionView.removeWidget(context);
context.planActionView.removeWidget(context);
}
}

View File

@@ -15,6 +15,7 @@ export class ExecutionPlanInput extends EditorInput {
public static SCHEMA: string = 'executionplan';
private _content?: string;
public _executionPlanFileViewUUID: string;
constructor(
private _uri: URI,
@@ -23,6 +24,14 @@ export class ExecutionPlanInput extends EditorInput {
super();
}
public get executionPlanFileViewUUID(): string {
return this._executionPlanFileViewUUID;
}
public set executionPlanFileViewUUID(v: string) {
this._executionPlanFileViewUUID = v;
}
override get typeId(): string {
return ExecutionPlanInput.ID;
}
@@ -31,7 +40,10 @@ export class ExecutionPlanInput extends EditorInput {
return path.basename(this._uri.fsPath);
}
public get content(): string | undefined {
public async content(): Promise<string> {
if (!this._content) {
this._content = (await this._fileService.read(this._uri, { acceptTextOnly: true })).value;
}
return this._content;
}

View File

@@ -145,12 +145,11 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
// on the current active editor.
const activeCellElement = this.container.nativeElement.querySelector(`.editor-group-container.active .notebook-cell.active`);
let handled = false;
if (DOM.isAncestor(this.container.nativeElement, document.activeElement) && this.isActive() && this.model.activeCell) {
if ((DOM.isAncestor(this.container.nativeElement, document.activeElement) || document.activeElement === activeCellElement) && this.isActive() && this.model.activeCell) {
const event = new StandardKeyboardEvent(e);
if (!this.model.activeCell?.isEditMode) {
if (event.keyCode === KeyCode.DownArrow) {
let next = (this.findCellIndex(this.model.activeCell) + 1) % this.cells.length;
this.navigateToCell(this.cells[next]);
handled = true;
} else if (event.keyCode === KeyCode.UpArrow) {
@@ -302,6 +301,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
private scrollToActiveCell(): void {
const activeCellElement = document.querySelector(`.editor-group-container.active .notebook-cell.active`);
(activeCellElement as HTMLElement).focus();
activeCellElement.scrollIntoView({ behavior: 'auto', block: 'nearest' });
}

View File

@@ -24,7 +24,8 @@ import { URI } from 'vs/base/common/uri';
import { attachTabbedPanelStyler } from 'sql/workbench/common/styler';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ILogService } from 'vs/platform/log/common/log';
import { ExecutionPlanTab } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import { ExecutionPlanTab } from 'sql/workbench/contrib/executionPlan/browser/executionPlanTab';
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
class MessagesView extends Disposable implements IPanelView {
private messagePanel: MessagePanel;
@@ -223,7 +224,9 @@ export class QueryResultsView extends Disposable {
this.hideResults();
this.hideChart();
this.hideTopOperations();
this.hidePlan2();
this.hidePlan();
// clearing execution plans whenever a new query starts executing
this.executionPlanTab.view.clearPlans();
this.hideDynamicViewModelTabs();
this.input?.state.visibleTabs.clear();
if (this.input) {
@@ -248,12 +251,19 @@ export class QueryResultsView extends Disposable {
this.runnerDisposables.add(runner.onExecutionPlanAvailable(e => {
if (this.executionPlanTab) {
if (!this.input.state.visibleTabs.has(this.executionPlanTab.identifier)) {
this.showPlan2();
/**
* Adding execution plan graphs to execution plan file view
* when they become available
*/
const executionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
if (executionPlanFileViewCache) {
const view = executionPlanFileViewCache.executionPlanFileViewMap.get(
this.input.state.executionPlanState.executionPlanFileViewUUID
);
if (view) {
view.addGraphs(e.planGraphs);
}
}
// Adding graph to state and tab as they become available
this.input.state.executionPlanState.graphs.push(...e.planGraphs);
this.executionPlanTab.view.addGraphs(e.planGraphs);
}
}));
@@ -301,6 +311,7 @@ export class QueryResultsView extends Disposable {
this.runnerDisposables.add(runner.onQueryEnd(() => {
if (runner.isQueryPlan) {
runner.planXml.then(e => {
this.showPlan();
this.showTopOperations(e);
});
}
@@ -326,9 +337,9 @@ export class QueryResultsView extends Disposable {
if (input) {
this.resultsTab.view.state = input.state.gridPanelState;
this.executionPlanTab.view.addGraphs(input.state.executionPlanState.graphs);
this.topOperationsTab.view.setState(input.state.topOperationsState);
this.chartTab.view.state = input.state.chartState;
this.executionPlanTab.view.state = input.state.executionPlanState;
this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => {
dynamicTab.captureState(input.state.dynamicModelViewTabsState);
});
@@ -368,6 +379,7 @@ export class QueryResultsView extends Disposable {
this.messagesTab.clear();
this.topOperationsTab.clear();
this.chartTab.clear();
this.executionPlanTab.clear();
this.dynamicModelViewTabs.forEach(t => t.clear());
}
@@ -416,7 +428,7 @@ export class QueryResultsView extends Disposable {
this.topOperationsTab.view.showPlan(xml);
}
public showPlan2() {
public showPlan() {
if (!this._panelView.contains(this.executionPlanTab.identifier)) {
this.input?.state.visibleTabs.add(this.executionPlanTab.identifier);
if (!this._panelView.contains(this.executionPlanTab.identifier)) {
@@ -432,11 +444,10 @@ export class QueryResultsView extends Disposable {
}
}
public hidePlan2() {
public hidePlan() {
if (this._panelView.contains(this.executionPlanTab.identifier)) {
this.executionPlanTab.clear();
this.input.state.executionPlanState.clearExecutionPlanState();
this._panelView.removeTab(this.executionPlanTab.identifier);
this.executionPlanTab.clear();
}
}

View File

@@ -15,6 +15,8 @@ import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { TopOperationsState } from 'sql/workbench/common/editor/query/topOperationsState';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
const topOperationColumns: Array<Slick.Column<any>> = [
{ name: localize('topOperations.operation', "Operation"), field: 'operation', sortable: true, width: 300 },
@@ -55,7 +57,10 @@ export class TopOperationsView extends Disposable implements IPanelView {
private container = document.createElement('div');
private dataView = new TableDataView();
constructor(@IThemeService private themeService: IThemeService) {
constructor(
@IThemeService private themeService: IThemeService,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super();
this.table = new Table(this.container, {
columns: topOperationColumns,
@@ -71,6 +76,8 @@ export class TopOperationsView extends Disposable implements IPanelView {
public render(container: HTMLElement): void {
container.appendChild(this.container);
this.telemetryService.sendActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.ViewTopOperations);
}
public layout(dimension: Dimension): void {

View File

@@ -809,15 +809,45 @@ export class ConnectionManagementService extends Disposable implements IConnecti
* @param connection The connection to fill in or update
*/
private getAzureResourceForConnection(connection: interfaces.IConnectionProfile): azdata.AzureResource {
// check if this is a PowerBI connection which is determined based on connection domain address
if (this.isPowerBiConnection(connection)) {
return AzureResource.PowerBi;
}
// default to SQL if there are no provides or registered resources
let provider = this._providers.get(connection.providerName);
if (!provider || !provider.properties || !provider.properties.azureResource) {
this._logService.warn('Connection providers incorrectly registered. Defaulting to SQL Azure resource,');
return AzureResource.Sql;
}
// lookup the Azure resource based on the provider azureResource properties
let result = ConnectionManagementService._azureResources.find(r => AzureResource[r] === provider.properties.azureResource);
return result ? result : AzureResource.Sql;
}
/**
* Determine if a connection is to PowerBI based on the servers domain name.
* PowerBi servers will be in one of the hard-coded domains listed in this method, based on the
* Azure cloud being used. This method can be removed once the connection/AAD service is updated
* to parse the server endpoint using TDS prior to connecting. But that will need to be part of a
* larger refactoring of the connection & auth functionality.
* @param connection The connection profile that is to be checked.
*/
private isPowerBiConnection(connection: interfaces.IConnectionProfile): boolean {
if (!connection || !connection.serverName || connection.serverName.length === 0) {
return false;
}
let powerBiDomains = [
'pbidedicated.windows.net',
'pbidedicated.cloudapi.de',
'pbidedicated.usgovcloudapi.net',
'pbidedicated.chinacloudapi.cn'
];
let serverName = connection.serverName.toLowerCase();
return !!powerBiDomains.find(d => serverName.indexOf(d) >= 0);
}
/**
* Fills in the account token if it's needed for this connection and doesn't already have one
* and clears it if it isn't.

View File

@@ -287,7 +287,10 @@ export class CellModel extends Disposable implements ICellModel {
public set hover(value: boolean) {
this._hover = value;
this.fireExecutionStateChanged();
// The Run button is always visible while the cell is active, so we only need to emit this event for inactive cells
if (!this.active) {
this.fireExecutionStateChanged();
}
}
public get executionCount(): number | undefined {

View File

@@ -1158,7 +1158,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor, ex
const notebooks: typeof vscode.notebooks = {
createNotebookController(id: string, notebookType: string, label: string, handler?, rendererScripts?: vscode.NotebookRendererScript[]) {
// {{SQL CARBON EDIT}} Use our own notebooks
return extHostNotebook.createNotebookController(extension, id, notebookType, label, handler, extension.enableProposedApi ? rendererScripts : undefined);
let getDocHandler = (notebookUri: URI) => extHostNotebookDocumentsAndEditors.getDocument(notebookUri.toString())?.document;
return extHostNotebook.createNotebookController(extension, id, notebookType, label, getDocHandler, handler, extension.enableProposedApi ? rendererScripts : undefined);
},
registerNotebookCellStatusBarItemProvider: (notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) => {
// {{SQL CARBON EDIT}} Disable VS Code notebooks

View File

@@ -16,7 +16,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { URI } from 'vs/base/common/uri';
import { platform } from 'vs/base/common/process';
const PROBABILITY = 0.15;
const PROBABILITY = 1; // {{SQL CARBON EDIT}} For now, we'd like all ADS users to receive the NPS survey.
const SESSION_COUNT_KEY = 'nps/sessionCount';
const LAST_SESSION_DATE_KEY = 'nps/lastSessionDate';
const SKIP_VERSION_KEY = 'nps/skipVersion';

View File

@@ -1831,9 +1831,9 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
"azdataGraph@github:Microsoft/azdataGraph#0.0.19":
version "0.0.19"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/4f5e3ef88b997c0190d2bd688a586b993f9fed67"
"azdataGraph@github:Microsoft/azdataGraph#0.0.21":
version "0.0.21"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
azure-storage@^2.10.2:
version "2.10.2"