mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-24 09:35:37 -05:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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); });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user