Connection Browse Tab (#12222)

* add browse tab and flat tree provider for azure

* fix tests

* add comment

* fix build errors

* fix test cases

Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Anthony Dresser
2020-10-13 14:58:09 -07:00
committed by GitHub
parent 3251b26317
commit 9fdb5037bc
17 changed files with 933 additions and 183 deletions

View File

@@ -137,7 +137,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
},
connect(connectionProfile: azdata.IConnectionProfile, saveConnection: boolean, showDashboard: boolean): Thenable<azdata.ConnectionResult> {
return extHostConnectionManagement.$connect(connectionProfile, saveConnection, showDashboard);
}
},
};
// Backcompat "sqlops" APIs

View File

@@ -25,6 +25,8 @@ const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Workbench
workbenchRegistry.registerWorkbenchContribution(ConnectionStatusbarItem, LifecyclePhase.Restored);
import 'sql/workbench/contrib/connection/common/connectionTreeProviderExentionPoint';
// Connection Dashboard registration
const actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);

View File

@@ -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<IConnectionTreeDescriptor[]>({ extensionPoint: 'connectionTreeProvider', jsonSchema: schema });
class ConnectionTreeProviderHandle implements IWorkbenchContribution {
private disposables = new Map<IConnectionTreeDescriptor, IDisposable>();
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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ConnectionTreeProviderHandle, LifecyclePhase.Ready);

View File

@@ -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<TreeModel, TreeElement> | undefined;
private model: TreeModel | undefined;
private treeLabels: ResourceLabels | undefined;
public onDidChangeVisibility = Event.None;
private readonly _onSelect = this._register(new Emitter<ITreeMouseEvent<TreeElement>>());
public readonly onSelect = this._onSelect.event;
private readonly _onDblClick = this._register(new Emitter<ITreeMouseEvent<TreeElement>>());
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<TreeElement, any, any>[] = [
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<TreeModel, TreeElement>);
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<void> {
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<ITreeItemFromProvider[]>
}
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<ITreeItemFromProvider[]> {
const children = await this.provider.getChildren(element);
return children.map(v => ({
element: v,
getChildren: () => this.getChildren(v)
}));
}
}
class ListDelegate implements IListVirtualDelegate<TreeElement> {
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<ConnectionProviderElement, void, ProviderElementTemplate> {
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<ConnectionProviderElement, void>, 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<ConnectionProviderElement, void, SavedConnectionNodeElementTemplate> {
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<ConnectionProviderElement, void>, index: number, templateData: SavedConnectionNodeElementTemplate, height: number): void {
templateData.name.innerText = localize('savedConnections', "Saved Connections");
}
disposeTemplate(templateData: SavedConnectionNodeElementTemplate): void {
}
}
class IdentityProvider implements IIdentityProvider<TreeElement> {
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<TreeElement> {
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<TreeModel, TreeElement> {
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<TreeElement> | Promise<Iterable<TreeElement>> {
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<ITreeItemFromProvider, void, ITreeExplorerTemplateData> {
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<ITreeItemFromProvider, void>, 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 = <TreeViewItemHandleArg>{ $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<ITreeItemFromProvider, void>, index: number, templateData: ITreeExplorerTemplateData): void {
templateData.elementDisposable.dispose();
}
disposeTemplate(templateData: ITreeExplorerTemplateData): void {
// templateData.resourceLabel.dispose();
// templateData.actionBar.dispose();
templateData.elementDisposable.dispose();
}
}

View File

@@ -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<void>();
public onResetConnection: Event<void> = 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<boolean>('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<boolean>('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<IView[]> = Event.None;
public readonly onDidRemoveViews: Event<IView[]> = Event.None;
public readonly onDidChangeViewVisibility: Event<IView> = 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<IPaneColors>(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<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({
id: 'dialog/connection',
name: 'ConnectionDialog',
ctorDescriptor: new SyncDescriptor(class { }),
order: 0,
storageId: `dialog/connection.state`
}, ViewContainerLocation.Dialog);

View File

@@ -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;
}

View File

@@ -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<ITreeViewDataProvider>;
readonly providers: Iterable<[string, ITreeViewDataProvider]>;
readonly descriptors: Iterable<IConnectionTreeDescriptor>;
readonly view: IView | undefined;
}
export const IConnectionTreeService = createDecorator<IConnectionTreeService>('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<ITreeViewDataProvider>();
public readonly onDidAddProvider = this._onDidAddProvider.event;
private readonly _onDidRemoveProvider = new Emitter<void>();
public readonly onDidRemoveProvider = this._onDidRemoveProvider.event;
private _providers = new Map<string, ITreeViewDataProvider>();
private _descriptors = new Set<IConnectionTreeDescriptor>();
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<IConnectionTreeDescriptor> {
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);

View File

@@ -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(() => {

View File

@@ -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<IViewsRegistry>(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

View File

@@ -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);

View File

@@ -197,9 +197,11 @@ export class TreeNodeRenderer implements ITreeRenderer<TreeNode, FuzzyScore, Tre
renderTemplate(container: HTMLElement): TreeNodeTemplate {
return this._instantiationService.createInstance(TreeNodeTemplate, container);
}
renderElement(node: ITreeNode<TreeNode, FuzzyScore>, index: number, template: TreeNodeTemplate): void {
template.set(node.element);
}
disposeTemplate(templateData: TreeNodeTemplate): void {
templateData.dispose();
}

View File

@@ -17,7 +17,7 @@ export enum TreeItemCollapsibleState {
}
export interface ObjectExplorerCallbacks {
getChildren(treeNode?: TreeNode): Thenable<TreeNode[]>;
getChildren(treeNode?: TreeNode): Promise<TreeNode[]>;
isExpanded(treeNode: TreeNode): Thenable<boolean>;
setNodeExpandedState(TreeNode: TreeNode, expandedState: TreeItemCollapsibleState): Thenable<void>;
setNodeSelected(TreeNode: TreeNode, selected: boolean, clearOtherSelections?: boolean): Thenable<void>;
@@ -160,7 +160,7 @@ export class TreeNode {
};
}
public getChildren(): Thenable<TreeNode[]> {
public getChildren(): Promise<TreeNode[]> {
return this._objectExplorerCallbacks?.getChildren(this) ?? Promise.resolve([]);
}