mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-29 09:35:38 -05:00
Added validations to configure dialog (#24418)
* Added validations to configure dialog * Improved validations messages text * Addressed PR feedback * Addressed PR feedback * Fixed build issue * Version bump * Changed to single quotes
This commit is contained in:
@@ -1516,8 +1516,18 @@ export const TDE_WIZARD_MSG_MANUAL = localize('sql.migration.tde.msg.manual', "Y
|
||||
export const TDE_WIZARD_MSG_TDE = localize('sql.migration.tde.msg.tde', "You have given Azure Data Studio access to migrate the encryption certificates and database.");
|
||||
export const TDE_WIZARD_MSG_EMPTY = localize('sql.migration.tde.msg.empty', "No encrypted database selected.");
|
||||
|
||||
export const TDE_VALIDATION_TITLE = localize('sql.migration.tde.validation.title', "Validation");
|
||||
export const TDE_VALIDATION_REQUIREMENTS_MESSAGE = localize('sql.migration.tde.validation.requirements.message', "In order for certificate migration to succeed, you must meet all of the requirements listed below.\n\nClick \"Run validation\" to check that requirements are met.");
|
||||
export const TDE_VALIDATION_STATUS_PENDING = localize('sql.migration.tde.validation.status.pending', "Pending");
|
||||
export const TDE_VALIDATION_STATUS_RUNNING = localize('sql.migration.tde.validation.running', "Running");
|
||||
export const TDE_VALIDATION_STATUS_SUCCEEDED = localize('sql.migration.tde.validation.status.succeeded', "Succeeded");
|
||||
export const TDE_VALIDATION_STATUS_RUN_VALIDATION = localize('sql.migration.tde.validation.run.validation', "Run validation");
|
||||
export const TDE_VALIDATION_DESCRIPTION = localize('sql.migration.tde.validation.description', "Description");
|
||||
export const TDE_VALIDATION_ERROR = localize('sql.migration.tde.validation.error', "Error");
|
||||
export const TDE_VALIDATION_TROUBLESHOOTING_TIPS = localize('sql.migration.tde.validation.troubleshooting.tips', "Troubleshooting tips");
|
||||
|
||||
export function TDE_MIGRATION_ERROR(message: string): string {
|
||||
return localize('sql.migration.starting.migration.error', "An error occurred while starting the certificate migration: '{0}'", message);
|
||||
return localize('sql.migration.starting.migration.error', "The following error has occurred while starting the certificate migration: '{0}'", message);
|
||||
}
|
||||
|
||||
export function TDE_MIGRATION_ERROR_DB(name: string, message: string): string {
|
||||
|
||||
@@ -9,7 +9,9 @@ import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
import * as utils from '../../api/utils';
|
||||
import { EOL } from 'os';
|
||||
import { ConfigDialogSetting } from '../../models/tdeModels'
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
|
||||
export class TdeConfigurationDialog {
|
||||
|
||||
@@ -22,8 +24,12 @@ export class TdeConfigurationDialog {
|
||||
private _adsConfirmationCheckBox!: azdata.CheckBoxComponent;
|
||||
private _manualMethodWarningContainer!: azdata.FlexContainer;
|
||||
private _networkPathText!: azdata.InputBoxComponent;
|
||||
private _validationTable!: azdata.TableComponent;
|
||||
private _validationMessagesText!: azdata.InputBoxComponent;
|
||||
private _onClosed: () => void;
|
||||
|
||||
private _validationSuccessDescriptionErrorAndTips!: string[][];
|
||||
|
||||
constructor(public migrationStateModel: MigrationStateModel, onClosed: () => void) {
|
||||
this._onClosed = onClosed;
|
||||
}
|
||||
@@ -130,6 +136,24 @@ export class TdeConfigurationDialog {
|
||||
adsMethodButton.onDidChangeCheckedState(async checked => {
|
||||
if (checked) {
|
||||
this.migrationStateModel.tdeMigrationConfig.setPendingTdeMigrationMethod(ConfigDialogSetting.ExportCertificates);
|
||||
let validationTitleData = await this.migrationStateModel.getTdeValidationTitles();
|
||||
|
||||
let networkPathValidated =
|
||||
(this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath() !== '') &&
|
||||
(this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath() === this.migrationStateModel.tdeMigrationConfig.getLastValidatedNetworkPath())
|
||||
|
||||
let result = validationTitleData.result.map(validationTitle => {
|
||||
return [
|
||||
validationTitle,
|
||||
{
|
||||
'icon': networkPathValidated ? IconPathHelper.completedMigration : IconPathHelper.notFound,
|
||||
'title': networkPathValidated ? constants.TDE_VALIDATION_STATUS_SUCCEEDED : constants.TDE_VALIDATION_STATUS_PENDING
|
||||
},
|
||||
networkPathValidated ? constants.TDE_VALIDATION_STATUS_SUCCEEDED : constants.TDE_VALIDATION_STATUS_PENDING
|
||||
]
|
||||
});
|
||||
|
||||
await this._validationTable.updateProperty('data', result)
|
||||
await this.updateUI();
|
||||
}
|
||||
}));
|
||||
@@ -228,6 +252,7 @@ export class TdeConfigurationDialog {
|
||||
this._disposables.push(
|
||||
this._networkPathText.onTextChanged(async networkPath => {
|
||||
this.migrationStateModel.tdeMigrationConfig.setPendingNetworkPath(networkPath);
|
||||
await this.updateUI();
|
||||
}));
|
||||
|
||||
this._adsConfirmationCheckBox = _view.modelBuilder.checkBox()
|
||||
@@ -245,15 +270,172 @@ export class TdeConfigurationDialog {
|
||||
await this.updateUI();
|
||||
}));
|
||||
|
||||
const preValidationSeparator = _view.modelBuilder.separator().component();
|
||||
|
||||
const validationRequiredLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.TDE_VALIDATION_REQUIREMENTS_MESSAGE,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '4px 2px 4px 2px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const runValidationButton = _view.modelBuilder.button()
|
||||
.withProps(
|
||||
{
|
||||
label: constants.TDE_VALIDATION_STATUS_RUN_VALIDATION,
|
||||
enabled: true
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
runValidationButton.onDidClick(async (e) => {
|
||||
let data = this._validationTable.data.map((e) => {
|
||||
return [
|
||||
e[0],
|
||||
{
|
||||
'icon': IconPathHelper.inProgressMigration,
|
||||
'title': constants.TDE_VALIDATION_STATUS_RUNNING
|
||||
},
|
||||
constants.TDE_VALIDATION_STATUS_RUNNING
|
||||
]
|
||||
});
|
||||
await this._validationTable.updateProperty('data', data);
|
||||
|
||||
let validationData = await this.migrationStateModel.runTdeValidation(
|
||||
this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath());
|
||||
|
||||
let allValidationsSucceeded = true;
|
||||
|
||||
this._validationSuccessDescriptionErrorAndTips = validationData.result.map(e => {
|
||||
return [
|
||||
e.validationStatus.toString(),
|
||||
e.validationDescription,
|
||||
e.validationErrorMessage,
|
||||
e.validationTroubleshootingTips
|
||||
]
|
||||
});
|
||||
|
||||
let res = validationData.result.map(e => {
|
||||
if (e.validationStatus < 0) {
|
||||
allValidationsSucceeded = false;
|
||||
}
|
||||
|
||||
return [
|
||||
e.validationTitle,
|
||||
{
|
||||
'icon': e.validationStatus > 0 ? IconPathHelper.completedMigration : IconPathHelper.error,
|
||||
'title': e.validationStatusString
|
||||
},
|
||||
e.validationStatusString
|
||||
]
|
||||
});
|
||||
|
||||
await this._validationTable.updateProperty('data', res);
|
||||
|
||||
if (allValidationsSucceeded) {
|
||||
this.migrationStateModel.tdeMigrationConfig.setLastValidatedNetworkPath(
|
||||
this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath());
|
||||
await this.updateUI();
|
||||
}
|
||||
}));
|
||||
|
||||
this._validationTable = this._createValidationTable(_view);
|
||||
|
||||
this._disposables.push(
|
||||
this._validationTable.onRowSelected(
|
||||
async (e) => {
|
||||
const selectedRows: number[] = this._validationTable.selectedRows ?? [];
|
||||
|
||||
let message: string = '';
|
||||
selectedRows.forEach((rowIndex) => {
|
||||
|
||||
let successful = this._validationSuccessDescriptionErrorAndTips[rowIndex][0] === "1" // Value will be "1" if successful
|
||||
let description = this._validationSuccessDescriptionErrorAndTips[rowIndex][1];
|
||||
let errorMessage = this._validationSuccessDescriptionErrorAndTips[rowIndex][2];
|
||||
let tips = this._validationSuccessDescriptionErrorAndTips[rowIndex][3];
|
||||
|
||||
message = `${constants.TDE_VALIDATION_DESCRIPTION}:${EOL}${description}`;
|
||||
if (!successful) {
|
||||
message += `${EOL}${EOL}`;
|
||||
if (errorMessage?.length > 0) {
|
||||
message += `${constants.TDE_VALIDATION_ERROR}:${EOL}${errorMessage}${EOL}${EOL}`;
|
||||
}
|
||||
|
||||
message += `${constants.TDE_VALIDATION_TROUBLESHOOTING_TIPS}:${EOL}${tips}`;
|
||||
}
|
||||
});
|
||||
|
||||
this._validationMessagesText.value = message;
|
||||
}));
|
||||
|
||||
this._validationMessagesText = _view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
inputType: 'text',
|
||||
height: 142,
|
||||
multiline: true,
|
||||
CSSStyles: { 'overflow': 'none auto' }
|
||||
})
|
||||
.component();
|
||||
|
||||
const postValidationSeparator = _view.modelBuilder.separator().component();
|
||||
|
||||
container.addItems([
|
||||
adsMethodInfoMessage,
|
||||
networkPathLabel,
|
||||
this._networkPathText,
|
||||
this._adsConfirmationCheckBox]);
|
||||
this._adsConfirmationCheckBox,
|
||||
preValidationSeparator,
|
||||
validationRequiredLabel,
|
||||
runValidationButton,
|
||||
this._validationTable,
|
||||
this._validationMessagesText,
|
||||
postValidationSeparator
|
||||
]);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private _createValidationTable(view: azdata.ModelView): azdata.TableComponent {
|
||||
return view.modelBuilder.table()
|
||||
.withProps({
|
||||
columns: [
|
||||
{
|
||||
value: 'title',
|
||||
name: constants.TDE_VALIDATION_TITLE,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 320,
|
||||
headerCssClass: 'no-borders',
|
||||
cssClass: 'no-borders align-with-header',
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
name: '',
|
||||
type: azdata.ColumnType.icon,
|
||||
width: 30,
|
||||
headerCssClass: 'no-borders display-none',
|
||||
cssClass: 'no-borders align-with-header',
|
||||
},
|
||||
{
|
||||
value: 'message',
|
||||
name: constants.TDE_MIGRATE_COLUMN_STATUS,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 100,
|
||||
headerCssClass: 'no-borders',
|
||||
cssClass: 'no-borders align-with-header',
|
||||
},
|
||||
],
|
||||
data: [],
|
||||
width: 450,
|
||||
height: 80,
|
||||
CSSStyles: {
|
||||
'margin-top': '10px',
|
||||
'margin-bottom': '10px',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
}
|
||||
|
||||
private createManualWarningContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
@@ -293,7 +475,7 @@ export class TdeConfigurationDialog {
|
||||
await utils.updateControlDisplay(this._adsMethodConfirmationContainer, exportCertsUsingAds);
|
||||
await utils.updateControlDisplay(this._manualMethodWarningContainer, this.migrationStateModel.tdeMigrationConfig.getPendingConfigDialogSetting() === ConfigDialogSetting.DoNotExport);
|
||||
|
||||
this.dialog!.okButton.enabled = this.migrationStateModel.tdeMigrationConfig.isAnyChangeReadyToBeApplied()
|
||||
this.dialog!.okButton.enabled = this.migrationStateModel.tdeMigrationConfig.isAnyChangeReadyToBeApplied();
|
||||
}
|
||||
|
||||
public async openDialog(dialogName?: string,) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { hashString, deepClone, getBlobContainerNameWithFolder, Blob, getLastBac
|
||||
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
|
||||
import { excludeDatabases, getEncryptConnectionValue, getSourceConnectionId, getSourceConnectionProfile, getSourceConnectionServerInfo, getSourceConnectionString, getSourceConnectionUri, getTrustServerCertificateValue, SourceDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
|
||||
import { LoginMigrationModel } from './loginMigrationModel';
|
||||
import { TdeMigrationDbResult, TdeMigrationModel } from './tdeModels';
|
||||
import { TdeMigrationDbResult, TdeMigrationModel, TdeValidationResult } from './tdeModels';
|
||||
import { NetworkInterfaceModel } from '../api/dataModels/azure/networkInterfaceModel';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -1000,6 +1000,55 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return opResult;
|
||||
}
|
||||
|
||||
public async getTdeValidationTitles(): Promise<OperationResult<string[]>> {
|
||||
const opResult: OperationResult<string[]> = {
|
||||
success: false,
|
||||
result: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
opResult.result = await this.migrationService.getTdeValidationTitles() ?? [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return opResult;
|
||||
}
|
||||
|
||||
public async runTdeValidation(networkSharePath: string): Promise<OperationResult<TdeValidationResult[]>> {
|
||||
const opResult: OperationResult<TdeValidationResult[]> = {
|
||||
success: false,
|
||||
result: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
const connectionString = await getSourceConnectionString();
|
||||
|
||||
try {
|
||||
let tdeValidationResult = await this.migrationService.runTdeValidation(
|
||||
connectionString,
|
||||
networkSharePath);
|
||||
|
||||
if (tdeValidationResult !== undefined) {
|
||||
opResult.result = tdeValidationResult?.map((e) => {
|
||||
return {
|
||||
validationTitle: e.validationTitle,
|
||||
validationDescription: e.validationDescription,
|
||||
validationTroubleshootingTips: e.validationTroubleshootingTips,
|
||||
validationErrorMessage: e.validationErrorMessage,
|
||||
validationStatus: e.validationStatus,
|
||||
validationStatusString: e.validationStatusString
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return opResult;
|
||||
}
|
||||
|
||||
public async startMigration() {
|
||||
const currentConnection = await getSourceConnectionProfile();
|
||||
const isOfflineMigration = this._databaseBackup.migrationMode === MigrationMode.OFFLINE;
|
||||
|
||||
@@ -41,6 +41,15 @@ export interface TdeMigrationDbResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TdeValidationResult {
|
||||
validationTitle: string;
|
||||
validationDescription: string;
|
||||
validationTroubleshootingTips: string;
|
||||
validationErrorMessage: string;
|
||||
validationStatus: number;
|
||||
validationStatusString: string;
|
||||
}
|
||||
|
||||
export class TdeMigrationModel {
|
||||
|
||||
// Settings for which the user has clicked the apply button
|
||||
@@ -53,6 +62,9 @@ export class TdeMigrationModel {
|
||||
private _pendingExportCertUserConsent: boolean;
|
||||
private _pendingNetworkPath: string;
|
||||
|
||||
// Last network path for which all validations succeeded
|
||||
private _lastValidatedNetworkPath: string;
|
||||
|
||||
private _configurationCompleted: boolean;
|
||||
private _shownBefore: boolean;
|
||||
private _encryptedDbs: string[];
|
||||
@@ -75,6 +87,7 @@ export class TdeMigrationModel {
|
||||
this._appliedExportCertUserConsent = false;
|
||||
this._pendingExportCertUserConsent = false;
|
||||
this._tdeMigrationCompleted = false;
|
||||
this._lastValidatedNetworkPath = '';
|
||||
|
||||
this._tdeMigrationCompleted = this._tdeMigrationCompleted;
|
||||
}
|
||||
@@ -176,7 +189,12 @@ export class TdeMigrationModel {
|
||||
}
|
||||
|
||||
if (this._pendingConfigDialogSetting === ConfigDialogSetting.ExportCertificates) {
|
||||
return this._pendingExportCertUserConsent;
|
||||
if (this._pendingNetworkPath !== this._lastValidatedNetworkPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._pendingNetworkPath.length > 0 &&
|
||||
this._pendingExportCertUserConsent;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -213,4 +231,12 @@ export class TdeMigrationModel {
|
||||
public setPendingExportCertUserConsent(pendingExportCertUserConsent: boolean) {
|
||||
this._pendingExportCertUserConsent = pendingExportCertUserConsent;
|
||||
}
|
||||
|
||||
public setLastValidatedNetworkPath(validatedNetworkPath: string) {
|
||||
this._lastValidatedNetworkPath = validatedNetworkPath;
|
||||
}
|
||||
|
||||
public getLastValidatedNetworkPath() {
|
||||
return this._lastValidatedNetworkPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +388,11 @@ export const enum VirtualMachineFamily {
|
||||
standardNVSv4Family
|
||||
}
|
||||
|
||||
export const enum TdeValidationStatus {
|
||||
Failed = -1,
|
||||
Succeeded = 1
|
||||
}
|
||||
|
||||
export namespace GetSqlMigrationSkuRecommendationsRequest {
|
||||
export const type = new RequestType<SqlMigrationSkuRecommendationsParams, SkuRecommendationResult, void, void>('migration/getskurecommendations');
|
||||
}
|
||||
@@ -549,3 +554,25 @@ export interface TdeMigrateProgressParams {
|
||||
message: string;
|
||||
statusCode: string;
|
||||
}
|
||||
|
||||
export interface TdeValidationResult {
|
||||
validationTitle: string;
|
||||
validationDescription: string;
|
||||
validationTroubleshootingTips: string;
|
||||
validationErrorMessage: string;
|
||||
validationStatus: TdeValidationStatus;
|
||||
validationStatusString: string;
|
||||
}
|
||||
|
||||
export interface TdeValidationParams {
|
||||
sourceSqlConnectionString: string;
|
||||
networkSharePath: string;
|
||||
}
|
||||
|
||||
export namespace TdeValidationRequest {
|
||||
export const type = new RequestType<TdeValidationParams, TdeValidationResult[], void, void>('migration/tdevalidation');
|
||||
}
|
||||
|
||||
export namespace TdeValidationTitlesRequest {
|
||||
export const type = new RequestType<{}, string[], void, void>('migration/tdevalidationtitles');
|
||||
}
|
||||
|
||||
@@ -316,5 +316,34 @@ export class SqlMigrationService extends MigrationExtensionService implements co
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async runTdeValidation(
|
||||
sourceSqlConnectionString: string,
|
||||
networkSharePath: string,
|
||||
) {
|
||||
let params: contracts.TdeValidationParams = {
|
||||
sourceSqlConnectionString: sourceSqlConnectionString,
|
||||
networkSharePath: networkSharePath,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this._client.sendRequest(contracts.TdeValidationRequest.type, params);
|
||||
}
|
||||
catch (e) {
|
||||
this._client.logFailedRequest(contracts.TdeValidationRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getTdeValidationTitles() {
|
||||
try {
|
||||
return await this._client.sendRequest(contracts.TdeValidationTitlesRequest.type, {});
|
||||
}
|
||||
catch (e) {
|
||||
this._client.logFailedRequest(contracts.TdeValidationRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user