Move DacFx wizard into separate extension (#4115)

* Moved dacfx wizard into separate extension

* updating to use azdata

* one more azdata change

* bump import extension version

* renaming extension to dacpac
This commit is contained in:
kisantia
2019-03-06 17:45:30 -08:00
committed by GitHub
parent b8f454b8ac
commit 5f003b0dd7
28 changed files with 473 additions and 66 deletions

View File

@@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
import { BasePage } from '../api/basePage';
const localize = nls.loadMessageBundle();
export class DacFxSummaryPage extends BasePage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private form: azdata.FormContainer;
private table: azdata.TableComponent;
private loader: azdata.LoadingComponent;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
}
async start(): Promise<boolean> {
this.table = this.view.modelBuilder.table().component();
this.loader = this.view.modelBuilder.loadingComponent().withItem(this.table).component();
this.form = this.view.modelBuilder.formContainer().withFormItems(
[
{
component: this.table,
title: ''
}
]
).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
this.populateTable();
this.loader.loading = false;
if (this.model.upgradeExisting && this.model.generateScriptAndDeploy) {
this.instance.wizard.generateScriptButton.hidden = false;
}
return true;
}
async onPageLeave(): Promise<boolean> {
this.instance.wizard.generateScriptButton.hidden = true;
return true;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
if (this.loader.loading) {
return false;
}
return true;
});
}
private populateTable() {
let data = [];
let targetServer = localize('dacfx.targetServerName', 'Target Server');
let targetDatabase = localize('dacfx.targetDatabaseName', 'Target Database');
let sourceServer = localize('dacfx.sourceServerName', 'Source Server');
let sourceDatabase = localize('dacfx.sourceDatabaseName', 'Source Database');
let fileLocation = localize('dacfx.fileLocation', 'File Location');
let scriptLocation = localize('dacfx.scriptLocation', 'Deployment Script Location');
let action = localize('dacfx.action', 'Action');
let deploy = localize('dacfx.deploy', 'Deploy');
let generateScript = localize('dacfx.generateScript', 'Generate Deployment Script');
switch (this.instance.selectedOperation) {
case Operation.deploy: {
data = [
[targetServer, this.model.serverName],
[fileLocation, this.model.filePath],
[targetDatabase, this.model.database]];
if (this.model.generateScriptAndDeploy) {
data[3] = [scriptLocation, this.model.scriptFilePath];
data[4] = [action, generateScript + ', ' + deploy];
}
else {
data[3] = [action, deploy];
}
break;
}
case Operation.extract: {
data = [
[sourceServer, this.model.serverName],
[sourceDatabase, this.model.database],
[localize('dacfxExtract.version', 'Version'), this.model.version],
[fileLocation, this.model.filePath]];
break;
}
case Operation.import: {
data = [
[targetServer, this.model.serverName],
[fileLocation, this.model.filePath],
[targetDatabase, this.model.database]];
break;
}
case Operation.export: {
data = [
[sourceServer, this.model.serverName],
[sourceDatabase, this.model.database],
[fileLocation, this.model.filePath]];
break;
}
case Operation.generateDeployScript: {
data = [
[targetServer, this.model.serverName],
[fileLocation, this.model.filePath],
[targetDatabase, this.model.database],
[scriptLocation, this.model.scriptFilePath],
[action, generateScript]];
break;
}
}
this.table.updateProperties({
data: data,
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

@@ -0,0 +1,181 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class DeployActionPage extends DacFxConfigPage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private deployRadioButton: azdata.RadioButtonComponent;
private deployScriptRadioButton: azdata.RadioButtonComponent;
private scriptRadioButton: azdata.RadioButtonComponent;
private form: azdata.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super(instance, wizardPage, model, view);
}
async start(): Promise<boolean> {
let deployComponent = await this.createDeployRadioButton();
let deployScriptComponent = await this.createDeployScriptRadioButton();
let scriptComponent = await this.createScriptRadioButton();
let fileBrowserComponent = await this.createFileBrowser();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
deployComponent,
scriptComponent,
deployScriptComponent,
fileBrowserComponent
]).component();
await this.view.initializeModel(this.form);
//default have the first radio button checked
this.deployRadioButton.checked = true;
this.toggleFileBrowser(false);
return true;
}
async onPageEnter(): Promise<boolean> {
// generate script file path in case the database changed since last time the page was entered
this.setDefaultScriptFilePath();
return true;
}
private async createDeployRadioButton(): Promise<azdata.FormComponent> {
this.deployRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedDeployAction',
label: localize('dacFx.deployRadioButtonLabel', 'Deploy'),
}).component();
this.deployRadioButton.onDidClick(() => {
this.model.generateScriptAndDeploy = false;
this.instance.setDoneButton(Operation.deploy);
this.toggleFileBrowser(false);
});
return {
component: this.deployRadioButton,
title: ''
};
}
private async createDeployScriptRadioButton(): Promise<azdata.FormComponent> {
this.deployScriptRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedDeployAction',
label: localize('dacFx.deployScriptRadioButtonLabel', 'Generate Deployment Script and Deploy'),
}).component();
this.deployScriptRadioButton.onDidClick(() => {
this.model.generateScriptAndDeploy = true;
this.instance.setDoneButton(Operation.deploy);
this.toggleFileBrowser(true);
});
return {
component: this.deployScriptRadioButton,
title: ''
};
}
private async createScriptRadioButton(): Promise<azdata.FormComponent> {
this.scriptRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedDeployAction',
label: localize('dacFx.scriptRadioButtonLabel', 'Generate Deployment Script'),
}).component();
this.scriptRadioButton.onDidClick(() => {
this.model.generateScriptAndDeploy = false;
this.toggleFileBrowser(true);
//change button text and operation
this.instance.setDoneButton(Operation.generateDeployScript);
});
return {
component: this.scriptRadioButton,
title: ''
};
}
private async createFileBrowser(): Promise<azdata.FormComponentGroup> {
this.createFileBrowserParts();
//default filepath
this.setDefaultScriptFilePath();
this.fileButton.onDidClick(async (click) => {
let fileUri = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(this.fileTextBox.value),
saveLabel: localize('dacfxDeployScript.saveFile', 'Save'),
filters: {
'SQL Files': ['sql'],
}
}
);
if (!fileUri) {
return;
}
this.fileTextBox.value = fileUri.fsPath;
this.model.scriptFilePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.scriptFilePath = this.fileTextBox.value;
});
return {
title: '',
components: [
{
title: localize('dacfx.generatedScriptLocation', 'Deployment Script Location'),
component: this.fileTextBox,
layout: {
horizontal: true,
componentWidth: 400
},
actions: [this.fileButton]
},],
};
}
private toggleFileBrowser(enable: boolean): void {
this.fileTextBox.enabled = enable;
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

@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, DeployOperationPath, Operation } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class DeployConfigPage extends DacFxConfigPage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private databaseDropdownComponent: azdata.FormComponent;
private databaseComponent: azdata.FormComponent;
private formBuilder: azdata.FormBuilder;
private form: azdata.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let serverComponent = await this.createServerDropdown(true);
let fileBrowserComponent = await this.createFileBrowser();
this.databaseComponent = await this.createDatabaseTextBox();
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
this.databaseDropdownComponent = await this.createDeployDatabaseDropdown();
this.databaseDropdownComponent.title = localize('dacFx.databaseNameDropdown', 'Database Name');
let radioButtons = await this.createRadiobuttons();
this.formBuilder = this.view.modelBuilder.formContainer()
.withFormItems(
[
fileBrowserComponent,
serverComponent,
radioButtons,
this.databaseDropdownComponent
], {
horizontal: true,
componentWidth: 400
});
this.form = this.formBuilder.component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDeployDatabaseDropdown();
return r1 && r2;
}
private async createFileBrowser(): Promise<azdata.FormComponent> {
this.createFileBrowserParts();
this.fileButton.onDidClick(async (click) => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(os.homedir()),
openLabel: localize('dacFxDeploy.openFile', 'Open'),
filters: {
'dacpac Files': ['dacpac'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
this.databaseTextBox.value = this.generateDatabaseName(this.model.filePath);
if (!this.model.upgradeExisting) {
this.model.database = this.databaseTextBox.value;
}
});
return {
component: this.fileTextBox,
title: localize('dacFxDeploy.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private async createRadiobuttons(): Promise<azdata.FormComponent> {
let upgradeRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'updateExisting',
label: localize('dacFx.upgradeRadioButtonLabel', 'Upgrade Existing Database'),
}).component();
let newRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'updateExisting',
label: localize('dacFx.newRadioButtonLabel', 'New Database'),
}).component();
upgradeRadioButton.onDidClick(() => {
this.model.upgradeExisting = true;
this.formBuilder.removeFormItem(this.databaseComponent);
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
this.model.database = (<azdata.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(() => {
this.model.upgradeExisting = false;
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
upgradeRadioButton.checked = true;
this.model.upgradeExisting = true;
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
}).withItems([
upgradeRadioButton, newRadioButton]
).component();
return {
component: flexRadioButtonsModel,
title: localize('dacFx.targetDatabaseRadioButtonsTitle', 'Target Database')
};
}
protected async createDeployDatabaseDropdown(): Promise<azdata.FormComponent> {
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
//Handle database changes
this.databaseDropdown.onValueChanged(async () => {
this.model.database = (<azdata.CategoryValue>this.databaseDropdown.value).name;
});
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
return {
component: this.databaseLoader,
title: localize('dacFx.targetDatabaseDropdownTitle', 'Database Name')
};
}
protected async populateDeployDatabaseDropdown(): Promise<boolean> {
this.databaseLoader.loading = true;
this.databaseDropdown.updateProperties({ values: [] });
if (!this.model.server) {
this.databaseLoader.loading = false;
return false;
}
let values = await this.getDatabaseValues();
//set the database to the first dropdown value if upgrading, otherwise it should get set to the textbox value
if (this.model.upgradeExisting) {
this.model.database = values[0].name;
}
this.databaseDropdown.updateProperties({
values: values
});
this.databaseLoader.loading = false;
return true;
}
private generateDatabaseName(filePath: string): string {
let result = path.parse(filePath);
return result.name;
}
}

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 azdata from 'azdata';
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: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private formBuilder: azdata.FormBuilder;
private form: azdata.FormContainer;
private table: azdata.TableComponent;
private loader: azdata.LoadingComponent;
private dataLossCheckbox: azdata.CheckBoxComponent;
private dataLossText: azdata.TextComponent;
private dataLossComponentGroup: azdata.FormComponentGroup;
private noDataLossTextComponent: azdata.FormComponent;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.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<azdata.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<azdata.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<azdata.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): azdata.TableColumn[] {
let columns: azdata.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

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class ExportConfigPage extends DacFxConfigPage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private form: azdata.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let databaseComponent = await this.createDatabaseDropdown();
let serverComponent = await this.createServerDropdown(false);
let fileBrowserComponent = await this.createFileBrowser();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
serverComponent,
databaseComponent,
fileBrowserComponent,
], {
horizontal: true,
componentWidth: 400
}).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDatabaseDropdown();
return r1 && r2;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
if (this.databaseLoader.loading) {
return false;
}
return true;
});
}
private async createFileBrowser(): Promise<azdata.FormComponent> {
this.createFileBrowserParts();
// default filepath
this.fileTextBox.value = this.generateFilePath();
this.model.filePath = this.fileTextBox.value;
this.fileButton.onDidClick(async (click) => {
let fileUri = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(this.fileTextBox.value),
saveLabel: localize('dacfxExport.saveFile', 'Save'),
filters: {
'bacpac Files': ['bacpac'],
}
}
);
if (!fileUri) {
return;
}
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
});
return {
component: this.fileTextBox,
title: localize('dacFxExport.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class ExtractConfigPage extends DacFxConfigPage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private form: azdata.FormContainer;
private versionTextBox: azdata.InputBoxComponent;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.dacpac';
}
async start(): Promise<boolean> {
let databaseComponent = await this.createDatabaseDropdown();
let serverComponent = await this.createServerDropdown(false);
let fileBrowserComponent = await this.createFileBrowser();
let versionComponent = await this.createVersionTextBox();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
serverComponent,
databaseComponent,
versionComponent,
fileBrowserComponent,
], {
horizontal: true,
componentWidth: 400
}).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDatabaseDropdown();
return r1 && r2;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
if (this.databaseLoader.loading) {
return false;
}
return true;
});
}
private async createFileBrowser(): Promise<azdata.FormComponent> {
this.createFileBrowserParts();
// default filepath
this.fileTextBox.value = this.generateFilePath();
this.model.filePath = this.fileTextBox.value;
this.fileButton.onDidClick(async (click) => {
let fileUri = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(this.fileTextBox.value),
saveLabel: localize('dacfxExtract.saveFile', 'Save'),
filters: {
'dacpac Files': ['dacpac'],
}
}
);
if (!fileUri) {
return;
}
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
});
return {
component: this.fileTextBox,
title: localize('dacFxExtract.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private async createVersionTextBox(): Promise<azdata.FormComponent> {
this.versionTextBox = this.view.modelBuilder.inputBox().withProperties({
required: true
}).component();
// default filepath
this.versionTextBox.value = '1.0.0.0';
this.model.version = this.versionTextBox.value;
this.versionTextBox.onTextChanged(async () => {
this.model.version = this.versionTextBox.value;
});
return {
component: this.versionTextBox,
title: localize('dacFxExtract.versionTextboxTitle', 'Version (use x.x.x.x where x is a number)'),
};
}
}

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
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 { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class ImportConfigPage extends DacFxConfigPage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private form: azdata.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let databaseComponent = await this.createDatabaseTextBox();
let serverComponent = await this.createServerDropdown(true);
let fileBrowserComponent = await this.createFileBrowser();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
fileBrowserComponent,
serverComponent,
databaseComponent,
], {
horizontal: true,
componentWidth: 400
}).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
return r1;
}
private async createFileBrowser(): Promise<azdata.FormComponent> {
this.createFileBrowserParts();
this.fileButton.onDidClick(async (click) => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(os.homedir()),
openLabel: localize('dacFxImport.openFile', 'Open'),
filters: {
'bacpac Files': ['bacpac'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
this.model.database = this.generateDatabaseName(this.model.filePath);
this.databaseTextBox.value = this.model.database;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
this.model.database = this.generateDatabaseName(this.model.filePath);
this.databaseTextBox.value = this.model.database;
});
return {
component: this.fileTextBox,
title: localize('dacFxImport.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private generateDatabaseName(filePath: string): string {
let result = path.parse(filePath);
return result.name;
}
}

View File

@@ -0,0 +1,185 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, Operation, DeployOperationPath, ExtractOperationPath, ImportOperationPath, ExportOperationPath } from '../dataTierApplicationWizard';
import { BasePage } from '../api/basePage';
const localize = nls.loadMessageBundle();
export class SelectOperationPage extends BasePage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
private deployRadioButton: azdata.RadioButtonComponent;
private extractRadioButton: azdata.RadioButtonComponent;
private importRadioButton: azdata.RadioButtonComponent;
private exportRadioButton: azdata.RadioButtonComponent;
private form: azdata.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
}
async start(): Promise<boolean> {
let deployComponent = await this.createDeployRadioButton();
let extractComponent = await this.createExtractRadioButton();
let importComponent = await this.createImportRadioButton();
let exportComponent = await this.createExportRadioButton();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
deployComponent,
extractComponent,
importComponent,
exportComponent
], {
horizontal: true
}).component();
await this.view.initializeModel(this.form);
// default have the first radio button checked
this.deployRadioButton.checked = true;
this.instance.setDoneButton(Operation.deploy);
return true;
}
async onPageEnter(): Promise<boolean> {
return true;
}
private async createDeployRadioButton(): Promise<azdata.FormComponent> {
this.deployRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.deployRadioButtonLabel', 'Deploy a data-tier application .dacpac file to an instance of SQL Server [Deploy Dacpac]'),
}).component();
this.deployRadioButton.onDidClick(() => {
this.removePages();
//add deploy pages
let configPage = this.instance.pages.get('deployConfig');
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, DeployOperationPath.deployAction);
this.addSummaryPage(DeployOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.deploy);
});
return {
component: this.deployRadioButton,
title: ''
};
}
private async createExtractRadioButton(): Promise<azdata.FormComponent> {
this.extractRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.extractRadioButtonLabel', 'Extract a data-tier application from an instance of SQL Server to a .dacpac file [Extract Dacpac]'),
}).component();
this.extractRadioButton.onDidClick(() => {
this.removePages();
// add the extract page
let page = this.instance.pages.get('extractConfig');
this.instance.wizard.addPage(page.wizardPage, ExtractOperationPath.options);
this.addSummaryPage(ExtractOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.extract);
});
return {
component: this.extractRadioButton,
title: ''
};
}
private async createImportRadioButton(): Promise<azdata.FormComponent> {
this.importRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.importRadioButtonLabel', 'Create a database from a .bacpac file [Import Bacpac]'),
}).component();
this.importRadioButton.onDidClick(() => {
this.removePages();
// add the import page
let page = this.instance.pages.get('importConfig');
this.instance.wizard.addPage(page.wizardPage, ImportOperationPath.options);
this.addSummaryPage(ImportOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.import);
});
return {
component: this.importRadioButton,
title: ''
};
}
private async createExportRadioButton(): Promise<azdata.FormComponent> {
this.exportRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.exportRadioButtonLabel', 'Export the schema and data from a database to the logical .bacpac file format [Export Bacpac]'),
}).component();
this.exportRadioButton.onDidClick(() => {
this.removePages();
// add the export pages
let page = this.instance.pages.get('exportConfig');
this.instance.wizard.addPage(page.wizardPage, ExportOperationPath.options);
this.addSummaryPage(ExportOperationPath.summary);
// change button text and operation
this.instance.setDoneButton(Operation.export);
});
return {
component: this.exportRadioButton,
title: ''
};
}
private removePages() {
let numPages = this.instance.wizard.pages.length;
for (let i = numPages - 1; i > 0; --i) {
this.instance.wizard.removePage(i);
}
}
private addSummaryPage(index: number) {
let summaryPage = this.instance.pages.get('summary');
this.instance.wizard.addPage(summaryPage.wizardPage, index);
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
return true;
});
}
}