Adding inline actions to OE (#23101)

This commit is contained in:
Aasim Khan
2023-05-13 09:24:49 -07:00
committed by GitHub
parent 31a88cc9eb
commit 2beba9ac08
18 changed files with 279 additions and 66 deletions

View File

@@ -60,12 +60,14 @@
{ {
"command": "mssql.newTable", "command": "mssql.newTable",
"category": "MSSQL", "category": "MSSQL",
"title": "%title.newTable%" "title": "%title.newTable%",
"icon": "$(add)"
}, },
{ {
"command": "mssql.designTable", "command": "mssql.designTable",
"category": "MSSQL", "category": "MSSQL",
"title": "%title.designTable%" "title": "%title.designTable%",
"icon": "$(edit)"
}, },
{ {
"command": "mssql.changeNotebookConnection", "command": "mssql.changeNotebookConnection",
@@ -75,17 +77,20 @@
{ {
"command": "mssql.newObject", "command": "mssql.newObject",
"category": "MSSQL", "category": "MSSQL",
"title": "%title.newObject%" "title": "%title.newObject%",
"icon": "$(add)"
}, },
{ {
"command": "mssql.objectProperties", "command": "mssql.objectProperties",
"category": "MSSQL", "category": "MSSQL",
"title": "%title.objectProperties%" "title": "%title.objectProperties%",
"icon": "$(edit)"
}, },
{ {
"command": "mssql.deleteObject", "command": "mssql.deleteObject",
"category": "MSSQL", "category": "MSSQL",
"title": "%title.deleteObject%" "title": "%title.deleteObject%",
"icon": "$(trash)"
}, },
{ {
"command": "mssql.renameObject", "command": "mssql.renameObject",
@@ -525,6 +530,33 @@
{ {
"command": "mssql.disableGroupBySchema", "command": "mssql.disableGroupBySchema",
"when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && config.mssql.objectExplorer.groupBySchema" "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && config.mssql.objectExplorer.groupBySchema"
},
{
"command": "mssql.designTable",
"when": "connectionProvider == MSSQL && nodeType == Table && nodeSubType != LedgerDropped",
"group": "inline@2",
"isDefault": true
},
{
"command": "mssql.newTable",
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Tables",
"group": "inline@0"
},
{
"command": "mssql.newObject",
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType =~ /^(ServerLevelLogins|Users|ServerLevelServerRoles|ApplicationRoles|DatabaseRoles)$/ && config.workbench.enablePreviewFeatures",
"group": "inline@0"
},
{
"command": "mssql.objectProperties",
"when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures",
"group": "inline@2",
"isDefault": true
},
{
"command": "mssql.deleteObject",
"when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole|Database)$/ && config.workbench.enablePreviewFeatures",
"group": "inline@3"
} }
], ],
"dataExplorer/context": [ "dataExplorer/context": [

View File

@@ -13,5 +13,6 @@ export const enum SqlIconId {
addServerGroupAction = 'add-server-group-action', addServerGroupAction = 'add-server-group-action',
addServerAction = 'add-server-action', addServerAction = 'add-server-action',
activeConnectionsAction = 'active-connections-action', activeConnectionsAction = 'active-connections-action',
serverPage = 'server-page' serverPage = 'server-page',
removeFilter = 'remove-filter-action'
} }

View File

@@ -126,6 +126,9 @@ export interface IConnectionManagementService {
onConnectionProfileGroupMoved: Event<ConnectionElementMovedParams>; onConnectionProfileGroupMoved: Event<ConnectionElementMovedParams>;
// End of Event Emitters for async tree // End of Event Emitters for async tree
// Event emitters for recent connections tree
onRecentConnectionProfileDeleted: Event<ConnectionProfile>;
// Properties // Properties
providerNameToDisplayNameMap: { [providerDisplayName: string]: string }; providerNameToDisplayNameMap: { [providerDisplayName: string]: string };

View File

@@ -36,6 +36,7 @@ export class TestConnectionManagementService implements IConnectionManagementSer
public onConnectionProfileGroupEdited: Event<any> = Event.None; public onConnectionProfileGroupEdited: Event<any> = Event.None;
public onConnectionProfileGroupDeleted: Event<any> = Event.None; public onConnectionProfileGroupDeleted: Event<any> = Event.None;
public onConnectionProfileGroupMoved: Event<any> = Event.None; public onConnectionProfileGroupMoved: Event<any> = Event.None;
public onRecentConnectionProfileDeleted: Event<any> = Event.None;
public get onConnect(): Event<any> { public get onConnect(): Event<any> {
return Event.None; return Event.None;

View File

@@ -46,14 +46,6 @@
color: #ffffff; color: #ffffff;
} }
.server-explorer-viewlet .monaco-action-bar .action-label {
margin-right: 0.3em;
margin-left: 0.3em;
line-height: 15px;
width: 10px !important;
height: 10px !important;
}
/* Add space beneath the button */ /* Add space beneath the button */
.new-connection .monaco-text-button { .new-connection .monaco-text-button {
margin-bottom: 2px; margin-bottom: 2px;
@@ -276,3 +268,21 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 16px 16px; background-size: 16px 16px;
} }
.server-explorer-viewlet .monaco-list .monaco-list-row .actions {
display: none;
padding-right: 10px;
}
.server-explorer-viewlet .monaco-list .monaco-list-row .actions .action-label {
background-size: 14px 14px;
background-repeat: no-repeat;
background-position: center;
}
.server-explorer-viewlet .monaco-list .monaco-list-row:hover .actions,
.server-explorer-viewlet .monaco-list .monaco-list-row.selected .actions,
.server-explorer-viewlet .monaco-list .monaco-list-row.focused .actions {
display: block;
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M0,1H16V2.7l-6,6V15H6V8.7l-6-6ZM15,2.3V2H1v.3l6,6V14H9V8.3Zm.3,7.7.7.7-1.8,1.8L16,14.3l-.7.7-1.8-1.8L11.7,15l-.7-.7,1.8-1.8L11,10.7l.7-.7,1.8,1.8Z" />
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#fff" d="M0,1H16V2.7l-6,6V15H6V8.7l-6-6ZM15,2.3V2H1v.3l6,6V14H9V8.3Zm.3,7.7.7.7-1.8,1.8L16,14.3l-.7.7-1.8-1.8L11.7,15l-.7-.7,1.8-1.8L11,10.7l.7-.7,1.8,1.8Z" />
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -10,7 +10,8 @@
} }
.monaco-workbench.vs-dark .actions-container .action-label.add-server-action, .monaco-workbench.vs-dark .actions-container .action-label.add-server-action,
.monaco-workbench.hc-black .actions-container .action-label.add-server-action { .monaco-workbench.hc-black .actions-container .action-label.add-server-action,
.monaco-list:focus .monaco-list-rows .actions-container .action-label.add-server-action {
background-image: url('add_server_inverse.svg') !important; background-image: url('add_server_inverse.svg') !important;
} }
@@ -44,6 +45,17 @@
background-image: url('server_page_inverse.svg') !important; background-image: url('server_page_inverse.svg') !important;
} }
.monaco-workbench .actions-container .action-label.remove-filter-action,
.monaco-workbench.hc-light .actions-container .action-label.remove-filter-action {
background-image: url('remove_filter.svg') !important;
}
.monaco-workbench.vs-dark .actions-container .action-label.remove-filter-action,
.monaco-workbench.hc-black .actions-container .action-label.remove-filter-action,
.monaco-list:focus .monaco-list-rows .actions-container .action-label.remove-filter-action {
background-image: url('remove_filter_inverse.svg') !important;
}
/* styles for action labels - without these, the buttons for the Servers view will not be sized correctly */ /* styles for action labels - without these, the buttons for the Servers view will not be sized correctly */
.monaco-pane-view .pane > .pane-header > .actions .action-label.icon, .monaco-pane-view .pane > .pane-header > .actions .action-label.icon,
.monaco-pane-view .pane > .pane-header > .actions .action-label.codicon { .monaco-pane-view .pane > .pane-header > .actions .action-label.codicon {

View File

@@ -930,7 +930,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView {
} }
} }
private getActionContext(element: ServerTreeElement): any { public getActionContext(element: ServerTreeElement): any {
let actionContext: any; let actionContext: any;
if (element instanceof TreeNode) { if (element instanceof TreeNode) {
let context = new ObjectExplorerActionsContext(); let context = new ObjectExplorerActionsContext();

View File

@@ -17,6 +17,7 @@ import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/
import { EditDataAction } from 'sql/workbench/browser/scriptingActions'; import { EditDataAction } from 'sql/workbench/browser/scriptingActions';
import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes'; import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';
import { ServerInfoContextKey } from 'sql/workbench/services/connection/common/serverInfoContextKey'; import { ServerInfoContextKey } from 'sql/workbench/services/connection/common/serverInfoContextKey';
import { Codicon } from 'vs/base/common/codicons';
//#region -- Data Explorer //#region -- Data Explorer
// Script as Create // Script as Create
@@ -287,6 +288,27 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, {
TreeNodeContextKey.NodeType.isEqualTo(NodeType.TableValuedFunction)) TreeNodeContextKey.NodeType.isEqualTo(NodeType.TableValuedFunction))
}); });
MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, {
group: 'inline',
order: 7,
command: {
id: commands.OE_REFRESH_COMMAND_ID,
title: localize('refreshNode', "Refresh"),
icon: Codicon.refresh
},
when: ContextKeyExpr.or(
TreeNodeContextKey.NodeType.isEqualTo(NodeType.Table),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.View),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.Schema),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.User),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.UserDefinedTableType),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.StoredProcedure),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.AggregateFunction),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.PartitionFunction),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.ScalarValuedFunction),
TreeNodeContextKey.NodeType.isEqualTo(NodeType.TableValuedFunction))
});
//#endregion //#endregion
//#region -- explorer widget //#region -- explorer widget

