Initial project deploy dialog (#10288)

* connection picking dialog for deploy

* add data source option

* fix errors

* merge fix

* show connection name instead of connection string if there is one

* remove unnecessary async

* async (#10292)

* Revert "async (#10292)"

This reverts commit c94139aa45d8f1d868ebd251f3016315718b56ae.

* add a few tests

* addressing comments

* remove cancel click handler

* remove text box for generate script file path because it will just open up a new editor with the script

* fix test

Co-authored-by: Amir Omidi <amomidi@microsoft.com>
This commit is contained in:
Kim Santiago
2020-05-11 18:12:29 -07:00
committed by GitHub
parent 301ce1cf87
commit f3d36c1b86
7 changed files with 347 additions and 3 deletions

View File

@@ -23,6 +23,10 @@ export class ApiWrapper {
return azdata.connection.getCurrentConnection();
}
public openConnectionDialog(): Thenable<azdata.connection.Connection> {
return azdata.connection.openConnectionDialog();
}
public getCredentials(connectionId: string): Thenable<{ [name: string]: string }> {
return azdata.connection.getCredentials(connectionId);
}

View File

@@ -10,6 +10,7 @@ const localize = nls.loadMessageBundle();
// Placeholder values
export const dataSourcesFileName = 'datasources.json';
export const sqlprojExtension = '.sqlproj';
export const initialCatalogSetting = 'Initial Catalog';
// UI Strings
@@ -21,6 +22,23 @@ export const newDatabaseProjectName = localize('newDatabaseProjectName', "New da
export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project");
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
// Deploy dialog strings
export const deployDialogName = localize('deployDialogName', "Deploy Database");
export const deployDialogOkButtonText = localize('deployDialogOkButtonText', "Deploy");
export const cancelButtonText = localize('cancelButtonText', "Cancel");
export const generateScriptButtonText = localize('generateScriptButtonText', "Generate Script");
export const targetDatabaseSettings = localize('targetDatabaseSettings', "Target Database Settings");
export const databaseNameLabel = localize('databaseNameLabel', "Database");
export const deployScriptNameLabel = localize('deployScriptName', "Deploy script name");
export const targetConnectionLabel = localize('targetConnectionLabel', "Target Connection");
export const editConnectionButtonText = localize('editConnectionButtonText', "Edit");
export const clearButtonText = localize('clearButtonText', "Clear");
export const dataSourceRadioButtonLabel = localize('dataSourceRadioButtonLabel', "Data sources");
export const connectionRadioButtonLabel = localize('connectionRadioButtonLabel', "Connections");
export const selectConnectionRadioButtonsTitle = localize('selectconnectionRadioButtonsTitle', "Specify connection from:");
export const dataSourceDropdownTitle = localize('dataSourceDropdownTitle', "Data source");
// Error messages
export const multipleSqlProjFiles = localize('multipleSqlProjFilesSelected', "Multiple .sqlproj files selected; please select only one.");

View File

@@ -50,7 +50,7 @@ export default class MainController implements Disposable {
this.apiWrapper.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await this.apiWrapper.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO
this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.build(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', (node: BaseProjectTreeItem) => { this.projectsController.deploy(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); });

View File

@@ -18,6 +18,7 @@ import { promises as fs } from 'fs';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { FolderNode } from '../models/tree/fileFolderTreeItem';
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
/**
* Controller for managing project lifecycle
@@ -118,9 +119,10 @@ export class ProjectsController {
await this.apiWrapper.showErrorMessage(`Build not yet implemented: ${project.projectFilePath}`); // TODO
}
public async deploy(treeNode: BaseProjectTreeItem) {
public deploy(treeNode: BaseProjectTreeItem): void {
const project = this.getProjectContextFromTreeNode(treeNode);
await this.apiWrapper.showErrorMessage(`Deploy not yet implemented: ${project.projectFilePath}`); // TODO
const deployDatabaseDialog = new DeployDatabaseDialog(this.apiWrapper, project);
deployDatabaseDialog.openDialog();
}
public async import(treeNode: BaseProjectTreeItem) {

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as constants from '../common/constants';
import { Project } from '../models/project';
import { DataSource } from '../models/dataSources/dataSources';
import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource';
import { ApiWrapper } from '../common/apiWrapper';
interface DataSourceDropdownValue extends azdata.CategoryValue {
dataSource: DataSource;
database: string;
}
export class DeployDatabaseDialog {
public dialog: azdata.window.Dialog;
public deployTab: azdata.window.DialogTab;
private targetConnectionTextBox: azdata.InputBoxComponent | undefined;
private targetConnectionFormComponent: azdata.FormComponent | undefined;
private dataSourcesFormComponent: azdata.FormComponent | undefined;
private dataSourcesDropDown: azdata.DropDownComponent | undefined;
private targetDatabaseTextBox: azdata.InputBoxComponent | undefined;
private connectionsRadioButton: azdata.RadioButtonComponent | undefined;
private dataSourcesRadioButton: azdata.RadioButtonComponent | undefined;
private formBuilder: azdata.FormBuilder | undefined;
private connection: azdata.connection.Connection | undefined;
private connectionIsDataSource: boolean | undefined;
constructor(private apiWrapper: ApiWrapper, private project: Project) {
this.dialog = azdata.window.createModelViewDialog(constants.deployDialogName);
this.deployTab = azdata.window.createTab(constants.deployDialogName);
}
public openDialog(): void {
this.initializeDialog();
this.dialog.okButton.label = constants.deployDialogOkButtonText;
this.dialog.okButton.enabled = false;
this.dialog.okButton.onClick(async () => await this.deploy());
this.dialog.cancelButton.label = constants.cancelButtonText;
let generateScriptButton: azdata.window.Button = azdata.window.createButton(constants.generateScriptButtonText);
generateScriptButton.onClick(async () => await this.generateScript());
generateScriptButton.enabled = false;
this.dialog.customButtons = [];
this.dialog.customButtons.push(generateScriptButton);
azdata.window.openDialog(this.dialog);
}
private initializeDialog(): void {
this.initializeDeployTab();
this.dialog.content = [this.deployTab];
}
private initializeDeployTab(): void {
this.deployTab.registerContent(async view => {
let selectConnectionRadioButtons = this.createRadioButtons(view);
this.targetConnectionFormComponent = this.createTargetConnectionComponent(view);
this.targetDatabaseTextBox = view.modelBuilder.inputBox().withProperties({
value: this.getDefaultDatabaseName(),
ariaLabel: constants.databaseNameLabel
}).component();
this.dataSourcesFormComponent = this.createDataSourcesDropdown(view);
this.targetDatabaseTextBox.onTextChanged(() => {
this.tryEnableGenerateScriptAndOkButtons();
});
this.formBuilder = <azdata.FormBuilder>view.modelBuilder.formContainer()
.withFormItems([
{
title: constants.targetDatabaseSettings,
components: [
{
title: constants.selectConnectionRadioButtonsTitle,
component: selectConnectionRadioButtons
},
this.targetConnectionFormComponent,
{
title: constants.databaseNameLabel,
component: this.targetDatabaseTextBox
}
]
}
], {
horizontal: false
})
.withLayout({
width: '100%'
});
let formModel = this.formBuilder.component();
await view.initializeModel(formModel);
});
}
private async deploy(): Promise<void> {
// TODO: hook up with build and deploy
// if target connection is a data source, have to check if already connected or if connection dialog needs to be opened
}
private async generateScript(): Promise<void> {
// TODO: hook up with build and generate script
// if target connection is a data source, have to check if already connected or if connection dialog needs to be opened
azdata.window.closeDialog(this.dialog);
}
public getDefaultDatabaseName(): string {
return this.project.projectFileName;
}
private createRadioButtons(view: azdata.ModelView): azdata.Component {
this.connectionsRadioButton = view.modelBuilder.radioButton()
.withProperties({
name: 'connection',
label: constants.connectionRadioButtonLabel
}).component();
this.connectionsRadioButton.checked = true;
this.connectionsRadioButton.onDidClick(async () => {
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.dataSourcesFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>this.targetConnectionFormComponent, 2);
this.connectionIsDataSource = false;
this.targetDatabaseTextBox!.value = this.getDefaultDatabaseName();
});
this.dataSourcesRadioButton = view.modelBuilder.radioButton()
.withProperties({
name: 'connection',
label: constants.dataSourceRadioButtonLabel
}).component();
this.dataSourcesRadioButton.onDidClick(async () => {
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.targetConnectionFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>this.dataSourcesFormComponent, 2);
this.connectionIsDataSource = true;
this.setDatabaseToSelectedDataSourceDatabase();
});
let flexRadioButtonsModel: azdata.FlexContainer = view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([this.connectionsRadioButton, this.dataSourcesRadioButton])
.withProperties({ ariaRole: 'radiogroup' })
.component();
return flexRadioButtonsModel;
}
private createTargetConnectionComponent(view: azdata.ModelView): azdata.FormComponent {
// TODO: make this not editable
this.targetConnectionTextBox = view.modelBuilder.inputBox().withProperties({
value: '',
ariaLabel: constants.targetConnectionLabel
}).component();
this.targetConnectionTextBox.onTextChanged(() => {
this.tryEnableGenerateScriptAndOkButtons();
});
let editConnectionButton: azdata.Component = this.createEditConnectionButton(view);
let clearButton: azdata.Component = this.createClearButton(view);
return {
title: constants.targetConnectionLabel,
component: this.targetConnectionTextBox,
actions: [editConnectionButton, clearButton]
};
}
private createDataSourcesDropdown(view: azdata.ModelView): azdata.FormComponent {
let dataSourcesValues: DataSourceDropdownValue[] = [];
this.project.dataSources.forEach(dataSource => {
const dbName: string = (dataSource as SqlConnectionDataSource).getSetting(constants.initialCatalogSetting);
const displayName: string = `${dataSource.name}`;
dataSourcesValues.push({
displayName: displayName,
name: dataSource.name,
dataSource: dataSource,
database: dbName
});
});
this.dataSourcesDropDown = view.modelBuilder.dropDown().withProperties({
values: dataSourcesValues,
}).component();
this.dataSourcesDropDown.onValueChanged(() => {
this.setDatabaseToSelectedDataSourceDatabase();
this.tryEnableGenerateScriptAndOkButtons();
});
return {
title: constants.dataSourceDropdownTitle,
component: this.dataSourcesDropDown
};
}
private setDatabaseToSelectedDataSourceDatabase(): void {
if ((<DataSourceDropdownValue>this.dataSourcesDropDown!.value).database) {
this.targetDatabaseTextBox!.value = (<DataSourceDropdownValue>this.dataSourcesDropDown!.value).database;
}
}
private createEditConnectionButton(view: azdata.ModelView): azdata.Component {
let editConnectionButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({
label: constants.editConnectionButtonText,
title: constants.editConnectionButtonText,
ariaLabel: constants.editConnectionButtonText
}).component();
editConnectionButton.onDidClick(async () => {
this.connection = await this.apiWrapper.openConnectionDialog();
// show connection name if there is one, otherwise show connection string
if (this.connection.options['connectionName']) {
this.targetConnectionTextBox!.value = this.connection.options['connectionName'];
} else {
this.targetConnectionTextBox!.value = await azdata.connection.getConnectionString(this.connection.connectionId, false);
}
// change the database inputbox value to the connection's database if there is one
if (this.connection.options.database) {
this.targetDatabaseTextBox!.value = this.connection.options.database;
}
});
return editConnectionButton;
}
private createClearButton(view: azdata.ModelView): azdata.Component {
let clearButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({
label: constants.clearButtonText,
title: constants.clearButtonText,
ariaLabel: constants.clearButtonText
}).component();
clearButton.onDidClick(() => {
this.targetConnectionTextBox!.value = '';
});
return clearButton;
}
// only enable Generate Script and Ok buttons if all fields are filled
private tryEnableGenerateScriptAndOkButtons(): void {
if (this.targetConnectionTextBox!.value && this.targetDatabaseTextBox!.value
|| this.connectionIsDataSource && this.targetDatabaseTextBox!.value) {
this.dialog.okButton.enabled = true;
this.dialog.customButtons[0].enabled = true;
} else {
this.dialog.okButton.enabled = false;
this.dialog.customButtons[0].enabled = false;
}
}
}

View File

@@ -16,6 +16,7 @@ import { DataSource } from './dataSources/dataSources';
*/
export class Project {
public projectFilePath: string;
public projectFileName: string;
public files: ProjectEntry[] = [];
public dataSources: DataSource[] = [];
@@ -27,6 +28,7 @@ export class Project {
constructor(projectFilePath: string) {
this.projectFilePath = projectFilePath;
this.projectFileName = path.basename(projectFilePath, '.sqlproj');
}
/**

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as path from 'path';
import * as os from 'os';
import * as vscode from 'vscode';
import * as baselines from './baselines/baselines';
import * as templates from '../templates/templates';
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
import { Project } from '../models/project';
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
import { ProjectsController } from '../controllers/projectController';
import { createContext, TestContext } from './testContext';
let testContext: TestContext;
describe('Deploy Database Dialog', () => {
before(async function (): Promise<void> {
testContext = createContext();
await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates'));
await baselines.loadBaselines();
});
it('Should open dialog successfully ', async function (): Promise<void> {
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
const project = new Project(projFilePath);
const deployDatabaseDialog = new DeployDatabaseDialog(testContext.apiWrapper.object, project);
deployDatabaseDialog.openDialog();
should.notEqual(deployDatabaseDialog.deployTab, undefined);
});
it('Should create default database name correctly ', async function (): Promise<void> {
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const projFolder = `TestProject_${new Date().getTime()}`;
const projFileDir = path.join(os.tmpdir(), projFolder);
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
const project = new Project(projFilePath);
const deployDatabaseDialog = new DeployDatabaseDialog(testContext.apiWrapper.object, project);
should.equal(deployDatabaseDialog.getDefaultDatabaseName(), project.projectFileName);
});
});