mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 01:25:37 -05:00
758 lines
30 KiB
TypeScript
758 lines
30 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { IConnectionProfile } from 'azdata';
|
|
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
|
import { IPanelTab, IPanelView } from 'sql/base/browser/ui/panel/panel';
|
|
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
|
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
|
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
|
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
|
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
|
|
import { ITreeItem } from 'sql/workbench/common/views';
|
|
import { CONNECTIONS_SORT_BY_CONFIG_KEY } from 'sql/platform/connection/common/connectionConfig';
|
|
import { IConnectionTreeDescriptor, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService';
|
|
import { AsyncRecentConnectionTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource';
|
|
import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree';
|
|
import { ConnectionProfileRenderer, TreeNodeRenderer } from 'sql/workbench/services/objectExplorer/browser/asyncServerTreeRenderer';
|
|
import { ServerTreeRenderer } from 'sql/workbench/services/objectExplorer/browser/serverTreeRenderer';
|
|
import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils';
|
|
import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode';
|
|
import * as DOM from 'vs/base/browser/dom';
|
|
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
|
import { status } from 'vs/base/browser/ui/aria/aria';
|
|
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
|
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
|
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
|
|
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
|
|
import { IAction, IActionViewItemProvider } from 'vs/base/common/actions';
|
|
import { debounce } from 'vs/base/common/decorators';
|
|
import { Emitter, Event } from 'vs/base/common/event';
|
|
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 'vs/css!./media/connectionBrowseTab';
|
|
import { localize } from 'vs/nls';
|
|
import { createAndFillInContextMenuActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
|
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
|
|
import { ICommandService } from 'vs/platform/commands/common/commands';
|
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
|
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
|
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 { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
|
|
import { ColorScheme } from 'vs/platform/theme/common/theme';
|
|
import { FileThemeIcon, FolderThemeIcon, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
|
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
|
import { ITreeItemLabel, ITreeViewDataProvider, TreeItemCollapsibleState, TreeViewItemHandleArg } from 'vs/workbench/common/views';
|
|
|
|
|
|
|
|
export type TreeElement = ConnectionDialogTreeProviderElement | ITreeItemFromProvider | SavedConnectionNode | ServerTreeElement;
|
|
|
|
export class ConnectionBrowseTab implements IPanelTab {
|
|
public readonly title = localize('connectionDialog.browser', "Browse (Preview)");
|
|
public readonly identifier = 'connectionBrowse';
|
|
public readonly view = this.instantiationService.createInstance(ConnectionBrowserView);
|
|
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }
|
|
}
|
|
|
|
export interface SelectedConnectionChangedEventArgs {
|
|
connectionProfile: IConnectionProfile,
|
|
connect: boolean
|
|
}
|
|
|
|
export class ConnectionBrowserView extends Disposable implements IPanelView {
|
|
private tree: WorkbenchAsyncDataTree<TreeModel, TreeElement> | undefined;
|
|
private filterInput: InputBox | undefined;
|
|
private treeContainer: HTMLElement | undefined;
|
|
private model: TreeModel | undefined;
|
|
private treeLabels: ResourceLabels | undefined;
|
|
private treeMenus: ConnectionBrowseTreeMenuProvider | undefined;
|
|
private treeDataSource: DataSource | undefined;
|
|
private filterProgressBar: ProgressBar | undefined;
|
|
|
|
public onDidChangeVisibility = Event.None;
|
|
|
|
private readonly _onSelectedConnectionChanged = this._register(new Emitter<SelectedConnectionChangedEventArgs>());
|
|
public readonly onSelectedConnectionChanged = this._onSelectedConnectionChanged.event;
|
|
|
|
constructor(
|
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
|
@IConnectionTreeService private readonly connectionTreeService: IConnectionTreeService,
|
|
@IContextViewService private readonly contextViewService: IContextViewService,
|
|
@IThemeService private readonly themeService: IThemeService,
|
|
@ICommandService private readonly commandService: ICommandService,
|
|
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
|
@IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService,
|
|
@ICapabilitiesService private readonly capabilitiesService: ICapabilitiesService,
|
|
@IConfigurationService private readonly configurationService: IConfigurationService,
|
|
) {
|
|
super();
|
|
this.connectionTreeService.setView(this);
|
|
}
|
|
|
|
render(container: HTMLElement): void {
|
|
this.renderFilterBox(container);
|
|
this.renderTree(container);
|
|
}
|
|
|
|
renderFilterBox(container: HTMLElement): void {
|
|
this.filterInput = new InputBox(container, this.contextViewService, {
|
|
placeholder: localize('connectionDialog.FilterPlaceHolder', "Type here to filter the list"),
|
|
ariaLabel: localize('connectionDialog.FilterInputTitle', "Filter connections")
|
|
});
|
|
this.filterProgressBar = new ProgressBar(this.filterInput.element);
|
|
this._register(this.filterProgressBar);
|
|
this._register(attachProgressBarStyler(this.filterProgressBar, this.themeService));
|
|
this.filterInput.element.style.margin = '5px';
|
|
this._register(this.filterInput);
|
|
this._register(attachInputBoxStyler(this.filterInput, this.themeService));
|
|
this._register(this.filterInput.onDidChange(async () => {
|
|
await this.applyFilter();
|
|
}));
|
|
}
|
|
|
|
@debounce(500)
|
|
async applyFilter(): Promise<void> {
|
|
const applyingFilterStatusMessage = this.filterInput.value ? localize('connectionDialog.ApplyingFilter', "Applying filter")
|
|
: localize('connectionDialog.RemovingFilter', "Removing filter");
|
|
const filterAppliedStatusMessage = this.filterInput.value ? localize('connectionDialog.FilterApplied', "Filter applied")
|
|
: localize('connectionDialog.FilterRemoved', "Filter removed");
|
|
status(applyingFilterStatusMessage);
|
|
this.filterProgressBar.infinite().show(100);
|
|
this.treeDataSource.setFilter(this.filterInput.value);
|
|
await this.refresh();
|
|
await this.expandAll();
|
|
this.filterProgressBar.hide();
|
|
status(filterAppliedStatusMessage);
|
|
}
|
|
|
|
async expandAll(): Promise<void> {
|
|
const expandedTreeItems: TreeElement[] = [];
|
|
let treeItemsToExpand: TreeElement[] = this.treeDataSource.expandableTreeNodes;
|
|
// expand the nodes one by one here to avoid the possible azure api traffic throttling.
|
|
while (treeItemsToExpand.length !== 0) {
|
|
for (const treeItem of treeItemsToExpand) {
|
|
await this.tree.expand(treeItem);
|
|
}
|
|
expandedTreeItems.push(...treeItemsToExpand);
|
|
treeItemsToExpand = this.treeDataSource.expandableTreeNodes.filter(el => expandedTreeItems.indexOf(el) === -1);
|
|
}
|
|
}
|
|
|
|
renderTree(container: HTMLElement): void {
|
|
this.treeContainer = container.appendChild(DOM.$('div'));
|
|
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
|
|
this.treeMenus = this.instantiationService.createInstance(ConnectionBrowseTreeMenuProvider);
|
|
const actionViewItemProvider = (action: IAction) => {
|
|
if (action instanceof MenuItemAction) {
|
|
return this.instantiationService.createInstance(MenuEntryActionViewItem, action);
|
|
}
|
|
return undefined;
|
|
};
|
|
const renderers: ITreeRenderer<TreeElement, any, any>[] = [
|
|
new ProviderElementRenderer(this.treeMenus, actionViewItemProvider),
|
|
this.instantiationService.createInstance(TreeItemRenderer, this.treeMenus, this.treeLabels, actionViewItemProvider),
|
|
this.instantiationService.createInstance(ConnectionProfileRenderer, true),
|
|
this.instantiationService.createInstance(ConnectionProfileGroupRenderer),
|
|
this.instantiationService.createInstance(TreeNodeRenderer),
|
|
new SavedConnectionsNodeRenderer()
|
|
];
|
|
|
|
this.model = this.instantiationService.createInstance(TreeModel);
|
|
this.treeDataSource = new DataSource();
|
|
this.tree = this._register(this.instantiationService.createInstance(
|
|
WorkbenchAsyncDataTree,
|
|
'Browser Connections',
|
|
this.treeContainer,
|
|
new ListDelegate(),
|
|
renderers,
|
|
this.treeDataSource,
|
|
{
|
|
identityProvider: new IdentityProvider(),
|
|
horizontalScrolling: false,
|
|
setRowLineHeight: false,
|
|
transformOptimization: false,
|
|
accessibilityProvider: new ListAccessibilityProvider()
|
|
}) as WorkbenchAsyncDataTree<TreeModel, TreeElement>);
|
|
this._register(this.tree.onContextMenu(e => {
|
|
const actionContext = this.treeMenus.getActionContext(e.element);
|
|
if (actionContext) {
|
|
const actions = this.treeMenus.getActions(e.element);
|
|
this.contextMenuService.showContextMenu({
|
|
getAnchor: () => e.anchor,
|
|
getActions: () => actions,
|
|
getActionsContext: () => actionContext
|
|
});
|
|
}
|
|
}));
|
|
this._register(this.tree.onMouseDblClick(e => {
|
|
this.handleTreeElementSelection(e?.element, true);
|
|
}));
|
|
|
|
this._register(this.tree.onDidOpen((e) => {
|
|
this.handleTreeElementSelection(e?.element, false);
|
|
}));
|
|
|
|
this.tree.setInput(this.model);
|
|
|
|
this._register(this.connectionTreeService.onDidAddProvider(() => this.tree.updateChildren(this.model)));
|
|
|
|
// this event will be fired when connections/connection groups are created/edited
|
|
this._register(this.connectionManagementService.onAddConnectionProfile(() => {
|
|
this.updateSavedConnectionsNode();
|
|
}));
|
|
|
|
// this event will be fired when connections/connection groups are deleted
|
|
this._register(this.connectionManagementService.onDeleteConnectionProfile(() => {
|
|
this.updateSavedConnectionsNode();
|
|
}));
|
|
|
|
// this event will be fired when connection provider is registered
|
|
// when a connection's provider is not registered (e.g. the extensions are not fully loaded or the provider extension has been uninstalled)
|
|
// it will be displayed as 'loading...', this event will be fired when a connection's provider becomes available.
|
|
this._register(this.capabilitiesService.onCapabilitiesRegistered(() => {
|
|
this.updateSavedConnectionsNode();
|
|
}));
|
|
|
|
this._register(this.themeService.onDidColorThemeChange(async () => {
|
|
await this.refresh();
|
|
}));
|
|
|
|
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
|
if (e.affectsConfiguration(CONNECTIONS_SORT_BY_CONFIG_KEY)) {
|
|
this.updateSavedConnectionsNode();
|
|
}
|
|
}));
|
|
|
|
}
|
|
|
|
private handleTreeElementSelection(selectedNode: TreeElement, connect: boolean): void {
|
|
if (!selectedNode) {
|
|
return;
|
|
}
|
|
if ('element' in selectedNode) {
|
|
if (selectedNode.element.command) {
|
|
this.commandService.executeCommand(selectedNode.element.command.id, ...(selectedNode.element.command.arguments || []));
|
|
} else {
|
|
if (selectedNode.element.payload) {
|
|
this._onSelectedConnectionChanged.fire(
|
|
{
|
|
connectionProfile: selectedNode.element.payload,
|
|
connect: connect
|
|
});
|
|
}
|
|
}
|
|
} else if (selectedNode instanceof ConnectionProfile) {
|
|
this._onSelectedConnectionChanged.fire({
|
|
connectionProfile: selectedNode,
|
|
connect: connect
|
|
});
|
|
}
|
|
}
|
|
|
|
private updateSavedConnectionsNode(): void {
|
|
if (this.model.savedConnectionNode) {
|
|
this.tree.updateChildren(this.model.savedConnectionNode);
|
|
}
|
|
}
|
|
|
|
async refresh(items?: ITreeItem[]): Promise<void> {
|
|
if (this.tree) {
|
|
return this.tree.updateChildren();
|
|
}
|
|
}
|
|
|
|
layout(dimension: DOM.Dimension): void {
|
|
const treeHeight = dimension.height - DOM.getTotalHeight(this.filterInput.element);
|
|
this.treeContainer.style.width = `${dimension.width}px`;
|
|
this.treeContainer.style.height = `${treeHeight}px`;
|
|
this.tree.layout(treeHeight, dimension.width);
|
|
}
|
|
|
|
focus(): void {
|
|
this.tree.domFocus();
|
|
}
|
|
}
|
|
|
|
export interface ITreeItemFromProvider {
|
|
readonly element: ITreeItem;
|
|
readonly treeId?: string;
|
|
getChildren?(): Promise<ITreeItemFromProvider[]>;
|
|
}
|
|
|
|
export function instanceOfITreeItemFromProvider(obj: any): obj is ITreeItemFromProvider {
|
|
return !!(<ITreeItemFromProvider>obj)?.element;
|
|
}
|
|
|
|
class ConnectionDialogTreeProviderElement {
|
|
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,
|
|
treeId: this.descriptor.id,
|
|
getChildren: () => this.getChildren(v)
|
|
}));
|
|
}
|
|
}
|
|
|
|
class ListDelegate implements IListVirtualDelegate<TreeElement> {
|
|
getHeight(): number {
|
|
return 22;
|
|
}
|
|
|
|
getTemplateId(element: TreeElement): string {
|
|
if (element instanceof ConnectionDialogTreeProviderElement) {
|
|
return ProviderElementRenderer.TEMPLATE_ID;
|
|
} else if (element instanceof ConnectionProfile) {
|
|
return ServerTreeRenderer.CONNECTION_TEMPLATE_ID;
|
|
} else if (element instanceof ConnectionProfileGroup) {
|
|
return ConnectionProfileGroupRenderer.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 TreeElementTemplate {
|
|
readonly icon: HTMLElement;
|
|
readonly name: HTMLElement;
|
|
readonly actionBar?: ActionBar;
|
|
}
|
|
|
|
abstract class BaseTreeItemRender<T> implements ITreeRenderer<T, void, TreeElementTemplate> {
|
|
public readonly abstract templateId: string;
|
|
protected abstract getText(element: T): string;
|
|
protected abstract getIconClass(element: T): string;
|
|
|
|
constructor(private menus?: ConnectionBrowseTreeMenuProvider,
|
|
private actionViewItemProvider?: IActionViewItemProvider
|
|
) {
|
|
}
|
|
|
|
renderTemplate(container: HTMLElement): TreeElementTemplate {
|
|
const root = DOM.append(container, DOM.$('div.browse-tree-element'));
|
|
const icon = DOM.append(root, DOM.$('.icon'));
|
|
const name = DOM.append(root, DOM.$('.name'));
|
|
let actionBar: ActionBar | undefined;
|
|
if (this.menus) {
|
|
const actionsContainer = DOM.append(root, DOM.$('.actions'));
|
|
actionBar = new ActionBar(actionsContainer, {
|
|
actionViewItemProvider: this.actionViewItemProvider
|
|
});
|
|
}
|
|
return { name, icon, actionBar };
|
|
}
|
|
|
|
renderElement(element: ITreeNode<T, void>, index: number, templateData: TreeElementTemplate, height: number): void {
|
|
templateData.name.innerText = this.getText(element.element);
|
|
templateData.icon.classList.add('codicon', this.getIconClass(element.element));
|
|
if (this.menus) {
|
|
templateData.actionBar.clear();
|
|
templateData.actionBar.context = this.menus.getActionContext(element.element);
|
|
templateData.actionBar.push(this.menus.getActions(element.element), { icon: true, label: false });
|
|
}
|
|
}
|
|
|
|
disposeTemplate(templateData: TreeElementTemplate): void {
|
|
templateData.actionBar?.dispose();
|
|
}
|
|
}
|
|
|
|
class ProviderElementRenderer extends BaseTreeItemRender<ConnectionDialogTreeProviderElement> {
|
|
public static readonly TEMPLATE_ID = 'ProviderElementTemplate';
|
|
public readonly templateId = ProviderElementRenderer.TEMPLATE_ID;
|
|
|
|
constructor(menus: ConnectionBrowseTreeMenuProvider, actionViewItemProvider: IActionViewItemProvider) {
|
|
super(menus, actionViewItemProvider);
|
|
}
|
|
|
|
getText(element: ConnectionDialogTreeProviderElement): string {
|
|
return element.name;
|
|
}
|
|
|
|
getIconClass(element: ConnectionDialogTreeProviderElement): string {
|
|
return 'codicon-folder';
|
|
}
|
|
}
|
|
|
|
class SavedConnectionsNodeRenderer extends BaseTreeItemRender<SavedConnectionNode> {
|
|
public static readonly TEMPLATE_ID = 'savedConnectionNode';
|
|
public readonly templateId = SavedConnectionsNodeRenderer.TEMPLATE_ID;
|
|
|
|
getText(element: SavedConnectionNode): string {
|
|
return localize('savedConnections', "Saved Connections");
|
|
}
|
|
|
|
getIconClass(element: SavedConnectionNode): string {
|
|
return 'codicon-folder';
|
|
}
|
|
}
|
|
|
|
class ConnectionProfileGroupRenderer extends BaseTreeItemRender<ConnectionProfileGroup>{
|
|
public static readonly TEMPLATE_ID = 'connectionProfileGroup';
|
|
public readonly templateId = ConnectionProfileGroupRenderer.TEMPLATE_ID;
|
|
|
|
getText(element: ConnectionProfileGroup): string {
|
|
return element.name;
|
|
}
|
|
|
|
getIconClass(element: ConnectionProfileGroup): string {
|
|
return 'codicon-folder';
|
|
}
|
|
}
|
|
|
|
class IdentityProvider implements IIdentityProvider<TreeElement> {
|
|
getId(element: TreeElement): string {
|
|
if (element instanceof ConnectionDialogTreeProviderElement) {
|
|
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 {
|
|
private _savedConnectionNode: SavedConnectionNode | undefined;
|
|
|
|
constructor(
|
|
@IConnectionTreeService private readonly connectionTreeService: IConnectionTreeService,
|
|
@IInstantiationService private readonly instantiationService: IInstantiationService
|
|
) { }
|
|
|
|
getChildren(): TreeElement[] {
|
|
this._savedConnectionNode = this.instantiationService.createInstance(SavedConnectionNode);
|
|
const descriptors = Array.from(this.connectionTreeService.descriptors);
|
|
return [this._savedConnectionNode, ...Iterable.map(this.connectionTreeService.providers, ([id, provider]) => new ConnectionDialogTreeProviderElement(provider, descriptors.find(i => i.id === id)))];
|
|
}
|
|
|
|
public get savedConnectionNode(): SavedConnectionNode | undefined {
|
|
return this._savedConnectionNode;
|
|
}
|
|
}
|
|
|
|
class ListAccessibilityProvider implements IListAccessibilityProvider<TreeElement> {
|
|
getAriaLabel(element: TreeElement): string {
|
|
if (element instanceof ConnectionDialogTreeProviderElement) {
|
|
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.label.label;
|
|
}
|
|
}
|
|
|
|
getWidgetAriaLabel(): string {
|
|
return localize('connectionBrowserTree', "Connection Browser Tree");
|
|
}
|
|
}
|
|
|
|
class DataSource implements IAsyncDataSource<TreeModel, TreeElement> {
|
|
private _filter: string | undefined;
|
|
private _filterRegex: RegExp | undefined;
|
|
public setFilter(filter: string): void {
|
|
this._filter = filter;
|
|
this._filterRegex = new RegExp(filter, 'i');
|
|
}
|
|
|
|
hasChildren(element: TreeModel | TreeElement): boolean {
|
|
if (element instanceof TreeModel) {
|
|
return true;
|
|
} else if (element instanceof ConnectionDialogTreeProviderElement) {
|
|
return true;
|
|
} else if (element instanceof ConnectionProfile) {
|
|
return false;
|
|
} else if (element instanceof ConnectionProfileGroup) {
|
|
return true;
|
|
} else if (element instanceof TreeNode) {
|
|
return element.children.length > 0;
|
|
} else if (element instanceof SavedConnectionNode) {
|
|
return true;
|
|
} else {
|
|
return element.element.collapsibleState !== TreeItemCollapsibleState.None;
|
|
}
|
|
}
|
|
|
|
public treeNodes: TreeElement[] = [];
|
|
|
|
public get expandableTreeNodes(): TreeElement[] {
|
|
return this.treeNodes.filter(node => {
|
|
return (node instanceof TreeModel)
|
|
|| (node instanceof ConnectionDialogTreeProviderElement)
|
|
|| (node instanceof SavedConnectionNode)
|
|
|| (node instanceof ConnectionProfileGroup)
|
|
|| (instanceOfITreeItemFromProvider(node) && node.element.collapsibleState !== TreeItemCollapsibleState.None);
|
|
});
|
|
}
|
|
|
|
async getChildren(element: TreeModel | TreeElement): Promise<Iterable<TreeElement>> {
|
|
if (element instanceof TreeModel) {
|
|
this.treeNodes = [];
|
|
}
|
|
if (!(element instanceof ConnectionProfile)) {
|
|
let children = await element.getChildren();
|
|
if (this._filter) {
|
|
if ((element instanceof SavedConnectionNode) || (element instanceof ConnectionProfileGroup)) {
|
|
children = (children as (ConnectionProfile | ConnectionProfileGroup)[]).filter(item => {
|
|
return (item instanceof ConnectionProfileGroup) || this._filterRegex.test(item.title);
|
|
});
|
|
} else if (instanceOfITreeItemFromProvider(element)) {
|
|
children = (children as ITreeItemFromProvider[]).filter(item => {
|
|
return item.element.collapsibleState !== TreeItemCollapsibleState.None || this._filterRegex.test(item.element.label.label);
|
|
});
|
|
}
|
|
}
|
|
this.treeNodes.push(...children);
|
|
return children;
|
|
}
|
|
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 ConnectionBrowseTreeMenuProvider {
|
|
constructor(
|
|
@IMenuService private readonly menuService: IMenuService,
|
|
@IContextKeyService private readonly contextKeyService: IContextKeyService
|
|
) {
|
|
}
|
|
|
|
public getActionContext(element: any): TreeViewItemHandleArg | ConnectionDialogTreeProviderElement | undefined {
|
|
let actionContext: TreeViewItemHandleArg | ConnectionDialogTreeProviderElement | undefined;
|
|
if (instanceOfITreeItemFromProvider(element)) {
|
|
actionContext = <TreeViewItemHandleArg>{ $treeViewId: element.treeId, $treeItemHandle: element.element.handle, $treeItem: element.element };
|
|
} else if (element instanceof ConnectionDialogTreeProviderElement) {
|
|
actionContext = element;
|
|
}
|
|
return actionContext;
|
|
}
|
|
|
|
public getActions(element: any): IAction[] {
|
|
if (!this.contextKeyService) {
|
|
return [];
|
|
}
|
|
|
|
let context: ITreeItem | ConnectionDialogTreeProviderElement | undefined;
|
|
if (instanceOfITreeItemFromProvider(element)) {
|
|
context = element.element;
|
|
} else if (element instanceof ConnectionDialogTreeProviderElement) {
|
|
context = element;
|
|
}
|
|
|
|
if (!context) {
|
|
return [];
|
|
}
|
|
|
|
const contextKeyService = this.contextKeyService.createScoped();
|
|
const contextKey = new ContextKey(contextKeyService);
|
|
contextKey.set(context);
|
|
const menu = this.menuService.createMenu(MenuId.ConnectionDialogBrowseTreeContext, contextKeyService);
|
|
const primary: IAction[] = [];
|
|
const secondary: IAction[] = [];
|
|
const result = { primary, secondary };
|
|
createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result);
|
|
|
|
menu.dispose();
|
|
contextKeyService.dispose();
|
|
|
|
return result.primary;
|
|
}
|
|
}
|
|
|
|
class TreeItemRenderer extends Disposable implements ITreeRenderer<ITreeItemFromProvider, void, ITreeExplorerTemplateData> {
|
|
static readonly ITEM_HEIGHT = 22;
|
|
static readonly TREE_TEMPLATE_ID = 'treeExplorer';
|
|
|
|
constructor(
|
|
private menus: ConnectionBrowseTreeMenuProvider,
|
|
private labels: ResourceLabels,
|
|
private actionViewItemProvider: IActionViewItemProvider,
|
|
@IThemeService private readonly themeService: IThemeService,
|
|
@IConfigurationService private readonly configurationService: IConfigurationService,
|
|
@ILabelService private readonly labelService: ILabelService
|
|
) {
|
|
super();
|
|
}
|
|
|
|
get templateId(): string {
|
|
return TreeItemRenderer.TREE_TEMPLATE_ID;
|
|
}
|
|
|
|
renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {
|
|
container.classList.add('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, actionBar };
|
|
}
|
|
|
|
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 = this.menus.getActionContext(element.element);
|
|
templateData.actionBar.push(this.menus.getActions(element.element), { icon: true, label: false });
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
type ContextValueType = ITreeItem | ConnectionDialogTreeProviderElement;
|
|
|
|
class ContextKey extends Disposable implements IContextKey<ContextValueType> {
|
|
static readonly ContextValue = new RawContextKey<string | undefined>('contextValue', undefined);
|
|
static readonly TreeId = new RawContextKey<string | undefined>('treeId', undefined);
|
|
private _contextValueKey: IContextKey<string | undefined>;
|
|
private _treeIdKey: IContextKey<string | undefined>;
|
|
private _item: ContextValueType;
|
|
|
|
constructor(
|
|
@IContextKeyService contextKeyService: IContextKeyService
|
|
) {
|
|
super();
|
|
this._contextValueKey = ContextKey.ContextValue.bindTo(contextKeyService);
|
|
this._treeIdKey = ContextKey.TreeId.bindTo(contextKeyService);
|
|
}
|
|
set(value: ContextValueType): void {
|
|
this.reset();
|
|
this._item = value;
|
|
if (value instanceof ConnectionDialogTreeProviderElement) {
|
|
this._treeIdKey.set(value.id);
|
|
} else {
|
|
this._contextValueKey.set(value.contextValue);
|
|
}
|
|
}
|
|
reset(): void {
|
|
this._contextValueKey.reset();
|
|
this._treeIdKey.reset();
|
|
}
|
|
get(): ContextValueType {
|
|
return this._item;
|
|
}
|
|
}
|