diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 3381441aad..4bd65aa647 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -60,12 +60,14 @@ { "command": "mssql.newTable", "category": "MSSQL", - "title": "%title.newTable%" + "title": "%title.newTable%", + "icon": "$(add)" }, { "command": "mssql.designTable", "category": "MSSQL", - "title": "%title.designTable%" + "title": "%title.designTable%", + "icon": "$(edit)" }, { "command": "mssql.changeNotebookConnection", @@ -75,17 +77,20 @@ { "command": "mssql.newObject", "category": "MSSQL", - "title": "%title.newObject%" + "title": "%title.newObject%", + "icon": "$(add)" }, { "command": "mssql.objectProperties", "category": "MSSQL", - "title": "%title.objectProperties%" + "title": "%title.objectProperties%", + "icon": "$(edit)" }, { "command": "mssql.deleteObject", "category": "MSSQL", - "title": "%title.deleteObject%" + "title": "%title.deleteObject%", + "icon": "$(trash)" }, { "command": "mssql.renameObject", @@ -525,6 +530,33 @@ { "command": "mssql.disableGroupBySchema", "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": [ diff --git a/src/sql/base/common/codicons.ts b/src/sql/base/common/codicons.ts index 767dd1c364..e3e7f26cb6 100644 --- a/src/sql/base/common/codicons.ts +++ b/src/sql/base/common/codicons.ts @@ -13,5 +13,6 @@ export const enum SqlIconId { addServerGroupAction = 'add-server-group-action', addServerAction = 'add-server-action', activeConnectionsAction = 'active-connections-action', - serverPage = 'server-page' + serverPage = 'server-page', + removeFilter = 'remove-filter-action' } diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 77ec582292..0061ba5ee6 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -126,6 +126,9 @@ export interface IConnectionManagementService { onConnectionProfileGroupMoved: Event; // End of Event Emitters for async tree + // Event emitters for recent connections tree + onRecentConnectionProfileDeleted: Event; + // Properties providerNameToDisplayNameMap: { [providerDisplayName: string]: string }; diff --git a/src/sql/platform/connection/test/common/testConnectionManagementService.ts b/src/sql/platform/connection/test/common/testConnectionManagementService.ts index 6a6f46057e..b91e7850bc 100644 --- a/src/sql/platform/connection/test/common/testConnectionManagementService.ts +++ b/src/sql/platform/connection/test/common/testConnectionManagementService.ts @@ -36,6 +36,7 @@ export class TestConnectionManagementService implements IConnectionManagementSer public onConnectionProfileGroupEdited: Event = Event.None; public onConnectionProfileGroupDeleted: Event = Event.None; public onConnectionProfileGroupMoved: Event = Event.None; + public onRecentConnectionProfileDeleted: Event = Event.None; public get onConnect(): Event { return Event.None; diff --git a/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css b/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css index 83a4ba5cc9..bd810ecd3e 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css +++ b/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css @@ -46,14 +46,6 @@ 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 */ .new-connection .monaco-text-button { margin-bottom: 2px; @@ -276,3 +268,21 @@ background-repeat: no-repeat; 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; +} + diff --git a/src/sql/workbench/contrib/objectExplorer/browser/media/remove_filter.svg b/src/sql/workbench/contrib/objectExplorer/browser/media/remove_filter.svg new file mode 100644 index 0000000000..26be73c770 --- /dev/null +++ b/src/sql/workbench/contrib/objectExplorer/browser/media/remove_filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/sql/workbench/contrib/objectExplorer/browser/media/remove_filter_inverse.svg b/src/sql/workbench/contrib/objectExplorer/browser/media/remove_filter_inverse.svg new file mode 100644 index 0000000000..8d94e329fa --- /dev/null +++ b/src/sql/workbench/contrib/objectExplorer/browser/media/remove_filter_inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css b/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css index 170a0e9126..c9f73a45c4 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css +++ b/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css @@ -10,7 +10,8 @@ } .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; } @@ -44,6 +45,17 @@ 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 */ .monaco-pane-view .pane > .pane-header > .actions .action-label.icon, .monaco-pane-view .pane > .pane-header > .actions .action-label.codicon { diff --git a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts index ab3340824d..6e7455d0c4 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts +++ b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts @@ -930,7 +930,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { } } - private getActionContext(element: ServerTreeElement): any { + public getActionContext(element: ServerTreeElement): any { let actionContext: any; if (element instanceof TreeNode) { let context = new ObjectExplorerActionsContext(); diff --git a/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts b/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts index 6ad245d889..49a77aabec 100644 --- a/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts +++ b/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts @@ -17,6 +17,7 @@ import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/ import { EditDataAction } from 'sql/workbench/browser/scriptingActions'; import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ServerInfoContextKey } from 'sql/workbench/services/connection/common/serverInfoContextKey'; +import { Codicon } from 'vs/base/common/codicons'; //#region -- Data Explorer // Script as Create @@ -287,6 +288,27 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { 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 //#region -- explorer widget diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index 11c96939ab..0d16c4df85 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -46,6 +46,7 @@ import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserve import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { onUnexpectedError } from 'vs/base/common/errors'; import { FieldSet } from 'sql/base/browser/ui/fieldset/fieldset'; +import { KeyCode } from 'vs/base/common/keyCodes'; export interface OnShowUIResponse { selectedProviderDisplayName: string; @@ -357,34 +358,56 @@ export class ConnectionDialogWidget extends Modal { }; const actionProvider = this.instantiationService.createInstance(RecentConnectionActionsProvider); const controller = new RecentConnectionTreeController(leftClick, actionProvider, this.connectionManagementService, this.contextMenuService); - actionProvider.onRecentConnectionRemoved(() => { - 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 action provider: ${err}`)); - }); - controller.onRecentConnectionRemoved(() => { - 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(actionProvider.onRecentConnectionRemoved(async () => { + await this.refreshTree(); + })); + this._register(controller.onRecentConnectionRemoved(async () => { + await this.refreshTree(); + })); + + 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); if (this._recentConnectionTree instanceof AsyncServerTree) { - this._recentConnectionTree.onMouseClick(e => { + this._register(this._recentConnectionTree.onMouseClick(e => { if (e.element instanceof ConnectionProfile) { this._connectionSource = 'recent'; this.onConnectionClick(e.element, false).catch(onUnexpectedError); } - }); - this._recentConnectionTree.onMouseDblClick(e => { + })); + + this._register(this._recentConnectionTree.onMouseDblClick(e => { if (e.element instanceof ConnectionProfile) { this._connectionSource = 'recent'; 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 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() { this.createRecentConnectionList(); const noRecentConnectionContainer = DOM.append(this._noRecentConnection, DOM.$('.connection-recent-content')); diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index 26dfc082af..c0255ea82a 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -88,6 +88,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti private _onConnectionProfileGroupEdited = new Emitter(); private _onConnectionProfileGroupMoved = new Emitter(); + private _onRecentConnectionProfileDeleted = new Emitter(); + private _mementoContext: Memento; private _mementoObj: MementoObject; private _connectionStore: ConnectionStore; @@ -246,6 +248,10 @@ export class ConnectionManagementService extends Disposable implements IConnecti return this._onConnectionProfileGroupMoved.event; } + public get onRecentConnectionProfileDeleted(): Event { + return this._onRecentConnectionProfileDeleted.event; + } + public get providerNameToDisplayNameMap(): { readonly [providerDisplayName: string]: string } { return this._providerNameToDisplayNameMap; } @@ -829,6 +835,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti public clearRecentConnection(connectionProfile: interfaces.IConnectionProfile): void { this._connectionStore.removeRecentConnection(connectionProfile); + this._onRecentConnectionProfileDeleted.fire(connectionProfile); } public getActiveConnections(providers?: string[]): ConnectionProfile[] { diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts index 85520ba893..b6f61a4c70 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts @@ -53,6 +53,7 @@ import { TestConfigurationService } from 'sql/platform/connection/test/common/te import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; import { ConnectionBrowserView } from 'sql/workbench/services/connection/browser/connectionBrowseTab'; import { ConnectionProviderProperties, ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; +import { Emitter } from 'vs/base/common/event'; suite('ConnectionDialogService tests', () => { const testTreeViewId = 'testTreeView'; @@ -90,7 +91,7 @@ suite('ConnectionDialogService tests', () => { testInstantiationService.stub(IViewDescriptorService, viewDescriptorService); let errorMessageService = getMockErrorMessageService(); let capabilitiesService = new TestCapabilitiesService(); - mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, + mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, undefined, // connection dialog service testInstantiationService, // instantiation service undefined, // editor service @@ -129,6 +130,7 @@ suite('ConnectionDialogService tests', () => { } }; }); + mockConnectionManagementService.setup(x => x.onRecentConnectionProfileDeleted).returns(() => new Emitter().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.render(); testConnectionDialog['renderBody'](DOM.createStyleSheet()); @@ -184,6 +186,7 @@ suite('ConnectionDialogService tests', () => { return Promise.resolve(connectionProfile); }); 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.setup(x => x.focusOnOpen()); mockWidget.setup(x => x.handleOnConnecting()); diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts index c354331920..a703c6e385 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts @@ -29,6 +29,9 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView'; import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; 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', () => { const testTreeViewId = 'testTreeView'; const ViewsRegistry = Registry.as(Extensions.ViewsRegistry); @@ -66,6 +69,7 @@ suite('ConnectionDialogWidget tests', () => { mockConnectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => true); mockConnectionManagementService.setup(x => x.getConnectionIconId(TypeMoq.It.isAnyString())).returns(() => ''); mockConnectionManagementService.setup(x => x.getProviderProperties(TypeMoq.It.isAnyString())).returns(() => undefined); + mockConnectionManagementService.setup(x => x.onRecentConnectionProfileDeleted).returns(() => new Emitter().event); cmInstantiationService.stub(IConnectionManagementService, mockConnectionManagementService.object); let providerDisplayNames = ['Mock SQL Server']; let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts index d155461ebe..036f19f95e 100644 --- a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts @@ -24,6 +24,7 @@ import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/comm import { instanceOfSqlThemeIcon } from 'sql/workbench/services/objectExplorer/common/nodeType'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar'; const DefaultConnectionIconClass = 'server-page'; export interface ConnectionProfileGroupDisplayOptions { @@ -35,11 +36,13 @@ class ConnectionProfileGroupTemplate extends Disposable { private _icon: HTMLElement; private _labelContainer: HTMLElement; private _label: ResourceLabel; + private _actionBar: ActionBar; constructor( container: HTMLElement, private _option: ConnectionProfileGroupDisplayOptions, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IObjectExplorerService private _objectExplorerService: IObjectExplorerService ) { super(); 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._labelContainer = dom.append(this._root, dom.$('span.name')); 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) { @@ -63,6 +69,13 @@ class ConnectionProfileGroupTemplate extends Disposable { this._label.element.setLabel(element.name, '', { 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 _labelContainer: HTMLElement; private _label: ResourceLabel; + private _actionBar: ActionBar; + /** * _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 @@ -110,6 +125,9 @@ class ConnectionProfileTemplate extends Disposable { this._connectionStatusBadge = dom.append(this._icon, dom.$('div.connection-status-badge')); this._labelContainer = dom.append(this._root, dom.$('div.label')); 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) { @@ -132,6 +150,19 @@ class ConnectionProfileTemplate extends Disposable { matches: createMatches(filterData) }); 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 _labelContainer: HTMLElement; private _label: ResourceLabel; + private _actionBar: ActionBar; constructor( container: HTMLElement, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IObjectExplorerService private _objectExplorerService: IObjectExplorerService ) { super(); this._root = dom.append(container, dom.$('.object-element-container')); this._icon = dom.append(this._root, dom.$('div.object-icon')); this._labelContainer = dom.append(this._root, dom.$('div.label')); 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) { @@ -211,6 +247,12 @@ class TreeNodeTemplate extends Disposable { matches: createMatches(filterData) }); 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 }); } } diff --git a/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts b/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts index 159d96aaf3..e50a97a7d8 100644 --- a/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts +++ b/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts @@ -21,6 +21,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; import { SqlIconId } from 'sql/base/common/codicons'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { Codicon } from 'vs/base/common/codicons'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; @@ -44,7 +45,7 @@ export class RefreshAction extends Action { @IErrorMessageService private _errorMessageService: IErrorMessageService, @ILogService private _logService: ILogService ) { - super(id, label); + super(id, label, Codicon.refresh.classNames); } public override async run(): Promise { let treeNode: TreeNode | undefined = undefined; @@ -103,8 +104,7 @@ export class EditConnectionAction extends Action { private _connectionProfile: ConnectionProfile, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService ) { - super(id, label); - this.class = 'edit-server-action'; + super(id, label, Codicon.edit.classNames); } public override async run(): Promise { @@ -124,7 +124,7 @@ export class DisconnectConnectionAction extends Action { private _connectionProfile: ConnectionProfile, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService ) { - super(id, label); + super(id, label, Codicon.debugDisconnect.classNames); } override async run(actionContext: ObjectExplorerActionsContext): Promise { @@ -223,8 +223,7 @@ export class EditServerGroupAction extends Action { private _group: ConnectionProfileGroup, @IServerGroupController private readonly serverGroupController: IServerGroupController ) { - super(id, label); - this.class = 'edit-server-group-action'; + super(id, label, Codicon.edit.classNames); } public override run(): Promise { @@ -277,8 +276,7 @@ export class DeleteConnectionAction extends Action { @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IDialogService private _dialogService: IDialogService ) { - super(id, label); - this.class = 'delete-connection-action'; + super(id, label, Codicon.trash.classNames); if (element instanceof ConnectionProfileGroup && element.id === UNSAVED_GROUP_ID) { this.enabled = false; } @@ -322,14 +320,19 @@ export class FilterChildrenAction extends Action { label: string, private _node: TreeNode, @IObjectExplorerService private _objectExplorerService: IObjectExplorerService) { - super(id, label); + super(id, label, getFilterActionIconClass(_node)); } public override async run(): Promise { 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 { public static ID = 'objectExplorer.removeFilter'; public static LABEL = localize('objectExplorer.removeFilter', "Remove Filter"); @@ -343,7 +346,7 @@ export class RemoveFilterAction extends Action { @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, @IAdsTelemetryService private _telemetryService: IAdsTelemetryService ) { - super(id, label); + super(id, label, SqlIconId.removeFilter); } public override async run(): Promise { @@ -373,3 +376,23 @@ export class RemoveFilterAction extends Action { }).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 { + if (this._connectionProfile) { + this._connectionManagementService.clearRecentConnection(this._connectionProfile); + } + } +} diff --git a/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts b/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts index 1c3008c181..01889f776d 100644 --- a/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts +++ b/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts @@ -56,6 +56,7 @@ export interface IServerTreeView { layout(size: number): void; showFilteredTree(view: ServerTreeViewView): void; filterElementChildren(node: TreeNode): Promise; + getActionContext(element: ServerTreeElement): any; view: ServerTreeViewView; } diff --git a/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts b/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts index 728d4cfba9..6ea0ef5eef 100644 --- a/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts +++ b/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts @@ -10,7 +10,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { DisconnectConnectionAction, EditConnectionAction, - DeleteConnectionAction, RefreshAction, EditServerGroupAction, AddServerAction, FilterChildrenAction, RemoveFilterAction + DeleteConnectionAction, RefreshAction, EditServerGroupAction, AddServerAction, FilterChildrenAction, RemoveFilterAction, DeleteRecentConnectionsAction } from 'sql/workbench/services/objectExplorer/browser/connectionTreeAction'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType'; @@ -52,9 +52,9 @@ export class ServerTreeActionProvider { /** * 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) { - return this.getConnectionActions(tree, element); + return this.getConnectionActions(tree, element, inlineOnly); } if (element instanceof ConnectionProfileGroup) { return this.getConnectionProfileGroupActions(element); @@ -66,7 +66,7 @@ export class ServerTreeActionProvider { tree: tree, profile, treeNode: element - }); + }, inlineOnly); } } return []; @@ -95,10 +95,18 @@ export class ServerTreeActionProvider { 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 */ - 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); // Only update password and not access tokens to avoid login prompts when opening context menu. this._connectionManagementService.addSavedPassword(profile, true); @@ -107,10 +115,11 @@ export class ServerTreeActionProvider { tree: tree, profile: profile, 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 let scopedContextService = this.getContextKeyService(context); let menu = this.menuService.createMenu(MenuId.ObjectExplorerItemContext, scopedContextService); @@ -118,8 +127,11 @@ export class ServerTreeActionProvider { // Fill in all actions const builtIn = getDefaultActions(context); const actions: IAction[] = []; - const options = { arg: undefined, shouldForwardArgs: true }; - const groups = menu.getActions(options); + const options = { + arg: undefined, shouldForwardArgs: true + }; + + let groups = menu.getActions(options); let insertIndex: number | undefined = 0; const queryIndex = groups.findIndex(v => { if (v[0] === '0_query') { @@ -132,24 +144,36 @@ export class ServerTreeActionProvider { } }); insertIndex = queryIndex > -1 ? insertIndex + groups[queryIndex][1].length : undefined; - fillInActions(groups, actions, false); - if (insertIndex) { - if (!(actions[insertIndex] instanceof Separator) && builtIn.length > 0) { - builtIn.unshift(new Separator()); - } - actions?.splice(insertIndex, 0, ...builtIn); - } else { - if (actions.length > 0 && builtIn.length > 0) { - builtIn.push(new Separator()); - } + if (inlineOnly) { + groups = groups.filter(g => g[0].includes('inline')); + fillInActions(groups, actions, false); 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 menu.dispose(); return actions; - } private getBuiltinConnectionActions(context: ObjectExplorerContext): IAction[] { @@ -209,8 +233,8 @@ export class ServerTreeActionProvider { /** * Return actions for OE elements */ - private getObjectExplorerNodeActions(context: ObjectExplorerContext): IAction[] { - return this.getAllActions(context, (context) => this.getBuiltInNodeActions(context)); + private getObjectExplorerNodeActions(context: ObjectExplorerContext, inlineOnly: boolean = false): IAction[] { + return this.getAllActions(context, (context) => this.getBuiltInNodeActions(context), inlineOnly); } private getBuiltInNodeActions(context: ObjectExplorerContext): IAction[] { @@ -226,16 +250,15 @@ export class ServerTreeActionProvider { } // Contribute refresh action for scriptable objects via contribution 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 if (treeNode?.filterProperties?.length > 0 && this._configurationService.getValue(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES)) { 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) { 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; }