diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index fbc9612d1b..fafd2f6055 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -176,19 +176,17 @@ } } ], + "connectionTreeProvider": [ + { + "id": "connectionDialog/azureResourceExplorer", + "name": "%azure.resource.explorer.title%" + } + ], "dataExplorer": { - "dialog/connection": [ - { - "id": "azureResourceExplorer_dialog", - "name": "%azure.resource.explorer.title%", - "when": "config.connection.dialog.browse" - } - ], "dataExplorer": [ { "id": "azureResourceExplorer", - "name": "%azure.resource.explorer.title%", - "when": "!config.connection.dialog.browse" + "name": "%azure.resource.explorer.title%" } ] }, diff --git a/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts b/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts new file mode 100644 index 0000000000..97ae7c283b --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/flatTreeProvider.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import { AppContext } from '../../appContext'; +import * as nls from 'vscode-nls'; +import { TokenCredentials } from '@azure/ms-rest-js'; +const localize = nls.loadMessageBundle(); + +import { TreeNode } from '../treeNode'; +import { AzureResourceMessageTreeNode } from '../messageTreeNode'; +import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; +import { IAzureResourceNodeWithProviderId, IAzureResourceSubscriptionService } from '../interfaces'; +import { AzureResourceServiceNames } from '../constants'; +import { AzureResourceService } from '../resourceService'; + + +export class FlatAzureResourceTreeProvider implements vscode.TreeDataProvider, IAzureResourceTreeChangeHandler { + public isSystemInitialized: boolean = false; + + private _onDidChangeTreeData = new vscode.EventEmitter(); + + private resourceLoader: ResourceLoader; + + public constructor(private readonly appContext: AppContext) { + } + + public async getChildren(element?: TreeNode): Promise { + if (element) { + return element.getChildren(true); + } + + if (!this.resourceLoader) { + this.resourceLoader = new ResourceLoader(this.appContext); + this.resourceLoader.onDidAddNewResource(e => this._onDidChangeTreeData.fire(e)); + } + + if (this.resourceLoader.state === LoaderState.NotStarted) { + this.resourceLoader.start(); + return [AzureResourceMessageTreeNode.create(localize('azure.resource.tree.treeProvider.loadingLabel', "Loading ..."), undefined)]; + } + + return this.resourceLoader.children; + } + + public get onDidChangeTreeData(): vscode.Event { + return this._onDidChangeTreeData.event; + } + + public notifyNodeChanged(node: TreeNode): void { + this._onDidChangeTreeData.fire(node); + } + + public async refresh(node: TreeNode, isClearingCache: boolean): Promise { + if (isClearingCache) { + if ((node instanceof AzureResourceContainerTreeNodeBase)) { + node.clearCache(); + } + } + + this._onDidChangeTreeData.fire(node); + } + + public getTreeItem(element: TreeNode): vscode.TreeItem | Thenable { + return element.getTreeItem(); + } +} + +enum LoaderState { + NotStarted, + Loading, + Complete +} + +class ResourceLoader { + private _state: LoaderState = LoaderState.NotStarted; + + private readonly resourceGroups = new Map(); + + private readonly subscriptionService: IAzureResourceSubscriptionService; + private readonly resourceService: AzureResourceService; + + private readonly _onDidAddNewResource = new vscode.EventEmitter(); + public readonly onDidAddNewResource = this._onDidAddNewResource.event; + + constructor(private readonly appContext: AppContext) { + this.subscriptionService = appContext.getService(AzureResourceServiceNames.subscriptionService); + this.resourceService = appContext.getService(AzureResourceServiceNames.resourceService); + } + + get state(): LoaderState { + return this._state; + } + + get children(): AzureResourceResourceTreeNode[] { + return Array.from(this.resourceGroups.values()); + } + + async start(): Promise { + if (this.state === LoaderState.Loading) { + throw new Error('Resource Loader already loading'); + } + + let doRefresh = false; + + // if we just fire every time we get an a new resource we crash the application + // this effectively buffers the event so that we don't cause hangs. + let interval = setInterval(() => { + if (doRefresh) { + doRefresh = false; + this._onDidAddNewResource.fire(undefined); + } + }, 500); + + this._state = LoaderState.Loading; + + const accounts = await azdata.accounts.getAllAccounts(); + + for (const account of accounts) { + for (const tenant of account.properties.tenants) { + const token = await azdata.accounts.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.ResourceManagement); + + for (const subscription of await this.subscriptionService.getSubscriptions(account, new TokenCredentials(token.token, token.tokenType), tenant.id)) { + for (const providerId of await this.resourceService.listResourceProviderIds()) { + for (const group of await this.resourceService.getRootChildren(providerId, account, subscription, subscription.tenant)) { + const children = await this.resourceService.getChildren(providerId, group.resourceNode); + if (this.resourceGroups.has(group.resourceProviderId)) { + const groupNode = this.resourceGroups.get(group.resourceProviderId); + groupNode.pushItems(...children); + } else { + const groupNode = new AzureResourceResourceTreeNode(group, this.appContext); + this.resourceGroups.set(group.resourceProviderId, groupNode); + groupNode.pushItems(...children); + } + doRefresh = true; + } + } + } + } + } + + console.log('finished loading'); + + clearInterval(interval); + + this._state = LoaderState.Complete; + } +} + +class AzureResourceResourceTreeNode extends TreeNode { + private _resourceService: AzureResourceService; + + public constructor( + public readonly resourceNodeWithProviderId: IAzureResourceNodeWithProviderId, + private appContext: AppContext + ) { + super(); + this._resourceService = appContext.getService(AzureResourceServiceNames.resourceService); + } + + private _children: IAzureResourceNodeWithProviderId[] = []; + + pushItems(...items: IAzureResourceNodeWithProviderId[]): void { + this._children.push(...items); + } + + public async getChildren(): Promise { + // It is a leaf node. + + try { + + if (this._children.length === 0) { + return [AzureResourceMessageTreeNode.create(localize('azure.resource.resourceTreeNode.noResourcesLabel', "No Resources found"), this)]; + } else { + return this._children.map((child) => { + // To make tree node's id unique, otherwise, treeModel.js would complain 'item already registered' + child.resourceNode.treeItem.id = `${this.resourceNodeWithProviderId.resourceNode.treeItem.id}.${child.resourceNode.treeItem.id}`; + return new AzureResourceResourceTreeNode(child, this.appContext); + }); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public getTreeItem(): vscode.TreeItem | Promise { + return this._resourceService.getTreeItem(this.resourceNodeWithProviderId.resourceProviderId, this.resourceNodeWithProviderId.resourceNode); + } + + public getNodeInfo(): azdata.NodeInfo { + const treeItem = this.resourceNodeWithProviderId.resourceNode.treeItem; + + return { + label: treeItem.label, + isLeaf: treeItem.collapsibleState === vscode.TreeItemCollapsibleState.None ? true : false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: treeItem.contextValue, + nodeSubType: undefined, + iconType: treeItem.contextValue + }; + } + + public get nodePathValue(): string { + return this.resourceNodeWithProviderId.resourceNode.treeItem.id; + } + +} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index f5c916e5e4..e983b7ef71 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -44,6 +44,7 @@ import * as constants from './constants'; import { AzureResourceGroupService } from './azureResource/providers/resourceGroup/resourceGroupService'; import { Logger } from './utils/Logger'; import { TokenCredentials } from '@azure/ms-rest-js'; +import { FlatAzureResourceTreeProvider } from './azureResource/tree/flatTreeProvider'; let extensionContext: vscode.ExtensionContext; @@ -84,8 +85,9 @@ export async function activate(context: vscode.ExtensionContext): Promise onDidChangeConfiguration(e), this)); registerAzureResourceCommands(appContext, azureResourceTree); diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index 24b0e3da5e..3196cffd5a 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -137,7 +137,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp }, connect(connectionProfile: azdata.IConnectionProfile, saveConnection: boolean, showDashboard: boolean): Thenable { return extHostConnectionManagement.$connect(connectionProfile, saveConnection, showDashboard); - } + }, }; // Backcompat "sqlops" APIs diff --git a/src/sql/workbench/contrib/connection/browser/connection.contribution.ts b/src/sql/workbench/contrib/connection/browser/connection.contribution.ts index e100ee4040..f8e5a71947 100644 --- a/src/sql/workbench/contrib/connection/browser/connection.contribution.ts +++ b/src/sql/workbench/contrib/connection/browser/connection.contribution.ts @@ -25,6 +25,8 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ConnectionStatusbarItem, LifecyclePhase.Restored); +import 'sql/workbench/contrib/connection/common/connectionTreeProviderExentionPoint'; + // Connection Dashboard registration const actionRegistry = Registry.as(Extensions.WorkbenchActions); diff --git a/src/sql/workbench/contrib/connection/common/connectionTreeProviderExentionPoint.ts b/src/sql/workbench/contrib/connection/common/connectionTreeProviderExentionPoint.ts new file mode 100644 index 0000000000..606b6b4b1f --- /dev/null +++ b/src/sql/workbench/contrib/connection/common/connectionTreeProviderExentionPoint.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConnectionTreeDescriptor, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { isArray, isString } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +const schema: IJSONSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: localize('connectionTreeProvider.schema.name', "User visible name for the tree provider") + }, + id: { + type: 'string', + description: localize('connectionTreeProvider.schema.id', "Id for the provider, must be the same as when registering the tree data provider and must start with `connectionDialog/`") + } + } + } +}; + +const connectionTreeProviderExt = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'connectionTreeProvider', jsonSchema: schema }); + +class ConnectionTreeProviderHandle implements IWorkbenchContribution { + private disposables = new Map(); + + constructor(@IConnectionTreeService connectionTreeService: IConnectionTreeService) { + connectionTreeProviderExt.setHandler((extensions, delta) => { + + function handleProvider(contrib: IConnectionTreeDescriptor) { + return connectionTreeService.registerTreeDescriptor(contrib); + } + + delta.added.forEach(added => { + // resolveIconPath(added); + if (!isArray(added.value)) { + added.collector.error('Value must be array'); + return; + } + + for (const provider of added.value) { + if (!validateDescriptor(provider)) { + added.collector.error('Invalid descriptor'); + continue; + } + this.disposables.set(provider, handleProvider(provider)); + } + }); + + delta.removed.forEach(removed => { + for (const provider of removed.value) { + this.disposables.get(provider)!.dispose(); + } + }); + }); + } +} + +function validateDescriptor(descriptor: IConnectionTreeDescriptor): boolean { + if (!isString(descriptor.name)) { + return false; + } + if (!isString(descriptor.id)) { + return false; + } + return true; +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ConnectionTreeProviderHandle, LifecyclePhase.Ready); diff --git a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts new file mode 100644 index 0000000000..f16f8fd028 --- /dev/null +++ b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- + * 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/connectionBrowseTab'; +import { IPanelTab, IPanelView } from 'sql/base/browser/ui/panel/panel'; +import { ITreeItem } from 'sql/workbench/common/views'; +import { IConnectionTreeDescriptor, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; +import * as DOM from 'vs/base/browser/dom'; +import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IAsyncDataSource, ITreeMouseEvent, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/resources'; +import { isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FileKind } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { FileThemeIcon, FolderThemeIcon, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ITreeItemLabel, ITreeViewDataProvider, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { Emitter, Event } from 'vs/base/common/event'; +import { AsyncRecentConnectionTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; +import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; +import { ServerTreeRenderer } from 'sql/workbench/services/objectExplorer/browser/serverTreeRenderer'; +import { ConnectionProfileGroupRenderer, ConnectionProfileRenderer, TreeNodeRenderer } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; + +export type TreeElement = ConnectionProviderElement | ITreeItemFromProvider | SavedConnectionNode | ServerTreeElement; + +export class ConnectionBrowseTab implements IPanelTab { + public readonly title = localize('connectionDialog.browser', "Browse"); + public readonly identifier = 'connectionBrowse'; + public readonly view = this.instantiationService.createInstance(ConnectionBrowserView); + constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { } +} + +export class ConnectionBrowserView extends Disposable implements IPanelView { + private tree: WorkbenchAsyncDataTree | undefined; + private model: TreeModel | undefined; + private treeLabels: ResourceLabels | undefined; + public onDidChangeVisibility = Event.None; + + private readonly _onSelect = this._register(new Emitter>()); + public readonly onSelect = this._onSelect.event; + + private readonly _onDblClick = this._register(new Emitter>()); + public readonly onDblClick = this._onDblClick.event; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConnectionTreeService private readonly connectionTreeService: IConnectionTreeService + ) { + super(); + this.connectionTreeService.setView(this); + } + + render(container: HTMLElement): void { + + this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); + const renderers: ITreeRenderer[] = [ + new ProviderElementRenderer(), + this.instantiationService.createInstance(TreeItemRenderer, this.treeLabels), + this.instantiationService.createInstance(ConnectionProfileRenderer, true), + this.instantiationService.createInstance(ConnectionProfileGroupRenderer), + this.instantiationService.createInstance(TreeNodeRenderer), + new SavedConnectionsNodeRenderer() + ]; + + this.model = this.instantiationService.createInstance(TreeModel); + + this.tree = this._register(this.instantiationService.createInstance( + WorkbenchAsyncDataTree, + 'Browser Connections', + container, + new ListDelegate(), + renderers, + new DataSource(), + { + identityProvider: new IdentityProvider(), + horizontalScrolling: false, + setRowLineHeight: false, + transformOptimization: false, + accessibilityProvider: new ListAccessibilityProvider() + }) as WorkbenchAsyncDataTree); + + this.tree.onMouseDblClick(e => this._onDblClick.fire(e)); + this.tree.onMouseClick(e => this._onSelect.fire(e)); + + this.tree.setInput(this.model); + + this._register(this.connectionTreeService.onDidAddProvider(() => this.tree.updateChildren(this.model))); + } + + async refresh(items?: ITreeItem[]): Promise { + if (this.tree) { + if (items) { + for (const item of items) { + await this.tree.updateChildren({ element: item }); + } + } else { + return this.tree.updateChildren(); + } + } + } + + layout(dimension: DOM.Dimension): void { + this.tree.layout(dimension.height, dimension.width); + } + + focus(): void { + this.tree.domFocus(); + } +} + +export interface ITreeItemFromProvider { + readonly element: ITreeItem; + getChildren?(): Promise +} + +class ConnectionProviderElement { + public readonly id = this.descriptor.id; + public readonly name = this.descriptor.name; + + constructor(private readonly provider: ITreeViewDataProvider, private readonly descriptor: IConnectionTreeDescriptor) { + } + + async getChildren(element?: ITreeItem): Promise { + const children = await this.provider.getChildren(element); + return children.map(v => ({ + element: v, + getChildren: () => this.getChildren(v) + })); + } +} + +class ListDelegate implements IListVirtualDelegate { + getHeight(): number { + return 22; + } + + getTemplateId(element: TreeElement): string { + if (element instanceof ConnectionProviderElement) { + return ProviderElementRenderer.TEMPLATE_ID; + } else if (element instanceof ConnectionProfile) { + return ServerTreeRenderer.CONNECTION_TEMPLATE_ID; + } else if (element instanceof ConnectionProfileGroup) { + return ServerTreeRenderer.CONNECTION_GROUP_TEMPLATE_ID; + } else if (element instanceof TreeNode) { + return ServerTreeRenderer.OBJECTEXPLORER_TEMPLATE_ID; + } else if (element instanceof SavedConnectionNode) { + return SavedConnectionsNodeRenderer.TEMPLATE_ID; + } else { + return TreeItemRenderer.TREE_TEMPLATE_ID; + } + } +} + +interface ProviderElementTemplate { + readonly icon: HTMLElement; + readonly name: HTMLElement; +} + +class ProviderElementRenderer implements ITreeRenderer { + public static readonly TEMPLATE_ID = 'ProviderElementTemplate'; + public readonly templateId = ProviderElementRenderer.TEMPLATE_ID; + + renderTemplate(container: HTMLElement): ProviderElementTemplate { + const icon = DOM.append(container, DOM.$('.icon')); + const name = DOM.append(container, DOM.$('.name')); + return { name, icon }; + } + + renderElement(element: ITreeNode, index: number, templateData: ProviderElementTemplate, height: number): void { + templateData.name.innerText = element.element.name; + } + + disposeTemplate(templateData: ProviderElementTemplate): void { + } +} + +interface SavedConnectionNodeElementTemplate { + readonly icon: HTMLElement; + readonly name: HTMLElement; +} + +class SavedConnectionsNodeRenderer implements ITreeRenderer { + public static readonly TEMPLATE_ID = 'savedConnectionNode'; + public readonly templateId = SavedConnectionsNodeRenderer.TEMPLATE_ID; + + renderTemplate(container: HTMLElement): SavedConnectionNodeElementTemplate { + const icon = DOM.append(container, DOM.$('.icon')); + const name = DOM.append(container, DOM.$('.name')); + return { name, icon }; + } + + renderElement(element: ITreeNode, index: number, templateData: SavedConnectionNodeElementTemplate, height: number): void { + templateData.name.innerText = localize('savedConnections', "Saved Connections"); + } + + disposeTemplate(templateData: SavedConnectionNodeElementTemplate): void { + } +} + +class IdentityProvider implements IIdentityProvider { + getId(element: TreeElement): string { + if (element instanceof ConnectionProviderElement) { + return element.id; + } else if (element instanceof ConnectionProfile) { + return element.id; + } else if (element instanceof ConnectionProfileGroup) { + return element.id!; + } else if (element instanceof TreeNode) { + return element.id; + } else if (element instanceof SavedConnectionNode) { + return element.id; + } else { + return element.element.handle; + } + } +} + +class TreeModel { + + constructor( + @IConnectionTreeService private readonly connectionTreeService: IConnectionTreeService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + getChildren(): TreeElement[] { + const descriptors = Array.from(this.connectionTreeService.descriptors); + return [this.instantiationService.createInstance(SavedConnectionNode), ...Iterable.map(this.connectionTreeService.providers, ([id, provider]) => new ConnectionProviderElement(provider, descriptors.find(i => i.id === id)))]; + } +} + +class ListAccessibilityProvider implements IListAccessibilityProvider { + getAriaLabel(element: TreeElement): string { + if (element instanceof ConnectionProviderElement) { + return element.name; + } else if (element instanceof ConnectionProfile) { + return element.serverName; + } else if (element instanceof ConnectionProfileGroup) { + return element.name; + } else if (element instanceof TreeNode) { + return element.label; + } else if (element instanceof SavedConnectionNode) { + return localize('savedConnection', "Saved Connections"); + } else { + return element.element.handle; + } + } + + getWidgetAriaLabel(): string { + return localize('connectionBrowserTree', "Connection Browser Tree"); + } +} + +class DataSource implements IAsyncDataSource { + hasChildren(element: TreeModel | TreeElement): boolean { + if (element instanceof TreeModel) { + return true; + } else if (element instanceof ConnectionProviderElement) { + return true; + } else if (element instanceof ConnectionProfile) { + return false; + } else if (element instanceof ConnectionProfileGroup) { + return element.hasChildren(); + } else if (element instanceof TreeNode) { + return element.children.length > 0; + } else if (element instanceof SavedConnectionNode) { + return true; + } else { + return element.element.collapsibleState !== TreeItemCollapsibleState.None; + } + } + + getChildren(element: TreeModel | TreeElement): Iterable | Promise> { + if (!(element instanceof ConnectionProfile)) { + return element.getChildren(); + } + return []; + } +} + +class SavedConnectionNode { + public readonly id = 'SavedConnectionNode'; + private readonly dataSource: AsyncRecentConnectionTreeDataSource; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService + ) { + this.dataSource = instantiationService.createInstance(AsyncRecentConnectionTreeDataSource); + } + + getChildren() { + return this.dataSource.getChildren(TreeUpdateUtils.getTreeInput(this.connectionManagementService)); + } +} + +interface ITreeExplorerTemplateData { + elementDisposable: IDisposable; + container: HTMLElement; + resourceLabel: IResourceLabel; + icon: HTMLElement; + // actionBar: ActionBar; +} + +class TreeItemRenderer extends Disposable implements ITreeRenderer { + static readonly ITEM_HEIGHT = 22; + static readonly TREE_TEMPLATE_ID = 'treeExplorer'; + + // private _actionRunner: MultipleSelectionActionRunner | undefined; + + constructor( + // private treeViewId: string, + // private menus: TreeMenus, + private labels: ResourceLabels, + // private actionViewItemProvider: IActionViewItemProvider, + // private aligner: Aligner, + @IThemeService private readonly themeService: IThemeService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILabelService private readonly labelService: ILabelService + ) { + super(); + } + + get templateId(): string { + return TreeItemRenderer.TREE_TEMPLATE_ID; + } + + // set actionRunner(actionRunner: MultipleSelectionActionRunner) { + // this._actionRunner = actionRunner; + // } + + renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { + DOM.addClass(container, 'custom-view-tree-node-item'); + + const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); + + const resourceLabel = this.labels.create(container, { supportHighlights: true }); + // const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); + // const actionBar = new ActionBar(actionsContainer, { + // actionViewItemProvider: this.actionViewItemProvider + // }); + + return { resourceLabel, icon, container, elementDisposable: Disposable.None }; + } + + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + const node = element.element.element; + const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; + const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : resource ? { label: basename(resource) } : undefined; + const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined; + const label = treeItemLabel ? treeItemLabel.label : undefined; + const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark; + const iconUrl = icon ? URI.revive(icon) : null; + const title = node.tooltip ? isString(node.tooltip) ? node.tooltip : undefined : resource ? undefined : label; + const sqlIcon = node.sqlIcon; + + // reset + // templateData.actionBar.clear(); + + if (resource || this.isFileKindThemeIcon(node.themeIcon)) { + const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); + templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); + } else { + templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); + } + + templateData.icon.title = title ? title : ''; + + if (iconUrl || sqlIcon) { + templateData.icon.className = 'custom-view-tree-node-item-icon'; + if (sqlIcon) { + DOM.toggleClass(templateData.icon, sqlIcon, !!sqlIcon); // tracked change + } + DOM.toggleClass(templateData.icon, 'icon', !!sqlIcon); + templateData.icon.style.backgroundImage = iconUrl ? DOM.asCSSUrl(iconUrl) : ''; + } else { + let iconClass: string | undefined; + if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) { + iconClass = ThemeIcon.asClassName(node.themeIcon); + } + templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : ''; + templateData.icon.style.backgroundImage = ''; + } + + // templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; + // templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + // if (this._actionRunner) { + // templateData.actionBar.actionRunner = this._actionRunner; + // } + this.setAlignment(templateData.container, node); + templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); + } + + private setAlignment(container: HTMLElement, treeItem: ITreeItem) { + // DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); + } + + private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { + if (icon) { + return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id; + } else { + return false; + } + } + + private getFileKind(node: ITreeItem): FileKind { + if (node.themeIcon) { + switch (node.themeIcon.id) { + case FileThemeIcon.id: + return FileKind.FILE; + case FolderThemeIcon.id: + return FileKind.FOLDER; + } + } + return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; + } + + disposeElement(resource: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + } + + disposeTemplate(templateData: ITreeExplorerTemplateData): void { + // templateData.resourceLabel.dispose(); + // templateData.actionBar.dispose(); + templateData.elementDisposable.dispose(); + } +} diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index 5dc1d13541..151641596e 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -24,7 +24,7 @@ import * as styler from 'vs/platform/theme/common/styler'; import * as DOM from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; import { ILogService } from 'vs/platform/log/common/log'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; @@ -32,13 +32,7 @@ import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { entries } from 'sql/base/common/collections'; import { attachTabbedPanelStyler, attachModalDialogStyler } from 'sql/workbench/common/styler'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IViewPaneContainer, IView, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewDescriptorService, ViewContainer, IViewContainerModel, IAddedViewDescriptorRef, IViewDescriptorRef, ITreeViewDescriptor } from 'vs/workbench/common/views'; -import { IAction, IActionViewItem } from 'vs/base/common/actions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { ViewPane, IPaneColors } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { ITreeView } from 'sql/workbench/common/views'; + import { IConnectionProfile } from 'azdata'; import { TreeUpdateUtils, IExpandableTree } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; import { SavedConnectionTreeController } from 'sql/workbench/services/connection/browser/savedConnectionTreeController'; @@ -46,17 +40,17 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults'; 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 } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConnectionBrowseTab, ITreeItemFromProvider } from 'sql/workbench/services/connection/browser/connectionBrowseTab'; export interface OnShowUIResponse { selectedProviderDisplayName: string; container: HTMLElement; } -export class ConnectionDialogWidget extends Modal implements IViewPaneContainer { +export class ConnectionDialogWidget extends Modal { private _body: HTMLElement; private _recentConnection: HTMLElement; private _noRecentConnection: HTMLElement; @@ -95,11 +89,10 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer private _onResetConnection = new Emitter(); public onResetConnection: Event = this._onResetConnection.event; + private browsePanel: ConnectionBrowseTab; + private _connecting = false; - readonly viewContainer: ViewContainer; - protected readonly viewContainerModel: IViewContainerModel; - private paneItems: { pane: ViewPane, disposable: IDisposable }[] = []; private orthogonalSize = 0; constructor( @@ -110,7 +103,6 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer @IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextViewService private readonly contextViewService: IContextViewService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IThemeService themeService: IThemeService, @ILayoutService layoutService: ILayoutService, @IAdsTelemetryService telemetryService: IAdsTelemetryService, @@ -132,16 +124,16 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer contextKeyService, { hasSpinner: true, spinnerTitle: localize('connecting', "Connecting"), hasErrors: true }); - const container = viewDescriptorService.getViewContainerById(VIEW_CONTAINER.id); - if (!container) { - throw new Error('Could not find container'); - } - - this.viewContainer = container; - this.viewContainerModel = viewDescriptorService.getViewContainerModel(container); - } - getActionsContext(): unknown { - throw new Error('Method not implemented.'); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('connection.dialog.browse') && this.browsePanel) { + const doUseBrowsePanel = this._configurationService.getValue('connection.dialog.browse'); + if (doUseBrowsePanel && !this._panel.contains(this.browsePanel)) { + this._panel.pushTab(this.browsePanel); + } else if (!doUseBrowsePanel && this._panel.contains(this.browsePanel)) { + this._panel.removeTab(this.browsePanel.identifier); + } + } + })); } /** @@ -266,6 +258,28 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer } }); + this.browsePanel = new ConnectionBrowseTab(this.instantiationService); + + this.browsePanel.view.onSelect(e => { + if (e.element instanceof ConnectionProfile) { + this.onConnectionClick(e.element); + } else if ((e.element as ITreeItemFromProvider)?.element?.payload) { + this.onConnectionClick((e.element as ITreeItemFromProvider).element.payload); + } + }); + + this.browsePanel.view.onDblClick(e => { + if (e.element instanceof ConnectionProfile) { + this.onConnectionClick(e.element, true); + } else if ((e.element as ITreeItemFromProvider)?.element?.payload) { + this.onConnectionClick((e.element as ITreeItemFromProvider).element.payload, true); + } + }); + + if (this._configurationService.getValue('connection.dialog.browse')) { + this._panel.pushTab(this.browsePanel); + } + this._connectionDetailTitle = DOM.append(this._body, DOM.$('.connection-details-title')); this._connectionDetailTitle.innerText = localize('connectionDetailsTitle', "Connection Details"); @@ -278,17 +292,6 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer this._connectionUIContainer = DOM.$('.connection-provider-info', { id: 'connectionProviderInfo' }); this._body.append(this._connectionUIContainer); - this._register(this.viewContainerModel.onDidAddVisibleViewDescriptors(added => this.onDidAddViewDescriptors(added))); - this._register(this.viewContainerModel.onDidRemoveVisibleViewDescriptors(removed => this.onDidRemoveViewDescriptors(removed))); - const addedViews: IAddedViewDescriptorRef[] = this.viewContainerModel.visibleViewDescriptors.map((viewDescriptor, index) => { - const size = this.viewContainerModel.getSize(viewDescriptor.id); - const collapsed = this.viewContainerModel.isCollapsed(viewDescriptor.id); - return ({ viewDescriptor, index, size, collapsed }); - }); - if (addedViews.length) { - this.onDidAddViewDescriptors(addedViews); - } - this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e))); this.updateTheme(this._themeService.getColorTheme()); } @@ -506,9 +509,6 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer // Height is the overall height. Since we're laying out a specific component, always get its actual height const width = DOM.getContentWidth(this._body); this.orthogonalSize = width; - for (const { pane } of this.paneItems) { - pane.orthogonalSize = width; - } this._panel.layout(new DOM.Dimension(this.orthogonalSize, height - 38 - 35 - 326)); // height - connection title - connection type input - connection widget } @@ -564,132 +564,4 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer public get databaseDropdownExpanded(): boolean { return this._databaseDropdownExpanded; } - - //#region ViewletContainer - public readonly onDidAddViews: Event = Event.None; - public readonly onDidRemoveViews: Event = Event.None; - public readonly onDidChangeViewVisibility: Event = Event.None; - public get views(): IView[] { - return []; - } - - setVisible(visible: boolean): void { - } - - isVisible(): boolean { - return true; - } - - focus(): void { - } - - getActions(): IAction[] { - return []; - } - - getSecondaryActions(): IAction[] { - return []; - } - - getActionViewItem(action: IAction): IActionViewItem { - throw new Error('Method not implemented.'); - } - - getView(viewId: string): IView { - throw new Error('Method not implemented.'); - } - - saveState(): void { - } - - private onDidRemoveViewDescriptors(removed: IViewDescriptorRef[]): void { - for (const ref of removed) { - this.removePane(ref); - } - } - - private removePane(ref: IViewDescriptorRef): void { - const item = this.paneItems.find(p => p.pane.id === ref.viewDescriptor.id); - this._panel.removeTab(item.pane.id); - dispose(item.disposable); - } - - protected onDidAddViewDescriptors(added: IAddedViewDescriptorRef[]): ViewPane[] { - const panesToAdd: { pane: ViewPane, size: number, treeView: ITreeView }[] = []; - - for (const { viewDescriptor, size } of added) { - const treeViewDescriptor = viewDescriptor as ITreeViewDescriptor; - const pane = this.createView(treeViewDescriptor, - { - id: viewDescriptor.id, - title: viewDescriptor.name, - expanded: true - }); - - pane.render(); - - panesToAdd.push({ pane, size: size || pane.minimumSize, treeView: treeViewDescriptor.treeView as ITreeView }); - } - - this.addPanes(panesToAdd); - - const panes: ViewPane[] = []; - for (const { pane } of panesToAdd) { - pane.setVisible(this.isVisible()); - pane.headerVisible = false; - panes.push(pane); - } - return panes; - } - - protected createView(viewDescriptor: ITreeViewDescriptor, options: IViewletViewOptions): ViewPane { - return (this.instantiationService as any).createInstance(viewDescriptor.ctorDescriptor.ctor, ...(viewDescriptor.ctorDescriptor.staticArguments || []), options) as ViewPane; - } - - private addPanes(panes: { pane: ViewPane, treeView: ITreeView, size: number }[]): void { - - for (const { pane, treeView, size } of panes) { - this.addPane({ pane, treeView }, size); - } - - // this._onDidAddViews.fire(panes.map(({ pane }) => pane)); - } - - private addPane({ pane, treeView }: { pane: ViewPane, treeView: ITreeView }, size: number): void { - const paneStyler = styler.attachStyler(this._themeService, { - headerForeground: PANEL_SECTION_HEADER_FOREGROUND, - headerBackground: PANEL_SECTION_HEADER_BACKGROUND, - headerBorder: PANEL_SECTION_HEADER_BORDER, - dropBackground: PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, - leftBorder: PANEL_SECTION_BORDER - }, pane); - - const disposable = combinedDisposable(pane, paneStyler); - const paneItem = { pane, disposable }; - treeView.onDidChangeSelection(e => { - if (e.length > 0 && e[0].payload) { - this.onConnectionClick(e[0].payload); - } - }); - - this.paneItems.push(paneItem); - this._panel.pushTab({ - identifier: pane.id, - title: pane.title, - view: { - focus: () => pane.focus(), - layout: d => pane.layout(d.height), - render: e => e.appendChild(pane.element), - } - }); - } - //#endregion } - -export const VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ - id: 'dialog/connection', - name: 'ConnectionDialog', - ctorDescriptor: new SyncDescriptor(class { }), - order: 0, - storageId: `dialog/connection.state` -}, ViewContainerLocation.Dialog); diff --git a/src/sql/workbench/services/connection/browser/media/connectionBrowseTab.css b/src/sql/workbench/services/connection/browser/media/connectionBrowseTab.css new file mode 100644 index 0000000000..5e8f8e3069 --- /dev/null +++ b/src/sql/workbench/services/connection/browser/media/connectionBrowseTab.css @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.connection-dialog .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon { + background-size: 16px; + background-position: left center; + background-repeat: no-repeat; + padding-right: 6px; + width: 16px; + height: 22px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.connection-dialog .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { + margin-top: 3px; +} + +.connection-dialog .monaco-list .monaco-list-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { + color: currentColor !important; +} + +.connection-dialog .monaco-list .monaco-list-row .custom-view-tree-node-item { + display: flex; + height: 22px; + line-height: 22px; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + flex-wrap: nowrap; + padding-left: 3px; +} + +.connection-dialog .monaco-list .monaco-list-row { + padding-right: 12px; + padding-left: 0px; +} diff --git a/src/sql/workbench/services/connection/common/connectionTreeService.ts b/src/sql/workbench/services/connection/common/connectionTreeService.ts new file mode 100644 index 0000000000..54cac43e4f --- /dev/null +++ b/src/sql/workbench/services/connection/common/connectionTreeService.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITreeViewDataProvider } from 'vs/workbench/common/views'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ITreeItem } from 'sql/workbench/common/views'; + +export interface IConnectionTreeService { + _serviceBrand: undefined; + registerTreeProvider(id: string, provider: ITreeViewDataProvider): IDisposable; + registerTreeDescriptor(descriptor: IConnectionTreeDescriptor): IDisposable; + setView(view: IView): void; + readonly onDidAddProvider: Event; + readonly providers: Iterable<[string, ITreeViewDataProvider]>; + readonly descriptors: Iterable; + readonly view: IView | undefined; +} + +export const IConnectionTreeService = createDecorator('connectionTreeService'); + +export interface IView { + refresh(items?: ITreeItem[]) +} + +export interface IConnectionTreeDescriptor { + readonly id: string; + readonly name: string; + readonly icon: string; +} + +export class ConnectionTreeService implements IConnectionTreeService { + _serviceBrand; + private readonly _onDidAddProvider = new Emitter(); + public readonly onDidAddProvider = this._onDidAddProvider.event; + + private readonly _onDidRemoveProvider = new Emitter(); + public readonly onDidRemoveProvider = this._onDidRemoveProvider.event; + + private _providers = new Map(); + private _descriptors = new Set(); + + private _view: IView | undefined; + + registerTreeProvider(id: string, provider: ITreeViewDataProvider): IDisposable { + this._providers.set(id, provider); + this._onDidAddProvider.fire(provider); + return toDisposable(() => { + this._providers.delete(id); + this._onDidRemoveProvider.fire(); + }); + } + + registerTreeDescriptor(descriptor: IConnectionTreeDescriptor): IDisposable { + this._descriptors.add(descriptor); + return toDisposable(() => { + this._descriptors.delete(descriptor); + }); + } + + get descriptors(): Iterable { + return this._descriptors.values(); + } + + get providers(): Iterable<[string, ITreeViewDataProvider]> { + return this._providers.entries(); + } + + get view(): IView | undefined { + return this._view; + } + + setView(view: IView): void { + this._view = view; + } +} + +registerSingleton(IConnectionTreeService, ConnectionTreeService, false); diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts index de2cf5ffcd..11fcf46d67 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts @@ -17,7 +17,6 @@ import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/t import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; -import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; import { createConnectionProfile } from 'sql/workbench/services/connection/test/browser/connectionManagementService.test'; import { getUniqueConnectionProvidersByNameMap } from 'sql/workbench/services/connection/test/browser/connectionDialogWidget.test'; import { TestConnectionDialogWidget } from 'sql/workbench/services/connection/test/browser/testConnectionDialogWidget'; @@ -50,6 +49,9 @@ import { ViewContainer, Extensions, IViewsRegistry, IViewContainersRegistry, ITr import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView'; +import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; +import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; +import { ConnectionBrowserView } from 'sql/workbench/services/connection/browser/connectionBrowseTab'; suite('ConnectionDialogService tests', () => { const testTreeViewId = 'testTreeView'; @@ -101,6 +103,7 @@ suite('ConnectionDialogService tests', () => { testInstantiationService.stub(IThemeService, new TestThemeService()); testInstantiationService.stub(ILayoutService, new TestLayoutService()); testInstantiationService.stub(IAdsTelemetryService, new NullAdsTelemetryService()); + testInstantiationService.stub(IConnectionTreeService, new ConnectionTreeService()); connectionDialogService = new ConnectionDialogService(testInstantiationService, capabilitiesService, errorMessageService.object, new TestConfigurationService(), new BrowserClipboardService(), NullCommandService, new NullLogService()); (connectionDialogService as any)._connectionManagementService = mockConnectionManagementService.object; @@ -213,6 +216,9 @@ suite('ConnectionDialogService tests', () => { mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(RecentConnectionsDragAndDrop))).returns(() => { return testInstantiationService.createInstance(RecentConnectionsDragAndDrop); }); + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(ConnectionBrowserView))).returns(() => { + return testInstantiationService.createInstance(ConnectionBrowserView); + }); }); teardown(() => { diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts index fd1f364e8f..a1d3775912 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts @@ -27,6 +27,7 @@ import { ViewContainer, Extensions, IViewsRegistry, IViewContainersRegistry, ITr import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView'; +import { ConnectionTreeService, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; suite('ConnectionDialogWidget tests', () => { const testTreeViewId = 'testTreeView'; const ViewsRegistry = Registry.as(Extensions.ViewsRegistry); @@ -51,6 +52,7 @@ suite('ConnectionDialogWidget tests', () => { ViewsRegistry.registerViews([viewDescriptor], container); cmInstantiationService = new TestInstantiationService(); cmInstantiationService.stub(IStorageService, new TestStorageService()); + cmInstantiationService.stub(IConnectionTreeService, new ConnectionTreeService()); mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, undefined, // connection store undefined, // connection status manager diff --git a/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts b/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts index 7c0cdb7fc1..8ada9e1168 100644 --- a/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts @@ -36,7 +36,7 @@ export class TestConnectionDialogWidget extends ConnectionDialogWidget { @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, @IConfigurationService configurationService: IConfigurationService ) { - super(providerDisplayNameOptions, selectedProviderType, providerNameToDisplayNameMap, _instantiationService, _connectionManagementService, _contextMenuService, _contextViewService, viewDescriptorService, themeService, layoutService, telemetryService, contextKeyService, clipboardService, logService, textResourcePropertiesService, configurationService); + super(providerDisplayNameOptions, selectedProviderType, providerNameToDisplayNameMap, _instantiationService, _connectionManagementService, _contextMenuService, _contextViewService, themeService, layoutService, telemetryService, contextKeyService, clipboardService, logService, textResourcePropertiesService, configurationService); } public renderBody(container: HTMLElement) { super.renderBody(container); diff --git a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts index ee655ab9eb..6750e53f07 100644 --- a/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts +++ b/src/sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer.ts @@ -197,9 +197,11 @@ export class TreeNodeRenderer implements ITreeRenderer, index: number, template: TreeNodeTemplate): void { template.set(node.element); } + disposeTemplate(templateData: TreeNodeTemplate): void { templateData.dispose(); } diff --git a/src/sql/workbench/services/objectExplorer/common/treeNode.ts b/src/sql/workbench/services/objectExplorer/common/treeNode.ts index 5a8683845c..67f9574e08 100644 --- a/src/sql/workbench/services/objectExplorer/common/treeNode.ts +++ b/src/sql/workbench/services/objectExplorer/common/treeNode.ts @@ -17,7 +17,7 @@ export enum TreeItemCollapsibleState { } export interface ObjectExplorerCallbacks { - getChildren(treeNode?: TreeNode): Thenable; + getChildren(treeNode?: TreeNode): Promise; isExpanded(treeNode: TreeNode): Thenable; setNodeExpandedState(TreeNode: TreeNode, expandedState: TreeItemCollapsibleState): Thenable; setNodeSelected(TreeNode: TreeNode, selected: boolean, clearOtherSelections?: boolean): Thenable; @@ -160,7 +160,7 @@ export class TreeNode { }; } - public getChildren(): Thenable { + public getChildren(): Promise { return this._objectExplorerCallbacks?.getChildren(this) ?? Promise.resolve([]); } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 4653e5a9d7..d556cbabd0 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -13,6 +13,7 @@ import { isUndefinedOrNull, isNumber } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -25,7 +26,8 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie @IViewsService private readonly viewsService: IViewsService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IConnectionTreeService private readonly connectionTreeService: IConnectionTreeService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); @@ -46,6 +48,8 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie viewer.dataProvider = dataProvider; this.registerListeners(treeViewId, viewer); this._proxy.$setVisible(treeViewId, viewer.visible); + } else if (treeViewId.includes('connectionDialog')) { + this.connectionTreeService.registerTreeProvider(treeViewId, dataProvider); } else { this.notificationService.error('No view is registered with id: ' + treeViewId); } @@ -73,6 +77,9 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie if (viewer && dataProvider) { const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle); return viewer.refresh(itemsToRefresh.length ? itemsToRefresh : undefined); + } else if (treeViewId.includes('connectionDialog')) { + const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle); + return this.connectionTreeService.view?.refresh(itemsToRefresh.length ? itemsToRefresh : undefined); } return Promise.resolve(); } diff --git a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts index db70d8a2a0..c95318ef7d 100644 --- a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts @@ -67,7 +67,7 @@ suite('MainThreadHostTreeView', function () { return extHostTreeViewsShape; } drain(): any { return null; } - }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService()); + }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService(), undefined!); mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false }); await testExtensionService.whenInstalledExtensionsRegistered(); });