diff --git a/extensions/mssql/src/errorDiagnostics/errorDiagnosticsConstants.ts b/extensions/mssql/src/errorDiagnostics/errorDiagnosticsConstants.ts index a569dc49b6..fd1a6bad9b 100644 --- a/extensions/mssql/src/errorDiagnostics/errorDiagnosticsConstants.ts +++ b/extensions/mssql/src/errorDiagnostics/errorDiagnosticsConstants.ts @@ -3,5 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + // Error code reference comes from here: https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver16 export const MssqlPasswordResetErrorCode: number = 18488; +export const MssqlCertValidationFailedErrorCode: number = -2146893019; + +export const MssqlConnectionTelemetryView = 'MssqlConnectionErrorDialog'; +export const ConnectionErrorDialogTitle = localize('connectionError', "Connection error"); + +// Trust Server certificate custom dialog constants. +export const TSC_ActionId = 'enableTrustServerCertificate'; +export const TSC_OptionName = 'trustServerCertificate'; +export const TSC_EnableTrustServerCert = localize('enableTrustServerCertificate', "Enable Trust server certificate"); +export const TSC_InstructionText = localize('trustServerCertInstructionText', `Encryption was enabled on this connection, review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog. + +Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry? `); +export const TSC_ReadMoreLink = "https://learn.microsoft.com/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine" diff --git a/extensions/mssql/src/errorDiagnostics/errorDiagnosticsProvider.ts b/extensions/mssql/src/errorDiagnostics/errorDiagnosticsProvider.ts index 99bd306873..d86f5f0844 100644 --- a/extensions/mssql/src/errorDiagnostics/errorDiagnosticsProvider.ts +++ b/extensions/mssql/src/errorDiagnostics/errorDiagnosticsProvider.ts @@ -51,13 +51,18 @@ export class ErrorDiagnosticsProvider extends SqlOpsFeature { } protected override registerProvider(options: any): Disposable { - let handleConnectionError = async (errorCode: number, errorMessage: string, connection: azdata.connection.ConnectionProfile): Promise => { + let handleConnectionError = async (errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Promise => { let restoredProfile = this.convertToIConnectionProfile(connection); - if (errorCode === ErrorDiagnosticsConstants.MssqlPasswordResetErrorCode) { - logDebug(`Error Code ${errorCode} requires user to change their password, launching change password dialog.`) + + if (errorInfo.errorCode === ErrorDiagnosticsConstants.MssqlPasswordResetErrorCode) { + logDebug(`ErrorDiagnosticsProvider: Error Code ${errorInfo.errorCode} requires user to change their password, launching change password dialog.`); return await this.handleChangePassword(restoredProfile); } - logDebug(`No error handler found for errorCode ${errorCode}.`); + else if (errorInfo.errorCode === ErrorDiagnosticsConstants.MssqlCertValidationFailedErrorCode) { + logDebug(`ErrorDiagnosticsProvider: Error Code ${errorInfo.errorCode} indicates certificate validation has failed, launching error dialog with instructionText.`); + return await this.showCertValidationDialog(restoredProfile, errorInfo.errorMessage, errorInfo.messageDetails); + } + logDebug(`ErrorDiagnosticsProvider: No error handler found for errorCode ${errorInfo.errorCode}.`); return { handled: false }; } @@ -68,6 +73,43 @@ export class ErrorDiagnosticsProvider extends SqlOpsFeature { }); } + private async showCertValidationDialog(connection: azdata.IConnectionProfile, errorMessage: string, callStack: string): Promise { + try { + let actions: azdata.window.IDialogAction[] = []; + let trustServerCertAction: azdata.window.IDialogAction = { + id: ErrorDiagnosticsConstants.TSC_ActionId, + label: ErrorDiagnosticsConstants.TSC_EnableTrustServerCert, + isPrimary: true + }; + + actions.push(trustServerCertAction); + const result = await azdata.window.openCustomErrorDialog( + { + severity: azdata.window.MessageLevel.Error, + headerTitle: ErrorDiagnosticsConstants.ConnectionErrorDialogTitle, + message: errorMessage, + messageDetails: callStack, + telemetryView: ErrorDiagnosticsConstants.MssqlConnectionTelemetryView, + instructionText: ErrorDiagnosticsConstants.TSC_InstructionText, + readMoreLink: ErrorDiagnosticsConstants.TSC_ReadMoreLink, + actions: actions + } + ); + + // Result represents id of action taken by user. + if (result === ErrorDiagnosticsConstants.TSC_ActionId) { + connection.options[ErrorDiagnosticsConstants.TSC_OptionName] = true; + return { handled: true, reconnect: true, options: connection.options }; + } else { + return { handled: true, reconnect: false }; + } + } + catch (e) { + console.error(`Unexpected exception occurred when showing certificate validation custom dialog: ${e}`); + } + return { handled: false }; + } + private async handleChangePassword(connection: azdata.IConnectionProfile): Promise { try { const result = await azdata.connection.openChangePasswordDialog(connection); @@ -75,7 +117,7 @@ export class ErrorDiagnosticsProvider extends SqlOpsFeature { if (result) { // MSSQL uses 'password' as the option key for connection profile. connection.options['password'] = result; - return { handled: true, options: connection.options }; + return { handled: true, reconnect: true, options: connection.options }; } } catch (e) { diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index c840b29c1a..9e4072d1a1 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -432,7 +432,6 @@ declare module 'azdata' { azurePortalEndpoint?: string; } - export namespace diagnostics { /** * Represents a diagnostics provider of accounts. @@ -447,27 +446,48 @@ declare module 'azdata' { export interface ConnectionDiagnosticsResult { /** - * Status indicating if the error was handled or not. + * Whether the error was handled or not. */ handled: boolean, + /** + * Whether reconnect should be attempted. + */ + reconnect?: boolean, /** * If given, the new set of connection options to assign to the original connection profile, overwriting any previous options. */ options?: { [name: string]: any }; } + /** + * Provides error information + */ + export interface IErrorInformation { + /** + * Error code + */ + errorCode: number, + /** + * Error Message + */ + errorMessage: string, + /** + * Stack trace of error + */ + messageDetails: string + } + /** * Diagnostics object for handling errors for a provider. */ export interface ErrorDiagnosticsProvider { /** * Called when a connection error occurs, allowing the provider to optionally handle the error and fix any issues before continuing with completing the connection. - * @param errorCode The error code of the connection error. - * @param errorMessage The error message of the connection error. + * @param errorInfo The error information of the connection error. * @param connection The connection profile that caused the error. * @returns ConnectionDiagnosticsResult: The result from the provider for whether the error was handled. */ - handleConnectionError(errorCode: number, errorMessage: string, connection: connection.ConnectionProfile): Thenable; + handleConnectionError(errorInfo: IErrorInformation, connection: connection.ConnectionProfile): Thenable; } /** @@ -1801,5 +1821,69 @@ declare module 'azdata' { export interface Dialog extends LoadingComponentBase { } + + /** + * Opens the error dialog with customization options provided. + * @param options Dialog options to customize error dialog. + * @returns Id of action button clicked by user, e.g. ok, cancel + */ + export function openCustomErrorDialog(options: IErrorDialogOptions): Thenable; + + /** + * Provides dialog options to customize modal dialog content and layout + */ + export interface IErrorDialogOptions { + /** + * Severity Level to identify icon of modal dialog. + */ + severity: MessageLevel; + /** + * Title of modal dialog header. + */ + headerTitle: string; + /** + * Message text to show on dialog. + */ + message: string; + /** + * (Optional) Detailed message, e.g stack trace of error. + */ + messageDetails?: string; + /** + * Telemetry View to be used for emitting telemetry events. + */ + telemetryView?: string, + /** + * (Optional) List of custom actions to include in modal dialog alongwith a 'Cancel' button. + * If custom 'actions' are not provided, 'OK' button will be shown by default. + */ + actions?: IDialogAction[]; + /** + * (Optional) If provided, instruction text is shown in bold below message. + */ + instructionText?: string; + /** + * (Optional) If provided, appends read more link after instruction text. + */ + readMoreLink?: string; + } + + /** + * An action that will be rendered as a button on the dialog. + */ + export interface IDialogAction { + /** + * Identifier of action. + */ + id: string; + /** + * Label of Action button. + */ + label: string; + /** + * Defines if button styling and focus should be based on primary action. + */ + isPrimary: boolean; + } } } diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 72ed8c2fac..4198d542b0 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -68,7 +68,7 @@ export interface IConnectionResult { connected: boolean; errorMessage: string; errorCode: number; - callStack: string; + messageDetails: string; errorHandled?: boolean; connectionProfile?: IConnectionProfile; } diff --git a/src/sql/platform/connection/test/common/testConnectionManagementService.ts b/src/sql/platform/connection/test/common/testConnectionManagementService.ts index 9b3bff1dd2..4fc15b01f5 100644 --- a/src/sql/platform/connection/test/common/testConnectionManagementService.ts +++ b/src/sql/platform/connection/test/common/testConnectionManagementService.ts @@ -172,7 +172,7 @@ export class TestConnectionManagementService implements IConnectionManagementSer connect(connection: IConnectionProfile, uri: string, options?: IConnectionCompletionOptions, callbacks?: IConnectionCallbacks): Promise { return new Promise((resolve, reject) => { - resolve({ connected: true, errorMessage: undefined!, errorCode: undefined!, callStack: undefined! }); + resolve({ connected: true, errorMessage: undefined!, errorCode: undefined!, messageDetails: undefined! }); }); } @@ -334,4 +334,8 @@ export class TestConnectionManagementService implements IConnectionManagementSer openChangePasswordDialog(profile: IConnectionProfile): Promise { return undefined; } + + openCustomErrorDialog(options: azdata.window.IErrorDialogOptions): Promise { + return undefined; + } } diff --git a/src/sql/platform/errorMessage/common/errorMessageService.ts b/src/sql/platform/errorMessage/common/errorMessageService.ts index 355c03cfcd..3fd13863e5 100644 --- a/src/sql/platform/errorMessage/common/errorMessageService.ts +++ b/src/sql/platform/errorMessage/common/errorMessageService.ts @@ -6,6 +6,7 @@ import Severity from 'vs/base/common/severity'; import { IAction } from 'vs/base/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; export const IErrorMessageService = createDecorator('errorMessageService'); export interface IErrorMessageService { @@ -16,9 +17,10 @@ export interface IErrorMessageService { * @param headerTitle Title to show on Error modal dialog * @param message Message containng error message * @param messageDetails Message details containing stacktrace along with error message + * @param telemetryView Telemetry View to be used for dispatching telemetry events. * @param actions Custom actions to display on the error message dialog - * @param instructionText Spcial instructions to display to user when displaying error message + * @param instructionText Special instructions to display to user when displaying error message * @param readMoreLink External link to read more about the instructions. */ - showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void; + showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, telemetryView?: TelemetryView, actions?: IAction[], instructionText?: string, readMoreLink?: string): void; } diff --git a/src/sql/platform/errorMessage/test/common/testErrorMessageService.ts b/src/sql/platform/errorMessage/test/common/testErrorMessageService.ts index 03b22af96a..4cd50fc8c6 100644 --- a/src/sql/platform/errorMessage/test/common/testErrorMessageService.ts +++ b/src/sql/platform/errorMessage/test/common/testErrorMessageService.ts @@ -5,9 +5,14 @@ import Severity from 'vs/base/common/severity'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; +import { IErrorDialogOptions } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; export class TestErrorMessageService implements IErrorMessageService { _serviceBrand: undefined; showDialog(severity: Severity, headerTitle: string, message: string): void { } + showDialogAsync(options: IErrorDialogOptions, telemetryView: TelemetryView): Promise { + return Promise.resolve(undefined); + } } diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index e4336b1af9..fccdfffa07 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -38,11 +38,13 @@ export const enum TelemetryView { AgentJobSteps = 'AgentJobSteps', AgentNotebookHistory = 'AgentNotebookHistory', AgentNotebooks = 'AgentNotebooks', - ConnectionDialog = 'ConnectionDialog', + ConnectionErrorDialog = 'ConnectionErrorDialog', + ErrorMessageDialog = 'ErrorMessageDialog', ExecutionPlan = 'ExecutionPlan', ExtensionHost = 'ExtensionHost', ExtensionRecommendationDialog = 'ExtensionRecommendationDialog', Notebook = 'Notebook', + NotifyEncryptionDialog = 'NotifyEncryptionDialog', ResultsPanel = 'ResultsPanel', Shell = 'Shell', SqlAssessment = 'SqlAssessment', @@ -75,7 +77,6 @@ export const enum TelemetryAction { DeleteAgentProxy = 'DeleteAgentProxy', DeleteConnection = 'DeleteConnection', DeleteServerGroup = 'DeleteServerGroup', - EnableTrustServerCertificate = 'EnableTrustServerCertificate', FindNode = 'FindNode', FirewallRuleRequested = 'FirewallRuleCreated', GenerateScript = 'GenerateScript', diff --git a/src/sql/workbench/api/browser/mainThreadErrorDiagnostics.ts b/src/sql/workbench/api/browser/mainThreadErrorDiagnostics.ts index 6b5cca64f1..491ad26a71 100644 --- a/src/sql/workbench/api/browser/mainThreadErrorDiagnostics.ts +++ b/src/sql/workbench/api/browser/mainThreadErrorDiagnostics.ts @@ -34,8 +34,8 @@ export class MainThreadErrorDiagnostics extends Disposable implements MainThread //Create the error handler that interfaces with the extension via the proxy and register it let errorDiagnostics: azdata.diagnostics.ErrorDiagnosticsProvider = { - handleConnectionError(errorCode: number, errorMessage: string, connection: azdata.connection.ConnectionProfile): Thenable { - return self._proxy.$handleConnectionError(handle, errorCode, errorMessage, connection); + handleConnectionError(errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Thenable { + return self._proxy.$handleConnectionError(handle, errorInfo, connection); } }; this._errorDiagnosticsService.registerDiagnosticsProvider(providerMetadata.targetProviderId, errorDiagnostics); diff --git a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts index edda5c55e3..6560654113 100644 --- a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts @@ -9,7 +9,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { MainThreadModelViewDialogShape, ExtHostModelViewDialogShape } from 'sql/workbench/api/common/sqlExtHost.protocol'; import { Dialog, DialogTab, DialogButton, WizardPage, Wizard } from 'sql/workbench/services/dialog/common/dialogTypes'; import { CustomDialogService, DefaultWizardOptions, DefaultDialogOptions } from 'sql/workbench/services/dialog/browser/customDialogService'; -import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, IModelViewWizardPageDetails, IModelViewWizardDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, IModelViewWizardPageDetails, IModelViewWizardDetails, IErrorDialogOptions } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/workbench/browser/modelComponents/modelViewInput'; import * as vscode from 'vscode'; @@ -113,6 +113,10 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM return Promise.resolve(); } + public $openCustomErrorDialog(options: IErrorDialogOptions): Promise { + return this._dialogService.openCustomErrorDialog(options); + } + public $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable { let dialog = this._dialogs.get(handle); if (!dialog) { diff --git a/src/sql/workbench/api/common/extHostErrorDiagnostics.ts b/src/sql/workbench/api/common/extHostErrorDiagnostics.ts index 5d71deb7f9..a280206896 100644 --- a/src/sql/workbench/api/common/extHostErrorDiagnostics.ts +++ b/src/sql/workbench/api/common/extHostErrorDiagnostics.ts @@ -25,13 +25,13 @@ export class ExtHostErrorDiagnostics extends ExtHostErrorDiagnosticsShape { // PUBLIC METHODS ////////////////////////////////////////////////////// // - MAIN THREAD AVAILABLE METHODS ///////////////////////////////////// - public override $handleConnectionError(handle: number, errorCode: number, errorMessage: string, connection: azdata.connection.ConnectionProfile): Thenable { + public override $handleConnectionError(handle: number, errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Thenable { let provider = this._providers[handle]; if (provider === undefined) { return Promise.resolve({ handled: false }); } else { - return provider.provider.handleConnectionError(errorCode, errorMessage, connection); + return provider.provider.handleConnectionError(errorInfo, connection); } } diff --git a/src/sql/workbench/api/common/extHostModelViewDialog.ts b/src/sql/workbench/api/common/extHostModelViewDialog.ts index ae239d0f1e..e93cd9106a 100644 --- a/src/sql/workbench/api/common/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/common/extHostModelViewDialog.ts @@ -777,6 +777,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { this._proxy.$openDialog(handle); } + public openCustomErrorDialog(options: azdata.window.IErrorDialogOptions): Thenable { + return this._proxy.$openCustomErrorDialog(options); + } + public closeDialog(dialog: azdata.window.Dialog): void { let handle = this.getHandle(dialog); this._proxy.$closeDialog(handle); diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index 00135f3802..77146109c1 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -224,7 +224,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp const diagnostics: typeof azdata.diagnostics = { registerDiagnosticsProvider: (providerMetadata: azdata.diagnostics.ErrorDiagnosticsProviderMetadata, errorDiagnostics: azdata.diagnostics.ErrorDiagnosticsProvider): vscode.Disposable => { return extHostErrorDiagnostics.$registerDiagnosticsProvider(providerMetadata, errorDiagnostics); - } + }, } let registerConnectionProvider = (provider: azdata.ConnectionProvider): vscode.Disposable => { @@ -477,7 +477,10 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp createModelViewDashboard(title: string, name?: string, options?: azdata.ModelViewDashboardOptions): azdata.window.ModelViewDashboard { return extHostModelViewDialog.createModelViewDashboard(title, name, options, extension); }, - MessageLevel: sqlExtHostTypes.MessageLevel + MessageLevel: sqlExtHostTypes.MessageLevel, + openCustomErrorDialog(options: sqlExtHostTypes.IErrorDialogOptions): Thenable { + return extHostModelViewDialog.openCustomErrorDialog(options); + } }; const tasks: typeof azdata.tasks = { diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 257e99feb4..fd11e57951 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -18,7 +18,8 @@ import { IModelViewWizardDetails, IModelViewWizardPageDetails, IExecuteManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType, INotebookFutureDone, INotebookEditOperation, NotebookChangeKind, - ISerializationManagerDetails + ISerializationManagerDetails, + IErrorDialogOptions } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IUndoStopOptions } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -613,7 +614,7 @@ export abstract class ExtHostErrorDiagnosticsShape { /** * Handle other connection error types */ - $handleConnectionError(handle: number, errorCode: number, errorMessage: string, connection: azdata.connection.ConnectionProfile): Thenable { throw ni(); } + $handleConnectionError(handle: number, errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Thenable { throw ni(); } } /** @@ -878,6 +879,7 @@ export interface MainThreadModelViewDialogShape extends IDisposable { $openEditor(handle: number, modelViewId: string, title: string, name?: string, options?: azdata.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable; $closeEditor(handle: number): Thenable; $openDialog(handle: number, dialogName?: string): Thenable; + $openCustomErrorDialog(options: IErrorDialogOptions): Promise; $closeDialog(handle: number): Thenable; $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable; $setTabDetails(handle: number, details: IModelViewTabDetails): Thenable; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index ee63d60e7e..15e2702ba3 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -6,6 +6,7 @@ import * as azdata from 'azdata'; import * as vsExtTypes from 'vs/workbench/api/common/extHostTypes'; import { URI } from 'vs/base/common/uri'; +import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; // SQL added extension host types export enum ServiceOptionType { @@ -328,6 +329,29 @@ export interface IDialogProperties { height: number } +/** + * Provides dialog options to customize modal dialog content and layout + */ +export interface IErrorDialogOptions { + severity: MessageLevel; + headerTitle: string; + message: string; + messageDetails?: string; + telemetryView?: TelemetryView | string; + actions?: IDialogAction[]; + instructionText?: string; + readMoreLink?: string; +} + +/** + * An action that will be rendered as a button on the dialog. + */ +export interface IDialogAction { + id: string; + label: string; + isPrimary: boolean; +} + export enum MessageLevel { Error = 0, Warning = 1, diff --git a/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts b/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts index 4dae196c63..9fed886852 100644 --- a/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts +++ b/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts @@ -50,7 +50,7 @@ suite('SQL Connection Tree Action tests', () => { connected: true, errorMessage: undefined, errorCode: undefined, - callStack: undefined + messageDetails: undefined }; let capabilitiesService = new TestCapabilitiesService(); const logService = new LogService(new ConsoleLogger()); diff --git a/src/sql/workbench/contrib/welcome/notifyEncryption/notifyEncryptionDialog.ts b/src/sql/workbench/contrib/welcome/notifyEncryption/notifyEncryptionDialog.ts index 0b823c86d0..f1e7f75339 100644 --- a/src/sql/workbench/contrib/welcome/notifyEncryption/notifyEncryptionDialog.ts +++ b/src/sql/workbench/contrib/welcome/notifyEncryption/notifyEncryptionDialog.ts @@ -20,6 +20,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; +import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; export class NotifyEncryptionDialog extends ErrorMessageDialog { private static NOTIFY_ENCRYPT_SHOWN = 'workbench.notifyEncryptionShown'; @@ -52,7 +53,7 @@ export class NotifyEncryptionDialog extends ErrorMessageDialog { return; } - super.open(Severity.Info, + super.open(TelemetryView.NotifyEncryptionDialog, Severity.Info, localize('notifyEncryption.title', 'Important Update'), localize('notifyEncryption.message', 'Azure Data Studio now has encryption enabled by default for all SQL Server connections. This may result in your existing connections no longer working unless certain Encryption related connection properties are changed.{0}We recommend you review the link below for more details.', '\n\n')); } diff --git a/src/sql/workbench/services/connection/browser/connectionDialogService.ts b/src/sql/workbench/services/connection/browser/connectionDialogService.ts index f4050f9879..289957c9e5 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogService.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogService.ts @@ -27,8 +27,7 @@ import * as types from 'vs/base/common/types'; import { trim } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; -import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; import { CmsConnectionController } from 'sql/workbench/services/connection/browser/cmsConnectionController'; import { entries } from 'sql/base/common/collections'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -91,7 +90,6 @@ export class ConnectionDialogService implements IConnectionDialogService { @IClipboardService private _clipboardService: IClipboardService, @ICommandService private _commandService: ICommandService, @ILogService private _logService: ILogService, - @IAdsTelemetryService private _telemetryService: IAdsTelemetryService ) { this.initializeConnectionProviders(); } @@ -285,7 +283,7 @@ export class ConnectionDialogService implements IConnectionDialogService { this._logService.debug(`ConnectionDialogService: Error handled and connection reset - Error: ${connectionResult.errorMessage}`); } else { this._connectionDialog.resetConnection(); - this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack, connectionResult.errorCode); + this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.messageDetails); this._logService.debug(`ConnectionDialogService: Connection error: ${connectionResult.errorMessage}`); } } catch (err) { @@ -471,7 +469,7 @@ export class ConnectionDialogService implements IConnectionDialogService { await this.showDialogWithModel(); if (connectionResult && connectionResult.errorMessage) { - this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack, connectionResult.errorCode); + this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.messageDetails); } } @@ -503,8 +501,7 @@ export class ConnectionDialogService implements IConnectionDialogService { recentConnections.forEach(conn => conn.dispose()); } - - private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, errorCode?: number): void { + private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): void { // Kerberos errors are currently very hard to understand, so adding handling of these to solve the common scenario // note that ideally we would have an extensible service to handle errors by error code and provider, but for now // this solves the most common "hard error" that we've noticed @@ -532,22 +529,6 @@ export class ConnectionDialogService implements IConnectionDialogService { this._logService.error(message); - // Set instructionText for MSSQL Provider Encryption error code -2146893019 thrown by SqlClient when certificate validation fails. - if (errorCode === -2146893019) { - let enableTrustServerCert = localize('enableTrustServerCertificate', "Enable Trust server certificate"); - let instructionText = localize('trustServerCertInstructionText', `Encryption was enabled on this connection, review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog. - - Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry? `); - let readMoreLink = "https://learn.microsoft.com/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine" - actions.push(new Action('trustServerCert', enableTrustServerCert, undefined, true, async () => { - this._telemetryService.sendActionEvent(TelemetryKeys.TelemetryView.ConnectionDialog, TelemetryKeys.TelemetryAction.EnableTrustServerCertificate); - this._model.options[Constants.trustServerCertificate] = true; - await this.handleOnConnect(this._connectionDialog.newConnectionParams, this._model as IConnectionProfile); - return; - })); - this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions, instructionText, readMoreLink); - } else { - this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions); - } + this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, TelemetryView.ConnectionErrorDialog, actions, undefined, undefined); } } diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index 3ac8ad7057..649ae37cb3 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -295,7 +295,7 @@ export class ConnectionDialogWidget extends Modal { private connect(element?: IConnectionProfile): void { this.logService.debug('ConnectionDialogWidget: Connect button is clicked'); if (this._connectButton.enabled) { - this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.ConnectionDialog, TelemetryKeys.TelemetryAction.ConnectToServer).withAdditionalProperties( + this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.ConnectionErrorDialog, TelemetryKeys.TelemetryAction.ConnectToServer).withAdditionalProperties( { [TelemetryKeys.TelemetryPropertyName.ConnectionSource]: this._connectionSource } ).send(); this._connecting = true; diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index 7cacf653a0..334ab8cfcb 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -329,7 +329,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti // If the password is required and still not loaded show the dialog if ((!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) || !tokenFillSuccess) { - return this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options); + return this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, messageDetails: undefined, errorCode: undefined }, options); } else { // Try to connect return this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => { @@ -564,14 +564,22 @@ export class ConnectionManagementService extends Disposable implements IConnecti return this.connectWithOptions(connection, uri, options, callbacks); } else { - let connectionErrorHandled = await this._errorDiagnosticsService.tryHandleConnectionError(connectionResult.errorCode, connectionResult.errorMessage, connection.providerName, connection); - if (connectionErrorHandled.handled) { + let connectionErrorHandleResult = await this._errorDiagnosticsService.tryHandleConnectionError(connectionResult, connection.providerName, connection); + if (connectionErrorHandleResult.handled) { connectionResult.errorHandled = true; - if (connectionErrorHandled.options) { + if (connectionErrorHandleResult.options) { //copy over altered connection options from the result if provided. - connection.options = connectionErrorHandled.options; + connection.options = connectionErrorHandleResult.options; + } + if (connectionErrorHandleResult.reconnect) { + // Attempt reconnect if requested by provider + return this.connectWithOptions(connection, uri, options, callbacks); + } else { + if (callbacks.onConnectCanceled) { + callbacks.onConnectCanceled(); + } + return connectionResult; } - return this.connectWithOptions(connection, uri, options, callbacks); } else { // Error not handled by any registered providers so fail the connection @@ -1299,18 +1307,18 @@ export class ConnectionManagementService extends Disposable implements IConnecti if (connectionMngInfo && connectionMngInfo.deleted) { this._logService.info(`Found deleted connection management info for ${uri} - removing`); this._connectionStatusManager.deleteConnection(uri); - resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, callStack: undefined, errorHandled: true, connectionProfile: connection }); + resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, messageDetails: undefined, errorHandled: true, connectionProfile: connection }); } else { if (errorMessage) { // Connection to the server failed this._logService.info(`Error occurred while connecting, removing connection management info for ${uri}`); this._connectionStatusManager.deleteConnection(uri); - resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack, connectionProfile: connection }); + resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, messageDetails: callStack, connectionProfile: connection }); } else { if (connectionMngInfo.serverInfo) { connection.options.isCloud = connectionMngInfo.serverInfo.isCloud; } - resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack, connectionProfile: connection }); + resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, messageDetails: callStack, connectionProfile: connection }); } } }); diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts index 844490c2dd..8e65154ebc 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts @@ -108,10 +108,9 @@ suite('ConnectionDialogService tests', () => { testInstantiationService.stub(ICapabilitiesService, new TestCapabilitiesService()); let logService: ILogService = new NullLogService(); - let telemetryService: IAdsTelemetryService = new NullAdsTelemetryService(); connectionDialogService = new ConnectionDialogService(testInstantiationService, capabilitiesService, errorMessageService.object, - new TestConfigurationService(), new BrowserClipboardService(layoutService, logService), NullCommandService, logService, telemetryService); + new TestConfigurationService(), new BrowserClipboardService(layoutService, logService), NullCommandService, logService); (connectionDialogService as any)._connectionManagementService = mockConnectionManagementService.object; let providerDisplayNames = ['Mock SQL Server']; let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; @@ -290,7 +289,7 @@ suite('ConnectionDialogService tests', () => { connected: false, errorMessage: 'test_error', errorCode: -1, - callStack: 'testCallStack' + messageDetails: 'testCallStack' }; // promise only resolves upon handleDefaultOnConnect, must return it at the end let connectionPromise = connectionDialogService.openDialogAndWait(mockConnectionManagementService.object, testConnectionParams, connectionProfile, connectionResult, false); diff --git a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts index b7ddbf8380..cebdad39dd 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts @@ -203,7 +203,7 @@ suite('SQL ConnectionManagementService tests', () => { TypeMoq.It.isAny(), TypeMoq.It.is(p => p.connectionType === connectionType && (uri === undefined || p.input.uri === uri)), TypeMoq.It.is(c => c !== undefined && c.serverName === connectionProfile.serverName), - connectionResult ? TypeMoq.It.is(r => r.errorMessage === connectionResult.errorMessage && r.callStack === connectionResult.callStack) : undefined, + connectionResult ? TypeMoq.It.is(r => r.errorMessage === connectionResult.errorMessage && r.messageDetails === connectionResult.messageDetails) : undefined, options ? TypeMoq.It.isAny() : undefined), didShow ? TypeMoq.Times.once() : TypeMoq.Times.never()); @@ -212,7 +212,7 @@ suite('SQL ConnectionManagementService tests', () => { TypeMoq.It.isAny(), TypeMoq.It.is(p => p.connectionType === connectionType && ((uri === undefined && p.input === undefined) || p.input.uri === uri)), undefined, - connectionResult ? TypeMoq.It.is(r => r.errorMessage === connectionResult.errorMessage && r.callStack === connectionResult.callStack) : undefined, + connectionResult ? TypeMoq.It.is(r => r.errorMessage === connectionResult.errorMessage && r.messageDetails === connectionResult.messageDetails) : undefined, options ? TypeMoq.It.isAny() : undefined), didShow ? TypeMoq.Times.once() : TypeMoq.Times.never()); } @@ -422,7 +422,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: error, errorCode: errorCode, - callStack: errorCallStack + messageDetails: errorCallStack }; let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); @@ -450,7 +450,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: error, errorCode: errorCode, - callStack: errorCallStack + messageDetails: errorCallStack }; let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); @@ -1097,7 +1097,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: error, errorCode: errorCode, - callStack: errorCallStack + messageDetails: errorCallStack }; let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); @@ -1177,7 +1177,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: error, errorCode: errorCode, - callStack: errorCallStack + messageDetails: errorCallStack }; let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); @@ -1209,7 +1209,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: error, errorCode: errorCode, - callStack: errorCallStack + messageDetails: errorCallStack }; let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); @@ -1233,7 +1233,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: undefined, errorCode: undefined, - callStack: undefined + messageDetails: undefined }; let result = await connect(uri, options, false, connectionProfileWithEmptyUnsavedPassword); @@ -1258,7 +1258,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: undefined, errorCode: undefined, - callStack: undefined + messageDetails: undefined }; let result = await connect(uri, options, false, connectionProfileWithEmptySavedPassword); @@ -1294,7 +1294,7 @@ suite('SQL ConnectionManagementService tests', () => { connected: expectedConnection, errorMessage: undefined, errorCode: undefined, - callStack: undefined + messageDetails: undefined }; let result = await connect(uri, options, false, connectionProfileWithEmptySavedPassword); diff --git a/src/sql/workbench/services/connection/test/common/testConnectionDialogService.ts b/src/sql/workbench/services/connection/test/common/testConnectionDialogService.ts index c817acc073..7a7c0f5f60 100644 --- a/src/sql/workbench/services/connection/test/common/testConnectionDialogService.ts +++ b/src/sql/workbench/services/connection/test/common/testConnectionDialogService.ts @@ -5,6 +5,7 @@ import { INewConnectionParams, IConnectionResult, IConnectionManagementService, IConnectionCompletionOptions } from 'sql/platform/connection/common/connectionManagement'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +import { IErrorDialogOptions } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; export class TestConnectionDialogService implements IConnectionDialogService { @@ -29,4 +30,8 @@ export class TestConnectionDialogService implements IConnectionDialogService { public callDefaultOnConnect(connection: IConnectionProfile, params: INewConnectionParams): Promise { return Promise.resolve(undefined); } + + public async showErrorDialogAsync(options: IErrorDialogOptions): Promise { + return Promise.resolve(undefined); + } } diff --git a/src/sql/workbench/services/connection/test/common/testErrorDiagnosticsService.ts b/src/sql/workbench/services/connection/test/common/testErrorDiagnosticsService.ts index 5a40cd0412..c51c3bf426 100644 --- a/src/sql/workbench/services/connection/test/common/testErrorDiagnosticsService.ts +++ b/src/sql/workbench/services/connection/test/common/testErrorDiagnosticsService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { IConnectionResult } from 'sql/platform/connection/common/connectionManagement'; import { IErrorDiagnosticsService } from 'sql/workbench/services/diagnostics/common/errorDiagnosticsService'; export class TestErrorDiagnosticsService implements IErrorDiagnosticsService { @@ -15,7 +16,7 @@ export class TestErrorDiagnosticsService implements IErrorDiagnosticsService { unregisterDiagnosticsProvider(ProviderId: string): void { } - tryHandleConnectionError(errorCode: number, errorMessage: string, providerId: string, connection: azdata.IConnectionProfile): Promise { + tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: azdata.IConnectionProfile): Promise { return Promise.resolve({ handled: false }); } } diff --git a/src/sql/workbench/services/diagnostics/browser/errorDiagnosticsService.ts b/src/sql/workbench/services/diagnostics/browser/errorDiagnosticsService.ts index b64f16f6a9..61176b7fbc 100644 --- a/src/sql/workbench/services/diagnostics/browser/errorDiagnosticsService.ts +++ b/src/sql/workbench/services/diagnostics/browser/errorDiagnosticsService.ts @@ -8,6 +8,7 @@ import * as azdata from 'azdata'; import { ILogService } from 'vs/platform/log/common/log'; import * as Utils from 'sql/platform/connection/common/utils'; import * as interfaces from 'sql/platform/connection/common/interfaces'; +import { IConnectionResult } from 'sql/platform/connection/common/connectionManagement'; export class ErrorDiagnosticsService implements IErrorDiagnosticsService { @@ -18,11 +19,16 @@ export class ErrorDiagnosticsService implements IErrorDiagnosticsService { @ILogService private readonly _logService: ILogService ) { } - public async tryHandleConnectionError(errorCode: number, errorMessage: string, providerId: string, connection: interfaces.IConnectionProfile): Promise { + public async tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: interfaces.IConnectionProfile): Promise { let result = { handled: false }; let provider = this._providers[providerId] if (provider) { - result = await provider.handleConnectionError(errorCode, errorMessage, Utils.convertToRpcConnectionProfile(connection)); + let errorInfo: azdata.diagnostics.IErrorInformation = { + errorCode: connectionResult.errorCode ?? 0, + errorMessage: connectionResult.errorMessage ?? '', + messageDetails: connectionResult.messageDetails ?? '' + } + result = await provider.handleConnectionError(errorInfo, Utils.convertToRpcConnectionProfile(connection)); } return result; } diff --git a/src/sql/workbench/services/diagnostics/common/errorDiagnosticsService.ts b/src/sql/workbench/services/diagnostics/common/errorDiagnosticsService.ts index 000fe0ecaf..ed38cbb336 100644 --- a/src/sql/workbench/services/diagnostics/common/errorDiagnosticsService.ts +++ b/src/sql/workbench/services/diagnostics/common/errorDiagnosticsService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { IConnectionResult } from 'sql/platform/connection/common/connectionManagement'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const SERVICE_ID = 'errorDiagnosticsService'; @@ -34,5 +35,5 @@ export interface IErrorDiagnosticsService { * @param connection Connection profile that is utilized for connection * @returns a Promise containing a ConnectionDiagnosticsResult object (with handling status and altered options) */ - tryHandleConnectionError(errorCode: number, errorMessage: string, providerId: string, connection: azdata.IConnectionProfile): Promise; + tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: azdata.IConnectionProfile): Promise; } diff --git a/src/sql/workbench/services/dialog/browser/customDialogService.ts b/src/sql/workbench/services/dialog/browser/customDialogService.ts index 057a6db1f1..7de990f63f 100644 --- a/src/sql/workbench/services/dialog/browser/customDialogService.ts +++ b/src/sql/workbench/services/dialog/browser/customDialogService.ts @@ -8,6 +8,8 @@ import { WizardModal } from 'sql/workbench/services/dialog/browser/wizardModal'; import { Dialog, Wizard } from 'sql/workbench/services/dialog/common/dialogTypes'; import { IModalOptions } from 'sql/workbench/browser/modal/modal'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ErrorMessageDialog } from 'sql/workbench/services/errorMessage/browser/errorMessageDialog'; +import { IErrorDialogOptions } from 'sql/workbench/api/common/sqlExtHostTypes'; export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true, hasSpinner: true }; export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true, hasSpinner: true }; @@ -51,4 +53,15 @@ export class CustomDialogService { public getWizardModal(wizard: Wizard): WizardModal | undefined { return this._wizardModals.get(wizard); } + + /** + * Shows error dialog customized with given options + * @param options Error Dialog options to customize error message dialog. + */ + public async openCustomErrorDialog(options: IErrorDialogOptions): Promise { + let dialog = this._instantiationService.createInstance(ErrorMessageDialog); + dialog.render(); + let result = await dialog.openCustomAsync(options); + return result; + } } diff --git a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts index 36b4722ecc..51b8eb5b4d 100644 --- a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts +++ b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts @@ -15,7 +15,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { localize } from 'vs/nls'; -import { IAction } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { ILogService } from 'vs/platform/log/common/log'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; @@ -26,6 +26,8 @@ import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { Link } from 'vs/platform/opener/browser/link'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Deferred } from 'sql/base/common/promise'; +import { IErrorDialogOptions, MessageLevel } from 'sql/workbench/api/common/sqlExtHostTypes'; const maxActions = 1; @@ -44,6 +46,7 @@ export class ErrorMessageDialog extends Modal { private _okLabel: string; private _closeLabel: string; private _readMoreLabel: string; + private _promiseResolver: (value: string) => void; private _onOk = new Emitter(); public onOk: Event = this._onOk.event; @@ -56,7 +59,8 @@ export class ErrorMessageDialog extends Modal { @IContextKeyService contextKeyService: IContextKeyService, @ILogService logService: ILogService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, - @IOpenerService private readonly _openerService: IOpenerService + @IOpenerService private readonly _openerService: IOpenerService, + protected _telemetryView: TelemetryKeys.TelemetryView | string = TelemetryKeys.TelemetryView.ErrorMessageDialog, ) { super('', TelemetryKeys.ModalDialogName.ErrorMessage, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'normal', hasTitleIcon: true }); this._okLabel = localize('errorMessageDialog.ok', "OK"); @@ -101,11 +105,15 @@ export class ErrorMessageDialog extends Modal { } private onActionSelected(index: number): void { - // Call OK so it always closes - this.ok(); - // Run the action if possible if (this._actions && index < this._actions.length) { + const actionId = this._actions[index].id; + this._telemetryService.sendActionEvent(this._telemetryView, actionId); + // Call OK to close dialog. + this.ok(false); + // Run the action if possible this._actions[index].run(); + // Resolve promise after running action. + this._promiseResolver(actionId); } } @@ -157,16 +165,23 @@ export class ErrorMessageDialog extends Modal { this.ok(); } - public ok(): void { + public ok(resolvePromise: boolean = true): void { + this._telemetryService.sendActionEvent(this._telemetryView, 'ok'); this._onOk.fire(); - this.close('ok'); + this.close('ok', resolvePromise); } - public close(hideReason: HideReason = 'close') { + public close(hideReason: HideReason = 'close', resolvePromise: boolean) { + this._telemetryService.sendActionEvent(this._telemetryView, hideReason.toString()); this.hide(hideReason); + if (resolvePromise) { + this._promiseResolver(hideReason.toString()); + } } - public open(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { + public open(telemetryView: TelemetryKeys.TelemetryView | string, severity: Severity, headerTitle: string, message: string, messageDetails?: string, + actions?: IAction[], instructionText?: string, readMoreLink?: string, resetActions: boolean = true): void { + this._telemetryView = telemetryView; this._severity = severity; this._message = message; this._instructionText = instructionText; @@ -181,7 +196,9 @@ export class ErrorMessageDialog extends Modal { if (this._message) { this._bodyContainer.setAttribute('aria-description', this._message); } - this.resetActions(); + if (resetActions) { + this.resetActions(); + } if (actions?.length > 0) { for (let i = 0; i < maxActions && i < actions.length; i++) { this._actions.push(actions[i]); @@ -209,6 +226,42 @@ export class ErrorMessageDialog extends Modal { } } + public openCustomAsync(options: IErrorDialogOptions): Promise { + if (!options) { + return undefined; + } + + let actions: IAction[] = []; + this.resetActions(); + options.actions?.forEach(action => { + actions.push(new Action(action.id, action.label, '', true, () => { })); + }); + + this.open(options.telemetryView, this.convertToSeverity(options.severity), + options.headerTitle, options.message, options.messageDetails, actions, + options.instructionText, options.readMoreLink, false); + + const deferred = new Deferred(); + this._promiseResolver = deferred.resolve; + return deferred.promise; + } + + private convertToSeverity(messageLevel: MessageLevel): Severity { + let severity: Severity = Severity.Error; + switch (messageLevel) { + case MessageLevel.Error: + severity = Severity.Error; + break; + case MessageLevel.Information: + severity = Severity.Info; + break; + case MessageLevel.Warning: + severity = Severity.Warning; + break; + } + return severity; + } + private resetActions(): void { this._actions = []; for (let actionButton of this._actionButtons) { diff --git a/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts b/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts index 01a789143d..2da024e70c 100644 --- a/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts +++ b/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts @@ -10,6 +10,7 @@ import { IAction } from 'vs/base/common/actions'; import { ErrorMessageDialog } from 'sql/workbench/services/errorMessage/browser/errorMessageDialog'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; +import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; export class ErrorMessageService implements IErrorMessageService { @@ -24,11 +25,11 @@ export class ErrorMessageService implements IErrorMessageService { @IInstantiationService private _instantiationService: IInstantiationService ) { } - public showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { - this.doShowDialog(severity, headerTitle, message, messageDetails, actions, instructionText, readMoreLink); + public showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, telemetryView: TelemetryView = TelemetryView.ErrorMessageDialog, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { + this.doShowDialog(telemetryView, severity, headerTitle, message, messageDetails, actions, instructionText, readMoreLink); } - private doShowDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { + private doShowDialog(telemetryView: TelemetryView, severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { if (!this._errorDialog) { this._errorDialog = this._instantiationService.createInstance(ErrorMessageDialog); this._errorDialog.onOk(() => this.handleOnOk()); @@ -36,7 +37,7 @@ export class ErrorMessageService implements IErrorMessageService { } let title = headerTitle ? headerTitle : this.getDefaultTitle(severity); - return this._errorDialog.open(severity, title, message, messageDetails, actions, instructionText, readMoreLink); + return this._errorDialog.open(telemetryView, severity, title, message, messageDetails, actions, instructionText, readMoreLink); } private getDefaultTitle(severity: Severity) {