Add DB Reference quickpick dialog (#16601)

* Add DB Reference quickpick dialog

* pr comments & cleanup
This commit is contained in:
Charles Gagnon
2021-08-09 12:46:53 -07:00
committed by GitHub
parent c22589574c
commit b2ff8162e6
7 changed files with 342 additions and 41 deletions

View File

@@ -69,6 +69,7 @@ export const databaseReferencesNodeName = localize('databaseReferencesNodeName',
export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string");
export const yesString = localize('yesString', "Yes");
export const noString = localize('noString', "No");
export const noStringDefault = localize('noStringDefault', "No (default)");
export const okString = localize('okString', "Ok");
export const selectString = localize('selectString', "Select");
export const dacpacFiles = localize('dacpacFiles', "dacpac Files");
@@ -79,6 +80,7 @@ export const objectType = localize('objectType', "Object Type");
export const schema = localize('schema', "Schema");
export const schemaObjectType = localize('schemaObjectType', "Schema/Object Type");
export const defaultProjectNameStarter = localize('defaultProjectNameStarter', "DatabaseProject");
export const location = localize('location', "Location");
export const reloadProject = localize('reloadProject', "Would you like to reload your database project?");
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
export function deleteConfirmation(toDelete: string) { return localize('deleteConfirmation', "Are you sure you want to delete {0}?", toDelete); }
@@ -129,12 +131,10 @@ export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not
export const addDatabaseReferenceDialogName = localize('addDatabaseReferencedialogName', "Add database reference");
export const addDatabaseReferenceOkButtonText = localize('addDatabaseReferenceOkButtonText', "Add reference");
export const referenceRadioButtonsGroupTitle = localize('referenceRadioButtonsGroupTitle', "Type");
export const projectRadioButtonTitle = localize('projectRadioButtonTitle', "Project");
export const systemDatabaseRadioButtonTitle = localize('systemDatabaseRadioButtonTitle', "System database");
export const projectLabel = localize('projectLocString', "Project");
export const systemDatabase = localize('systemDatabase', "System database");
export const dacpacText = localize('dacpacText', "Data-tier application (.dacpac)");
export const dacpacPlaceholder = localize('dacpacPlaceholder', "Select .dacpac");
export const loadDacpacButton = localize('loadDacpacButton', "Select .dacpac");
export const locationDropdown = localize('locationDropdown', "Location");
export const selectDacpac = localize('selectDacpac', "Select .dacpac");
export const sameDatabase = localize('sameDatabase', "Same database");
export const differentDbSameServer = localize('differentDbSameServer', "Different database, same server");
export const differentDbDifferentServer = localize('differentDbDifferentServer', "Different database, different server");
@@ -153,6 +153,7 @@ export const otherServer = 'OtherServer';
export const otherSeverVariable = 'OtherServer';
export const databaseProject = localize('databaseProject', "Database project");
export const dacpacNotOnSameDrive = (projectLocation: string): string => { return localize('dacpacNotOnSameDrive', "Dacpac references need to be located on the same drive as the project file. The project file is located at {0}", projectLocation); };
export const referenceType = localize('referenceType', "Reference type");
// Create Project From Database dialog strings
@@ -163,7 +164,6 @@ export const targetProject = localize('targetProject', "Target project");
export const createProjectSettings = localize('createProjectSettings', "Settings");
export const projectNameLabel = localize('projectNameLabel', "Name");
export const projectNamePlaceholderText = localize('projectNamePlaceholderText', "Enter project name");
export const projectLocationLabel = localize('projectLocationLabel', "Location");
export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Select location to create project");
export const browseButtonText = localize('browseButtonText', "Browse folder");
export const selectFolderStructure = localize('selectFolderStructure', "Select folder structure");

View File

@@ -37,6 +37,7 @@ import { DashboardData, PublishData, Status } from '../models/dashboardData/dash
import { launchPublishDatabaseQuickpick } from '../dialogs/publishDatabaseQuickpick';
import { SqlTargetPlatform } from 'sqldbproj';
import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick';
import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick';
const maxTableLength = 10;
@@ -53,6 +54,8 @@ export enum TaskExecutionMode {
executeAndScript = 2
}
export type AddDatabaseReferenceSettings = ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings;
/**
* Controller for managing lifecycle of projects
*/
@@ -672,15 +675,25 @@ export class ProjectsController {
* Adds a database reference to the project
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
*/
public async addDatabaseReference(context: Project | dataworkspace.WorkspaceTreeItem): Promise<AddDatabaseReferenceDialog> {
public async addDatabaseReference(context: Project | dataworkspace.WorkspaceTreeItem): Promise<AddDatabaseReferenceDialog | undefined> {
const project = this.getProjectFromContext(context);
const addDatabaseReferenceDialog = this.getAddDatabaseReferenceDialog(project);
addDatabaseReferenceDialog.addReference = async (proj, prof) => await this.addDatabaseReferenceCallback(proj, prof, context as dataworkspace.WorkspaceTreeItem);
if (utils.getAzdataApi()) {
const addDatabaseReferenceDialog = this.getAddDatabaseReferenceDialog(project);
addDatabaseReferenceDialog.addReference = async (proj, settings) => await this.addDatabaseReferenceCallback(proj, settings, context as dataworkspace.WorkspaceTreeItem);
addDatabaseReferenceDialog.openDialog();
return addDatabaseReferenceDialog;
} else {
const settings = await addDatabaseReferenceQuickpick(project);
if (settings) {
await this.addDatabaseReferenceCallback(project, settings, context as dataworkspace.WorkspaceTreeItem);
}
return undefined;
}
addDatabaseReferenceDialog.openDialog();
return addDatabaseReferenceDialog;
}
/**
@@ -689,7 +702,7 @@ export class ProjectsController {
* @param settings settings for the database reference
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
*/
public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings, context: dataworkspace.WorkspaceTreeItem): Promise<void> {
public async addDatabaseReferenceCallback(project: Project, settings: AddDatabaseReferenceSettings, context: dataworkspace.WorkspaceTreeItem): Promise<void> {
try {
if ((<IProjectReferenceSettings>settings).projectName !== undefined) {
// get project path and guid

View File

@@ -165,7 +165,7 @@ export class AddDatabaseReferenceDialog {
} else if (this.currentReferenceType === ReferenceType.systemDb) {
referenceSettings = {
databaseName: <string>this.databaseNameTextbox?.value,
systemDb: <string>this.systemDatabaseDropdown?.value === constants.master ? SystemDatabase.master : SystemDatabase.msdb,
systemDb: getSystemDatabase(<string>this.systemDatabaseDropdown?.value),
suppressMissingDependenciesErrors: <boolean>this.suppressMissingDependenciesErrorsCheckbox?.checked
};
} else { // this.currentReferenceType === ReferenceType.dacpac
@@ -192,7 +192,7 @@ export class AddDatabaseReferenceDialog {
this.projectRadioButton = this.view!.modelBuilder.radioButton()
.withProps({
name: 'referenceType',
label: constants.projectRadioButtonTitle
label: constants.projectLabel
}).component();
this.projectRadioButton.onDidClick(() => {
@@ -202,7 +202,7 @@ export class AddDatabaseReferenceDialog {
this.systemDatabaseRadioButton = this.view!.modelBuilder.radioButton()
.withProps({
name: 'referenceType',
label: constants.systemDatabaseRadioButtonTitle
label: constants.systemDatabase
}).component();
this.systemDatabaseRadioButton.onDidClick(() => {
@@ -307,7 +307,7 @@ export class AddDatabaseReferenceDialog {
private createSystemDatabaseDropdown(): azdataType.FormComponent {
this.systemDatabaseDropdown = this.view!.modelBuilder.dropDown().withProps({
values: [constants.master, constants.msdb],
values: getSystemDbOptions(this.project),
ariaLabel: constants.databaseNameLabel
}).component();
@@ -315,11 +315,6 @@ export class AddDatabaseReferenceDialog {
this.setDefaultDatabaseValues();
});
// only master is a valid system db reference for projects targetting Azure and DW
if (this.project.getProjectTargetVersion().toLowerCase().includes('azure') || this.project.getProjectTargetVersion().toLowerCase().includes('dw')) {
this.systemDatabaseDropdown.values?.splice(1);
}
return {
component: this.systemDatabaseDropdown,
title: constants.databaseNameLabel
@@ -329,7 +324,7 @@ export class AddDatabaseReferenceDialog {
private createDacpacTextbox(): azdataType.FormComponent {
this.dacpacTextbox = this.view!.modelBuilder.inputBox().withProps({
ariaLabel: constants.dacpacText,
placeHolder: constants.dacpacPlaceholder,
placeHolder: constants.selectDacpac,
width: '400px'
}).component();
@@ -351,25 +346,14 @@ export class AddDatabaseReferenceDialog {
private createLoadDacpacButton(): azdataType.ButtonComponent {
const loadDacpacButton = this.view!.modelBuilder.button().withProps({
ariaLabel: constants.loadDacpacButton,
ariaLabel: constants.selectDacpac,
iconPath: IconPathHelper.folder_blue,
height: '18px',
width: '18px'
}).component();
loadDacpacButton.onDidClick(async () => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined,
openLabel: constants.selectString,
filters: {
[constants.dacpacFiles]: ['dacpac'],
}
}
);
let fileUris = await promptDacpacLocation();
if (!fileUris || fileUris.length === 0) {
return;
@@ -383,7 +367,7 @@ export class AddDatabaseReferenceDialog {
private createLocationDropdown(): azdataType.FormComponent {
this.locationDropdown = this.view!.modelBuilder.dropDown().withProps({
ariaLabel: constants.locationDropdown,
ariaLabel: constants.location,
values: this.currentReferenceType === ReferenceType.systemDb ? constants.systemDbLocationDropdownValues : constants.locationDropdownValues
}).component();
@@ -397,7 +381,7 @@ export class AddDatabaseReferenceDialog {
return {
component: this.locationDropdown,
title: constants.locationDropdown
title: constants.location
};
}
@@ -630,3 +614,31 @@ export class AddDatabaseReferenceDialog {
return !!this.databaseNameTextbox?.value && !!this.serverNameTextbox?.value && !!this.serverVariableTextbox?.value;
}
}
export function getSystemDbOptions(project: Project): string[] {
// only master is a valid system db reference for projects targeting Azure and DW
if (project.getProjectTargetVersion().toLowerCase().includes('azure') || project.getProjectTargetVersion().toLowerCase().includes('dw')) {
return [constants.master];
}
return [constants.master, constants.msdb];
}
export function getSystemDatabase(name: string): SystemDatabase {
return name === constants.master ? SystemDatabase.master : SystemDatabase.msdb;
}
export async function promptDacpacLocation(): Promise<vscode.Uri[] | undefined> {
return await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined,
openLabel: constants.selectString,
title: constants.selectDacpac,
filters: {
[constants.dacpacFiles]: ['dacpac'],
}
}
);
}

View File

@@ -0,0 +1,277 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import path = require('path');
import * as vscode from 'vscode';
import * as constants from '../common/constants';
import { getSqlProjectsInWorkspace, isValidSqlCmdVariableName, removeSqlCmdVariableFormatting } from '../common/utils';
import { AddDatabaseReferenceSettings } from '../controllers/projectController';
import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { Project } from '../models/project';
import { getSystemDatabase, getSystemDbOptions, promptDacpacLocation } from './addDatabaseReferenceDialog';
interface DbServerValues {
dbName?: string,
dbVariable?: string,
serverName?: string,
serverVariable?: string
}
/**
* Create flow for adding a database reference using only VS Code-native APIs such as QuickPick
* @param connectionInfo Optional connection info to use instead of prompting the user for a connection
*/
export async function addDatabaseReferenceQuickpick(project: Project): Promise<AddDatabaseReferenceSettings | undefined> {
const otherProjectsInWorkspace = (await getSqlProjectsInWorkspace()).filter(p => p.fsPath !== project.projectFilePath);
// 1. Prompt for reference type
// Only show project option if we have at least one other project in the workspace
const referenceTypes = otherProjectsInWorkspace.length > 0 ?
[constants.projectLabel, constants.systemDatabase, constants.dacpacText] :
[constants.systemDatabase, constants.dacpacText];
const referenceType = await vscode.window.showQuickPick(
referenceTypes,
{ title: constants.referenceType, ignoreFocusOut: true });
if (!referenceType) {
// User cancelled
return undefined;
}
switch (referenceType) {
case constants.projectLabel:
return addProjectReference(otherProjectsInWorkspace);
case constants.systemDatabase:
return addSystemDatabaseReference(project);
case constants.dacpacText:
return addDacpacReference();
default:
console.log(`Unknown reference type ${referenceType}`);
return undefined;
}
}
async function addProjectReference(otherProjectsInWorkspace: vscode.Uri[]): Promise<IProjectReferenceSettings | undefined> {
// (steps continued from addDatabaseReferenceQuickpick)
// 2. Prompt database project
const otherProjectQuickpickItems: (vscode.QuickPickItem & { uri: vscode.Uri })[] = otherProjectsInWorkspace.map(p => {
return {
label: path.parse(p.fsPath).name,
uri: p
};
});
const selectedProject = await vscode.window.showQuickPick(
otherProjectQuickpickItems,
{ title: constants.databaseProject, ignoreFocusOut: true, });
if (!selectedProject) {
return;
}
// 3. Prompt location
const location = await promptLocation();
if (!location) {
// User cancelled
return;
}
const referenceSettings: IProjectReferenceSettings = {
projectName: selectedProject.label,
projectGuid: '',
projectRelativePath: undefined,
databaseName: undefined,
databaseVariable: undefined,
serverName: undefined,
serverVariable: undefined,
suppressMissingDependenciesErrors: false
};
const dbServerValues = await promptDbServerValues(location, selectedProject.label);
if (!dbServerValues) {
// User cancelled
return;
}
referenceSettings.databaseName = dbServerValues.dbName;
referenceSettings.databaseVariable = dbServerValues.dbVariable;
referenceSettings.serverName = dbServerValues.serverName;
referenceSettings.serverVariable = dbServerValues.serverVariable;
// 7. Prompt suppress unresolved ref errors
const suppressErrors = await promptSuppressUnresolvedRefErrors();
referenceSettings.suppressMissingDependenciesErrors = suppressErrors;
return referenceSettings;
}
async function addSystemDatabaseReference(project: Project): Promise<ISystemDatabaseReferenceSettings | undefined> {
// (steps continued from addDatabaseReferenceQuickpick)
// 2. Prompt System DB
const selectedSystemDb = await vscode.window.showQuickPick(
getSystemDbOptions(project),
{ title: constants.systemDatabase, ignoreFocusOut: true, });
if (!selectedSystemDb) {
// User cancelled
return undefined;
}
// 3. Prompt DB name
const dbName = await promptDbName(selectedSystemDb);
// 4. Prompt suppress unresolved ref errors
const suppressErrors = await promptSuppressUnresolvedRefErrors();
return {
databaseName: dbName,
systemDb: getSystemDatabase(selectedSystemDb),
suppressMissingDependenciesErrors: suppressErrors
};
}
async function addDacpacReference(): Promise<IDacpacReferenceSettings | undefined> {
// (steps continued from addDatabaseReferenceQuickpick)
// 2. Prompt for location
const location = await promptLocation();
if (!location) {
// User cancelled
return undefined;
}
// 3. Prompt for dacpac location
// Show quick pick with just browse option to give user context about what the file dialog is for (since that doesn't always have a title)
const browseSelected = await vscode.window.showQuickPick(
[constants.browseEllipsis],
{ title: constants.selectDacpac, ignoreFocusOut: true });
if (!browseSelected) {
return undefined;
}
const dacPacLocation = (await promptDacpacLocation())?.[0];
if (!dacPacLocation) {
// User cancelled
return undefined;
}
// 4. Prompt for db/server values
const dbServerValues = await promptDbServerValues(location, path.parse(dacPacLocation.fsPath).name);
if (!dbServerValues) {
// User cancelled
return;
}
// 5. Prompt suppress unresolved ref errors
const suppressErrors = await promptSuppressUnresolvedRefErrors();
return {
databaseName: dbServerValues.dbName,
dacpacFileLocation: dacPacLocation,
databaseVariable: removeSqlCmdVariableFormatting(dbServerValues.dbVariable),
serverName: dbServerValues.serverName,
serverVariable: removeSqlCmdVariableFormatting(dbServerValues.serverVariable),
suppressMissingDependenciesErrors: suppressErrors
};
}
async function promptLocation(): Promise<string | undefined> {
return vscode.window.showQuickPick(
constants.locationDropdownValues,
{ title: constants.location, ignoreFocusOut: true, });
}
async function promptDbName(defaultValue: string): Promise<string | undefined> {
return vscode.window.showInputBox(
{
title: constants.databaseName,
value: defaultValue,
validateInput: (value) => {
return value ? undefined : constants.nameMustNotBeEmpty;
},
ignoreFocusOut: true
});
}
async function promptDbVar(defaultValue: string): Promise<string> {
return await vscode.window.showInputBox(
{
title: constants.databaseVariable,
value: defaultValue,
validateInput: (value: string) => {
return isValidSqlCmdVariableName(value) ? '' : constants.notValidVariableName(value);
},
ignoreFocusOut: true
}) ?? '';
}
async function promptServerName(): Promise<string | undefined> {
return vscode.window.showInputBox(
{
title: constants.serverName,
value: constants.otherServer,
validateInput: (value) => {
return value ? undefined : constants.nameMustNotBeEmpty;
},
ignoreFocusOut: true
});
}
async function promptServerVar(): Promise<string> {
return await vscode.window.showInputBox(
{
title: constants.serverVariable,
value: constants.otherSeverVariable,
validateInput: (value: string) => {
return isValidSqlCmdVariableName(value) ? '' : constants.notValidVariableName(value);
},
ignoreFocusOut: true
}) ?? '';
}
async function promptSuppressUnresolvedRefErrors(): Promise<boolean> {
const selectedOption = await vscode.window.showQuickPick(
[constants.noStringDefault, constants.yesString],
{ title: constants.suppressMissingDependenciesErrors, ignoreFocusOut: true, });
return selectedOption === constants.yesString ? true : false;
}
async function promptDbServerValues(location: string, defaultDbName: string): Promise<DbServerValues | undefined> {
const ret: DbServerValues = {};
// Only prompt db values if the location is on a different db/server
if (location !== constants.sameDatabase) {
// 4. Prompt database name
const dbName = await promptDbName(defaultDbName);
if (!dbName) {
// User cancelled
return undefined;
}
ret.dbName = dbName;
// 5. Prompt db var
const dbVar = await promptDbVar(dbName);
// DB Variable is optional so treat escape as skipping it (not cancel in this case)
ret.dbVariable = dbVar;
}
// Only prompt server values if location is different server
if (location === constants.differentDbDifferentServer) {
// 5. Prompt server name
const serverName = await promptServerName();
if (!serverName) {
// User cancelled
return undefined;
}
ret.serverName = serverName;
// 6. Prompt server var
const serverVar = await promptServerVar();
if (!serverVar) {
// User cancelled
return undefined;
}
ret.serverVariable = serverVar;
}
return ret;
}

View File

@@ -269,7 +269,7 @@ export class CreateProjectFromDatabaseDialog {
this.projectLocationTextBox = view.modelBuilder.inputBox().withProps({
value: '',
ariaLabel: constants.projectLocationLabel,
ariaLabel: constants.location,
placeHolder: constants.projectLocationPlaceholderText,
width: cssStyles.createProjectFromDatabaseTextboxWidth
}).component();
@@ -280,7 +280,7 @@ export class CreateProjectFromDatabaseDialog {
});
const projectLocationLabel = view.modelBuilder.text().withProps({
value: constants.projectLocationLabel,
value: constants.location,
requiredIndicator: true,
width: cssStyles.createProjectFromDatabaseLabelWidth
}).component();

View File

@@ -192,7 +192,6 @@ export async function launchPublishDatabaseQuickpick(project: Project, projectCo
return;
}
// TODO@chgagnon: Get deployment options
// 6. Generate script/publish
let settings: IDeploySettings = {
databaseName: databaseName,

View File

@@ -583,7 +583,7 @@ describe('ProjectsController', function (): void {
});
let dialog = await projController.object.addDatabaseReference(proj);
await dialog.addReferenceClick();
await dialog!.addReferenceClick();
should(holler).equal(addDbRefHoller, 'executionCallback() is supposed to have been setup and called for add database reference scenario');
});