From 2c007115f77427a7ff7af47ad6916fccf111ea11 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Wed, 17 Oct 2018 12:04:42 -0700 Subject: [PATCH 01/13] Fix query plan scrollbars (#2927) * Fix query plan scrollbars * Remove toString --- src/sql/parts/queryPlan/queryPlan.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sql/parts/queryPlan/queryPlan.ts b/src/sql/parts/queryPlan/queryPlan.ts index f4e56a1207..607b9d4bb1 100644 --- a/src/sql/parts/queryPlan/queryPlan.ts +++ b/src/sql/parts/queryPlan/queryPlan.ts @@ -45,6 +45,8 @@ export class QueryPlanView implements IPanelView { } public layout(dimension: Dimension): void { + this.container.style.width = dimension.width + 'px'; + this.container.style.height = dimension.height + 'px'; } public showPlan(xml: string) { From 5a0490e81fcfc194a38ab188193639716dc501f5 Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Wed, 17 Oct 2018 15:43:00 -0700 Subject: [PATCH 02/13] Display multi-line messages correctly in query results (#2935) --- src/sql/parts/query/editor/messagePanel.ts | 29 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/sql/parts/query/editor/messagePanel.ts b/src/sql/parts/query/editor/messagePanel.ts index 3f0f699255..231ff1b9a6 100644 --- a/src/sql/parts/query/editor/messagePanel.ts +++ b/src/sql/parts/query/editor/messagePanel.ts @@ -74,8 +74,9 @@ export class MessagePanelState { } export class MessagePanel extends ViewletPanel { + private messageLineCountMap = new Map(); private ds = new MessageDataSource(); - private renderer = new MessageRenderer(); + private renderer = new MessageRenderer(this.messageLineCountMap); private model = new Model(); private controller: MessageController; private container = $('div message-tree').getHTMLElement(); @@ -143,29 +144,40 @@ export class MessagePanel extends ViewletPanel { private onMessage(message: IResultMessage | IResultMessage[]) { let hasError = false; + let lines: number; if (isArray(message)) { hasError = message.find(e => e.isError) ? true : false; + lines = message.reduce((currentTotal, resultMessage) => currentTotal + this.countMessageLines(resultMessage), 0); this.model.messages.push(...message); } else { hasError = message.isError; + lines = this.countMessageLines(message); this.model.messages.push(message); } + this.maximumBodySize += lines * 22; if (hasError) { this.setExpanded(true); } if (this.state.scrollPosition) { this.tree.refresh(this.model).then(() => { - this.tree.setScrollPosition(1); + // Restore the previous scroll position when switching between tabs + this.tree.setScrollPosition(this.state.scrollPosition); }); } else { const previousScrollPosition = this.tree.getScrollPosition(); this.tree.refresh(this.model).then(() => { + // Scroll to the end if the user was already at the end otherwise leave the current scroll position if (previousScrollPosition === 1) { this.tree.setScrollPosition(1); } }); } - this.maximumBodySize = this.model.messages.length * 22; + } + + private countMessageLines(resultMessage: IResultMessage): number { + let lines = resultMessage.message.split('\n').length; + this.messageLineCountMap.set(resultMessage, lines); + return lines; } private reset() { @@ -220,8 +232,15 @@ class MessageDataSource implements IDataSource { } class MessageRenderer implements IRenderer { + constructor(private messageLineCountMap: Map) { + } + getHeight(tree: ITree, element: any): number { - return 22; + const lineHeight = 22; + if (this.messageLineCountMap.has(element)) { + return lineHeight * this.messageLineCountMap.get(element); + } + return lineHeight; } getTemplateId(tree: ITree, element: any): string { @@ -258,7 +277,7 @@ class MessageRenderer implements IRenderer { renderElement(tree: ITree, element: IResultMessage, templateId: string, templateData: IMessageTemplate | IBatchTemplate): void { if (templateId === TemplateIds.MESSAGE || templateId === TemplateIds.ERROR) { let data: IMessageTemplate = templateData; - data.message.innerText = element.message.replace(/(\r\n|\n|\r)/g, ' '); + data.message.innerText = element.message; } else if (templateId === TemplateIds.BATCH) { let data = templateData as IBatchTemplate; data.timeStamp.innerText = element.time; From f309979126774fef0eb31e816e819cddfe0770a1 Mon Sep 17 00:00:00 2001 From: Gene Lee Date: Wed, 17 Oct 2018 19:26:56 -0700 Subject: [PATCH 03/13] Added feature to disable checkbox of checkboxTreeNode (#2942) --- .../parts/modelComponents/tree/treeComponentRenderer.ts | 8 ++++++++ src/sql/sqlops.proposed.d.ts | 1 + src/sql/workbench/api/node/extHostModelViewTree.ts | 2 +- src/sql/workbench/common/views.ts | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts b/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts index 6923471116..6e2edd4031 100644 --- a/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts +++ b/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts @@ -71,6 +71,13 @@ export class TreeDataTemplate extends Disposable { } } + public set enableCheckbox(value: boolean) { + if (value === undefined) { + value = true; + } + this._checkbox.disabled = !value; + } + public get checkbox(): HTMLInputElement { return this._checkbox; } @@ -155,6 +162,7 @@ export class TreeComponentRenderer extends Disposable implements IRenderer { templateData.label.textContent = label; templateData.root.title = label; templateData.checkboxState = this.getCheckboxState(treeNode); + templateData.enableCheckbox = treeNode.enabled; } private getCheckboxState(treeNode: ITreeComponentItem): TreeCheckboxState { diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 0989c95ee9..6738450517 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -58,6 +58,7 @@ declare module 'sqlops' { export class TreeComponentItem extends vscode.TreeItem { checked?: boolean; + enabled?: boolean; } export interface ComponentBuilder { diff --git a/src/sql/workbench/api/node/extHostModelViewTree.ts b/src/sql/workbench/api/node/extHostModelViewTree.ts index ba94fa56e9..9c53d312a5 100644 --- a/src/sql/workbench/api/node/extHostModelViewTree.ts +++ b/src/sql/workbench/api/node/extHostModelViewTree.ts @@ -164,7 +164,7 @@ export class ExtHostTreeView extends vsTreeExt.ExtHostTreeView { protected createTreeItem(element: T, extensionTreeItem: sqlops.TreeComponentItem, parent?: vsTreeExt.TreeNode): ITreeComponentItem { let item = super.createTreeItem(element, extensionTreeItem, parent); - item = Object.assign({}, item, { checked: extensionTreeItem.checked }); + item = Object.assign({}, item, { checked: extensionTreeItem.checked, enabled: extensionTreeItem.enabled }); return item; } } \ No newline at end of file diff --git a/src/sql/workbench/common/views.ts b/src/sql/workbench/common/views.ts index b921e7de63..b42b3a2723 100644 --- a/src/sql/workbench/common/views.ts +++ b/src/sql/workbench/common/views.ts @@ -7,6 +7,7 @@ import { ITreeViewDataProvider, ITreeItem } from 'vs/workbench/common/views'; export interface ITreeComponentItem extends ITreeItem { checked?: boolean; + enabled?: boolean; onCheckedChanged?: (checked: boolean) => void; children?: ITreeComponentItem[]; } From b17882a1c1c3d53b1910f950c77ba7df2572858f Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Wed, 17 Oct 2018 21:41:18 -0700 Subject: [PATCH 04/13] Bump agent and import extension versions (#2949) --- extensions/agent/package.json | 2 +- extensions/import/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/agent/package.json b/extensions/agent/package.json index dd2d7effe1..1a3f5612f6 100644 --- a/extensions/agent/package.json +++ b/extensions/agent/package.json @@ -2,7 +2,7 @@ "name": "agent", "displayName": "SQL Server Agent", "description": "Manage and troubleshoot SQL Server Agent jobs", - "version": "0.33.0", + "version": "0.34.0", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt", diff --git a/extensions/import/package.json b/extensions/import/package.json index 1c16667ad3..fc7932a503 100644 --- a/extensions/import/package.json +++ b/extensions/import/package.json @@ -2,7 +2,7 @@ "name": "import", "displayName": "SQL Server Import", "description": "SQL Server Import for Azure Data Studio supports importing CSV or JSON files into SQL Server.", - "version": "0.2.0", + "version": "0.3.0", "publisher": "Microsoft", "preview": true, "engines": { From bfe44c1621db394d1c14d01c804c96ee379a5926 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Thu, 18 Oct 2018 07:35:44 -0700 Subject: [PATCH 05/13] Update README for October release (#2950) --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 18 +++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c2b13e94..9e1bd558d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## Version 1.1.3 +* Release date: October 18, 2018 +* Release status: General Availability + +## What's new in this version +* Introducing the Azure Resource Explorer to browse Azure SQL Databases +* Improve Object Explorer and Query Editor connectivity robustness +* SQL Server 2019 and SQL Agent extension improvements + +## Contributions and "thank you" +We would like to thank all our users who raised issues, and in particular the following users who helped contribute fixes: + +* philoushka for `center the icon #2760` +* anthonypants for `Typo #2775` +* kstolte for `Fix Invalid Configuration in Launch.json #2789` +* kstolte for `Fixing a reference to SQL Ops Studio #2788` ## Version 1.0.0 * Release date: September 24, 2018 diff --git a/README.md b/README.md index 3f85be0f82..7658fc4c08 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ Azure Data Studio is a data management tool that enables you to work with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. -**Download Azure Data Studio August Public Preview** +**Download the latest Azure Data Studio release** Platform | Link -- | -- -Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=2024683 -Windows ZIP | https://go.microsoft.com/fwlink/?linkid=2024680 -macOS ZIP | https://go.microsoft.com/fwlink/?linkid=2024677 -Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=2024675 -Linux RPM | https://go.microsoft.com/fwlink/?linkid=2024672 -Linux DEB | https://go.microsoft.com/fwlink/?linkid=2024668 +Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=2030731 +Windows ZIP | https://go.microsoft.com/fwlink/?linkid=2030736 +macOS ZIP | https://go.microsoft.com/fwlink/?linkid=2030738 +Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=2030741 +Linux RPM | https://go.microsoft.com/fwlink/?linkid=2030746 +Linux DEB | https://go.microsoft.com/fwlink/?linkid=2030750 Go to our [download page](https://aka.ms/azuredatastudio) for more specific instructions. @@ -61,6 +61,10 @@ The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.micro ## Contributions and "Thank You" We would like to thank all our users who raised issues, and in particular the following users who helped contribute fixes: +* philoushka for `center the icon #2760` +* anthonypants for `Typo #2775` +* kstolte for `Fix Invalid Configuration in Launch.json #2789` +* kstolte for `Fixing a reference to SQL Ops Studio #2788` * AlexFsmn `Feature: Ability to add connection name #2332` * AlexFsmn `Disabled connection name input when connecting to a server. #2566` * SebastianPfliegel `Added more saveAsCsv options #2099` From 8a570069f882c2ef75b55d00a17be713f75e81f3 Mon Sep 17 00:00:00 2001 From: kenvanhyning Date: Thu, 18 Oct 2018 12:58:07 -0700 Subject: [PATCH 06/13] Kenvh/editdatatabname (#2906) * Remove unnecessary string encoding on edit data naming * Cleaning up duplication in the naming methods. * fix typo in method name --- .../query/services/queryEditorService.ts | 35 +++++-------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/src/sql/parts/query/services/queryEditorService.ts b/src/sql/parts/query/services/queryEditorService.ts index 557e46d981..a5e7378884 100644 --- a/src/sql/parts/query/services/queryEditorService.ts +++ b/src/sql/parts/query/services/queryEditorService.ts @@ -124,7 +124,7 @@ export class QueryEditorService implements IQueryEditorService { try { // Create file path and file URI let objectName = schemaName ? schemaName + '.' + tableName : tableName; - let filePath = this.createEditDataFileName(objectName); + let filePath = this.createPrefixedSqlFilePath(objectName); let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements @@ -265,45 +265,26 @@ export class QueryEditorService implements IQueryEditorService { ////// Private functions private createUntitledSqlFilePath(): string { - let sqlFileName = (counter: number): string => { - return `${untitledFilePrefix}${counter}`; - }; - - let counter = 1; - // Get document name and check if it exists - let filePath = sqlFileName(counter); - while (fs.existsSync(filePath)) { - counter++; - filePath = sqlFileName(counter); - } - - // check if this document name already exists in any open documents - let untitledEditors = this._untitledEditorService.getAll(); - while (untitledEditors.find(x => x.getName().toUpperCase() === filePath.toUpperCase())) { - counter++; - filePath = sqlFileName(counter); - } - - return filePath; + return this.createPrefixedSqlFilePath(untitledFilePrefix); } - private createEditDataFileName(tableName: string): string { - let editDataFileName = (counter: number): string => { - return encodeURIComponent(`${tableName}_${counter}`); + private createPrefixedSqlFilePath(prefix: string): string { + let prefixFileName = (counter: number): string => { + return `${prefix}_${counter}`; }; let counter = 1; // Get document name and check if it exists - let filePath = editDataFileName(counter); + let filePath = prefixFileName(counter); while (fs.existsSync(filePath)) { counter++; - filePath = editDataFileName(counter); + filePath = prefixFileName(counter); } let untitledEditors = this._untitledEditorService.getAll(); while (untitledEditors.find(x => x.getName().toUpperCase() === filePath.toUpperCase())) { counter++; - filePath = editDataFileName(counter); + filePath = prefixFileName(counter); } return filePath; From e002ad3b6a06f20c616aa9b26031f920078d0696 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Thu, 18 Oct 2018 13:27:30 -0700 Subject: [PATCH 07/13] fix test errors (#2938) --- src/sql/base/browser/ui/panel/panel.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sql/base/browser/ui/panel/panel.ts b/src/sql/base/browser/ui/panel/panel.ts index aa5b608994..21c3b207a7 100644 --- a/src/sql/base/browser/ui/panel/panel.ts +++ b/src/sql/base/browser/ui/panel/panel.ts @@ -6,7 +6,7 @@ import { IThemable } from 'vs/platform/theme/common/styler'; import { Event, Emitter } from 'vs/base/common/event'; import { Dimension, EventType, $, addDisposableListener } from 'vs/base/browser/dom'; -import { $ as quickBuilder } from 'vs/base/browser/builder'; +import { $ as qb } from 'vs/base/browser/builder'; import { IAction } from 'vs/base/common/actions'; import { IActionOptions, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -146,18 +146,18 @@ export class TabbedPanel extends Disposable implements IThemable { } if (this._shownTab) { - this._tabMap.get(this._shownTab).label.classList.remove('active'); - this._tabMap.get(this._shownTab).header.classList.remove('active'); + qb(this._tabMap.get(this._shownTab).label).removeClass('active'); + qb(this._tabMap.get(this._shownTab).header).removeClass('active'); this._tabMap.get(this._shownTab).header.setAttribute('aria-selected', 'false'); } this._shownTab = id; this.tabHistory.push(id); - quickBuilder(this.body).empty(); + qb(this.body).empty(); let tab = this._tabMap.get(this._shownTab); this.body.setAttribute('aria-labelledby', tab.identifier); - tab.label.classList.add('active'); - tab.header.classList.add('active'); + qb(tab.label).addClass('active'); + qb(tab.header).addClass('active'); tab.header.setAttribute('aria-selected', 'true'); tab.view.render(this.body); this._onTabChange.fire(id); @@ -168,11 +168,11 @@ export class TabbedPanel extends Disposable implements IThemable { public removeTab(tab: PanelTabIdentifier) { let actualTab = this._tabMap.get(tab); - quickBuilder(actualTab.header).destroy(); + qb(actualTab.header).destroy(); if (actualTab.view.remove) { actualTab.view.remove(); } - quickBuilder(this._tabMap.get(tab).header).destroy(); + qb(this._tabMap.get(tab).header).destroy(); this._tabMap.delete(tab); if (this._shownTab === tab) { this._shownTab = undefined; From 0824c779db878831093744e009db50020f8dc987 Mon Sep 17 00:00:00 2001 From: Todd Ortmann <43353067+hi-im-T0dd@users.noreply.github.com> Date: Fri, 19 Oct 2018 16:16:40 -0700 Subject: [PATCH 08/13] Fixed sync issue with my forked master so this commit is correct (#2948) --- .../parts/extensions/node/extensionsWorkbenchService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 8395c53ebb..20487abe61 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -298,7 +298,7 @@ ${this.description} if (!changelogUrl) { if (this.type === LocalExtensionType.System) { - return TPromise.as('Please check the [VS Code Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.'); + return TPromise.as('Please check the [Azure Data Studio Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.'); } return TPromise.wrapError(new Error('not available')); From dc2f6235a13595abc9973eccc8a71586ff7ae6b6 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 22 Oct 2018 17:04:54 -0700 Subject: [PATCH 09/13] Update SQL Tools Service to 1.5.0-alpha.46 --- extensions/mssql/src/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index a809c622ea..f10f220555 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "1.5.0-alpha.43", + "version": "1.5.0-alpha.46", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.1.zip", "Windows_64": "win-x64-netcoreapp2.1.zip", From 201174e2932b3786b83fa7c17f807326d3ace02c Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 22 Oct 2018 17:11:41 -0700 Subject: [PATCH 10/13] Bump Azure Data Studio to 1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 82af714725..80b31d6cd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azuredatastudio", - "version": "1.1.3", + "version": "1.2.1", "distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee", "author": { "name": "Microsoft Corporation" From f7d92caae51a679934a9f945ea0b48e89f02e1de Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 22 Oct 2018 17:20:30 -0700 Subject: [PATCH 11/13] Correct SQL Tools Service filenames to netcoreapp2.2 --- extensions/mssql/src/config.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index f10f220555..5b8605b61a 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -2,17 +2,17 @@ "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", "version": "1.5.0-alpha.46", "downloadFileNames": { - "Windows_86": "win-x86-netcoreapp2.1.zip", - "Windows_64": "win-x64-netcoreapp2.1.zip", - "OSX": "osx-x64-netcoreapp2.1.tar.gz", - "CentOS_7": "rhel-x64-netcoreapp2.1.tar.gz", - "Debian_8": "rhel-x64-netcoreapp2.1.tar.gz", - "Fedora_23": "rhel-x64-netcoreapp2.1.tar.gz", - "OpenSUSE_13_2": "rhel-x64-netcoreapp2.1.tar.gz", - "RHEL_7": "rhel-x64-netcoreapp2.1.tar.gz", - "SLES_12_2": "rhel-x64-netcoreapp2.1.tar.gz", - "Ubuntu_14": "rhel-x64-netcoreapp2.1.tar.gz", - "Ubuntu_16": "rhel-x64-netcoreapp2.1.tar.gz" + "Windows_86": "win-x86-netcoreapp2.2.zip", + "Windows_64": "win-x64-netcoreapp2.2.zip", + "OSX": "osx-x64-netcoreapp2.2.tar.gz", + "CentOS_7": "rhel-x64-netcoreapp2.2.tar.gz", + "Debian_8": "rhel-x64-netcoreapp2.2.tar.gz", + "Fedora_23": "rhel-x64-netcoreapp2.2.tar.gz", + "OpenSUSE_13_2": "rhel-x64-netcoreapp2.2.tar.gz", + "RHEL_7": "rhel-x64-netcoreapp2.2.tar.gz", + "SLES_12_2": "rhel-x64-netcoreapp2.2.tar.gz", + "Ubuntu_14": "rhel-x64-netcoreapp2.2.tar.gz", + "Ubuntu_16": "rhel-x64-netcoreapp2.2.tar.gz" }, "installDirectory": "../sqltoolsservice/{#platform#}/{#version#}", "executableFiles": ["MicrosoftSqlToolsServiceLayer.exe", "MicrosoftSqlToolsServiceLayer"] From 29cc57f52a8cdee0e9961799d37de930f5f57f24 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 22 Oct 2018 22:05:01 -0700 Subject: [PATCH 12/13] Remove unhandled exception telemetry (#2973) * Remove unhandled exception telemetry * Remove additional unhandled exception tests * One more failing test --- .../telemetry/browser/errorTelemetry.ts | 3 +- .../electron-browser/telemetryService.test.ts | 950 +++++++++--------- 2 files changed, 478 insertions(+), 475 deletions(-) diff --git a/src/vs/platform/telemetry/browser/errorTelemetry.ts b/src/vs/platform/telemetry/browser/errorTelemetry.ts index b59d778901..18279f2938 100644 --- a/src/vs/platform/telemetry/browser/errorTelemetry.ts +++ b/src/vs/platform/telemetry/browser/errorTelemetry.ts @@ -166,7 +166,8 @@ export default class ErrorTelemetry { "${include}": [ "${ErrorEvent}" ] } */ - this._telemetryService.publicLog('UnhandledError', error, true); + // {{SQL CARBON EDIT}} + //this._telemetryService.publicLog('UnhandledError', error, true); } this._buffer.length = 0; } diff --git a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts index 87f5f73e90..e1fa04565a 100644 --- a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts @@ -206,35 +206,36 @@ suite('TelemetryService', () => { }); })); - test('Error events', sinon.test(function (this: any) { + // {{SQL CARBON EDIT}} + // test('Error events', sinon.test(function (this: any) { - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); - try { - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); + // try { + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); - let e: any = new Error('This is a test.'); - // for Phantom - if (!e.stack) { - e.stack = 'blah'; - } + // let e: any = new Error('This is a test.'); + // // for Phantom + // if (!e.stack) { + // e.stack = 'blah'; + // } - Errors.onUnexpectedError(e); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - assert.equal(testAppender.getEventsCount(), 1); - assert.equal(testAppender.events[0].eventName, 'UnhandledError'); - assert.equal(testAppender.events[0].data.msg, 'This is a test.'); + // Errors.onUnexpectedError(e); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + // assert.equal(testAppender.getEventsCount(), 1); + // assert.equal(testAppender.events[0].eventName, 'UnhandledError'); + // assert.equal(testAppender.events[0].data.msg, 'This is a test.'); - errorTelemetry.dispose(); - service.dispose(); - } finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); + // errorTelemetry.dispose(); + // service.dispose(); + // } finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); // test('Unhandled Promise Error events', sinon.test(function() { // @@ -265,457 +266,458 @@ suite('TelemetryService', () => { // } // })); - test('Handle global errors', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let testError = new Error('test'); - (window.onerror)('Error Message', 'file.js', 2, 42, testError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.alwaysCalledWithExactly('Error Message', 'file.js', 2, 42, testError), true); - assert.equal(errorStub.callCount, 1); - - assert.equal(testAppender.getEventsCount(), 1); - assert.equal(testAppender.events[0].eventName, 'UnhandledError'); - assert.equal(testAppender.events[0].data.msg, 'Error Message'); - assert.equal(testAppender.events[0].data.file, 'file.js'); - assert.equal(testAppender.events[0].data.line, 2); - assert.equal(testAppender.events[0].data.column, 42); - assert.equal(testAppender.events[0].data.uncaught_error_msg, 'test'); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Error Telemetry removes PII from filename with spaces', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let personInfoWithSpaces = settings.personalInfo.slice(0, 2) + ' ' + settings.personalInfo.slice(2); - let dangerousFilenameError: any = new Error('dangerousFilename'); - dangerousFilenameError.stack = settings.stack; - (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces) + '/test.js', 2, 42, dangerousFilenameError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces)), -1); - assert.equal(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Uncaught Error Telemetry removes PII from filename', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousFilenameError: any = new Error('dangerousFilename'); - dangerousFilenameError.stack = settings.stack; - (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo + '/test.js', 2, 42, dangerousFilenameError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); - - dangerousFilenameError = new Error('dangerousFilename'); - dangerousFilenameError.stack = settings.stack; - (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo + '/test.js', 2, 42, dangerousFilenameError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 2); - assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); - assert.equal(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Unexpected Error Telemetry removes PII', sinon.test(function (this: any) { - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - try { - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithoutImportantInfoError: any = new Error(settings.dangerousPathWithoutImportantInfo); - dangerousPathWithoutImportantInfoError.stack = settings.stack; - Errors.onUnexpectedError(dangerousPathWithoutImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - } - finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); - - test('Uncaught Error Telemetry removes PII', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithoutImportantInfoError: any = new Error('dangerousPathWithoutImportantInfo'); - dangerousPathWithoutImportantInfoError.stack = settings.stack; - (window.onerror)(settings.dangerousPathWithoutImportantInfo, 'test.js', 2, 42, dangerousPathWithoutImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - // Test that no file information remains, esp. personal info - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Unexpected Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { - - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - - try { - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); - dangerousPathWithImportantInfoError.stack = settings.stack; - - // Test that important information remains but personal info does not - Errors.onUnexpectedError(dangerousPathWithImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - } - finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); - - test('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); - dangerousPathWithImportantInfoError.stack = settings.stack; - (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - // Test that important information remains but personal info does not - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Unexpected Error Telemetry removes PII but preserves Code file path with node modules', sinon.test(function (this: any) { - - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - - try { - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); - dangerousPathWithImportantInfoError.stack = settings.stack; - - - Errors.onUnexpectedError(dangerousPathWithImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); - - errorTelemetry.dispose(); - service.dispose(); - } - finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); - - test('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); - dangerousPathWithImportantInfoError.stack = settings.stack; - (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); - - errorTelemetry.dispose(); - service.dispose(); - })); - - - test('Unexpected Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function (this: any) { - - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - - try { - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); - dangerousPathWithImportantInfoError.stack = settings.stack; - - // Test that important information remains but personal info does not - Errors.onUnexpectedError(dangerousPathWithImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - } - finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); - - test('Uncaught Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); - dangerousPathWithImportantInfoError.stack = settings.stack; - (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - // Test that important information remains but personal info does not - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Unexpected Error Telemetry removes PII but preserves Missing Model error message', sinon.test(function (this: any) { - - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - - try { - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let missingModelError: any = new Error(settings.missingModelMessage); - missingModelError.stack = settings.stack; - - // Test that no file information remains, but this particular - // error message does (Received model events for missing model) - Errors.onUnexpectedError(missingModelError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - } finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); - - test('Uncaught Error Telemetry removes PII but preserves Missing Model error message', sinon.test(function (this: any) { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let missingModelError: any = new Error('missingModelMessage'); - missingModelError.stack = settings.stack; - (window.onerror)(settings.missingModelMessage, 'test.js', 2, 42, missingModelError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - // Test that no file information remains, but this particular - // error message does (Received model events for missing model) - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - })); - - test('Unexpected Error Telemetry removes PII but preserves No Such File error message', sinon.test(function (this: any) { - - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - - try { - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let noSuchFileError: any = new Error(settings.noSuchFileMessage); - noSuchFileError.stack = settings.stack; - - // Test that no file information remains, but this particular - // error message does (ENOENT: no such file or directory) - Errors.onUnexpectedError(noSuchFileError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - } finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); - - test('Uncaught Error Telemetry removes PII but preserves No Such File error message', sinon.test(function (this: any) { - let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => { }); - - try { - let errorStub = sinon.stub(); - window.onerror = errorStub; - let settings = new ErrorTestingSettings(); - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined); - const errorTelemetry = new ErrorTelemetry(service); - - let noSuchFileError: any = new Error('noSuchFileMessage'); - noSuchFileError.stack = settings.stack; - (window.onerror)(settings.noSuchFileMessage, 'test.js', 2, 42, noSuchFileError); - this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); - - assert.equal(errorStub.callCount, 1); - // Test that no file information remains, but this particular - // error message does (ENOENT: no such file or directory) - Errors.onUnexpectedError(noSuchFileError); - assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); - - errorTelemetry.dispose(); - service.dispose(); - } finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); - } - })); + // test('Handle global errors', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let testError = new Error('test'); + // (window.onerror)('Error Message', 'file.js', 2, 42, testError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.alwaysCalledWithExactly('Error Message', 'file.js', 2, 42, testError), true); + // assert.equal(errorStub.callCount, 1); + + // assert.equal(testAppender.getEventsCount(), 1); + // assert.equal(testAppender.events[0].eventName, 'UnhandledError'); + // assert.equal(testAppender.events[0].data.msg, 'Error Message'); + // assert.equal(testAppender.events[0].data.file, 'file.js'); + // assert.equal(testAppender.events[0].data.line, 2); + // assert.equal(testAppender.events[0].data.column, 42); + // assert.equal(testAppender.events[0].data.uncaught_error_msg, 'test'); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + // test('Error Telemetry removes PII from filename with spaces', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let personInfoWithSpaces = settings.personalInfo.slice(0, 2) + ' ' + settings.personalInfo.slice(2); + // let dangerousFilenameError: any = new Error('dangerousFilename'); + // dangerousFilenameError.stack = settings.stack; + // (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces) + '/test.js', 2, 42, dangerousFilenameError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces)), -1); + // assert.equal(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + + // test('Uncaught Error Telemetry removes PII from filename', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousFilenameError: any = new Error('dangerousFilename'); + // dangerousFilenameError.stack = settings.stack; + // (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo + '/test.js', 2, 42, dangerousFilenameError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); + + // dangerousFilenameError = new Error('dangerousFilename'); + // dangerousFilenameError.stack = settings.stack; + // (window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo + '/test.js', 2, 42, dangerousFilenameError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 2); + // assert.equal(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); + // assert.equal(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + // test('Unexpected Error Telemetry removes PII', sinon.test(function (this: any) { + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + // try { + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithoutImportantInfoError: any = new Error(settings.dangerousPathWithoutImportantInfo); + // dangerousPathWithoutImportantInfoError.stack = settings.stack; + // Errors.onUnexpectedError(dangerousPathWithoutImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // } + // finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); + + // test('Uncaught Error Telemetry removes PII', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithoutImportantInfoError: any = new Error('dangerousPathWithoutImportantInfo'); + // dangerousPathWithoutImportantInfoError.stack = settings.stack; + // (window.onerror)(settings.dangerousPathWithoutImportantInfo, 'test.js', 2, 42, dangerousPathWithoutImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // // Test that no file information remains, esp. personal info + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + // test('Unexpected Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { + + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + + // try { + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); + // dangerousPathWithImportantInfoError.stack = settings.stack; + + // // Test that important information remains but personal info does not + // Errors.onUnexpectedError(dangerousPathWithImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // } + // finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); + + // test('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); + // dangerousPathWithImportantInfoError.stack = settings.stack; + // (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // // Test that important information remains but personal info does not + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + // test('Unexpected Error Telemetry removes PII but preserves Code file path with node modules', sinon.test(function (this: any) { + + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + + // try { + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); + // dangerousPathWithImportantInfoError.stack = settings.stack; + + + // Errors.onUnexpectedError(dangerousPathWithImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); + + // errorTelemetry.dispose(); + // service.dispose(); + // } + // finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); + + // test('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); + // dangerousPathWithImportantInfoError.stack = settings.stack; + // (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + + // test('Unexpected Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function (this: any) { + + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + + // try { + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); + // dangerousPathWithImportantInfoError.stack = settings.stack; + + // // Test that important information remains but personal info does not + // Errors.onUnexpectedError(dangerousPathWithImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // } + // finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); + + // test('Uncaught Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); + // dangerousPathWithImportantInfoError.stack = settings.stack; + // (window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // // Test that important information remains but personal info does not + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + // test('Unexpected Error Telemetry removes PII but preserves Missing Model error message', sinon.test(function (this: any) { + + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + + // try { + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let missingModelError: any = new Error(settings.missingModelMessage); + // missingModelError.stack = settings.stack; + + // // Test that no file information remains, but this particular + // // error message does (Received model events for missing model) + // Errors.onUnexpectedError(missingModelError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // } finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); + + // test('Uncaught Error Telemetry removes PII but preserves Missing Model error message', sinon.test(function (this: any) { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let missingModelError: any = new Error('missingModelMessage'); + // missingModelError.stack = settings.stack; + // (window.onerror)(settings.missingModelMessage, 'test.js', 2, 42, missingModelError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // // Test that no file information remains, but this particular + // // error message does (Received model events for missing model) + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // })); + + // test('Unexpected Error Telemetry removes PII but preserves No Such File error message', sinon.test(function (this: any) { + + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + + // try { + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let noSuchFileError: any = new Error(settings.noSuchFileMessage); + // noSuchFileError.stack = settings.stack; + + // // Test that no file information remains, but this particular + // // error message does (ENOENT: no such file or directory) + // Errors.onUnexpectedError(noSuchFileError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // } finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); + + // test('Uncaught Error Telemetry removes PII but preserves No Such File error message', sinon.test(function (this: any) { + // let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + // Errors.setUnexpectedErrorHandler(() => { }); + + // try { + // let errorStub = sinon.stub(); + // window.onerror = errorStub; + // let settings = new ErrorTestingSettings(); + // let testAppender = new TestTelemetryAppender(); + // let service = new TelemetryService({ appender: testAppender }, undefined); + // const errorTelemetry = new ErrorTelemetry(service); + + // let noSuchFileError: any = new Error('noSuchFileMessage'); + // noSuchFileError.stack = settings.stack; + // (window.onerror)(settings.noSuchFileMessage, 'test.js', 2, 42, noSuchFileError); + // this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + // assert.equal(errorStub.callCount, 1); + // // Test that no file information remains, but this particular + // // error message does (ENOENT: no such file or directory) + // Errors.onUnexpectedError(noSuchFileError); + // assert.notEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); + // assert.equal(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); + // assert.notEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + // assert.equal(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + + // errorTelemetry.dispose(); + // service.dispose(); + // } finally { + // Errors.setUnexpectedErrorHandler(origErrorHandler); + // } + // })); test('Telemetry Service sends events when enableTelemetry is on', sinon.test(function () { let testAppender = new TestTelemetryAppender(); From 82486ee22e859307e481d9639330949742fe8f94 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Mon, 22 Oct 2018 23:53:28 -0700 Subject: [PATCH 13/13] Wizard message UI improvement (#2971) * improve the wizard message experience - WIP * undo gitignorechanges * fix expand button issue * fix the cursor issue * use flex to control height * toggle message detail support * apply style * new images * use template string * address comments --- src/sql/base/browser/ui/modal/media/close.svg | 1 + .../browser/ui/modal/media/close_inverse.svg | 1 + src/sql/base/browser/ui/modal/media/copy.svg | 1 + .../browser/ui/modal/media/copy_inverse.svg | 1 + .../ui/modal/media/error_notification.svg | 1 + .../media/error_notification_inverse.svg | 1 + .../ui/modal/media/info_notification.svg | 1 + .../modal/media/info_notification_inverse.svg | 1 + src/sql/base/browser/ui/modal/media/modal.css | 145 ++++++++++++--- .../browser/ui/modal/media/show_details.svg | 1 + .../ui/modal/media/show_details_inverse.svg | 1 + .../ui/modal/media/warning_notification.svg | 1 + .../media/warning_notification_inverse.svg | 1 + src/sql/base/browser/ui/modal/modal.ts | 169 ++++++++++++++---- .../base/browser/ui/modal/optionsDialog.ts | 6 +- .../base/browser/ui/modal/webViewDialog.ts | 4 +- .../accountDialog/accountDialog.ts | 5 +- .../autoOAuthDialog/autoOAuthDialog.ts | 5 +- .../firewallRuleDialog/firewallRuleDialog.ts | 3 + .../connectionDialogWidget.ts | 6 +- .../newDashboardTabDialog.ts | 5 +- .../disasterRecovery/backup/backupDialog.ts | 6 +- .../disasterRecovery/restore/restoreDialog.ts | 6 +- .../parts/fileBrowser/fileBrowserDialog.ts | 6 +- .../insights/browser/insightsDialogView.ts | 6 +- .../serverGroupDialog/serverGroupDialog.ts | 6 +- .../dialog/profilerColumnEditorDialog.ts | 6 +- src/sql/platform/dialog/dialogModal.ts | 8 +- src/sql/platform/dialog/dialogTypes.ts | 12 +- src/sql/platform/dialog/media/dialogModal.css | 1 + .../dialog/media/wizardNavigation.css | 5 +- src/sql/platform/dialog/wizardModal.ts | 10 +- src/sql/sqlops.proposed.d.ts | 1 + .../workbench/api/common/sqlExtHostTypes.ts | 1 + .../errorMessageDialog/errorMessageDialog.ts | 4 +- .../accountDialogController.test.ts | 2 +- .../autoOAuthDialogController.test.ts | 2 +- .../firewallRuleDialogController.test.ts | 2 +- 38 files changed, 339 insertions(+), 105 deletions(-) create mode 100644 src/sql/base/browser/ui/modal/media/close.svg create mode 100644 src/sql/base/browser/ui/modal/media/close_inverse.svg create mode 100644 src/sql/base/browser/ui/modal/media/copy.svg create mode 100644 src/sql/base/browser/ui/modal/media/copy_inverse.svg create mode 100644 src/sql/base/browser/ui/modal/media/error_notification.svg create mode 100644 src/sql/base/browser/ui/modal/media/error_notification_inverse.svg create mode 100644 src/sql/base/browser/ui/modal/media/info_notification.svg create mode 100644 src/sql/base/browser/ui/modal/media/info_notification_inverse.svg create mode 100644 src/sql/base/browser/ui/modal/media/show_details.svg create mode 100644 src/sql/base/browser/ui/modal/media/show_details_inverse.svg create mode 100644 src/sql/base/browser/ui/modal/media/warning_notification.svg create mode 100644 src/sql/base/browser/ui/modal/media/warning_notification_inverse.svg diff --git a/src/sql/base/browser/ui/modal/media/close.svg b/src/sql/base/browser/ui/modal/media/close.svg new file mode 100644 index 0000000000..e0c5fb71f2 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/close.svg @@ -0,0 +1 @@ +close \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/close_inverse.svg b/src/sql/base/browser/ui/modal/media/close_inverse.svg new file mode 100644 index 0000000000..1763fe5353 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/close_inverse.svg @@ -0,0 +1 @@ +close copy \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/copy.svg b/src/sql/base/browser/ui/modal/media/copy.svg new file mode 100644 index 0000000000..91692de258 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/copy.svg @@ -0,0 +1 @@ +copy \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/copy_inverse.svg b/src/sql/base/browser/ui/modal/media/copy_inverse.svg new file mode 100644 index 0000000000..fa65571127 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/copy_inverse.svg @@ -0,0 +1 @@ +copy_inverse \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/error_notification.svg b/src/sql/base/browser/ui/modal/media/error_notification.svg new file mode 100644 index 0000000000..37901e4913 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/error_notification.svg @@ -0,0 +1 @@ +error_notification \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/error_notification_inverse.svg b/src/sql/base/browser/ui/modal/media/error_notification_inverse.svg new file mode 100644 index 0000000000..2b51d32010 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/error_notification_inverse.svg @@ -0,0 +1 @@ +error_notification_inverse \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/info_notification.svg b/src/sql/base/browser/ui/modal/media/info_notification.svg new file mode 100644 index 0000000000..6b20c7a149 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/info_notification.svg @@ -0,0 +1 @@ +info_notification \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/info_notification_inverse.svg b/src/sql/base/browser/ui/modal/media/info_notification_inverse.svg new file mode 100644 index 0000000000..f32f61aa9c --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/info_notification_inverse.svg @@ -0,0 +1 @@ +info_notification_inverse \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/modal.css b/src/sql/base/browser/ui/modal/media/modal.css index 6e120f15be..6493b52181 100644 --- a/src/sql/base/browser/ui/modal/media/modal.css +++ b/src/sql/base/browser/ui/modal/media/modal.css @@ -50,6 +50,8 @@ .monaco-shell .modal.flyout-dialog .modal-content { height: 100%; font-size: 11px; + display: flex; + flex-direction: column; } .modal .modal-title { @@ -63,13 +65,10 @@ margin-right: 10px; } -.monaco-shell .modal.flyout-dialog .modal-body { - height: calc(100% - 105px); -} - -/* modal body for angular component dialog */ +.monaco-shell .modal.flyout-dialog .modal-body, .monaco-shell .modal.flyout-dialog .angular-modal-body { - height: calc(100% - 90px); + margin-bottom: auto; + height: 100%; } /* Style for body and footer in modal dialog. This should be applied to dialog created with angular component. */ @@ -128,13 +127,6 @@ border: 1px solid transparent; } -.modal.flyout-dialog .dialogErrorMessage { - overflow: hidden; - padding-left: 10px; - overflow-y: auto; - display: flex; -} - .modal .modal-footer { display: flex; } @@ -167,27 +159,122 @@ margin-right: none; } -.modal.flyout-dialog .icon.error { - float: left; - margin-right: 10px; - width: 20px; - height: 20px; + +.modal.flyout-dialog .dialog-message { + padding: 6px 10px 10px 10px; + font-size: 13px; + display: flex; + flex-direction: column; } -.modal .modal-footer .dialogErrorMessage { - align-items: center; - max-height: 30px; - margin-right: 5px; +.modal.flyout-dialog .dialog-message.error { + background-color:#B62E00 !important; + color:#FFFFFF !important; } -.modal .dialogErrorMessage .icon { - float: left; - margin-right: 10px; - width: auto; - height: 20px; +.modal.flyout-dialog .dialog-message.warning { + background-color:#F9E385 !important; + color:#4A4A4A !important; } -.modal .modal-footer .dialogErrorMessage .errorMessage { - max-height: 100%; +.modal.flyout-dialog .dialog-message.info { + background-color:#0078D7 !important; + color:#FFFFFF !important; +} + +.modal.flyout-dialog .dialog-message-header { + overflow: hidden; + overflow-y: hidden; + display: flex; + flex-direction: row; +} + +.modal.flyout-dialog .dialog-message-body { + margin-left: 26px; overflow-y: scroll; + overflow-x: hidden; + max-height: 90px; + display: flex; + flex-direction: column; +} + +.modal.flyout-dialog .dialog-message-icon { + margin-right: 10px; + margin-top: 5px; + width: 16px; + height: 16px; + min-width: 16px; +} + +.modal.flyout-dialog .dialog-message-severity { + margin-right: auto; + line-height: 26px; +} + +.modal.flyout-dialog .dialog-message-button { + min-width: 60px; + margin-right: 10px; +} + +.modal.flyout-dialog .dialog-message-button > a { + background-position-x: 2px !important; + background-color: inherit !important; + color:inherit !important; + padding-left: 22px !important; + background-size: 16px 16px !important; + text-align: left !important; +} + +.modal.flyout-dialog .dialog-message-summary { + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.modal.flyout-dialog .dialog-message-summary.expanded { + white-space: normal; + overflow-x: unset; +} + +.modal.flyout-dialog .dialog-message-detail { + margin-top: 5px; + white-space: normal; + -webkit-user-select: text; + font-size: 11px; +} + +.close-message-icon { + background: url('close_inverse.svg') center center no-repeat; +} + +.dialog-message.warning .close-message-icon { + background: url('close.svg') center center no-repeat; +} + +.copy-message-icon { + background: url('copy_inverse.svg') center center no-repeat; +} + +.dialog-message.warning .copy-message-icon { + background: url('copy.svg') center center no-repeat; +} + +.message-details-icon { + background: url('show_details_inverse.svg') center center no-repeat; +} + +.dialog-message.warning .message-details-icon { + background: url('show_details.svg') center center no-repeat; +} + +.dialog-message.info .dialog-message-icon { + background: url('info_notification_inverse.svg') center center no-repeat; +} + +.dialog-message.warning .dialog-message-icon { + background: url('warning_notification.svg') center center no-repeat; +} + +.dialog-message.error .dialog-message-icon { + background: url('error_notification_inverse.svg') center center no-repeat; } \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/show_details.svg b/src/sql/base/browser/ui/modal/media/show_details.svg new file mode 100644 index 0000000000..1fb58da242 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/show_details.svg @@ -0,0 +1 @@ +show_details \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/show_details_inverse.svg b/src/sql/base/browser/ui/modal/media/show_details_inverse.svg new file mode 100644 index 0000000000..0ae07bc622 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/show_details_inverse.svg @@ -0,0 +1 @@ +show_details_inverse \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/warning_notification.svg b/src/sql/base/browser/ui/modal/media/warning_notification.svg new file mode 100644 index 0000000000..34a28f8280 --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/warning_notification.svg @@ -0,0 +1 @@ +warning_notification \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/media/warning_notification_inverse.svg b/src/sql/base/browser/ui/modal/media/warning_notification_inverse.svg new file mode 100644 index 0000000000..c2f3c2103d --- /dev/null +++ b/src/sql/base/browser/ui/modal/media/warning_notification_inverse.svg @@ -0,0 +1 @@ +warning_notification_inverse \ No newline at end of file diff --git a/src/sql/base/browser/ui/modal/modal.ts b/src/sql/base/browser/ui/modal/modal.ts index 65540daf0e..55b0d968a2 100644 --- a/src/sql/base/browser/ui/modal/modal.ts +++ b/src/sql/base/browser/ui/modal/modal.ts @@ -2,7 +2,6 @@ * 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!sql/media/icons/common-icons'; import 'vs/css!./media/modal'; import { IThemable } from 'vs/platform/theme/common/styler'; import { Color } from 'vs/base/common/color'; @@ -16,18 +15,25 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Button } from 'sql/base/browser/ui/button/button'; import * as TelemetryUtils from 'sql/common/telemetryUtilities'; import * as TelemetryKeys from 'sql/common/telemetryKeys'; import { localize } from 'vs/nls'; import { MessageLevel } from 'sql/workbench/api/common/sqlExtHostTypes'; +import * as os from 'os'; export const MODAL_SHOWING_KEY = 'modalShowing'; export const MODAL_SHOWING_CONTEXT = new RawContextKey>(MODAL_SHOWING_KEY, []); -const INFO_ALT_TEXT = localize('infoAltText', 'Info'); +const INFO_ALT_TEXT = localize('infoAltText', 'Infomation'); const WARNING_ALT_TEXT = localize('warningAltText', 'Warning'); const ERROR_ALT_TEXT = localize('errorAltText', 'Error'); +const SHOW_DETAILS_TEXT = localize('showMessageDetails', 'Show Details'); +const HIDE_DETAILS_TEXT = localize('hideMessageDetails', 'Hide Details'); +const COPY_TEXT = localize('copyMessage', 'Copy'); +const CLOSE_TEXT = localize('closeMessage', 'Close'); +const MESSAGE_EXPANDED_MODE_CLASS = 'expanded'; export interface IModalDialogStyles { dialogForeground?: Color; @@ -66,10 +72,20 @@ const defaultOptions: IModalOptions = { export abstract class Modal extends Disposable implements IThemable { - private _errorMessage: Builder; - private _spinnerElement: HTMLElement; - private _errorIconElement: HTMLElement; + private _messageElement: HTMLElement; + private _messageIcon: HTMLElement; + private _messageSeverity: Builder; + private _messageSummary: Builder; + private _messageSummaryElement: HTMLElement; + private _messageDetail: Builder; + private _messageDetailElement: HTMLElement; + private _toggleMessageDetailButton: Button; + private _copyMessageButton: Button; + private _closeMessageButton: Button; + private _messageSummaryText: string; + private _messageDetailText: string; + private _spinnerElement: HTMLElement; private _focusableElements: NodeListOf; private _firstFocusableElement: HTMLElement; private _lastFocusableElement: HTMLElement; @@ -82,6 +98,7 @@ export abstract class Modal extends Disposable implements IThemable { private _modalDialog: Builder; private _modalHeaderSection: Builder; + private _modalMessageSection: Builder; private _modalBodySection: HTMLElement; private _modalFooterSection: Builder; private _closeButtonInHeader: Builder; @@ -135,6 +152,7 @@ export abstract class Modal extends Disposable implements IThemable { private _name: string, private _partService: IPartService, private _telemetryService: ITelemetryService, + protected _clipboardService: IClipboardService, _contextKeyService: IContextKeyService, options?: IModalOptions ) { @@ -149,7 +167,7 @@ export abstract class Modal extends Disposable implements IThemable { /** * Build and render the modal, will call {@link Modal#renderBody} */ - public render(errorMessagesInFooter: boolean = false) { + public render() { let modalBodyClass = (this._modalOptions.isAngular === false ? 'modal-body' : 'modal-body-and-footer'); let parts: Array = []; // This modal header section refers to the header of of the dialog @@ -176,11 +194,68 @@ export abstract class Modal extends Disposable implements IThemable { parts.push(this._modalHeaderSection.getHTMLElement()); } + if (this._modalOptions.isAngular === false && this._modalOptions.hasErrors) { + + this._modalMessageSection = $().div({ class: 'dialog-message error' }, (messageContainer) => { + messageContainer.div({ class: 'dialog-message-header' }, (headerContainer) => { + headerContainer.div({ class: 'dialog-message-icon' }, (iconContainer) => { + this._messageIcon = iconContainer.getHTMLElement(); + }); + headerContainer.div({ class: 'dialog-message-severity' }, (messageSeverityContainer) => { + this._messageSeverity = messageSeverityContainer; + }); + headerContainer.div({ class: 'dialog-message-button' }, (buttonContainer) => { + this._toggleMessageDetailButton = new Button(buttonContainer); + this._toggleMessageDetailButton.icon = 'message-details-icon'; + this._toggleMessageDetailButton.label = SHOW_DETAILS_TEXT; + this._toggleMessageDetailButton.onDidClick((e) => { + this.toggleMessageDetail(); + }); + }); + headerContainer.div({ class: 'dialog-message-button' }, (buttonContainer) => { + this._copyMessageButton = new Button(buttonContainer); + this._copyMessageButton.icon = 'copy-message-icon'; + this._copyMessageButton.label = COPY_TEXT; + this._copyMessageButton.onDidClick((e) => { + this._clipboardService.writeText(this.getTextForClipboard()); + }); + }); + headerContainer.div({ class: 'dialog-message-button' }, (buttonContainer) => { + this._closeMessageButton = new Button(buttonContainer); + this._closeMessageButton.icon = 'close-message-icon'; + this._closeMessageButton.label = CLOSE_TEXT; + this._closeMessageButton.onDidClick((e) => { + this.setError(undefined); + }); + }); + }); + messageContainer.div({ class: 'dialog-message-body' }, (messageBody) => { + messageBody.div({ class: 'dialog-message-summary' }, (summaryContainer) => { + this._messageSummary = summaryContainer; + this._messageSummaryElement = summaryContainer.getHTMLElement(); + this._messageSummaryElement.onclick = (e) => { + this.toggleMessageDetail(); + }; + }); + messageBody.div({ class: 'dialog-message-detail' }, (detailContainer) => { + this._messageDetail = detailContainer; + this._messageDetailElement = detailContainer.getHTMLElement(); + this._messageDetailElement.style.display = 'none'; + }); + }); + }); + this._messageElement = this._modalMessageSection.getHTMLElement(); + this.updateElementVisibility(this._messageElement, false); + + parts.push(this._messageElement); + } + // This modal body section refers to the body of of the dialog let body: Builder; $().div({ class: modalBodyClass }, (builder) => { body = builder; }); + this._modalBodySection = body.getHTMLElement(); parts.push(body.getHTMLElement()); @@ -214,19 +289,6 @@ export abstract class Modal extends Disposable implements IThemable { builderClass += ' wide'; } - if (this._modalOptions.isAngular === false && this._modalOptions.hasErrors) { - let builder = errorMessagesInFooter ? this._leftFooter : body; - builder.div({ class: 'dialogErrorMessage', id: 'dialogErrorMessage' }, (errorMessageContainer) => { - errorMessageContainer.div({ class: 'sql icon error' }, (iconContainer) => { - this._errorIconElement = iconContainer.getHTMLElement(); - this._errorIconElement.style.visibility = 'hidden'; - }); - errorMessageContainer.div({ class: 'errorMessage' }, (messageContainer) => { - this._errorMessage = messageContainer; - }); - }); - } - // The builder builds the dialog. It append header, body and footer sections. this._builder = $().div({ class: builderClass, 'role': 'dialog', 'aria-label': this._title }, (dialogContainer) => { this._modalDialog = dialogContainer.div({ class: 'modal-dialog ', role: 'document' }, (modalDialog) => { @@ -273,6 +335,39 @@ export abstract class Modal extends Disposable implements IThemable { } } + private getTextForClipboard(): string { + return this._messageDetailText === '' ? this._messageSummaryText : `${this._messageSummaryText}${os.EOL}========================${os.EOL}${this._messageDetailText}`; + } + + private updateElementVisibility(element: HTMLElement, visible: boolean) { + element.style.display = visible ? 'block' : 'none'; + } + + private updateExpandMessageState() { + this._messageSummaryElement.style.cursor = this.shouldShowExpandMessageButton ? 'pointer' : 'default'; + this._messageSummaryElement.classList.remove(MESSAGE_EXPANDED_MODE_CLASS); + this.updateElementVisibility(this._toggleMessageDetailButton.element, this.shouldShowExpandMessageButton); + } + + private toggleMessageDetail() { + let isExpanded = this._messageSummaryElement.classList.contains(MESSAGE_EXPANDED_MODE_CLASS); + if (isExpanded) { + this._messageSummaryElement.classList.remove(MESSAGE_EXPANDED_MODE_CLASS); + this._toggleMessageDetailButton.label = SHOW_DETAILS_TEXT; + } else { + this._messageSummaryElement.classList.add(MESSAGE_EXPANDED_MODE_CLASS); + this._toggleMessageDetailButton.label = HIDE_DETAILS_TEXT; + } + + if (this._messageDetailText !== '') { + this.updateElementVisibility(this._messageDetailElement, !isExpanded); + } + } + + private get shouldShowExpandMessageButton(): boolean { + return this._messageDetailText !== '' || this._messageSummaryElement.scrollWidth > this._messageSummaryElement.offsetWidth; + } + /** * Set focusable elements in the modal dialog */ @@ -374,35 +469,45 @@ export abstract class Modal extends Disposable implements IThemable { /** * Show an error in the error message element - * @param err Text to show in the error message + * @param message Text to show in the message + * @param level Severity level of the message + * @param description Description of the message */ - protected setError(err: string, level: MessageLevel = MessageLevel.Error) { + protected setError(message: string, level: MessageLevel = MessageLevel.Error, description: string = '') { if (this._modalOptions.hasErrors) { - if (err === '') { - this._errorIconElement.style.visibility = 'hidden'; - } else { + this._messageSummaryText = message ? message : ''; + this._messageDetailText = description ? description : ''; + + if (this._messageSummaryText !== '') { const levelClasses = ['info', 'warning', 'error']; let selectedLevel = levelClasses[2]; - let altText = ERROR_ALT_TEXT; + let severityText = ERROR_ALT_TEXT; if (level === MessageLevel.Information) { selectedLevel = levelClasses[0]; - altText = INFO_ALT_TEXT; + severityText = INFO_ALT_TEXT; } else if (level === MessageLevel.Warning) { selectedLevel = levelClasses[1]; - altText = WARNING_ALT_TEXT; + severityText = WARNING_ALT_TEXT; } levelClasses.forEach(level => { if (selectedLevel === level) { - this._errorIconElement.classList.add(level); + this._messageIcon.classList.add(level); + this._messageElement.classList.add(level); } else { - this._errorIconElement.classList.remove(level); + this._messageIcon.classList.remove(level); + this._messageElement.classList.remove(level); } }); - this._errorIconElement.title = altText; - this._errorIconElement.style.visibility = 'visible'; + this._messageIcon.title = severityText; + this._messageSeverity.text(severityText); + this._messageSummary.text(message); + this._messageSummary.title(message); + this._messageDetail.text(description); } - this._errorMessage.text(err); + this.updateElementVisibility(this._messageDetailElement, false); + this.updateElementVisibility(this._messageElement, this._messageSummaryText !== ''); + this.updateExpandMessageState(); } } diff --git a/src/sql/base/browser/ui/modal/optionsDialog.ts b/src/sql/base/browser/ui/modal/optionsDialog.ts index 9599c63c2a..f3a477c66a 100644 --- a/src/sql/base/browser/ui/modal/optionsDialog.ts +++ b/src/sql/base/browser/ui/modal/optionsDialog.ts @@ -30,6 +30,7 @@ import { SplitView, CollapsibleState } from 'sql/base/browser/ui/splitview/split import { Builder, $ } from 'vs/base/browser/builder'; import { Widget } from 'vs/base/browser/ui/widget'; import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export class CategoryView extends FixedCollapsibleView { private _treecontainer: HTMLElement; @@ -95,9 +96,10 @@ export class OptionsDialog extends Modal { @IWorkbenchThemeService private _themeService: IWorkbenchThemeService, @IContextViewService private _contextViewService: IContextViewService, @ITelemetryService telemetryService: ITelemetryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService ) { - super(title, name, partService, telemetryService, contextKeyService, options); + super(title, name, partService, telemetryService, clipboardService, contextKeyService, options); } public render() { diff --git a/src/sql/base/browser/ui/modal/webViewDialog.ts b/src/sql/base/browser/ui/modal/webViewDialog.ts index 3019de5b4e..3bd0141688 100644 --- a/src/sql/base/browser/ui/modal/webViewDialog.ts +++ b/src/sql/base/browser/ui/modal/webViewDialog.ts @@ -46,7 +46,7 @@ export class WebViewDialog extends Modal { constructor( @IThemeService private _themeService: IThemeService, - @IClipboardService private _clipboardService: IClipboardService, + @IClipboardService clipboardService: IClipboardService, @IPartService private _webViewPartService: IPartService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @@ -54,7 +54,7 @@ export class WebViewDialog extends Modal { @IEnvironmentService private _environmentService: IEnvironmentService, @IInstantiationService private _instantiationService: IInstantiationService ) { - super('', TelemetryKeys.WebView, _webViewPartService, telemetryService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + super('', TelemetryKeys.WebView, _webViewPartService, telemetryService, clipboardService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); this._okLabel = localize('webViewDialog.ok', 'OK'); this._closeLabel = localize('webViewDialog.close', 'Close'); } diff --git a/src/sql/parts/accountManagement/accountDialog/accountDialog.ts b/src/sql/parts/accountManagement/accountDialog/accountDialog.ts index db17e6480c..0e3d1f822d 100644 --- a/src/sql/parts/accountManagement/accountDialog/accountDialog.ts +++ b/src/sql/parts/accountManagement/accountDialog/accountDialog.ts @@ -33,6 +33,7 @@ import { AccountListRenderer, AccountListDelegate } from 'sql/parts/accountManag import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/services/accountManagement/eventTypes'; import { FixedListView } from 'sql/platform/views/fixedListView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; export interface IProviderViewUiComponent { view: FixedListView; @@ -72,13 +73,15 @@ export class AccountDialog extends Modal { @IContextMenuService private _contextMenuService: IContextMenuService, @IKeybindingService private _keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService ) { super( localize('linkedAccounts', 'Linked accounts'), TelemetryKeys.Accounts, partService, telemetryService, + clipboardService, contextKeyService, { hasSpinner: true } ); diff --git a/src/sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialog.ts b/src/sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialog.ts index e8f90934f6..0afe325cc6 100644 --- a/src/sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialog.ts +++ b/src/sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialog.ts @@ -21,6 +21,7 @@ import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/common/telemetryKeys'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export class AutoOAuthDialog extends Modal { private _copyAndOpenButton: Button; @@ -45,13 +46,15 @@ export class AutoOAuthDialog extends Modal { @IThemeService private _themeService: IThemeService, @IContextViewService private _contextViewService: IContextViewService, @ITelemetryService telemetryService: ITelemetryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService ) { super( '', TelemetryKeys.AutoOAuth, partService, telemetryService, + clipboardService, contextKeyService, { isFlyout: true, diff --git a/src/sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog.ts b/src/sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog.ts index 4dd6e9f500..6dcb739d32 100644 --- a/src/sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog.ts +++ b/src/sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog.ts @@ -29,6 +29,7 @@ import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/st import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import { IAccountPickerService } from 'sql/parts/accountManagement/common/interfaces'; import * as TelemetryKeys from 'sql/common/telemetryKeys'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; // TODO: Make the help link 1) extensible (01/08/2018, https://github.com/Microsoft/azuredatastudio/issues/450) // in case that other non-Azure sign in is to be used @@ -70,12 +71,14 @@ export class FirewallRuleDialog extends Modal { @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @IWindowsService private _windowsService: IWindowsService, + @IClipboardService clipboardService: IClipboardService ) { super( localize('createNewFirewallRule', 'Create new firewall rule'), TelemetryKeys.FireWallRule, partService, telemetryService, + clipboardService, contextKeyService, { isFlyout: true, diff --git a/src/sql/parts/connection/connectionDialog/connectionDialogWidget.ts b/src/sql/parts/connection/connectionDialog/connectionDialogWidget.ts index 6a409fc6c6..1832b63b52 100644 --- a/src/sql/parts/connection/connectionDialog/connectionDialogWidget.ts +++ b/src/sql/parts/connection/connectionDialog/connectionDialogWidget.ts @@ -36,6 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import * as styler from 'vs/platform/theme/common/styler'; import * as DOM from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export interface OnShowUIResponse { selectedProviderType: string; @@ -92,9 +93,10 @@ export class ConnectionDialogWidget extends Modal { @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService private _contextMenuService: IContextMenuService, - @IContextViewService private _contextViewService: IContextViewService + @IContextViewService private _contextViewService: IContextViewService, + @IClipboardService clipboardService: IClipboardService ) { - super(localize('connection', 'Connection'), TelemetryKeys.Connection, _partService, telemetryService, contextKeyService, { hasSpinner: true, hasErrors: true }); + super(localize('connection', 'Connection'), TelemetryKeys.Connection, _partService, telemetryService, clipboardService, contextKeyService, { hasSpinner: true, hasErrors: true }); } public refresh(): void { diff --git a/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts index e0d25d379c..5767cd945a 100644 --- a/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts +++ b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts @@ -32,6 +32,7 @@ import * as TelemetryKeys from 'sql/common/telemetryKeys'; import { Orientation } from 'sql/base/browser/ui/splitview/splitview'; import { NewDashboardTabViewModel, IDashboardUITab } from 'sql/parts/dashboard/newDashboardTabDialog/newDashboardTabViewModel'; import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; class ExtensionListDelegate implements IVirtualDelegate { @@ -125,13 +126,15 @@ export class NewDashboardTabDialog extends Modal { @IContextMenuService private _contextMenuService: IContextMenuService, @IKeybindingService private _keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService ) { super( localize('newDashboardTab.openDashboardExtensions', 'Open dashboard extensions'), TelemetryKeys.AddNewDashboardTab, partService, telemetryService, + clipboardService, contextKeyService, { hasSpinner: true } ); diff --git a/src/sql/parts/disasterRecovery/backup/backupDialog.ts b/src/sql/parts/disasterRecovery/backup/backupDialog.ts index e810adb47e..fd9a2f5deb 100644 --- a/src/sql/parts/disasterRecovery/backup/backupDialog.ts +++ b/src/sql/parts/disasterRecovery/backup/backupDialog.ts @@ -18,6 +18,7 @@ import { Builder } from 'vs/base/browser/builder'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { bootstrapAngular } from 'sql/services/bootstrap/bootstrapService'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export class BackupDialog extends Modal { private _bodyBuilder: Builder; @@ -31,9 +32,10 @@ export class BackupDialog extends Modal { @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService private _instantiationService: IInstantiationService + @IInstantiationService private _instantiationService: IInstantiationService, + @IClipboardService clipboardService: IClipboardService ) { - super('', TelemetryKeys.Backup, partService, telemetryService, contextKeyService, { isAngular: true, hasErrors: true }); + super('', TelemetryKeys.Backup, partService, telemetryService, clipboardService, contextKeyService, { isAngular: true, hasErrors: true }); } protected renderBody(container: HTMLElement) { diff --git a/src/sql/parts/disasterRecovery/restore/restoreDialog.ts b/src/sql/parts/disasterRecovery/restore/restoreDialog.ts index a19d4199f7..5decd95fc1 100644 --- a/src/sql/parts/disasterRecovery/restore/restoreDialog.ts +++ b/src/sql/parts/disasterRecovery/restore/restoreDialog.ts @@ -42,6 +42,7 @@ import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown'; import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel'; import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IFileBrowserDialogController } from 'sql/parts/fileBrowser/common/interfaces'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; interface FileListElement { logicalFileName: string; @@ -133,9 +134,10 @@ export class RestoreDialog extends Modal { @IContextViewService private _contextViewService: IContextViewService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, - @IFileBrowserDialogController private fileBrowserDialogService: IFileBrowserDialogController + @IFileBrowserDialogController private fileBrowserDialogService: IFileBrowserDialogController, + @IClipboardService clipboardService: IClipboardService ) { - super(localize('RestoreDialogTitle', 'Restore database'), TelemetryKeys.Restore, partService, telemetryService, contextKeyService, { hasErrors: true, isWide: true, hasSpinner: true }); + super(localize('RestoreDialogTitle', 'Restore database'), TelemetryKeys.Restore, partService, telemetryService, clipboardService, contextKeyService, { hasErrors: true, isWide: true, hasSpinner: true }); this._restoreTitle = localize('restoreDialog.restoreTitle', 'Restore database'); this._databaseTitle = localize('restoreDialog.database', 'Database'); this._backupFileTitle = localize('restoreDialog.backupFile', 'Backup file'); diff --git a/src/sql/parts/fileBrowser/fileBrowserDialog.ts b/src/sql/parts/fileBrowser/fileBrowserDialog.ts index f92fc09ad1..662a53de4a 100644 --- a/src/sql/parts/fileBrowser/fileBrowserDialog.ts +++ b/src/sql/parts/fileBrowser/fileBrowserDialog.ts @@ -34,6 +34,7 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; export class FileBrowserDialog extends Modal { private _viewModel: FileBrowserViewModel; @@ -56,9 +57,10 @@ export class FileBrowserDialog extends Modal { @IInstantiationService private _instantiationService: IInstantiationService, @IContextViewService private _contextViewService: IContextViewService, @ITelemetryService telemetryService: ITelemetryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService ) { - super(title, TelemetryKeys.Backup, partService, telemetryService, contextKeyService, { isFlyout: true, hasTitleIcon: false, hasBackButton: true, hasSpinner: true }); + super(title, TelemetryKeys.Backup, partService, telemetryService, clipboardService, contextKeyService, { isFlyout: true, hasTitleIcon: false, hasBackButton: true, hasSpinner: true }); this._viewModel = this._instantiationService.createInstance(FileBrowserViewModel); this._viewModel.onAddFileTree(args => this.handleOnAddFileTree(args.rootNode, args.selectedNode, args.expandedNodes)); this._viewModel.onPathValidate(args => this.handleOnValidate(args.succeeded, args.message)); diff --git a/src/sql/parts/insights/browser/insightsDialogView.ts b/src/sql/parts/insights/browser/insightsDialogView.ts index b27c8856ed..8ea4708d0f 100644 --- a/src/sql/parts/insights/browser/insightsDialogView.ts +++ b/src/sql/parts/insights/browser/insightsDialogView.ts @@ -39,6 +39,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { MenuRegistry, ExecuteCommandAction } from 'vs/platform/actions/common/actions'; import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; const labelDisplay = nls.localize("insights.item", "Item"); const valueDisplay = nls.localize("insights.value", "Value"); @@ -131,9 +132,10 @@ export class InsightsDialogView extends Modal { @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @ICommandService private _commandService: ICommandService, - @ICapabilitiesService private _capabilitiesService: ICapabilitiesService + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, + @IClipboardService clipboardService: IClipboardService ) { - super(nls.localize("InsightsDialogTitle", "Insights"), TelemetryKeys.Insights, partService, telemetryService, contextKeyService); + super(nls.localize("InsightsDialogTitle", "Insights"), TelemetryKeys.Insights, partService, telemetryService, clipboardService, contextKeyService); this._model.onDataChange(e => this.build()); } diff --git a/src/sql/parts/objectExplorer/serverGroupDialog/serverGroupDialog.ts b/src/sql/parts/objectExplorer/serverGroupDialog/serverGroupDialog.ts index 28d7cac8d6..8ea56e6e21 100644 --- a/src/sql/parts/objectExplorer/serverGroupDialog/serverGroupDialog.ts +++ b/src/sql/parts/objectExplorer/serverGroupDialog/serverGroupDialog.ts @@ -26,6 +26,7 @@ import { ServerGroupViewModel } from 'sql/parts/objectExplorer/serverGroupDialog import { attachButtonStyler, attachModalDialogStyler } from 'sql/common/theme/styler'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/common/telemetryKeys'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; export class ServerGroupDialog extends Modal { private _bodyBuilder: Builder; @@ -53,9 +54,10 @@ export class ServerGroupDialog extends Modal { @IThemeService private _themeService: IThemeService, @IContextViewService private _contextViewService: IContextViewService, @ITelemetryService telemetryService: ITelemetryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService ) { - super(localize('ServerGroupsDialogTitle', 'Server Groups'), TelemetryKeys.ServerGroups, partService, telemetryService, contextKeyService); + super(localize('ServerGroupsDialogTitle', 'Server Groups'), TelemetryKeys.ServerGroups, partService, telemetryService, clipboardService, contextKeyService); } public render() { diff --git a/src/sql/parts/profiler/dialog/profilerColumnEditorDialog.ts b/src/sql/parts/profiler/dialog/profilerColumnEditorDialog.ts index 7198e05538..c9717a0a64 100644 --- a/src/sql/parts/profiler/dialog/profilerColumnEditorDialog.ts +++ b/src/sql/parts/profiler/dialog/profilerColumnEditorDialog.ts @@ -26,6 +26,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; class EventItem { @@ -316,9 +317,10 @@ export class ProfilerColumnEditorDialog extends Modal { @IThemeService private _themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, - @IContextViewService private _contextViewService: IContextViewService + @IContextViewService private _contextViewService: IContextViewService, + @IClipboardService clipboardService: IClipboardService ) { - super(nls.localize('profilerColumnDialog.profiler', 'Profiler'), TelemetryKeys.Profiler, _partService, telemetryService, contextKeyService); + super(nls.localize('profilerColumnDialog.profiler', 'Profiler'), TelemetryKeys.Profiler, _partService, telemetryService, clipboardService, contextKeyService); } public render(): void { diff --git a/src/sql/platform/dialog/dialogModal.ts b/src/sql/platform/dialog/dialogModal.ts index 270daf326b..b52517fad7 100644 --- a/src/sql/platform/dialog/dialogModal.ts +++ b/src/sql/platform/dialog/dialogModal.ts @@ -25,6 +25,7 @@ import { Emitter } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DialogMessage, MessageLevel } from '../../workbench/api/common/sqlExtHostTypes'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export class DialogModal extends Modal { private _dialogPane: DialogPane; @@ -43,9 +44,10 @@ export class DialogModal extends Modal { @IWorkbenchThemeService private _themeService: IWorkbenchThemeService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService, @IInstantiationService private _instantiationService: IInstantiationService ) { - super(_dialog.title, name, partService, telemetryService, contextKeyService, options); + super(_dialog.title, name, partService, telemetryService, clipboardService, contextKeyService, options); } public layout(): void { @@ -53,7 +55,7 @@ export class DialogModal extends Modal { } public render() { - super.render(true); + super.render(); attachModalDialogStyler(this, this._themeService); if (this.backButton) { @@ -78,7 +80,7 @@ export class DialogModal extends Modal { let messageChangeHandler = (message: DialogMessage) => { if (message && message.text) { - this.setError(message.text, message.level); + this.setError(message.text, message.level, message.description); } else { this.setError(''); } diff --git a/src/sql/platform/dialog/dialogTypes.ts b/src/sql/platform/dialog/dialogTypes.ts index ed1e39a9ba..4ea7d7a4e5 100644 --- a/src/sql/platform/dialog/dialogTypes.ts +++ b/src/sql/platform/dialog/dialogTypes.ts @@ -63,10 +63,8 @@ export class Dialog extends ModelViewPane { } public set message(value: DialogMessage) { - if (this._message && !value || !this._message && value || this._message && value && (this._message.level !== value.level || this._message.text !== value.text)) { - this._message = value; - this._onMessageChange.fire(this._message); - } + this._message = value; + this._onMessageChange.fire(this._message); } public registerCloseValidator(validator: () => boolean | Thenable): void { @@ -255,9 +253,7 @@ export class Wizard { } public set message(value: DialogMessage) { - if (this._message && !value || !this._message && value || this._message && value && (this._message.level !== value.level || this._message.text !== value.text)) { - this._message = value; - this._onMessageChange.fire(this._message); - } + this._message = value; + this._onMessageChange.fire(this._message); } } \ No newline at end of file diff --git a/src/sql/platform/dialog/media/dialogModal.css b/src/sql/platform/dialog/media/dialogModal.css index 8b1c90fad3..7966684ea0 100644 --- a/src/sql/platform/dialog/media/dialogModal.css +++ b/src/sql/platform/dialog/media/dialogModal.css @@ -21,6 +21,7 @@ flex-direction: column; width: 100%; height: 100%; + overflow: scroll; } .dialogModal-hidden { diff --git a/src/sql/platform/dialog/media/wizardNavigation.css b/src/sql/platform/dialog/media/wizardNavigation.css index c8c4440642..b1d782d622 100644 --- a/src/sql/platform/dialog/media/wizardNavigation.css +++ b/src/sql/platform/dialog/media/wizardNavigation.css @@ -7,8 +7,7 @@ display: flex; flex-direction: column; width: 80px; - height: calc(100% + 25px); - margin-top: -25px; + height: 100%; } .hc-black .wizardNavigation-container { @@ -24,7 +23,7 @@ flex-direction: column; align-items: center; justify-content: center; - max-height: 130px; + max-height: 100px; } .wizardNavigation-pageNumber a { diff --git a/src/sql/platform/dialog/wizardModal.ts b/src/sql/platform/dialog/wizardModal.ts index 0a7bf90461..d61e602e7e 100644 --- a/src/sql/platform/dialog/wizardModal.ts +++ b/src/sql/platform/dialog/wizardModal.ts @@ -24,6 +24,7 @@ import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Emitter } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export class WizardModal extends Modal { private _dialogPanes = new Map(); @@ -48,9 +49,10 @@ export class WizardModal extends Modal { @IWorkbenchThemeService private _themeService: IWorkbenchThemeService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService private _instantiationService: IInstantiationService + @IInstantiationService private _instantiationService: IInstantiationService, + @IClipboardService clipboardService: IClipboardService ) { - super(_wizard.title, name, partService, telemetryService, contextKeyService, options); + super(_wizard.title, name, partService, telemetryService, clipboardService, contextKeyService, options); } public layout(): void { @@ -58,7 +60,7 @@ export class WizardModal extends Modal { } public render() { - super.render(true); + super.render(); attachModalDialogStyler(this, this._themeService); if (this.backButton) { @@ -83,7 +85,7 @@ export class WizardModal extends Modal { let messageChangeHandler = (message: DialogMessage) => { if (message && message.text) { - this.setError(message.text, message.level); + this.setError(message.text, message.level, message.description); } else { this.setError(''); } diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 6738450517..16214c70dc 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -890,6 +890,7 @@ declare module 'sqlops' { */ export type DialogMessage = { readonly text: string, + readonly description?: string, readonly level?: MessageLevel }; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index a02bbe84b4..46a49fc0db 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -245,6 +245,7 @@ export enum MessageLevel { export interface DialogMessage { text: string; level?: MessageLevel; + description?: string; } /// Card-related APIs that need to be here to avoid early load issues diff --git a/src/sql/workbench/errorMessageDialog/errorMessageDialog.ts b/src/sql/workbench/errorMessageDialog/errorMessageDialog.ts index 7e5aa36641..4dbe92b8e2 100644 --- a/src/sql/workbench/errorMessageDialog/errorMessageDialog.ts +++ b/src/sql/workbench/errorMessageDialog/errorMessageDialog.ts @@ -43,12 +43,12 @@ export class ErrorMessageDialog extends Modal { constructor( @IThemeService private _themeService: IThemeService, - @IClipboardService private _clipboardService: IClipboardService, + @IClipboardService clipboardService: IClipboardService, @IPartService partService: IPartService, @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService ) { - super('', TelemetryKeys.ErrorMessage, partService, telemetryService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + super('', TelemetryKeys.ErrorMessage, partService, telemetryService, clipboardService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); this._okLabel = localize('errorMessageDialog.ok', 'OK'); this._closeLabel = localize('errorMessageDialog.close', 'Close'); } diff --git a/src/sqltest/parts/accountManagement/accountDialogController.test.ts b/src/sqltest/parts/accountManagement/accountDialogController.test.ts index 5ead263296..bc249228e4 100644 --- a/src/sqltest/parts/accountManagement/accountDialogController.test.ts +++ b/src/sqltest/parts/accountManagement/accountDialogController.test.ts @@ -88,7 +88,7 @@ function createInstantiationService(addAccountFailureEmitter?: Emitter): .returns(() => undefined); // Create a mock account dialog - let accountDialog = new AccountDialog(null, null, null, instantiationService.object, null, null, null, new ContextKeyServiceStub()); + let accountDialog = new AccountDialog(null, null, null, instantiationService.object, null, null, null, new ContextKeyServiceStub(), null); let mockAccountDialog = TypeMoq.Mock.ofInstance(accountDialog); mockAccountDialog.setup(x => x.onAddAccountErrorEvent) .returns(() => { return addAccountFailureEmitter ? addAccountFailureEmitter.event : mockEvent.event; }); diff --git a/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts b/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts index 2c225126a9..25a3809c83 100644 --- a/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts +++ b/src/sqltest/parts/accountManagement/autoOAuthDialogController.test.ts @@ -38,7 +38,7 @@ suite('auto OAuth dialog controller tests', () => { mockOnCloseEvent = new Emitter(); // Create a mock auto OAuth dialog - let autoOAuthDialog = new AutoOAuthDialog(null, null, null, null, new ContextKeyServiceStub()); + let autoOAuthDialog = new AutoOAuthDialog(null, null, null, null, new ContextKeyServiceStub(), null); mockAutoOAuthDialog = TypeMoq.Mock.ofInstance(autoOAuthDialog); mockAutoOAuthDialog.setup(x => x.onCancel).returns(() => mockOnCancelEvent.event); diff --git a/src/sqltest/parts/accountManagement/firewallRuleDialogController.test.ts b/src/sqltest/parts/accountManagement/firewallRuleDialogController.test.ts index d63d00791b..e76730230d 100644 --- a/src/sqltest/parts/accountManagement/firewallRuleDialogController.test.ts +++ b/src/sqltest/parts/accountManagement/firewallRuleDialogController.test.ts @@ -59,7 +59,7 @@ suite('Firewall rule dialog controller tests', () => { .returns(() => mockFirewallRuleViewModel.object); // Create a mock account picker - let firewallRuleDialog = new FirewallRuleDialog(null, null, null, instantiationService.object, null, null, new ContextKeyServiceStub(), null); + let firewallRuleDialog = new FirewallRuleDialog(null, null, null, instantiationService.object, null, null, new ContextKeyServiceStub(), null, null); mockFirewallRuleDialog = TypeMoq.Mock.ofInstance(firewallRuleDialog); let mockEvent = new Emitter();