Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e56e1d931b | ||
|
|
4bd07a00c0 | ||
|
|
4ef3706cf2 | ||
|
|
0704471e6b | ||
|
|
f5aebda7de | ||
|
|
9400c56c0b | ||
|
|
aeaf0ef473 | ||
|
|
fd0a8c0ef4 | ||
|
|
a7361002cb | ||
|
|
af42325121 | ||
|
|
85983140cc | ||
|
|
58d6b22263 | ||
|
|
09a74aadd5 | ||
|
|
26eeb78018 | ||
|
|
9d610d17ba | ||
|
|
8bed834226 | ||
|
|
835a644e7d | ||
|
|
f0f83d005b | ||
|
|
89ee54ab8f | ||
|
|
07ed6abfd5 | ||
|
|
b1b0b9e7af | ||
|
|
691d46a0d8 | ||
|
|
b8d47cc97e | ||
|
|
5050111a42 | ||
|
|
0bd8450cf6 |
34
CHANGELOG.md
@@ -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
|
||||
|
||||
14
README.md
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "3.0.0-release.229",
|
||||
"version": "3.0.0-release.238",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-net6.0.zip",
|
||||
"Windows_64": "win-x64-net6.0.zip",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "schema-compare",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"version": "1.13.0",
|
||||
"version": "1.13.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": false,
|
||||
"engines": {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
},
|
||||
{
|
||||
"command": "sqlBindings.createAzureFunction",
|
||||
"when": "view == objectExplorer && viewItem == Table && !azdataAvailable",
|
||||
"when": "!azdataAvailable",
|
||||
"group": "zAzure_Function@1"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
@@ -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)$/",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "azuredatastudio",
|
||||
"version": "1.36.0",
|
||||
"version": "1.36.1",
|
||||
"distro": "8f4d839fbcb98eaa7aa3fd8bf9f84ab9bae899d9",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
@@ -75,7 +75,7 @@
|
||||
"angular2-grid": "2.0.6",
|
||||
"ansi_up": "^5.1.0",
|
||||
"applicationinsights": "1.0.8",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.19",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.20",
|
||||
"chart.js": "^2.9.4",
|
||||
"chokidar": "3.5.2",
|
||||
"graceful-fs": "4.2.6",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
49
src/sql/azdata.proposed.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ export enum ComponentEventType {
|
||||
onCellAction,
|
||||
onEnterKeyPressed,
|
||||
onInput,
|
||||
onComponentLoaded
|
||||
onComponentLoaded,
|
||||
onChildClick
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
Before Width: | Height: | Size: 414 B After Width: | Height: | Size: 414 B |
|
Before Width: | Height: | Size: 625 B After Width: | Height: | Size: 625 B |
|
Before Width: | Height: | Size: 911 B After Width: | Height: | Size: 911 B |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -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('.', '')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { platform } from 'vs/base/common/process';
|
||||
|
||||
const PROBABILITY = 0.15;
|
||||
const PROBABILITY = 1; // {{SQL CARBON EDIT}} For now, we'd like all ADS users to receive the NPS survey.
|
||||
const SESSION_COUNT_KEY = 'nps/sessionCount';
|
||||
const LAST_SESSION_DATE_KEY = 'nps/lastSessionDate';
|
||||
const SKIP_VERSION_KEY = 'nps/skipVersion';
|
||||
|
||||
@@ -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"
|
||||
|
||||