diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 6d6745f85e..410dc1fe5c 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.6.0.6", + "version": "4.6.0.10", "downloadFileNames": { "Windows_86": "win-x86-net7.0.zip", "Windows_64": "win-x64-net7.0.zip", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 84fb03f233..b250984a8d 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -92,6 +92,11 @@ "category": "MSSQL", "title": "%title.deleteObject%" }, + { + "command": "mssql.renameObject", + "category": "MSSQL", + "title": "%title.renameObject%" + }, { "command": "mssql.enableGroupBySchema", "category": "MSSQL", @@ -478,6 +483,10 @@ { "command": "mssql.deleteObject", "when": "false" + }, + { + "command": "mssql.renameObject", + "when": "false" } ], "objectExplorer/item/context": [ @@ -511,6 +520,16 @@ "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", "group": "0_query@2" }, + { + "command": "mssql.renameObject", + "when": "connectionProvider == MSSQL && nodeType =~ /^(Database|ServerLevelLogin|User|Table|View)$/ && config.workbench.enablePreviewFeatures", + "group": "0_query@3" + }, + { + "command": "mssql.renameObject", + "when": "connectionProvider == MSSQL && nodeType == Column && config.workbench.enablePreviewFeatures && nodePath =~ /^.*\/Tables\/.*\/Columns\/.*$", + "group": "0_query@3" + }, { "command": "mssql.enableGroupBySchema", "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema" @@ -551,6 +570,16 @@ "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", "group": "connection@2" }, + { + "command": "mssql.renameObject", + "when": "connectionProvider == MSSQL && nodeType =~ /^(Database|ServerLevelLogin|User|Table|View)$/ && config.workbench.enablePreviewFeatures", + "group": "connection@3" + }, + { + "command": "mssql.renameObject", + "when": "connectionProvider == MSSQL && nodeType == Column && config.workbench.enablePreviewFeatures && nodePath =~ /^.*\/Tables\/.*\/Columns\/.*$", + "group": "connection@3" + }, { "command": "mssql.enableGroupBySchema", "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema" diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 702b9b306b..45621c6ad7 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -196,5 +196,6 @@ "title.newApplicationRole": "New Application Role", "title.newUser": "New User", "title.objectProperties": "Properties (Preview)", - "title.deleteObject": "Delete" + "title.deleteObject": "Delete", + "title.renameObject": "Rename" } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index f0d7987366..86a4103f28 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1604,4 +1604,14 @@ export namespace DisposeUserViewRequest { export const type = new RequestType('objectManagement/disposeUserView'); } +export interface RenameObjectRequestParams { + connectionUri: string; + newName: string; + objectUrn: string; +} + +export namespace RenameObjectRequest { + export const type = new RequestType('objectManagement/rename'); +} + // ------------------------------- < Object Management > ------------------------------------ diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 6fbe217cfd..6817ec5bfb 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -1235,6 +1235,13 @@ declare module 'mssql' { * @param contextId The id of the view. */ disposeUserView(contextId: string): Thenable; + /** + * Rename an object. + * @param connectionUri The URI of the server connection. + * @param objectUrn Urn of the object to be renamed. More information: https://learn.microsoft.com/en-us/sql/relational-databases/server-management-objects-smo/overview-smo. + * @param newName The new name of the object. + */ + rename(connectionUri: string, objectUrn: string, newName: string): Thenable; } // Object Management - End. } diff --git a/extensions/mssql/src/objectManagement/commands.ts b/extensions/mssql/src/objectManagement/commands.ts index a6cc3fe49a..8ce76bb7e4 100644 --- a/extensions/mssql/src/objectManagement/commands.ts +++ b/extensions/mssql/src/objectManagement/commands.ts @@ -32,6 +32,9 @@ export function registerObjectManagementCommands(appContext: AppContext) { appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.deleteObject', async (context: azdata.ObjectExplorerContext) => { await handleDeleteObjectCommand(context, service); })); + appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.renameObject', async (context: azdata.ObjectExplorerContext) => { + await handleRenameObjectCommand(context, service); + })); } function getObjectManagementService(appContext: AppContext, useTestService: boolean): IObjectManagementService { @@ -159,8 +162,60 @@ async function handleDeleteObjectCommand(context: azdata.ObjectExplorerContext, }).send(); return; } - await refreshParentNode(context); operation.updateStatus(azdata.TaskStatus.Succeeded); + await refreshParentNode(context); + } + }); +} + +async function handleRenameObjectCommand(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise { + const connectionUri = await getConnectionUri(context); + if (!connectionUri) { + return; + } + const nodeTypeDisplayName = getNodeTypeDisplayName(context.nodeInfo.nodeType); + const originalName = context.nodeInfo.metadata.name; + const newName = await vscode.window.showInputBox({ + title: localizedConstants.RenameObjectDialogTitle, + value: originalName, + validateInput: (value: string): string | undefined => { + if (!value) { + return localizedConstants.NameCannotBeEmptyError; + } else { + // valid + return undefined; + } + } + }); + + // return if no change was made or the dialog was canceled. + if (newName === originalName || !newName) { + return; + } + + azdata.tasks.startBackgroundOperation({ + displayName: localizedConstants.RenameObjectOperationDisplayName(nodeTypeDisplayName, originalName, newName), + description: '', + isCancelable: false, + operation: async (operation) => { + try { + const startTime = Date.now(); + await service.rename(connectionUri, context.nodeInfo.metadata.urn, newName); + TelemetryReporter.sendTelemetryEvent(TelemetryActions.RenameObject, { + objectType: context.nodeInfo.nodeType + }, { + elapsedTimeMs: Date.now() - startTime + }); + } + catch (err) { + operation.updateStatus(azdata.TaskStatus.Failed, localizedConstants.RenameObjectError(nodeTypeDisplayName, originalName, newName, getErrorMessage(err))); + TelemetryReporter.createErrorEvent2(TelemetryViews.ObjectManagement, TelemetryActions.RenameObject, err).withAdditionalProperties({ + objectType: context.nodeInfo.nodeType + }).send(); + return; + } + operation.updateStatus(azdata.TaskStatus.Succeeded); + await refreshParentNode(context); } }); } diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index 7b0fcf5134..9da417d509 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -7,8 +7,12 @@ * The object types in object explorer's node context. */ export enum NodeType { + Column = 'Column', + Database = 'Database', Login = 'ServerLevelLogin', - User = 'User' + Table = 'Table', + User = 'User', + View = 'View' } export const PublicServerRoleName = 'public'; @@ -51,10 +55,11 @@ export const AlterLoginDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/sta export enum TelemetryActions { CreateObject = 'CreateObject', - UpdateObject = 'UpdateObject', DeleteObject = 'DeleteObject', OpenNewObjectDialog = 'OpenNewObjectDialog', - OpenPropertiesDialog = 'OpenPropertiesDialog' + OpenPropertiesDialog = 'OpenPropertiesDialog', + RenameObject = 'RenameObject', + UpdateObject = 'UpdateObject' } export enum TelemetryViews { diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 57529a42d4..b12454c969 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -11,6 +11,10 @@ export const LoginTypeDisplayName: string = localize('objectManagement.LoginType export const UserTypeDisplayName: string = localize('objectManagement.UserDisplayName', "user"); export const LoginTypeDisplayNameInTitle: string = localize('objectManagement.LoginTypeDisplayNameInTitle', "Login"); export const UserTypeDisplayNameInTitle: string = localize('objectManagement.UserTypeDisplayNameInTitle', "User"); +export const TableTypeDisplayName: string = localize('objectManagement.TableDisplayName', "table"); +export const ViewTypeDisplayName: string = localize('objectManagement.ViewDisplayName', "view"); +export const ColumnTypeDisplayName: string = localize('objectManagement.ColumnDisplayName', "column"); +export const DatabaseTypeDisplayName: string = localize('objectManagement.DatabaseDisplayName', "database"); // Shared Strings export const HelpText: string = localize('objectManagement.helpText', "Help"); @@ -18,6 +22,7 @@ export const YesText: string = localize('objectManagement.yesText', "Yes"); export const OkText: string = localize('objectManagement.OkText', "OK"); export const LoadingDialogText: string = localize('objectManagement.loadingDialog', "Loading dialog..."); export const FailedToRetrieveConnectionInfoErrorMessage: string = localize('objectManagement.noConnectionUriError', "Failed to retrieve the connection information, please reconnect and try again.") +export const RenameObjectDialogTitle: string = localize('objectManagement.renameObjectDialogTitle', "Enter new name"); export function RefreshObjectExplorerError(error: string): string { return localize({ @@ -89,6 +94,20 @@ export function ObjectPropertiesDialogTitle(objectType: string, objectName: stri }, '{0} - {1} (Preview)', objectType, objectName); } +export function RenameObjectOperationDisplayName(objectType: string, originalName: string, newName: string): string { + return localize({ + key: 'objectManagement.renameObjectOperationName', + comment: ['{0} object type, {1}: original name, {2}: new name'] + }, "Rename {0} '{1}' to '{2}'", objectType, originalName, newName); +} + +export function RenameObjectError(objectType: string, originalName: string, newName: string, error: string): string { + return localize({ + key: 'objectManagement.renameObjectError', + comment: ['{0} object type, {1}: original name, {2}: new name, {3}: error message.'] + }, "An error occurred while renaming {0} '{1}' to '{2}'. {3}", objectType, originalName, newName, error); +} + export const NameText = localize('objectManagement.nameLabel', "Name"); export const SelectedText = localize('objectManagement.selectedLabel', "Selected"); export const GeneralSectionHeader = localize('objectManagement.generalSectionHeader', "General"); diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 98d0048d57..9a7c776515 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -136,6 +136,16 @@ export class ObjectManagementService implements IObjectManagementService { } ); } + rename(connectionUri: string, objectUrn: string, newName: string): Thenable { + const params: contracts.RenameObjectRequestParams = { connectionUri, objectUrn, newName }; + return this.client.sendRequest(contracts.RenameObjectRequest.type, params).then( + r => { }, + e => { + this.client.logFailedRequest(contracts.RenameObjectRequest.type, e); + return Promise.reject(new Error(e.message)); + } + ); + } } export class TestObjectManagementService implements IObjectManagementService { @@ -292,6 +302,9 @@ export class TestObjectManagementService implements IObjectManagementService { } async disposeUserView(contextId: string): Promise { } + async rename(connectionUri: string, objectUrn: string, newName: string): Promise { + return this.delayAndResolve(); + } private delayAndResolve(): Promise { return new Promise((resolve, reject) => { diff --git a/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts b/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts index ef5c51200f..1cbf1f5c11 100644 --- a/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts +++ b/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts @@ -234,9 +234,9 @@ export abstract class ObjectManagementDialogBase(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -59,6 +59,14 @@ export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): return inTitle ? LoginTypeDisplayNameInTitle : LoginTypeDisplayName; case NodeType.User: return inTitle ? UserTypeDisplayNameInTitle : UserTypeDisplayName; + case NodeType.Table: + return TableTypeDisplayName; + case NodeType.View: + return ViewTypeDisplayName; + case NodeType.Column: + return ColumnTypeDisplayName; + case NodeType.Database: + return DatabaseTypeDisplayName; default: throw new Error(`Unkown node type: ${type}`); } @@ -85,6 +93,7 @@ export function getAuthenticationTypeByDisplayName(displayValue: string): Authen return AuthenticationType.Sql; } } + export function getUserTypeDisplayName(userType: UserType): string { switch (userType) { case UserType.WithLogin: diff --git a/src/sql/workbench/services/objectExplorer/browser/mssqlNodeContext.ts b/src/sql/workbench/services/objectExplorer/browser/mssqlNodeContext.ts index c552c27b4c..a218b4c935 100644 --- a/src/sql/workbench/services/objectExplorer/browser/mssqlNodeContext.ts +++ b/src/sql/workbench/services/objectExplorer/browser/mssqlNodeContext.ts @@ -44,6 +44,7 @@ export class MssqlNodeContext extends Disposable { static IsWindows = new RawContextKey('isWindows', isWindows); static IsCloud = new RawContextKey('isCloud', false); static NodeType = new RawContextKey('nodeType', undefined); + static NodePath = new RawContextKey('nodePath', undefined); static ObjectType = new RawContextKey('objectType', undefined); static NodeLabel = new RawContextKey('nodeLabel', undefined); static EngineEdition = new RawContextKey('engineEdition', DatabaseEngineEdition.Unknown); @@ -60,6 +61,7 @@ export class MssqlNodeContext extends Disposable { private nodeProviderKey!: IContextKey; private isCloudKey!: IContextKey; private nodeTypeKey!: IContextKey; + private nodePathKey!: IContextKey; private objectTypeKey!: IContextKey; private nodeLabelKey!: IContextKey; private isDatabaseOrServerKey!: IContextKey; @@ -99,6 +101,9 @@ export class MssqlNodeContext extends Disposable { this.setScriptingContextKeys(); this.nodeTypeKey.set(node.contextValue); } + if (node.nodeInfo?.nodePath) { + this.nodePathKey.set(node.nodeInfo.nodePath); + } this.setQueryEnabledKey(); } if (node.label) { @@ -112,6 +117,7 @@ export class MssqlNodeContext extends Disposable { this.isCloudKey = MssqlNodeContext.IsCloud.bindTo(this.contextKeyService); this.engineEditionKey = MssqlNodeContext.EngineEdition.bindTo(this.contextKeyService); this.nodeTypeKey = MssqlNodeContext.NodeType.bindTo(this.contextKeyService); + this.nodePathKey = MssqlNodeContext.NodePath.bindTo(this.contextKeyService); this.objectTypeKey = MssqlNodeContext.ObjectType.bindTo(this.contextKeyService); this.nodeLabelKey = MssqlNodeContext.NodeLabel.bindTo(this.contextKeyService); this.isDatabaseOrServerKey = MssqlNodeContext.IsDatabaseOrServer.bindTo(this.contextKeyService);