support rename for login, user and a few other types (#22331)

* rename object

* add comment

* use URN property

* vbump STS

* revert loc string change

* fix name check

* pr comments
This commit is contained in:
Alan Ren
2023-03-16 15:00:07 -07:00
committed by GitHub
parent f5628ed8e3
commit 20cf2489a2
12 changed files with 164 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
{ {
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "4.6.0.6", "version": "4.6.0.10",
"downloadFileNames": { "downloadFileNames": {
"Windows_86": "win-x86-net7.0.zip", "Windows_86": "win-x86-net7.0.zip",
"Windows_64": "win-x64-net7.0.zip", "Windows_64": "win-x64-net7.0.zip",

View File

@@ -92,6 +92,11 @@
"category": "MSSQL", "category": "MSSQL",
"title": "%title.deleteObject%" "title": "%title.deleteObject%"
}, },
{
"command": "mssql.renameObject",
"category": "MSSQL",
"title": "%title.renameObject%"
},
{ {
"command": "mssql.enableGroupBySchema", "command": "mssql.enableGroupBySchema",
"category": "MSSQL", "category": "MSSQL",
@@ -478,6 +483,10 @@
{ {
"command": "mssql.deleteObject", "command": "mssql.deleteObject",
"when": "false" "when": "false"
},
{
"command": "mssql.renameObject",
"when": "false"
} }
], ],
"objectExplorer/item/context": [ "objectExplorer/item/context": [
@@ -511,6 +520,16 @@
"when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures",
"group": "0_query@2" "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", "command": "mssql.enableGroupBySchema",
"when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema" "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema"
@@ -551,6 +570,16 @@
"when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures",
"group": "connection@2" "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", "command": "mssql.enableGroupBySchema",
"when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema" "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema"

View File

@@ -196,5 +196,6 @@
"title.newApplicationRole": "New Application Role", "title.newApplicationRole": "New Application Role",
"title.newUser": "New User", "title.newUser": "New User",
"title.objectProperties": "Properties (Preview)", "title.objectProperties": "Properties (Preview)",
"title.deleteObject": "Delete" "title.deleteObject": "Delete",
"title.renameObject": "Rename"
} }

View File

@@ -1604,4 +1604,14 @@ export namespace DisposeUserViewRequest {
export const type = new RequestType<DisposeUserViewRequestParams, void, void, void>('objectManagement/disposeUserView'); export const type = new RequestType<DisposeUserViewRequestParams, void, void, void>('objectManagement/disposeUserView');
} }
export interface RenameObjectRequestParams {
connectionUri: string;
newName: string;
objectUrn: string;
}
export namespace RenameObjectRequest {
export const type = new RequestType<RenameObjectRequestParams, void, void, void>('objectManagement/rename');
}
// ------------------------------- < Object Management > ------------------------------------ // ------------------------------- < Object Management > ------------------------------------

View File

@@ -1235,6 +1235,13 @@ declare module 'mssql' {
* @param contextId The id of the view. * @param contextId The id of the view.
*/ */
disposeUserView(contextId: string): Thenable<void>; disposeUserView(contextId: string): Thenable<void>;
/**
* 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<void>;
} }
// Object Management - End. // Object Management - End.
} }

View File

@@ -32,6 +32,9 @@ export function registerObjectManagementCommands(appContext: AppContext) {
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.deleteObject', async (context: azdata.ObjectExplorerContext) => { appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.deleteObject', async (context: azdata.ObjectExplorerContext) => {
await handleDeleteObjectCommand(context, service); 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 { function getObjectManagementService(appContext: AppContext, useTestService: boolean): IObjectManagementService {
@@ -159,8 +162,60 @@ async function handleDeleteObjectCommand(context: azdata.ObjectExplorerContext,
}).send(); }).send();
return; return;
} }
await refreshParentNode(context);
operation.updateStatus(azdata.TaskStatus.Succeeded); operation.updateStatus(azdata.TaskStatus.Succeeded);
await refreshParentNode(context);
}
});
}
async function handleRenameObjectCommand(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
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);
} }
}); });
} }

View File

@@ -7,8 +7,12 @@
* The object types in object explorer's node context. * The object types in object explorer's node context.
*/ */
export enum NodeType { export enum NodeType {
Column = 'Column',
Database = 'Database',
Login = 'ServerLevelLogin', Login = 'ServerLevelLogin',
User = 'User' Table = 'Table',
User = 'User',
View = 'View'
} }
export const PublicServerRoleName = 'public'; export const PublicServerRoleName = 'public';
@@ -51,10 +55,11 @@ export const AlterLoginDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/sta
export enum TelemetryActions { export enum TelemetryActions {
CreateObject = 'CreateObject', CreateObject = 'CreateObject',
UpdateObject = 'UpdateObject',
DeleteObject = 'DeleteObject', DeleteObject = 'DeleteObject',
OpenNewObjectDialog = 'OpenNewObjectDialog', OpenNewObjectDialog = 'OpenNewObjectDialog',
OpenPropertiesDialog = 'OpenPropertiesDialog' OpenPropertiesDialog = 'OpenPropertiesDialog',
RenameObject = 'RenameObject',
UpdateObject = 'UpdateObject'
} }
export enum TelemetryViews { export enum TelemetryViews {

View File

@@ -11,6 +11,10 @@ export const LoginTypeDisplayName: string = localize('objectManagement.LoginType
export const UserTypeDisplayName: string = localize('objectManagement.UserDisplayName', "user"); export const UserTypeDisplayName: string = localize('objectManagement.UserDisplayName', "user");
export const LoginTypeDisplayNameInTitle: string = localize('objectManagement.LoginTypeDisplayNameInTitle', "Login"); export const LoginTypeDisplayNameInTitle: string = localize('objectManagement.LoginTypeDisplayNameInTitle', "Login");
export const UserTypeDisplayNameInTitle: string = localize('objectManagement.UserTypeDisplayNameInTitle', "User"); 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 // Shared Strings
export const HelpText: string = localize('objectManagement.helpText', "Help"); 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 OkText: string = localize('objectManagement.OkText', "OK");
export const LoadingDialogText: string = localize('objectManagement.loadingDialog', "Loading dialog..."); 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 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 { export function RefreshObjectExplorerError(error: string): string {
return localize({ return localize({
@@ -89,6 +94,20 @@ export function ObjectPropertiesDialogTitle(objectType: string, objectName: stri
}, '{0} - {1} (Preview)', objectType, objectName); }, '{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 NameText = localize('objectManagement.nameLabel', "Name");
export const SelectedText = localize('objectManagement.selectedLabel', "Selected"); export const SelectedText = localize('objectManagement.selectedLabel', "Selected");
export const GeneralSectionHeader = localize('objectManagement.generalSectionHeader', "General"); export const GeneralSectionHeader = localize('objectManagement.generalSectionHeader', "General");

View File

@@ -136,6 +136,16 @@ export class ObjectManagementService implements IObjectManagementService {
} }
); );
} }
rename(connectionUri: string, objectUrn: string, newName: string): Thenable<void> {
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 { export class TestObjectManagementService implements IObjectManagementService {
@@ -292,6 +302,9 @@ export class TestObjectManagementService implements IObjectManagementService {
} }
async disposeUserView(contextId: string): Promise<void> { async disposeUserView(contextId: string): Promise<void> {
} }
async rename(connectionUri: string, objectUrn: string, newName: string): Promise<void> {
return this.delayAndResolve();
}
private delayAndResolve(): Promise<void> { private delayAndResolve(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -234,9 +234,9 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
protected createGroup(header: string, items: azdata.Component[], collapsible: boolean = true, collapsed: boolean = false): azdata.GroupContainer { protected createGroup(header: string, items: azdata.Component[], collapsible: boolean = true, collapsed: boolean = false): azdata.GroupContainer {
return this.modelView.modelBuilder.groupContainer().withLayout({ return this.modelView.modelBuilder.groupContainer().withLayout({
header: header, header: header,
collapsed: false, collapsible: collapsible,
collapsible: collapsible collapsed: collapsed
}).withProps({ collapsed: collapsed }).withItems(items).component(); }).withItems(items).component();
} }
protected createFormContainer(items: azdata.Component[]): azdata.DivContainer { protected createFormContainer(items: azdata.Component[]): azdata.DivContainer {

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getErrorMessage } from '../utils'; import { getErrorMessage } from '../utils';
import { AuthenticationType, NodeType, UserType } from './constants'; import { AuthenticationType, NodeType, UserType } from './constants';
import { AADAuthenticationTypeDisplayText, ContainedUserText, LoginTypeDisplayName, LoginTypeDisplayNameInTitle, RefreshObjectExplorerError, SQLAuthenticationTypeDisplayText, UserTypeDisplayName, UserTypeDisplayNameInTitle, UserWithLoginText, UserWithNoConnectAccess, UserWithWindowsGroupLoginText, WindowsAuthenticationTypeDisplayText } from './localizedConstants'; import { AADAuthenticationTypeDisplayText, ColumnTypeDisplayName, ContainedUserText, DatabaseTypeDisplayName, LoginTypeDisplayName, LoginTypeDisplayNameInTitle, RefreshObjectExplorerError, SQLAuthenticationTypeDisplayText, TableTypeDisplayName, UserTypeDisplayName, UserTypeDisplayNameInTitle, UserWithLoginText, UserWithNoConnectAccess, UserWithWindowsGroupLoginText, ViewTypeDisplayName, WindowsAuthenticationTypeDisplayText } from './localizedConstants';
export function deepClone<T>(obj: T): T { export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') { if (!obj || typeof obj !== 'object') {
@@ -59,6 +59,14 @@ export function getNodeTypeDisplayName(type: string, inTitle: boolean = false):
return inTitle ? LoginTypeDisplayNameInTitle : LoginTypeDisplayName; return inTitle ? LoginTypeDisplayNameInTitle : LoginTypeDisplayName;
case NodeType.User: case NodeType.User:
return inTitle ? UserTypeDisplayNameInTitle : UserTypeDisplayName; 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: default:
throw new Error(`Unkown node type: ${type}`); throw new Error(`Unkown node type: ${type}`);
} }
@@ -85,6 +93,7 @@ export function getAuthenticationTypeByDisplayName(displayValue: string): Authen
return AuthenticationType.Sql; return AuthenticationType.Sql;
} }
} }
export function getUserTypeDisplayName(userType: UserType): string { export function getUserTypeDisplayName(userType: UserType): string {
switch (userType) { switch (userType) {
case UserType.WithLogin: case UserType.WithLogin:

View File

@@ -44,6 +44,7 @@ export class MssqlNodeContext extends Disposable {
static IsWindows = new RawContextKey<boolean>('isWindows', isWindows); static IsWindows = new RawContextKey<boolean>('isWindows', isWindows);
static IsCloud = new RawContextKey<boolean>('isCloud', false); static IsCloud = new RawContextKey<boolean>('isCloud', false);
static NodeType = new RawContextKey<string>('nodeType', undefined); static NodeType = new RawContextKey<string>('nodeType', undefined);
static NodePath = new RawContextKey<string>('nodePath', undefined);
static ObjectType = new RawContextKey<string>('objectType', undefined); static ObjectType = new RawContextKey<string>('objectType', undefined);
static NodeLabel = new RawContextKey<string>('nodeLabel', undefined); static NodeLabel = new RawContextKey<string>('nodeLabel', undefined);
static EngineEdition = new RawContextKey<number>('engineEdition', DatabaseEngineEdition.Unknown); static EngineEdition = new RawContextKey<number>('engineEdition', DatabaseEngineEdition.Unknown);
@@ -60,6 +61,7 @@ export class MssqlNodeContext extends Disposable {
private nodeProviderKey!: IContextKey<string>; private nodeProviderKey!: IContextKey<string>;
private isCloudKey!: IContextKey<boolean>; private isCloudKey!: IContextKey<boolean>;
private nodeTypeKey!: IContextKey<string>; private nodeTypeKey!: IContextKey<string>;
private nodePathKey!: IContextKey<string>;
private objectTypeKey!: IContextKey<string>; private objectTypeKey!: IContextKey<string>;
private nodeLabelKey!: IContextKey<string>; private nodeLabelKey!: IContextKey<string>;
private isDatabaseOrServerKey!: IContextKey<boolean>; private isDatabaseOrServerKey!: IContextKey<boolean>;
@@ -99,6 +101,9 @@ export class MssqlNodeContext extends Disposable {
this.setScriptingContextKeys(); this.setScriptingContextKeys();
this.nodeTypeKey.set(node.contextValue); this.nodeTypeKey.set(node.contextValue);
} }
if (node.nodeInfo?.nodePath) {
this.nodePathKey.set(node.nodeInfo.nodePath);
}
this.setQueryEnabledKey(); this.setQueryEnabledKey();
} }
if (node.label) { if (node.label) {
@@ -112,6 +117,7 @@ export class MssqlNodeContext extends Disposable {
this.isCloudKey = MssqlNodeContext.IsCloud.bindTo(this.contextKeyService); this.isCloudKey = MssqlNodeContext.IsCloud.bindTo(this.contextKeyService);
this.engineEditionKey = MssqlNodeContext.EngineEdition.bindTo(this.contextKeyService); this.engineEditionKey = MssqlNodeContext.EngineEdition.bindTo(this.contextKeyService);
this.nodeTypeKey = MssqlNodeContext.NodeType.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.objectTypeKey = MssqlNodeContext.ObjectType.bindTo(this.contextKeyService);
this.nodeLabelKey = MssqlNodeContext.NodeLabel.bindTo(this.contextKeyService); this.nodeLabelKey = MssqlNodeContext.NodeLabel.bindTo(this.contextKeyService);
this.isDatabaseOrServerKey = MssqlNodeContext.IsDatabaseOrServer.bindTo(this.contextKeyService); this.isDatabaseOrServerKey = MssqlNodeContext.IsDatabaseOrServer.bindTo(this.contextKeyService);