View File

@@ -46,6 +46,7 @@ import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserve
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { onUnexpectedError } from 'vs/base/common/errors'; import { onUnexpectedError } from 'vs/base/common/errors';
import { FieldSet } from 'sql/base/browser/ui/fieldset/fieldset'; import { FieldSet } from 'sql/base/browser/ui/fieldset/fieldset';
import { KeyCode } from 'vs/base/common/keyCodes';
export interface OnShowUIResponse { export interface OnShowUIResponse {
selectedProviderDisplayName: string; selectedProviderDisplayName: string;
@@ -357,34 +358,56 @@ export class ConnectionDialogWidget extends Modal {
}; };
const actionProvider = this.instantiationService.createInstance(RecentConnectionActionsProvider); const actionProvider = this.instantiationService.createInstance(RecentConnectionActionsProvider);
const controller = new RecentConnectionTreeController(leftClick, actionProvider, this.connectionManagementService, this.contextMenuService); const controller = new RecentConnectionTreeController(leftClick, actionProvider, this.connectionManagementService, this.contextMenuService);
actionProvider.onRecentConnectionRemoved(() => { this._register(actionProvider.onRecentConnectionRemoved(async () => {
const recentConnections: ConnectionProfile[] = this.connectionManagementService.getRecentConnections(); await this.refreshTree();
this.open(recentConnections.length > 0).catch(err => this.logService.error(`Unexpected error opening connection widget after a recent connection was removed from action provider: ${err}`)); }));
}); this._register(controller.onRecentConnectionRemoved(async () => {
controller.onRecentConnectionRemoved(() => { await this.refreshTree();
const recentConnections: ConnectionProfile[] = this.connectionManagementService.getRecentConnections(); }));
this.open(recentConnections.length > 0).catch(err => this.logService.error(`Unexpected error opening connection widget after a recent connection was removed from controller : ${err}`));
}); this._register(this.connectionManagementService.onRecentConnectionProfileDeleted(async (e) => {
await this.refreshTree();
}));
this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer, this.instantiationService, this._configurationService, localize('connectionDialog.recentConnections', "Recent Connections"), controller); this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer, this.instantiationService, this._configurationService, localize('connectionDialog.recentConnections', "Recent Connections"), controller);
if (this._recentConnectionTree instanceof AsyncServerTree) { if (this._recentConnectionTree instanceof AsyncServerTree) {
this._recentConnectionTree.onMouseClick(e => { this._register(this._recentConnectionTree.onMouseClick(e => {
if (e.element instanceof ConnectionProfile) { if (e.element instanceof ConnectionProfile) {
this._connectionSource = 'recent'; this._connectionSource = 'recent';
this.onConnectionClick(e.element, false).catch(onUnexpectedError); this.onConnectionClick(e.element, false).catch(onUnexpectedError);
} }
}); }));
this._recentConnectionTree.onMouseDblClick(e => {
this._register(this._recentConnectionTree.onMouseDblClick(e => {
if (e.element instanceof ConnectionProfile) { if (e.element instanceof ConnectionProfile) {
this._connectionSource = 'recent'; this._connectionSource = 'recent';
this.onConnectionClick(e.element, true).catch(onUnexpectedError); this.onConnectionClick(e.element, true).catch(onUnexpectedError);
} }
}); }));
this._register(this._recentConnectionTree.onKeyDown(e => {
const keyboardEvent = new StandardKeyboardEvent(e);
if (keyboardEvent.keyCode === KeyCode.Delete) {
const element = this._recentConnectionTree.getSelection()[0];
if (element instanceof ConnectionProfile) {
this.connectionManagementService.clearRecentConnection(element);
}
}
}));
} }
// Theme styler // Theme styler
this._register(styler.attachListStyler(this._recentConnectionTree, this._themeService)); this._register(styler.attachListStyler(this._recentConnectionTree, this._themeService));
} }
private async refreshTree() {
try {
const recentConnections: ConnectionProfile[] = this.connectionManagementService.getRecentConnections();
await this.open(recentConnections.length > 0);
} catch (err) {
this.logService.error(`Unexpected error opening connection widget after a recent connection was removed from controller : ${err}`);
}
}
private createRecentConnections() { private createRecentConnections() {
this.createRecentConnectionList(); this.createRecentConnectionList();
const noRecentConnectionContainer = DOM.append(this._noRecentConnection, DOM.$('.connection-recent-content')); const noRecentConnectionContainer = DOM.append(this._noRecentConnection, DOM.$('.connection-recent-content'));

View File

@@ -88,6 +88,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
private _onConnectionProfileGroupEdited = new Emitter<ConnectionProfileGroup>(); private _onConnectionProfileGroupEdited = new Emitter<ConnectionProfileGroup>();
private _onConnectionProfileGroupMoved = new Emitter<ConnectionElementMovedParams>(); private _onConnectionProfileGroupMoved = new Emitter<ConnectionElementMovedParams>();
private _onRecentConnectionProfileDeleted = new Emitter<ConnectionProfile>();
private _mementoContext: Memento; private _mementoContext: Memento;
private _mementoObj: MementoObject; private _mementoObj: MementoObject;
private _connectionStore: ConnectionStore; private _connectionStore: ConnectionStore;
@@ -246,6 +248,10 @@ export class ConnectionManagementService extends Disposable implements IConnecti
return this._onConnectionProfileGroupMoved.event; return this._onConnectionProfileGroupMoved.event;
} }
public get onRecentConnectionProfileDeleted(): Event<ConnectionProfile> {
return this._onRecentConnectionProfileDeleted.event;
}
public get providerNameToDisplayNameMap(): { readonly [providerDisplayName: string]: string } { public get providerNameToDisplayNameMap(): { readonly [providerDisplayName: string]: string } {
return this._providerNameToDisplayNameMap; return this._providerNameToDisplayNameMap;
} }
@@ -829,6 +835,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
public clearRecentConnection(connectionProfile: interfaces.IConnectionProfile): void { public clearRecentConnection(connectionProfile: interfaces.IConnectionProfile): void {
this._connectionStore.removeRecentConnection(connectionProfile); this._connectionStore.removeRecentConnection(connectionProfile);
this._onRecentConnectionProfileDeleted.fire(<ConnectionProfile>connectionProfile);
} }
public getActiveConnections(providers?: string[]): ConnectionProfile[] { public getActiveConnections(providers?: string[]): ConnectionProfile[] {

View File

@@ -53,6 +53,7 @@ import { TestConfigurationService } from 'sql/platform/connection/test/common/te
import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService';
import { ConnectionBrowserView } from 'sql/workbench/services/connection/browser/connectionBrowseTab'; import { ConnectionBrowserView } from 'sql/workbench/services/connection/browser/connectionBrowseTab';
import { ConnectionProviderProperties, ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { ConnectionProviderProperties, ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { Emitter } from 'vs/base/common/event';
suite('ConnectionDialogService tests', () => { suite('ConnectionDialogService tests', () => {
const testTreeViewId = 'testTreeView'; const testTreeViewId = 'testTreeView';
@@ -90,7 +91,7 @@ suite('ConnectionDialogService tests', () => {
testInstantiationService.stub(IViewDescriptorService, viewDescriptorService); testInstantiationService.stub(IViewDescriptorService, viewDescriptorService);
let errorMessageService = getMockErrorMessageService(); let errorMessageService = getMockErrorMessageService();
let capabilitiesService = new TestCapabilitiesService(); let capabilitiesService = new TestCapabilitiesService();
mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose,
undefined, // connection dialog service undefined, // connection dialog service
testInstantiationService, // instantiation service testInstantiationService, // instantiation service
undefined, // editor service undefined, // editor service
@@ -129,6 +130,7 @@ suite('ConnectionDialogService tests', () => {
} }
}; };
}); });
mockConnectionManagementService.setup(x => x.onRecentConnectionProfileDeleted).returns(() => new Emitter<ConnectionProfile>().event);
testConnectionDialog = new TestConnectionDialogWidget(providerDisplayNames, providerNameToDisplayMap['MSSQL'], providerNameToDisplayMap, testInstantiationService, mockConnectionManagementService.object, undefined, undefined, viewDescriptorService, new TestThemeService(), new TestLayoutService(), new NullAdsTelemetryService(), new MockContextKeyService(), undefined, new NullLogService(), new TestTextResourcePropertiesService(new TestConfigurationService), new TestConfigurationService(), new TestCapabilitiesService()); testConnectionDialog = new TestConnectionDialogWidget(providerDisplayNames, providerNameToDisplayMap['MSSQL'], providerNameToDisplayMap, testInstantiationService, mockConnectionManagementService.object, undefined, undefined, viewDescriptorService, new TestThemeService(), new TestLayoutService(), new NullAdsTelemetryService(), new MockContextKeyService(), undefined, new NullLogService(), new TestTextResourcePropertiesService(new TestConfigurationService), new TestConfigurationService(), new TestCapabilitiesService());
testConnectionDialog.render(); testConnectionDialog.render();
testConnectionDialog['renderBody'](DOM.createStyleSheet()); testConnectionDialog['renderBody'](DOM.createStyleSheet());
@@ -184,6 +186,7 @@ suite('ConnectionDialogService tests', () => {
return Promise.resolve(connectionProfile); return Promise.resolve(connectionProfile);
}); });
mockConnectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => true); mockConnectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => true);
mockWidget = TypeMoq.Mock.ofType(ConnectionWidget, TypeMoq.MockBehavior.Strict, [], undefined, 'MSSQL', undefined, undefined, mockConnectionManagementService.object); mockWidget = TypeMoq.Mock.ofType(ConnectionWidget, TypeMoq.MockBehavior.Strict, [], undefined, 'MSSQL', undefined, undefined, mockConnectionManagementService.object);
mockWidget.setup(x => x.focusOnOpen()); mockWidget.setup(x => x.focusOnOpen());
mockWidget.setup(x => x.handleOnConnecting()); mockWidget.setup(x => x.handleOnConnecting());

