Compare commits

...

22 Commits

Author SHA1 Message Date
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
64 changed files with 2609 additions and 1273 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

@@ -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.234",
"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

@@ -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.1",
"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)$/",

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));
}
});

View File

@@ -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.20",
"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.20",
"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.20",
"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.20":
version "0.0.20"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/d685f37f72f0b22c42d15fbb725b3a5a5b3d71ae"
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.20":
version "0.0.20"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/d685f37f72f0b22c42d15fbb725b3a5a5b3d71ae"
binary-extensions@^2.0.0:
version "2.0.0"

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 {
@@ -1031,3 +1032,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,522 @@
/*---------------------------------------------------------------------------------------------
* 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.graph.zoomIn();
}
/**
* Zooms out of the diagram
*/
public zoomOut(): void {
this._diagram.graph.zoomOut();
}
/**
* Fits the diagram into the parent container size.
*/
public zoomToFit(): void {
this._diagram.graph.fit();
this._diagram.graph.view.rendering = true;
this._diagram.graph.view.refresh();
}
/**
* 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.graph.view.setScale(level / 100);
}
/**
* 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,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F6F6F6;fill-opacity:0;}
.st1{fill:#F6F6F6;}
.st2{fill:#DCB67A;}
.st3{fill:#424242;}
</style>
<path id="canvas" class="st0" d="M16,16H0V0h16V16z"/>
<circle class="st1" cx="8" cy="8" r="8"/>
<circle class="st2" cx="8" cy="8" r="7"/>
<g id="iconBg">
<path class="st3" d="M3,5.5C3,4.7,3.7,4,4.5,4c0.7,0,1.2,0.4,1.4,1h5.2l-0.9-0.9l0.7-0.7L13,5.5l-2.1,2.1l-0.7-0.7L11.1,6H5.9
C5.7,6.6,5.2,7,4.5,7C3.7,7,3,6.3,3,5.5z M10.2,9.1l0.9,0.9H5.9C5.7,9.4,5.2,9,4.5,9C3.7,9,3,9.7,3,10.5S3.7,12,4.5,12
c0.7,0,1.2-0.4,1.4-1h5.2l-0.9,0.9l0.7,0.7l2.1-2.1l-2.1-2.1C10.9,8.4,10.2,9.1,10.2,9.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 976 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

@@ -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

@@ -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.20":
version "0.0.20"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/d685f37f72f0b22c42d15fbb725b3a5a5b3d71ae"
azure-storage@^2.10.2:
version "2.10.2"