Add Deploy Plan page to DacFx wizard (#3911)

* upgrade plan is piped through and returns the xml plan

* Added review deploy plan page

* checkbox validation now working and columns formatted

* formatting and cleaning up code

* refactored populateTable()

* addressing comments

* addressing comments

* updating tooltips

* add padding to table cells to align with headers

* fix problems when going back and forth between pages and changing config options

* bump sqltoolsservice version to 71

* fix localization
This commit is contained in:
kisantia
2019-02-07 16:39:22 -08:00
committed by GitHub
parent 9ce9a1598f
commit 393be65aa6
13 changed files with 438 additions and 32 deletions

View File

@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
import * as sqlops from 'sqlops';
import { SelectOperationPage } from './pages/selectOperationpage';
import { DeployConfigPage } from './pages/deployConfigPage';
import { DeployPlanPage } from './pages/deployPlanPage';
import { DeployActionPage } from './pages/deployActionPage';
import { DacFxSummaryPage } from './pages/dacFxSummaryPage';
import { ExportConfigPage } from './pages/exportConfigPage';
@@ -38,10 +39,17 @@ export enum Operation {
export enum DeployOperationPath {
selectOperation,
deployOptions,
deployPlan,
deployAction,
summary
}
export enum DeployNewOperationPath {
selectOperation,
deployOptions,
summary
}
export enum ExtractOperationPath {
selectOperation,
options,
@@ -87,6 +95,7 @@ export class DataTierApplicationWizard {
this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard');
let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
let deployPlanWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployPlanPage', 'Review the deploy plan'));
let deployActionWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployActionPageName', 'Select Action'));
let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
@@ -95,6 +104,7 @@ export class DataTierApplicationWizard {
this.pages.set('selectOperation', new Page(selectOperationWizardPage));
this.pages.set('deployConfig', new Page(deployConfigWizardPage));
this.pages.set('deployPlan', new Page(deployPlanWizardPage));
this.pages.set('deployAction', new Page(deployActionWizardPage));
this.pages.set('extractConfig', new Page(extractConfigWizardPage));
this.pages.set('importConfig', new Page(importConfigWizardPage));
@@ -116,6 +126,12 @@ export class DataTierApplicationWizard {
await deployConfigDacFxPage.start();
});
deployPlanWizardPage.registerContent(async (view) => {
let deployPlanDacFxPage = new DeployPlanPage(this, deployPlanWizardPage, this.model, view);
this.pages.get('deployPlan').dacFxPage = deployPlanDacFxPage;
await deployPlanDacFxPage.start();
});
deployActionWizardPage.registerContent(async (view) => {
let deployActionDacFxPage = new DeployActionPage(this, deployActionWizardPage, this.model, view);
this.pages.get('deployAction').dacFxPage = deployActionDacFxPage;
@@ -166,7 +182,7 @@ export class DataTierApplicationWizard {
}
});
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployActionWizardPage, summaryWizardPage];
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployPlanWizardPage, deployActionWizardPage, summaryWizardPage];
this.wizard.generateScriptButton.hidden = true;
this.wizard.generateScriptButton.onClick(async () => await this.generateDeployScript());
this.wizard.doneButton.onClick(async () => await this.executeOperation());
@@ -323,10 +339,12 @@ export class DataTierApplicationWizard {
break;
}
}
} else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployAction) {
page = this.pages.get('deployAction');
} else if (this.isSummaryPage(idx)) {
page = this.pages.get('summary');
} else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployPlan) {
page = this.pages.get('deployPlan');
} else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployAction) {
page = this.pages.get('deployAction');
}
return page;
@@ -336,9 +354,24 @@ export class DataTierApplicationWizard {
return this.selectedOperation === Operation.import && idx === ImportOperationPath.summary
|| this.selectedOperation === Operation.export && idx === ExportOperationPath.summary
|| this.selectedOperation === Operation.extract && idx === ExtractOperationPath.summary
|| this.selectedOperation === Operation.deploy && !this.model.upgradeExisting && idx === DeployNewOperationPath.summary
|| (this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.summary;
}
public async generateDeployPlan(): Promise<string> {
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.deployPlanErrorMessage', "Generating deploy plan failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
return result.report;
}
private static async getService(providerName: string): Promise<sqlops.DacFxServicesProvider> {
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(providerName, sqlops.DataProviderType.DacFxServicesProvider);
return service;

View File

@@ -131,7 +131,15 @@ export class DacFxSummaryPage extends BasePage {
this.table.updateProperties({
data: data,
columns: ['Setting', 'Value'],
columns: [
{
value: localize('dacfx.settingColumn', 'Setting'),
cssClass: 'align-with-header'
},
{
value: localize('dacfx.valueColumn', 'Value'),
cssClass: 'align-with-header'
}],
width: 700,
height: 200
});

View File

@@ -54,6 +54,8 @@ export class DeployActionPage extends DacFxConfigPage {
}
async onPageEnter(): Promise<boolean> {
// generate script file path in case the database changed since last time the page was entered
this.setDefaultScriptFilePath();
return true;
}
@@ -120,11 +122,7 @@ export class DeployActionPage extends DacFxConfigPage {
this.createFileBrowserParts();
//default filepath
let now = new Date();
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
this.fileTextBox.value= path.join(os.homedir(), this.model.database + '_UpgradeDACScript_' + datetime + '.sql');
this.model.scriptFilePath = this.fileTextBox.value;
this.setDefaultScriptFilePath();
this.fileButton.onDidClick(async (click) => {
let fileUri = await vscode.window.showSaveDialog(
{
@@ -151,15 +149,15 @@ export class DeployActionPage extends DacFxConfigPage {
return {
title: '',
components: [
{
title: localize('dacfx.generatedScriptLocation','Deployment Script Location'),
component: this.fileTextBox,
layout: {
horizontal: true,
componentWidth: 400
},
actions: [this.fileButton]
},],
{
title: localize('dacfx.generatedScriptLocation', 'Deployment Script Location'),
component: this.fileTextBox,
layout: {
horizontal: true,
componentWidth: 400
},
actions: [this.fileButton]
},],
};
}
@@ -168,6 +166,13 @@ export class DeployActionPage extends DacFxConfigPage {
this.fileButton.enabled = enable;
}
private setDefaultScriptFilePath(): void {
let now = new Date();
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
this.fileTextBox.value = path.join(os.homedir(), this.model.database + '_UpgradeDACScript_' + datetime + '.sql');
this.model.scriptFilePath = this.fileTextBox.value;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
return true;

View File

@@ -10,7 +10,7 @@ import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DataTierApplicationWizard, DeployOperationPath, Operation } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
@@ -122,6 +122,12 @@ export class DeployConfigPage extends DacFxConfigPage {
this.formBuilder.removeFormItem(this.databaseComponent);
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
// add deploy plan and generate script pages
let deployPlanPage = this.instance.pages.get('deployPlan');
this.instance.wizard.addPage(deployPlanPage.wizardPage, DeployOperationPath.deployPlan);
let deployActionPage = this.instance.pages.get('deployAction');
this.instance.wizard.addPage(deployActionPage.wizardPage, DeployOperationPath.deployAction);
});
newRadioButton.onDidClick(() => {
@@ -129,6 +135,11 @@ export class DeployConfigPage extends DacFxConfigPage {
this.formBuilder.removeFormItem(this.databaseDropdownComponent);
this.formBuilder.addFormItem(this.databaseComponent, { horizontal: true, componentWidth: 400 });
this.model.database = this.databaseTextBox.value;
this.instance.setDoneButton(Operation.deploy);
// remove deploy plan and generate script pages
this.instance.wizard.removePage(DeployOperationPath.deployAction);
this.instance.wizard.removePage(DeployOperationPath.deployPlan);
});
//Initialize with upgrade existing true

View File

@@ -0,0 +1,296 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
import * as parser from 'htmlparser2';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export enum deployPlanXml {
AlertElement = 'Alert',
OperationElement = 'Operation',
ItemElement = 'Item',
NameAttribute = 'Name',
ValueAttribute = 'Value',
TypeAttribute = 'Type',
IdAttribute = 'Id',
DataIssueAttribute = 'DataIssue'
}
export class TableObject {
operation: string;
object: string;
type: string;
dataloss: boolean;
}
export class DeployPlanResult {
columnData: Array<Array<string>>;
dataLossAlerts: Set<string>;
}
export class DeployPlanPage extends DacFxConfigPage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private formBuilder: sqlops.FormBuilder;
private form: sqlops.FormContainer;
private table: sqlops.TableComponent;
private loader: sqlops.LoadingComponent;
private dataLossCheckbox: sqlops.CheckBoxComponent;
private dataLossText: sqlops.TextComponent;
private dataLossComponentGroup: sqlops.FormComponentGroup;
private noDataLossTextComponent: sqlops.FormComponent;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super(instance, wizardPage, model, view);
}
async start(): Promise<boolean> {
this.table = this.view.modelBuilder.table().component();
this.loader = this.view.modelBuilder.loadingComponent().withItem(this.table).component();
this.dataLossComponentGroup = await this.createDataLossComponents();
this.noDataLossTextComponent = await this.createNoDataLossText();
this.formBuilder = this.view.modelBuilder.formContainer()
.withFormItems(
[
{
component: this.loader,
title: ''
},
this.dataLossComponentGroup
], {
horizontal: true,
});
this.form = this.formBuilder.component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
// reset checkbox settings
this.formBuilder.addFormItem(this.dataLossComponentGroup, { horizontal: true, componentWidth: 400 });
this.dataLossCheckbox.checked = false;
this.dataLossCheckbox.enabled = false;
this.formBuilder.removeFormItem(this.noDataLossTextComponent);
this.loader.loading = true;
this.table.data = [];
await this.populateTable();
this.loader.loading = false;
return true;
}
private async populateTable() {
let report = await this.instance.generateDeployPlan();
let result = this.parseXml(report);
this.table.updateProperties({
data: this.getColumnData(result),
columns: this.getTableColumns(result.dataLossAlerts.size > 0),
width: 875,
height: 300
});
if (result.dataLossAlerts.size > 0) {
// update message to list how many operations could result in data loss
this.dataLossText.updateProperties({
value: localize('dacfx.dataLossTextWithCount', '{0} of the deploy actions listed may result in data loss. Please ensure you have a backup or snapshot available in the event of an issue with the deployment.', result.dataLossAlerts.size)
});
this.dataLossCheckbox.enabled = true;
} else {
// check checkbox to enable Next button and remove checkbox because there won't be any possible data loss
this.dataLossCheckbox.checked = true;
this.formBuilder.removeFormItem(this.dataLossComponentGroup);
this.formBuilder.addFormItem(this.noDataLossTextComponent, { componentWidth: 300, horizontal: true });
}
}
private async createDataLossCheckbox(): Promise<sqlops.FormComponent> {
this.dataLossCheckbox = this.view.modelBuilder.checkBox()
.withValidation(component => component.checked === true)
.withProperties({
label: localize('dacFx.dataLossCheckbox', 'Proceed despite possible data loss'),
}).component();
return {
component: this.dataLossCheckbox,
title: '',
required: true
};
}
private async createNoDataLossText(): Promise<sqlops.FormComponent> {
let noDataLossText = this.view.modelBuilder.text()
.withProperties({
value: localize('dacfx.noDataLossText', 'No data loss will occur from the listed deploy actions.')
}).component();
return {
title: '',
component: noDataLossText
};
}
private async createDataLossComponents(): Promise<sqlops.FormComponentGroup> {
let dataLossComponent = await this.createDataLossCheckbox();
this.dataLossText = this.view.modelBuilder.text()
.withProperties({
value: localize('dacfx.dataLossText', 'The deploy actions may result in data loss. Please ensure you have a backup or snapshot available in the event of an issue with the deployment.')
}).component();
return {
title: '',
components: [
{
component: this.dataLossText,
layout: {
componentWidth: 400,
horizontal: true
},
title: ''
},
dataLossComponent
]
};
}
private getColumnData(result: DeployPlanResult): Array<Array<string>> {
// remove data loss column data if there aren't any alerts
let columnData = result.columnData;
if (result.dataLossAlerts.size === 0) {
columnData.forEach(entry => {
entry.shift();
});
}
return columnData;
}
private getTableColumns(dataloss: boolean): sqlops.TableColumn[] {
let columns: sqlops.TableColumn[] = [
{
value: localize('dacfx.operationColumn', 'Operation'),
width: 75,
cssClass: 'align-with-header',
toolTip: localize('dacfx.operationTooltip', 'Operation(Create, Alter, Delete) that will occur during deployment')
},
{
value: localize('dacfx.typeColumn', 'Type'),
width: 100,
cssClass: 'align-with-header',
toolTip: localize('dacfx.typeTooltip', 'Type of object that will be affected by deployment')
},
{
value: localize('dacfx.objectColumn', 'Object'),
width: 300,
cssClass: 'align-with-header',
toolTip: localize('dacfx.objecTooltip', 'Name of object that will be affected by deployment')
}];
if (dataloss) {
columns.unshift(
{
value: localize('dacfx.dataLossColumn', 'Data Loss'),
width: 50,
cssClass: 'center-align',
toolTip: localize('dacfx.dataLossTooltip', 'Operations that may result in data loss are marked with a warning sign')
});
}
return columns;
}
private parseXml(report: string): DeployPlanResult {
let operations = new Array<TableObject>();
let dataLossAlerts = new Set<string>();
let currentOperation = '';
let dataIssueAlert = false;
let currentReportSection: deployPlanXml;
let currentTableObj: TableObject;
let p = new parser.Parser({
onopentagname(name) {
if (name === deployPlanXml.AlertElement) {
currentReportSection = deployPlanXml.AlertElement;
} else if (name === deployPlanXml.OperationElement) {
currentReportSection = deployPlanXml.OperationElement;
} else if (name === deployPlanXml.ItemElement) {
currentTableObj = new TableObject();
}
},
onattribute: function (name, value) {
if (currentReportSection === deployPlanXml.AlertElement) {
switch (name) {
case deployPlanXml.NameAttribute: {
// only care about showing data loss alerts
if (value === deployPlanXml.DataIssueAttribute) {
dataIssueAlert = true;
}
break;
}
case deployPlanXml.IdAttribute: {
if (dataIssueAlert) {
dataLossAlerts.add(value);
}
break;
}
}
} else if (currentReportSection === deployPlanXml.OperationElement) {
switch (name) {
case deployPlanXml.NameAttribute: {
currentOperation = value;
break;
}
case deployPlanXml.ValueAttribute: {
currentTableObj.object = value;
break;
}
case deployPlanXml.TypeAttribute: {
currentTableObj.type = value;
break;
}
case deployPlanXml.IdAttribute: {
if (dataLossAlerts.has(value)) {
currentTableObj.dataloss = true;
}
break;
}
}
}
},
onclosetag: function (name) {
if (name === deployPlanXml.ItemElement) {
currentTableObj.operation = currentOperation;
operations.push(currentTableObj);
}
}
}, { xmlMode: true, decodeEntities: true });
p.parseChunk(report);
let data = new Array<Array<string>>();
operations.forEach(operation => {
let isDataLoss = operation.dataloss ? '⚠️' : '';
data.push([isDataLoss, operation.operation, operation.type, operation.object]);
});
return {
columnData: data,
dataLossAlerts: dataLossAlerts
};
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
return true;
});
}
}

View File

@@ -7,7 +7,7 @@
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
import { DataTierApplicationWizard, Operation, DeployOperationPath, ExtractOperationPath, ImportOperationPath, ExportOperationPath } from '../dataTierApplicationWizard';
import { BasePage } from '../api/basePage';
const localize = nls.loadMessageBundle();
@@ -73,10 +73,12 @@ export class SelectOperationPage extends BasePage {
//add deploy pages
let configPage = this.instance.pages.get('deployConfig');
this.instance.wizard.addPage(configPage.wizardPage, 1);
this.instance.wizard.addPage(configPage.wizardPage, DeployOperationPath.deployOptions);
let deployPlanPage = this.instance.pages.get('deployPlan');
this.instance.wizard.addPage(deployPlanPage.wizardPage, DeployOperationPath.deployPlan);
let actionPage = this.instance.pages.get('deployAction');
this.instance.wizard.addPage(actionPage.wizardPage, 2);
this.addSummaryPage(3);
this.instance.wizard.addPage(actionPage.wizardPage, DeployOperationPath.deployAction);
this.addSummaryPage(DeployOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.deploy);
@@ -100,8 +102,8 @@ export class SelectOperationPage extends BasePage {
// add the extract page
let page = this.instance.pages.get('extractConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
this.addSummaryPage(2);
this.instance.wizard.addPage(page.wizardPage, ExtractOperationPath.options);
this.addSummaryPage(ExtractOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.extract);
@@ -125,8 +127,8 @@ export class SelectOperationPage extends BasePage {
// add the import page
let page = this.instance.pages.get('importConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
this.addSummaryPage(2);
this.instance.wizard.addPage(page.wizardPage, ImportOperationPath.options);
this.addSummaryPage(ImportOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.import);
@@ -150,8 +152,8 @@ export class SelectOperationPage extends BasePage {
// add the export pages
let page = this.instance.pages.get('exportConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
this.addSummaryPage(2);
this.instance.wizard.addPage(page.wizardPage, ExportOperationPath.options);
this.addSummaryPage(ExportOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.export);