From 4c075df327127bd5ea6d5545b222a931897662d4 Mon Sep 17 00:00:00 2001 From: kisantia <31145923+kisantia@users.noreply.github.com> Date: Tue, 27 Nov 2018 16:10:17 -0800 Subject: [PATCH] DacFx import/export wizard (#3162) Basic wizard to import/export bacpacs and deploy/extract dacpacs --- extensions/import/package.json | 14 + .../import/src/controllers/mainController.ts | 6 + extensions/import/src/wizard/api/basePage.ts | 139 ++++++++++ .../import/src/wizard/api/dacFxConfigPage.ts | 158 +++++++++++ .../import/src/wizard/api/importPage.ts | 36 +-- extensions/import/src/wizard/api/models.ts | 21 +- .../src/wizard/dataTierApplicationWizard.ts | 253 ++++++++++++++++++ .../src/wizard/pages/dacFxSummaryPage.ts | 112 ++++++++ .../src/wizard/pages/deployConfigPage.ts | 190 +++++++++++++ .../src/wizard/pages/exportConfigPage.ts | 100 +++++++ .../src/wizard/pages/extractConfigPage.ts | 122 +++++++++ .../import/src/wizard/pages/fileConfigPage.ts | 90 ++----- .../src/wizard/pages/importConfigPage.ts | 101 +++++++ .../src/wizard/pages/selectOperationpage.ts | 174 ++++++++++++ extensions/mssql/src/contracts.ts | 57 ++++ extensions/mssql/src/features.ts | 100 ++++++- extensions/mssql/src/main.ts | 5 +- src/sql/services/dacfx/dacFxService.ts | 78 ++++++ src/sql/sqlops.d.ts | 45 ++++ src/sql/sqlops.proposed.d.ts | 3 +- .../workbench/api/common/sqlExtHostTypes.ts | 3 +- .../workbench/api/node/extHostDataProtocol.ts | 6 + .../api/node/mainThreadDataProtocol.ts | 24 +- .../workbench/api/node/sqlExtHost.api.impl.ts | 5 + .../workbench/api/node/sqlExtHost.protocol.ts | 21 ++ .../workbench/electron-browser/workbench.ts | 3 + 26 files changed, 1745 insertions(+), 121 deletions(-) create mode 100644 extensions/import/src/wizard/api/basePage.ts create mode 100644 extensions/import/src/wizard/api/dacFxConfigPage.ts create mode 100644 extensions/import/src/wizard/dataTierApplicationWizard.ts create mode 100644 extensions/import/src/wizard/pages/dacFxSummaryPage.ts create mode 100644 extensions/import/src/wizard/pages/deployConfigPage.ts create mode 100644 extensions/import/src/wizard/pages/exportConfigPage.ts create mode 100644 extensions/import/src/wizard/pages/extractConfigPage.ts create mode 100644 extensions/import/src/wizard/pages/importConfigPage.ts create mode 100644 extensions/import/src/wizard/pages/selectOperationpage.ts create mode 100644 src/sql/services/dacfx/dacFxService.ts diff --git a/extensions/import/package.json b/extensions/import/package.json index d348a32841..625f9e44e6 100644 --- a/extensions/import/package.json +++ b/extensions/import/package.json @@ -33,6 +33,15 @@ "light": "./images/light_icon.svg", "dark": "./images/dark_icon.svg" } + }, + { + "command": "dacFx.start", + "title": "Data-tier Application Wizard", + "category": "Data-tier Application", + "icon": { + "light": "./images/light_icon.svg", + "dark": "./images/dark_icon.svg" + } } ], "keybindings": [ @@ -48,6 +57,11 @@ "command": "flatFileImport.start", "when": "connectionProvider == MSSQL && nodeType && nodeType == Database", "group": "import" + }, + { + "command": "dacFx.start", + "when": "connectionProvider == MSSQL && nodeType && nodeType == Database", + "group": "export" } ] } diff --git a/extensions/import/src/controllers/mainController.ts b/extensions/import/src/controllers/mainController.ts index 0038e0538e..313a2b2263 100644 --- a/extensions/import/src/controllers/mainController.ts +++ b/extensions/import/src/controllers/mainController.ts @@ -13,6 +13,7 @@ import { FlatFileWizard } from '../wizard/flatFileWizard'; import { ServiceClient } from '../services/serviceClient'; import { ApiType, managerInstance } from '../services/serviceApiManager'; import { FlatFileProvider } from '../services/contracts'; +import { DataTierApplicationWizard } from '../wizard/dataTierApplicationWizard'; /** * The main controller class that initializes the extension @@ -35,10 +36,15 @@ export default class MainController extends ControllerBase { this.initializeFlatFileProvider(provider); }); + this.initializeDacFxWizard(); return Promise.resolve(true); } private initializeFlatFileProvider(provider: FlatFileProvider) { sqlops.tasks.registerTask('flatFileImport.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new FlatFileWizard(provider).start(profile, args)); } + + private initializeDacFxWizard() { + sqlops.tasks.registerTask('dacFx.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new DataTierApplicationWizard().start(profile, args)); + } } diff --git a/extensions/import/src/wizard/api/basePage.ts b/extensions/import/src/wizard/api/basePage.ts new file mode 100644 index 0000000000..80689ab152 --- /dev/null +++ b/extensions/import/src/wizard/api/basePage.ts @@ -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 sqlops from 'sqlops'; +import { BaseDataModel } from './models'; + +export abstract class BasePage { + + protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage; + protected readonly model: BaseDataModel; + protected readonly view: sqlops.ModelView; + + /** + * This method constructs all the elements of the page. + * @returns {Promise} + */ + public async abstract start(): Promise; + + /** + * This method is called when the user is entering the page. + * @returns {Promise} + */ + public async abstract onPageEnter(): Promise; + + /** + * This method is called when the user is leaving the page. + * @returns {Promise} + */ + async onPageLeave(): Promise { + return true; + } + + /** + * Override this method to cleanup what you don't need cached in the page. + * @returns {Promise} + */ + public async cleanup(): Promise { + 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 sqlops.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 = ''; + } + + 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 sqlops.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; + } +} + diff --git a/extensions/import/src/wizard/api/dacFxConfigPage.ts b/extensions/import/src/wizard/api/dacFxConfigPage.ts new file mode 100644 index 0000000000..94ee387127 --- /dev/null +++ b/extensions/import/src/wizard/api/dacFxConfigPage.ts @@ -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 sqlops from 'sqlops'; +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: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + protected serverDropdown: sqlops.DropDownComponent; + protected databaseTextBox: sqlops.InputBoxComponent; + protected databaseDropdown: sqlops.DropDownComponent; + protected databaseLoader: sqlops.LoadingComponent; + protected fileTextBox: sqlops.InputBoxComponent; + protected fileButton: sqlops.ButtonComponent; + protected fileExtension: string; + + protected constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.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 { + 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 { + 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 { + 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 { + this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({ + required: true + }).component(); + + // Handle database changes + this.databaseDropdown.onValueChanged(async () => { + this.model.database = (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 { + 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 sqlops.CategoryValue { + connection: sqlops.connection.Connection; +} + diff --git a/extensions/import/src/wizard/api/importPage.ts b/extensions/import/src/wizard/api/importPage.ts index b44312c558..1ff65ed0ca 100644 --- a/extensions/import/src/wizard/api/importPage.ts +++ b/extensions/import/src/wizard/api/importPage.ts @@ -8,8 +8,9 @@ import { ImportDataModel } from './models'; import * as sqlops from 'sqlops'; import { FlatFileProvider } from '../../services/contracts'; import { FlatFileWizard } from '../flatFileWizard'; +import { BasePage } from './basePage'; -export abstract class ImportPage { +export abstract class ImportPage extends BasePage { protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage; protected readonly instance: FlatFileWizard; @@ -18,42 +19,11 @@ export abstract class ImportPage { protected readonly provider: FlatFileProvider; protected constructor(instance: FlatFileWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) { + super(); this.instance = instance; this.wizardPage = wizardPage; this.model = model; this.view = view; this.provider = provider; } - - /** - * This method constructs all the elements of the page. - * @returns {Promise} - */ - public async abstract start(): Promise; - - /** - * This method is called when the user is entering the page. - * @returns {Promise} - */ - public async abstract onPageEnter(): Promise; - - /** - * This method is called when the user is leaving the page. - * @returns {Promise} - */ - public async abstract onPageLeave(): Promise; - - /** - * Sets up a navigation validator. - * This will be called right before onPageEnter(). - */ - public abstract setupNavigationValidator(); - - /** - * Override this method to cleanup what you don't need cached in the page. - * @returns {Promise} - */ - public async cleanup(): Promise { - return true; - } } diff --git a/extensions/import/src/wizard/api/models.ts b/extensions/import/src/wizard/api/models.ts index 81a8bc7f0c..12b305dcb7 100644 --- a/extensions/import/src/wizard/api/models.ts +++ b/extensions/import/src/wizard/api/models.ts @@ -6,15 +6,19 @@ import * as sqlops from 'sqlops'; +export interface BaseDataModel { + server: sqlops.connection.Connection; + serverId: string; + database: string; +} + /** * The main data model that communicates between the pages. */ -export interface ImportDataModel { +export interface ImportDataModel extends BaseDataModel { ownerUri: string; proseColumns: ColumnMetadata[]; proseDataPreview: string[][]; - server: sqlops.connection.Connection; - serverId: string; database: string; table: string; schema: string; @@ -31,3 +35,14 @@ export interface ColumnMetadata { primaryKey: boolean; nullable: boolean; } + +/** + * Data model to communicate between DacFx pages + */ +export interface DacFxDataModel extends BaseDataModel { + serverName: string; + serverId: string; + filePath: string; + version: string; + upgradeExisting: boolean; +} diff --git a/extensions/import/src/wizard/dataTierApplicationWizard.ts b/extensions/import/src/wizard/dataTierApplicationWizard.ts new file mode 100644 index 0000000000..1af7254046 --- /dev/null +++ b/extensions/import/src/wizard/dataTierApplicationWizard.ts @@ -0,0 +1,253 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import { SelectOperationPage } from './pages/selectOperationpage'; +import { DeployConfigPage } from './pages/deployConfigPage'; +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: sqlops.window.modelviewdialog.WizardPage; + dacFxPage: BasePage; + + constructor(wizardPage: sqlops.window.modelviewdialog.WizardPage) { + this.wizardPage = wizardPage; + } +} + +export enum Operation { + deploy, + extract, + import, + export +} + +export class DataTierApplicationWizard { + public wizard: sqlops.window.modelviewdialog.Wizard; + private connection: sqlops.connection.Connection; + private model: DacFxDataModel; + public pages: Map = new Map(); + public selectedOperation: Operation; + + constructor() { + } + + public async start(p: any, ...args: any[]) { + this.model = {}; + + let profile = p ? p.connectionProfile : undefined; + if (profile) { + this.model.serverId = profile.id; + this.model.database = profile.databaseName; + } + + this.connection = await sqlops.connection.getCurrentConnection(); + if (!this.connection) { + this.connection = await sqlops.connection.openConnectionDialog(); + } + + this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard'); + let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation')); + let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings')); + let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary')); + let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings')); + let importConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.importConfigPageName', 'Select Import Bacpac Settings')); + let exportConfigWizardPage = sqlops.window.modelviewdialog.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('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(); + }); + + 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: 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 (idx === 2) { + page = this.pages.get('summary'); + } + + if (page !== undefined) { + page.dacFxPage.setupNavigationValidator(); + page.dacFxPage.onPageEnter(); + } + }); + + this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, summaryWizardPage]; + this.wizard.generateScriptButton.hidden = true; + this.wizard.doneButton.onClick(async () => await this.executeOperation()); + + this.wizard.open(); + } + + public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.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; + } + } + } + + 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; + } + } + } + + private async deploy() { + let service = await DataTierApplicationWizard.getService(); + let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + + let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, sqlops.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(); + let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + + let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, sqlops.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(); + let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + + let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, sqlops.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(); + let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + + let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute); + if (!result || !result.success) { + vscode.window.showErrorMessage( + localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown')); + } + } + + public static async getService(): Promise { + let currentConnection = await sqlops.connection.getCurrentConnection(); + let service = sqlops.dataprotocol.getProvider(currentConnection.providerName, sqlops.DataProviderType.DacFxServicesProvider); + return service; + } +} diff --git a/extensions/import/src/wizard/pages/dacFxSummaryPage.ts b/extensions/import/src/wizard/pages/dacFxSummaryPage.ts new file mode 100644 index 0000000000..0318f94c21 --- /dev/null +++ b/extensions/import/src/wizard/pages/dacFxSummaryPage.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import * as sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +import { 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: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + + private form: sqlops.FormContainer; + private table: sqlops.TableComponent; + private loader: sqlops.LoadingComponent; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(); + this.instance = instance; + this.wizardPage = wizardPage; + this.model = model; + this.view = view; + } + + async start(): Promise { + 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 { + this.populateTable(); + this.loader.loading = false; + 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'); + + switch (this.instance.selectedOperation) { + case Operation.deploy: { + data = [ + [targetServer, this.model.serverName], + [fileLocation, this.model.filePath], + [targetDatabase, this.model.database]]; + 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; + } + } + + this.table.updateProperties({ + data: data, + columns: ['Setting', 'Value'], + width: 600, + height: 200 + }); + } +} + diff --git a/extensions/import/src/wizard/pages/deployConfigPage.ts b/extensions/import/src/wizard/pages/deployConfigPage.ts new file mode 100644 index 0000000000..67d4fd61a3 --- /dev/null +++ b/extensions/import/src/wizard/pages/deployConfigPage.ts @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import * as sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +import * as 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 DeployConfigPage extends DacFxConfigPage { + + protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + private databaseDropdownComponent: sqlops.FormComponent; + private databaseComponent: sqlops.FormComponent; + private formBuilder: sqlops.FormBuilder; + private form: sqlops.FormContainer; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(instance, wizardPage, model, view); + this.fileExtension = '.bacpac'; + } + + async start(): Promise { + 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 { + let r1 = await this.populateServerDropdown(); + let r2 = await this.populateDeployDatabaseDropdown(); + return r1 && r2; + } + + private async createFileBrowser(): Promise { + 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 { + 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 = (this.databaseDropdown.value).name; + }); + + 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; + }); + + // 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 { + this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({ + required: true + }).component(); + // Handle database changes + this.databaseDropdown.onValueChanged(async () => { + this.model.database = (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 { + this.databaseLoader.loading = true; + this.databaseDropdown.updateProperties({ values: [] }); + if (!this.model.server) { + this.databaseLoader.loading = false; + return false; + } + let values = await this.getDatabaseValues(); + + if (this.model.database === undefined) { + 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; + } +} diff --git a/extensions/import/src/wizard/pages/exportConfigPage.ts b/extensions/import/src/wizard/pages/exportConfigPage.ts new file mode 100644 index 0000000000..4660961cc5 --- /dev/null +++ b/extensions/import/src/wizard/pages/exportConfigPage.ts @@ -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 sqlops from 'sqlops'; +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: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + + private form: sqlops.FormContainer; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(instance, wizardPage, model, view); + this.fileExtension = '.bacpac'; + } + + async start(): Promise { + 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 { + 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 { + 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] + }; + } +} diff --git a/extensions/import/src/wizard/pages/extractConfigPage.ts b/extensions/import/src/wizard/pages/extractConfigPage.ts new file mode 100644 index 0000000000..fcf9c35478 --- /dev/null +++ b/extensions/import/src/wizard/pages/extractConfigPage.ts @@ -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 sqlops from 'sqlops'; +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: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + + private form: sqlops.FormContainer; + private versionTextBox: sqlops.InputBoxComponent; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(instance, wizardPage, model, view); + this.fileExtension = '.dacpac'; + } + + async start(): Promise { + 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 { + 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 { + 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 { + 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)'), + }; + } +} diff --git a/extensions/import/src/wizard/pages/fileConfigPage.ts b/extensions/import/src/wizard/pages/fileConfigPage.ts index b2666a751f..11b52b2018 100644 --- a/extensions/import/src/wizard/pages/fileConfigPage.ts +++ b/extensions/import/src/wizard/pages/fileConfigPage.ts @@ -102,57 +102,9 @@ export class FileConfigPage extends ImportPage { } private async populateServerDropdown(): Promise { - let cons = await sqlops.connection.getActiveConnections(); - // This user has no active connections ABORT MISSION - if (!cons || cons.length === 0) { - return true; - } - - - 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 = ''; - } - - 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 { - delete this.model.server; - delete this.model.serverId; - delete this.model.database; - delete this.model.schema; + let values = await this.getServerValues(); + if (values === undefined) { + return false; } this.model.server = values[0].connection; @@ -195,29 +147,7 @@ export class FileConfigPage extends ImportPage { return false; } - - let idx = -1; - let count = -1; - let values = (await sqlops.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 { - delete this.model.database; - delete this.model.schema; - } + let values = await this.getDatabaseValues(); this.model.database = values[0].name; @@ -377,6 +307,18 @@ export class FileConfigPage extends ImportPage { return true; } + protected deleteServerValues() { + delete this.model.server; + delete this.model.serverId; + delete this.model.database; + delete this.model.schema; + } + + protected deleteDatabaseValues() { + delete this.model.database; + delete this.model.schema; + } + // private async populateTableNames(): Promise { // this.tableNames = []; // let databaseName = (this.databaseDropdown.value).name; diff --git a/extensions/import/src/wizard/pages/importConfigPage.ts b/extensions/import/src/wizard/pages/importConfigPage.ts new file mode 100644 index 0000000000..8620f68c36 --- /dev/null +++ b/extensions/import/src/wizard/pages/importConfigPage.ts @@ -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 sqlops from 'sqlops'; +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: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + + private form: sqlops.FormContainer; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(instance, wizardPage, model, view); + this.fileExtension = '.bacpac'; + } + + async start(): Promise { + 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 { + let r1 = await this.populateServerDropdown(); + return r1; + } + + private async createFileBrowser(): Promise { + 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; + } +} diff --git a/extensions/import/src/wizard/pages/selectOperationpage.ts b/extensions/import/src/wizard/pages/selectOperationpage.ts new file mode 100644 index 0000000000..2d4a8331b3 --- /dev/null +++ b/extensions/import/src/wizard/pages/selectOperationpage.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import * as sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +import { DacFxDataModel } from '../api/models'; +import { DataTierApplicationWizard, Operation } from '../DataTierApplicationWizard'; +import { BasePage } from '../api/basePage'; + +const localize = nls.loadMessageBundle(); + +export class SelectOperationPage extends BasePage { + + protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + + private deployRadioButton: sqlops.RadioButtonComponent; + private extractRadioButton: sqlops.RadioButtonComponent; + private importRadioButton: sqlops.RadioButtonComponent; + private exportRadioButton: sqlops.RadioButtonComponent; + private form: sqlops.FormContainer; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(); + this.instance = instance; + this.wizardPage = wizardPage; + this.model = model; + this.view = view; + } + + async start(): Promise { + 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 { + let numPages = this.instance.wizard.pages.length; + for (let i = numPages - 1; i > 2; --i) { + await this.instance.wizard.removePage(i); + } + + return true; + } + + private async createDeployRadioButton(): Promise { + 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(() => { + // remove the previous page + this.instance.wizard.removePage(1); + + // add deploy page + let page = this.instance.pages.get('deployConfig'); + this.instance.wizard.addPage(page.wizardPage, 1); + + // change button text and operation + this.instance.setDoneButton(Operation.deploy); + }); + + return { + component: this.deployRadioButton, + title: '' + }; + } + + private async createExtractRadioButton(): Promise { + 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(() => { + // remove the previous pages + this.instance.wizard.removePage(1); + + // add the extract page + let page = this.instance.pages.get('extractConfig'); + this.instance.wizard.addPage(page.wizardPage, 1); + + // change button text and operation + this.instance.setDoneButton(Operation.extract); + }); + + return { + component: this.extractRadioButton, + title: '' + }; + } + + private async createImportRadioButton(): Promise { + 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(() => { + // remove the previous page + this.instance.wizard.removePage(1); + + // add the import page + let page = this.instance.pages.get('importConfig'); + this.instance.wizard.addPage(page.wizardPage, 1); + + // change button text and operation + this.instance.setDoneButton(Operation.import); + }); + + return { + component: this.importRadioButton, + title: '' + }; + } + + private async createExportRadioButton(): Promise { + 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(() => { + // remove the 2 previous pages + this.instance.wizard.removePage(1); + + // add the export pages + let page = this.instance.pages.get('exportConfig'); + this.instance.wizard.addPage(page.wizardPage, 1); + + // change button text and operation + this.instance.setDoneButton(Operation.export); + }); + + return { + component: this.exportRadioButton, + title: '' + }; + } + + public setupNavigationValidator() { + this.instance.registerNavigationValidator(() => { + return true; + }); + } +} diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 4b2b7bba86..9d17ad48cc 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -291,3 +291,60 @@ export namespace DeleteAgentJobScheduleRequest { } // ------------------------------- < Agent Management > ------------------------------------ + +// ------------------------------- < DacFx > ------------------------------------ + +export enum TaskExecutionMode { + execute = 0, + script = 1, + executeAndScript = 2, +} +export interface ExportParams { + databaseName: string; + packageFilePath: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; +} + +export interface ImportParams { + packageFilePath: string; + databaseName: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; +} + + +export interface ExtractParams { + databaseName: string; + packageFilePath: string; + applicationName: string; + applicationVersion: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; +} + +export interface DeployParams { + packageFilePath: string; + databaseName: string; + upgradeExisting: boolean; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; +} + +export namespace ExportRequest { + export const type = new RequestType('dacfx/export'); +} + +export namespace ImportRequest { + export const type = new RequestType('dacfx/import'); +} + +export namespace ExtractRequest { + export const type = new RequestType('dacfx/extract'); +} + +export namespace DeployRequest { + export const type = new RequestType('dacfx/deploy'); +} + +// ------------------------------- < DacFx > ------------------------------------ \ No newline at end of file diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index a108d16cc0..cdbd5783a1 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -8,7 +8,7 @@ import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client'; import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient'; import { Disposable } from 'vscode'; import { Telemetry } from './telemetry'; -import * as contracts from './contracts'; +import * as contracts from './contracts'; import * as sqlops from 'sqlops'; import * as Utils from './utils'; import * as UUID from 'vscode-languageclient/lib/utils/uuid'; @@ -28,6 +28,94 @@ export class TelemetryFeature implements StaticFeature { } } +export class DacFxServicesFeature extends SqlOpsFeature { + private static readonly messageTypes: RPCMessageType[] = [ + contracts.ExportRequest.type, + contracts.ImportRequest.type, + contracts.ExtractRequest.type, + contracts.DeployRequest.type + ]; + + constructor(client: SqlOpsDataClient) { + super(client, DacFxServicesFeature.messageTypes); + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + } + + public initialize(capabilities: ServerCapabilities): void { + this.register(this.messages, { + id: UUID.generateUuid(), + registerOptions: undefined + }); + } + + protected registerProvider(options: undefined): Disposable { + const client = this._client; + let self = this; + + let exportBacpac = (databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable => { + let params: contracts.ExportParams = { databaseName: databaseName, packageFilePath: packageFilePath, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode }; + return client.sendRequest(contracts.ExportRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.ExportRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + let importBacpac = (packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable => { + let params: contracts.ImportParams = { packageFilePath: packageFilePath, databaseName: databaseName, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode }; + return client.sendRequest(contracts.ImportRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.ImportRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + let extractDacpac = (databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable => { + let params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: packageFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode }; + return client.sendRequest(contracts.ExtractRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.ExtractRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + let deployDacpac = (packageFilePath: string, targetDatabaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable => { + let params: contracts.DeployParams = { packageFilePath: packageFilePath, databaseName: targetDatabaseName, upgradeExisting: upgradeExisting, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode }; + return client.sendRequest(contracts.DeployRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.DeployRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + return sqlops.dataprotocol.registerDacFxServicesProvider({ + providerId: client.providerId, + exportBacpac, + importBacpac, + extractDacpac, + deployDacpac + }); + } +} + export class AgentServicesFeature extends SqlOpsFeature { private static readonly messagesTypes: RPCMessageType[] = [ contracts.AgentJobsRequest.type, @@ -229,7 +317,7 @@ export class AgentServicesFeature extends SqlOpsFeature { }; // Alert management methods - let getAlerts = (ownerUri: string): Thenable => { + let getAlerts = (ownerUri: string): Thenable => { let params: contracts.AgentAlertsParams = { ownerUri: ownerUri }; @@ -299,7 +387,7 @@ export class AgentServicesFeature extends SqlOpsFeature { }; // Operator management methods - let getOperators = (ownerUri: string): Thenable => { + let getOperators = (ownerUri: string): Thenable => { let params: contracts.AgentOperatorsParams = { ownerUri: ownerUri }; @@ -369,7 +457,7 @@ export class AgentServicesFeature extends SqlOpsFeature { }; // Proxy management methods - let getProxies = (ownerUri: string): Thenable => { + let getProxies = (ownerUri: string): Thenable => { let params: contracts.AgentProxiesParams = { ownerUri: ownerUri }; @@ -439,7 +527,7 @@ export class AgentServicesFeature extends SqlOpsFeature { }; // Agent Credential Method - let getCredentials = (ownerUri: string): Thenable => { + let getCredentials = (ownerUri: string): Thenable => { let params: contracts.GetCredentialsParams = { ownerUri: ownerUri }; @@ -455,7 +543,7 @@ export class AgentServicesFeature extends SqlOpsFeature { // Job Schedule management methods - let getJobSchedules = (ownerUri: string): Thenable => { + let getJobSchedules = (ownerUri: string): Thenable => { let params: contracts.AgentJobScheduleParams = { ownerUri: ownerUri }; diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 401a6d3b15..2588febfcc 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -16,7 +16,7 @@ import { CredentialStore } from './credentialstore/credentialstore'; import { AzureResourceProvider } from './resourceProvider/resourceProvider'; import * as Utils from './utils'; import { Telemetry, LanguageClientErrorHandler } from './telemetry'; -import { TelemetryFeature, AgentServicesFeature } from './features'; +import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features'; const baseConfig = require('./config.json'); const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); @@ -55,7 +55,8 @@ export async function activate(context: vscode.ExtensionContext) { // we only want to add new features ...SqlOpsDataClient.defaultFeatures, TelemetryFeature, - AgentServicesFeature + AgentServicesFeature, + DacFxServicesFeature, ], outputChannel: new CustomOutputChannel() }; diff --git a/src/sql/services/dacfx/dacFxService.ts b/src/sql/services/dacfx/dacFxService.ts new file mode 100644 index 0000000000..231071724d --- /dev/null +++ b/src/sql/services/dacfx/dacFxService.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as sqlops from 'sqlops'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { localize } from 'vs/nls'; +import { data } from 'vs/base/test/common/filters.perf.data'; + +export const SERVICE_ID = 'dacFxService'; +export const IDacFxService = createDecorator(SERVICE_ID); + +export interface IDacFxService { + _serviceBrand: any; + + registerProvider(providerId: string, provider: sqlops.DacFxServicesProvider): void; + exportBacpac(sourceDatabaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; + importBacpac(packageFilePath: string, targetDatabaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; + extractDacpac(sourceDatabaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; + deployDacpac(packageFilePath: string, targetDatabaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; +} + +export class DacFxService implements IDacFxService { + _serviceBrand: any; + private _providers: { [handle: string]: sqlops.DacFxServicesProvider; } = Object.create(null); + + constructor( + @IConnectionManagementService private _connectionService: IConnectionManagementService + ) { + } + + registerProvider(providerId: string, provider: sqlops.DacFxServicesProvider): void { + this._providers[providerId] = provider; + } + + exportBacpac(databasesName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return this._runAction(ownerUri, (runner) => { + return runner.exportBacpac(databasesName, packageFilePath, ownerUri, taskExecutionMode); + }); + } + + importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return this._runAction(ownerUri, (runner) => { + return runner.importBacpac(packageFilePath, databaseName, ownerUri, taskExecutionMode); + }); + } + + extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return this._runAction(ownerUri, (runner) => { + return runner.extractDacpac(databaseName, packageFilePath, applicationName, applicationVersion, ownerUri, taskExecutionMode); + }); + } + + deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return this._runAction(ownerUri, (runner) => { + return runner.deployDacpac(packageFilePath, databaseName, upgradeExisting, ownerUri, taskExecutionMode); + }); + } + + private _runAction(uri: string, action: (handler: sqlops.DacFxServicesProvider) => Thenable): Thenable { + let providerId: string = this._connectionService.getProviderIdFromUri(uri); + + if (!providerId) { + return TPromise.wrapError(new Error(localize('providerIdNotValidError', "Connection is required in order to interact with DacFxService"))); + } + let handler = this._providers[providerId]; + if (handler) { + return action(handler); + } else { + return TPromise.wrapError(new Error(localize('noHandlerRegistered', "No Handler Registered"))); + } + } +} \ No newline at end of file diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index abd0f050b2..f0f44a26c1 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -37,6 +37,8 @@ declare module 'sqlops' { export function registerCapabilitiesServiceProvider(provider: CapabilitiesProvider): vscode.Disposable; + export function registerDacFxServicesProvider(provider: DacFxServicesProvider): vscode.Disposable; + /** * An [event](#Event) which fires when the specific flavor of a language used in DMP * connections has changed. And example is for a SQL connection, the flavor changes @@ -1583,6 +1585,49 @@ declare module 'sqlops' { registerOnUpdated(handler: () => any): void; } + // DacFx interfaces ----------------------------------------------------------------------- + export interface DacFxResult extends ResultStatus { + operationId: string; + } + + export interface ExportParams { + databaseName: string; + packageFilePath: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; + } + + export interface ImportParams { + packageFilePath: string; + databaseName: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; + } + + export interface ExtractParams { + databaseName: string; + packageFilePath: string; + applicationName: string; + applicationVersion: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; + } + + export interface DeployParams { + packageFilePath: string; + databaseName: string; + upgradeExisting: boolean; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; + } + + export interface DacFxServicesProvider extends DataProvider { + exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; + importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; + extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; + deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; + } + // Security service interfaces ------------------------------------------------------------------------ export interface CredentialInfo { id: number; diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 3b10534757..a9eb3d71c2 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1232,7 +1232,8 @@ declare module 'sqlops' { QueryProvider = 'QueryProvider', AdminServicesProvider = 'AdminServicesProvider', AgentServicesProvider = 'AgentServicesProvider', - CapabilitiesProvider = 'CapabilitiesProvider' + CapabilitiesProvider = 'CapabilitiesProvider', + DacFxServicesProvider = 'DacFxServicesProvider', } export namespace dataprotocol { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 83f3aa4ab8..c333e2e82b 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -285,7 +285,8 @@ export enum DataProviderType { QueryProvider = 'QueryProvider', AdminServicesProvider = 'AdminServicesProvider', AgentServicesProvider = 'AgentServicesProvider', - CapabilitiesProvider = 'CapabilitiesProvider' + CapabilitiesProvider = 'CapabilitiesProvider', + DacFxServicesProvider = 'DacFxServicesProvider', } export enum DeclarativeDataType { diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index 25c32f36eb..af4e9ad4cf 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -156,6 +156,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return rt; } + $registerDacFxServiceProvider(provider: sqlops.DacFxServicesProvider): vscode.Disposable { + let rt = this.registerProvider(provider, DataProviderType.DacFxServicesProvider); + this._proxy.$registerDacFxServicesProvider(provider.providerId, provider.handle); + return rt; + } + // Capabilities Discovery handlers $getServerCapabilities(handle: number, client: sqlops.DataProtocolClientCapabilities): Thenable { return this._resolveProvider(handle).getServerCapabilities(client); diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index 0081ac381b..a09877555d 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -26,6 +26,7 @@ import { IProfilerService } from 'sql/parts/profiler/service/interfaces'; import { ISerializationService } from 'sql/services/serialization/serializationService'; import { IFileBrowserService } from 'sql/parts/fileBrowser/common/interfaces'; import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { IDacFxService } from 'sql/services/dacfx/dacFxService'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; /** @@ -55,7 +56,8 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { @ITaskService private _taskService: ITaskService, @IProfilerService private _profilerService: IProfilerService, @ISerializationService private _serializationService: ISerializationService, - @IFileBrowserService private _fileBrowserService: IFileBrowserService + @IFileBrowserService private _fileBrowserService: IFileBrowserService, + @IDacFxService private _dacFxService: IDacFxService, ) { if (extHostContext) { this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostDataProtocol); @@ -399,6 +401,26 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { return undefined; } + public $registerDacFxServicesProvider(providerId: string, handle: number): TPromise { + const self = this; + this._dacFxService.registerProvider(providerId, { + exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return self._proxy.$exportBacpac(handle, databaseName, packageFilePath, ownerUri, taskExecutionMode); + }, + importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return self._proxy.$importBacpac(handle, packageFilePath, databaseName, ownerUri, taskExecutionMode); + }, + extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return self._proxy.$extractDacpac(handle, databaseName, packageFilePath, applicationName, applicationVersion, ownerUri, taskExecutionMode); + }, + deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return self._proxy.$deployDacpac(handle, packageFilePath, databaseName, upgradeExisting, ownerUri, taskExecutionMode); + } + }); + + return undefined; + } + // Connection Management handlers public $onConnectionComplete(handle: number, connectionInfoSummary: sqlops.ConnectionInfoSummary): void { this._connectionManagementService.onConnectionComplete(handle, connectionInfoSummary); diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 9c1c67dd3f..7e46d4a15d 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -310,6 +310,10 @@ export function createApiFactory( return extHostDataProvider.$registerAgentServiceProvider(provider); }; + let registerDacFxServicesProvider = (provider: sqlops.DacFxServicesProvider): vscode.Disposable => { + return extHostDataProvider.$registerDacFxServiceProvider(provider); + }; + // namespace: dataprotocol const dataprotocol: typeof sqlops.dataprotocol = { registerBackupProvider, @@ -325,6 +329,7 @@ export function createApiFactory( registerAdminServicesProvider, registerAgentServicesProvider, registerCapabilitiesServiceProvider, + registerDacFxServicesProvider, onDidChangeLanguageFlavor(listener: (e: sqlops.DidChangeLanguageFlavorParams) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) { return extHostDataProvider.onDidChangeLanguageFlavor(listener, thisArgs, disposables); }, diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 2d11959136..9297c84688 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -409,6 +409,26 @@ export abstract class ExtHostDataProtocolShape { * Get Agent Credentials list */ $getCredentials(handle: number, connectionUri: string): Thenable { throw ni(); } + + /** + * DacFx export bacpac + */ + $exportBacpac(handle: number, databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { throw ni(); } + + /** + * DacFx import bacpac + */ + $importBacpac(handle: number, packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { throw ni(); } + + /** + * DacFx extract dacpac + */ + $extractDacpac(handle: number, databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { throw ni(); } + + /** + * DacFx deploy dacpac + */ + $deployDacpac(handle: number, packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { throw ni(); } } /** @@ -476,6 +496,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { $registerCapabilitiesServiceProvider(providerId: string, handle: number): TPromise; $registerAdminServicesProvider(providerId: string, handle: number): TPromise; $registerAgentServicesProvider(providerId: string, handle: number): TPromise; + $registerDacFxServicesProvider(providerId: string, handle: number): TPromise; $unregisterProvider(handle: number): TPromise; $onConnectionComplete(handle: number, connectionInfoSummary: sqlops.ConnectionInfoSummary): void; $onIntelliSenseCacheComplete(handle: number, connectionUri: string): void; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 08947eeb22..e8b49d4f42 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -137,6 +137,7 @@ import { IScriptingService, ScriptingService } from 'sql/services/scripting/scri import { IAdminService, AdminService } from 'sql/parts/admin/common/adminService'; import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces'; import { JobManagementService } from 'sql/parts/jobManagement/common/jobManagementService'; +import { IDacFxService, DacFxService } from 'sql/services/dacfx/dacFxService'; import { IBackupService, IBackupUiService } from 'sql/parts/disasterRecovery/backup/common/backupService'; import { BackupService, BackupUiService } from 'sql/parts/disasterRecovery/backup/common/backupServiceImp'; import { IRestoreDialogController, IRestoreService } from 'sql/parts/disasterRecovery/restore/common/restoreService'; @@ -586,6 +587,8 @@ export class Workbench extends Disposable implements IPartService { // {{SQL CARBON EDIT}} serviceCollection.set(ICommandLineProcessing, this.instantiationService.createInstance(CommandLineService)); // {{SQL CARBON EDIT}} + serviceCollection.set(IDacFxService, this.instantiationService.createInstance(DacFxService)); + this._register(toDisposable(() => connectionManagementService.shutdown())); this._register(toDisposable(() => accountManagementService.shutdown())); this._register(toDisposable(() => notebookService.shutdown()));