View File

@@ -29,6 +29,9 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView'; import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView';
import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Emitter } from 'vs/base/common/event';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
suite('ConnectionDialogWidget tests', () => { suite('ConnectionDialogWidget tests', () => {
const testTreeViewId = 'testTreeView'; const testTreeViewId = 'testTreeView';
const ViewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry); const ViewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
@@ -66,6 +69,7 @@ suite('ConnectionDialogWidget tests', () => {
mockConnectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => true); mockConnectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => true);
mockConnectionManagementService.setup(x => x.getConnectionIconId(TypeMoq.It.isAnyString())).returns(() => ''); mockConnectionManagementService.setup(x => x.getConnectionIconId(TypeMoq.It.isAnyString())).returns(() => '');
mockConnectionManagementService.setup(x => x.getProviderProperties(TypeMoq.It.isAnyString())).returns(() => undefined); mockConnectionManagementService.setup(x => x.getProviderProperties(TypeMoq.It.isAnyString())).returns(() => undefined);
mockConnectionManagementService.setup(x => x.onRecentConnectionProfileDeleted).returns(() => new Emitter<ConnectionProfile>().event);
cmInstantiationService.stub(IConnectionManagementService, mockConnectionManagementService.object); cmInstantiationService.stub(IConnectionManagementService, mockConnectionManagementService.object);
let providerDisplayNames = ['Mock SQL Server']; let providerDisplayNames = ['Mock SQL Server'];
let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' };

View File

@@ -24,6 +24,7 @@ import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/comm
import { instanceOfSqlThemeIcon } from 'sql/workbench/services/objectExplorer/common/nodeType'; import { instanceOfSqlThemeIcon } from 'sql/workbench/services/objectExplorer/common/nodeType';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { ResourceLabel } from 'vs/workbench/browser/labels'; import { ResourceLabel } from 'vs/workbench/browser/labels';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
const DefaultConnectionIconClass = 'server-page'; const DefaultConnectionIconClass = 'server-page';
export interface ConnectionProfileGroupDisplayOptions { export interface ConnectionProfileGroupDisplayOptions {
@@ -35,11 +36,13 @@ class ConnectionProfileGroupTemplate extends Disposable {
private _icon: HTMLElement; private _icon: HTMLElement;
private _labelContainer: HTMLElement; private _labelContainer: HTMLElement;
private _label: ResourceLabel; private _label: ResourceLabel;
private _actionBar: ActionBar;
constructor( constructor(
container: HTMLElement, container: HTMLElement,
private _option: ConnectionProfileGroupDisplayOptions, private _option: ConnectionProfileGroupDisplayOptions,
@IInstantiationService private readonly _instantiationService: IInstantiationService @IInstantiationService private readonly _instantiationService: IInstantiationService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService
) { ) {
super(); super();
container.parentElement!.classList.add('async-server-group'); container.parentElement!.classList.add('async-server-group');
@@ -48,6 +51,9 @@ class ConnectionProfileGroupTemplate extends Disposable {
this._icon = dom.append(this._root, dom.$('div.icon')); this._icon = dom.append(this._root, dom.$('div.icon'));
this._labelContainer = dom.append(this._root, dom.$('span.name')); this._labelContainer = dom.append(this._root, dom.$('span.name'));
this._label = this._instantiationService.createInstance(ResourceLabel, this._labelContainer, { supportHighlights: true }); this._label = this._instantiationService.createInstance(ResourceLabel, this._labelContainer, { supportHighlights: true });
const actionsContainer = dom.append(this._label.element.element, dom.$('.actions'));
this._actionBar = new ActionBar(actionsContainer, {
});
} }
set(element: ConnectionProfileGroup, filterData: FuzzyScore) { set(element: ConnectionProfileGroup, filterData: FuzzyScore) {
@@ -63,6 +69,13 @@ class ConnectionProfileGroupTemplate extends Disposable {
this._label.element.setLabel(element.name, '', { this._label.element.setLabel(element.name, '', {
matches: createMatches(filterData) matches: createMatches(filterData)
}); });
const actionProvider = this._objectExplorerService.getServerTreeView().treeActionProvider;
const tree = this._objectExplorerService.getServerTreeView().tree;
const actions = actionProvider.getActions(tree, element, true);
this._actionBar.context = this._objectExplorerService.getServerTreeView().getActionContext(element);
this._actionBar.clear();
this._actionBar.pushAction(actions, { icon: true, label: false });
} }
} }
@@ -91,6 +104,8 @@ class ConnectionProfileTemplate extends Disposable {
private _connectionStatusBadge: HTMLElement; private _connectionStatusBadge: HTMLElement;
private _labelContainer: HTMLElement; private _labelContainer: HTMLElement;
private _label: ResourceLabel; private _label: ResourceLabel;
private _actionBar: ActionBar;
/** /**
* _isCompact is used to render connections tiles with and without the action buttons. * _isCompact is used to render connections tiles with and without the action buttons.
* When set to true, like in the connection dialog recent connections tree, the connection * When set to true, like in the connection dialog recent connections tree, the connection
@@ -110,6 +125,9 @@ class ConnectionProfileTemplate extends Disposable {
this._connectionStatusBadge = dom.append(this._icon, dom.$('div.connection-status-badge')); this._connectionStatusBadge = dom.append(this._icon, dom.$('div.connection-status-badge'));
this._labelContainer = dom.append(this._root, dom.$('div.label')); this._labelContainer = dom.append(this._root, dom.$('div.label'));
this._label = this._instantiationService.createInstance(ResourceLabel, this._labelContainer, { supportHighlights: true }); this._label = this._instantiationService.createInstance(ResourceLabel, this._labelContainer, { supportHighlights: true });
const actionsContainer = dom.append(this._label.element.element, dom.$('.actions'));
this._actionBar = new ActionBar(actionsContainer, {
});
} }
set(element: ConnectionProfile, filterData: FuzzyScore) { set(element: ConnectionProfile, filterData: FuzzyScore) {
@@ -132,6 +150,19 @@ class ConnectionProfileTemplate extends Disposable {
matches: createMatches(filterData) matches: createMatches(filterData)
}); });
this._root.title = labelText; this._root.title = labelText;
const actionProvider = this._objectExplorerService.getServerTreeView().treeActionProvider;
if (!this._isCompact) {
const tree = this._objectExplorerService.getServerTreeView().tree;
const actions = actionProvider.getActions(tree, element, true);
this._actionBar.context = this._objectExplorerService.getServerTreeView().getActionContext(element);
this._actionBar.clear();
this._actionBar.pushAction(actions, { icon: true, label: false });
} else {
const actions = actionProvider.getRecentConnectionActions(element);
this._actionBar.context = undefined;
this._actionBar.clear();
this._actionBar.pushAction(actions, { icon: true, label: false });
}
} }
} }
@@ -159,16 +190,21 @@ class TreeNodeTemplate extends Disposable {
private _icon: HTMLElement; private _icon: HTMLElement;
private _labelContainer: HTMLElement; private _labelContainer: HTMLElement;
private _label: ResourceLabel; private _label: ResourceLabel;
private _actionBar: ActionBar;
constructor( constructor(
container: HTMLElement, container: HTMLElement,
@IInstantiationService private readonly _instantiationService: IInstantiationService @IInstantiationService private readonly _instantiationService: IInstantiationService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService
) { ) {
super(); super();
this._root = dom.append(container, dom.$('.object-element-container')); this._root = dom.append(container, dom.$('.object-element-container'));
this._icon = dom.append(this._root, dom.$('div.object-icon')); this._icon = dom.append(this._root, dom.$('div.object-icon'));
this._labelContainer = dom.append(this._root, dom.$('div.label')); this._labelContainer = dom.append(this._root, dom.$('div.label'));
this._label = this._instantiationService.createInstance(ResourceLabel, this._labelContainer, { supportHighlights: true }); this._label = this._instantiationService.createInstance(ResourceLabel, this._labelContainer, { supportHighlights: true });
const actionsContainer = dom.append(this._label.element.element, dom.$('.actions'));
this._actionBar = new ActionBar(actionsContainer, {
});
} }
set(element: TreeNode, filterData: FuzzyScore) { set(element: TreeNode, filterData: FuzzyScore) {
@@ -211,6 +247,12 @@ class TreeNodeTemplate extends Disposable {
matches: createMatches(filterData) matches: createMatches(filterData)
}); });
this._root.title = labelText; this._root.title = labelText;
const tree = this._objectExplorerService.getServerTreeView().tree;
const actionProvider = this._objectExplorerService.getServerTreeView().treeActionProvider;
const actions = actionProvider.getActions(tree, element, true);
this._actionBar.context = this._objectExplorerService.getServerTreeView().getActionContext(element);
this._actionBar.clear();
this._actionBar.pushAction(actions, { icon: true, label: false });
} }
} }

View File

@@ -21,6 +21,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree';
import { SqlIconId } from 'sql/base/common/codicons'; import { SqlIconId } from 'sql/base/common/codicons';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { Codicon } from 'vs/base/common/codicons';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
@@ -44,7 +45,7 @@ export class RefreshAction extends Action {
@IErrorMessageService private _errorMessageService: IErrorMessageService, @IErrorMessageService private _errorMessageService: IErrorMessageService,
@ILogService private _logService: ILogService @ILogService private _logService: ILogService
) { ) {
super(id, label); super(id, label, Codicon.refresh.classNames);
} }
public override async run(): Promise<void> { public override async run(): Promise<void> {
let treeNode: TreeNode | undefined = undefined; let treeNode: TreeNode | undefined = undefined;
@@ -103,8 +104,7 @@ export class EditConnectionAction extends Action {
private _connectionProfile: ConnectionProfile, private _connectionProfile: ConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService @IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) { ) {
super(id, label); super(id, label, Codicon.edit.classNames);
this.class = 'edit-server-action';
} }
public override async run(): Promise<void> { public override async run(): Promise<void> {
@@ -124,7 +124,7 @@ export class DisconnectConnectionAction extends Action {
private _connectionProfile: ConnectionProfile, private _connectionProfile: ConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService @IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) { ) {
super(id, label); super(id, label, Codicon.debugDisconnect.classNames);
} }
override async run(actionContext: ObjectExplorerActionsContext): Promise<any> { override async run(actionContext: ObjectExplorerActionsContext): Promise<any> {
@@ -223,8 +223,7 @@ export class EditServerGroupAction extends Action {
private _group: ConnectionProfileGroup, private _group: ConnectionProfileGroup,
@IServerGroupController private readonly serverGroupController: IServerGroupController @IServerGroupController private readonly serverGroupController: IServerGroupController
) { ) {
super(id, label); super(id, label, Codicon.edit.classNames);
this.class = 'edit-server-group-action';
} }
public override run(): Promise<void> { public override run(): Promise<void> {
@@ -277,8 +276,7 @@ export class DeleteConnectionAction extends Action {
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IDialogService private _dialogService: IDialogService @IDialogService private _dialogService: IDialogService
) { ) {
super(id, label); super(id, label, Codicon.trash.classNames);
this.class = 'delete-connection-action';
if (element instanceof ConnectionProfileGroup && element.id === UNSAVED_GROUP_ID) { if (element instanceof ConnectionProfileGroup && element.id === UNSAVED_GROUP_ID) {
this.enabled = false; this.enabled = false;
} }
@@ -322,14 +320,19 @@ export class FilterChildrenAction extends Action {
label: string, label: string,
private _node: TreeNode, private _node: TreeNode,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService) { @IObjectExplorerService private _objectExplorerService: IObjectExplorerService) {
super(id, label); super(id, label, getFilterActionIconClass(_node));
} }
public override async run(): Promise<void> { public override async run(): Promise<void> {
await this._objectExplorerService.getServerTreeView().filterElementChildren(this._node); await this._objectExplorerService.getServerTreeView().filterElementChildren(this._node);
this.class = getFilterActionIconClass(this._node);
} }
} }
function getFilterActionIconClass(node: TreeNode): string {
return node.filters.length > 0 ? Codicon.filterFilled.classNames : Codicon.filter.classNames;
}
export class RemoveFilterAction extends Action { export class RemoveFilterAction extends Action {
public static ID = 'objectExplorer.removeFilter'; public static ID = 'objectExplorer.removeFilter';
public static LABEL = localize('objectExplorer.removeFilter', "Remove Filter"); public static LABEL = localize('objectExplorer.removeFilter', "Remove Filter");
@@ -343,7 +346,7 @@ export class RemoveFilterAction extends Action {
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService, @IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService @IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) { ) {
super(id, label); super(id, label, SqlIconId.removeFilter);
} }
public override async run(): Promise<void> { public override async run(): Promise<void> {
@@ -373,3 +376,23 @@ export class RemoveFilterAction extends Action {
}).send(); }).send();
} }
} }
export class DeleteRecentConnectionsAction extends Action {
public static ID = 'registeredServers.clearRecentConnections';
public static LABEL = localize('registeredServers.clearRecentConnections', "Delete");
constructor(
id: string,
label: string,
private _connectionProfile: ConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) {
super(id, label, Codicon.trash.classNames);
}
public override async run(): Promise<void> {
if (this._connectionProfile) {
this._connectionManagementService.clearRecentConnection(this._connectionProfile);
}
}
}

View File

@@ -56,6 +56,7 @@ export interface IServerTreeView {
layout(size: number): void; layout(size: number): void;
showFilteredTree(view: ServerTreeViewView): void; showFilteredTree(view: ServerTreeViewView): void;
filterElementChildren(node: TreeNode): Promise<void>; filterElementChildren(node: TreeNode): Promise<void>;
getActionContext(element: ServerTreeElement): any;
view: ServerTreeViewView; view: ServerTreeViewView;
} }

View File

@@ -10,7 +10,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { import {
DisconnectConnectionAction, EditConnectionAction, DisconnectConnectionAction, EditConnectionAction,
DeleteConnectionAction, RefreshAction, EditServerGroupAction, AddServerAction, FilterChildrenAction, RemoveFilterAction DeleteConnectionAction, RefreshAction, EditServerGroupAction, AddServerAction, FilterChildrenAction, RemoveFilterAction, DeleteRecentConnectionsAction
} from 'sql/workbench/services/objectExplorer/browser/connectionTreeAction'; } from 'sql/workbench/services/objectExplorer/browser/connectionTreeAction';
import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode';
import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType'; import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType';
@@ -52,9 +52,9 @@ export class ServerTreeActionProvider {
/** /**
* Return actions given an element in the tree * Return actions given an element in the tree
*/ */
public getActions(tree: AsyncServerTree | ITree, element: ServerTreeElement): IAction[] { public getActions(tree: AsyncServerTree | ITree, element: ServerTreeElement, inlineOnly: boolean = false): IAction[] {
if (element instanceof ConnectionProfile) { if (element instanceof ConnectionProfile) {
return this.getConnectionActions(tree, element); return this.getConnectionActions(tree, element, inlineOnly);
} }
if (element instanceof ConnectionProfileGroup) { if (element instanceof ConnectionProfileGroup) {
return this.getConnectionProfileGroupActions(element); return this.getConnectionProfileGroupActions(element);
@@ -66,7 +66,7 @@ export class ServerTreeActionProvider {
tree: tree, tree: tree,
profile, profile,
treeNode: element treeNode: element
}); }, inlineOnly);
} }
} }
return []; return [];
@@ -95,10 +95,18 @@ export class ServerTreeActionProvider {
return undefined; return undefined;
} }
public getRecentConnectionActions(element: ConnectionProfile): IAction[] {
return [
this._instantiationService.createInstance(EditConnectionAction, EditConnectionAction.ID, EditConnectionAction.LABEL, element),
this._instantiationService.createInstance(DeleteRecentConnectionsAction, DeleteRecentConnectionsAction.ID, DeleteRecentConnectionsAction.LABEL, element)
]
}
/** /**
* Return actions for connection elements * Return actions for connection elements
*/ */
private getConnectionActions(tree: AsyncServerTree | ITree, profile: ConnectionProfile): IAction[] { private getConnectionActions(tree: AsyncServerTree | ITree, profile: ConnectionProfile, inlineOnly: boolean = false): IAction[] {
let node = new TreeNode(NodeType.Server, NodeType.Server, '', false, '', '', '', '', undefined, undefined, undefined, undefined); let node = new TreeNode(NodeType.Server, NodeType.Server, '', false, '', '', '', '', undefined, undefined, undefined, undefined);
// Only update password and not access tokens to avoid login prompts when opening context menu. // Only update password and not access tokens to avoid login prompts when opening context menu.
this._connectionManagementService.addSavedPassword(profile, true); this._connectionManagementService.addSavedPassword(profile, true);
@@ -107,10 +115,11 @@ export class ServerTreeActionProvider {
tree: tree, tree: tree,
profile: profile, profile: profile,
treeNode: node treeNode: node
}, (context) => this.getBuiltinConnectionActions(context)); }, (context) => this.getBuiltinConnectionActions(context),
inlineOnly);
} }
private getAllActions(context: ObjectExplorerContext, getDefaultActions: (context: ObjectExplorerContext) => IAction[]) { private getAllActions(context: ObjectExplorerContext, getDefaultActions: (context: ObjectExplorerContext) => IAction[], inlineOnly: boolean = false) {
// Create metadata needed to get a useful set of actions // Create metadata needed to get a useful set of actions
let scopedContextService = this.getContextKeyService(context); let scopedContextService = this.getContextKeyService(context);
let menu = this.menuService.createMenu(MenuId.ObjectExplorerItemContext, scopedContextService); let menu = this.menuService.createMenu(MenuId.ObjectExplorerItemContext, scopedContextService);
@@ -118,8 +127,11 @@ export class ServerTreeActionProvider {
// Fill in all actions // Fill in all actions
const builtIn = getDefaultActions(context); const builtIn = getDefaultActions(context);
const actions: IAction[] = []; const actions: IAction[] = [];
const options = { arg: undefined, shouldForwardArgs: true }; const options = {
const groups = menu.getActions(options); arg: undefined, shouldForwardArgs: true
};
let groups = menu.getActions(options);
let insertIndex: number | undefined = 0; let insertIndex: number | undefined = 0;
const queryIndex = groups.findIndex(v => { const queryIndex = groups.findIndex(v => {
if (v[0] === '0_query') { if (v[0] === '0_query') {
@@ -132,24 +144,36 @@ export class ServerTreeActionProvider {
} }
}); });
insertIndex = queryIndex > -1 ? insertIndex + groups[queryIndex][1].length : undefined; insertIndex = queryIndex > -1 ? insertIndex + groups[queryIndex][1].length : undefined;
fillInActions(groups, actions, false);
if (insertIndex) { if (inlineOnly) {
if (!(actions[insertIndex] instanceof Separator) && builtIn.length > 0) { groups = groups.filter(g => g[0].includes('inline'));
builtIn.unshift(new Separator()); fillInActions(groups, actions, false);
}
actions?.splice(insertIndex, 0, ...builtIn);
} else {
if (actions.length > 0 && builtIn.length > 0) {
builtIn.push(new Separator());
}
actions.unshift(...builtIn); actions.unshift(...builtIn);
// Moving refresh action to the end of the list
const refreshIndex = actions.findIndex(f => {
return f instanceof RefreshAction;
});
if (refreshIndex > -1) {
actions.push(actions.splice(refreshIndex, 1)[0]);
}
} else {
fillInActions(groups, actions, false);
if (insertIndex) {
if (!(actions[insertIndex] instanceof Separator) && builtIn.length > 0 && !inlineOnly) {
builtIn.unshift(new Separator());
}
actions?.splice(insertIndex, 0, ...builtIn);
} else {
if (actions.length > 0 && builtIn.length > 0) {
builtIn.push(new Separator());
}
actions.unshift(...builtIn);
}
} }
// Cleanup // Cleanup
menu.dispose(); menu.dispose();
return actions; return actions;
} }
private getBuiltinConnectionActions(context: ObjectExplorerContext): IAction[] { private getBuiltinConnectionActions(context: ObjectExplorerContext): IAction[] {
@@ -209,8 +233,8 @@ export class ServerTreeActionProvider {
/** /**
* Return actions for OE elements * Return actions for OE elements
*/ */
private getObjectExplorerNodeActions(context: ObjectExplorerContext): IAction[] { private getObjectExplorerNodeActions(context: ObjectExplorerContext, inlineOnly: boolean = false): IAction[] {
return this.getAllActions(context, (context) => this.getBuiltInNodeActions(context)); return this.getAllActions(context, (context) => this.getBuiltInNodeActions(context), inlineOnly);
} }
private getBuiltInNodeActions(context: ObjectExplorerContext): IAction[] { private getBuiltInNodeActions(context: ObjectExplorerContext): IAction[] {
@@ -226,16 +250,15 @@ export class ServerTreeActionProvider {
} }
// Contribute refresh action for scriptable objects via contribution // Contribute refresh action for scriptable objects via contribution
if (!this.isScriptableObject(context)) { if (!this.isScriptableObject(context)) {
actions.push(this._instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL, context.tree, context.treeNode || context.profile));
// Adding filter action if the node has filter properties // Adding filter action if the node has filter properties
if (treeNode?.filterProperties?.length > 0 && this._configurationService.getValue<boolean>(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES)) { if (treeNode?.filterProperties?.length > 0 && this._configurationService.getValue<boolean>(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES)) {
actions.push(this._instantiationService.createInstance(FilterChildrenAction, FilterChildrenAction.ID, FilterChildrenAction.LABEL, context.treeNode)); actions.push(this._instantiationService.createInstance(FilterChildrenAction, FilterChildrenAction.ID, FilterChildrenAction.LABEL, context.treeNode));
} }
// Adding remove filter action if the node has filters applied to it. // Adding remove filter action if the node has filters applied to it and the action is not inline only.
if (treeNode?.filters?.length > 0) { if (treeNode?.filters?.length > 0) {
actions.push(this._instantiationService.createInstance(RemoveFilterAction, RemoveFilterAction.ID, RemoveFilterAction.LABEL, context.treeNode, context.tree, undefined)); actions.push(this._instantiationService.createInstance(RemoveFilterAction, RemoveFilterAction.ID, RemoveFilterAction.LABEL, context.treeNode, context.tree, undefined));
} }
actions.push(this._instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL, context.tree, context.treeNode || context.profile));
} }
return actions; return actions;
} }