mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 17:22:45 -05:00
Migrate cert validation error handling to mssql extension (#21829)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user