mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Import project from database (#10326)
* Initial changes for Import database as new project * Functionally complete code * Initial changes for Import database as new project * Functionally complete code * Resolved conflicts with latest changes. Also did some code refactoring. * Addressed comments. Added unit tests. * Addressed comments * Moved ExtractTarget enum from azdata to mssql * Addressed comments * Fixed indentation in project templates
This commit is contained in:
@@ -421,6 +421,7 @@ export enum TaskExecutionMode {
|
|||||||
script = 1,
|
script = 1,
|
||||||
executeAndScript = 2,
|
executeAndScript = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportParams {
|
export interface ExportParams {
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
packageFilePath: string;
|
packageFilePath: string;
|
||||||
@@ -442,6 +443,7 @@ export interface ExtractParams {
|
|||||||
applicationName: string;
|
applicationName: string;
|
||||||
applicationVersion: string;
|
applicationVersion: string;
|
||||||
ownerUri: string;
|
ownerUri: string;
|
||||||
|
extractTarget?: mssql.ExtractTarget;
|
||||||
taskExecutionMode: TaskExecutionMode;
|
taskExecutionMode: TaskExecutionMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,18 @@ export class DacFxService implements mssql.IDacFxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> {
|
public extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> {
|
||||||
const params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: packageFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
|
const params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: packageFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, extractTarget: mssql.ExtractTarget.dacpac, taskExecutionMode: taskExecutionMode };
|
||||||
|
return this.client.sendRequest(contracts.ExtractRequest.type, params).then(
|
||||||
|
undefined,
|
||||||
|
e => {
|
||||||
|
this.client.logFailedRequest(contracts.ExtractRequest.type, e);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> {
|
||||||
|
const params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: targetFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, extractTarget: extractTarget, taskExecutionMode: taskExecutionMode };
|
||||||
return this.client.sendRequest(contracts.ExtractRequest.type, params).then(
|
return this.client.sendRequest(contracts.ExtractRequest.type, params).then(
|
||||||
undefined,
|
undefined,
|
||||||
e => {
|
e => {
|
||||||
|
|||||||
11
extensions/mssql/src/mssql.d.ts
vendored
11
extensions/mssql/src/mssql.d.ts
vendored
@@ -319,10 +319,20 @@ export interface SchemaCompareOpenScmpResult extends azdata.ResultStatus {
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region --- dacfx
|
//#region --- dacfx
|
||||||
|
export const enum ExtractTarget {
|
||||||
|
dacpac = 0,
|
||||||
|
file = 1,
|
||||||
|
flat = 2,
|
||||||
|
objectType = 3,
|
||||||
|
schema = 4,
|
||||||
|
schemaObjectType = 5
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDacFxService {
|
export interface IDacFxService {
|
||||||
exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||||
importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||||
extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||||
|
importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||||
deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record<string, string>): Thenable<DacFxResult>;
|
deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record<string, string>): Thenable<DacFxResult>;
|
||||||
generateDeployScript(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record<string, string>): Thenable<DacFxResult>;
|
generateDeployScript(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record<string, string>): Thenable<DacFxResult>;
|
||||||
generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<GenerateDeployPlanResult>;
|
generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<GenerateDeployPlanResult>;
|
||||||
@@ -356,6 +366,7 @@ export interface ExtractParams {
|
|||||||
applicationName: string;
|
applicationName: string;
|
||||||
applicationVersion: string;
|
applicationVersion: string;
|
||||||
ownerUri: string;
|
ownerUri: string;
|
||||||
|
extractTarget?: ExtractTarget;
|
||||||
taskExecutionMode: azdata.TaskExecutionMode;
|
taskExecutionMode: azdata.TaskExecutionMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onCommand:sqlDatabaseProjects.new",
|
"onCommand:sqlDatabaseProjects.new",
|
||||||
"onCommand:sqlDatabaseProjects.open",
|
"onCommand:sqlDatabaseProjects.open",
|
||||||
|
"onCommand:sqlDatabaseProjects.importDatabase",
|
||||||
"workspaceContains:**/*.sqlproj"
|
"workspaceContains:**/*.sqlproj"
|
||||||
],
|
],
|
||||||
"main": "./out/extension",
|
"main": "./out/extension",
|
||||||
@@ -107,6 +108,11 @@
|
|||||||
"command": "sqlDatabaseProjects.schemaCompare",
|
"command": "sqlDatabaseProjects.schemaCompare",
|
||||||
"title": "%sqlDatabaseProjects.schemaCompare%",
|
"title": "%sqlDatabaseProjects.schemaCompare%",
|
||||||
"category": "%sqlDatabaseProjects.displayName%"
|
"category": "%sqlDatabaseProjects.displayName%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "sqlDatabaseProjects.importDatabase",
|
||||||
|
"title": "%sqlDatabaseProjects.importDatabase%",
|
||||||
|
"category": "%sqlDatabaseProjects.displayName%"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -154,7 +160,8 @@
|
|||||||
"when": "false"
|
"when": "false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "sqlDatabaseProjects.import"
|
"command": "sqlDatabaseProjects.import",
|
||||||
|
"when": "false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "sqlDatabaseProjects.properties",
|
"command": "sqlDatabaseProjects.properties",
|
||||||
@@ -163,6 +170,9 @@
|
|||||||
{
|
{
|
||||||
"command": "sqlDatabaseProjects.schemaCompare",
|
"command": "sqlDatabaseProjects.schemaCompare",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "sqlDatabaseProjects.importDatabase"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
@@ -226,6 +236,13 @@
|
|||||||
"when": "view == sqlDatabaseProjectsView",
|
"when": "view == sqlDatabaseProjectsView",
|
||||||
"group": "9_dbProjectsLast"
|
"group": "9_dbProjectsLast"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"objectExplorer/item/context": [
|
||||||
|
{
|
||||||
|
"command": "sqlDatabaseProjects.importDatabase",
|
||||||
|
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database && mssql:engineedition != 11",
|
||||||
|
"group": "export"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"sqlDatabaseProjects.newItem": "Add Item...",
|
"sqlDatabaseProjects.newItem": "Add Item...",
|
||||||
"sqlDatabaseProjects.newFolder": "Add Folder",
|
"sqlDatabaseProjects.newFolder": "Add Folder",
|
||||||
|
|
||||||
|
"sqlDatabaseProjects.importDatabase": "Import New Database Project",
|
||||||
|
|
||||||
"sqlDatabaseProjects.Settings": "Database Projects",
|
"sqlDatabaseProjects.Settings": "Database Projects",
|
||||||
"sqlDatabaseProjects.netCoreInstallLocation": "Full Path to .Net Core SDK on the machine."
|
"sqlDatabaseProjects.netCoreInstallLocation": "Full Path to .Net Core SDK on the machine."
|
||||||
|
|||||||
@@ -55,10 +55,10 @@
|
|||||||
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets"/>
|
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets"/>
|
||||||
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Properties" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Properties" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export class ApiWrapper {
|
|||||||
return vscode.window.showInputBox(options, token);
|
return vscode.window.showInputBox(options, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public showSaveDialog(options: vscode.SaveDialogOptions): Thenable<vscode.Uri | undefined> {
|
||||||
|
return vscode.window.showSaveDialog(options);
|
||||||
|
}
|
||||||
|
|
||||||
public listDatabases(connectionId: string): Thenable<string[]> {
|
public listDatabases(connectionId: string): Thenable<string[]> {
|
||||||
return azdata.connection.listDatabases(connectionId);
|
return azdata.connection.listDatabases(connectionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ const localize = nls.loadMessageBundle();
|
|||||||
// Placeholder values
|
// Placeholder values
|
||||||
export const dataSourcesFileName = 'datasources.json';
|
export const dataSourcesFileName = 'datasources.json';
|
||||||
export const sqlprojExtension = '.sqlproj';
|
export const sqlprojExtension = '.sqlproj';
|
||||||
|
export const sqlFileExtension = '.sql';
|
||||||
// Commands
|
|
||||||
export const schemaCompareExtensionId = 'microsoft.schema-compare';
|
export const schemaCompareExtensionId = 'microsoft.schema-compare';
|
||||||
export const sqlDatabaseProjectExtensionId = 'microsoft.sql-database-projects';
|
export const sqlDatabaseProjectExtensionId = 'microsoft.sql-database-projects';
|
||||||
export const mssqlExtensionId = 'microsoft.mssql';
|
export const mssqlExtensionId = 'microsoft.mssql';
|
||||||
@@ -24,6 +23,8 @@ export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources
|
|||||||
export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string");
|
export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string");
|
||||||
export const newDatabaseProjectName = localize('newDatabaseProjectName', "New database project name:");
|
export const newDatabaseProjectName = localize('newDatabaseProjectName', "New database project name:");
|
||||||
export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project");
|
export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project");
|
||||||
|
export const extractTargetInput = localize('extractTargetInput', "Target for extraction:");
|
||||||
|
export const selectFileFolder = localize('selectFileFolder', "Select");
|
||||||
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
|
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
|
||||||
|
|
||||||
// Deploy dialog strings
|
// Deploy dialog strings
|
||||||
@@ -55,10 +56,15 @@ export const unknownDataSourceType = localize('unknownDataSourceType', "Unknown
|
|||||||
export const invalidSqlConnectionString = localize('invalidSqlConnectionString', "Invalid SQL connection string");
|
export const invalidSqlConnectionString = localize('invalidSqlConnectionString', "Invalid SQL connection string");
|
||||||
export const projectNameRequired = localize('projectNameRequired', "Name is required to create a new database project.");
|
export const projectNameRequired = localize('projectNameRequired', "Name is required to create a new database project.");
|
||||||
export const projectLocationRequired = localize('projectLocationRequired', "Location is required to create a new database project.");
|
export const projectLocationRequired = localize('projectLocationRequired', "Location is required to create a new database project.");
|
||||||
|
export const projectLocationNotEmpty = localize('projectLocationNotEmpty', "Current project location is not empty. Select an empty folder for precise extraction.");
|
||||||
|
export const extractTargetRequired = localize('extractTargetRequired', "Target information for extract is required to import database to project.");
|
||||||
export const schemaCompareNotInstalled = localize('schemaCompareNotInstalled', "Schema compare extension installation is required to run schema compare");
|
export const schemaCompareNotInstalled = localize('schemaCompareNotInstalled', "Schema compare extension installation is required to run schema compare");
|
||||||
export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac created from build not found");
|
export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac created from build not found");
|
||||||
export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); }
|
export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); }
|
||||||
export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); }
|
export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); }
|
||||||
|
export function noFileExist(fileName: string) { return localize('noFileExist', "File {0} doesn't exist", fileName); }
|
||||||
|
export function cannotResolvePath(path: string) { return localize('cannotResolvePath', "Cannot resolve path {0}", path); }
|
||||||
|
|
||||||
export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); }
|
export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); }
|
||||||
export function projBuildFailed(errorMessage: string) { return localize('projBuildFailed', "Build failed. Check output pane for more details. {0}", errorMessage); }
|
export function projBuildFailed(errorMessage: string) { return localize('projBuildFailed', "Build failed. Check output pane for more details. {0}", errorMessage); }
|
||||||
export function unexpectedProjectContext(uri: string) { return localize('unexpectedProjectContext', "Unable to establish project context. Command invoked from unexpected location: {0}", uri); }
|
export function unexpectedProjectContext(uri: string) { return localize('unexpectedProjectContext', "Unable to establish project context. Command invoked from unexpected location: {0}", uri); }
|
||||||
|
|||||||
@@ -51,6 +51,25 @@ export function trimChars(input: string, chars: string): string {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the folder or file exists @param path path of the folder/file
|
||||||
|
*/
|
||||||
|
export async function exists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(path);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert camelCase input to PascalCase
|
||||||
|
*/
|
||||||
|
export function toPascalCase(input: string): string {
|
||||||
|
return input.charAt(0).toUpperCase() + input.substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get quoted path to be used in any commandline argument
|
* get quoted path to be used in any commandline argument
|
||||||
* @param filePath
|
* @param filePath
|
||||||
@@ -76,15 +95,3 @@ export function getSafeNonWindowsPath(filePath: string): string {
|
|||||||
filePath = filePath.split('\\').join('/').split('"').join('');
|
filePath = filePath.split('\\').join('/').split('"').join('');
|
||||||
return '"' + filePath + '"';
|
return '"' + filePath + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the folder or file exists @param path path of the folder/file
|
|
||||||
*/
|
|
||||||
export async function exists(path: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await fs.access(path);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
import * as templates from '../templates/templates';
|
import * as templates from '../templates/templates';
|
||||||
import * as constants from '../common/constants';
|
import * as constants from '../common/constants';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -61,6 +62,8 @@ export default class MainController implements Disposable {
|
|||||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); });
|
this.apiWrapper.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); });
|
||||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); });
|
this.apiWrapper.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); });
|
||||||
|
|
||||||
|
this.apiWrapper.registerCommand('sqlDatabaseProjects.importDatabase', async (profile: azdata.IConnectionProfile) => { await this.projectsController.importNewDatabaseProject(profile); });
|
||||||
|
|
||||||
// init view
|
// init view
|
||||||
this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider(SQL_DATABASE_PROJECTS_VIEW_ID, this.dbProjectTreeViewProvider));
|
this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider(SQL_DATABASE_PROJECTS_VIEW_ID, this.dbProjectTreeViewProvider));
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import * as utils from '../common/utils';
|
|||||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||||
import * as templates from '../templates/templates';
|
import * as templates from '../templates/templates';
|
||||||
|
|
||||||
import { TaskExecutionMode } from 'azdata';
|
import { Uri, QuickPickItem, WorkspaceFolder, extensions, Extension } from 'vscode';
|
||||||
|
import { IConnectionProfile, TaskExecutionMode } from 'azdata';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { Uri, QuickPickItem, extensions, Extension } from 'vscode';
|
|
||||||
import { ApiWrapper } from '../common/apiWrapper';
|
import { ApiWrapper } from '../common/apiWrapper';
|
||||||
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
|
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
|
||||||
import { Project } from '../models/project';
|
import { Project } from '../models/project';
|
||||||
@@ -22,9 +22,20 @@ import { FolderNode } from '../models/tree/fileFolderTreeItem';
|
|||||||
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
|
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
|
||||||
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
||||||
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
|
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
|
||||||
|
import { ImportDataModel } from '../models/api/import';
|
||||||
import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool';
|
import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool';
|
||||||
import { BuildHelper } from '../tools/buildHelper';
|
import { BuildHelper } from '../tools/buildHelper';
|
||||||
|
|
||||||
|
// TODO: use string enums
|
||||||
|
export enum ExtractTarget {
|
||||||
|
dacpac = 0,
|
||||||
|
file = 1,
|
||||||
|
flat = 2,
|
||||||
|
objectType = 3,
|
||||||
|
schema = 4,
|
||||||
|
schemaObjectType = 5
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for managing project lifecycle
|
* Controller for managing project lifecycle
|
||||||
*/
|
*/
|
||||||
@@ -269,7 +280,7 @@ export class ProjectsController {
|
|||||||
// TODO: file already exists?
|
// TODO: file already exists?
|
||||||
|
|
||||||
const newFileText = this.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName });
|
const newFileText = this.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName });
|
||||||
const relativeFilePath = path.join(relativePath, itemObjectName + '.sql');
|
const relativeFilePath = path.join(relativePath, itemObjectName + constants.sqlFileExtension);
|
||||||
|
|
||||||
const newEntry = await project.addScriptItem(relativeFilePath, newFileText);
|
const newEntry = await project.addScriptItem(relativeFilePath, newFileText);
|
||||||
|
|
||||||
@@ -336,5 +347,218 @@ export class ProjectsController {
|
|||||||
return treeNode instanceof FolderNode ? utils.trimUri(treeNode.root.uri, treeNode.uri) : '';
|
return treeNode instanceof FolderNode ? utils.trimUri(treeNode.root.uri, treeNode.uri) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a new SQL database project from the existing database,
|
||||||
|
* prompting the user for a name, file path location and extract target
|
||||||
|
*/
|
||||||
|
public async importNewDatabaseProject(context: any): Promise<void> {
|
||||||
|
let model = <ImportDataModel>{};
|
||||||
|
|
||||||
|
// TODO: Refactor code
|
||||||
|
try {
|
||||||
|
let profile = context ? <IConnectionProfile>context.connectionProfile : undefined;
|
||||||
|
//TODO: Prompt for new connection addition and get database information if context information isn't provided.
|
||||||
|
if (profile) {
|
||||||
|
model.serverId = profile.id;
|
||||||
|
model.database = profile.databaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get project name
|
||||||
|
let newProjName = await this.getProjectName(model.database);
|
||||||
|
if (!newProjName) {
|
||||||
|
throw new Error(constants.projectNameRequired);
|
||||||
|
}
|
||||||
|
model.projName = newProjName;
|
||||||
|
|
||||||
|
// Get extractTarget
|
||||||
|
let extractTarget: mssql.ExtractTarget = await this.getExtractTarget();
|
||||||
|
model.extractTarget = extractTarget;
|
||||||
|
|
||||||
|
// Get folder location for project creation
|
||||||
|
let newProjUri = await this.getFolderLocation(model.extractTarget);
|
||||||
|
if (!newProjUri) {
|
||||||
|
throw new Error(constants.projectLocationRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set project folder/file location
|
||||||
|
let newProjFolderUri;
|
||||||
|
if (extractTarget === mssql.ExtractTarget['file']) {
|
||||||
|
// Get folder info, if extractTarget = File
|
||||||
|
newProjFolderUri = Uri.file(path.dirname(newProjUri.fsPath));
|
||||||
|
} else {
|
||||||
|
newProjFolderUri = newProjUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check folder is empty
|
||||||
|
let isEmpty: boolean = await this.isDirEmpty(newProjFolderUri.fsPath);
|
||||||
|
if (!isEmpty) {
|
||||||
|
throw new Error(constants.projectLocationNotEmpty);
|
||||||
|
}
|
||||||
|
// TODO: what if the selected folder is outside the workspace?
|
||||||
|
model.filePath = newProjUri.fsPath;
|
||||||
|
|
||||||
|
//Set model version
|
||||||
|
model.version = '1.0.0.0';
|
||||||
|
|
||||||
|
// Call ExtractAPI in DacFx Service
|
||||||
|
await this.importApiCall(model);
|
||||||
|
// TODO: Check for success
|
||||||
|
|
||||||
|
// Create and open new project
|
||||||
|
const newProjFilePath = await this.createNewProject(newProjName as string, newProjFolderUri as Uri);
|
||||||
|
const project = await this.openProject(Uri.file(newProjFilePath));
|
||||||
|
|
||||||
|
//Create a list of all the files and directories to be added to project
|
||||||
|
let fileFolderList: string[] = await this.generateList(model.filePath);
|
||||||
|
|
||||||
|
// Add generated file structure to the project
|
||||||
|
await project.addToProject(fileFolderList);
|
||||||
|
|
||||||
|
//Refresh project to show the added files
|
||||||
|
this.refreshProjectsTree();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.apiWrapper.showErrorMessage(utils.getErrorMessage(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectName(dbName: string): Promise<string | undefined> {
|
||||||
|
let projName = await this.apiWrapper.showInputBox({
|
||||||
|
prompt: constants.newDatabaseProjectName,
|
||||||
|
value: `DatabaseProject${dbName}`
|
||||||
|
});
|
||||||
|
|
||||||
|
projName = projName?.trim();
|
||||||
|
|
||||||
|
return projName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapExtractTargetEnum(inputTarget: any): mssql.ExtractTarget {
|
||||||
|
if (inputTarget) {
|
||||||
|
switch (inputTarget) {
|
||||||
|
case 'File': return mssql.ExtractTarget['file'];
|
||||||
|
case 'Flat': return mssql.ExtractTarget['flat'];
|
||||||
|
case 'ObjectType': return mssql.ExtractTarget['objectType'];
|
||||||
|
case 'Schema': return mssql.ExtractTarget['schema'];
|
||||||
|
case 'SchemaObjectType': return mssql.ExtractTarget['schemaObjectType'];
|
||||||
|
default: throw new Error(`Invalid input: ${inputTarget}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(constants.extractTargetRequired);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getExtractTarget(): Promise<mssql.ExtractTarget> {
|
||||||
|
let extractTarget: mssql.ExtractTarget;
|
||||||
|
|
||||||
|
let extractTargetOptions: QuickPickItem[] = [];
|
||||||
|
|
||||||
|
let keys: string[] = Object.keys(ExtractTarget).filter(k => typeof ExtractTarget[k as any] === 'number');
|
||||||
|
|
||||||
|
// TODO: Create a wrapper class to handle the mapping
|
||||||
|
keys.forEach((targetOption: string) => {
|
||||||
|
if (targetOption !== 'dacpac') { //Do not present the option to create Dacpac
|
||||||
|
let pascalCaseTargetOption: string = utils.toPascalCase(targetOption); // for better readability
|
||||||
|
extractTargetOptions.push({ label: pascalCaseTargetOption });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let input = await this.apiWrapper.showQuickPick(extractTargetOptions, {
|
||||||
|
canPickMany: false,
|
||||||
|
placeHolder: constants.extractTargetInput
|
||||||
|
});
|
||||||
|
let extractTargetInput = input?.label;
|
||||||
|
|
||||||
|
extractTarget = this.mapExtractTargetEnum(extractTargetInput);
|
||||||
|
|
||||||
|
return extractTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFolderLocation(extractTarget: mssql.ExtractTarget): Promise<Uri | undefined> {
|
||||||
|
let selectionResult;
|
||||||
|
let projUri;
|
||||||
|
|
||||||
|
if (extractTarget !== mssql.ExtractTarget.file) {
|
||||||
|
selectionResult = await this.apiWrapper.showOpenDialog({
|
||||||
|
canSelectFiles: false,
|
||||||
|
canSelectFolders: true,
|
||||||
|
canSelectMany: false,
|
||||||
|
openLabel: constants.selectFileFolder,
|
||||||
|
defaultUri: this.apiWrapper.workspaceFolders() ? (this.apiWrapper.workspaceFolders() as WorkspaceFolder[])[0].uri : undefined
|
||||||
|
});
|
||||||
|
if (selectionResult) {
|
||||||
|
projUri = (selectionResult as Uri[])[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get filename
|
||||||
|
selectionResult = await this.apiWrapper.showSaveDialog(
|
||||||
|
{
|
||||||
|
defaultUri: this.apiWrapper.workspaceFolders() ? (this.apiWrapper.workspaceFolders() as WorkspaceFolder[])[0].uri : undefined,
|
||||||
|
saveLabel: constants.selectFileFolder,
|
||||||
|
filters: {
|
||||||
|
'SQL files': ['sql'],
|
||||||
|
'All files': ['*']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (selectionResult) {
|
||||||
|
projUri = selectionResult as unknown as Uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isDirEmpty(newProjFolderUri: string): Promise<boolean> {
|
||||||
|
return (await fs.readdir(newProjFolderUri)).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async importApiCall(model: ImportDataModel): Promise<void> {
|
||||||
|
let ext = this.apiWrapper.getExtension(mssql.extension.name)!;
|
||||||
|
|
||||||
|
const service = (await ext.activate() as mssql.IExtension).dacFx;
|
||||||
|
const ownerUri = await this.apiWrapper.getUriForConnection(model.serverId);
|
||||||
|
|
||||||
|
await service.importDatabaseProject(model.database, model.filePath, model.projName, model.version, ownerUri, model.extractTarget, TaskExecutionMode.execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a flat list of all files and folder under a folder.
|
||||||
|
*/
|
||||||
|
public async generateList(absolutePath: string): Promise<string[]> {
|
||||||
|
let fileFolderList: string[] = [];
|
||||||
|
|
||||||
|
if (!await utils.exists(absolutePath)) {
|
||||||
|
if (await utils.exists(absolutePath + constants.sqlFileExtension)) {
|
||||||
|
absolutePath += constants.sqlFileExtension;
|
||||||
|
} else {
|
||||||
|
await this.apiWrapper.showErrorMessage(constants.cannotResolvePath(absolutePath));
|
||||||
|
return fileFolderList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [absolutePath];
|
||||||
|
do {
|
||||||
|
const filepath = files.pop();
|
||||||
|
|
||||||
|
if (filepath) {
|
||||||
|
const stat = await fs.stat(filepath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
fileFolderList.push(filepath);
|
||||||
|
(await fs
|
||||||
|
.readdir(filepath))
|
||||||
|
.forEach((f: string) => files.push(path.join(filepath, f)));
|
||||||
|
}
|
||||||
|
else if (stat.isFile()) {
|
||||||
|
fileFolderList.push(filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (files.length !== 0);
|
||||||
|
|
||||||
|
return fileFolderList;
|
||||||
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
18
extensions/sql-database-projects/src/models/api/import.ts
Normal file
18
extensions/sql-database-projects/src/models/api/import.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { ExtractTarget } from '../../../../mssql';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data model to communicate for Import API
|
||||||
|
*/
|
||||||
|
export interface ImportDataModel {
|
||||||
|
serverId: string;
|
||||||
|
database: string;
|
||||||
|
projName: string;
|
||||||
|
filePath: string;
|
||||||
|
version: string;
|
||||||
|
extractTarget: ExtractTarget;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as xmldom from 'xmldom';
|
import * as xmldom from 'xmldom';
|
||||||
import * as constants from '../common/constants';
|
import * as constants from '../common/constants';
|
||||||
|
import * as utils from '../common/utils';
|
||||||
|
|
||||||
import { Uri } from 'vscode';
|
import { Uri } from 'vscode';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
@@ -60,7 +61,12 @@ export class Project {
|
|||||||
*/
|
*/
|
||||||
public async addFolderItem(relativeFolderPath: string): Promise<ProjectEntry> {
|
public async addFolderItem(relativeFolderPath: string): Promise<ProjectEntry> {
|
||||||
const absoluteFolderPath = path.join(this.projectFolderPath, relativeFolderPath);
|
const absoluteFolderPath = path.join(this.projectFolderPath, relativeFolderPath);
|
||||||
await fs.mkdir(absoluteFolderPath, { recursive: true });
|
|
||||||
|
//If folder doesn't exist, create it
|
||||||
|
let exists = await utils.exists(absoluteFolderPath);
|
||||||
|
if (!exists) {
|
||||||
|
await fs.mkdir(absoluteFolderPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const folderEntry = this.createProjectEntry(relativeFolderPath, EntryType.Folder);
|
const folderEntry = this.createProjectEntry(relativeFolderPath, EntryType.Folder);
|
||||||
this.files.push(folderEntry);
|
this.files.push(folderEntry);
|
||||||
@@ -70,14 +76,23 @@ export class Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a file to disk, adds that file to the project, and writes it to disk
|
* Writes a file to disk if contents are provided, adds that file to the project, and writes it to disk
|
||||||
* @param relativeFilePath Relative path of the file
|
* @param relativeFilePath Relative path of the file
|
||||||
* @param contents Contents to be written to the new file
|
* @param contents Contents to be written to the new file
|
||||||
*/
|
*/
|
||||||
public async addScriptItem(relativeFilePath: string, contents: string): Promise<ProjectEntry> {
|
public async addScriptItem(relativeFilePath: string, contents?: string): Promise<ProjectEntry> {
|
||||||
const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath);
|
const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath);
|
||||||
await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true });
|
|
||||||
await fs.writeFile(absoluteFilePath, contents);
|
if (contents) {
|
||||||
|
await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true });
|
||||||
|
await fs.writeFile(absoluteFilePath, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check that file actually exists
|
||||||
|
let exists = await utils.exists(absoluteFilePath);
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(constants.noFileExist(absoluteFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
const fileEntry = this.createProjectEntry(relativeFilePath, EntryType.File);
|
const fileEntry = this.createProjectEntry(relativeFilePath, EntryType.File);
|
||||||
this.files.push(fileEntry);
|
this.files.push(fileEntry);
|
||||||
@@ -145,6 +160,29 @@ export class Project {
|
|||||||
|
|
||||||
await fs.writeFile(this.projectFilePath, xml);
|
await fs.writeFile(this.projectFilePath, xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the list of sql files and directories to the project, and saves the project file
|
||||||
|
* @param absolutePath Absolute path of the folder
|
||||||
|
*/
|
||||||
|
public async addToProject(list: string[]): Promise<void> {
|
||||||
|
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
let file: string = list[i];
|
||||||
|
const relativePath = utils.trimChars(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(file)), '/');
|
||||||
|
|
||||||
|
if (relativePath.length > 0) {
|
||||||
|
let fileStat = await fs.stat(file);
|
||||||
|
|
||||||
|
if (fileStat.isFile() && file.toLowerCase().endsWith(constants.sqlFileExtension)) {
|
||||||
|
await this.addScriptItem(relativePath);
|
||||||
|
}
|
||||||
|
else if (fileStat.isDirectory()) {
|
||||||
|
await this.addFolderItem(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -55,10 +55,10 @@
|
|||||||
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets"/>
|
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets"/>
|
||||||
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Properties" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Properties" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -52,8 +52,12 @@
|
|||||||
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
|
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
|
||||||
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
|
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Condition="'$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets"/>
|
||||||
<Import Condition="'$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
|
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Properties" />
|
<Folder Include="Properties" />
|
||||||
<Folder Include="Tables" />
|
<Folder Include="Tables" />
|
||||||
|
|||||||
@@ -54,4 +54,17 @@ describe('Project: sqlproj content operations', function (): void {
|
|||||||
|
|
||||||
should (newFileContents).equal(fileContents);
|
should (newFileContents).equal(fileContents);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should add Folder and Build entries to sqlproj with pre-existing scripts on disk', async function (): Promise<void> {
|
||||||
|
projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
|
||||||
|
const project: Project = new Project(projFilePath);
|
||||||
|
await project.readProjFile();
|
||||||
|
|
||||||
|
let list: string[] = await testUtils.createListOfFiles(path.dirname(projFilePath));
|
||||||
|
|
||||||
|
await project.addToProject(list);
|
||||||
|
|
||||||
|
should(project.files.filter(f => f.type === EntryType.File).length).equal(11); // txt file shouldn't be added to the project
|
||||||
|
should(project.files.filter(f => f.type === EntryType.Folder).length).equal(3); // 2folders + default Properties folder
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,11 +6,13 @@
|
|||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as TypeMoq from 'typemoq';
|
import * as TypeMoq from 'typemoq';
|
||||||
import * as baselines from './baselines/baselines';
|
import * as baselines from './baselines/baselines';
|
||||||
import * as templates from '../templates/templates';
|
import * as templates from '../templates/templates';
|
||||||
import * as testUtils from './testUtils';
|
import * as testUtils from './testUtils';
|
||||||
|
import * as constants from '../common/constants';
|
||||||
|
|
||||||
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
|
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
|
||||||
import { ProjectsController } from '../controllers/projectController';
|
import { ProjectsController } from '../controllers/projectController';
|
||||||
@@ -23,6 +25,23 @@ import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymen
|
|||||||
|
|
||||||
let testContext: TestContext;
|
let testContext: TestContext;
|
||||||
|
|
||||||
|
// Mock test data
|
||||||
|
const mockConnectionProfile: azdata.IConnectionProfile = {
|
||||||
|
connectionName: 'My Connection',
|
||||||
|
serverName: 'My Server',
|
||||||
|
databaseName: 'My Database',
|
||||||
|
userName: 'My User',
|
||||||
|
password: 'My Pwd',
|
||||||
|
authenticationType: 'SqlLogin',
|
||||||
|
savePassword: false,
|
||||||
|
groupFullName: 'My groupName',
|
||||||
|
groupId: 'My GroupId',
|
||||||
|
providerName: 'My Server',
|
||||||
|
saveProfile: true,
|
||||||
|
id: 'My Id',
|
||||||
|
options: undefined as any
|
||||||
|
};
|
||||||
|
|
||||||
describe('ProjectsController: project controller operations', function (): void {
|
describe('ProjectsController: project controller operations', function (): void {
|
||||||
before(async function (): Promise<void> {
|
before(async function (): Promise<void> {
|
||||||
testContext = createContext();
|
testContext = createContext();
|
||||||
@@ -126,3 +145,64 @@ describe('ProjectsController: project controller operations', function (): void
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ProjectsController: import operations', function (): void {
|
||||||
|
it('Should create list of all files and folders correctly', async function (): Promise<void> {
|
||||||
|
const testFolderPath = await testUtils.createDummyFileStructure();
|
||||||
|
|
||||||
|
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||||
|
const fileList = await projController.generateList(testFolderPath);
|
||||||
|
|
||||||
|
should(fileList.length).equal(15); // Parent folder + 2 files under parent folder + 2 directories with 5 files each
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should error out for inaccessible path', async function (): Promise<void> {
|
||||||
|
let testFolderPath = await testUtils.generateTestFolderPath();
|
||||||
|
testFolderPath += '_nonExistant'; // Modify folder path to point to a non-existant location
|
||||||
|
|
||||||
|
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||||
|
|
||||||
|
await testUtils.shouldThrowSpecificError(async () => await projController.generateList(testFolderPath), constants.cannotResolvePath(testFolderPath));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show error when no project name provided', async function (): Promise<void> {
|
||||||
|
for (const name of ['', ' ', undefined]) {
|
||||||
|
testContext.apiWrapper.reset();
|
||||||
|
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name));
|
||||||
|
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
|
||||||
|
|
||||||
|
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||||
|
await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject(mockConnectionProfile), constants.projectNameRequired, `case: '${name}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show error when no target information provided', async function (): Promise<void> {
|
||||||
|
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName'));
|
||||||
|
testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
|
||||||
|
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
|
||||||
|
|
||||||
|
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||||
|
await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject(mockConnectionProfile), constants.extractTargetRequired);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show error when no location provided with ExtractTarget = File', async function (): Promise<void> {
|
||||||
|
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName'));
|
||||||
|
testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({label: 'File'}));
|
||||||
|
testContext.apiWrapper.setup(x => x.showSaveDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
|
||||||
|
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
|
||||||
|
|
||||||
|
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||||
|
await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject(mockConnectionProfile), constants.projectLocationRequired);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show error when no location provided with ExtractTarget = SchemaObjectType', async function (): Promise<void> {
|
||||||
|
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName'));
|
||||||
|
testContext.apiWrapper.setup(x => x.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve({label: 'SchemaObjectType'}));
|
||||||
|
testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
|
||||||
|
testContext.apiWrapper.setup(x => x.workspaceFolders()).returns(() => undefined);
|
||||||
|
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
|
||||||
|
|
||||||
|
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||||
|
await testUtils.shouldThrowSpecificError(async () => await projController.importNewDatabaseProject(mockConnectionProfile), constants.projectLocationRequired);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -55,3 +55,65 @@ async function createTestFile(contents: string, fileName: string, folderPath?: s
|
|||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* TestFolder directory structure
|
||||||
|
* - file1.sql
|
||||||
|
* - folder1
|
||||||
|
* -file1.sql
|
||||||
|
* -file2.sql
|
||||||
|
* -file3.sql
|
||||||
|
* -file4.sql
|
||||||
|
* -file5.sql
|
||||||
|
* - folder2
|
||||||
|
* -file1.sql
|
||||||
|
* -file2.sql
|
||||||
|
* -file3.sql
|
||||||
|
* -file4.sql
|
||||||
|
* -file5.sql
|
||||||
|
* - file2.txt
|
||||||
|
*
|
||||||
|
* @param createList Boolean specifying to create a list of the files and folders been created
|
||||||
|
* @param list List of files and folders that are been created
|
||||||
|
*/
|
||||||
|
export async function createDummyFileStructure(createList?: boolean, list?: string[], testFolderPath?: string): Promise<string> {
|
||||||
|
testFolderPath = testFolderPath ?? await generateTestFolderPath();
|
||||||
|
|
||||||
|
let filePath = path.join(testFolderPath, 'file1.sql');
|
||||||
|
await fs.open(filePath, 'w');
|
||||||
|
if (createList) {
|
||||||
|
list?.push(testFolderPath);
|
||||||
|
list?.push(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let dirCount = 1; dirCount <= 2; dirCount++) {
|
||||||
|
let dirName = path.join(testFolderPath, `folder${dirCount}`);
|
||||||
|
await fs.mkdir(dirName, { recursive: true });
|
||||||
|
if (createList) {
|
||||||
|
list?.push(dirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let fileCount = 1; fileCount <= 5; fileCount++) {
|
||||||
|
let fileName = path.join(dirName, `file${fileCount}.sql`);
|
||||||
|
await fs.open(fileName, 'w');
|
||||||
|
if (createList) {
|
||||||
|
list?.push(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = path.join(testFolderPath, 'file2.txt');
|
||||||
|
await fs.open(filePath, 'w');
|
||||||
|
if (createList) {
|
||||||
|
list?.push(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return testFolderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createListOfFiles(filePath?: string): Promise<string[]> {
|
||||||
|
let fileFolderList: string[] = [];
|
||||||
|
|
||||||
|
await createDummyFileStructure(true, fileFolderList, filePath);
|
||||||
|
|
||||||
|
return fileFolderList;
|
||||||
|
}
|
||||||
|
|||||||
30
extensions/sql-database-projects/src/test/utils.test.ts
Normal file
30
extensions/sql-database-projects/src/test/utils.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 {createDummyFileStructure} from './testUtils';
|
||||||
|
import {toPascalCase, exists} from '../common/utils';
|
||||||
|
|
||||||
|
describe('Tests for conversion within PascalCase and camelCase', function (): void {
|
||||||
|
it('Should generate PascalCase from camelCase correctly', async () => {
|
||||||
|
should(toPascalCase('')).equal('');
|
||||||
|
should(toPascalCase('camelCase')).equal('CamelCase');
|
||||||
|
should(toPascalCase('camel.case')).equal('Camel.case');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tests to verify exists function', function (): void {
|
||||||
|
it('Should determine existance of files/folders', async () => {
|
||||||
|
let testFolderPath = await createDummyFileStructure();
|
||||||
|
|
||||||
|
should(await exists(testFolderPath)).equal(true);
|
||||||
|
should(await exists(path.join(testFolderPath, 'file1.sql'))).equal(true);
|
||||||
|
should(await exists(path.join(testFolderPath, 'folder2'))).equal(true);
|
||||||
|
should(await exists(path.join(testFolderPath, 'folder4'))).equal(false);
|
||||||
|
should(await exists(path.join(testFolderPath, 'folder2','file4.sql'))).equal(true);
|
||||||
|
should(await exists(path.join(testFolderPath, 'folder4','file2.sql'))).equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,6 +17,7 @@ export const IMPORT_COMMAND_ID = 'dataExplorer.flatFileImport';
|
|||||||
export const SCHEMA_COMPARE_COMMAND_ID = 'dataExplorer.schemaCompare';
|
export const SCHEMA_COMPARE_COMMAND_ID = 'dataExplorer.schemaCompare';
|
||||||
export const GENERATE_SCRIPTS_COMMAND_ID = 'dataExplorer.generateScripts';
|
export const GENERATE_SCRIPTS_COMMAND_ID = 'dataExplorer.generateScripts';
|
||||||
export const PROPERTIES_COMMAND_ID = 'dataExplorer.properties';
|
export const PROPERTIES_COMMAND_ID = 'dataExplorer.properties';
|
||||||
|
export const IMPORT_DATABASE_COMMAND_ID = 'dataExplorer.importDatabase';
|
||||||
|
|
||||||
|
|
||||||
// Data Tier Wizard
|
// Data Tier Wizard
|
||||||
@@ -98,3 +99,13 @@ CommandsRegistry.registerCommand({
|
|||||||
return commandService.executeCommand('adminToolExtWin.launchSsmsMinPropertiesDialog', objectExplorerContext);
|
return commandService.executeCommand('adminToolExtWin.launchSsmsMinPropertiesDialog', objectExplorerContext);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Import Database
|
||||||
|
CommandsRegistry.registerCommand({
|
||||||
|
id: IMPORT_DATABASE_COMMAND_ID,
|
||||||
|
handler: (accessor, args: TreeViewItemHandleArg) => {
|
||||||
|
const commandService = accessor.get(ICommandService);
|
||||||
|
let connectedContext: azdata.ConnectedContext = { connectionProfile: args.$treeItem.payload };
|
||||||
|
return commandService.executeCommand('sqlDatabaseProjects.importDatabase', connectedContext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||||
import { DATA_TIER_WIZARD_COMMAND_ID, PROFILER_COMMAND_ID, IMPORT_COMMAND_ID, SCHEMA_COMPARE_COMMAND_ID, GENERATE_SCRIPTS_COMMAND_ID, PROPERTIES_COMMAND_ID } from 'sql/workbench/contrib/dataExplorer/browser/extensionActions';
|
import { DATA_TIER_WIZARD_COMMAND_ID, PROFILER_COMMAND_ID, IMPORT_COMMAND_ID, SCHEMA_COMPARE_COMMAND_ID, GENERATE_SCRIPTS_COMMAND_ID, PROPERTIES_COMMAND_ID, IMPORT_DATABASE_COMMAND_ID } from 'sql/workbench/contrib/dataExplorer/browser/extensionActions';
|
||||||
import { ContextKeyExpr, ContextKeyRegexExpr } from 'vs/platform/contextkey/common/contextkey';
|
import { ContextKeyExpr, ContextKeyRegexExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||||
import { MssqlNodeContext } from 'sql/workbench/services/objectExplorer/browser/mssqlNodeContext';
|
import { MssqlNodeContext } from 'sql/workbench/services/objectExplorer/browser/mssqlNodeContext';
|
||||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||||
@@ -38,10 +38,22 @@ MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
|||||||
MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Import Database
|
||||||
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
|
group: 'export',
|
||||||
|
order: 8,
|
||||||
|
command: {
|
||||||
|
id: IMPORT_DATABASE_COMMAND_ID,
|
||||||
|
title: localize('importDatabase', "Import New Database Project")
|
||||||
|
},
|
||||||
|
when: ContextKeyExpr.and(MssqlNodeContext.NodeProvider.isEqualTo(mssqlProviderName),
|
||||||
|
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
||||||
|
});
|
||||||
|
|
||||||
// Profiler
|
// Profiler
|
||||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
group: 'profiler',
|
group: 'profiler',
|
||||||
order: 8,
|
order: 9,
|
||||||
command: {
|
command: {
|
||||||
id: PROFILER_COMMAND_ID,
|
id: PROFILER_COMMAND_ID,
|
||||||
title: localize('profiler', "Launch Profiler")
|
title: localize('profiler', "Launch Profiler")
|
||||||
@@ -50,22 +62,10 @@ MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
|||||||
MssqlNodeContext.NodeType.isEqualTo(NodeType.Server), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
MssqlNodeContext.NodeType.isEqualTo(NodeType.Server), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Flat File Import
|
|
||||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
|
||||||
group: 'import',
|
|
||||||
order: 10,
|
|
||||||
command: {
|
|
||||||
id: IMPORT_COMMAND_ID,
|
|
||||||
title: localize('flatFileImport', "Import Wizard")
|
|
||||||
},
|
|
||||||
when: ContextKeyExpr.and(MssqlNodeContext.NodeProvider.isEqualTo(mssqlProviderName),
|
|
||||||
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schema Compare
|
// Schema Compare
|
||||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
group: 'export',
|
group: 'export',
|
||||||
order: 9,
|
order: 10,
|
||||||
command: {
|
command: {
|
||||||
id: SCHEMA_COMPARE_COMMAND_ID,
|
id: SCHEMA_COMPARE_COMMAND_ID,
|
||||||
title: localize('schemaCompare', "Schema Compare")
|
title: localize('schemaCompare', "Schema Compare")
|
||||||
@@ -74,10 +74,22 @@ MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
|||||||
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Flat File Import
|
||||||
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
|
group: 'import',
|
||||||
|
order: 11,
|
||||||
|
command: {
|
||||||
|
id: IMPORT_COMMAND_ID,
|
||||||
|
title: localize('flatFileImport', "Import Wizard")
|
||||||
|
},
|
||||||
|
when: ContextKeyExpr.and(MssqlNodeContext.NodeProvider.isEqualTo(mssqlProviderName),
|
||||||
|
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database))
|
||||||
|
});
|
||||||
|
|
||||||
// Generate Scripts Action
|
// Generate Scripts Action
|
||||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
group: 'z-AdminToolExt@1',
|
group: 'z-AdminToolExt@1',
|
||||||
order: 11,
|
order: 12,
|
||||||
command: {
|
command: {
|
||||||
id: GENERATE_SCRIPTS_COMMAND_ID,
|
id: GENERATE_SCRIPTS_COMMAND_ID,
|
||||||
title: localize('generateScripts', "Generate Scripts...")
|
title: localize('generateScripts', "Generate Scripts...")
|
||||||
@@ -90,7 +102,7 @@ MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
|||||||
// Properties Action
|
// Properties Action
|
||||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
group: 'z-AdminToolExt@2',
|
group: 'z-AdminToolExt@2',
|
||||||
order: 12,
|
order: 13,
|
||||||
command: {
|
command: {
|
||||||
id: PROPERTIES_COMMAND_ID,
|
id: PROPERTIES_COMMAND_ID,
|
||||||
title: localize('properties', "Properties")
|
title: localize('properties', "Properties")
|
||||||
@@ -102,7 +114,7 @@ MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
|||||||
|
|
||||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||||
group: 'z-AdminToolExt@2',
|
group: 'z-AdminToolExt@2',
|
||||||
order: 12,
|
order: 13,
|
||||||
command: {
|
command: {
|
||||||
id: PROPERTIES_COMMAND_ID,
|
id: PROPERTIES_COMMAND_ID,
|
||||||
title: localize('properties', "Properties")
|
title: localize('properties', "Properties")
|
||||||
|
|||||||
Reference in New Issue
Block a user