mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Add Detach Database option to database context menu (#23480)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "4.8.0.29",
|
||||
"version": "4.8.0.31",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-net7.0.zip",
|
||||
"Windows_64": "win-x64-net7.0.zip",
|
||||
|
||||
@@ -101,6 +101,11 @@
|
||||
"category": "MSSQL",
|
||||
"title": "%title.renameObject%"
|
||||
},
|
||||
{
|
||||
"command": "mssql.detachDatabase",
|
||||
"category": "MSSQL",
|
||||
"title": "%title.detachDatabase%"
|
||||
},
|
||||
{
|
||||
"command": "mssql.enableGroupBySchema",
|
||||
"category": "MSSQL",
|
||||
@@ -542,6 +547,11 @@
|
||||
"when": "connectionProvider == MSSQL && nodeType == Column && config.workbench.enablePreviewFeatures && nodePath =~ /^.*\\/Tables\\/.*\\/Columns\\/.*$/",
|
||||
"group": "0_query@3"
|
||||
},
|
||||
{
|
||||
"command": "mssql.detachDatabase",
|
||||
"when": "connectionProvider == MSSQL && nodeType == Database && !isCloud && config.workbench.enablePreviewFeatures",
|
||||
"group": "0_query@4"
|
||||
},
|
||||
{
|
||||
"command": "mssql.enableGroupBySchema",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema"
|
||||
|
||||
@@ -187,5 +187,6 @@
|
||||
"title.newObject": "New",
|
||||
"title.objectProperties": "Properties (Preview)",
|
||||
"title.deleteObject": "Delete",
|
||||
"title.renameObject": "Rename"
|
||||
"title.renameObject": "Rename",
|
||||
"title.detachDatabase": "Detach"
|
||||
}
|
||||
|
||||
@@ -1628,6 +1628,18 @@ export namespace SearchObjectRequest {
|
||||
export const type = new RequestType<SearchObjectRequestParams, mssql.ObjectManagement.SearchResultItem[], void, void>('objectManagement/search');
|
||||
}
|
||||
|
||||
export interface DetachDatabaseRequestParams {
|
||||
connectionUri: string;
|
||||
objectUrn: string;
|
||||
dropConnections: boolean;
|
||||
updateStatistics: boolean;
|
||||
generateScript: boolean;
|
||||
}
|
||||
|
||||
export namespace DetachDatabaseRequest {
|
||||
export const type = new RequestType<DetachDatabaseRequestParams, string, void, void>('objectManagement/detachDatabase');
|
||||
}
|
||||
|
||||
// ------------------------------- < Object Management > ------------------------------------
|
||||
|
||||
// ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------
|
||||
|
||||
10
extensions/mssql/src/mssql.d.ts
vendored
10
extensions/mssql/src/mssql.d.ts
vendored
@@ -974,6 +974,16 @@ declare module 'mssql' {
|
||||
* @param schema Schema to search in.
|
||||
*/
|
||||
search(contextId: string, objectTypes: string[], searchText?: string, schema?: string): Thenable<ObjectManagement.SearchResultItem[]>;
|
||||
/**
|
||||
* Detach a database.
|
||||
* @param connectionUri The URI of the server connection.
|
||||
* @param objectUrn SMO Urn of the database to be detached. More information: https://learn.microsoft.com/sql/relational-databases/server-management-objects-smo/overview-smo
|
||||
* @param dropConnections Whether to drop active connections to this database.
|
||||
* @param updateStatistics Whether to update the optimization statistics related to this database.
|
||||
* @param generateScript Whether to generate a TSQL script for the operation instead of detaching the database.
|
||||
* @returns A string value representing the generated TSQL query if generateScript was set to true, and an empty string otherwise.
|
||||
*/
|
||||
detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Thenable<string>;
|
||||
}
|
||||
// Object Management - End.
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DatabaseRoleDialog } from './ui/databaseRoleDialog';
|
||||
import { ApplicationRoleDialog } from './ui/applicationRoleDialog';
|
||||
import { DatabaseDialog } from './ui/databaseDialog';
|
||||
import { ServerPropertiesDialog } from './ui/serverPropertiesDialog';
|
||||
import { DetachDatabaseDialog } from './ui/detachDatabaseDialog';
|
||||
|
||||
export function registerObjectManagementCommands(appContext: AppContext) {
|
||||
// Notes: Change the second parameter to false to use the actual object management service.
|
||||
@@ -39,6 +40,9 @@ export function registerObjectManagementCommands(appContext: AppContext) {
|
||||
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.renameObject', async (context: azdata.ObjectExplorerContext) => {
|
||||
await handleRenameObjectCommand(context, service);
|
||||
}));
|
||||
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.detachDatabase', async (context: azdata.ObjectExplorerContext) => {
|
||||
await handleDetachDatabase(context, service);
|
||||
}));
|
||||
}
|
||||
|
||||
function getObjectManagementService(appContext: AppContext, useTestService: boolean): IObjectManagementService {
|
||||
@@ -237,6 +241,35 @@ async function handleRenameObjectCommand(context: azdata.ObjectExplorerContext,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDetachDatabase(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
|
||||
const connectionUri = await getConnectionUri(context);
|
||||
if (!connectionUri) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parentUrn = await getParentUrn(context);
|
||||
const options: ObjectManagementDialogOptions = {
|
||||
connectionUri: connectionUri,
|
||||
isNewObject: false,
|
||||
database: context.connectionProfile!.databaseName!,
|
||||
objectType: context.nodeInfo.nodeType as ObjectManagement.NodeType,
|
||||
objectName: context.nodeInfo.label,
|
||||
parentUrn: parentUrn,
|
||||
objectUrn: context.nodeInfo!.metadata!.urn,
|
||||
objectExplorerContext: context
|
||||
};
|
||||
const dialog = new DetachDatabaseDialog(service, options);
|
||||
await dialog.open();
|
||||
}
|
||||
catch (err) {
|
||||
TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.OpenDetachDatabaseDialog, err).withAdditionalProperties({
|
||||
objectType: context.nodeInfo!.nodeType
|
||||
}).send();
|
||||
console.error(err);
|
||||
await vscode.window.showErrorMessage(objectManagementLoc.OpenDetachDatabaseDialogError(getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
function getDialog(service: IObjectManagementService, dialogOptions: ObjectManagementDialogOptions): ObjectManagementDialogBase<ObjectManagement.SqlObject, ObjectManagement.ObjectViewInfo<ObjectManagement.SqlObject>> {
|
||||
switch (dialogOptions.objectType) {
|
||||
case ObjectManagement.NodeType.ApplicationRole:
|
||||
|
||||
@@ -29,6 +29,7 @@ export const CreateDatabaseRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/s
|
||||
export const AlterDatabaseRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-role-transact-sql';
|
||||
export const CreateDatabaseDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-database-transact-sql';
|
||||
export const ViewServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql';
|
||||
export const DetachDatabaseDocUrl = 'https://go.microsoft.com/fwlink/?linkid=2240322';
|
||||
export const DatabasePropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-general-page';
|
||||
|
||||
export const enum TelemetryActions {
|
||||
@@ -37,7 +38,8 @@ export const enum TelemetryActions {
|
||||
OpenNewObjectDialog = 'OpenNewObjectDialog',
|
||||
OpenPropertiesDialog = 'OpenPropertiesDialog',
|
||||
RenameObject = 'RenameObject',
|
||||
UpdateObject = 'UpdateObject'
|
||||
UpdateObject = 'UpdateObject',
|
||||
OpenDetachDatabaseDialog = 'OpenDetachDatabaseDialog'
|
||||
}
|
||||
|
||||
export const ObjectManagementViewName = 'ObjectManagement';
|
||||
|
||||
@@ -451,6 +451,7 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo<Databa
|
||||
compatibilityLevels: string[];
|
||||
containmentTypes: string[];
|
||||
recoveryModels: string[];
|
||||
files: DatabaseFile[];
|
||||
|
||||
isAzureDB: boolean;
|
||||
azureBackupRedundancyLevels: string[];
|
||||
@@ -488,3 +489,10 @@ export interface Server extends ObjectManagement.SqlObject {
|
||||
|
||||
export interface ServerViewInfo extends ObjectManagement.ObjectViewInfo<Server> {
|
||||
}
|
||||
|
||||
export interface DatabaseFile {
|
||||
name: string;
|
||||
type: string;
|
||||
path: string;
|
||||
fileGroup: string;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,13 @@ export function DeleteObjectError(objectType: string, objectName: string, error:
|
||||
}, "An error occurred while deleting the {0}: {1}. {2}", objectType, objectName, error);
|
||||
}
|
||||
|
||||
export function OpenDetachDatabaseDialogError(error: string): string {
|
||||
return localize({
|
||||
key: 'objectManagement.openDetachDatabaseDialogError',
|
||||
comment: ['{0}: error message.']
|
||||
}, "An error occurred while opening the detach database dialog. {0}", error);
|
||||
}
|
||||
|
||||
export function OpenObjectPropertiesDialogError(objectType: string, objectName: string, error: string): string {
|
||||
return localize({
|
||||
key: 'objectManagement.openObjectPropertiesDialogError',
|
||||
@@ -162,6 +169,15 @@ export const CurrentSLOText = localize('objectManagement.currentSLOLabel', "Curr
|
||||
export const EditionText = localize('objectManagement.editionLabel', "Edition");
|
||||
export const MaxSizeText = localize('objectManagement.maxSizeLabel', "Max Size");
|
||||
export const AzurePricingLinkText = localize('objectManagement.azurePricingLink', "Azure SQL Database pricing calculator");
|
||||
export const DetachDatabaseDialogTitle = (dbName: string) => localize('objectManagement.detachDatabaseDialogTitle', "Detach Database - {0} (Preview)", dbName);
|
||||
export const DetachDropConnections = localize('objectManagement.detachDropConnections', "Drop connnections");
|
||||
export const DetachUpdateStatistics = localize('objectManagement.detachUpdateStatistics', "Update statistics");
|
||||
export const DatabaseFilesLabel = localize('objectManagement.databaseFiles', "Database Files");
|
||||
export const DatabaseFileNameLabel = localize('objectManagement.databaseFileName', "Name");
|
||||
export const DatabaseFileTypeLabel = localize('objectManagement.databaseFileType', "Type");
|
||||
export const DatabaseFilePathLabel = localize('objectManagement.databaseFilePath', "Path");
|
||||
export const DatabaseFileGroupLabel = localize('objectManagement.databaseFileGroup', "File Group");
|
||||
export const DetachDatabaseOptions = localize('objectManagement.detachDatabaseOptions', "Detach Database Options");
|
||||
|
||||
// Login
|
||||
export const BlankPasswordConfirmationText: string = localize('objectManagement.blankPasswordConfirmation', "Creating a login with a blank password is a security risk. Are you sure you want to continue?");
|
||||
|
||||
@@ -65,6 +65,11 @@ export class ObjectManagementService extends BaseService implements IObjectManag
|
||||
const params: contracts.SearchObjectRequestParams = { contextId, searchText, objectTypes, schema };
|
||||
return this.runWithErrorHandling(contracts.SearchObjectRequest.type, params);
|
||||
}
|
||||
|
||||
async detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Promise<string> {
|
||||
const params: contracts.DetachDatabaseRequestParams = { connectionUri, objectUrn, dropConnections, updateStatistics, generateScript };
|
||||
return this.runWithErrorHandling(contracts.DetachDatabaseRequest.type, params);
|
||||
}
|
||||
}
|
||||
|
||||
const ServerLevelSecurableTypes: SecurableTypeMetadata[] = [
|
||||
@@ -232,6 +237,10 @@ export class TestObjectManagementService implements IObjectManagementService {
|
||||
return this.delayAndResolve(items);
|
||||
}
|
||||
|
||||
async detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Promise<string> {
|
||||
return this.delayAndResolve('');
|
||||
}
|
||||
|
||||
private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] {
|
||||
let items: ObjectManagement.SearchResultItem[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase';
|
||||
import { IObjectManagementService, ObjectManagement } from 'mssql';
|
||||
import { Database, DatabaseViewInfo } from '../interfaces';
|
||||
import { DetachDatabaseDocUrl } from '../constants';
|
||||
import { DatabaseFileGroupLabel, DatabaseFileNameLabel, DatabaseFilePathLabel, DatabaseFileTypeLabel, DatabaseFilesLabel, DetachDatabaseDialogTitle, DetachDatabaseOptions, DetachDropConnections, DetachUpdateStatistics } from '../localizedConstants';
|
||||
|
||||
export class DetachDatabaseDialog extends ObjectManagementDialogBase<Database, DatabaseViewInfo> {
|
||||
private _dropConnections = false;
|
||||
private _updateStatistics = false;
|
||||
|
||||
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
|
||||
super(objectManagementService, options, DetachDatabaseDialogTitle(options.database), 'DetachDatabase');
|
||||
}
|
||||
|
||||
protected override get isDirty(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async initializeUI(): Promise<void> {
|
||||
let tableData = this.viewInfo.files.map(file => [file.name, file.type, file.fileGroup, file.path]);
|
||||
let columnNames = [DatabaseFileNameLabel, DatabaseFileTypeLabel, DatabaseFileGroupLabel, DatabaseFilePathLabel];
|
||||
let fileTable = this.createTable(DatabaseFilesLabel, columnNames, tableData);
|
||||
let tableGroup = this.createGroup(DatabaseFilesLabel, [fileTable], false);
|
||||
|
||||
let connCheckbox = this.createCheckbox(DetachDropConnections, async checked => {
|
||||
this._dropConnections = checked;
|
||||
});
|
||||
let updateCheckbox = this.createCheckbox(DetachUpdateStatistics, async checked => {
|
||||
this._updateStatistics = checked;
|
||||
});
|
||||
let checkboxGroup = this.createGroup(DetachDatabaseOptions, [connCheckbox, updateCheckbox], false);
|
||||
|
||||
let components = [tableGroup, checkboxGroup];
|
||||
this.formContainer.addItems(components);
|
||||
}
|
||||
|
||||
protected override get helpUrl(): string {
|
||||
return DetachDatabaseDocUrl;
|
||||
}
|
||||
|
||||
protected override async saveChanges(contextId: string, object: ObjectManagement.SqlObject): Promise<void> {
|
||||
await this.objectManagementService.detachDatabase(this.options.connectionUri, this.options.objectUrn, this._dropConnections, this._updateStatistics, false);
|
||||
}
|
||||
|
||||
protected override async generateScript(): Promise<string> {
|
||||
return await this.objectManagementService.detachDatabase(this.options.connectionUri, this.options.objectUrn, this._dropConnections, this._updateStatistics, true);
|
||||
}
|
||||
|
||||
protected override async validateInput(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,16 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
|
||||
private _viewInfo: ViewInfoType;
|
||||
private _originalObjectInfo: ObjectInfoType;
|
||||
|
||||
constructor(protected readonly objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
|
||||
super(options.isNewObject ? localizedConstants.NewObjectDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true)) :
|
||||
localizedConstants.ObjectPropertiesDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true), options.objectName),
|
||||
getDialogName(options.objectType, options.isNewObject),
|
||||
options
|
||||
);
|
||||
constructor(protected readonly objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions, dialogTitle?: string, dialogName?: string) {
|
||||
if (!dialogTitle) {
|
||||
dialogTitle = options.isNewObject
|
||||
? localizedConstants.NewObjectDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true))
|
||||
: localizedConstants.ObjectPropertiesDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true), options.objectName);
|
||||
}
|
||||
if (!dialogName) {
|
||||
dialogName = getDialogName(options.objectType, options.isNewObject);
|
||||
}
|
||||
super(dialogTitle, dialogName, options);
|
||||
this._contextId = generateUuid();
|
||||
}
|
||||
|
||||
@@ -54,6 +58,10 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
|
||||
return errors;
|
||||
}
|
||||
|
||||
protected async saveChanges(contextId: string, object: ObjectManagement.SqlObject): Promise<void> {
|
||||
await this.objectManagementService.save(this._contextId, this.objectInfo);
|
||||
}
|
||||
|
||||
protected override async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
const typeDisplayName = localizedConstants.getNodeTypeDisplayName(this.options.objectType);
|
||||
@@ -67,7 +75,7 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
|
||||
try {
|
||||
if (this.isDirty) {
|
||||
const startTime = Date.now();
|
||||
await this.objectManagementService.save(this._contextId, this.objectInfo);
|
||||
await this.saveChanges(this._contextId, this.objectInfo);
|
||||
if (this.options.objectExplorerContext) {
|
||||
if (this.options.isNewObject) {
|
||||
await refreshNode(this.options.objectExplorerContext);
|
||||
|
||||
Reference in New Issue
Block a user