diff --git a/src/sql/platform/connection/common/connectionProfileGroup.ts b/src/sql/platform/connection/common/connectionProfileGroup.ts index b27484d55d..6248b4956b 100644 --- a/src/sql/platform/connection/common/connectionProfileGroup.ts +++ b/src/sql/platform/connection/common/connectionProfileGroup.ts @@ -23,6 +23,7 @@ export class ConnectionProfileGroup extends Disposable implements IConnectionPro private _childConnections: ConnectionProfile[] = []; public parentId?: string; private _isRenamed = false; + public readonly isRoot: boolean = false; public constructor( public name: string, public parent: ConnectionProfileGroup | undefined, @@ -34,6 +35,7 @@ export class ConnectionProfileGroup extends Disposable implements IConnectionPro this.parentId = parent ? parent.id : undefined; if (this.name === ConnectionProfileGroup.RootGroupName) { this.name = ''; + this.isRoot = true; } } diff --git a/src/sql/workbench/contrib/connection/browser/media/collapsed-dark.svg b/src/sql/workbench/contrib/connection/browser/media/collapsed-dark.svg deleted file mode 100644 index cf5c3641aa..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/collapsed-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/connection/browser/media/connected_active_server.svg b/src/sql/workbench/contrib/connection/browser/media/connected_active_server.svg deleted file mode 100644 index 315a9c0602..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/connected_active_server.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/sql/workbench/contrib/connection/browser/media/connected_active_server_inverse.svg b/src/sql/workbench/contrib/connection/browser/media/connected_active_server_inverse.svg deleted file mode 100644 index 50483cd3a5..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/connected_active_server_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css b/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css deleted file mode 100644 index 8355072d08..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css +++ /dev/null @@ -1,138 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* --- Registered servers tree viewlet --- */ -.server-explorer-viewlet .monaco-tree .monaco-tree-row .content .server-group { - cursor: default; - width: 100%; - display: flex; - align-items: center; -} - -/* Bold font style does not go well with CJK fonts */ -.server-explorer-viewlet:lang(zh-Hans) .monaco-tree .monaco-tree-row .server-group, -.server-explorer-viewlet:lang(zh-Hant) .monaco-tree .monaco-tree-row .server-group, -.server-explorer-viewlet:lang(ja) .monaco-tree .monaco-tree-row .server-group, -.server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group { font-weight: normal; } - -/* High Contrast Theming */ -.monaco-workbench.hc-black .server-explorer-viewlet .server-group { - line-height: 20px; -} - -.monaco-workbench > .activitybar .monaco-action-bar .action-label.serverTree { - background-size: 22px; - background-repeat: no-repeat; - background-position: 50% !important; -} - -.server-explorer-viewlet .object-explorer-view { - height: calc(100% - 36px); -} - -.server-explorer-viewlet .server-group { - height: 38px; - line-height: 38px; - 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; -} - -/* display action buttons on hover */ -.server-explorer-viewlet .monaco-tree .monaco-tree-row > .content { - display: flex; -} - -/* Added to display the tree in connection dialog */ -.server-explorer-viewlet { - height: 100%; -} - -.explorer-servers { - height: 100%; -} - -/* search box */ -.server-explorer-viewlet .search-box { - padding-bottom: 4px; - margin: auto; - width: 95%; -} - -/* OE and connection element group */ -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile, -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group { - padding: 5px; - overflow: hidden; -} - -/* OE and connection label */ -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .label, -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .label { - text-overflow: ellipsis; - overflow: hidden; -} - -/* OE and connection icon */ -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon, -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .icon { - float: left; - height: 16px; - width: 16px; - padding-right: 10px; -} - -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected { - background: url('connected_active_server.svg') center center no-repeat; -} - -.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected, -.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected{ - background: url('connected_active_server_inverse.svg') center center no-repeat; -} - -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected { - background: url('disconnected_server.svg') center center no-repeat; -} - -.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected, -.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected{ - background: url('disconnected_server_inverse.svg') center center no-repeat; -} - -/* loading for OE node */ -.server-explorer-viewlet .monaco-tree .monaco-tree-rows > .monaco-tree-row > .codicon.in-progress .connection-tile:before, -.server-explorer-viewlet .monaco-tree .monaco-tree-rows > .monaco-tree-row > .codicon.in-progress .object-element-group:before { - position: absolute; - display: block; - width: 36px; - height: 100%; - top: 0; - left: -35px; -} - -.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded.has-children > .content.server-group:before { - background: url('expanded-dark.svg') 50% 50% no-repeat; -} - -.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content.server-group:before { - background: url('collapsed-dark.svg') 50% 50% no-repeat; -} - -/* Add connection button */ -.server-explorer-viewlet .button-section { - padding: 20px; -} diff --git a/src/sql/workbench/contrib/connection/browser/media/disconnected_server.svg b/src/sql/workbench/contrib/connection/browser/media/disconnected_server.svg deleted file mode 100644 index 7ee461c1a7..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/disconnected_server.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/sql/workbench/contrib/connection/browser/media/disconnected_server_inverse.svg b/src/sql/workbench/contrib/connection/browser/media/disconnected_server_inverse.svg deleted file mode 100644 index 6cdf7983ba..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/disconnected_server_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/sql/workbench/contrib/connection/browser/media/expanded-dark.svg b/src/sql/workbench/contrib/connection/browser/media/expanded-dark.svg deleted file mode 100644 index 73d41e6399..0000000000 --- a/src/sql/workbench/contrib/connection/browser/media/expanded-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/dashboard/browser/dashboardActions.ts b/src/sql/workbench/contrib/dashboard/browser/dashboardActions.ts index f188b7d897..f87d809792 100644 --- a/src/sql/workbench/contrib/dashboard/browser/dashboardActions.ts +++ b/src/sql/workbench/contrib/dashboard/browser/dashboardActions.ts @@ -14,7 +14,6 @@ import { Action } from 'vs/base/common/actions'; import { TreeSelectionHandler } from 'sql/workbench/services/objectExplorer/browser/treeSelectionHandler'; import { ObjectExplorerActionsContext, getTreeNode } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; @@ -91,7 +90,7 @@ export class OEManageConnectionAction extends Action { private async doManage(actionContext: ObjectExplorerActionsContext): Promise { let treeNode: TreeNode = undefined; - let connectionProfile: IConnectionProfile = undefined; + let connectionProfile: ConnectionProfile = undefined; if (actionContext instanceof ObjectExplorerActionsContext) { // Must use a real connection profile for this action due to lookup connectionProfile = ConnectionProfile.fromIConnectionProfile(this._capabilitiesService, actionContext.connectionProfile); diff --git a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts index ecba62230a..79a6ecd388 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts @@ -18,13 +18,14 @@ import { } from 'sql/workbench/services/objectExplorer/browser/connectionTreeAction'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ITree } from 'vs/base/parts/tree/browser/tree'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITree } from 'vs/base/parts/tree/browser/tree'; +import { AsyncServerTree } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; export class ConnectionViewletPanel extends ViewPane { @@ -82,7 +83,7 @@ export class ConnectionViewletPanel extends ViewPane { this._root = container; } - get serversTree(): ITree { + get serversTree(): ITree | AsyncServerTree { return this._serverTreeView.tree; } diff --git a/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css b/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css index f0aef69dd4..60c470b96a 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css +++ b/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ /* --- Registered servers tree viewlet --- */ -.server-explorer-viewlet .monaco-tree .monaco-tree-row .content .server-group { +.server-explorer-viewlet .monaco-tree .monaco-tree-row .content .server-group, +.server-explorer-viewlet .monaco-list .monaco-list-row .content .server-group { cursor: default; width: 100%; display: flex; @@ -15,7 +16,13 @@ .server-explorer-viewlet:lang(zh-Hans) .monaco-tree .monaco-tree-row .server-group, .server-explorer-viewlet:lang(zh-Hant) .monaco-tree .monaco-tree-row .server-group, .server-explorer-viewlet:lang(ja) .monaco-tree .monaco-tree-row .server-group, -.server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group { font-weight: normal; } +.server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group, +.server-explorer-viewlet:lang(zh-Hans) .monaco-list .monaco-list-row .server-group, +.server-explorer-viewlet:lang(zh-Hant) .monaco-list .monaco-list-row .server-group, +.server-explorer-viewlet:lang(ja) .monaco-list .monaco-list-row .server-group, +.server-explorer-viewlet:lang(ko) .monaco-list .monaco-list-row .server-group { + font-weight: normal; +} /* High Contrast Theming */ .monaco-workbench.hc-black .server-explorer-viewlet .server-group { @@ -48,14 +55,19 @@ /* Add space beneath the button */ .new-connection .monaco-text-button { -margin-bottom: 2px; + margin-bottom: 2px; } /* display action buttons on hover */ -.server-explorer-viewlet .monaco-tree .monaco-tree-row > .content { +.server-explorer-viewlet .monaco-tree .monaco-tree-row > .content, +.server-explorer-viewlet .monaco-list .monaco-list-row { display: flex; } +.server-explorer-viewlet .monaco-tl-row { + width: 100%; +} + /* Added to display the tree in connection dialog */ .server-explorer-viewlet { height: 100%; @@ -74,7 +86,9 @@ margin-bottom: 2px; /* OE and connection element group */ .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile, -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group { +.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group, +.monaco-list .monaco-list-rows > .monaco-list-row .connection-tile, +.monaco-list .monaco-list-rows > .monaco-list-row .object-element-group { padding-left: 5px; padding-right: 5px; padding-top: 3px; @@ -84,26 +98,33 @@ margin-bottom: 2px; /* OE and connection label */ .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .label, -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .label { +.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .label, +.monaco-list .monaco-list-rows > .monaco-list-row .connection-tile > .label, +.monaco-list .monaco-list-rows > .monaco-list-row .object-element-group > .label { text-overflow: ellipsis; overflow: hidden; } /* OE and connection icon */ .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon, -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .icon { +.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .icon, +.monaco-list .monaco-list-rows > .monaco-list-row .connection-tile > .icon, +.monaco-list .monaco-list-rows > .monaco-list-row .object-element-group > .icon { float: left; height: 16px; width: 16px; padding-right: 10px; } -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page { +.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page, +.monaco-list .monaco-list-rows > .monaco-list-row .connection-tile > .icon.server-page { background: url('default_server.svg') center center no-repeat; } .vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page, -.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page{ +.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page, +.vs-dark .monaco-list .monaco-list-rows > .monaco-list-row .connection-tile > .icon.server-page, +.hc-black .monaco-list .monaco-list-rows > .monaco-list-row .connection-tile > .icon.server-page { background: url('default_server_inverse.svg') center center no-repeat; } @@ -118,11 +139,40 @@ margin-bottom: 2px; left: -35px; } -.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded.has-children > .content.server-group:before { +.monaco-list .connection-tile > .icon.server-page::after { + position: absolute; + height: 0.25rem; + width: 0.25rem; + top: 14px; + left: 45px; + border-radius: 100%; + content:""; + font-size: 100%; + line-height: 100%; + color:white; + text-align:center; + vertical-align:middle; +} + +/* Connected badge */ +.monaco-list .connection-tile > .icon.server-page.connected::after { + border: 0.12rem solid rgba(59, 180, 74, 100%); + background: rgba(59, 180, 74, 100%); +} + +/* Disconnected badge */ +.monaco-list .connection-tile > .icon.server-page.disconnected::after { + border: 0.12rem solid rgba(208, 46, 0, 100%); + background: rgba(255, 255, 255, 80%); +} + +.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded.has-children > .content.server-group:before, +.monaco-list .monaco-list-rows.show-twisties > .monaco-list-row.expanded.has-children > .content.server-group:before { background: url('expanded-dark.svg') 50% 50% no-repeat; } -.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content.server-group:before { +.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content.server-group:before, +.monaco-list .monaco-list-rows.show-twisties > .monaco-list-row.has-children > .content.server-group:before { background: url('collapsed-dark.svg') 50% 50% no-repeat; } diff --git a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts index c38ad8d605..23e6496ca8 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts +++ b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts @@ -38,6 +38,11 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { startsWith } from 'vs/base/common/strings'; import { SERVER_GROUP_CONFIG } from 'sql/workbench/services/serverGroup/common/interfaces'; import { horizontalScrollingKey } from 'vs/platform/list/browser/listService'; +import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { ObjectExplorerActionsContext } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; /** * ServerTreeview implements the dynamic tree view. @@ -48,7 +53,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { private _buttonSection: HTMLElement; private _treeSelectionHandler: TreeSelectionHandler; private _activeConnectionsFilterAction: ActiveConnectionsFilterAction; - private _tree: ITree; + private _tree: ITree | AsyncServerTree; private _onSelectionOrFocusChange: Emitter; private _actionProvider: ServerTreeActionProvider; @@ -59,7 +64,9 @@ export class ServerTreeView extends Disposable implements IServerTreeView { @IThemeService private _themeService: IThemeService, @IErrorMessageService private _errorMessageService: IErrorMessageService, @IConfigurationService private _configurationService: IConfigurationService, - @ICapabilitiesService capabilitiesService: ICapabilitiesService + @ICapabilitiesService capabilitiesService: ICapabilitiesService, + @IContextMenuService private _contextMenuService: IContextMenuService, + @IKeybindingService private _keybindingService: IKeybindingService ) { super(); this._activeConnectionsFilterAction = this._instantiationService.createInstance( @@ -71,9 +78,17 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this._onSelectionOrFocusChange = new Emitter(); this._actionProvider = this._instantiationService.createInstance(ServerTreeActionProvider); capabilitiesService.onCapabilitiesRegistered(async () => { - if (this._connectionManagementService.hasRegisteredServers()) { - await this.refreshTree(); + if (this._tree instanceof AsyncServerTree) { + // Refresh the tree input now that the capabilities are registered so that we can + // get the full ConnectionProfiles with the server info updated properly + const treeInput = TreeUpdateUtils.getTreeInput(this._connectionManagementService); + await this._tree.setInput(treeInput); this._treeSelectionHandler.onTreeActionStateChange(false); + } else { + if (this._connectionManagementService.hasRegisteredServers()) { + await this.refreshTree(); + this._treeSelectionHandler.onTreeActionStateChange(false); + } } }); this.registerCommands(); @@ -97,7 +112,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { return this._actionProvider; } - public get tree(): ITree { + public get tree(): ITree | AsyncServerTree { return this._tree; } @@ -143,11 +158,30 @@ export class ServerTreeView extends Disposable implements IServerTreeView { } const horizontalScrollEnabled: boolean = this._configurationService.getValue(horizontalScrollingKey) || false; - this._tree = this._register(TreeCreationUtils.createRegisteredServersTree(container, this._instantiationService, horizontalScrollEnabled)); - //this._tree.setInput(undefined); + this._tree = this._register(TreeCreationUtils.createServersTree(container, this._instantiationService, this._configurationService, horizontalScrollEnabled)); this._register(this._tree.onDidChangeSelection((event) => this.onSelected(event))); this._register(this._tree.onDidBlur(() => this._onSelectionOrFocusChange.fire())); this._register(this._tree.onDidChangeFocus(() => this._onSelectionOrFocusChange.fire())); + if (this._tree instanceof AsyncServerTree) { + this._register(this._tree.onContextMenu(e => this.onContextMenu(e))); + this._register(this._tree.onMouseDblClick(e => { + // Open dashboard on double click for server and database nodes + let connectionProfile: ConnectionProfile; + if (e.element instanceof ConnectionProfile) { + connectionProfile = e.element; + } else if (e.element instanceof TreeNode) { + if (TreeUpdateUtils.isAvailableDatabaseNode(e.element)) { + connectionProfile = TreeUpdateUtils.getConnectionProfile(e.element); + } + } + if (connectionProfile) { + this._connectionManagementService.showDashboard(connectionProfile); + } + })); + this._register(this._connectionManagementService.onConnectionChanged(() => { + this.refreshTree().catch(err => errors.onUnexpectedError); + })); + } // Theme styler this._register(attachListStyler(this._tree, this._themeService)); @@ -171,7 +205,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this.showError(args.errorMessage); } if (args.connection) { - this.onObjectExplorerSessionCreated(args.connection); + this.onObjectExplorerSessionCreated(args.connection).catch(err => errors.onUnexpectedError); } })); } @@ -182,7 +216,14 @@ export class ServerTreeView extends Disposable implements IServerTreeView { const expandGroups: boolean = this._configurationService.getValue(SERVER_GROUP_CONFIG)[SERVER_GROUP_AUTOEXPAND_CONFIG]; if (expandGroups) { - await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(root)); + if (this._tree instanceof AsyncServerTree) { + await Promise.all(ConnectionProfileGroup.getSubgroups(root).map(subgroup => { + return this._tree.expand(subgroup); + })); + } else { + await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(root)); + } + } if (root && !root.hasValidConnections) { @@ -200,30 +241,55 @@ export class ServerTreeView extends Disposable implements IServerTreeView { return uri && startsWith(uri, ConnectionUtils.uriPrefixes.default) && !isBackupRestoreUri; } - private async handleAddConnectionProfile(newProfile: IConnectionProfile): Promise { - if (newProfile) { - const groups = this._connectionManagementService.getConnectionGroups(); - const profile = ConnectionUtils.findProfileInGroup(newProfile, groups); - if (profile) { - newProfile = profile; - } - } - + private async handleAddConnectionProfile(newProfile?: IConnectionProfile): Promise { if (this._buttonSection) { hide(this._buttonSection); this._activeConnectionsFilterAction.enabled = true; } - const currentSelections = this._tree.getSelection(); - const currentSelectedElement = currentSelections && currentSelections.length >= 1 ? currentSelections[0] : undefined; - const newProfileIsSelected = currentSelectedElement && newProfile ? currentSelectedElement.id === newProfile.id : false; - if (newProfile && currentSelectedElement && !newProfileIsSelected) { - this._tree.clearSelection(); - } - await this.refreshTree(); - if (newProfile && !newProfileIsSelected) { - await this._tree.reveal(newProfile); - this._tree.select(newProfile); + + if (this._tree instanceof AsyncServerTree) { + // When new connection groups are added the event is fired with undefined so + // we still want to refresh the tree in that case to pick up the changes + await this.refreshTree(); + if (newProfile) { + const currentSelections = this._tree.getSelection(); + const currentSelectedElement = currentSelections && currentSelections.length >= 1 ? currentSelections[0] : undefined; + const newProfileIsSelected = currentSelectedElement && currentSelectedElement.id === newProfile.id; + // Clear any other selected elements first + if (currentSelectedElement && !newProfileIsSelected) { + this._tree.setSelection([]); + } + const newConnectionProfile = this.getConnectionInTreeInput(newProfile.id); + if (newConnectionProfile) { + // Re-render to update the connection status badge + this._tree.rerender(newConnectionProfile); + this._tree.setSelection([newConnectionProfile]); + this._tree.expand(newConnectionProfile); + } + } + + } else { + if (newProfile) { + const groups = this._connectionManagementService.getConnectionGroups(); + const profile = ConnectionUtils.findProfileInGroup(newProfile, groups); + if (profile) { + newProfile = profile; + } + } + + const currentSelections = this._tree.getSelection(); + const currentSelectedElement = currentSelections && currentSelections.length >= 1 ? currentSelections[0] : undefined; + const newProfileIsSelected = currentSelectedElement && newProfile ? currentSelectedElement.id === newProfile.id : false; + if (newProfile && currentSelectedElement && !newProfileIsSelected) { + this._tree.clearSelection(); + } + await this.refreshTree(); + if (newProfile && !newProfileIsSelected) { + await this._tree.reveal(newProfile); + this._tree.select(newProfile); + } } + } private showError(errorMessage: string) { @@ -232,66 +298,83 @@ export class ServerTreeView extends Disposable implements IServerTreeView { } } - private getConnectionInTreeInput(connectionId: string): ConnectionProfile { - const root = TreeUpdateUtils.getTreeInput(this._connectionManagementService); - const connections = ConnectionProfileGroup.getConnectionsInGroup(root); - const results = connections.filter(con => { - if (connectionId === con.id) { - return true; - } else { - return false; + /** + * Gets the ConnectionProfile object in the tree for the specified ID, or undefined if it doesn't exist. + * @param connectionId The connection ID to search for + */ + private getConnectionInTreeInput(connectionId: string): ConnectionProfile | undefined { + if (this._tree instanceof AsyncServerTree) { + const root = this._tree.getInput(); + const connections = ConnectionProfileGroup.getConnectionsInGroup(root); + return connections.find(conn => conn.id === connectionId); + } else { + const root = TreeUpdateUtils.getTreeInput(this._connectionManagementService); + const connections = ConnectionProfileGroup.getConnectionsInGroup(root); + const results = connections.filter(con => { + if (connectionId === con.id) { + return true; + } else { + return false; + } + }); + if (results && results.length > 0) { + return results[0]; } - }); - if (results && results.length > 0) { - return results[0]; + return undefined; } - return null; } - private onObjectExplorerSessionCreated(connection: IConnectionProfile) { - const conn = this.getConnectionInTreeInput(connection.id); - if (conn) { - this._tree.refresh(conn).then(() => { - return this._tree.expand(conn).then(() => { - return this._tree.reveal(conn, 0.5).then(() => { - this._treeSelectionHandler.onTreeActionStateChange(false); - }); - }); - }).then(null, errors.onUnexpectedError); + private async onObjectExplorerSessionCreated(connection: IConnectionProfile): Promise { + const element = this.getConnectionInTreeInput(connection.id); + if (element) { + if (this._tree instanceof AsyncServerTree) { + this._tree.rerender(element); + } else { + await this._tree.refresh(element); + } + await this._tree.expand(element); + await this._tree.reveal(element, 0.5); + this._treeSelectionHandler.onTreeActionStateChange(false); } } public addObjectExplorerNodeAndRefreshTree(connection: IConnectionProfile): void { hide(this.messages); if (!this._objectExplorerService.getObjectExplorerNode(connection)) { - this._objectExplorerService.updateObjectExplorerNodes(connection).then(() => { - // The oe request is sent. an event will be raised when the session is created - }, error => { - }); + this._objectExplorerService.updateObjectExplorerNodes(connection).catch(e => errors.onUnexpectedError(e)); } } - public deleteObjectExplorerNodeAndRefreshTree(connection: IConnectionProfile): Promise { + public async deleteObjectExplorerNodeAndRefreshTree(connection: IConnectionProfile): Promise { if (connection) { const conn = this.getConnectionInTreeInput(connection.id); if (conn) { - return this._objectExplorerService.deleteObjectExplorerNode(conn).then(async () => { + await this._objectExplorerService.deleteObjectExplorerNode(conn); + if (this._tree instanceof AsyncServerTree) { + // Collapse the node before refreshing so the refresh doesn't try to fetch + // the children again (which causes it to try and connect) + this._tree.collapse(conn); + await this.refreshTree(); + } else { await this._tree.collapse(conn); return this._tree.refresh(conn); - }); + } } } - return Promise.resolve(); } - public refreshTree(): Promise { + public async refreshTree(): Promise { hide(this.messages); this.clearOtherActions(); return TreeUpdateUtils.registeredServerUpdate(this._tree, this._connectionManagementService); } - public refreshElement(element: any): Promise { - return this._tree.refresh(element); + public async refreshElement(element: ServerTreeElement): Promise { + if (this._tree instanceof AsyncServerTree) { + return this._tree.updateChildren(element); + } else { + return this._tree.refresh(element); + } } /** @@ -350,9 +433,19 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this._tree.setInput(treeInput).then(async () => { if (isHidden(this.messages)) { this._tree.getFocus(); - await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(treeInput)); + if (this._tree instanceof AsyncServerTree) { + await Promise.all(ConnectionProfileGroup.getSubgroups(treeInput).map(subgroup => { + this._tree.expand(subgroup); + })); + } else { + await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(treeInput)); + } } else { - this._tree.clearFocus(); + if (this._tree instanceof AsyncServerTree) { + this._tree.setFocus([]); + } else { + this._tree.clearFocus(); + } } }, errors.onUnexpectedError); } else { @@ -382,9 +475,19 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this._tree.setInput(treeInput).then(async () => { if (isHidden(this.messages)) { this._tree.getFocus(); - await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(treeInput)); + if (this._tree instanceof AsyncServerTree) { + await Promise.all(ConnectionProfileGroup.getSubgroups(treeInput).map(subgroup => { + this._tree.expand(subgroup); + })); + } else { + await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(treeInput)); + } } else { - this._tree.clearFocus(); + if (this._tree instanceof AsyncServerTree) { + this._tree.setFocus([]); + } else { + this._tree.clearFocus(); + } } }, errors.onUnexpectedError); } @@ -450,17 +553,6 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this._tree.layout(height); } - /** - * set the visibility of the view - */ - public setVisible(visible: boolean): void { - if (visible) { - this._tree.onVisible(); - } else { - this._tree.onHidden(); - } - } - /** * Get the list of selected nodes in the tree */ @@ -472,48 +564,108 @@ export class ServerTreeView extends Disposable implements IServerTreeView { * Get whether the tree view currently has focus */ public isFocused(): boolean { - return this._tree.isDOMFocused(); + return this._tree.getHTMLElement() === document.activeElement; } /** * Set whether the given element is expanded or collapsed */ - public setExpandedState(element: TreeNode | ConnectionProfile, expandedState: TreeItemCollapsibleState): Promise { + public async setExpandedState(element: ServerTreeElement, expandedState: TreeItemCollapsibleState): Promise { if (expandedState === TreeItemCollapsibleState.Collapsed) { return this._tree.collapse(element); } else if (expandedState === TreeItemCollapsibleState.Expanded) { return this._tree.expand(element); } - return Promise.resolve(); } /** * Reveal the given element in the tree */ - public reveal(element: TreeNode | ConnectionProfile): Promise { + public async reveal(element: ServerTreeElement): Promise { return this._tree.reveal(element); } /** * Select the given element in the tree and clear any other selections */ - public setSelected(element: TreeNode | ConnectionProfile, selected: boolean, clearOtherSelections: boolean): Promise { + public async setSelected(element: ServerTreeElement, selected: boolean, clearOtherSelections: boolean): Promise { if (clearOtherSelections || (selected && clearOtherSelections !== false)) { - this._tree.clearSelection(); + if (this._tree instanceof AsyncServerTree) { + this._tree.setSelection([]); + } else { + this._tree.clearSelection(); + } + } if (selected) { - this._tree.select(element); - return this._tree.reveal(element); + if (this._tree instanceof AsyncServerTree) { + this._tree.setSelection(this._tree.getSelection().concat(element)); + this._tree.reveal(element); + } else { + this._tree.select(element); + return this._tree.reveal(element); + } } else { - this._tree.deselect(element); - return Promise.resolve(); + if (this._tree instanceof AsyncServerTree) { + this._tree.setSelection(this._tree.getSelection().filter(item => item !== element)); + } else { + this._tree.deselect(element); + } } } /** * Check if the given element in the tree is expanded */ - public isExpanded(element: TreeNode | ConnectionProfile): boolean { - return this._tree.isExpanded(element); + public isExpanded(element: ServerTreeElement): boolean { + if (this._tree instanceof AsyncServerTree) { + return !this._tree.getNode(element).collapsed; + } else { + return this._tree.isExpanded(element); + } + + } + + /** + * Return actions in the context menu + */ + private onContextMenu(e: ITreeContextMenuEvent): boolean { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + this._tree.setSelection([e.element]); + + let actionContext: any; + if (e.element instanceof TreeNode) { + let context = new ObjectExplorerActionsContext(); + context.nodeInfo = e.element.toNodeInfo(); + // Note: getting DB name before, but intentionally not using treeUpdateUtils.getConnectionProfile as it replaces + // the connection ID with a new one. This breaks a number of internal tasks + context.connectionProfile = e.element.getConnectionProfile().toIConnectionProfile(); + context.connectionProfile.databaseName = e.element.getDatabaseName(); + actionContext = context; + } else if (e.element instanceof ConnectionProfile) { + let context = new ObjectExplorerActionsContext(); + context.connectionProfile = e.element.toIConnectionProfile(); + context.isConnectionNode = true; + actionContext = context; + } else { + // TODO: because the connection group is used as a context object and isn't serializable, + // the Group-level context menu is not currently extensible + actionContext = e.element; + } + + this._contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => this._actionProvider.getActions(this._tree, e.element), + getKeyBinding: (action) => this._keybindingService.lookupKeybinding(action.id), + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this._tree.domFocus(); + } + }, + getActionsContext: () => (actionContext) + }); + + return true; } } diff --git a/src/sql/workbench/contrib/objectExplorer/common/serverGroup.contribution.ts b/src/sql/workbench/contrib/objectExplorer/common/serverGroup.contribution.ts index 48882f1c2d..15aa1ebe69 100644 --- a/src/sql/workbench/contrib/objectExplorer/common/serverGroup.contribution.ts +++ b/src/sql/workbench/contrib/objectExplorer/common/serverGroup.contribution.ts @@ -8,6 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; import { SERVER_GROUP_CONFIG, SERVER_GROUP_COLORS_CONFIG } from 'sql/workbench/services/serverGroup/common/interfaces'; +import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/common/serverGroupViewModel'; const configurationRegistry = Registry.as(Extensions.Configuration); @@ -29,7 +30,7 @@ const serverGroupConfig: IConfigurationNode = { '#98AFC7', '#4452A6', '#6A6599', - '#515151' + DefaultServerGroupColor ] }, [SERVER_GROUP_CONFIG + '.' + SERVER_GROUP_AUTOEXPAND_CONFIG]: { @@ -40,4 +41,18 @@ const serverGroupConfig: IConfigurationNode = { } }; +const serverTreeConfig: IConfigurationNode = { + 'id': 'serverTree', + 'title': 'Server Tree', + 'type': 'object', + 'properties': { + 'serverTree.useAsyncServerTree': { + 'type': 'boolean', + 'default': true, + 'description': localize('serverTree.useAsyncServerTree', "(Preview) Use the new async server tree for the Servers view and Connection Dialog with support for new features such as dynamic node filtering.") + } + } +}; + configurationRegistry.registerConfiguration(serverGroupConfig); +configurationRegistry.registerConfiguration(serverTreeConfig); diff --git a/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts b/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts index 6384b6454a..28725e9b61 100644 --- a/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts +++ b/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts @@ -20,8 +20,6 @@ import * as LocalizedConstants from 'sql/workbench/services/connection/browser/ import { ObjectExplorerService, ObjectExplorerNodeEventArgs } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType'; -import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; -import { ServerTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/serverTreeDataSource'; import { Emitter, Event } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import { ObjectExplorerActionsContext } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions'; @@ -35,6 +33,12 @@ import { IViewsService, IView, ViewContainerLocation, ViewContainer, IViewPaneCo import { ConsoleLogService } from 'vs/platform/log/common/log'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { TestAccessibilityService, TestListService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; +import { ServerTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/serverTreeDataSource'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { AsyncServerTree } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; suite('SQL Connection Tree Action tests', () => { let errorMessageService: TypeMoq.Mock; @@ -152,8 +156,14 @@ suite('SQL Connection Tree Action tests', () => { } }; - let manageConnectionAction: OEManageConnectionAction = new OEManageConnectionAction(OEManageConnectionAction.ID, - OEManageConnectionAction.LABEL, connectionManagementService.object, capabilitiesService, instantiationService.object, objectExplorerService.object, viewsService); + let manageConnectionAction: OEManageConnectionAction = new OEManageConnectionAction( + OEManageConnectionAction.ID, + OEManageConnectionAction.LABEL, + connectionManagementService.object, + capabilitiesService, + instantiationService.object, + objectExplorerService.object, + viewsService); let actionContext = new ObjectExplorerActionsContext(); actionContext.connectionProfile = connection.toIConnectionProfile(); @@ -190,8 +200,14 @@ suite('SQL Connection Tree Action tests', () => { return treeSelectionMock.object; }); - let manageConnectionAction: OEManageConnectionAction = new OEManageConnectionAction(OEManageConnectionAction.ID, - OEManageConnectionAction.LABEL, connectionManagementService.object, capabilitiesService, instantiationService.object, objectExplorerService.object, undefined); + let manageConnectionAction: OEManageConnectionAction = new OEManageConnectionAction( + OEManageConnectionAction.ID, + OEManageConnectionAction.LABEL, + connectionManagementService.object, + capabilitiesService, + instantiationService.object, + objectExplorerService.object, + undefined); let actionContext = new ObjectExplorerActionsContext(); actionContext.connectionProfile = connection.toIConnectionProfile(); @@ -444,7 +460,7 @@ suite('SQL Connection Tree Action tests', () => { tree.setup(x => x.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve(null)); tree.setup(x => x.expand(TypeMoq.It.isAny())).returns(() => Promise.resolve(null)); tree.setup(x => x.collapse(TypeMoq.It.isAny())).returns(() => Promise.resolve(null)); - let connectionAction: RefreshAction = new RefreshAction(RefreshAction.ID, + let refreshAction: RefreshAction = new RefreshAction(RefreshAction.ID, RefreshAction.LABEL, tree.object, connection, @@ -453,7 +469,7 @@ suite('SQL Connection Tree Action tests', () => { undefined, logService); - return connectionAction.run().then((value) => { + return refreshAction.run().then((value) => { connectionManagementService.verify(x => x.isConnected(undefined, TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); objectExplorerService.verify(x => x.getObjectExplorerNode(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); objectExplorerService.verify(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); @@ -547,6 +563,200 @@ suite('SQL Connection Tree Action tests', () => { }); }); + // chgagnon TODO - skipping for now since mocking and instanceof don't work well together. Will re-enable once old tree is removed + test.skip('RefreshConnectionAction - AsyncServerTree - refresh should be called if connection status is connect', () => { + let isConnectedReturnValue: boolean = true; + let sqlProvider = { + providerId: mssqlProviderName, + displayName: 'MSSQL', + connectionOptions: [], + }; + + capabilitiesService.capabilities[mssqlProviderName] = { connection: sqlProvider }; + + let connection = new ConnectionProfile(capabilitiesService, { + connectionName: 'Test', + savePassword: false, + groupFullName: 'testGroup', + serverName: 'testServerName', + databaseName: 'testDatabaseName', + authenticationType: 'inetgrated', + password: 'test', + userName: 'testUsername', + groupId: undefined, + providerName: mssqlProviderName, + options: {}, + saveProfile: true, + id: 'testID' + }); + let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined); + conProfGroup.connections = [connection]; + let connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + connectionManagementService.callBase = true; + connectionManagementService.setup(x => x.getConnectionGroups()).returns(() => [conProfGroup]); + connectionManagementService.setup(x => x.getActiveConnections()).returns(() => [connection]); + connectionManagementService.setup(x => x.addSavedPassword(TypeMoq.It.isAny())).returns(() => new Promise((resolve) => { + resolve(connection); + })); + connectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => isConnectedReturnValue); + + let objectExplorerSession = { + success: true, + sessionId: '1234', + rootNode: { + nodePath: 'testServerName\tables', + nodeType: NodeType.Folder, + label: 'Tables', + isLeaf: false, + metadata: null, + nodeSubType: '', + nodeStatus: '', + errorMessage: '' + }, + errorMessage: '' + }; + + let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null, undefined, undefined); + tablesNode.connection = connection; + tablesNode.session = objectExplorerSession; + let table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined); + let table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined); + tablesNode.children = [table1Node, table2Node]; + let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Loose, connectionManagementService.object); + objectExplorerService.callBase = true; + objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => tablesNode); + objectExplorerService.setup(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([table1Node, table2Node])); + const treeMock = TypeMoq.Mock.ofType(AsyncServerTree, TypeMoq.MockBehavior.Strict, + 'ConnectionTreeActionsTest', // user + $('div'), // container + {}, // delegate + [], // renderers + {}, // data source + {}, // options + new MockContextKeyService(), // IContextKeyService + new TestListService(), // IListService, + undefined, // IThemeService, + new TestConfigurationService(), // IConfigurationService, + undefined, // IKeybindingService, + new TestAccessibilityService()); // IAccessibilityService + treeMock.callBase = true; + treeMock.setup(x => x.expand(TypeMoq.It.isAny())).returns(() => Promise.resolve(null)); + treeMock.setup(x => x.collapse(TypeMoq.It.isAny())).returns(() => true); + treeMock.setup(x => x.updateChildren(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + let refreshAction: RefreshAction = new RefreshAction(RefreshAction.ID, + RefreshAction.LABEL, + treeMock.instance, + connection, + connectionManagementService.object, + objectExplorerService.object, + undefined, + logService); + + return refreshAction.run().then((value) => { + connectionManagementService.verify(x => x.isConnected(undefined, TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); + objectExplorerService.verify(x => x.getObjectExplorerNode(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); + objectExplorerService.verify(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); + treeMock.verify(x => x.updateChildren(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); + }); + }); + + test('RefreshConnectionAction - AsyncServerTree - refresh should not be called if connection status is not connect', () => { + let isConnectedReturnValue: boolean = false; + let sqlProvider = { + providerId: mssqlProviderName, + displayName: 'MSSQL', + connectionOptions: [] + }; + + capabilitiesService.capabilities[mssqlProviderName] = { connection: sqlProvider }; + + let connection = new ConnectionProfile(capabilitiesService, { + connectionName: 'Test', + savePassword: false, + groupFullName: 'testGroup', + serverName: 'testServerName', + databaseName: 'testDatabaseName', + authenticationType: 'inetgrated', + password: 'test', + userName: 'testUsername', + groupId: undefined, + providerName: mssqlProviderName, + options: {}, + saveProfile: true, + id: 'testID' + }); + let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined); + conProfGroup.connections = [connection]; + let connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + connectionManagementService.callBase = true; + connectionManagementService.setup(x => x.getConnectionGroups()).returns(() => [conProfGroup]); + connectionManagementService.setup(x => x.getActiveConnections()).returns(() => [connection]); + connectionManagementService.setup(x => x.addSavedPassword(TypeMoq.It.isAny())).returns(() => new Promise((resolve) => { + resolve(connection); + })); + connectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => isConnectedReturnValue); + + let objectExplorerSession = { + success: true, + sessionId: '1234', + rootNode: { + nodePath: 'testServerName\tables', + nodeType: NodeType.Folder, + label: 'Tables', + isLeaf: false, + metadata: null, + nodeSubType: '', + nodeStatus: '', + errorMessage: '' + }, + errorMessage: '' + }; + + let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null, undefined, undefined); + tablesNode.connection = connection; + tablesNode.session = objectExplorerSession; + let table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined); + let table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined); + tablesNode.children = [table1Node, table2Node]; + let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Loose, connectionManagementService.object); + objectExplorerService.callBase = true; + objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => tablesNode); + objectExplorerService.setup(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([table1Node, table2Node])); + let tree = TypeMoq.Mock.ofType(AsyncServerTree, TypeMoq.MockBehavior.Loose, + 'ConnectionTreeActionsTest', // user + $('div'), // container + {}, // delegate + [], // renderers + {}, // data source + {}, // options + new MockContextKeyService(), // IContextKeyService + new TestListService(), // IListService, + undefined, // IThemeService, + new TestConfigurationService(), // IConfigurationService, + undefined, // IKeybindingService, + new TestAccessibilityService()); // IAccessibilityService + tree.callBase = true; + + tree.setup(x => x.updateChildren(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + tree.setup(x => x.expand(TypeMoq.It.isAny())).returns(() => Promise.resolve(null)); + let connectionAction: RefreshAction = new RefreshAction(RefreshAction.ID, + RefreshAction.LABEL, + tree.object, + connection, + connectionManagementService.object, + objectExplorerService.object, + undefined, + logService); + + return connectionAction.run().then((value) => { + connectionManagementService.verify(x => x.isConnected(undefined, TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); + objectExplorerService.verify(x => x.getObjectExplorerNode(TypeMoq.It.isAny()), TypeMoq.Times.exactly(0)); + objectExplorerService.verify(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(0)); + tree.verify(x => x.updateChildren(TypeMoq.It.isAny()), TypeMoq.Times.exactly(0)); + tree.verify(x => x.expand(TypeMoq.It.isAny()), TypeMoq.Times.exactly(0)); + }); + }); + test('EditConnectionAction - test if show connection dialog is called', () => { let connectionManagementService = createConnectionManagementService(true, undefined); diff --git a/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts b/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts index dcef94428b..40a070e2f7 100644 --- a/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts +++ b/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts @@ -39,7 +39,7 @@ suite('ServerTreeView onAddConnectionProfile handler tests', () => { ); mockConnectionManagementService.setup(x => x.getConnectionGroups()).returns(x => []); mockConnectionManagementService.setup(x => x.hasRegisteredServers()).returns(() => true); - serverTreeView = new ServerTreeView(mockConnectionManagementService.object, instantiationService, undefined, new TestThemeService(), undefined, undefined, capabilitiesService); + serverTreeView = new ServerTreeView(mockConnectionManagementService.object, instantiationService, undefined, new TestThemeService(), undefined, undefined, capabilitiesService, undefined, undefined); mockTree = TypeMoq.Mock.ofType(TestTree); (serverTreeView as any)._tree = mockTree.object; mockRefreshTreeMethod = TypeMoq.Mock.ofType(Function); @@ -64,15 +64,6 @@ suite('ServerTreeView onAddConnectionProfile handler tests', () => { mockTree.verify(x => x.layout(TypeMoq.It.isAnyNumber()), TypeMoq.Times.once()); }); - test('setVisibility', async () => { - mockTree.setup(x => x.onVisible()); - mockTree.setup(x => x.onHidden()); - serverTreeView.setVisible(true); - mockTree.verify(x => x.onVisible(), TypeMoq.Times.once()); - serverTreeView.setVisible(false); - mockTree.verify(x => x.onHidden(), TypeMoq.Times.once()); - }); - test('getSelection', async () => { mockTree.setup(x => x.getSelection()); @@ -81,9 +72,9 @@ suite('ServerTreeView onAddConnectionProfile handler tests', () => { }); test('isFocused', async () => { - mockTree.setup(x => x.isDOMFocused()); + mockTree.setup(x => x.getHTMLElement()); serverTreeView.isFocused(); - mockTree.verify(x => x.isDOMFocused(), TypeMoq.Times.once()); + mockTree.verify(x => x.getHTMLElement(), TypeMoq.Times.once()); }); test('reveal', async () => { diff --git a/src/sql/workbench/contrib/scripting/browser/scriptingActions.ts b/src/sql/workbench/contrib/scripting/browser/scriptingActions.ts index c3bda5f57a..685d416bff 100644 --- a/src/sql/workbench/contrib/scripting/browser/scriptingActions.ts +++ b/src/sql/workbench/contrib/scripting/browser/scriptingActions.ts @@ -27,6 +27,7 @@ import { getErrorMessage } from 'vs/base/common/errors'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AsyncServerTree } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; //#region -- Data Explorer export const SCRIPT_AS_CREATE_COMMAND_ID = 'dataExplorer.scriptAsCreate'; @@ -322,7 +323,11 @@ export async function handleOeRefreshCommand(accessor: ServicesAccessor, args: O const tree = objectExplorerService.getServerTreeView().tree; try { await objectExplorerService.refreshTreeNode(treeNode.getSession(), treeNode); - await tree.refresh(treeNode); + if (tree instanceof AsyncServerTree) { + await tree.updateChildren(treeNode); + } else { + await tree.refresh(treeNode); + } } catch (err) { // Display message to the user but also log the entire error to the console for the stack trace notificationService.error(localize('refreshError', "An error occurred refreshing node '{0}': {1}", args.nodeInfo.label, getErrorMessage(err))); diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index a48fc71566..0b7647f5bd 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -10,6 +10,7 @@ import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; import { Modal } from 'sql/workbench/browser/modal/modal'; import { IConnectionManagementService, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement'; import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper'; +import { TreeCreationUtils } from 'sql/workbench/services/objectExplorer/browser/treeCreationUtils'; import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -39,15 +40,16 @@ import { ViewPane, IPaneColors } from 'vs/workbench/browser/parts/views/viewPane import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ITreeView } from 'sql/workbench/common/views'; import { IConnectionProfile } from 'azdata'; -import { ITree } from 'vs/base/parts/tree/browser/tree'; import { TreeUpdateUtils, IExpandableTree } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; import { SavedConnectionTreeController } from 'sql/workbench/services/connection/browser/savedConnectionTreeController'; -import { TreeCreationUtils } from 'sql/workbench/services/objectExplorer/browser/treeCreationUtils'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults'; -import { RecentConnectionTreeController, RecentConnectionActionsProvider } from 'sql/workbench/services/connection/browser/recentConnectionTreeController'; +import { RecentConnectionActionsProvider, RecentConnectionTreeController } from 'sql/workbench/services/connection/browser/recentConnectionTreeController'; import { ClearRecentConnectionsAction } from 'sql/workbench/services/connection/browser/connectionActions'; import { combinedDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ITree } from 'vs/base/parts/tree/browser/tree'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export interface OnShowUIResponse { selectedProviderDisplayName: string; @@ -65,8 +67,8 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer private _closeButton: Button; private _providerTypeSelectBox: SelectBox; private _newConnectionParams: INewConnectionParams; - private _recentConnectionTree: ITree; - private _savedConnectionTree: ITree; + private _recentConnectionTree: AsyncServerTree | ITree; + private _savedConnectionTree: AsyncServerTree | ITree; private _connectionUIContainer: HTMLElement; private _databaseDropdownExpanded: boolean; private _actionbar: ActionBar; @@ -115,7 +117,8 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer @IContextKeyService contextKeyService: IContextKeyService, @IClipboardService clipboardService: IClipboardService, @ILogService logService: ILogService, - @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, + @IConfigurationService private _configurationService: IConfigurationService ) { super( localize('connection', "Connection"), @@ -222,21 +225,37 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer }); this._panel.onTabChange(async c => { - // convert to old VS Code tree interface with expandable methods - const expandableTree: IExpandableTree = this._savedConnectionTree; + if (this._savedConnectionTree instanceof AsyncServerTree) { + if (c === savedConnectionTabId && this._savedConnectionTree.contentHeight === 0) { + // Update saved connection tree + await TreeUpdateUtils.structuralTreeUpdate(this._savedConnectionTree, 'saved', this.connectionManagementService, this._providers); - if (c === savedConnectionTabId && expandableTree.getContentHeight() === 0) { - // Update saved connection tree - await TreeUpdateUtils.structuralTreeUpdate(this._savedConnectionTree, 'saved', this.connectionManagementService, this._providers); - - if (expandableTree.getContentHeight() > 0) { - DOM.hide(this._noSavedConnection); - DOM.show(this._savedConnection); - } else { - DOM.show(this._noSavedConnection); - DOM.hide(this._savedConnection); + if (this._savedConnectionTree.contentHeight > 0) { + DOM.hide(this._noSavedConnection); + DOM.show(this._savedConnection); + } else { + DOM.show(this._noSavedConnection); + DOM.hide(this._savedConnection); + } + this._savedConnectionTree.layout(DOM.getTotalHeight(this._savedConnectionTree.getHTMLElement())); + } + } else { + // convert to old VS Code tree interface with expandable methods + const expandableTree: IExpandableTree = this._savedConnectionTree; + + if (c === savedConnectionTabId && expandableTree.getContentHeight() === 0) { + // Update saved connection tree + await TreeUpdateUtils.structuralTreeUpdate(this._savedConnectionTree, 'saved', this.connectionManagementService, this._providers); + + if (expandableTree.getContentHeight() > 0) { + DOM.hide(this._noSavedConnection); + DOM.show(this._savedConnection); + } else { + DOM.show(this._noSavedConnection); + DOM.hide(this._savedConnection); + } + this._savedConnectionTree.layout(DOM.getTotalHeight(this._savedConnectionTree.getHTMLElement())); } - this._savedConnectionTree.layout(DOM.getTotalHeight(this._savedConnectionTree.getHTMLElement())); } }); @@ -362,9 +381,7 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer const leftClick = (element: any, eventish: ICancelableEvent, origin: string) => { // element will be a server group if the tree is clicked rather than a item const isDoubleClick = origin === 'mouse' && (eventish as MouseEvent).detail === 2; - if (element instanceof ConnectionProfile) { - this.onConnectionClick(element, isDoubleClick); - } + this.onConnectionClick(element, isDoubleClick); }; const actionProvider = this.instantiationService.createInstance(RecentConnectionActionsProvider); const controller = new RecentConnectionTreeController(leftClick, actionProvider, this.connectionManagementService, this.contextMenuService); @@ -380,7 +397,11 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer // We're just using the connections to determine if there are connections to show, dispose them right after to clean up their handlers recentConnections.forEach(conn => conn.dispose()); }); - this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer, this.instantiationService, controller); + 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.onConnectionClick(e.element, false)); + this._recentConnectionTree.onMouseDblClick(e => this.onConnectionClick(e.element, true)); + } // Theme styler this._register(styler.attachListStyler(this._recentConnectionTree, this._themeService)); @@ -406,7 +427,11 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer }; const controller = new SavedConnectionTreeController(leftClick); - this._savedConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer, this.instantiationService, controller); + this._savedConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer, this.instantiationService, this._configurationService, localize('connectionDialog.savedConnections', "Saved Connections"), controller); + if (this._savedConnectionTree instanceof AsyncServerTree) { + this._savedConnectionTree.onMouseClick(e => this.onConnectionClick(e.element, false)); + this._savedConnectionTree.onMouseDblClick(e => this.onConnectionClick(e.element, true)); + } // Theme styler this._register(styler.attachListStyler(this._savedConnectionTree, this._themeService)); @@ -419,7 +444,10 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer DOM.append(noSavedConnectionContainer, DOM.$('.no-saved-connections')).innerText = noSavedConnectionLabel; } - private onConnectionClick(element: IConnectionProfile, connect: boolean = false) { + private onConnectionClick(element: ServerTreeElement, connect: boolean = false): void { + if (!(element instanceof ConnectionProfile)) { + return; + } if (connect) { this.connect(element); } else { @@ -443,9 +471,12 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer DOM.show(this._noRecentConnection); } await TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this.connectionManagementService, this._providers); + this._recentConnectionTree.layout(DOM.getTotalHeight(this._recentConnectionTree.getHTMLElement())); - // // reset saved connection tree - await this._savedConnectionTree.setInput([]); + if (!(this._savedConnectionTree instanceof AsyncServerTree)) { + // reset saved connection tree + await this._savedConnectionTree.setInput([]); + } // call layout with view height this.initDialog(); @@ -616,7 +647,7 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer const disposable = combinedDisposable(pane, paneStyler); const paneItem = { pane, disposable }; treeView.onDidChangeSelection(e => { - if (e.length > 0 && e[0].payload) { + if (e.length > 0 && e[0].payload instanceof ConnectionProfile) { this.onConnectionClick(e[0].payload); } }); diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index 4a41a673f8..ffcbc465b0 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -995,6 +995,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti let id = Utils.generateUri(source); this._telemetryService.sendActionEvent(TelemetryKeys.TelemetryView.Shell, TelemetryKeys.MoveServerGroup); return this._connectionStore.changeGroupIdForConnection(source, targetGroupId).then(result => { + this._onAddConnectionProfile.fire(source); if (id && targetGroupId) { source.groupId = targetGroupId; } diff --git a/src/sql/workbench/services/connection/browser/media/connectionDialog.css b/src/sql/workbench/services/connection/browser/media/connectionDialog.css index 0907b33bdc..3f74be6da1 100644 --- a/src/sql/workbench/services/connection/browser/media/connectionDialog.css +++ b/src/sql/workbench/services/connection/browser/media/connectionDialog.css @@ -162,3 +162,8 @@ width: 8px; height: 8px; } + +.connection-dialog .explorer-servers .connection-profile .monaco-tl-twistie { + /* Hide twisties */ + display: none !important; +} diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource.ts b/src/sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource.ts new file mode 100644 index 0000000000..f31724bed2 --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; + +/** + * Implements the DataSource(that returns a parent/children of an element) for the recent connection tree + */ +export class AsyncRecentConnectionTreeDataSource implements IAsyncDataSource { + + /** + * Returns a boolean value indicating whether the element has children. + */ + public hasChildren(element: ServerTreeElement): boolean { + if (element instanceof ConnectionProfileGroup) { + return element.hasChildren(); + } + return false; + } + + /** + * Returns the element's children as an array in a promise. + */ + public async getChildren(element: ServerTreeElement): Promise> { + if (element instanceof ConnectionProfileGroup) { + return element.getChildren(); + } else { + return []; + } + } +} diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTree.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTree.ts new file mode 100644 index 0000000000..c2c698a98e --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTree.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; + +export class AsyncServerTree extends WorkbenchAsyncDataTree { } + +export type ServerTreeElement = ConnectionProfile | ConnectionProfileGroup | TreeNode; diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDataSource.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDataSource.ts new file mode 100644 index 0000000000..5cd182cd11 --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDataSource.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; +import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; +import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import Severity from 'vs/base/common/severity'; +import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; +import { IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; + +/** + * Implements the DataSource(that returns a parent/children of an element) for the server tree + */ +export class AsyncServerTreeDataSource implements IAsyncDataSource { + + constructor( + @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, + @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, + @IErrorMessageService private _errorMessageService: IErrorMessageService + ) { + } + /** + * Returns a boolean value indicating whether the element has children. + */ + public hasChildren(element: ServerTreeElement): boolean { + if (element instanceof ConnectionProfile) { + return true; + } else if (element instanceof ConnectionProfileGroup) { + return element.hasChildren(); + } else if (element instanceof TreeNode) { + return !element.isAlwaysLeaf; + } + return false; + } + + /** + * Returns the element's children as an array in a promise. + */ + public async getChildren(element: ServerTreeElement): Promise { + try { + if (element instanceof ConnectionProfile) { + return await TreeUpdateUtils.getAsyncConnectionNodeChildren(element, this._connectionManagementService, this._objectExplorerService); + } else if (element instanceof ConnectionProfileGroup) { + return (element as ConnectionProfileGroup).getChildren(); + } else if (element instanceof TreeNode) { + if (element.children) { + return element.children; + } else { + return await this._objectExplorerService.resolveTreeNodeChildren(element.getSession(), element); + } + } + } catch (err) { + if (element instanceof TreeNode) { + element.errorStateMessage = err.message ?? err; + } + if (err.message) { + this.showError(err.message); + } + + throw err; + } + return []; + } + + private showError(errorMessage: string) { + if (this._errorMessageService) { + this._errorMessageService.showDialog(Severity.Error, '', errorMessage); + } + } +} diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDelegate.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDelegate.ts new file mode 100644 index 0000000000..5fddf52397 --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDelegate.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ServerTreeRenderer } from 'sql/workbench/services/objectExplorer/browser/serverTreeRenderer'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; + +export class AsyncServerTreeDelegate implements IListVirtualDelegate { + + getHeight(element: ServerTreeElement): number { + if (element instanceof ConnectionProfileGroup) { + return ServerTreeRenderer.CONNECTION_GROUP_HEIGHT; + } else if (element instanceof ConnectionProfile) { + return ServerTreeRenderer.CONNECTION_HEIGHT; + } else { + return ServerTreeRenderer.OBJECTEXPLORER_HEIGHT; + } + } + + getTemplateId(element: ServerTreeElement): string { + if (element instanceof ConnectionProfileGroup) { + return ServerTreeRenderer.CONNECTION_GROUP_TEMPLATE_ID; + } else if (element instanceof ConnectionProfile) { + return ServerTreeRenderer.CONNECTION_TEMPLATE_ID; + } else { + return ServerTreeRenderer.OBJECTEXPLORER_TEMPLATE_ID; + } + } +} diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDragAndDrop.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDragAndDrop.ts new file mode 100644 index 0000000000..f1689594e5 --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeDragAndDrop.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; +import { IDragAndDropData } from 'vs/base/browser/dnd'; +import { ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverReactions } from 'vs/base/browser/ui/tree/tree'; +import { ServerTreeDragAndDrop } from 'sql/workbench/services/objectExplorer/browser/dragAndDropController'; +import { IDragAndDrop } from 'vs/base/parts/tree/browser/tree'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; + +/** + * Implements drag and drop for the server tree + */ +export class AsyncServerTreeDragAndDrop implements ITreeDragAndDrop { + + private _dragAndDrop: IDragAndDrop; + + constructor( + @IConnectionManagementService connectionManagementService: IConnectionManagementService, + ) { + this._dragAndDrop = new ServerTreeDragAndDrop(connectionManagementService); + } + + /** + * Returns a uri if the given element should be allowed to drag. + * Returns null, otherwise. + */ + public getDragURI(element: ServerTreeElement): string { + return this._dragAndDrop.getDragURI(undefined, element); + } + + /** + * Returns a label(name) to display when dragging the element. + */ + public getDragLabel(elements: ServerTreeElement[]): string { + return this._dragAndDrop.getDragLabel(undefined, elements); + } + + /** + * Called when the drag operation starts. + */ + public onDragStart(dragAndDropData: IDragAndDropData, originalEvent: DragEvent): void { + // Force the event cast while in preview - we don't use any of the mouse properties on the + // implementation so this is fine for now + return this._dragAndDrop.onDragStart(undefined, dragAndDropData, originalEvent); + } + + public onDragOver(data: IDragAndDropData, targetElement: ServerTreeElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + // Force the event cast while in preview - we don't use any of the mouse properties on the + // implementation so this is fine for now + const canDragOver = this._dragAndDrop.onDragOver(undefined, data, targetElement, originalEvent); + + if (canDragOver.accept) { + return TreeDragOverReactions.acceptBubbleDown(true); + } else { + return { accept: false }; + } + } + + /** + * Handle a drop in the server tree. + */ + public drop(data: IDragAndDropData, targetElement: ServerTreeElement, targetIndex: number, originalEvent: DragEvent): void { + // Force the event cast while in preview - we don't use any of the mouse properties on the + // implementation so this is fine for now + + // TODO: chgagnon Drop on root node + this._dragAndDrop.drop(undefined, data, targetElement, originalEvent); + } + + public onDragEnd(originalEvent: DragEvent): void { + TreeUpdateUtils.isInDragAndDrop = false; + } +} + +export class AsyncRecentConnectionsDragAndDrop implements ITreeDragAndDrop { + + /** + * Returns a uri if the given element should be allowed to drag. + * Returns null, otherwise. + */ + public getDragURI(element: ServerTreeElement): string | null { + if (element instanceof ConnectionProfile) { + return (element).id; + } + else if (element instanceof ConnectionProfileGroup) { + return (element).id; + } + return null; + } + + /** + * Returns a label(name) to display when dragging the element. + */ + public getDragLabel(elements: ServerTreeElement[]): string { + if (elements[0] instanceof ConnectionProfile) { + return elements[0].serverName; + } + else if (elements[0] instanceof ConnectionProfileGroup) { + return elements[0].name; + } + return undefined; + } + + /** + * Returns a DragOverReaction indicating whether sources can be + * dropped into target or some parent of the target. + */ + public onDragOver(data: IDragAndDropData, targetElement: ServerTreeElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + return { accept: false }; + } + + /** + * Handle drop in the server tree. + */ + public drop(data: IDragAndDropData, targetElement: ServerTreeElement, targetIndex: number, originalEvent: DragEvent): void { + // No op + } +} diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeIdentityProvider.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeIdentityProvider.ts new file mode 100644 index 0000000000..e1d21032bd --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeIdentityProvider.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; + +export class AsyncServerTreeIdentityProvider implements IIdentityProvider { + getId(element: ServerTreeElement): { toString(): string; } { + return element.id; + } +} diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts new file mode 100644 index 0000000000..9655a267c9 --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts @@ -0,0 +1,296 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/objectTypes/objecttypes'; + +import * as dom from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; +import { iconRenderer } from 'sql/workbench/services/objectExplorer/browser/iconRenderer'; +import { URI } from 'vs/base/common/uri'; +import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { ServerTreeRenderer } from 'sql/workbench/services/objectExplorer/browser/serverTreeRenderer'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; +import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/common/serverGroupViewModel'; + +class ConnectionProfileGroupTemplate extends Disposable { + private _root: HTMLElement; + private _nameContainer: HTMLElement; + + constructor( + container: HTMLElement + ) { + super(); + container.parentElement.classList.add('server-group'); + container.classList.add('server-group'); + this._root = dom.append(container, dom.$('.server-group')); + this._nameContainer = dom.append(this._root, dom.$('span.name')); + } + + set(element: ConnectionProfileGroup) { + let rowElement = findParentElement(this._root, 'monaco-list-row'); + if (rowElement) { + if (element.color) { + rowElement.style.background = element.color; + } else { + // If the group doesn't contain specific color, assign the default color + rowElement.style.background = DefaultServerGroupColor; + } + } + if (element.description && (element.description !== '')) { + this._root.title = element.description; + } + this._nameContainer.hidden = false; + this._nameContainer.textContent = element.name; + } +} + +export class ConnectionProfileGroupRenderer implements ITreeRenderer { + + readonly templateId: string = ServerTreeRenderer.CONNECTION_GROUP_TEMPLATE_ID; + + constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { } + + renderTemplate(container: HTMLElement): ConnectionProfileGroupTemplate { + return this._instantiationService.createInstance(ConnectionProfileGroupTemplate, container); + } + renderElement(node: ITreeNode, index: number, template: ConnectionProfileGroupTemplate): void { + template.set(node.element); + } + disposeTemplate(templateData: ConnectionProfileGroupTemplate): void { + templateData.dispose(); + } +} + +class ConnectionProfileTemplate extends Disposable { + + private _root: HTMLElement; + private _icon: HTMLElement; + private _label: HTMLElement; + /** + * _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 + * tile is rendered without the action buttons( such as connect, new query). + */ + constructor( + container: HTMLElement, + private _isCompact: boolean, + @IConnectionManagementService private _connectionManagementService: IConnectionManagementService + ) { + super(); + container.parentElement.classList.add('connection-profile'); + this._root = dom.append(container, dom.$('.connection-tile')); + this._icon = dom.append(this._root, dom.$('div.icon server-page')); + this._label = dom.append(this._root, dom.$('div.label')); + } + + set(element: ConnectionProfile) { + if (!this._isCompact) { + let iconPath: IconPath = getIconPath(element, this._connectionManagementService); + if (this._connectionManagementService.isConnected(undefined, element)) { + this._icon.classList.remove('disconnected'); + this._icon.classList.add('connected'); + renderServerIcon(this._icon, iconPath, true); + } else { + this._icon.classList.remove('connected'); + this._icon.classList.add('disconnected'); + renderServerIcon(this._icon, iconPath, false); + } + } + + let label = element.title; + if (!element.isConnectionOptionsValid) { + label = localize('loading', "Loading..."); + } + + this._label.textContent = label; + this._root.title = element.serverInfo; + } +} + +export class ConnectionProfileRenderer implements ITreeRenderer { + + readonly templateId: string = ServerTreeRenderer.CONNECTION_TEMPLATE_ID; + + constructor( + private _isCompact: boolean, + @IInstantiationService private readonly _instantiationService: IInstantiationService) { } + + renderTemplate(container: HTMLElement): ConnectionProfileTemplate { + return this._instantiationService.createInstance(ConnectionProfileTemplate, container, this._isCompact); + } + renderElement(node: ITreeNode, index: number, template: ConnectionProfileTemplate): void { + template.set(node.element); + } + disposeTemplate(templateData: ConnectionProfileTemplate): void { + templateData.dispose(); + } +} + +class TreeNodeTemplate extends Disposable { + private _root: HTMLElement; + private _icon: HTMLElement; + private _label: HTMLElement; + + constructor( + container: HTMLElement + ) { + super(); + this._root = dom.append(container, dom.$('.object-element-group')); + this._icon = dom.append(this._root, dom.$('div.object-icon')); + this._label = dom.append(this._root, dom.$('div.label')); + } + + set(element: TreeNode) { + // Use an explicitly defined iconType first. If not defined, fall back to using nodeType and + // other compount indicators instead. + let iconName: string = undefined; + if (element.iconType) { + iconName = (typeof element.iconType === 'string') ? element.iconType : element.iconType.id; + } else { + iconName = element.nodeTypeId; + if (element.nodeStatus) { + iconName = element.nodeTypeId + '_' + element.nodeStatus; + } + if (element.nodeSubType) { + iconName = element.nodeTypeId + '_' + element.nodeSubType; + } + } + + let tokens: string[] = []; + for (let index = 1; index < this._icon.classList.length; index++) { + tokens.push(this._icon.classList.item(index)); + } + this._icon.classList.remove(...tokens); + this._icon.classList.add('icon'); + let iconLowerCaseName = iconName.toLocaleLowerCase(); + this._icon.classList.add(iconLowerCaseName); + + if (element.iconPath) { + iconRenderer.putIcon(this._icon, element.iconPath); + } + + this._label.textContent = element.label; + this._root.title = element.label; + } +} + +export class TreeNodeRenderer implements ITreeRenderer { + + readonly templateId: string = ServerTreeRenderer.OBJECTEXPLORER_TEMPLATE_ID; + + constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { } + + renderTemplate(container: HTMLElement): TreeNodeTemplate { + return this._instantiationService.createInstance(TreeNodeTemplate, container); + } + renderElement(node: ITreeNode, index: number, template: TreeNodeTemplate): void { + template.set(node.element); + } + disposeTemplate(templateData: TreeNodeTemplate): void { + templateData.dispose(); + } +} + +export class ServerTreeKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + + constructor() { } + + getKeyboardNavigationLabel(element: ServerTreeElement): { toString(): string; } { + if (element instanceof ConnectionProfileGroup) { + return element.groupName; + } else if (element instanceof ConnectionProfile) { + return element.title; + } else { + return element.label; + } + } +} + +export class ServerTreeAccessibilityProvider implements IListAccessibilityProvider { + + constructor(private _widgetAriaLabel: string) { } + + getWidgetAriaLabel(): string { + return this._widgetAriaLabel; + } + + getAriaLabel(element: ServerTreeElement): string | null { + if (element instanceof ConnectionProfileGroup) { + return element.fullName; + } else if (element instanceof ConnectionProfile) { + return element.title; + } + return element.label; + } +} + +/** + * Returns the first parent which contains the className + */ +function findParentElement(container: HTMLElement, className: string): HTMLElement { + let currentElement = container; + while (currentElement) { + if (currentElement.className.indexOf(className) > -1) { + break; + } + currentElement = currentElement.parentElement; + } + return currentElement; +} + +function getIconPath(connection: ConnectionProfile, connectionManagementService: IConnectionManagementService): IconPath { + if (!connection) { return undefined; } + + if (connection['iconPath']) { + return connection['iconPath']; + } + + let iconId = connectionManagementService.getConnectionIconId(connection.id); + if (!iconId) { return undefined; } + + let providerProperties = connectionManagementService.getProviderProperties(connection.providerName); + if (!providerProperties) { return undefined; } + + let iconPath: IconPath = undefined; + let pathConfig: URI | IconPath | { id: string, path: IconPath }[] = providerProperties['iconPath']; + if (Array.isArray(pathConfig)) { + for (const e of pathConfig) { + if (!e.id || e.id === iconId) { + iconPath = e.path; + connection['iconPath'] = iconPath; + break; + } + } + } else if (pathConfig['light']) { + iconPath = pathConfig as IconPath; + connection['iconPath'] = iconPath; + } else { + let singlePath = pathConfig as URI; + iconPath = { light: singlePath, dark: singlePath }; + connection['iconPath'] = iconPath; + } + return iconPath; +} + +function renderServerIcon(element: HTMLElement, iconPath: IconPath, isConnected: boolean): void { + if (!element) { return; } + if (iconPath) { + iconRenderer.putIcon(element, iconPath); + } +} + +interface IconPath { + light: URI; + dark: URI; +} diff --git a/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts b/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts index 1c81aab1f0..3624cf97ba 100644 --- a/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts +++ b/src/sql/workbench/services/objectExplorer/browser/connectionTreeAction.ts @@ -18,6 +18,7 @@ import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMess import { UNSAVED_GROUP_ID } from 'sql/platform/connection/common/constants'; import { IServerGroupController } from 'sql/platform/serverGroup/common/serverGroupController'; import { ILogService } from 'vs/platform/log/common/log'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; export interface IServerView { showFilteredTree(filter: string): void; @@ -28,20 +29,18 @@ export class RefreshAction extends Action { public static ID = 'objectExplorer.refresh'; public static LABEL = localize('connectionTree.refresh', "Refresh"); - private _tree: ITree; constructor( id: string, label: string, - tree: ITree, - private element: IConnectionProfile | TreeNode, + private _tree: AsyncServerTree | ITree, + private element: ServerTreeElement, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, @IErrorMessageService private _errorMessageService: IErrorMessageService, @ILogService private _logService: ILogService ) { super(id, label); - this._tree = tree; } public async run(): Promise { let treeNode: TreeNode; @@ -66,7 +65,11 @@ export class RefreshAction extends Action { this.showError(error); return true; } - await this._tree.refresh(this.element); + if (this._tree instanceof AsyncServerTree) { + await this._tree.updateChildren(this.element); + } else { + await this._tree.refresh(this.element); + } } catch (ex) { this._logService.error(ex); return true; diff --git a/src/sql/workbench/services/objectExplorer/browser/dragAndDropController.ts b/src/sql/workbench/services/objectExplorer/browser/dragAndDropController.ts index 73a8c4ecaf..601e30bbfb 100644 --- a/src/sql/workbench/services/objectExplorer/browser/dragAndDropController.ts +++ b/src/sql/workbench/services/objectExplorer/browser/dragAndDropController.ts @@ -20,6 +20,17 @@ export function supportsNodeNameDrop(nodeId: string): boolean { return false; } +export function supportsFolderNodeNameDrop(nodeId: string, label: string): boolean { + if (nodeId === 'Folder' && label === 'Columns') { + return true; + } + return false; +} + +function escapeString(input: string | undefined): string | undefined { + return input?.replace(/]/g, ']]'); +} + /** * Implements drag and drop for the server tree */ @@ -42,7 +53,7 @@ export class ServerTreeDragAndDrop implements IDragAndDrop { return (element).id; } else if (supportsNodeNameDrop(element.nodeTypeId)) { return (element).id; - } else if (element.nodeTypeId === 'Folder' && element.label === 'Columns' && element.children) { + } else if (supportsFolderNodeNameDrop(element.nodeTypeId, element.label) && element.children) { return (element).id; } else { return undefined; @@ -83,17 +94,17 @@ export class ServerTreeDragAndDrop implements IDragAndDrop { const data = dragAndDropData.getData(); const element = data[0]; if (supportsNodeNameDrop(element.nodeTypeId)) { - escapedSchema = this.escapeString(element.metadata.schema); - escapedName = this.escapeString(element.metadata.name); + escapedSchema = escapeString(element.metadata.schema); + escapedName = escapeString(element.metadata.name); finalString = escapedSchema ? `[${escapedSchema}].[${escapedName}]` : `[${escapedName}]`; originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([`${element.nodeTypeId}:${element.id}?${finalString}`])); } - if (element.nodeTypeId === 'Folder' && element.label === 'Columns') { + if (supportsFolderNodeNameDrop(element.nodeTypeId, element.label)) { // get children let returnString = ''; for (let child of element.children) { - escapedSchema = this.escapeString(child.metadata.schema); - escapedName = this.escapeString(child.metadata.name); + escapedSchema = escapeString(child.metadata.schema); + escapedName = escapeString(child.metadata.name); finalString = escapedSchema ? `[${escapedSchema}].[${escapedName}]` : `[${escapedName}]`; returnString = returnString ? `${returnString},${finalString}` : `${finalString}`; } @@ -103,14 +114,6 @@ export class ServerTreeDragAndDrop implements IDragAndDrop { return; } - private escapeString(input: string | undefined): string | undefined { - if (input) { - let output = input.replace(/]/g, ']]'); - return output; - } - return undefined; - } - public canDragToConnectionProfileGroup(source: any, targetConnectionProfileGroup: ConnectionProfileGroup) { let canDragOver: boolean = true; @@ -177,12 +180,18 @@ export class ServerTreeDragAndDrop implements IDragAndDrop { if (source instanceof ConnectionProfile) { // Change group id of profile this._connectionManagementService.changeGroupIdForConnection(source, targetConnectionProfileGroup.id).then(() => { - TreeUpdateUtils.registeredServerUpdate(tree, self._connectionManagementService, targetConnectionProfileGroup); + if (tree) { + TreeUpdateUtils.registeredServerUpdate(tree, self._connectionManagementService, targetConnectionProfileGroup); + } + }); } else if (source instanceof ConnectionProfileGroup) { // Change parent id of group this._connectionManagementService.changeGroupIdForConnectionGroup(source, targetConnectionProfileGroup).then(() => { - TreeUpdateUtils.registeredServerUpdate(tree, self._connectionManagementService); + if (tree) { + TreeUpdateUtils.registeredServerUpdate(tree, self._connectionManagementService); + } + }); } } diff --git a/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts b/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts index d827831ab1..557c4c8161 100644 --- a/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts +++ b/src/sql/workbench/services/objectExplorer/browser/objectExplorerService.ts @@ -21,7 +21,9 @@ import { values } from 'vs/base/common/collections'; import { startsWith } from 'vs/base/common/strings'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IAction } from 'vs/base/common/actions'; +import { ServerTreeActionProvider } from 'sql/workbench/services/objectExplorer/browser/serverTreeActionProvider'; import { ITree } from 'vs/base/parts/tree/browser/tree'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; export const SERVICE_ID = 'ObjectExplorerService'; @@ -32,22 +34,22 @@ export interface NodeExpandInfoWithProviderId extends azdata.ObjectExplorerExpan } export interface IServerTreeView { - readonly tree: ITree; + readonly tree: ITree | AsyncServerTree; readonly onSelectionOrFocusChange: Event; isObjectExplorerConnectionUri(uri: string): boolean; deleteObjectExplorerNodeAndRefreshTree(profile: ConnectionProfile): Promise; - getSelection(): Array; + getSelection(): Array; isFocused(): boolean; refreshElement(node: TreeNode): Promise; - readonly treeActionProvider: { getActions: (tree: ITree, node: TreeNode | ConnectionProfile) => IAction[] } - isExpanded(node: TreeNode | ConnectionProfile): boolean; - reveal(node: TreeNode | ConnectionProfile): Promise; - setExpandedState(node: TreeNode | ConnectionProfile, state: TreeItemCollapsibleState): Promise; - setSelected(node: TreeNode | ConnectionProfile, selected: boolean, clearOtherSelections: boolean): Promise; + readonly treeActionProvider: ServerTreeActionProvider; + isExpanded(node: ServerTreeElement): boolean; + reveal(node: ServerTreeElement): Promise; + setExpandedState(node: ServerTreeElement, state: TreeItemCollapsibleState): Promise; + setSelected(node: ServerTreeElement, selected: boolean, clearOtherSelections: boolean): Promise; refreshTree(): Promise; readonly activeConnectionsFilterAction: IAction; renderBody(container: HTMLElement): Promise; - layout(size: number); + layout(size: number): void; } export interface IObjectExplorerService { @@ -78,7 +80,7 @@ export interface IObjectExplorerService { registerNodeProvider(expander: azdata.ObjectExplorerNodeProvider): void; - getObjectExplorerNode(connection: IConnectionProfile): TreeNode; + getObjectExplorerNode(connection: IConnectionProfile): TreeNode | undefined; updateObjectExplorerNodes(connectionProfile: IConnectionProfile): Promise; @@ -203,7 +205,7 @@ export class ObjectExplorerService implements IObjectExplorerService { public async updateObjectExplorerNodes(connection: IConnectionProfile): Promise { const withPassword = await this._connectionManagementService.addSavedPassword(connection); - let connectionProfile = ConnectionProfile.fromIConnectionProfile(this._capabilitiesService, withPassword); + const connectionProfile = ConnectionProfile.fromIConnectionProfile(this._capabilitiesService, withPassword); return this.updateNewObjectExplorerNode(connectionProfile); } @@ -330,7 +332,7 @@ export class ObjectExplorerService implements IObjectExplorerService { } } - public getObjectExplorerNode(connection: IConnectionProfile): TreeNode { + public getObjectExplorerNode(connection: IConnectionProfile): TreeNode | undefined { return this._activeObjectExplorerNodes[connection.id]; } @@ -678,7 +680,7 @@ export class ObjectExplorerService implements IObjectExplorerService { */ public async getNodeActions(connectionId: string, nodePath: string): Promise { const node = await this.getTreeNode(connectionId, nodePath); - let actions = this._serverTreeView.treeActionProvider.getActions(this._serverTreeView.tree, this.getTreeItem(node)); + const actions = this._serverTreeView.treeActionProvider.getActions(this._serverTreeView.tree, this.getTreeItem(node)); return actions.filter(action => action.label).map(action => action.label); } diff --git a/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts b/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts index 43245d1e7b..c5a253b0cb 100644 --- a/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts +++ b/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts @@ -25,6 +25,7 @@ import { IQueryManagementService } from 'sql/workbench/services/query/common/que import { ServerInfoContextKey } from 'sql/workbench/services/connection/common/serverInfoContextKey'; import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { firstIndex, find } from 'vs/base/common/arrays'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; /** * Provides actions for the server tree elements @@ -40,19 +41,15 @@ export class ServerTreeActionProvider { ) { } - public hasActions(tree: ITree, element: any): boolean { - return element instanceof ConnectionProfileGroup || (element instanceof ConnectionProfile) || (element instanceof TreeNode); - } - /** * Return actions given an element in the tree */ - public getActions(tree: ITree, element: any): IAction[] { + public getActions(tree: AsyncServerTree | ITree, element: ServerTreeElement): IAction[] { if (element instanceof ConnectionProfile) { return this.getConnectionActions(tree, element); } if (element instanceof ConnectionProfileGroup) { - return this.getConnectionProfileGroupActions(tree, element); + return this.getConnectionProfileGroupActions(element); } if (element instanceof TreeNode) { return this.getObjectExplorerNodeActions({ @@ -61,18 +58,13 @@ export class ServerTreeActionProvider { treeNode: element }); } - return []; } - public hasSecondaryActions(tree: ITree, element: any): boolean { - return false; - } - /** * Return actions for connection elements */ - public getConnectionActions(tree: ITree, profile: ConnectionProfile): IAction[] { + private getConnectionActions(tree: AsyncServerTree | ITree, profile: ConnectionProfile): IAction[] { let node = new TreeNode(NodeType.Server, '', false, '', '', '', undefined, undefined, undefined, undefined); return this.getAllActions({ tree: tree, @@ -158,7 +150,7 @@ export class ServerTreeActionProvider { /** * Return actions for connection group elements */ - public getConnectionProfileGroupActions(tree: ITree, element: ConnectionProfileGroup): IAction[] { + private getConnectionProfileGroupActions(element: ConnectionProfileGroup): IAction[] { return [ this._instantiationService.createInstance(AddServerAction, AddServerAction.ID, AddServerAction.LABEL), this._instantiationService.createInstance(EditServerGroupAction, EditServerGroupAction.ID, EditServerGroupAction.LABEL, element), @@ -202,7 +194,7 @@ export class ServerTreeActionProvider { } interface ObjectExplorerContext { - tree: ITree; + tree: AsyncServerTree | ITree; profile: ConnectionProfile; treeNode?: TreeNode; } diff --git a/src/sql/workbench/services/objectExplorer/browser/serverTreeDataSource.ts b/src/sql/workbench/services/objectExplorer/browser/serverTreeDataSource.ts index b4d10aeff6..bdac74bbca 100644 --- a/src/sql/workbench/services/objectExplorer/browser/serverTreeDataSource.ts +++ b/src/sql/workbench/services/objectExplorer/browser/serverTreeDataSource.ts @@ -58,7 +58,7 @@ export class ServerTreeDataSource implements IDataSource { */ public async getChildren(tree: ITree, element: any): Promise<(ConnectionProfile | ConnectionProfileGroup | TreeNode)[]> { if (element instanceof ConnectionProfile) { - return TreeUpdateUtils.getObjectExplorerNode(element, this._connectionManagementService, this._objectExplorerService); + return TreeUpdateUtils.getConnectionNodeChildren(element, this._objectExplorerService); } else if (element instanceof ConnectionProfileGroup) { return (element as ConnectionProfileGroup).getChildren(); } else if (element instanceof TreeNode) { diff --git a/src/sql/workbench/services/objectExplorer/browser/serverTreeRenderer.ts b/src/sql/workbench/services/objectExplorer/browser/serverTreeRenderer.ts index 68626fe0fc..cc7403386c 100644 --- a/src/sql/workbench/services/objectExplorer/browser/serverTreeRenderer.ts +++ b/src/sql/workbench/services/objectExplorer/browser/serverTreeRenderer.ts @@ -15,6 +15,7 @@ import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode' import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { badgeRenderer, iconRenderer } from 'sql/workbench/services/objectExplorer/browser/iconRenderer'; import { URI } from 'vs/base/common/uri'; +import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/common/serverGroupViewModel'; export interface IConnectionTemplateData { root: HTMLElement; @@ -44,10 +45,10 @@ export class ServerTreeRenderer implements IRenderer { public static CONNECTION_HEIGHT = 23; public static CONNECTION_GROUP_HEIGHT = 38; - private static CONNECTION_TEMPLATE_ID = 'connectionProfile'; - private static CONNECTION_GROUP_TEMPLATE_ID = 'connectionProfileGroup'; + public static CONNECTION_TEMPLATE_ID = 'connectionProfile'; + public static CONNECTION_GROUP_TEMPLATE_ID = 'connectionProfileGroup'; public static OBJECTEXPLORER_HEIGHT = 23; - private static OBJECTEXPLORER_TEMPLATE_ID = 'objectExplorer'; + public static OBJECTEXPLORER_TEMPLATE_ID = 'objectExplorer'; /** * _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 @@ -238,7 +239,7 @@ export class ServerTreeRenderer implements IRenderer { rowElement.style.background = connectionProfileGroup.color; } else { // If the group doesn't contain specific color, assign the default color - rowElement.style.background = '#515151'; + rowElement.style.background = DefaultServerGroupColor; } } if (connectionProfileGroup.description && (connectionProfileGroup.description !== '')) { diff --git a/src/sql/workbench/services/objectExplorer/browser/treeCreationUtils.ts b/src/sql/workbench/services/objectExplorer/browser/treeCreationUtils.ts index c54d0d8b61..0076c013df 100644 --- a/src/sql/workbench/services/objectExplorer/browser/treeCreationUtils.ts +++ b/src/sql/workbench/services/objectExplorer/browser/treeCreationUtils.ts @@ -15,48 +15,130 @@ import { IController } from 'vs/base/parts/tree/browser/tree'; import { ServerTreeDragAndDrop, RecentConnectionsDragAndDrop } from 'sql/workbench/services/objectExplorer/browser/dragAndDropController'; import { RecentConnectionDataSource } from 'sql/workbench/services/objectExplorer/browser/recentConnectionDataSource'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; +import { ConnectionProfileGroupRenderer, ConnectionProfileRenderer, TreeNodeRenderer, ServerTreeAccessibilityProvider, ServerTreeKeyboardNavigationLabelProvider } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer'; +import { AsyncServerTreeIdentityProvider } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeIdentityProvider'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { AsyncServerTreeDelegate } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeDelegate'; +import { AsyncRecentConnectionsDragAndDrop, AsyncServerTreeDragAndDrop } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeDragAndDrop'; +import { AsyncRecentConnectionTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource'; +import { AsyncServerTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeDataSource'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TreeCreationUtils { /** * Create a Recent Connections tree */ - public static createConnectionTree(treeContainer: HTMLElement, instantiationService: IInstantiationService, useController?: IController): Tree { - const dataSource = instantiationService.createInstance(RecentConnectionDataSource); - const renderer = instantiationService.createInstance(ServerTreeRenderer, true); - const controller = useController ? useController : new DefaultController(); - const dnd = instantiationService.createInstance(RecentConnectionsDragAndDrop); - const filter = new DefaultFilter(); - const sorter = undefined; - const accessibilityProvider = new DefaultAccessibilityProvider(); + public static createConnectionTree(treeContainer: HTMLElement, instantiationService: IInstantiationService, configurationService: IConfigurationService, ariaLabel: string, useController?: IController): Tree | AsyncServerTree { + if (useAsyncServerTree(configurationService)) { + const dataSource = instantiationService.createInstance(AsyncRecentConnectionTreeDataSource); + const connectionProfileGroupRender = instantiationService.createInstance(ConnectionProfileGroupRenderer); + const connectionProfileRenderer = instantiationService.createInstance(ConnectionProfileRenderer, true); + const treeNodeRenderer = instantiationService.createInstance(TreeNodeRenderer); + const dnd = instantiationService.createInstance(AsyncRecentConnectionsDragAndDrop); + const identityProvider = instantiationService.createInstance(AsyncServerTreeIdentityProvider); - return new Tree(treeContainer, { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider }, - { - indentPixels: 0, - twistiePixels: 0, - ariaLabel: nls.localize('treeAriaLabel', "Recent Connections") - }); + const treeOptions: IWorkbenchAsyncDataTreeOptions = { + keyboardSupport: true, + accessibilityProvider: new ServerTreeAccessibilityProvider(ariaLabel), + keyboardNavigationLabelProvider: instantiationService.createInstance(ServerTreeKeyboardNavigationLabelProvider), + dnd: dnd, + identityProvider: identityProvider + }; + + return instantiationService.createInstance( + AsyncServerTree, + 'ServerTreeView', + treeContainer, + new AsyncServerTreeDelegate(), + [ + connectionProfileGroupRender, + connectionProfileRenderer, + treeNodeRenderer + ], + dataSource, + treeOptions, + ); + } else { + const dataSource = instantiationService.createInstance(RecentConnectionDataSource); + const renderer = instantiationService.createInstance(ServerTreeRenderer, true); + const controller = useController ? useController : new DefaultController(); + const dnd = instantiationService.createInstance(RecentConnectionsDragAndDrop); + const filter = new DefaultFilter(); + const sorter = undefined; + const accessibilityProvider = new DefaultAccessibilityProvider(); + + return new Tree(treeContainer, { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider }, + { + indentPixels: 0, + twistiePixels: 0, + ariaLabel: nls.localize('treeAriaLabel', "Recent Connections") + }); + } } /** * Create a Servers viewlet tree */ - public static createRegisteredServersTree(treeContainer: HTMLElement, instantiationService: IInstantiationService, horizontalScrollMode: boolean = false): Tree { + public static createServersTree(treeContainer: HTMLElement, + instantiationService: IInstantiationService, + configurationService: IConfigurationService, + horizontalScrollMode: boolean = false): Tree | AsyncServerTree { - const dataSource = instantiationService.createInstance(ServerTreeDataSource); - const actionProvider = instantiationService.createInstance(ServerTreeActionProvider); - const renderer = instantiationService.createInstance(ServerTreeRenderer, false); - const controller = instantiationService.createInstance(ServerTreeController, actionProvider); - const dnd = instantiationService.createInstance(ServerTreeDragAndDrop); - const filter = new DefaultFilter(); - const sorter = undefined; - const accessibilityProvider = new DefaultAccessibilityProvider(); + if (useAsyncServerTree(configurationService)) { + const dataSource = instantiationService.createInstance(AsyncServerTreeDataSource); + const connectionProfileGroupRender = instantiationService.createInstance(ConnectionProfileGroupRenderer); + const connectionProfileRenderer = instantiationService.createInstance(ConnectionProfileRenderer, false); + const treeNodeRenderer = instantiationService.createInstance(TreeNodeRenderer); + const dnd = instantiationService.createInstance(AsyncServerTreeDragAndDrop); + const identityProvider = instantiationService.createInstance(AsyncServerTreeIdentityProvider); + + const treeOptions: IWorkbenchAsyncDataTreeOptions = { + keyboardSupport: true, + accessibilityProvider: new ServerTreeAccessibilityProvider(nls.localize('serversAriaLabel', "Servers")), + keyboardNavigationLabelProvider: instantiationService.createInstance(ServerTreeKeyboardNavigationLabelProvider), + openOnSingleClick: true, + openOnFocus: true, + dnd: dnd, + identityProvider: identityProvider + }; + + return instantiationService.createInstance( + AsyncServerTree, + 'ServerTreeView', + treeContainer, + new AsyncServerTreeDelegate(), + [ + connectionProfileGroupRender, + connectionProfileRenderer, + treeNodeRenderer + ], + dataSource, + treeOptions + ); + } else { + const dataSource = instantiationService.createInstance(ServerTreeDataSource); + const actionProvider = instantiationService.createInstance(ServerTreeActionProvider); + const renderer = instantiationService.createInstance(ServerTreeRenderer, false); + const controller = instantiationService.createInstance(ServerTreeController, actionProvider); + const dnd = instantiationService.createInstance(ServerTreeDragAndDrop); + const filter = new DefaultFilter(); + const sorter = undefined; + const accessibilityProvider = new DefaultAccessibilityProvider(); + + return new Tree(treeContainer, { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider }, + { + indentPixels: 10, + twistiePixels: 20, + ariaLabel: nls.localize('treeCreation.regTreeAriaLabel', "Servers"), + horizontalScrollMode: horizontalScrollMode ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden + }); + } - return new Tree(treeContainer, { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider }, - { - indentPixels: 10, - twistiePixels: 20, - ariaLabel: nls.localize('treeCreation.regTreeAriaLabel', "Servers"), - horizontalScrollMode: horizontalScrollMode ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden - }); } } + +function useAsyncServerTree(configurationService: IConfigurationService): boolean { + return configurationService.getValue('workbench.enablePreviewFeatures') && configurationService.getValue('serverTree.useAsyncServerTree'); +} diff --git a/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts b/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts index e1c5825b56..deb8a30a67 100644 --- a/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts +++ b/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts @@ -11,6 +11,7 @@ import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/br // import { IProgressRunner, IProgressService } from 'vs/platform/progress/common/progress'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; +import { AsyncServerTree } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; export class TreeSelectionHandler { // progressRunner: IProgressRunner; @@ -44,9 +45,9 @@ export class TreeSelectionHandler { } /** - * Handle select ion of tree element + * Handle selection of tree element */ - public onTreeSelect(event: any, tree: ITree, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, connectionCompleteCallback: () => void) { + public onTreeSelect(event: any, tree: AsyncServerTree | ITree, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, connectionCompleteCallback: () => void) { let sendSelectionEvent = ((event: any, selection: any, isDoubleClick: boolean, userInteraction: boolean) => { // userInteraction: defensive - don't touch this something else is handling it. if (userInteraction === true && this._lastClicked && this._lastClicked[0] === selection[0]) { @@ -93,44 +94,50 @@ export class TreeSelectionHandler { * * @param connectionCompleteCallback A function that gets called after a connection is established due to the selection, if needed */ - private handleTreeItemSelected(connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, isDoubleClick: boolean, isKeyboard: boolean, selection: any[], tree: ITree, connectionCompleteCallback: () => void): void { - let connectionProfile: ConnectionProfile = undefined; - let options: IConnectionCompletionOptions = { - params: undefined, - saveTheConnection: true, - showConnectionDialogOnError: true, - showFirewallRuleOnError: true, - showDashboard: isDoubleClick // only show the dashboard if the action is double click - }; - if (selection && selection.length > 0 && (selection[0] instanceof ConnectionProfile)) { - connectionProfile = selection[0]; - - if (connectionProfile) { + private handleTreeItemSelected(connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, isDoubleClick: boolean, isKeyboard: boolean, selection: any[], tree: AsyncServerTree | ITree, connectionCompleteCallback: () => void): void { + if (tree instanceof AsyncServerTree) { + if (selection && selection.length > 0 && (selection[0] instanceof ConnectionProfile)) { this.onTreeActionStateChange(true); - - TreeUpdateUtils.connectAndCreateOeSession(connectionProfile, options, connectionManagementService, objectExplorerService, tree).then(sessionCreated => { - if (!sessionCreated) { - this.onTreeActionStateChange(false); - } - if (connectionCompleteCallback) { - connectionCompleteCallback(); - } - }, error => { - this.onTreeActionStateChange(false); - }); } - } else if (isDoubleClick && selection && selection.length > 0 && (selection[0] instanceof TreeNode)) { - let treeNode = selection[0]; - if (TreeUpdateUtils.isAvailableDatabaseNode(treeNode)) { - connectionProfile = TreeUpdateUtils.getConnectionProfile(treeNode); + } else { + let connectionProfile: ConnectionProfile = undefined; + let options: IConnectionCompletionOptions = { + params: undefined, + saveTheConnection: true, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true, + showDashboard: isDoubleClick // only show the dashboard if the action is double click + }; + if (selection && selection.length > 0 && (selection[0] instanceof ConnectionProfile)) { + connectionProfile = selection[0]; + if (connectionProfile) { - connectionManagementService.showDashboard(connectionProfile); + this.onTreeActionStateChange(true); + + TreeUpdateUtils.connectAndCreateOeSession(connectionProfile, options, connectionManagementService, objectExplorerService, tree).then(sessionCreated => { + if (!sessionCreated) { + this.onTreeActionStateChange(false); + } + if (connectionCompleteCallback) { + connectionCompleteCallback(); + } + }, error => { + this.onTreeActionStateChange(false); + }); + } + } else if (isDoubleClick && selection && selection.length > 0 && (selection[0] instanceof TreeNode)) { + let treeNode = selection[0]; + if (TreeUpdateUtils.isAvailableDatabaseNode(treeNode)) { + connectionProfile = TreeUpdateUtils.getConnectionProfile(treeNode); + if (connectionProfile) { + connectionManagementService.showDashboard(connectionProfile); + } } } - } - if (isKeyboard) { - tree.toggleExpansion(selection[0]); + if (isKeyboard) { + tree.toggleExpansion(selection[0]); + } } } } diff --git a/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts b/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts index 9631f151db..1f6ab57e84 100644 --- a/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts +++ b/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts @@ -11,9 +11,9 @@ import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/br import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; -import * as errors from 'vs/base/common/errors'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { Disposable } from 'vs/base/common/lifecycle'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; export interface IExpandableTree extends ITree { /** @@ -47,13 +47,13 @@ export class TreeUpdateUtils { /** * Set input for the tree. */ - public static structuralTreeUpdate(tree: ITree, viewKey: 'recent' | 'active' | 'saved', connectionManagementService: IConnectionManagementService, providers?: string[]): Promise { + public static async structuralTreeUpdate(tree: AsyncServerTree | ITree, viewKey: 'recent' | 'active' | 'saved', connectionManagementService: IConnectionManagementService, providers?: string[]): Promise { // convert to old VS Code tree interface with expandable methods let expandableTree: IExpandableTree = tree; let selectedElement: any; let targetsToExpand: any[]; - if (tree) { + if (tree && !(tree instanceof AsyncServerTree)) { let selection = tree.getSelection(); if (selection && selection.length === 1) { selectedElement = selection[0]; @@ -72,10 +72,13 @@ export class TreeUpdateUtils { treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService, providers); } const previousTreeInput = tree.getInput(); - return tree.setInput(treeInput).then(async () => { - if (previousTreeInput instanceof Disposable) { - previousTreeInput.dispose(); - } + await tree.setInput(treeInput); + + if (previousTreeInput instanceof Disposable) { + previousTreeInput.dispose(); + } + + if (tree && !(tree instanceof AsyncServerTree)) { // Make sure to expand all folders that where expanded in the previous session if (targetsToExpand) { await tree.expandAll(targetsToExpand); @@ -83,64 +86,64 @@ export class TreeUpdateUtils { if (selectedElement) { tree.select(selectedElement); } - }); + } } /** * Set input for the registered servers tree. */ - public static registeredServerUpdate(tree: ITree, connectionManagementService: IConnectionManagementService, elementToSelect?: any): Promise { - // convert to old VS Code tree interface with expandable methods - let expandableTree: IExpandableTree = tree; + public static async registeredServerUpdate(tree: ITree | AsyncServerTree, connectionManagementService: IConnectionManagementService, elementToSelect?: any): Promise { + if (tree instanceof AsyncServerTree) { + const treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService); + await tree.setInput(treeInput); + tree.rerender(); + } else { + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = tree; - let selectedElement: any = elementToSelect; - let targetsToExpand: any[]; + let selectedElement: any = elementToSelect; + let targetsToExpand: any[]; - // Focus - tree.domFocus(); + // Focus + tree.domFocus(); - if (tree) { - let selection = tree.getSelection(); - if (!selectedElement) { - if (selection && selection.length === 1) { - selectedElement = selection[0]; + if (tree) { + let selection = tree.getSelection(); + if (!selectedElement) { + if (selection && selection.length === 1) { + selectedElement = selection[0]; + } + } + targetsToExpand = expandableTree.getExpandedElements(); + if (selectedElement && targetsToExpand.indexOf(selectedElement) === -1) { + targetsToExpand.push(selectedElement); } } - targetsToExpand = expandableTree.getExpandedElements(); - if (selectedElement && targetsToExpand.indexOf(selectedElement) === -1) { - targetsToExpand.push(selectedElement); - } - } - let treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService); - if (treeInput) { - if (treeInput !== tree.getInput()) { - return tree.setInput(treeInput).then(async () => { - // Make sure to expand all folders that where expanded in the previous session - if (targetsToExpand) { - await tree.expandAll(targetsToExpand); - } - if (selectedElement) { - tree.select(selectedElement); - } - tree.getFocus(); - }, errors.onUnexpectedError); + let treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService); + if (treeInput) { + if (treeInput !== tree.getInput()) { + return tree.setInput(treeInput).then(async () => { + // Make sure to expand all folders that where expanded in the previous session + if (targetsToExpand) { + await tree.expandAll(targetsToExpand); + } + if (selectedElement) { + tree.select(selectedElement); + } + tree.getFocus(); + }, onUnexpectedError); + } } } - return Promise.resolve(); } - public static getTreeInput(connectionManagementService: IConnectionManagementService, providers?: string[]): ConnectionProfileGroup { - - let groups = connectionManagementService.getConnectionGroups(providers); - if (groups && groups.length > 0) { - let treeInput = groups[0]; - treeInput.name = 'root'; - groups.forEach(cpg => cpg.dispose()); - return treeInput; - } - // Should never get to this case. - return undefined; + public static getTreeInput(connectionManagementService: IConnectionManagementService, providers?: string[]): ConnectionProfileGroup | undefined { + const groups = connectionManagementService.getConnectionGroups(providers); + const input = groups.find(group => group.isRoot); + // Dispose of the unused groups to clean up their handlers + groups.filter(g => g !== input).forEach(g => g.dispose()); + return input; } public static hasObjectExplorerNode(connection: ConnectionProfile, connectionManagementService: IConnectionManagementService): boolean { @@ -149,10 +152,10 @@ export class TreeUpdateUtils { } public static async connectIfNotConnected( - connection: IConnectionProfile, + connection: ConnectionProfile, options: IConnectionCompletionOptions, connectionManagementService: IConnectionManagementService, - tree: ITree): Promise { + tree: AsyncServerTree | ITree): Promise { if (!connectionManagementService.isProfileConnected(connection)) { // don't try to reconnect if currently connecting if (connectionManagementService.isProfileConnecting(connection)) { @@ -161,7 +164,15 @@ export class TreeUpdateUtils { // else if we aren't connected or connecting then try to connect } else { let callbacks: IConnectionCallbacks = undefined; - if (tree) { + if (tree instanceof AsyncServerTree) { + callbacks = { + onConnectStart: undefined, + onConnectReject: undefined, + onConnectSuccess: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + }; + } else if (tree) { // Show the spinner in OE by adding the 'loading' trait to the connection, and set up callbacks to hide the spinner tree.addTraits('loading', [connection]); let rejectOrCancelCallback = () => { @@ -176,12 +187,13 @@ export class TreeUpdateUtils { onConnectCanceled: rejectOrCancelCallback, }; } + const result = await connectionManagementService.connect(connection, undefined, options, callbacks); if (result.connected) { let existingConnection = connectionManagementService.findExistingConnection(connection); return existingConnection; } else { - throw new Error('connection failed'); + throw new Error(result.errorMessage); } } } else { @@ -202,8 +214,8 @@ export class TreeUpdateUtils { * @param connectionManagementService Connection management service instance * @param objectExplorerService Object explorer service instance */ - public static async connectAndCreateOeSession(connection: IConnectionProfile, options: IConnectionCompletionOptions, - connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, tree: ITree): Promise { + public static async connectAndCreateOeSession(connection: ConnectionProfile, options: IConnectionCompletionOptions, + connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, tree: AsyncServerTree | ITree): Promise { const connectedConnection = await TreeUpdateUtils.connectIfNotConnected(connection, options, connectionManagementService, tree); if (connectedConnection) { // append group ID and original display name to build unique OE session ID @@ -213,7 +225,6 @@ export class TreeUpdateUtils { let rootNode: TreeNode = objectExplorerService.getObjectExplorerNode(connectedConnection); if (!rootNode) { await objectExplorerService.updateObjectExplorerNodes(connectedConnection); - rootNode = objectExplorerService.getObjectExplorerNode(connectedConnection); return true; // The oe request is sent. an event will be raised when the session is created } else { @@ -224,50 +235,75 @@ export class TreeUpdateUtils { } } - public static getObjectExplorerNode(connection: ConnectionProfile, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService): Promise { - return new Promise((resolve, reject) => { - if (connection.isDisconnecting) { - resolve([]); - } else { - let rootNode = objectExplorerService.getObjectExplorerNode(connection); - if (rootNode) { - objectExplorerService.resolveTreeNodeChildren(rootNode.getSession(), rootNode).then(() => { - resolve(rootNode.children); - }, expandError => { - resolve([]); - }); - - } else { - resolve([]); + public static async getConnectionNodeChildren(connection: ConnectionProfile, objectExplorerService: IObjectExplorerService): Promise { + if (connection.isDisconnecting) { + return []; + } else { + let rootNode = objectExplorerService.getObjectExplorerNode(connection); + if (rootNode) { + try { + await objectExplorerService.resolveTreeNodeChildren(rootNode.getSession(), rootNode); + return rootNode.children; + } catch (err) { + onUnexpectedError(err); + return []; } + + } else { + return []; } - }); + } } - public static getObjectExplorerParent(objectExplorerNode: TreeNode, connectionManagementService: IConnectionManagementService): any { + public static async getAsyncConnectionNodeChildren(connection: ConnectionProfile, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService): Promise { + if (connection.isDisconnecting) { + return []; + } else { + let rootNode = objectExplorerService.getObjectExplorerNode(connection); + if (rootNode) { + await objectExplorerService.resolveTreeNodeChildren(rootNode.getSession(), rootNode); + return rootNode.children; + } else { + const options: IConnectionCompletionOptions = { + params: undefined, + saveTheConnection: true, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true, + showDashboard: false + }; + // Need to wait for the OE service to update its nodes in order to resolve the children + const nodesUpdatedPromise = new Promise((resolve, reject) => { + objectExplorerService.onUpdateObjectExplorerNodes(e => { + if (e.errorMessage) { + reject(new Error(e.errorMessage)); + } + if (e.connection.id === connection.id) { + resolve(); + } + }); + }); + await TreeUpdateUtils.connectAndCreateOeSession(connection, options, connectionManagementService, objectExplorerService, undefined); + await nodesUpdatedPromise; + let rootNode = objectExplorerService.getObjectExplorerNode(connection); + await objectExplorerService.resolveTreeNodeChildren(rootNode.getSession(), rootNode); + return rootNode.children; + } + } + } + + public static getObjectExplorerParent(objectExplorerNode: TreeNode, connectionManagementService: IConnectionManagementService): ServerTreeElement | undefined { if (objectExplorerNode && objectExplorerNode.parent) { // if object explorer node's parent is root, return connection profile if (!objectExplorerNode.parent.parent) { - let connectionId = objectExplorerNode.getConnectionProfile().id; - + const connectionId = objectExplorerNode.getConnectionProfile().id; // get connection profile from connection profile groups - let root = TreeUpdateUtils.getTreeInput(connectionManagementService); - let connections = ConnectionProfileGroup.getConnectionsInGroup(root); - let results = connections.filter(con => { - if (connectionId === con.id) { - return true; - } else { - return false; - } - }); - if (results && results.length > 0) { - return results[0]; - } + const root = TreeUpdateUtils.getTreeInput(connectionManagementService); + return ConnectionProfileGroup.getConnectionsInGroup(root).find(conn => connectionId === conn.id); } else { return objectExplorerNode.parent; } } - return null; + return undefined; } /** diff --git a/src/sql/workbench/services/objectExplorer/test/browser/asyncServerTreeDragAndDrop.test.ts b/src/sql/workbench/services/objectExplorer/test/browser/asyncServerTreeDragAndDrop.test.ts new file mode 100644 index 0000000000..d7b552305f --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/test/browser/asyncServerTreeDragAndDrop.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { ConnectionManagementService } from 'sql/workbench/services/connection/browser/connectionManagementService'; +import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService'; +import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +import { mssqlProviderName } from 'sql/platform/connection/common/constants'; +import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; +import * as TypeMoq from 'typemoq'; +import * as assert from 'assert'; +import { AsyncServerTreeDragAndDrop } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeDragAndDrop'; + + + +suite('AsyncServerTreeDragAndDrop', () => { + let serverTreeDragAndDrop: AsyncServerTreeDragAndDrop; + let msSQLCapabilities: ConnectionProviderProperties; + let capabilitiesService: TestCapabilitiesService; + + let iConnectionProfileId: IConnectionProfile = { + connectionName: 'new name', + serverName: 'new server', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g2/g2-2', + groupId: 'group id', + getOptionsKey: undefined!, + matches: undefined!, + providerName: mssqlProviderName, + options: {}, + saveProfile: true, + id: 'd936bb32-422b-49c3-963f-ae9532d63dc5' + }; + + let connectionProfile = new ConnectionProfile(capabilitiesService, iConnectionProfileId); + let connectionProfileArray = [connectionProfile]; + let connectionProfileGroupId = new ConnectionProfileGroup('name', undefined, 'd936bb32-422b-49c3-963f-ae9532d63dc5', 'color', 'description'); + let connectionProfileGroupArray = [connectionProfileGroupId]; + let treeNode = new TreeNode('Column', 'label', undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined); + let treeNodeArray = [treeNode]; + + setup(() => { + let instantiationService = new TestInstantiationService(); + instantiationService.stub(IStorageService, new TestStorageService()); + let mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, + undefined, //connection store + undefined, // connectionstatusmanager + undefined, // connectiondialog service + instantiationService, // instantiation service + undefined, // editor service + undefined, // telemetryservice + undefined, // configuration service + new TestCapabilitiesService(), // capabilities service + ); + serverTreeDragAndDrop = new AsyncServerTreeDragAndDrop(mockConnectionManagementService.object); + + capabilitiesService = new TestCapabilitiesService(); + capabilitiesService.capabilities[mssqlProviderName] = { connection: msSQLCapabilities }; + }); + + + test('create new serverTreeDragAndDrop object should create serverTreeDragAndDrop object successfully', async () => { + + assert.equal(serverTreeDragAndDrop !== null || serverTreeDragAndDrop !== undefined, true); + }); + + test('able to get DragURI', async () => { + let uri = serverTreeDragAndDrop.getDragURI(connectionProfile); + assert.equal(connectionProfile.id, uri); + + let uriGroup = serverTreeDragAndDrop.getDragURI(connectionProfileGroupId); + assert.equal(connectionProfileGroupId.id, uriGroup); + + let uriUndefined = serverTreeDragAndDrop.getDragURI(undefined); + assert.equal(null, uriUndefined); + + }); + + test('able to get DragLabel', async () => { + let label = serverTreeDragAndDrop.getDragLabel(connectionProfileArray); + assert.equal(connectionProfileArray[0].serverName, label); + + let labelGroup = serverTreeDragAndDrop.getDragLabel(connectionProfileGroupArray); + assert.equal(connectionProfileGroupArray[0].name, labelGroup); + + let labelTreeNode = serverTreeDragAndDrop.getDragLabel(treeNodeArray); + assert.equal(treeNodeArray[0].label, labelTreeNode); + + let labelUndefined = serverTreeDragAndDrop.getDragLabel(undefined); + assert.equal(undefined, labelUndefined); + + }); + + +}); diff --git a/src/sql/workbench/services/serverGroup/common/serverGroupViewModel.ts b/src/sql/workbench/services/serverGroup/common/serverGroupViewModel.ts index 1807d6b165..b9c1b839c7 100644 --- a/src/sql/workbench/services/serverGroup/common/serverGroupViewModel.ts +++ b/src/sql/workbench/services/serverGroup/common/serverGroupViewModel.ts @@ -9,17 +9,18 @@ import * as TypeChecker from 'vs/base/common/types'; import { localize } from 'vs/nls'; import * as strings from 'vs/base/common/strings'; +export const DefaultServerGroupColor = '#515151'; + export class ServerGroupViewModel { public groupName: string; public groupDescription?: string; public groupColor?: string; - public colors: string[] = ['#515151', '#004760', '#771b00', '#700060', '#a17d01', '#006749', '#654502', '#3A0293']; + public colors: string[] = [DefaultServerGroupColor, '#004760', '#771b00', '#700060', '#a17d01', '#006749', '#654502', '#3A0293']; private _domainModel?: IConnectionProfileGroup; private _editMode: boolean; private readonly _addServerGroupTitle: string = localize('serverGroup.addServerGroup', "Add server group"); private readonly _editServerGroupTitle: string = localize('serverGroup.editServerGroup', "Edit server group"); - private readonly _defaultColor: string = '#515151'; constructor(domainModel?: IConnectionProfileGroup, colors?: string[]) { // keep reference to domain model to be able to see if there are pending changes @@ -37,7 +38,7 @@ export class ServerGroupViewModel { // initialize defaults for a new group this.groupName = ''; this.groupDescription = ''; - this.groupColor = this._defaultColor; + this.groupColor = DefaultServerGroupColor; this._editMode = false; }