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,139 @@
/*---------------------------------------------------------------------------------------------
* 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 { DacFxDataModel } from './models';
export abstract class BasePage {
protected readonly wizardPage: azdata.window.modelviewdialog.WizardPage;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
/**
* This method constructs all the elements of the page.
* @returns {Promise<boolean>}
*/
public async abstract start(): Promise<boolean>;
/**
* This method is called when the user is entering the page.
* @returns {Promise<boolean>}
*/
public async abstract onPageEnter(): Promise<boolean>;
/**
* This method is called when the user is leaving the page.
* @returns {Promise<boolean>}
*/
async onPageLeave(): Promise<boolean> {
return true;
}
/**
* Override this method to cleanup what you don't need cached in the page.
* @returns {Promise<boolean>}
*/
public async cleanup(): Promise<boolean> {
return true;
}
/**
* Sets up a navigation validator.
* This will be called right before onPageEnter().
*/
public abstract setupNavigationValidator();
protected async getServerValues(): Promise<{ connection, displayName, name }[]> {
let cons = await azdata.connection.getActiveConnections();
// This user has no active connections ABORT MISSION
if (!cons || cons.length === 0) {
return undefined;
}
let count = -1;
let idx = -1;
let values = cons.map(c => {
// Handle the code to remember what the user's choice was from before
count++;
if (idx === -1) {
if (this.model.server && c.connectionId === this.model.server.connectionId) {
idx = count;
} else if (this.model.serverId && c.connectionId === this.model.serverId) {
idx = count;
}
}
let db = c.options.databaseDisplayName;
let usr = c.options.user;
let srv = c.options.server;
if (!db) {
db = '<default>';
}
if (!usr) {
usr = 'default';
}
let finalName = `${srv}, ${db} (${usr})`;
return {
connection: c,
displayName: finalName,
name: c.connectionId
};
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
this.deleteServerValues();
}
return values;
}
protected async getDatabaseValues(): Promise<{ displayName, name }[]> {
let idx = -1;
let count = -1;
let values = (await azdata.connection.listDatabases(this.model.server.connectionId)).map(db => {
count++;
if (this.model.database && db === this.model.database) {
idx = count;
}
return {
displayName: db,
name: db
};
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
this.deleteDatabaseValues();
}
return values;
}
protected deleteServerValues() {
delete this.model.server;
delete this.model.serverId;
delete this.model.database;
}
protected deleteDatabaseValues() {
return;
}
}

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* 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 os from 'os';
import * as path from 'path';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxDataModel } from './models';
import { BasePage } from './basePage';
const localize = nls.loadMessageBundle();
export abstract class DacFxConfigPage extends BasePage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: azdata.ModelView;
protected serverDropdown: azdata.DropDownComponent;
protected databaseTextBox: azdata.InputBoxComponent;
protected databaseDropdown: azdata.DropDownComponent;
protected databaseLoader: azdata.LoadingComponent;
protected fileTextBox: azdata.InputBoxComponent;
protected fileButton: azdata.ButtonComponent;
protected fileExtension: string;
protected 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;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
return true;
});
}
protected async createServerDropdown(isTargetServer: boolean): Promise<azdata.FormComponent> {
this.serverDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
// Handle server changes
this.serverDropdown.onValueChanged(async () => {
this.model.server = (this.serverDropdown.value as ConnectionDropdownValue).connection;
this.model.serverName = (this.serverDropdown.value as ConnectionDropdownValue).displayName;
await this.populateDatabaseDropdown();
});
let targetServerTitle = localize('dacFx.targetServerDropdownTitle', 'Target Server');
let sourceServerTitle = localize('dacFx.sourceServerDropdownTitle', 'Source Server');
return {
component: this.serverDropdown,
title: isTargetServer ? targetServerTitle : sourceServerTitle
};
}
protected async populateServerDropdown(): Promise<boolean> {
let values = await this.getServerValues();
if (values === undefined) {
return false;
}
this.model.server = values[0].connection;
this.model.serverName = values[0].displayName;
this.serverDropdown.updateProperties({
values: values
});
return true;
}
protected async createDatabaseTextBox(): Promise<azdata.FormComponent> {
this.databaseTextBox = this.view.modelBuilder.inputBox().withProperties({
required: true
}).component();
this.databaseTextBox.onTextChanged(async () => {
this.model.database = this.databaseTextBox.value;
});
return {
component: this.databaseTextBox,
title: localize('dacFx.databaseNameTextBox', 'Target Database')
};
}
protected async createDatabaseDropdown(): 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.fileTextBox.value = this.generateFilePath();
this.model.filePath = this.fileTextBox.value;
});
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
return {
component: this.databaseLoader,
title: localize('dacFx.sourceDatabaseDropdownTitle', 'Source Database')
};
}
protected async populateDatabaseDropdown(): 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();
this.model.database = values[0].name;
this.model.filePath = this.generateFilePath();
this.fileTextBox.value = this.model.filePath;
this.databaseDropdown.updateProperties({
values: values
});
this.databaseLoader.loading = false;
return true;
}
protected async createFileBrowserParts() {
this.fileTextBox = this.view.modelBuilder.inputBox().withProperties({
required: true
}).component();
this.fileButton = this.view.modelBuilder.button().withProperties({
label: '•••',
}).component();
}
protected generateFilePath(): string {
let now = new Date();
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
return path.join(os.homedir(), this.model.database + '-' + datetime + this.fileExtension);
}
}
interface ConnectionDropdownValue extends azdata.CategoryValue {
connection: azdata.connection.Connection;
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* 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';
/**
* Data model to communicate between DacFx pages
*/
export interface DacFxDataModel {
server: azdata.connection.Connection;
database: string;
serverName: string;
serverId: string;
filePath: string;
version: string;
upgradeExisting: boolean;
scriptFilePath: string;
generateScriptAndDeploy: boolean;
}

View File

@@ -0,0 +1,380 @@
/*---------------------------------------------------------------------------------------------
* 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 vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
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';
import { ExtractConfigPage } from './pages/extractConfigPage';
import { ImportConfigPage } from './pages/importConfigPage';
import { DacFxDataModel } from './api/models';
import { BasePage } from './api/basePage';
const localize = nls.loadMessageBundle();
class Page {
wizardPage: azdata.window.WizardPage;
dacFxPage: BasePage;
constructor(wizardPage: azdata.window.WizardPage) {
this.wizardPage = wizardPage;
}
}
export enum Operation {
deploy,
extract,
import,
export,
generateDeployScript
}
export enum DeployOperationPath {
selectOperation,
deployOptions,
deployPlan,
deployAction,
summary
}
export enum DeployNewOperationPath {
selectOperation,
deployOptions,
summary
}
export enum ExtractOperationPath {
selectOperation,
options,
summary
}
export enum ImportOperationPath {
selectOperation,
options,
summary
}
export enum ExportOperationPath {
selectOperation,
options,
summary
}
export class DataTierApplicationWizard {
public wizard: azdata.window.Wizard;
private connection: azdata.connection.ConnectionProfile;
private model: DacFxDataModel;
public pages: Map<string, Page> = new Map<string, Page>();
public selectedOperation: Operation;
constructor() {
}
public async start(p: any, ...args: any[]) {
this.model = <DacFxDataModel>{};
let profile = p ? <azdata.IConnectionProfile>p.connectionProfile : undefined;
if (profile) {
this.model.serverId = profile.id;
this.model.database = profile.databaseName;
}
this.connection = await azdata.connection.getCurrentConnection();
if (!this.connection) {
// @TODO: remove cast once azdata update complete - karlb 3/1/2019
this.connection = <azdata.connection.ConnectionProfile><any>await azdata.connection.openConnectionDialog();
}
this.wizard = azdata.window.createWizard('Data-tier Application Wizard');
let selectOperationWizardPage = azdata.window.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
let deployConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
let deployPlanWizardPage = azdata.window.createWizardPage(localize('dacFx.deployPlanPage', 'Review the deploy plan'));
let deployActionWizardPage = azdata.window.createWizardPage(localize('dacFx.deployActionPageName', 'Select Action'));
let summaryWizardPage = azdata.window.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
let extractConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
let importConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.importConfigPageName', 'Select Import Bacpac Settings'));
let exportConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.exportConfigPageName', 'Select Export Bacpac Settings'));
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));
this.pages.set('exportConfig', new Page(exportConfigWizardPage));
this.pages.set('summary', new Page(summaryWizardPage));
selectOperationWizardPage.registerContent(async (view) => {
let selectOperationDacFxPage = new SelectOperationPage(this, selectOperationWizardPage, this.model, view);
this.pages.get('selectOperation').dacFxPage = selectOperationDacFxPage;
await selectOperationDacFxPage.start().then(() => {
selectOperationDacFxPage.setupNavigationValidator();
selectOperationDacFxPage.onPageEnter();
});
});
deployConfigWizardPage.registerContent(async (view) => {
let deployConfigDacFxPage = new DeployConfigPage(this, deployConfigWizardPage, this.model, view);
this.pages.get('deployConfig').dacFxPage = deployConfigDacFxPage;
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;
await deployActionDacFxPage.start();
});
extractConfigWizardPage.registerContent(async (view) => {
let extractConfigDacFxPage = new ExtractConfigPage(this, extractConfigWizardPage, this.model, view);
this.pages.get('extractConfig').dacFxPage = extractConfigDacFxPage;
await extractConfigDacFxPage.start();
});
importConfigWizardPage.registerContent(async (view) => {
let importConfigDacFxPage = new ImportConfigPage(this, importConfigWizardPage, this.model, view);
this.pages.get('importConfig').dacFxPage = importConfigDacFxPage;
await importConfigDacFxPage.start();
});
exportConfigWizardPage.registerContent(async (view) => {
let exportConfigDacFxPage = new ExportConfigPage(this, exportConfigWizardPage, this.model, view);
this.pages.get('exportConfig').dacFxPage = exportConfigDacFxPage;
await exportConfigDacFxPage.start();
});
summaryWizardPage.registerContent(async (view) => {
let summaryDacFxPage = new DacFxSummaryPage(this, summaryWizardPage, this.model, view);
this.pages.get('summary').dacFxPage = summaryDacFxPage;
await summaryDacFxPage.start();
});
this.wizard.onPageChanged(async (event) => {
let idx = event.newPage;
let page = this.getPage(idx);
if (page !== undefined) {
page.dacFxPage.setupNavigationValidator();
page.dacFxPage.onPageEnter();
}
//do onPageLeave for summary page so that GenerateScript button only shows up if upgrading database
let idxLast = event.lastPage;
if (this.isSummaryPage(idxLast)) {
let lastPage = this.pages.get('summary');
if (lastPage) {
lastPage.dacFxPage.onPageLeave();
}
}
});
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());
this.wizard.open();
}
public registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean) {
this.wizard.registerNavigationValidator(validator);
}
public setDoneButton(operation: Operation): void {
switch (operation) {
case Operation.deploy: {
this.wizard.doneButton.label = localize('dacFx.deployButton', 'Deploy');
this.selectedOperation = Operation.deploy;
break;
}
case Operation.extract: {
this.wizard.doneButton.label = localize('dacFx.extractButton', 'Extract');
this.selectedOperation = Operation.extract;
break;
}
case Operation.import: {
this.wizard.doneButton.label = localize('dacFx.importButton', 'Import');
this.selectedOperation = Operation.import;
break;
}
case Operation.export: {
this.wizard.doneButton.label = localize('dacFx.exportButton', 'Export');
this.selectedOperation = Operation.export;
break;
}
case Operation.generateDeployScript: {
this.wizard.doneButton.label = localize('dacFx.generateScriptButton', 'Generate Script');
this.selectedOperation = Operation.generateDeployScript;
break;
}
}
if (operation !== Operation.deploy && operation !== Operation.generateDeployScript) {
this.model.upgradeExisting = false;
}
}
private async executeOperation() {
switch (this.selectedOperation) {
case Operation.deploy: {
await this.deploy();
break;
}
case Operation.extract: {
await this.extract();
break;
}
case Operation.import: {
await this.import();
break;
}
case Operation.export: {
await this.export();
break;
}
case Operation.generateDeployScript: {
await this.generateDeployScript();
break;
}
}
}
private async deploy() {
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async extract() {
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.extractErrorMessage', "Extract failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async export() {
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.exportErrorMessage', "Export failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async import() {
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async generateDeployScript() {
if (!this.model.scriptFilePath) {
return;
}
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
this.wizard.message = {
text: localize('dacfx.scriptGeneratingMessage', 'You can view the status of script generation in the Task History once the wizard is closed'),
level: azdata.window.MessageLevel.Information,
description: ''
};
let result = await service.generateDeployScript(this.model.filePath, this.model.database, this.model.scriptFilePath, ownerUri, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private getPage(idx: number): Page {
let page: Page;
if (idx === 1) {
switch (this.selectedOperation) {
case Operation.deploy: {
page = this.pages.get('deployConfig');
break;
}
case Operation.extract: {
page = this.pages.get('extractConfig');
break;
}
case Operation.import: {
page = this.pages.get('importConfig');
break;
}
case Operation.export: {
page = this.pages.get('exportConfig');
break;
}
}
} 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;
}
private isSummaryPage(idx: number): boolean {
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 azdata.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, azdata.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<azdata.DacFxServicesProvider> {
let service = azdata.dataprotocol.getProvider<azdata.DacFxServicesProvider>(providerName, azdata.DataProviderType.DacFxServicesProvider);
return service;
}
}

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