Migrate cert validation error handling to mssql extension (#21829)

This commit is contained in:
Cheena Malhotra
2023-02-07 09:21:35 -08:00
committed by GitHub
parent e1b35d266a
commit 66410edf02
29 changed files with 352 additions and 92 deletions

View File

@@ -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<azdata.diagnostics.ConnectionDiagnosticsResult> {
return self._proxy.$handleConnectionError(handle, errorCode, errorMessage, connection);
handleConnectionError(errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Thenable<azdata.diagnostics.ConnectionDiagnosticsResult> {
return self._proxy.$handleConnectionError(handle, errorInfo, connection);
}
};
this._errorDiagnosticsService.registerDiagnosticsProvider(providerMetadata.targetProviderId, errorDiagnostics);

View File

@@ -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<string | undefined> {
return this._dialogService.openCustomErrorDialog(options);
}
public $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void> {
let dialog = this._dialogs.get(handle);
if (!dialog) {

View File

@@ -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<azdata.diagnostics.ConnectionDiagnosticsResult> {
public override $handleConnectionError(handle: number, errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Thenable<azdata.diagnostics.ConnectionDiagnosticsResult> {
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);
}
}

View File

@@ -777,6 +777,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
this._proxy.$openDialog(handle);
}
public openCustomErrorDialog(options: azdata.window.IErrorDialogOptions): Thenable<string | undefined> {
return this._proxy.$openCustomErrorDialog(options);
}
public closeDialog(dialog: azdata.window.Dialog): void {
let handle = this.getHandle(dialog);
this._proxy.$closeDialog(handle);

View File

@@ -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<string | undefined> {
return extHostModelViewDialog.openCustomErrorDialog(options);
}
};
const tasks: typeof azdata.tasks = {

View File

@@ -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<azdata.diagnostics.ConnectionDiagnosticsResult> { throw ni(); }
$handleConnectionError(handle: number, errorInfo: azdata.diagnostics.IErrorInformation, connection: azdata.connection.ConnectionProfile): Thenable<azdata.diagnostics.ConnectionDiagnosticsResult> { 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<void>;
$closeEditor(handle: number): Thenable<void>;
$openDialog(handle: number, dialogName?: string): Thenable<void>;
$openCustomErrorDialog(options: IErrorDialogOptions): Promise<string | undefined>;
$closeDialog(handle: number): Thenable<void>;
$setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void>;
$setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void>;

View File

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

View File

@@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -203,7 +203,7 @@ suite('SQL ConnectionManagementService tests', () => {
TypeMoq.It.isAny(),
TypeMoq.It.is<INewConnectionParams>(p => p.connectionType === connectionType && (uri === undefined || p.input.uri === uri)),
TypeMoq.It.is<IConnectionProfile>(c => c !== undefined && c.serverName === connectionProfile.serverName),
connectionResult ? TypeMoq.It.is<IConnectionResult>(r => r.errorMessage === connectionResult.errorMessage && r.callStack === connectionResult.callStack) : undefined,
connectionResult ? TypeMoq.It.is<IConnectionResult>(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<INewConnectionParams>(p => p.connectionType === connectionType && ((uri === undefined && p.input === undefined) || p.input.uri === uri)),
undefined,
connectionResult ? TypeMoq.It.is<IConnectionResult>(r => r.errorMessage === connectionResult.errorMessage && r.callStack === connectionResult.callStack) : undefined,
connectionResult ? TypeMoq.It.is<IConnectionResult>(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);

View File

@@ -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<void> {
return Promise.resolve(undefined);
}
public async showErrorDialogAsync(options: IErrorDialogOptions): Promise<string | undefined> {
return Promise.resolve(undefined);
}
}

View File

@@ -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<azdata.diagnostics.ConnectionDiagnosticsResult> {
tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: azdata.IConnectionProfile): Promise<azdata.diagnostics.ConnectionDiagnosticsResult> {
return Promise.resolve({ handled: false });
}
}

View File

@@ -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<azdata.diagnostics.ConnectionDiagnosticsResult> {
public async tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: interfaces.IConnectionProfile): Promise<azdata.diagnostics.ConnectionDiagnosticsResult> {
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;
}

View File

@@ -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<azdata.diagnostics.ConnectionDiagnosticsResult>;
tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: azdata.IConnectionProfile): Promise<azdata.diagnostics.ConnectionDiagnosticsResult>;
}

View File

@@ -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<string | undefined> {
let dialog = this._instantiationService.createInstance(ErrorMessageDialog);
dialog.render();
let result = await dialog.openCustomAsync(options);
return result;
}
}

View File

@@ -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<void>();
public onOk: Event<void> = 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<string | undefined> {
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<string | undefined>();
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) {

View File

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