move code from parts to contrib (#8319)

This commit is contained in:
Anthony Dresser
2019-11-14 12:23:11 -08:00
committed by GitHub
parent 6438967202
commit 7a2c30e159
619 changed files with 848 additions and 848 deletions

View File

@@ -0,0 +1,331 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ServerTreeView } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeView';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import Severity from 'vs/base/common/severity';
import { ObjectExplorerActionsContext } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { UNSAVED_GROUP_ID } from 'sql/platform/connection/common/constants';
import { IServerGroupController } from 'sql/platform/serverGroup/common/serverGroupController';
import { ILogService } from 'vs/platform/log/common/log';
export class RefreshAction extends Action {
public static ID = 'objectExplorer.refresh';
public static LABEL = localize('connectionTree.refresh', "Refresh");
private _tree: ITree;
constructor(
id: string,
label: string,
tree: ITree,
private element: IConnectionProfile | TreeNode,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@ILogService private _logService: ILogService
) {
super(id, label);
this._tree = tree;
}
public async run(): Promise<boolean> {
let treeNode: TreeNode;
if (this.element instanceof ConnectionProfile) {
let connection: ConnectionProfile = this.element;
if (this._connectionManagementService.isConnected(undefined, connection)) {
treeNode = this._objectExplorerService.getObjectExplorerNode(connection);
if (treeNode === undefined) {
await this._objectExplorerService.updateObjectExplorerNodes(connection.toIConnectionProfile());
treeNode = this._objectExplorerService.getObjectExplorerNode(connection);
}
}
} else if (this.element instanceof TreeNode) {
treeNode = this.element;
}
if (treeNode) {
try {
await this._tree.collapse(this.element);
try {
await this._objectExplorerService.refreshTreeNode(treeNode.getSession(), treeNode);
} catch (error) {
this.showError(error);
return true;
}
await this._tree.refresh(this.element);
return this._tree.expand(this.element);
} catch (ex) {
this._logService.error(ex);
return true;
}
}
return true;
}
private showError(errorMessage: string) {
this._logService.error(errorMessage);
if (this._errorMessageService) {
this._errorMessageService.showDialog(Severity.Error, '', errorMessage);
}
}
}
export class DisconnectConnectionAction extends Action {
public static ID = 'objectExplorer.disconnect';
public static LABEL = localize('DisconnectAction', "Disconnect");
constructor(
id: string,
label: string,
private _connectionProfile: ConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) {
super(id, label);
}
async run(actionContext: ObjectExplorerActionsContext): Promise<any> {
if (!this._connectionProfile) {
return true;
}
if (this._connectionManagementService.isProfileConnected(this._connectionProfile)) {
let profileImpl = this._connectionProfile as ConnectionProfile;
if (profileImpl) {
profileImpl.isDisconnecting = true;
}
await this._connectionManagementService.disconnect(this._connectionProfile);
if (profileImpl) {
profileImpl.isDisconnecting = false;
}
return true;
} else {
return true;
}
}
}
/**
* Actions to add a server to the group
*/
export class AddServerAction extends Action {
public static ID = 'registeredServers.addConnection';
public static LABEL = localize('connectionTree.addConnection', "New Connection");
constructor(
id: string,
label: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) {
super(id, label);
this.class = 'add-server-action';
}
public async run(element: ConnectionProfileGroup): Promise<boolean> {
let connection: IConnectionProfile = element === undefined ? undefined : {
connectionName: undefined,
serverName: undefined,
databaseName: undefined,
userName: undefined,
password: undefined,
authenticationType: undefined,
groupId: undefined,
groupFullName: element.fullName,
savePassword: undefined,
getOptionsKey: undefined,
matches: undefined,
providerName: '',
options: {},
saveProfile: true,
id: element.id
};
await this._connectionManagementService.showConnectionDialog(undefined, undefined, connection);
return true;
}
}
/**
* Actions to add a server to the group
*/
export class AddServerGroupAction extends Action {
public static ID = 'registeredServers.addServerGroup';
public static LABEL = localize('connectionTree.addServerGroup', "New Server Group");
constructor(
id: string,
label: string,
@IServerGroupController private readonly serverGroupController: IServerGroupController
) {
super(id, label);
this.class = 'add-server-group-action';
}
public async run(): Promise<boolean> {
await this.serverGroupController.showCreateGroupDialog();
return true;
}
}
/**
* Actions to edit a server group
*/
export class EditServerGroupAction extends Action {
public static ID = 'registeredServers.editServerGroup';
public static LABEL = localize('connectionTree.editServerGroup', "Edit Server Group");
constructor(
id: string,
label: string,
private _group: ConnectionProfileGroup,
@IServerGroupController private readonly serverGroupController: IServerGroupController
) {
super(id, label);
this.class = 'edit-server-group-action';
}
public run(): Promise<boolean> {
this.serverGroupController.showEditGroupDialog(this._group);
return Promise.resolve(true);
}
}
/**
* Display active connections in the tree
*/
export class ActiveConnectionsFilterAction extends Action {
public static ID = 'registeredServers.recentConnections';
public static LABEL = localize('activeConnections', "Show Active Connections");
private static enabledClass = 'active-connections-action';
private static disabledClass = 'icon server-page';
private static showAllConnectionsLabel = localize('showAllConnections', "Show All Connections");
private _isSet: boolean;
public static readonly ACTIVE = 'active';
public get isSet(): boolean {
return this._isSet;
}
public set isSet(value: boolean) {
this._isSet = value;
this.class = (!this._isSet) ?
ActiveConnectionsFilterAction.enabledClass : ActiveConnectionsFilterAction.disabledClass;
}
constructor(
id: string,
label: string,
private view: ServerTreeView
) {
super(id, label);
this.class = ActiveConnectionsFilterAction.enabledClass;
}
public run(): Promise<boolean> {
if (!this.view) {
// return without doing anything
return Promise.resolve(true);
}
if (this.class === ActiveConnectionsFilterAction.enabledClass) {
// show active connections in the tree
this.view.showFilteredTree(ActiveConnectionsFilterAction.ACTIVE);
this.isSet = true;
this.label = ActiveConnectionsFilterAction.showAllConnectionsLabel;
} else {
// show full tree
this.view.refreshTree();
this.isSet = false;
this.label = ActiveConnectionsFilterAction.LABEL;
}
return Promise.resolve(true);
}
}
/**
* Display recent connections in the tree
*/
export class RecentConnectionsFilterAction extends Action {
public static ID = 'registeredServers.recentConnections';
public static LABEL = localize('recentConnections', "Recent Connections");
private static enabledClass = 'recent-connections-action';
private static disabledClass = 'recent-connections-action-set';
private _isSet: boolean;
public get isSet(): boolean {
return this._isSet;
}
public set isSet(value: boolean) {
this._isSet = value;
this.class = (!this._isSet) ?
RecentConnectionsFilterAction.enabledClass : RecentConnectionsFilterAction.disabledClass;
}
constructor(
id: string,
label: string,
private view: ServerTreeView
) {
super(id, label);
this.class = RecentConnectionsFilterAction.enabledClass;
this._isSet = false;
}
public run(): Promise<boolean> {
if (!this.view) {
// return without doing anything
return Promise.resolve(true);
}
if (this.class === RecentConnectionsFilterAction.enabledClass) {
// show recent connections in the tree
this.view.showFilteredTree('recent');
this.isSet = true;
} else {
// show full tree
this.view.refreshTree();
this.isSet = false;
}
return Promise.resolve(true);
}
}
/**
* Actions to delete a server/group
*/
export class DeleteConnectionAction extends Action {
public static ID = 'registeredServers.deleteConnection';
public static DELETE_CONNECTION_LABEL = localize('deleteConnection', "Delete Connection");
public static DELETE_CONNECTION_GROUP_LABEL = localize('deleteConnectionGroup', "Delete Group");
constructor(
id: string,
label: string,
private element: IConnectionProfile | ConnectionProfileGroup,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) {
super(id, label);
this.class = 'delete-connection-action';
if (element instanceof ConnectionProfileGroup && element.id === UNSAVED_GROUP_ID) {
this.enabled = false;
}
if (element instanceof ConnectionProfile) {
let parent: ConnectionProfileGroup = element.parent;
if (parent && parent.id === UNSAVED_GROUP_ID) {
this.enabled = false;
}
}
}
public run(): Promise<boolean> {
if (this.element instanceof ConnectionProfile) {
this._connectionManagementService.deleteConnection(this.element);
} else if (this.element instanceof ConnectionProfileGroup) {
this._connectionManagementService.deleteConnectionGroup(this.element);
}
return Promise.resolve(true);
}
}

View File

@@ -0,0 +1,204 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ITree, IDragAndDrop, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree';
import { DragMouseEvent } from 'vs/base/browser/mouseEvent';
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
import { UNSAVED_GROUP_ID } from 'sql/platform/connection/common/constants';
import { IDragAndDropData } from 'vs/base/browser/dnd';
/**
* Implements drag and drop for the server tree
*/
export class ServerTreeDragAndDrop implements IDragAndDrop {
constructor(
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
) {
}
/**
* Returns a uri if the given element should be allowed to drag.
* Returns null, otherwise.
*/
public getDragURI(tree: ITree, element: any): string {
if (element instanceof ConnectionProfile) {
return (<ConnectionProfile>element).id;
}
else if (element instanceof ConnectionProfileGroup) {
return (<ConnectionProfileGroup>element).id;
}
return null;
}
/**
* Returns a label(name) to display when dragging the element.
*/
public getDragLabel(tree: ITree, elements: any[]): string {
if (elements[0] instanceof ConnectionProfile) {
return (<ConnectionProfile>elements[0]).serverName;
} else if (elements[0] instanceof ConnectionProfileGroup) {
return (<ConnectionProfileGroup>elements[0]).name;
} else {
return undefined;
}
}
/**
* Called when the drag operation starts.
*/
public onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: DragMouseEvent): void {
TreeUpdateUtils.isInDragAndDrop = true;
return;
}
/**
* Returns a DragOverReaction indicating whether sources can be
* dropped into target or some parent of the target.
* Returns DRAG_OVER_ACCEPT_BUBBLE_DOWN when element is a connection group or connection
*/
public onDragOver(tree: ITree, data: IDragAndDropData, targetElement: any, originalEvent: DragMouseEvent): IDragOverReaction {
let canDragOver: boolean = true;
if (targetElement instanceof ConnectionProfile || targetElement instanceof ConnectionProfileGroup) {
let targetConnectionProfileGroup = this.getTargetGroup(targetElement);
// Verify if the connection can be moved to the target group
const source = data.getData()[0];
if (source instanceof ConnectionProfile) {
if (!this._connectionManagementService.canChangeConnectionConfig(source, targetConnectionProfileGroup.id)) {
canDragOver = false;
}
} else if (source instanceof ConnectionProfileGroup) {
// Dropping a group to itself or its descendants nodes is not allowed
// to avoid creating a circular structure.
canDragOver = source.id !== targetElement.id && !source.isAncestorOf(targetElement);
}
} else {
canDragOver = false;
}
if (canDragOver) {
return DRAG_OVER_ACCEPT_BUBBLE_DOWN(true);
} else {
return DRAG_OVER_REJECT;
}
}
/**
* Handle a drop in the server tree.
*/
public drop(tree: ITree, data: IDragAndDropData, targetElement: any, originalEvent: DragMouseEvent): void {
TreeUpdateUtils.isInDragAndDrop = false;
let targetConnectionProfileGroup: ConnectionProfileGroup = this.getTargetGroup(targetElement);
const source = data.getData()[0];
if (source && source.getParent) {
let oldParent: ConnectionProfileGroup = source.getParent();
const self = this;
if (this.isDropAllowed(targetConnectionProfileGroup, oldParent, source)) {
if (source instanceof ConnectionProfile) {
// Change group id of profile
this._connectionManagementService.changeGroupIdForConnection(source, targetConnectionProfileGroup.id).then(() => {
TreeUpdateUtils.registeredServerUpdate(tree, self._connectionManagementService, targetConnectionProfileGroup);
});
} else if (source instanceof ConnectionProfileGroup) {
// Change parent id of group
this._connectionManagementService.changeGroupIdForConnectionGroup(source, targetConnectionProfileGroup).then(() => {
TreeUpdateUtils.registeredServerUpdate(tree, self._connectionManagementService);
});
}
}
}
}
public dropAbort(tree: ITree, data: IDragAndDropData): void {
TreeUpdateUtils.isInDragAndDrop = false;
}
private getTargetGroup(targetElement: any): ConnectionProfileGroup {
let targetConnectionProfileGroup: ConnectionProfileGroup;
if (targetElement instanceof ConnectionProfile) {
targetConnectionProfileGroup = (<ConnectionProfile>targetElement).getParent();
}
else {
targetConnectionProfileGroup = <ConnectionProfileGroup>targetElement;
}
return targetConnectionProfileGroup;
}
private isDropAllowed(targetConnectionProfileGroup: ConnectionProfileGroup,
oldParent: ConnectionProfileGroup,
source: ConnectionProfile | ConnectionProfileGroup): boolean {
let isDropToItself = source && targetConnectionProfileGroup && (source instanceof ConnectionProfileGroup) && source.name === targetConnectionProfileGroup.name;
let isDropToSameLevel = oldParent && oldParent.equals(targetConnectionProfileGroup);
let isUnsavedDrag = source && (source instanceof ConnectionProfileGroup) && (source.id === UNSAVED_GROUP_ID);
return (!isDropToSameLevel && !isDropToItself && !isUnsavedDrag);
}
}
/**
* Implements drag and drop for the connection tree
*/
export class RecentConnectionsDragAndDrop implements IDragAndDrop {
/**
* Returns a uri if the given element should be allowed to drag.
* Returns null, otherwise.
*/
public getDragURI(tree: ITree, element: any): string {
if (element instanceof ConnectionProfile) {
return (<ConnectionProfile>element).id;
}
else if (element instanceof ConnectionProfileGroup) {
return (<ConnectionProfileGroup>element).id;
}
return null;
}
/**
* Returns a label(name) to display when dragging the element.
*/
public getDragLabel(tree: ITree, elements: any[]): string {
if (elements[0] instanceof ConnectionProfile) {
return (<ConnectionProfile>elements[0]).serverName;
}
else if (elements[0] instanceof ConnectionProfileGroup) {
return (<ConnectionProfileGroup>elements[0]).name;
}
return undefined;
}
/**
* Sent when the drag operation is starting.
*/
public onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: DragMouseEvent): void {
return;
}
/**
* Returns a DragOverReaction indicating whether sources can be
* dropped into target or some parent of the target.
*/
public onDragOver(tree: ITree, data: IDragAndDropData, targetElement: any, originalEvent: DragMouseEvent): IDragOverReaction {
return DRAG_OVER_REJECT;
}
/**
* Handle drop in the server tree.
*/
public drop(tree: ITree, data: IDragAndDropData, targetElement: any, originalEvent: DragMouseEvent): void {
// No op
}
public dropAbort(tree: ITree, data: IDragAndDropData): void { }
}

View File

@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
import { hash } from 'vs/base/common/hash';
import { URI } from 'vs/base/common/uri';
class IconRenderer {
private iconRegistered: Set<string> = new Set<string>();
public registerIcon(path: URI | IconPath): string {
if (!path) { return undefined; }
let iconPath: IconPath = this.toIconPath(path);
let iconUid: string = this.getIconUid(iconPath);
if (!this.iconRegistered.has(iconUid)) {
createCSSRule(`.icon#${iconUid}`, `background: ${asCSSUrl(iconPath.light || iconPath.dark)} center center no-repeat`);
createCSSRule(`.vs-dark .icon#${iconUid}, .hc-black .icon#${iconUid}`, `background: ${asCSSUrl(iconPath.dark)} center center no-repeat`);
this.iconRegistered.add(iconUid);
}
return iconUid;
}
public getIconUid(path: URI | IconPath): string {
if (!path) { return undefined; }
let iconPath: IconPath = this.toIconPath(path);
return `icon${hash(iconPath.light.toString() + iconPath.dark.toString())}`;
}
private toIconPath(path: URI | IconPath): IconPath {
if (path['light']) {
return path as IconPath;
} else {
let singlePath = path as URI;
return { light: singlePath, dark: singlePath };
}
}
public putIcon(element: HTMLElement, path: URI | IconPath): void {
if (!element || !path) { return undefined; }
let iconUid: string = this.registerIcon(path);
element.id = iconUid;
}
public removeIcon(element: HTMLElement): void {
if (!element) { return undefined; }
element.id = undefined;
}
}
export const iconRenderer: IconRenderer = new IconRenderer();
class BadgeRenderer {
public readonly serverConnected: string = 'serverConnected';
public readonly serverDisconnected: string = 'serverDisconnected';
public readonly newTag: string = 'newTag';
private badgeCreated: Set<string> = new Set<string>();
constructor() {
this.createBadge(this.serverConnected, this.getConnectionStatusBadge(true));
this.createBadge(this.serverDisconnected, this.getConnectionStatusBadge(false));
this.createBadge(this.newTag, this.getNewTagBadge());
}
private getConnectionStatusBadge(isConnected: boolean) {
let circleColor: string = isConnected ? 'rgba(59, 180, 74, 100%)' : 'rgba(208, 46, 0, 100%)';
let bgColor: string = isConnected ? 'rgba(59, 180, 74, 100%)' : 'rgba(255, 255, 255, 80%)';
return `position: absolute;
height: 0.25rem;
width: 0.25rem;
top: 14px;
left: 19px;
border: 0.12rem solid ${circleColor};
border-radius: 100%;
background: ${bgColor};
content:"";
font-size: 100%;
line-height: 100%;
color:white;
text-align:center;
vertical-align:middle;`
.replace(/\t/g, ' ').replace(/\r?\n/g, ' ').replace(/ +/g, ' ');
}
private getNewTagBadge(): string {
return `position: absolute;
height: 0.4rem;
width: 0.4rem;
top: 3px;
left: 5px;
border: 1px solid green;
border-radius: 15%;
background: green;
content:"N";
font-size: 0.3rem;
font-weight: bold;
line-height: 0.4rem;
color: white;
text-align:center;
vertical-align:middle;`
.replace(/\t/g, ' ').replace(/\r?\n/g, ' ').replace(/ +/g, ' ');
}
private createBadge(badgeClass: string, badge: string): void {
if (!this.badgeCreated.has(badgeClass)) {
createCSSRule(`.${badgeClass}:after`, badge);
this.badgeCreated.add(badgeClass);
}
}
public addBadge(element: HTMLElement, badgeClass: string): void {
element.innerHTML = (element.innerHTML || '') +
`<div class="${badgeClass}" style="width: 0px; height: 0px;"><div>`;
}
public removeBadge(element: HTMLElement, badgeClass: string): void {
let children: HTMLCollection = element.children;
let current = children[0];
while (current) {
let next = current.nextElementSibling;
if (current.classList.contains(badgeClass)) {
current.remove();
break;
}
current = next;
}
}
}
export const badgeRenderer: BadgeRenderer = new BadgeRenderer();
interface IconPath {
light: URI;
dark: URI;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>add_server_16x16</title><path d="M1.37.16V15.84H9.89v-1H2.37V10.13H10.5v.46h1V.16Zm9.13,9H2.37v-8H10.5Z"/><path d="M6,4.68H3.47V2.17H6Zm-2-.5H5.49V2.67H4Z"/><path d="M11.88,15.84V13.91H9.95v-.82h1.93V11.16h.82v1.93h1.93v.82H12.7v1.93Z"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>add_server_inverse_16x16</title><path class="cls-1" d="M.23.16V15.84H8.75v-1H1.23V10.13H9.36v.46h1V.16Zm9.13,9H1.23v-8H9.36Z"/><path class="cls-1" d="M4.84,4.69H2.32V2.17H4.84Zm-2-.5H4.34V2.67H2.82Z"/><path class="cls-1" d="M10.73,15.84V13.91H8.8v-.82h1.93V11.16h.82v1.93h1.93v.82H11.55v1.93Z"/></svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}.cls-2{fill:#3bb44a;}</style></defs><title>connected_active_server_16x16</title><path class="cls-1" d="M1.29.07V16h9.59a3.31,3.31,0,0,1-1.94-1H2.29V11h6a3.31,3.31,0,0,1,.54-.8A1.81,1.81,0,0,1,9,10H2.29v-9h8.53v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.3,1.8V4.25H5.75V1.8Zm2,2H3.8V2.3H5.25Z"/><circle class="cls-2" cx="11.24" cy="12.52" r="3.47"/></svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#3bb44a;}</style></defs><title>connected_active_server_inverse_16x16</title><path class="cls-1" d="M1.29.07V16h9.59a3.31,3.31,0,0,1-1.94-1H2.29V11h6a3.31,3.31,0,0,1,.54-.8A1.81,1.81,0,0,1,9,10H2.29v-9h8.53v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.3,1.8V4.25H5.75V1.8Zm2,2H3.8V2.3H5.25Z"/><circle class="cls-2" cx="11.24" cy="12.52" r="3.47"/></svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>new_servergroup_16x16</title><path d="M10.2,13h-5v-.75a1.12,1.12,0,0,1,.07-.43,1.81,1.81,0,0,1,.18-.34,3.89,3.89,0,0,1,.24-.32,2.14,2.14,0,0,0,.24-.36,2.26,2.26,0,0,0,.18-.45,2.16,2.16,0,0,0,.07-.61v-5a1,1,0,0,0-.07-.39A1,1,0,0,0,5.87,4l-3-3h8.29V7.08h1V1a1,1,0,0,0-.08-.39,1,1,0,0,0-.54-.54A1,1,0,0,0,11.16,0h-10V13.41L3.45,15.7a1,1,0,0,0,.71.3,1,1,0,0,0,.38-.08A1,1,0,0,0,5,15.56,1,1,0,0,0,5.16,15V14h5ZM5.09,10.18a1.81,1.81,0,0,1-.18.34,3.89,3.89,0,0,1-.24.32,2.14,2.14,0,0,0-.24.36,2.26,2.26,0,0,0-.18.45,2.16,2.16,0,0,0-.07.61V15l-2-2V1.71l3,3v5A1.12,1.12,0,0,1,5.09,10.18Z"/><path d="M11,12.67V10.75H9.11V9.93H11V8h.82V9.93h1.93v.82H11.85v1.93Z"/></svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>new_servergroup_inverse_16x16</title><path class="cls-1" d="M10.2,13h-5v-.75a1.12,1.12,0,0,1,.07-.43,1.81,1.81,0,0,1,.18-.34,3.89,3.89,0,0,1,.24-.32,2.14,2.14,0,0,0,.24-.36,2.26,2.26,0,0,0,.18-.45,2.16,2.16,0,0,0,.07-.61v-5a1,1,0,0,0-.07-.39A1,1,0,0,0,5.87,4l-3-3h8.29V7.08h1V1a1,1,0,0,0-.08-.39,1,1,0,0,0-.54-.54A1,1,0,0,0,11.16,0h-10V13.41L3.45,15.7a1,1,0,0,0,.71.3,1,1,0,0,0,.38-.08A1,1,0,0,0,5,15.56,1,1,0,0,0,5.16,15V14h5ZM5.09,10.18a1.81,1.81,0,0,1-.18.34,3.89,3.89,0,0,1-.24.32,2.14,2.14,0,0,0-.24.36,2.26,2.26,0,0,0-.18.45,2.16,2.16,0,0,0-.07.61V15l-2-2V1.71l3,3v5A1.12,1.12,0,0,1,5.09,10.18Z"/><path class="cls-1" d="M11,12.67V10.75H9.11V9.93H11V8h.82V9.93h1.93v.82H11.85v1.93Z"/></svg>

After

Width:  |  Height:  |  Size: 841 B

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.group-color-options {
display: flex;
width: 100%;
}
.group-color-options .server-group-color {
flex: 1 1 auto;
height: 20px;
margin: 5px;
}
.server-group-dialog {
padding: 15px
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Icons for various registered servers actions */
.monaco-workbench .add-server-action {
background-image: url('add_server.svg');
}
.vs-dark .monaco-workbench .add-server-action,
.hc-black .monaco-workbench .add-server-action {
background-image: url('add_server_inverse.svg');
}
.monaco-workbench .add-server-group-action {
background-image: url('new_servergroup.svg');
}
.vs-dark .monaco-workbench .add-server-group-action,
.hc-black .monaco-workbench .add-server-group-action {
background-image: url('new_servergroup_inverse.svg');
}
.monaco-workbench .active-connections-action {
background-image: url('connected_active_server.svg');
}
.vs-dark .monaco-workbench .active-connections-action,
.hc-black .monaco-workbench .active-connections-action{
background-image: url('connected_active_server_inverse.svg');
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExecuteCommandAction } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import * as azdata from 'azdata';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
import { TreeSelectionHandler } from 'sql/workbench/contrib/objectExplorer/browser/treeSelectionHandler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
export class ObjectExplorerActionsContext implements azdata.ObjectExplorerContext {
public connectionProfile: azdata.IConnectionProfile;
public nodeInfo: azdata.NodeInfo;
public isConnectionNode: boolean = false;
}
export async function getTreeNode(context: ObjectExplorerActionsContext, objectExplorerService: IObjectExplorerService): Promise<TreeNode> {
if (context.isConnectionNode) {
return Promise.resolve(undefined);
}
return await objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath);
}
export class OEAction extends ExecuteCommandAction {
private _treeSelectionHandler: TreeSelectionHandler;
constructor(
id: string, label: string,
@IInstantiationService private _instantiationService: IInstantiationService,
@ICommandService commandService: ICommandService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService
) {
super(id, label, commandService);
}
public async run(actionContext: any): Promise<boolean> {
this._treeSelectionHandler = this._instantiationService.createInstance(TreeSelectionHandler);
let profile: IConnectionProfile;
if (actionContext instanceof ObjectExplorerActionsContext) {
if (actionContext.isConnectionNode) {
profile = new ConnectionProfile(this._capabilitiesService, actionContext.connectionProfile);
} else {
// Get the "correct" version from the tree
let treeNode = await getTreeNode(actionContext, this._objectExplorerService);
profile = TreeUpdateUtils.getConnectionProfile(treeNode);
}
}
this._treeSelectionHandler.onTreeActionStateChange(true);
return super.run(profile).then(() => {
this._treeSelectionHandler.onTreeActionStateChange(false);
return true;
});
}
}

View File

@@ -0,0 +1,229 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
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 { ITreeItem } from 'sql/workbench/common/views';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { hash } from 'vs/base/common/hash';
import { Disposable } from 'vs/base/common/lifecycle';
import { generateUuid } from 'vs/base/common/uuid';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TreeItemCollapsibleState } from 'vs/workbench/common/views';
import { localize } from 'vs/nls';
import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
import { UserCancelledConnectionError } from 'sql/base/common/errors';
import { assign } from 'vs/base/common/objects';
export const SERVICE_ID = 'oeShimService';
export const IOEShimService = createDecorator<IOEShimService>(SERVICE_ID);
export interface IOEShimService {
_serviceBrand: undefined;
getChildren(node: ITreeItem, viewId: string): Promise<ITreeItem[]>;
disconnectNode(viewId: string, node: ITreeItem): Promise<boolean>;
providerExists(providerId: string): boolean;
isNodeConnected(viewId: string, node: ITreeItem): boolean;
getNodeInfoForTreeItem(treeItem: ITreeItem): azdata.NodeInfo;
}
export class OEShimService extends Disposable implements IOEShimService {
_serviceBrand: undefined;
private sessionMap = new Map<number, string>();
private nodeHandleMap = new Map<number, string>();
private nodeInfoMap = new Map<ITreeItem, azdata.NodeInfo>();
constructor(
@IObjectExplorerService private oe: IObjectExplorerService,
@IConnectionManagementService private cm: IConnectionManagementService,
@ICapabilitiesService private capabilities: ICapabilitiesService
) {
super();
}
private async createSession(viewId: string, providerId: string, node: ITreeItem): Promise<string> {
let connProfile = new ConnectionProfile(this.capabilities, node.payload);
connProfile.saveProfile = false;
if (this.cm.providerRegistered(providerId)) {
connProfile = await this.connectOrPrompt(connProfile);
} else {
// Throw and expect upstream handler to notify about the error
// TODO: In the future should use extension recommendations to prompt for correct extension
throw new Error(localize('noProviderFound', "Cannot expand as the required connection provider '{0}' was not found", providerId));
}
let sessionResp = await this.oe.createNewSession(providerId, connProfile);
let sessionId = sessionResp.sessionId;
await new Promise((resolve, reject) => {
let listener = this.oe.onUpdateObjectExplorerNodes(e => {
if (e.connection.id === connProfile.id) {
if (e.errorMessage) {
listener.dispose();
reject(new Error(e.errorMessage));
return;
}
let rootNode = this.oe.getSession(sessionResp.sessionId).rootNode;
// this is how we know it was shimmed
if (rootNode.nodePath) {
this.nodeHandleMap.set(generateNodeMapKey(viewId, node), rootNode.nodePath);
}
}
listener.dispose();
resolve(sessionResp.sessionId);
});
});
return sessionId;
}
private async connectOrPrompt(connProfile: ConnectionProfile): Promise<ConnectionProfile> {
connProfile = await new Promise(async (resolve, reject) => {
let result = await this.cm.connect(connProfile, undefined, { showConnectionDialogOnError: true, showFirewallRuleOnError: true, saveTheConnection: false, showDashboard: false, params: undefined }, {
onConnectSuccess: async (e, profile) => {
let existingConnection = this.cm.findExistingConnection(profile);
connProfile = new ConnectionProfile(this.capabilities, existingConnection);
connProfile = <ConnectionProfile>await this.cm.addSavedPassword(connProfile);
resolve(connProfile);
},
onConnectCanceled: () => {
reject(new UserCancelledConnectionError(localize('loginCanceled', "User canceled")));
},
onConnectReject: undefined,
onConnectStart: undefined,
onDisconnect: undefined
});
// connection cancelled from firewall dialog
if (!result) {
reject(new UserCancelledConnectionError(localize('firewallCanceled', "Firewall dialog canceled")));
}
});
return connProfile;
}
public async disconnectNode(viewId: string, node: ITreeItem): Promise<boolean> {
// we assume only nodes with payloads can be connected
// check to make sure we have an existing connection
let key = generateSessionMapKey(viewId, node);
let session = this.sessionMap.get(key);
if (session) {
let closed = (await this.oe.closeSession(node.childProvider, this.oe.getSession(session))).success;
if (closed) {
this.sessionMap.delete(key);
}
return closed;
}
return Promise.resolve(false);
}
private async getOrCreateSession(viewId: string, node: ITreeItem): Promise<string> {
// verify the map is correct
let key = generateSessionMapKey(viewId, node);
if (!this.sessionMap.has(key)) {
this.sessionMap.set(key, await this.createSession(viewId, node.childProvider, node));
}
return this.sessionMap.get(key);
}
public async getChildren(node: ITreeItem, viewId: string): Promise<ITreeItem[]> {
if (node.payload) {
let sessionId = await this.getOrCreateSession(viewId, node);
let requestHandle = this.nodeHandleMap.get(generateNodeMapKey(viewId, node)) || node.handle;
let treeNode = new TreeNode(undefined, undefined, undefined, requestHandle, undefined, undefined, undefined, undefined, undefined, undefined);
treeNode.connection = new ConnectionProfile(this.capabilities, node.payload);
return this.oe.resolveTreeNodeChildren({
success: undefined,
sessionId,
rootNode: undefined,
errorMessage: undefined
}, treeNode).then(e => e.map(n => this.treeNodeToITreeItem(viewId, n, node)));
} else {
return Promise.resolve([]);
}
}
private treeNodeToITreeItem(viewId: string, node: TreeNode, parentNode: ITreeItem): ITreeItem {
let handle = generateUuid();
let icon: string = '';
let nodePath = node.nodePath;
if (node.iconType) {
icon = (typeof node.iconType === 'string') ? node.iconType : node.iconType.id;
} else {
icon = node.nodeTypeId;
if (node.nodeStatus) {
icon = node.nodeTypeId + '_' + node.nodeStatus;
}
if (node.nodeSubType) {
icon = node.nodeTypeId + '_' + node.nodeSubType;
}
}
icon = icon.toLowerCase();
// Change the database if the node has a different database
// than its parent
let databaseChanged = false;
let updatedPayload: azdata.IConnectionProfile | any = {};
if (node.nodeTypeId === NodeType.Database) {
const database = node.getDatabaseName();
if (database) {
databaseChanged = true;
updatedPayload = assign(updatedPayload, parentNode.payload);
updatedPayload.databaseName = node.getDatabaseName();
}
}
const nodeInfo: azdata.NodeInfo = {
nodePath: nodePath,
nodeType: node.nodeTypeId,
nodeSubType: node.nodeSubType,
nodeStatus: node.nodeStatus,
label: node.label,
isLeaf: node.isAlwaysLeaf,
metadata: node.metadata,
errorMessage: node.errorStateMessage,
iconType: icon,
childProvider: node.childProvider || parentNode.childProvider,
payload: node.payload || (databaseChanged ? updatedPayload : parentNode.payload)
};
let newTreeItem: ITreeItem = {
parentHandle: node.parent.id,
handle,
collapsibleState: node.isAlwaysLeaf ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
label: {
label: node.label
},
childProvider: node.childProvider || parentNode.childProvider,
providerHandle: parentNode.childProvider,
payload: node.payload || (databaseChanged ? updatedPayload : parentNode.payload),
contextValue: node.nodeTypeId,
sqlIcon: icon
};
this.nodeHandleMap.set(generateNodeMapKey(viewId, newTreeItem), nodePath);
this.nodeInfoMap.set(newTreeItem, nodeInfo);
return newTreeItem;
}
public providerExists(providerId: string): boolean {
return this.oe.providerRegistered(providerId);
}
public isNodeConnected(viewId: string, node: ITreeItem): boolean {
return this.sessionMap.has(generateSessionMapKey(viewId, node));
}
public getNodeInfoForTreeItem(treeItem: ITreeItem): azdata.NodeInfo {
if (this.nodeInfoMap.has(treeItem)) {
return this.nodeInfoMap.get(treeItem);
}
return undefined;
}
}
function generateSessionMapKey(viewId: string, node: ITreeItem): number {
return hash([viewId, node.childProvider, node.payload]);
}
function generateNodeMapKey(viewId: string, node: ITreeItem): number {
return hash([viewId, node.handle]);
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ITree, IDataSource } from 'vs/base/parts/tree/browser/tree';
/**
* Implements the DataSource(that returns a parent/children of an element) for the recent connection tree
*/
export class RecentConnectionDataSource implements IDataSource {
/**
* Returns the unique identifier of the given element.
* No more than one element may use a given identifier.
*/
public getId(tree: ITree, element: any): string {
if (element instanceof ConnectionProfile) {
return (<ConnectionProfile>element).id;
} else if (element instanceof ConnectionProfileGroup) {
return (<ConnectionProfileGroup>element).id;
} else {
return undefined;
}
}
/**
* Returns a boolean value indicating whether the element has children.
*/
public hasChildren(tree: ITree, element: any): boolean {
if (element instanceof ConnectionProfile) {
return false;
} else if (element instanceof ConnectionProfileGroup) {
return (<ConnectionProfileGroup>element).hasChildren();
}
return false;
}
/**
* Returns the element's children as an array in a promise.
*/
public getChildren(tree: ITree, element: any): Promise<any> {
if (element instanceof ConnectionProfile) {
return Promise.resolve(null);
} else if (element instanceof ConnectionProfileGroup) {
return Promise.resolve((<ConnectionProfileGroup>element).getChildren());
} else {
return Promise.resolve(null);
}
}
/**
* Returns the element's parent in a promise.
*/
public getParent(tree: ITree, element: any): Promise<any> {
if (element instanceof ConnectionProfile) {
return Promise.resolve((<ConnectionProfile>element).getParent());
} else if (element instanceof ConnectionProfileGroup) {
return Promise.resolve((<ConnectionProfileGroup>element).getParent());
} else {
return Promise.resolve(null);
}
}
}

View File

@@ -0,0 +1,357 @@
/*---------------------------------------------------------------------------------------------
* 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/serverGroupDialog';
import { Colorbox } from 'sql/base/browser/ui/colorbox/colorbox';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { localize } from 'vs/nls';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { ServerGroupViewModel } from 'sql/workbench/contrib/objectExplorer/common/serverGroupViewModel';
import { attachButtonStyler, attachModalDialogStyler } from 'sql/platform/theme/common/styler';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { Color } from 'vs/base/common/color';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
export class ServerGroupDialog extends Modal {
private _addServerButton: Button;
private _closeButton: Button;
private _colorColorBoxesMap: Array<{ color: string, colorbox: Colorbox }> = [];
private _selectedColorOption: number;
private _groupNameInputBox: InputBox;
private _groupDescriptionInputBox: InputBox;
private _viewModel: ServerGroupViewModel;
private _skipGroupNameValidation: boolean = false;
private _serverGroupContainer: HTMLElement;
private _onAddServerGroup = new Emitter<void>();
public onAddServerGroup: Event<void> = this._onAddServerGroup.event;
private _onCancel = new Emitter<void>();
public onCancel: Event<void> = this._onCancel.event;
private _onCloseEvent = new Emitter<void>();
public onCloseEvent: Event<void> = this._onCloseEvent.event;
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IThemeService themeService: IThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) {
super(localize('ServerGroupsDialogTitle', "Server Groups"), TelemetryKeys.ServerGroups, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService);
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
const okLabel = localize('serverGroup.ok', "OK");
const cancelLabel = localize('serverGroup.cancel', "Cancel");
this._addServerButton = this.addFooterButton(okLabel, () => this.addGroup());
this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel());
this.registerListeners();
}
protected layout(height?: number): void {
// NO OP
}
protected renderBody(container: HTMLElement) {
const body = DOM.append(container, DOM.$('.server-group-dialog'));
// Connection Group Name
const serverGroupNameLabel = localize('connectionGroupName', "Server group name");
DOM.append(body, DOM.$('.dialog-label')).innerText = serverGroupNameLabel;
this._groupNameInputBox = new InputBox(DOM.append(body, DOM.$('.input-divider')), this._contextViewService, {
validationOptions: {
validation: (value: string) => !value && !this._skipGroupNameValidation ? ({ type: MessageType.ERROR, content: localize('MissingGroupNameError', "Group name is required.") }) : null
},
ariaLabel: serverGroupNameLabel
});
// Connection Group Description
const groupDescriptionLabel = localize('groupDescription', "Group description");
DOM.append(body, DOM.$('.dialog-label')).innerText = groupDescriptionLabel;
this._groupDescriptionInputBox = new InputBox(DOM.append(body, DOM.$('.input-divider')), this._contextViewService, {
ariaLabel: groupDescriptionLabel
});
// Connection Group Color
const groupColorLabel = localize('groupColor', "Group color");
DOM.append(body, DOM.$('.dialog-label')).innerText = groupColorLabel;
this._serverGroupContainer = DOM.append(body, DOM.$('.group-color-options'));
this.fillGroupColors(this._serverGroupContainer);
DOM.addStandardDisposableListener(body, DOM.EventType.KEY_DOWN, (event: StandardKeyboardEvent) => {
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
this.preventDefaultKeyboardEvent(event);
this.focusPrevious();
} else if (event.equals(KeyCode.Tab)) {
this.preventDefaultKeyboardEvent(event);
this.focusNext();
} else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.LeftArrow)) {
this.preventDefaultKeyboardEvent(event);
this.focusNextColor(event.equals(KeyCode.RightArrow));
}
});
}
private preventDefaultKeyboardEvent(e: StandardKeyboardEvent) {
e.preventDefault();
e.stopPropagation();
}
private isFocusOnColors(): boolean {
let result = false;
this._colorColorBoxesMap.forEach(({ colorbox: colorbox }) => {
if (document.activeElement === colorbox.domNode) {
result = true;
}
});
return result;
}
private focusNext(): void {
if (this._groupNameInputBox.hasFocus()) {
this._groupDescriptionInputBox.focus();
} else if (this._groupDescriptionInputBox.hasFocus()) {
this._colorColorBoxesMap[this._selectedColorOption].colorbox.focus();
} else if (this.isFocusOnColors()) {
this._addServerButton.enabled ? this._addServerButton.focus() : this._closeButton.focus();
} else if (document.activeElement === this._addServerButton.element) {
this._closeButton.focus();
}
else if (document.activeElement === this._closeButton.element) {
this._groupNameInputBox.focus();
}
}
private focusPrevious(): void {
if (document.activeElement === this._closeButton.element) {
this._addServerButton.enabled ? this._addServerButton.focus() : this._colorColorBoxesMap[this._selectedColorOption].colorbox.focus();
} else if (document.activeElement === this._addServerButton.element) {
this._colorColorBoxesMap[this._selectedColorOption].colorbox.focus();
} else if (this.isFocusOnColors()) {
this._groupDescriptionInputBox.focus();
} else if (this._groupDescriptionInputBox.hasFocus()) {
this._groupNameInputBox.focus();
} else if (this._groupNameInputBox.hasFocus()) {
this._closeButton.focus();
}
}
private focusNextColor(moveRight: boolean): void {
let focusIndex: number = -1;
for (let i = 0; i < this._colorColorBoxesMap.length; i++) {
if (document.activeElement === this._colorColorBoxesMap[i].colorbox.domNode) {
focusIndex = i;
break;
}
}
if (focusIndex >= 0) {
if (moveRight) {
focusIndex++;
}
else {
focusIndex--;
}
// check for wraps
if (focusIndex < 0) {
focusIndex = this._colorColorBoxesMap.length - 1;
} else if (focusIndex >= this._colorColorBoxesMap.length) {
focusIndex = 0;
}
this._colorColorBoxesMap[focusIndex].colorbox.focus();
}
}
private onSelectGroupColor(colorToSelect: string): void {
this._viewModel.groupColor = colorToSelect;
this._selectedColorOption = this._viewModel.colors.indexOf(colorToSelect);
this.updateView();
}
private registerListeners(): void {
// Theme styler
this._register(attachInputBoxStyler(this._groupNameInputBox, this._themeService));
this._register(attachInputBoxStyler(this._groupDescriptionInputBox, this._themeService));
this._register(attachButtonStyler(this._addServerButton, this._themeService));
this._register(attachButtonStyler(this._closeButton, this._themeService));
// handler for name change events
this._register(this._groupNameInputBox.onDidChange(groupName => {
this.groupNameChanged(groupName);
}));
// handler for description change events
this._register(this._groupDescriptionInputBox.onDidChange(groupDescription => {
this.groupDescriptionChanged(groupDescription);
}));
}
private fillGroupColors(container: HTMLElement): void {
for (let i = 0; i < this._viewModel.colors.length; i++) {
const color = this._viewModel.colors[i];
const colorColorBox = new Colorbox(container, {
name: 'server-group-color',
class: ['server-group-color'],
label: `Colobox Color: ${color}`,
});
this._register(colorColorBox.onSelect((viaKeyboard) => {
this.onSelectGroupColor(color);
}));
colorColorBox.style({
backgroundColor: Color.fromHex(color)
});
// Theme styler
this._register(attachCheckboxStyler(colorColorBox, this._themeService));
// add the new colorbox to the color map
this._colorColorBoxesMap[i] = { color, colorbox: colorColorBox };
}
}
private groupNameChanged(groupName: string) {
this._viewModel.groupName = groupName;
this.updateView();
}
private groupDescriptionChanged(groupDescription: string) {
this._viewModel.groupDescription = groupDescription;
this.updateView();
}
public get groupName(): string {
return this._groupNameInputBox.value;
}
public get groupDescription(): string {
return this._groupDescriptionInputBox.value;
}
public get selectedColor(): string {
return this._colorColorBoxesMap[this._selectedColorOption].color;
}
public get viewModel(): ServerGroupViewModel {
return this._viewModel;
}
public set viewModel(theViewModel: ServerGroupViewModel) {
this._viewModel = theViewModel;
if (this._serverGroupContainer) {
DOM.clearNode(this._serverGroupContainer);
this.fillGroupColors(this._serverGroupContainer);
}
}
public addGroup(): void {
if (this._addServerButton.enabled) {
if (this.validateInputs()) {
this._onAddServerGroup.fire();
}
}
}
public hideError() {
this.setError('');
}
private validateInputs(): boolean {
let validate = this._groupNameInputBox.validate();
if (!validate) {
this._groupNameInputBox.focus();
}
return validate;
}
// initialize the view based on the current state of the view model
private initializeView(): void {
this.title = this._viewModel.getDialogTitle();
this._skipGroupNameValidation = true;
this._groupNameInputBox.value = this._viewModel.groupName;
this._skipGroupNameValidation = false;
this._groupDescriptionInputBox.value = this._viewModel.groupDescription;
this.updateView();
}
// update UI elements that have derivative behaviors based on other state changes
private updateView(): void {
// check the color buttons and if their checked state does not match the view model state then correct it
for (let i = 0; i < this._colorColorBoxesMap.length; i++) {
let { colorbox: colorbox, color } = this._colorColorBoxesMap[i];
if ((this._viewModel.groupColor === color) && (colorbox.checked === false)) {
colorbox.checked = true;
this._selectedColorOption = i;
} else if ((this._viewModel.groupColor !== color) && (colorbox.checked === true)) {
colorbox.checked = false;
}
}
// OK button state - enabled if there are pending changes that can be saved
this._addServerButton.enabled = this._viewModel.hasPendingChanges();
}
/* Overwrite escape key behavior */
protected onClose() {
this.cancel();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.addGroup();
}
public cancel() {
this._onCancel.fire();
this.close();
}
public close() {
this.hide();
this._groupNameInputBox.hideMessage();
this._onCloseEvent.fire();
}
public open() {
// reset the dialog
this.hideError();
this.initializeView();
this.show();
this._groupNameInputBox.focus();
}
}

View File

@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import {
DisconnectConnectionAction, AddServerAction,
DeleteConnectionAction, RefreshAction, EditServerGroupAction
} from 'sql/workbench/contrib/objectExplorer/browser/connectionTreeAction';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { MenuId, IMenuService } from 'vs/platform/actions/common/actions';
import { ConnectionContextKey } from 'sql/workbench/contrib/connection/common/connectionContextKey';
import { TreeNodeContextKey } from 'sql/workbench/contrib/objectExplorer/common/treeNodeContextKey';
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import { ServerInfoContextKey } from 'sql/workbench/contrib/connection/common/serverInfoContextKey';
import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { firstIndex, find } from 'vs/base/common/arrays';
/**
* Provides actions for the server tree elements
*/
export class ServerTreeActionProvider extends ContributableActionProvider {
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IQueryManagementService private _queryManagementService: IQueryManagementService,
@IMenuService private menuService: IMenuService,
@IContextKeyService private _contextKeyService: IContextKeyService
) {
super();
}
public hasActions(tree: ITree, element: any): boolean {
return element instanceof ConnectionProfileGroup || (element instanceof ConnectionProfile) || (element instanceof TreeNode);
}
/**
* Return actions given an element in the tree
*/
public getActions(tree: ITree, element: any): IAction[] {
if (element instanceof ConnectionProfile) {
return this.getConnectionActions(tree, element);
}
if (element instanceof ConnectionProfileGroup) {
return this.getConnectionProfileGroupActions(tree, element);
}
if (element instanceof TreeNode) {
return this.getObjectExplorerNodeActions({
tree: tree,
profile: element.getConnectionProfile(),
treeNode: element
});
}
return [];
}
public hasSecondaryActions(tree: ITree, element: any): boolean {
return false;
}
/**
* Return actions for connection elements
*/
public getConnectionActions(tree: ITree, profile: ConnectionProfile): IAction[] {
let node = new TreeNode(NodeType.Server, '', false, '', '', '', undefined, undefined, undefined, undefined);
return this.getAllActions({
tree: tree,
profile: profile,
treeNode: node
}, (context) => this.getBuiltinConnectionActions(context));
}
private getAllActions(context: ObjectExplorerContext, getDefaultActions: (context: ObjectExplorerContext) => IAction[]) {
// Create metadata needed to get a useful set of actions
let scopedContextService = this.getContextKeyService(context);
let menu = this.menuService.createMenu(MenuId.ObjectExplorerItemContext, scopedContextService);
// Fill in all actions
const builtIn = getDefaultActions(context);
const actions = [];
const options = { arg: undefined, shouldForwardArgs: true };
const groups = menu.getActions(options);
let insertIndex: number | undefined = 0;
const queryIndex = firstIndex(groups, v => {
if (v[0] === '0_query') {
return true;
} else {
insertIndex += v[1].length;
return false;
}
});
insertIndex = queryIndex > -1 ? insertIndex + groups[queryIndex][1].length : undefined;
fillInActions(groups, actions, false);
if (insertIndex) {
if (!(actions[insertIndex] instanceof Separator) && builtIn.length > 0) {
builtIn.unshift(new Separator());
}
actions.splice(insertIndex, 0, ...builtIn);
} else {
if (actions.length > 0 && builtIn.length > 0) {
builtIn.push(new Separator());
}
actions.unshift(...builtIn);
}
// Cleanup
scopedContextService.dispose();
menu.dispose();
return actions;
}
private getBuiltinConnectionActions(context: ObjectExplorerContext): IAction[] {
let actions: IAction[] = [];
if (this._connectionManagementService.isProfileConnected(context.profile)) {
actions.push(this._instantiationService.createInstance(DisconnectConnectionAction, DisconnectConnectionAction.ID, DisconnectConnectionAction.LABEL, context.profile));
}
actions.push(this._instantiationService.createInstance(DeleteConnectionAction, DeleteConnectionAction.ID, DeleteConnectionAction.DELETE_CONNECTION_LABEL, context.profile));
// Contribute refresh action for scriptable objects via contribution
if (!this.isScriptableObject(context)) {
actions.push(this._instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL, context.tree, context.profile));
}
return actions;
}
private getContextKeyService(context: ObjectExplorerContext): IContextKeyService {
let scopedContextService = this._contextKeyService.createScoped();
let connectionContextKey = new ConnectionContextKey(scopedContextService, this._queryManagementService);
let connectionProfile = context && context.profile;
connectionContextKey.set(connectionProfile);
let serverInfoContextKey = new ServerInfoContextKey(scopedContextService);
if (connectionProfile.id) {
let serverInfo = this._connectionManagementService.getServerInfo(connectionProfile.id);
serverInfoContextKey.set(serverInfo);
}
let treeNodeContextKey = new TreeNodeContextKey(scopedContextService);
if (context.treeNode) {
treeNodeContextKey.set(context.treeNode);
}
return scopedContextService;
}
/**
* Return actions for connection group elements
*/
public getConnectionProfileGroupActions(tree: ITree, element: ConnectionProfileGroup): IAction[] {
return [
this._instantiationService.createInstance(AddServerAction, AddServerAction.ID, AddServerAction.LABEL),
this._instantiationService.createInstance(EditServerGroupAction, EditServerGroupAction.ID, EditServerGroupAction.LABEL, element),
this._instantiationService.createInstance(DeleteConnectionAction, DeleteConnectionAction.ID, DeleteConnectionAction.DELETE_CONNECTION_GROUP_LABEL, element)
];
}
/**
* Return actions for OE elements
*/
private getObjectExplorerNodeActions(context: ObjectExplorerContext): IAction[] {
return this.getAllActions(context, (context) => this.getBuiltInNodeActions(context));
}
private getBuiltInNodeActions(context: ObjectExplorerContext): IAction[] {
let actions: IAction[] = [];
let treeNode = context.treeNode;
if (TreeUpdateUtils.isDatabaseNode(treeNode)) {
if (TreeUpdateUtils.isAvailableDatabaseNode(treeNode)) {
} else {
return actions;
}
}
// Contribute refresh action for scriptable objects via contribution
if (!this.isScriptableObject(context)) {
actions.push(this._instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL, context.tree, context.profile));
}
return actions;
}
private isScriptableObject(context: ObjectExplorerContext): boolean {
if (context.treeNode) {
if (find(NodeType.SCRIPTABLE_OBJECTS, x => x === context.treeNode.nodeTypeId)) {
return true;
}
}
return false;
}
}
interface ObjectExplorerContext {
tree: ITree;
profile: ConnectionProfile;
treeNode?: TreeNode;
}

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITree, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree';
import * as treedefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ServerTreeActionProvider } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeActionProvider';
import { ObjectExplorerActionsContext } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
/**
* Extends the tree controller to handle clicks on the tree elements
*/
export class ServerTreeController extends treedefaults.DefaultController {
constructor(
private actionProvider: ServerTreeActionProvider,
@IContextMenuService private contextMenuService: IContextMenuService,
@IKeybindingService private keybindingService: IKeybindingService
) {
super({
clickBehavior: treedefaults.ClickBehavior.ON_MOUSE_DOWN,
openMode: treedefaults.OpenMode.SINGLE_CLICK
});
}
public onClick(tree: ITree, element: any, event: IMouseEvent): boolean {
return super.onClick(tree, element, event);
}
protected onLeftClick(tree: ITree, element: any, event: IMouseEvent, origin: string = 'mouse'): boolean {
return super.onLeftClick(tree, element, event, origin);
}
// Do not allow left / right to expand and collapse groups #7848
protected onLeft(tree: ITree, event: IKeyboardEvent): boolean {
return true;
}
protected onRight(tree: ITree, event: IKeyboardEvent): boolean {
return true;
}
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
return super.onEnter(tree, event);
}
/**
* Return actions in the context menu
*/
public onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean {
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
return false;
}
// Check if clicked on some element
if (element === tree.getInput()) {
return false;
}
event.preventDefault();
event.stopPropagation();
tree.setFocus(element);
let actionContext: any;
if (element instanceof TreeNode) {
let context = new ObjectExplorerActionsContext();
context.nodeInfo = element.toNodeInfo();
// Note: getting DB name before, but intentionally not using treeUpdateUtils.getConnectionProfile as it replaces
// the connection ID with a new one. This breaks a number of internal tasks
context.connectionProfile = element.getConnectionProfile().toIConnectionProfile();
context.connectionProfile.databaseName = element.getDatabaseName();
actionContext = context;
} else if (element instanceof ConnectionProfile) {
let context = new ObjectExplorerActionsContext();
context.connectionProfile = element.toIConnectionProfile();
context.isConnectionNode = true;
actionContext = context;
} else {
// TODO: because the connection group is used as a context object and isn't serializable,
// the Group-level context menu is not currently extensible
actionContext = element;
}
let anchor = { x: event.posx + 1, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this.actionProvider.getActions(tree, element),
getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id),
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
tree.domFocus();
}
},
getActionsContext: () => (actionContext)
});
return true;
}
}

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ITree, IDataSource } from 'vs/base/parts/tree/browser/tree';
import { TreeNode, TreeItemCollapsibleState } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import Severity from 'vs/base/common/severity';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
/**
* Implements the DataSource(that returns a parent/children of an element) for the server tree
*/
export class ServerTreeDataSource implements IDataSource {
constructor(
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) {
}
/**
* Returns the unique identifier of the given element.
* No more than one element may use a given identifier.
*/
public getId(tree: ITree, element: any): string {
if (element instanceof ConnectionProfile
|| element instanceof ConnectionProfileGroup
|| element instanceof TreeNode) {
return element.id;
} else {
return undefined;
}
}
/**
* Returns a boolean value indicating whether the element has children.
*/
public hasChildren(tree: ITree, element: any): boolean {
if (element instanceof ConnectionProfile) {
return true;
} else if (element instanceof ConnectionProfileGroup) {
return element.hasChildren();
} else if (element instanceof TreeNode) {
return !element.isAlwaysLeaf;
}
return false;
}
/**
* Returns the element's children as an array in a promise.
*/
public async getChildren(tree: ITree, element: any): Promise<(ConnectionProfile | ConnectionProfileGroup | TreeNode)[]> {
if (element instanceof ConnectionProfile) {
return TreeUpdateUtils.getObjectExplorerNode(<ConnectionProfile>element, this._connectionManagementService, this._objectExplorerService);
} else if (element instanceof ConnectionProfileGroup) {
return (element as ConnectionProfileGroup).getChildren();
} else if (element instanceof TreeNode) {
let node = element;
if (node.children) {
return node.children;
} else {
try {
return this._objectExplorerService.resolveTreeNodeChildren(node.getSession(), node);
} catch (expandError) {
await node.setExpandedState(TreeItemCollapsibleState.Collapsed);
node.errorStateMessage = expandError;
this.showError(expandError);
// collapse node and refresh in case of error so remove tree cache
setTimeout(() => {
tree.collapse(element).then(() => tree.refresh(element));
});
return [];
}
}
}
return [];
}
/**
* Returns the element's parent in a promise.
*/
public getParent(tree: ITree, element: any): Promise<any> {
if (element instanceof ConnectionProfile) {
return Promise.resolve(element.getParent());
} else if (element instanceof ConnectionProfileGroup) {
return Promise.resolve(element.getParent());
} else if (element instanceof TreeNode) {
return Promise.resolve(TreeUpdateUtils.getObjectExplorerParent(element, this._connectionManagementService));
} else {
return Promise.resolve(null);
}
}
private showError(errorMessage: string) {
if (this._errorMessageService) {
this._errorMessageService.showDialog(Severity.Error, '', errorMessage);
}
}
}

View File

@@ -0,0 +1,276 @@
/*---------------------------------------------------------------------------------------------
* 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!sql/media/objectTypes/objecttypes';
import 'vs/css!sql/media/icons/common-icons';
import * as dom from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { badgeRenderer, iconRenderer } from 'sql/workbench/contrib/objectExplorer/browser/iconRenderer';
import { URI } from 'vs/base/common/uri';
export interface IConnectionTemplateData {
root: HTMLElement;
label: HTMLSpanElement;
icon: HTMLElement;
connectionProfile: ConnectionProfile;
}
export interface IConnectionProfileGroupTemplateData {
root: HTMLElement;
name: HTMLSpanElement;
inputBox: InputBox;
}
export interface IObjectExplorerTemplateData {
root: HTMLElement;
label: HTMLSpanElement;
icon: HTMLElement;
treeNode: TreeNode;
}
/**
* Renders the tree items.
* Uses the dom template to render connection groups and connections.
*/
export class ServerTreeRenderer implements IRenderer {
public static CONNECTION_HEIGHT = 23;
public static CONNECTION_GROUP_HEIGHT = 38;
private static CONNECTION_TEMPLATE_ID = 'connectionProfile';
private static CONNECTION_GROUP_TEMPLATE_ID = 'connectionProfileGroup';
public static OBJECTEXPLORER_HEIGHT = 23;
private static OBJECTEXPLORER_TEMPLATE_ID = 'objectExplorer';
/**
* _isCompact is used to render connections tiles with and without the action buttons.
* When set to true, like in the connection dialog recent connections tree, the connection
* tile is rendered without the action buttons( such as connect, new query).
*/
private _isCompact: boolean = false;
constructor(
isCompact: boolean,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) {
// isCompact defaults to false unless explicitly set by instantiation call.
if (isCompact) {
this._isCompact = isCompact;
}
}
/**
* Returns the element's height in the tree, in pixels.
*/
public getHeight(tree: ITree, element: any): number {
if (element instanceof ConnectionProfileGroup) {
return ServerTreeRenderer.CONNECTION_GROUP_HEIGHT;
} else if (element instanceof ConnectionProfile) {
return ServerTreeRenderer.CONNECTION_HEIGHT;
}
return ServerTreeRenderer.OBJECTEXPLORER_HEIGHT;
}
/**
* Returns a template ID for a given element.
*/
public getTemplateId(tree: ITree, element: any): string {
if (element instanceof ConnectionProfileGroup) {
return ServerTreeRenderer.CONNECTION_GROUP_TEMPLATE_ID;
} else if (element instanceof ConnectionProfile) {
return ServerTreeRenderer.CONNECTION_TEMPLATE_ID;
}
return ServerTreeRenderer.OBJECTEXPLORER_TEMPLATE_ID;
}
/**
* Render template in a dom element based on template id
*/
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
if (templateId === ServerTreeRenderer.CONNECTION_TEMPLATE_ID) {
const connectionTemplate: IObjectExplorerTemplateData = Object.create(null);
connectionTemplate.root = dom.append(container, dom.$('.connection-tile'));
connectionTemplate.icon = dom.append(connectionTemplate.root, dom.$('div.icon server-page'));
connectionTemplate.label = dom.append(connectionTemplate.root, dom.$('div.label'));
return connectionTemplate;
} else if (templateId === ServerTreeRenderer.CONNECTION_GROUP_TEMPLATE_ID) {
container.classList.add('server-group');
const groupTemplate: IConnectionProfileGroupTemplateData = Object.create(null);
groupTemplate.root = dom.append(container, dom.$('.server-group'));
groupTemplate.name = dom.append(groupTemplate.root, dom.$('span.name'));
return groupTemplate;
} else {
const objectExplorerTemplate: IObjectExplorerTemplateData = Object.create(null);
objectExplorerTemplate.root = dom.append(container, dom.$('.object-element-group'));
objectExplorerTemplate.icon = dom.append(objectExplorerTemplate.root, dom.$('div.object-icon'));
objectExplorerTemplate.label = dom.append(objectExplorerTemplate.root, dom.$('div.label'));
return objectExplorerTemplate;
}
}
/**
* Render a element, given an object bag returned by the template
*/
public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
if (templateId === ServerTreeRenderer.CONNECTION_TEMPLATE_ID) {
this.renderConnection(element, templateData);
} else if (templateId === ServerTreeRenderer.CONNECTION_GROUP_TEMPLATE_ID) {
this.renderConnectionProfileGroup(element, templateData);
} else {
this.renderObjectExplorer(element, templateData);
}
}
private renderObjectExplorer(treeNode: TreeNode, templateData: IObjectExplorerTemplateData): void {
// Use an explicitly defined iconType first. If not defined, fall back to using nodeType and
// other compount indicators instead.
let iconName: string = undefined;
if (treeNode.iconType) {
iconName = (typeof treeNode.iconType === 'string') ? treeNode.iconType : treeNode.iconType.id;
} else {
iconName = treeNode.nodeTypeId;
if (treeNode.nodeStatus) {
iconName = treeNode.nodeTypeId + '_' + treeNode.nodeStatus;
}
if (treeNode.nodeSubType) {
iconName = treeNode.nodeTypeId + '_' + treeNode.nodeSubType;
}
}
let tokens: string[] = [];
for (let index = 1; index < templateData.icon.classList.length; index++) {
tokens.push(templateData.icon.classList.item(index));
}
templateData.icon.classList.remove(...tokens);
templateData.icon.classList.add('icon');
let iconLowerCaseName = iconName.toLocaleLowerCase();
templateData.icon.classList.add(iconLowerCaseName);
if (treeNode.iconPath) {
iconRenderer.putIcon(templateData.icon, treeNode.iconPath);
}
templateData.label.textContent = treeNode.label;
templateData.root.title = treeNode.label;
}
private getIconPath(connection: ConnectionProfile): IconPath {
if (!connection) { return undefined; }
if (connection['iconPath']) {
return connection['iconPath'];
}
let iconId = this._connectionManagementService.getConnectionIconId(connection.id);
if (!iconId) { return undefined; }
let providerProperties = this._connectionManagementService.getProviderProperties(connection.providerName);
if (!providerProperties) { return undefined; }
let iconPath: IconPath = undefined;
let pathConfig: URI | IconPath | { id: string, path: IconPath }[] = providerProperties['iconPath'];
if (Array.isArray(pathConfig)) {
for (const e of pathConfig) {
if (!e.id || e.id === iconId) {
iconPath = e.path;
connection['iconPath'] = iconPath;
break;
}
}
} else if (pathConfig['light']) {
iconPath = pathConfig as IconPath;
connection['iconPath'] = iconPath;
} else {
let singlePath = pathConfig as URI;
iconPath = { light: singlePath, dark: singlePath };
connection['iconPath'] = iconPath;
}
return iconPath;
}
private renderServerIcon(element: HTMLElement, iconPath: IconPath, isConnected: boolean): void {
if (!element) { return; }
if (iconPath) {
iconRenderer.putIcon(element, iconPath);
}
let badgeToRemove: string = isConnected ? badgeRenderer.serverDisconnected : badgeRenderer.serverConnected;
let badgeToAdd: string = isConnected ? badgeRenderer.serverConnected : badgeRenderer.serverDisconnected;
badgeRenderer.removeBadge(element, badgeToRemove);
badgeRenderer.addBadge(element, badgeToAdd);
}
private renderConnection(connection: ConnectionProfile, templateData: IConnectionTemplateData): void {
if (!this._isCompact) {
let iconPath: IconPath = this.getIconPath(connection);
if (this._connectionManagementService.isConnected(undefined, connection)) {
templateData.icon.classList.remove('disconnected');
templateData.icon.classList.add('connected');
this.renderServerIcon(templateData.icon, iconPath, true);
} else {
templateData.icon.classList.remove('connected');
templateData.icon.classList.add('disconnected');
this.renderServerIcon(templateData.icon, iconPath, false);
}
}
let label = connection.title;
if (!connection.isConnectionOptionsValid) {
label = localize('loading', "Loading...");
}
templateData.label.textContent = label;
templateData.root.title = connection.serverInfo;
templateData.connectionProfile = connection;
}
private renderConnectionProfileGroup(connectionProfileGroup: ConnectionProfileGroup, templateData: IConnectionProfileGroupTemplateData): void {
let rowElement = this.findParentElement(templateData.root, 'monaco-tree-row');
if (rowElement) {
if (connectionProfileGroup.color) {
rowElement.style.background = connectionProfileGroup.color;
} else {
// If the group doesn't contain specific color, assign the default color
rowElement.style.background = '#515151';
}
}
if (connectionProfileGroup.description && (connectionProfileGroup.description !== '')) {
templateData.root.title = connectionProfileGroup.description;
}
templateData.name.hidden = false;
templateData.name.textContent = connectionProfileGroup.name;
}
/**
* Returns the first parent which contains the className
*/
private findParentElement(container: HTMLElement, className: string): HTMLElement {
let currentElement = container;
while (currentElement) {
if (currentElement.className.indexOf(className) > -1) {
break;
}
currentElement = currentElement.parentElement;
}
return currentElement;
}
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
// no op
// InputBox disposed in wrapUp
}
}
interface IconPath {
light: URI;
dark: URI;
}

View File

@@ -0,0 +1,515 @@
/*---------------------------------------------------------------------------------------------
* 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/serverTreeActions';
import * as errors from 'vs/base/common/errors';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import Severity from 'vs/base/common/severity';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { Disposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Event, Emitter } from 'vs/base/common/event';
import { append, $, hide, show } from 'vs/base/browser/dom';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import * as ConnectionUtils from 'sql/platform/connection/common/utils';
import { ActiveConnectionsFilterAction } from 'sql/workbench/contrib/objectExplorer/browser/connectionTreeAction';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { TreeCreationUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeCreationUtils';
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
import { TreeSelectionHandler } from 'sql/workbench/contrib/objectExplorer/browser/treeSelectionHandler';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { Button } from 'sql/base/browser/ui/button/button';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import { TreeNode, TreeItemCollapsibleState } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { SERVER_GROUP_CONFIG, SERVER_GROUP_AUTOEXPAND_CONFIG } from 'sql/workbench/contrib/objectExplorer/common/serverGroup.contribution';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { ServerTreeActionProvider } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeActionProvider';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { isHidden } from 'sql/base/browser/dom';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { startsWith } from 'vs/base/common/strings';
/**
* ServerTreeview implements the dynamic tree view.
*/
export class ServerTreeView extends Disposable {
public messages: HTMLElement;
private _buttonSection: HTMLElement;
private _treeSelectionHandler: TreeSelectionHandler;
private _activeConnectionsFilterAction: ActiveConnectionsFilterAction;
private _tree: ITree;
private _onSelectionOrFocusChange: Emitter<void>;
private _actionProvider: ServerTreeActionProvider;
constructor(
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IThemeService private _themeService: IThemeService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IConfigurationService private _configurationService: IConfigurationService,
@ICapabilitiesService capabilitiesService: ICapabilitiesService
) {
super();
this._activeConnectionsFilterAction = this._instantiationService.createInstance(
ActiveConnectionsFilterAction,
ActiveConnectionsFilterAction.ID,
ActiveConnectionsFilterAction.LABEL,
this);
this._treeSelectionHandler = this._instantiationService.createInstance(TreeSelectionHandler);
this._onSelectionOrFocusChange = new Emitter();
this._actionProvider = this._instantiationService.createInstance(ServerTreeActionProvider);
capabilitiesService.onCapabilitiesRegistered(() => {
if (this._connectionManagementService.hasRegisteredServers()) {
this.refreshTree();
this._treeSelectionHandler.onTreeActionStateChange(false);
}
});
this.registerCommands();
}
/**
* Get active connections filter action
*/
public get activeConnectionsFilterAction(): ActiveConnectionsFilterAction {
return this._activeConnectionsFilterAction;
}
/**
* Event fired when the tree's selection or focus changes
*/
public get onSelectionOrFocusChange(): Event<void> {
return this._onSelectionOrFocusChange.event;
}
public get treeActionProvider(): ServerTreeActionProvider {
return this._actionProvider;
}
public get tree(): ITree {
return this._tree;
}
/**
*
* Register search related commands
*/
public registerCommands(): void {
CommandsRegistry.registerCommand({
id: 'registeredServers.searchServer',
handler: (accessor: ServicesAccessor, ...args: any[]) => {
this.searchTree(args[0]);
}
});
CommandsRegistry.registerCommand({
id: 'registeredServers.clearSearchServerResult',
handler: (accessor: ServicesAccessor, ...args: any[]) => {
this.refreshTree();
}
});
}
/**
* Render the view body
*/
public renderBody(container: HTMLElement): Thenable<void> {
// Add div to display no connections found message and hide it by default
this.messages = append(container, $('.title'));
const messageText = append(this.messages, $('span'));
messageText.style.paddingLeft = '10px';
messageText.innerText = localize('servers.noConnections', "No connections found.");
hide(this.messages);
if (!this._connectionManagementService.hasRegisteredServers()) {
this._activeConnectionsFilterAction.enabled = false;
this._buttonSection = append(container, $('.button-section'));
const connectButton = new Button(this._buttonSection);
connectButton.label = localize('serverTree.addConnection', "Add Connection");
this._register(attachButtonStyler(connectButton, this._themeService));
this._register(connectButton.onDidClick(() => {
this._connectionManagementService.showConnectionDialog();
}));
}
this._tree = this._register(TreeCreationUtils.createRegisteredServersTree(container, this._instantiationService));
//this._tree.setInput(undefined);
this._register(this._tree.onDidChangeSelection((event) => this.onSelected(event)));
this._register(this._tree.onDidBlur(() => this._onSelectionOrFocusChange.fire()));
this._register(this._tree.onDidChangeFocus(() => this._onSelectionOrFocusChange.fire()));
// Theme styler
this._register(attachListStyler(this._tree, this._themeService));
// Refresh Tree when these events are emitted
this._register(this._connectionManagementService.onAddConnectionProfile((newProfile: IConnectionProfile) => {
this.handleAddConnectionProfile(newProfile);
}));
this._register(this._connectionManagementService.onDeleteConnectionProfile(() => {
this.refreshTree();
}));
this._register(this._connectionManagementService.onDisconnect((connectionParams) => {
if (this.isObjectExplorerConnectionUri(connectionParams.connectionUri)) {
this.deleteObjectExplorerNodeAndRefreshTree(connectionParams.connectionProfile);
}
}));
if (this._objectExplorerService && this._objectExplorerService.onUpdateObjectExplorerNodes) {
this._register(this._objectExplorerService.onUpdateObjectExplorerNodes(args => {
if (args.errorMessage) {
this.showError(args.errorMessage);
}
if (args.connection) {
this.onObjectExplorerSessionCreated(args.connection);
}
}));
}
return new Promise<void>(async (resolve, reject) => {
await this.refreshTree();
const root = <ConnectionProfileGroup>this._tree.getInput();
const expandGroups: boolean = this._configurationService.getValue(SERVER_GROUP_CONFIG)[SERVER_GROUP_AUTOEXPAND_CONFIG];
if (expandGroups) {
await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(root));
}
if (root && !root.hasValidConnections) {
this._treeSelectionHandler.onTreeActionStateChange(true);
resolve();
} else {
resolve();
}
});
}
public isObjectExplorerConnectionUri(uri: string): boolean {
let isBackupRestoreUri: boolean = uri.indexOf(ConnectionUtils.ConnectionUriBackupIdAttributeName) >= 0 ||
uri.indexOf(ConnectionUtils.ConnectionUriRestoreIdAttributeName) >= 0;
return uri && startsWith(uri, ConnectionUtils.uriPrefixes.default) && !isBackupRestoreUri;
}
private async handleAddConnectionProfile(newProfile: IConnectionProfile): Promise<void> {
if (newProfile) {
const groups = this._connectionManagementService.getConnectionGroups();
const profile = ConnectionUtils.findProfileInGroup(newProfile, groups);
if (profile) {
newProfile = profile;
}
}
if (this._buttonSection) {
hide(this._buttonSection);
this._activeConnectionsFilterAction.enabled = true;
}
const currentSelections = this._tree.getSelection();
const currentSelectedElement = currentSelections && currentSelections.length >= 1 ? currentSelections[0] : undefined;
const newProfileIsSelected = currentSelectedElement && newProfile ? currentSelectedElement.id === newProfile.id : false;
if (newProfile && currentSelectedElement && !newProfileIsSelected) {
this._tree.clearSelection();
}
await this.refreshTree();
if (newProfile && !newProfileIsSelected) {
await this._tree.reveal(newProfile);
this._tree.select(newProfile);
}
}
private showError(errorMessage: string) {
if (this._errorMessageService) {
this._errorMessageService.showDialog(Severity.Error, '', errorMessage);
}
}
private getConnectionInTreeInput(connectionId: string): ConnectionProfile {
const root = TreeUpdateUtils.getTreeInput(this._connectionManagementService);
const connections = ConnectionProfileGroup.getConnectionsInGroup(root);
const results = connections.filter(con => {
if (connectionId === con.id) {
return true;
} else {
return false;
}
});
if (results && results.length > 0) {
return results[0];
}
return null;
}
private onObjectExplorerSessionCreated(connection: IConnectionProfile) {
const conn = this.getConnectionInTreeInput(connection.id);
if (conn) {
this._tree.refresh(conn).then(() => {
return this._tree.expand(conn).then(() => {
return this._tree.reveal(conn, 0.5).then(() => {
this._treeSelectionHandler.onTreeActionStateChange(false);
});
});
}).then(null, errors.onUnexpectedError);
}
}
public addObjectExplorerNodeAndRefreshTree(connection: IConnectionProfile): void {
hide(this.messages);
if (!this._objectExplorerService.getObjectExplorerNode(connection)) {
this._objectExplorerService.updateObjectExplorerNodes(connection).then(() => {
// The oe request is sent. an event will be raised when the session is created
}, error => {
});
}
}
public deleteObjectExplorerNodeAndRefreshTree(connection: IConnectionProfile): Thenable<void> {
if (connection) {
const conn = this.getConnectionInTreeInput(connection.id);
if (conn) {
return this._objectExplorerService.deleteObjectExplorerNode(conn).then(async () => {
await this._tree.collapse(conn);
return this._tree.refresh(conn);
});
}
}
return Promise.resolve();
}
public refreshTree(): Promise<void> {
hide(this.messages);
this.clearOtherActions();
return TreeUpdateUtils.registeredServerUpdate(this._tree, this._connectionManagementService);
}
public refreshElement(element: any): Thenable<void> {
return this._tree.refresh(element);
}
/**
* Filter connections based on view (recent/active)
*/
private filterConnections(treeInput: ConnectionProfileGroup[], view: string): ConnectionProfileGroup[] {
if (!treeInput || treeInput.length === 0) {
return undefined;
}
const result = treeInput.map(group => {
// Keep active/recent connections and remove the rest
if (group.connections) {
group.connections = group.connections.filter(con => {
if (view === 'active') {
return this._connectionManagementService.isConnected(undefined, con);
} else if (view === 'recent') {
return this._connectionManagementService.isRecent(con);
}
return false;
});
}
group.children = this.filterConnections(group.children, view);
// Remove subgroups that are undefined
if (group.children) {
group.children = group.children.filter(group => {
return (group) ? true : false;
});
}
// Return a group only if it has a filtered result or subgroup.
if ((group.connections && group.connections.length > 0) || (group.children && group.children.length > 0)) {
return group;
}
return undefined;
});
return result;
}
/**
* Set tree elements based on the view (recent/active)
*/
public showFilteredTree(view: string): void {
hide(this.messages);
// Clear other action views if user switched between two views
this.clearOtherActions(view);
const root = TreeUpdateUtils.getTreeInput(this._connectionManagementService);
let treeInput: ConnectionProfileGroup = null;
if (root) {
// Filter results based on view
const filteredResults = this.filterConnections([root], view);
if (!filteredResults || !filteredResults[0]) {
show(this.messages);
this.messages.focus();
} else {
treeInput = filteredResults[0];
}
this._tree.setInput(treeInput).then(async () => {
if (isHidden(this.messages)) {
this._tree.getFocus();
await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(treeInput));
} else {
this._tree.clearFocus();
}
}, errors.onUnexpectedError);
} else {
//no op
}
}
/**
* Searches and sets the tree input to the results
*/
public searchTree(searchString: string): void {
if (!searchString) {
return;
}
hide(this.messages);
// Clear other actions if user searched during other views
this.clearOtherActions();
// Filter connections based on search
const filteredResults = this.searchConnections(searchString);
if (!filteredResults || filteredResults.length === 0) {
show(this.messages);
this.messages.focus();
}
// Add all connections to tree root and set tree input
const treeInput = new ConnectionProfileGroup('searchroot', undefined, 'searchroot', undefined, undefined);
treeInput.addConnections(filteredResults);
this._tree.setInput(treeInput).then(async () => {
if (isHidden(this.messages)) {
this._tree.getFocus();
await this._tree.expandAll(ConnectionProfileGroup.getSubgroups(treeInput));
} else {
this._tree.clearFocus();
}
}, errors.onUnexpectedError);
}
/**
* Searches through all the connections and returns a list of matching connections
*/
private searchConnections(searchString: string): ConnectionProfile[] {
const root = TreeUpdateUtils.getTreeInput(this._connectionManagementService);
const connections = ConnectionProfileGroup.getConnectionsInGroup(root);
const results = connections.filter(con => {
if (searchString && (searchString.length > 0)) {
return this.isMatch(con, searchString);
} else {
return false;
}
});
return results;
}
/**
* Returns true if the connection matches the search string.
* For now, the search criteria is true if the
* server name or database name contains the search string (ignores case).
*/
private isMatch(connection: ConnectionProfile, searchString: string): boolean {
searchString = searchString.trim().toLocaleUpperCase();
if (this.checkIncludes(searchString, connection.databaseName) || this.checkIncludes(searchString, connection.serverName)) {
return true;
}
return false;
}
private checkIncludes(searchString: string, candidate: string): boolean {
if (candidate && searchString) {
return candidate.toLocaleUpperCase().indexOf(searchString) > -1;
}
return false;
}
/**
* Clears the toggle icons for active and recent
*/
private clearOtherActions(view?: string) {
if (!view) {
this._activeConnectionsFilterAction.isSet = false;
}
if (view === 'recent') {
this._activeConnectionsFilterAction.isSet = false;
}
}
private onSelected(event: any): void {
this._treeSelectionHandler.onTreeSelect(event, this._tree, this._connectionManagementService, this._objectExplorerService, () => this._onSelectionOrFocusChange.fire());
this._onSelectionOrFocusChange.fire();
}
/**
* set the layout of the view
*/
public layout(height: number): void {
this._tree.layout(height);
}
/**
* set the visibility of the view
*/
public setVisible(visible: boolean): void {
if (visible) {
this._tree.onVisible();
} else {
this._tree.onHidden();
}
}
/**
* Get the list of selected nodes in the tree
*/
public getSelection(): any[] {
return this._tree.getSelection();
}
/**
* Get whether the tree view currently has focus
*/
public isFocused(): boolean {
return this._tree.isDOMFocused();
}
/**
* Set whether the given element is expanded or collapsed
*/
public setExpandedState(element: TreeNode | ConnectionProfile, expandedState: TreeItemCollapsibleState): Thenable<void> {
if (expandedState === TreeItemCollapsibleState.Collapsed) {
return this._tree.collapse(element);
} else if (expandedState === TreeItemCollapsibleState.Expanded) {
return this._tree.expand(element);
}
return Promise.resolve();
}
/**
* Reveal the given element in the tree
*/
public reveal(element: TreeNode | ConnectionProfile): Thenable<void> {
return this._tree.reveal(element);
}
/**
* Select the given element in the tree and clear any other selections
*/
public setSelected(element: TreeNode | ConnectionProfile, selected: boolean, clearOtherSelections: boolean): Thenable<void> {
if (clearOtherSelections || (selected && clearOtherSelections !== false)) {
this._tree.clearSelection();
}
if (selected) {
this._tree.select(element);
return this._tree.reveal(element);
} else {
this._tree.deselect(element);
return Promise.resolve();
}
}
/**
* Check if the given element in the tree is expanded
*/
public isExpanded(element: TreeNode | ConnectionProfile): boolean {
return this._tree.isExpanded(element);
}
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* 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/serverTreeActions';
import * as nls from 'vs/nls';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServerTreeRenderer } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeRenderer';
import { ServerTreeDataSource } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeDataSource';
import { ServerTreeController } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeController';
import { ServerTreeActionProvider } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeActionProvider';
import { DefaultFilter, DefaultAccessibilityProvider, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
import { IController } from 'vs/base/parts/tree/browser/tree';
import { ServerTreeDragAndDrop, RecentConnectionsDragAndDrop } from 'sql/workbench/contrib/objectExplorer/browser/dragAndDropController';
import { RecentConnectionDataSource } from 'sql/workbench/contrib/objectExplorer/browser/recentConnectionDataSource';
export class TreeCreationUtils {
/**
* Create a Recent Connections tree
*/
public static createConnectionTree(treeContainer: HTMLElement, instantiationService: IInstantiationService, useController?: IController): Tree {
const dataSource = instantiationService.createInstance(RecentConnectionDataSource);
const renderer = instantiationService.createInstance(ServerTreeRenderer, true);
const controller = useController ? useController : new DefaultController();
const dnd = instantiationService.createInstance(RecentConnectionsDragAndDrop);
const filter = new DefaultFilter();
const sorter = undefined;
const accessibilityProvider = new DefaultAccessibilityProvider();
return new Tree(treeContainer, { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider },
{
indentPixels: 0,
twistiePixels: 0,
ariaLabel: nls.localize('treeAriaLabel', "Recent Connections")
});
}
/**
* Create a Servers viewlet tree
*/
public static createRegisteredServersTree(treeContainer: HTMLElement, instantiationService: IInstantiationService): Tree {
const dataSource = instantiationService.createInstance(ServerTreeDataSource);
const actionProvider = instantiationService.createInstance(ServerTreeActionProvider);
const renderer = instantiationService.createInstance(ServerTreeRenderer, false);
const controller = instantiationService.createInstance(ServerTreeController, actionProvider);
const dnd = instantiationService.createInstance(ServerTreeDragAndDrop);
const filter = new DefaultFilter();
const sorter = undefined;
const accessibilityProvider = new DefaultAccessibilityProvider();
return new Tree(treeContainer, { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider },
{
indentPixels: 10,
twistiePixels: 20,
ariaLabel: nls.localize('treeCreation.regTreeAriaLabel', "Servers")
});
}
}

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConnectionManagementService, IConnectionCompletionOptions } from 'sql/platform/connection/common/connectionManagement';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
// import { IProgressRunner, IProgressService } from 'vs/platform/progress/common/progress';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { TreeUpdateUtils } from 'sql/workbench/contrib/objectExplorer/browser/treeUpdateUtils';
export class TreeSelectionHandler {
// progressRunner: IProgressRunner;
private _lastClicked: any[];
private _clickTimer: any = undefined;
private _otherTimer: any = undefined;
// constructor(@IProgressService private _progressService: IProgressService) {
// }
public onTreeActionStateChange(started: boolean): void {
// if (this.progressRunner) {
// this.progressRunner.done();
// }
// if (started) {
// this.progressRunner = this._progressService.show(true);
// } else {
// this.progressRunner = null;
// }
}
private isMouseEvent(event: any): boolean {
return event && event.payload && event.payload.origin === 'mouse';
}
private isKeyboardEvent(event: any): boolean {
return event && event.payload && event.payload.origin === 'keyboard';
}
/**
* Handle select ion of tree element
*/
public onTreeSelect(event: any, tree: ITree, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, connectionCompleteCallback: () => void) {
let sendSelectionEvent = ((event: any, selection: any, isDoubleClick: boolean, userInteraction: boolean) => {
// userInteraction: defensive - don't touch this something else is handling it.
if (userInteraction === true && this._lastClicked && this._lastClicked[0] === selection[0]) {
this._lastClicked = undefined;
}
if (!TreeUpdateUtils.isInDragAndDrop) {
this.handleTreeItemSelected(connectionManagementService, objectExplorerService, isDoubleClick, this.isKeyboardEvent(event), selection, tree, connectionCompleteCallback);
}
});
let selection = tree.getSelection();
if (!selection || selection.length === 0) {
return;
}
let specificSelection = selection[0];
if (this.isMouseEvent(event) || this.isKeyboardEvent(event)) {
if (this._lastClicked !== undefined) {
clearTimeout(this._clickTimer);
let lastSpecificClick = this._lastClicked[0];
if (lastSpecificClick === specificSelection) {
sendSelectionEvent(event, selection, true, true);
return;
} else {
sendSelectionEvent(event, this._lastClicked, false, true);
}
}
this._lastClicked = selection;
this._clickTimer = setTimeout(() => {
sendSelectionEvent(event, selection, false, true);
}, 400);
} else {
clearTimeout(this._otherTimer);
this._otherTimer = setTimeout(() => {
sendSelectionEvent(event, selection, false, false);
}, 400);
}
}
/**
*
* @param connectionCompleteCallback A function that gets called after a connection is established due to the selection, if needed
*/
private handleTreeItemSelected(connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, isDoubleClick: boolean, isKeyboard: boolean, selection: any[], tree: ITree, connectionCompleteCallback: () => void): void {
let connectionProfile: ConnectionProfile = undefined;
let options: IConnectionCompletionOptions = {
params: undefined,
saveTheConnection: true,
showConnectionDialogOnError: true,
showFirewallRuleOnError: true,
showDashboard: isDoubleClick // only show the dashboard if the action is double click
};
if (selection && selection.length > 0 && (selection[0] instanceof ConnectionProfile)) {
connectionProfile = <ConnectionProfile>selection[0];
if (connectionProfile) {
this.onTreeActionStateChange(true);
TreeUpdateUtils.connectAndCreateOeSession(connectionProfile, options, connectionManagementService, objectExplorerService, tree).then(sessionCreated => {
if (!sessionCreated) {
this.onTreeActionStateChange(false);
}
if (connectionCompleteCallback) {
connectionCompleteCallback();
}
}, error => {
this.onTreeActionStateChange(false);
});
}
} else if (isDoubleClick && selection && selection.length > 0 && (selection[0] instanceof TreeNode)) {
let treeNode = selection[0];
if (TreeUpdateUtils.isAvailableDatabaseNode(treeNode)) {
connectionProfile = TreeUpdateUtils.getConnectionProfile(treeNode);
if (connectionProfile) {
connectionManagementService.showDashboard(connectionProfile);
}
}
}
if (isKeyboard) {
tree.toggleExpansion(selection[0]);
}
}
}

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { IConnectionManagementService, IConnectionCompletionOptions, IConnectionCallbacks } from 'sql/platform/connection/common/connectionManagement';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import * as errors from 'vs/base/common/errors';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { Disposable } from 'vs/base/common/lifecycle';
export interface IExpandableTree extends ITree {
// {{SQL CARBON EDIT }} - add back deleted VS Code tree methods
/**
* Returns a list of the currently expanded elements.
*/
getExpandedElements(): any[];
/**
* Returns a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way
* to the top; 1 means all the way down.
*/
getScrollPosition(): number;
/**
* Sets the scroll position with a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way
* to the top; 1 means all the way down.
*/
setScrollPosition(pos: number): void;
/**
* Returns the total height of the tree's content.
*/
getContentHeight(): number;
// {{SQL CARBON EDIT }} - end block
}
export class TreeUpdateUtils {
public static isInDragAndDrop: boolean = false;
/**
* Set input for the tree.
*/
public static structuralTreeUpdate(tree: ITree, viewKey: string, connectionManagementService: IConnectionManagementService, providers?: string[]): Promise<void> {
// convert to old VS Code tree interface with expandable methods
let expandableTree: IExpandableTree = <IExpandableTree>tree;
let selectedElement: any;
let targetsToExpand: any[];
if (tree) {
let selection = tree.getSelection();
if (selection && selection.length === 1) {
selectedElement = <any>selection[0];
}
targetsToExpand = expandableTree.getExpandedElements();
}
let groups;
let treeInput = new ConnectionProfileGroup('root', null, undefined, undefined, undefined);
if (viewKey === 'recent') {
groups = connectionManagementService.getRecentConnections(providers);
treeInput.addConnections(groups);
} else if (viewKey === 'active') {
groups = connectionManagementService.getActiveConnections(providers);
treeInput.addConnections(groups);
} else if (viewKey === 'saved') {
treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService, providers);
}
const previousTreeInput: any = tree.getInput();
return tree.setInput(treeInput).then(async () => {
if (previousTreeInput instanceof Disposable) {
previousTreeInput.dispose();
}
// Make sure to expand all folders that where expanded in the previous session
if (targetsToExpand) {
await tree.expandAll(targetsToExpand);
}
if (selectedElement) {
tree.select(selectedElement);
}
tree.getFocus();
});
}
/**
* Set input for the registered servers tree.
*/
public static registeredServerUpdate(tree: ITree, connectionManagementService: IConnectionManagementService, elementToSelect?: any): Promise<void> {
// convert to old VS Code tree interface with expandable methods
let expandableTree: IExpandableTree = <IExpandableTree>tree;
let selectedElement: any = elementToSelect;
let targetsToExpand: any[];
// Focus
tree.domFocus();
if (tree) {
let selection = tree.getSelection();
if (!selectedElement) {
if (selection && selection.length === 1) {
selectedElement = <any>selection[0];
}
}
targetsToExpand = expandableTree.getExpandedElements();
if (selectedElement && targetsToExpand.indexOf(selectedElement) === -1) {
targetsToExpand.push(selectedElement);
}
}
let treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService);
if (treeInput) {
if (treeInput !== tree.getInput()) {
return tree.setInput(treeInput).then(async () => {
// Make sure to expand all folders that where expanded in the previous session
if (targetsToExpand) {
await tree.expandAll(targetsToExpand);
}
if (selectedElement) {
tree.select(selectedElement);
}
tree.getFocus();
}, errors.onUnexpectedError);
}
}
return Promise.resolve();
}
public static getTreeInput(connectionManagementService: IConnectionManagementService, providers?: string[]): ConnectionProfileGroup {
let groups = connectionManagementService.getConnectionGroups(providers);
if (groups && groups.length > 0) {
let treeInput = groups[0];
treeInput.name = 'root';
groups.forEach(cpg => cpg.dispose());
return treeInput;
}
// Should never get to this case.
return undefined;
}
public static hasObjectExplorerNode(connection: ConnectionProfile, connectionManagementService: IConnectionManagementService): boolean {
let isConnected = connectionManagementService.isConnected(undefined, connection);
return isConnected;
}
public static async connectIfNotConnected(
connection: IConnectionProfile,
options: IConnectionCompletionOptions,
connectionManagementService: IConnectionManagementService,
tree: ITree): Promise<ConnectionProfile | undefined> {
if (!connectionManagementService.isProfileConnected(connection)) {
// don't try to reconnect if currently connecting
if (connectionManagementService.isProfileConnecting(connection)) {
return undefined;
// else if we aren't connected or connecting then try to connect
} else {
let callbacks: IConnectionCallbacks = undefined;
if (tree) {
// Show the spinner in OE by adding the 'loading' trait to the connection, and set up callbacks to hide the spinner
tree.addTraits('loading', [connection]);
let rejectOrCancelCallback = () => {
tree.collapse(connection);
tree.removeTraits('loading', [connection]);
};
callbacks = {
onConnectStart: undefined,
onConnectReject: rejectOrCancelCallback,
onConnectSuccess: () => tree.removeTraits('loading', [connection]),
onDisconnect: undefined,
onConnectCanceled: rejectOrCancelCallback,
};
}
const result = await connectionManagementService.connect(connection, undefined, options, callbacks);
if (result.connected) {
let existingConnection = connectionManagementService.findExistingConnection(connection);
return existingConnection;
} else {
throw new Error('connection failed');
}
}
} else {
let existingConnection = connectionManagementService.findExistingConnection(connection);
if (options && options.showDashboard) {
await connectionManagementService.showDashboard(connection);
}
return existingConnection;
}
}
/**
* Makes a connection if the not already connected and try to create new object explorer session
* I the profile is already connected, tries to do the action requested in the options (e.g. open dashboard)
* Returns true if new object explorer session created for the connection, otherwise returns false
* @param connection Connection Profile
* @param options Includes the actions to happened after connection is made
* @param connectionManagementService Connection management service instance
* @param objectExplorerService Object explorer service instance
*/
public static async connectAndCreateOeSession(connection: IConnectionProfile, options: IConnectionCompletionOptions,
connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, tree: ITree): Promise<boolean> {
const connectedConnection = await TreeUpdateUtils.connectIfNotConnected(connection, options, connectionManagementService, tree);
if (connectedConnection) {
// append group ID and original display name to build unique OE session ID
connectedConnection.options['groupId'] = connection.groupId;
connectedConnection.options['databaseDisplayName'] = connection.databaseName;
let rootNode: TreeNode = objectExplorerService.getObjectExplorerNode(connectedConnection);
if (!rootNode) {
await objectExplorerService.updateObjectExplorerNodes(connectedConnection);
rootNode = objectExplorerService.getObjectExplorerNode(connectedConnection);
return true;
// The oe request is sent. an event will be raised when the session is created
} else {
return false;
}
} else {
return false;
}
}
public static getObjectExplorerNode(connection: ConnectionProfile, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService): Promise<TreeNode[]> {
return new Promise<TreeNode[]>((resolve, reject) => {
if (connection.isDisconnecting) {
resolve([]);
} else {
let rootNode = objectExplorerService.getObjectExplorerNode(connection);
if (rootNode) {
objectExplorerService.resolveTreeNodeChildren(rootNode.getSession(), rootNode).then(() => {
resolve(rootNode.children);
}, expandError => {
resolve([]);
});
} else {
resolve([]);
}
}
});
}
public static getObjectExplorerParent(objectExplorerNode: TreeNode, connectionManagementService: IConnectionManagementService): any {
if (objectExplorerNode && objectExplorerNode.parent) {
// if object explorer node's parent is root, return connection profile
if (!objectExplorerNode.parent.parent) {
let connectionId = objectExplorerNode.getConnectionProfile().id;
// get connection profile from connection profile groups
let root = TreeUpdateUtils.getTreeInput(connectionManagementService);
let connections = ConnectionProfileGroup.getConnectionsInGroup(root);
let results = connections.filter(con => {
if (connectionId === con.id) {
return true;
} else {
return false;
}
});
if (results && results.length > 0) {
return results[0];
}
} else {
return objectExplorerNode.parent;
}
}
return null;
}
/**
*
* @param treeNode Returns true if the tree node is a database node
*/
public static isDatabaseNode(treeNode: TreeNode): boolean {
return treeNode && treeNode.nodeTypeId === NodeType.Database;
}
/**
*
* @param treeNode Returns true if the tree node is an available database node
*/
public static isAvailableDatabaseNode(treeNode: TreeNode): boolean {
return treeNode && treeNode.nodeTypeId === NodeType.Database && treeNode.nodeStatus !== 'Unavailable';
}
/**
* Get connection profile with the current database
*/
public static getConnectionProfile(treeNode: TreeNode): ConnectionProfile {
let connectionProfile = treeNode.getConnectionProfile();
let databaseName = treeNode.getDatabaseName();
if (databaseName !== undefined && connectionProfile.databaseName !== databaseName) {
connectionProfile = connectionProfile.cloneWithDatabase(databaseName);
}
return connectionProfile;
}
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class NodeType {
public static Folder = 'Folder';
public static Root = 'root';
public static Database = 'Database';
public static Server = 'Server';
public static ScalarValuedFunction = 'ScalarValuedFunction';
public static TableValuedFunction = 'TableValuedFunction';
public static AggregateFunction = 'AggregateFunction';
public static FileGroup = 'FileGroup';
public static StoredProcedure = 'StoredProcedure';
public static UserDefinedTableType = 'UserDefinedTableType';
public static View = 'View';
public static Table = 'Table';
public static HistoryTable = 'HistoryTable';
public static ServerLevelLinkedServerLogin = 'ServerLevelLinkedServerLogin';
public static ServerLevelServerAudit = 'ServerLevelServerAudit';
public static ServerLevelCryptographicProvider = 'ServerLevelCryptographicProvider';
public static ServerLevelCredential = 'ServerLevelCredential';
public static ServerLevelServerRole = 'ServerLevelServerRole';
public static ServerLevelLogin = 'ServerLevelLogin';
public static ServerLevelServerAuditSpecification = 'ServerLevelServerAuditSpecification';
public static ServerLevelServerTrigger = 'ServerLevelServerTrigger';
public static ServerLevelLinkedServer = 'ServerLevelLinkedServer';
public static ServerLevelEndpoint = 'ServerLevelEndpoint';
public static Synonym = 'Synonym';
public static DatabaseTrigger = 'DatabaseTrigger';
public static Assembly = 'Assembly';
public static MessageType = 'MessageType';
public static Contract = 'Contract';
public static Queue = 'Queue';
public static Service = 'Service';
public static Route = 'Route';
public static DatabaseAndQueueEventNotification = 'DatabaseAndQueueEventNotification';
public static RemoteServiceBinding = 'RemoteServiceBinding';
public static BrokerPriority = 'BrokerPriority';
public static FullTextCatalog = 'FullTextCatalog';
public static FullTextStopList = 'FullTextStopList';
public static SqlLogFile = 'SqlLogFile';
public static PartitionFunction = 'PartitionFunction';
public static PartitionScheme = 'PartitionScheme';
public static SearchPropertyList = 'SearchPropertyList';
public static User = 'User';
public static Schema = 'Schema';
public static AsymmetricKey = 'AsymmetricKey';
public static Certificate = 'Certificate';
public static SymmetricKey = 'SymmetricKey';
public static DatabaseEncryptionKey = 'DatabaseEncryptionKey';
public static MasterKey = 'MasterKey';
public static DatabaseAuditSpecification = 'DatabaseAuditSpecification';
public static Column = 'Column';
public static Key = 'Key';
public static Constraint = 'Constraint';
public static Trigger = 'Trigger';
public static Index = 'Index';
public static Statistic = 'Statistic';
public static UserDefinedDataType = 'UserDefinedDataType';
public static UserDefinedType = 'UserDefinedType';
public static XmlSchemaCollection = 'XmlSchemaCollection';
public static SystemExactNumeric = 'SystemExactNumeric';
public static SystemApproximateNumeric = 'SystemApproximateNumeric';
public static SystemDateAndTime = 'SystemDateAndTime';
public static SystemCharacterString = 'SystemCharacterString';
public static SystemUnicodeCharacterString = 'SystemUnicodeCharacterString';
public static SystemBinaryString = 'SystemBinaryString';
public static SystemOtherDataType = 'SystemOtherDataType';
public static SystemClrDataType = 'SystemClrDataType';
public static SystemSpatialDataType = 'SystemSpatialDataType';
public static UserDefinedTableTypeColumn = 'UserDefinedTableTypeColumn';
public static UserDefinedTableTypeKey = 'UserDefinedTableTypeKey';
public static UserDefinedTableTypeConstraint = 'UserDefinedTableTypeConstraint';
public static StoredProcedureParameter = 'StoredProcedureParameter';
public static TableValuedFunctionParameter = 'TableValuedFunctionParameter';
public static ScalarValuedFunctionParameter = 'ScalarValuedFunctionParameter';
public static AggregateFunctionParameter = 'AggregateFunctionParameter';
public static DatabaseRole = 'DatabaseRole';
public static ApplicationRole = 'ApplicationRole';
public static FileGroupFile = 'FileGroupFile';
public static SystemMessageType = 'SystemMessageType';
public static SystemContract = 'SystemContract';
public static SystemService = 'SystemService';
public static SystemQueue = 'SystemQueue';
public static Sequence = 'Sequence';
public static SecurityPolicy = 'SecurityPolicy';
public static DatabaseScopedCredential = 'DatabaseScopedCredential';
public static ExternalResource = 'ExternalResource';
public static ExternalDataSource = 'ExternalDataSource';
public static ExternalFileFormat = 'ExternalFileFormat';
public static ExternalTable = 'ExternalTable';
public static ColumnMasterKey = 'ColumnMasterKey';
public static ColumnEncryptionKey = 'ColumnEncryptionKey';
public static readonly SCRIPTABLE_OBJECTS = [NodeType.Table, NodeType.View, NodeType.Schema, NodeType.User, NodeType.UserDefinedTableType,
NodeType.StoredProcedure, NodeType.AggregateFunction, NodeType.PartitionFunction, NodeType.ScalarValuedFunction,
NodeType.TableValuedFunction];
}
export interface SqlThemeIcon {
readonly id: string;
}

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationRegistry, Extensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
export const SERVER_GROUP_CONFIG = 'serverGroup';
export const SERVER_GROUP_COLORS_CONFIG = 'colors';
export const SERVER_GROUP_AUTOEXPAND_CONFIG = 'autoExpand';
const serverGroupConfig: IConfigurationNode = {
id: 'Server Groups',
type: 'object',
properties: {
[SERVER_GROUP_CONFIG + '.' + SERVER_GROUP_COLORS_CONFIG]: <IJSONSchema>{
type: 'array',
items: 'string',
'description': localize('serverGroup.colors', "Server Group color palette used in the Object Explorer viewlet."),
default: [
'#A1634D',
'#7F0000',
'#914576',
'#85AE72',
'#98AFC7',
'#4452A6',
'#6A6599',
'#515151'
]
},
[SERVER_GROUP_CONFIG + '.' + SERVER_GROUP_AUTOEXPAND_CONFIG]: {
'type': 'boolean',
'description': localize('serverGroup.autoExpand', "Auto-expand Server Groups in the Object Explorer viewlet."),
'default': 'true'
},
}
};
configurationRegistry.registerConfiguration(serverGroupConfig);

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import * as TypeChecker from 'vs/base/common/types';
import { localize } from 'vs/nls';
import * as strings from 'vs/base/common/strings';
export class ServerGroupViewModel {
public groupName: string;
public groupDescription: string;
public groupColor: string;
public colors: string[] = ['#515151', '#004760', '#771b00', '#700060', '#a17d01', '#006749', '#654502', '#3A0293'];
private _domainModel: IConnectionProfileGroup;
private _editMode: boolean;
private readonly _addServerGroupTitle: string = localize('serverGroup.addServerGroup', "Add server group");
private readonly _editServerGroupTitle: string = localize('serverGroup.editServerGroup', "Edit server group");
private readonly _defaultColor: string = '#515151';
constructor(domainModel?: IConnectionProfileGroup, colors?: string[]) {
// keep reference to domain model to be able to see if there are pending changes
if (domainModel) {
this._domainModel = domainModel;
// initialize the view model properties
this.groupName = domainModel.name;
this.groupColor = domainModel.color;
this.groupDescription = domainModel.description;
this._editMode = true;
}
else {
// initialize defaults for a new group
this.groupName = '';
this.groupDescription = '';
this.groupColor = this._defaultColor;
this._editMode = false;
}
if (colors) {
this.colors = colors;
}
}
// check to see if the current state of the view model is different than the data in the domain model
public hasPendingChanges(): boolean {
if (!TypeChecker.isUndefinedOrNull(this._domainModel)) {
return ((strings.isFalsyOrWhitespace(this.groupName) === false) &&
((this.groupName !== this._domainModel.name) ||
(this.groupDescription !== this._domainModel.description) ||
(this.groupColor !== this._domainModel.color)));
}
else {
return (strings.isFalsyOrWhitespace(this.groupName) === false);
}
}
public getDialogTitle(): string {
if (this._editMode === true) {
return this._editServerGroupTitle;
} else {
return this._addServerGroupTitle;
}
}
}

View File

@@ -0,0 +1,178 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { NodeType, SqlThemeIcon } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
import * as azdata from 'sqlops';
import * as UUID from 'vs/base/common/uuid';
import { URI } from 'vs/base/common/uri';
export enum TreeItemCollapsibleState {
None = 0,
Collapsed = 1,
Expanded = 2
}
export interface ObjectExplorerCallbacks {
getChildren(treeNode: TreeNode): Thenable<TreeNode[]>;
isExpanded(treeNode: TreeNode): Thenable<boolean>;
setNodeExpandedState(TreeNode: TreeNode, expandedState: TreeItemCollapsibleState): Thenable<void>;
setNodeSelected(TreeNode: TreeNode, selected: boolean, clearOtherSelections?: boolean): Thenable<void>;
}
export class TreeNode {
/**
* Informs who provides the children to a node, used by data explorer tree view api
*/
public childProvider: string;
/**
* Holds the connection profile for nodes, used by data explorer tree view api
*/
public payload: any;
/**
* id for TreeNode
*/
public id: string;
/**
* string defining the type of the node - for example Server, Database, Folder, Table
*/
public nodeTypeId: string;
/**
* Label to display to the user, describing this node
*/
public label: string;
/**
* Is this a leaf node (in which case no children can be generated) or is it expandable?
*/
public isAlwaysLeaf: boolean;
/**
* Message to show if this Node is in an error state. This indicates
* that children could be retrieved
*/
public errorStateMessage: string;
/**
* Parent of this node
*/
public parent: TreeNode;
/**
* Path identifying this node
*/
public nodePath: string;
/**
* Node sub type
*/
public nodeSubType: string;
/**
* Node Status
*/
public nodeStatus: string;
/**
* Children of this node
*/
public children: TreeNode[];
public connection: ConnectionProfile;
public session: azdata.ObjectExplorerSession;
public metadata: azdata.ObjectMetadata;
public iconType: string | SqlThemeIcon;
public iconPath: URI | { light: URI, dark: URI };
constructor(nodeTypeId: string, label: string, isAlwaysLeaf: boolean, nodePath: string,
nodeSubType: string, nodeStatus: string, parent: TreeNode, metadata: azdata.ObjectMetadata,
iconType: string | SqlThemeIcon,
private _objectExplorerCallbacks: ObjectExplorerCallbacks) {
this.nodeTypeId = nodeTypeId;
this.label = label;
this.isAlwaysLeaf = isAlwaysLeaf;
this.nodePath = nodePath;
this.parent = parent;
this.metadata = metadata;
this.iconType = iconType;
this.id = UUID.generateUuid();
this.nodeSubType = nodeSubType;
this.nodeStatus = nodeStatus;
}
public getConnectionProfile(): ConnectionProfile {
let currentNode: TreeNode = this;
while (!currentNode.connection && currentNode.parent) {
currentNode = currentNode.parent;
}
return currentNode.connection;
}
public getDatabaseName(): string {
if (this.connection) {
return undefined;
}
let currentNode: TreeNode = this;
while (currentNode.nodeTypeId !== NodeType.Database && currentNode.nodeTypeId !== NodeType.Server && currentNode.parent) {
currentNode = currentNode.parent;
}
if (currentNode && currentNode.nodeTypeId === NodeType.Database) {
return currentNode.metadata ? currentNode.metadata.name : null;
}
return undefined;
}
public getSession(): azdata.ObjectExplorerSession {
let currentNode: TreeNode = this;
while (!currentNode.session && currentNode.parent) {
currentNode = currentNode.parent;
}
return currentNode.session;
}
public isTopLevel(): boolean {
if (this.parent && this.parent.nodeTypeId === NodeType.Root) {
return true;
}
return false;
}
public toNodeInfo(): azdata.NodeInfo {
return <azdata.NodeInfo>{
nodePath: this.nodePath,
nodeType: this.nodeTypeId,
nodeSubType: this.nodeSubType,
nodeStatus: this.nodeStatus,
label: this.label,
isLeaf: this.isAlwaysLeaf,
metadata: this.metadata,
errorMessage: this.errorStateMessage
};
}
public getChildren(): Thenable<TreeNode[]> {
return this._objectExplorerCallbacks.getChildren(this);
}
public isExpanded(): Thenable<boolean> {
return this._objectExplorerCallbacks.isExpanded(this);
}
public setExpandedState(expandedState: TreeItemCollapsibleState): Thenable<void> {
return this._objectExplorerCallbacks.setNodeExpandedState(this, expandedState);
}
public setSelected(selected: boolean, clearOtherSelections?: boolean): Thenable<void> {
return this._objectExplorerCallbacks.setNodeSelected(this, selected, clearOtherSelections);
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
export class TreeNodeContextKey implements IContextKey<TreeNode> {
static NodeType = new RawContextKey<string>('nodeType', undefined);
static SubType = new RawContextKey<string>('nodeSubType', undefined);
static Status = new RawContextKey<string>('nodeStatus', undefined);
static TreeNode = new RawContextKey<TreeNode>('treeNode', undefined);
static NodeLabel = new RawContextKey<string>('nodeLabel', undefined);
private _nodeTypeKey: IContextKey<string>;
private _subTypeKey: IContextKey<string>;
private _statusKey: IContextKey<string>;
private _treeNodeKey: IContextKey<TreeNode>;
private _nodeLabelKey: IContextKey<string>;
constructor(
@IContextKeyService contextKeyService: IContextKeyService
) {
this._nodeTypeKey = TreeNodeContextKey.NodeType.bindTo(contextKeyService);
this._subTypeKey = TreeNodeContextKey.SubType.bindTo(contextKeyService);
this._statusKey = TreeNodeContextKey.Status.bindTo(contextKeyService);
this._treeNodeKey = TreeNodeContextKey.TreeNode.bindTo(contextKeyService);
this._nodeLabelKey = TreeNodeContextKey.NodeLabel.bindTo(contextKeyService);
}
set(value: TreeNode) {
this._treeNodeKey.set(value);
this._nodeTypeKey.set(value && value.nodeTypeId);
this._subTypeKey.set(value && value.nodeSubType);
this._statusKey.set(value && value.nodeStatus);
this._nodeLabelKey.set(value && value.label);
}
reset(): void {
this._nodeTypeKey.reset();
this._subTypeKey.reset();
this._statusKey.reset();
this._treeNodeKey.reset();
this._nodeLabelKey.reset();
}
public get(): TreeNode {
return this._treeNodeKey.get();
}
}

View File

@@ -0,0 +1,523 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as TypeMoq from 'typemoq';
import * as assert from 'assert';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import {
RefreshAction, AddServerAction, DeleteConnectionAction, DisconnectConnectionAction,
ActiveConnectionsFilterAction, RecentConnectionsFilterAction
}
from 'sql/workbench/contrib/objectExplorer/browser/connectionTreeAction';
import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService';
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServerTreeView } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeView';
import * as LocalizedConstants from 'sql/workbench/contrib/connection/common/localizedConstants';
import { ObjectExplorerService, ObjectExplorerNodeEventArgs } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode';
import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ServerTreeDataSource } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeDataSource';
import { Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
import { ObjectExplorerActionsContext } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions';
import { IConnectionResult, IConnectionParams } from 'sql/platform/connection/common/connectionManagement';
import { TreeSelectionHandler } from 'sql/workbench/contrib/objectExplorer/browser/treeSelectionHandler';
import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService';
import { UNSAVED_GROUP_ID, mssqlProviderName } from 'sql/platform/connection/common/constants';
import { $ } from 'vs/base/browser/dom';
import { OEManageConnectionAction } from 'sql/workbench/contrib/dashboard/browser/dashboardActions';
import { IViewsService, IView, ViewContainer, IViewDescriptorCollection } from 'vs/workbench/common/views';
import { ConsoleLogService } from 'vs/platform/log/common/log';
suite('SQL Connection Tree Action tests', () => {
let errorMessageService: TypeMoq.Mock<TestErrorMessageService>;
let connectionResult: IConnectionResult = {
connected: true,
errorMessage: undefined,
errorCode: undefined,
callStack: undefined
};
let capabilitiesService = new TestCapabilitiesService();
const logService = new ConsoleLogService();
setup(() => {
errorMessageService = TypeMoq.Mock.ofType(TestErrorMessageService, TypeMoq.MockBehavior.Loose);
let nothing: void;
errorMessageService.setup(x => x.showDialog(Severity.Error, TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => nothing);
});
function createConnectionManagementService(isConnectedReturnValue: boolean, profileToReturn: ConnectionProfile): TypeMoq.Mock<TestConnectionManagementService> {
let connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.callBase = true;
connectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => isConnectedReturnValue);
connectionManagementService.setup(x => x.connect(TypeMoq.It.isAny(), undefined, TypeMoq.It.isAny(), undefined)).returns(() => Promise.resolve(connectionResult));
connectionManagementService.setup(x => x.disconnect(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
connectionManagementService.setup(x => x.findExistingConnection(TypeMoq.It.isAny())).returns(() => undefined);
connectionManagementService.setup(x => x.showDashboard(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
connectionManagementService.setup(x => x.isProfileConnected(TypeMoq.It.isAny())).returns(() => isConnectedReturnValue);
connectionManagementService.setup(x => x.isProfileConnecting(TypeMoq.It.isAny())).returns(() => false);
connectionManagementService.setup(x => x.showConnectionDialog(undefined, undefined, TypeMoq.It.isAny())).returns(() => new Promise<void>((resolve, reject) => resolve()));
connectionManagementService.setup(x => x.onConnect).returns(() => new Emitter<IConnectionParams>().event);
connectionManagementService.setup(x => x.onDisconnect).returns(() => new Emitter<any>().event);
connectionManagementService.setup(x => x.deleteConnectionGroup(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
connectionManagementService.setup(x => x.deleteConnection(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
connectionManagementService.setup(x => x.getConnectionProfile(TypeMoq.It.isAny())).returns(() => profileToReturn);
return connectionManagementService;
}
function createObjectExplorerService(connectionManagementService: TestConnectionManagementService, getTreeNodeReturnVal: TreeNode): TypeMoq.Mock<ObjectExplorerService> {
let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Strict, connectionManagementService);
objectExplorerService.callBase = true;
objectExplorerService.setup(x => x.getTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(getTreeNodeReturnVal));
objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => new TreeNode('', '', false, '', '', '', undefined, undefined, undefined, undefined));
objectExplorerService.setup(x => x.getObjectExplorerNode(undefined)).returns(() => new TreeNode('', '', false, '', '', '', undefined, undefined, undefined, undefined));
objectExplorerService.setup(x => x.onUpdateObjectExplorerNodes).returns(() => new Emitter<ObjectExplorerNodeEventArgs>().event);
objectExplorerService.setup(x => x.onUpdateObjectExplorerNodes).returns(() => new Emitter<ObjectExplorerNodeEventArgs>().event);
return objectExplorerService;
}
test('ManageConnectionAction - test if connect is called for manage action if not already connected', (done) => {
let isConnectedReturnValue: boolean = false;
let connection: ConnectionProfile = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'integrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testId'
});
let connectionManagementService = createConnectionManagementService(isConnectedReturnValue, connection);
let objectExplorerService = createObjectExplorerService(connectionManagementService.object, undefined);
let treeSelectionMock = TypeMoq.Mock.ofType(TreeSelectionHandler);
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Loose);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny())).returns((input) => {
return treeSelectionMock.object;
});
const viewsService = new class implements IViewsService {
_serviceBrand: undefined;
openView(id: string, focus?: boolean): Promise<IView> {
return Promise.resolve({
id: '',
serversTree: undefined
});
}
getViewDescriptors(container: ViewContainer): IViewDescriptorCollection {
throw new Error('Method not implemented.');
}
};
let manageConnectionAction: OEManageConnectionAction = new OEManageConnectionAction(OEManageConnectionAction.ID,
OEManageConnectionAction.LABEL, connectionManagementService.object, capabilitiesService, instantiationService.object, objectExplorerService.object, viewsService);
let actionContext = new ObjectExplorerActionsContext();
actionContext.connectionProfile = connection.toIConnectionProfile();
actionContext.isConnectionNode = true;
manageConnectionAction.run(actionContext).then((value) => {
connectionManagementService.verify(x => x.connect(TypeMoq.It.isAny(), undefined, TypeMoq.It.isAny(), undefined), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('ManageConnectionAction - test if connect is called for manage action on database node if not already connected', (done) => {
let isConnectedReturnValue: boolean = false;
let connection: ConnectionProfile = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'integrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testId'
});
let treeNode = new TreeNode(NodeType.Database, 'db node', false, '', '', '', undefined, undefined, undefined, undefined);
treeNode.connection = connection;
let connectionManagementService = createConnectionManagementService(isConnectedReturnValue, connection);
let objectExplorerService = createObjectExplorerService(connectionManagementService.object, treeNode);
let treeSelectionMock = TypeMoq.Mock.ofType(TreeSelectionHandler);
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Loose);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny())).returns((input) => {
return treeSelectionMock.object;
});
let manageConnectionAction: OEManageConnectionAction = new OEManageConnectionAction(OEManageConnectionAction.ID,
OEManageConnectionAction.LABEL, connectionManagementService.object, capabilitiesService, instantiationService.object, objectExplorerService.object, undefined);
let actionContext = new ObjectExplorerActionsContext();
actionContext.connectionProfile = connection.toIConnectionProfile();
actionContext.nodeInfo = treeNode.toNodeInfo();
manageConnectionAction.run(actionContext).then((value) => {
connectionManagementService.verify(x => x.showDashboard(TypeMoq.It.isAny()), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('DisconnectConnectionAction - test if disconnect is called when profile is connected', (done) => {
let isConnectedReturnValue: boolean = true;
let connection: ConnectionProfile = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'integrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testId'
});
let connectionManagementService = createConnectionManagementService(isConnectedReturnValue, connection);
let changeConnectionAction: DisconnectConnectionAction = new DisconnectConnectionAction(DisconnectConnectionAction.ID, DisconnectConnectionAction.LABEL, connection, connectionManagementService.object);
let actionContext = new ObjectExplorerActionsContext();
actionContext.connectionProfile = connection.toIConnectionProfile();
changeConnectionAction.run(actionContext).then((value) => {
connectionManagementService.verify(x => x.isProfileConnected(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
connectionManagementService.verify(x => x.disconnect(TypeMoq.It.isAny()), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('AddServerAction - test if show connection dialog is called', (done) => {
let connectionManagementService = createConnectionManagementService(true, undefined);
let connectionTreeAction: AddServerAction = new AddServerAction(AddServerAction.ID, AddServerAction.LABEL, connectionManagementService.object);
let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined);
connectionTreeAction.run(conProfGroup).then((value) => {
connectionManagementService.verify(x => x.showConnectionDialog(undefined, undefined, TypeMoq.It.isAny()), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('ActiveConnectionsFilterAction - test if view is called to display filtered results', (done) => {
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Loose);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny())).returns((input) => {
return new Promise((resolve) => resolve({}));
});
let serverTreeView = TypeMoq.Mock.ofType(ServerTreeView, TypeMoq.MockBehavior.Strict, undefined, instantiationService.object, undefined, undefined, undefined, undefined, capabilitiesService);
serverTreeView.setup(x => x.showFilteredTree(TypeMoq.It.isAnyString()));
serverTreeView.setup(x => x.refreshTree());
let connectionTreeAction: ActiveConnectionsFilterAction = new ActiveConnectionsFilterAction(ActiveConnectionsFilterAction.ID, ActiveConnectionsFilterAction.LABEL, serverTreeView.object);
connectionTreeAction.run().then((value) => {
serverTreeView.verify(x => x.showFilteredTree('active'), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('ActiveConnectionsFilterAction - test if view is called refresh results if action is toggled', (done) => {
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Loose);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny())).returns((input) => {
return new Promise((resolve) => resolve({}));
});
let serverTreeView = TypeMoq.Mock.ofType(ServerTreeView, TypeMoq.MockBehavior.Strict, undefined, instantiationService.object, undefined, undefined, undefined, undefined, capabilitiesService);
serverTreeView.setup(x => x.showFilteredTree(TypeMoq.It.isAnyString()));
serverTreeView.setup(x => x.refreshTree());
let connectionTreeAction: ActiveConnectionsFilterAction = new ActiveConnectionsFilterAction(ActiveConnectionsFilterAction.ID, ActiveConnectionsFilterAction.LABEL, serverTreeView.object);
connectionTreeAction.isSet = true;
connectionTreeAction.run().then((value) => {
serverTreeView.verify(x => x.refreshTree(), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('RecentConnectionsFilterAction - test if view is called to display filtered results', (done) => {
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Loose);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny())).returns((input) => {
return new Promise((resolve) => resolve({}));
});
let serverTreeView = TypeMoq.Mock.ofType(ServerTreeView, TypeMoq.MockBehavior.Strict, undefined, instantiationService.object, undefined, undefined, undefined, undefined, capabilitiesService);
serverTreeView.setup(x => x.showFilteredTree(TypeMoq.It.isAnyString()));
serverTreeView.setup(x => x.refreshTree());
let connectionTreeAction: RecentConnectionsFilterAction = new RecentConnectionsFilterAction(RecentConnectionsFilterAction.ID, RecentConnectionsFilterAction.LABEL, serverTreeView.object);
connectionTreeAction.run().then((value) => {
serverTreeView.verify(x => x.showFilteredTree('recent'), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('RecentConnectionsFilterAction - test if view is called refresh results if action is toggled', (done) => {
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Loose);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isAny())).returns((input) => {
return new Promise((resolve) => resolve({}));
});
let serverTreeView = TypeMoq.Mock.ofType(ServerTreeView, TypeMoq.MockBehavior.Strict, undefined, instantiationService.object, undefined, undefined, undefined, undefined, capabilitiesService);
serverTreeView.setup(x => x.showFilteredTree(TypeMoq.It.isAnyString()));
serverTreeView.setup(x => x.refreshTree());
let connectionTreeAction: RecentConnectionsFilterAction = new RecentConnectionsFilterAction(RecentConnectionsFilterAction.ID, RecentConnectionsFilterAction.LABEL, serverTreeView.object);
connectionTreeAction.isSet = true;
connectionTreeAction.run().then((value) => {
serverTreeView.verify(x => x.refreshTree(), TypeMoq.Times.once());
}).then(() => done(), (err) => done(err));
});
test('DeleteConnectionAction - test delete connection', (done) => {
let connectionManagementService = createConnectionManagementService(true, undefined);
let connection: ConnectionProfile = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'integrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testId'
});
let connectionAction: DeleteConnectionAction = new DeleteConnectionAction(DeleteConnectionAction.ID,
DeleteConnectionAction.DELETE_CONNECTION_LABEL,
connection,
connectionManagementService.object);
connectionAction.run().then((value) => {
connectionManagementService.verify(x => x.deleteConnection(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
}).then(() => done(), (err) => done(err));
});
test('DeleteConnectionAction - test delete connection group', (done) => {
let isConnectedReturnValue: boolean = false;
let connectionManagementService = createConnectionManagementService(isConnectedReturnValue, undefined);
let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined);
let connectionAction: DeleteConnectionAction = new DeleteConnectionAction(DeleteConnectionAction.ID,
DeleteConnectionAction.DELETE_CONNECTION_LABEL,
conProfGroup,
connectionManagementService.object);
connectionAction.run().then((value) => {
connectionManagementService.verify(x => x.deleteConnectionGroup(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
}).then(() => done(), (err) => done(err));
});
test('DeleteConnectionAction - delete should not be called if connect is an unsaved connection', (done) => {
let isConnectedReturnValue: boolean = false;
let connectionManagementService = createConnectionManagementService(isConnectedReturnValue, undefined);
let connection: ConnectionProfile = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'integrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testId'
});
connection.parent = new ConnectionProfileGroup(LocalizedConstants.unsavedGroupLabel, undefined, UNSAVED_GROUP_ID, undefined, undefined);
let connectionAction: DeleteConnectionAction = new DeleteConnectionAction(DeleteConnectionAction.ID,
DeleteConnectionAction.DELETE_CONNECTION_LABEL,
connection,
connectionManagementService.object);
assert.equal(connectionAction.enabled, false, 'delete action should be disabled.');
done();
});
test('RefreshConnectionAction - refresh should be called if connection status is connect', (done) => {
let isConnectedReturnValue: boolean = true;
let sqlProvider = {
providerId: mssqlProviderName,
displayName: 'MSSQL',
connectionOptions: [],
};
capabilitiesService.capabilities[mssqlProviderName] = { connection: sqlProvider };
let connection = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'inetgrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testID'
});
let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined);
conProfGroup.connections = [connection];
let connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.callBase = true;
connectionManagementService.setup(x => x.getConnectionGroups()).returns(() => [conProfGroup]);
connectionManagementService.setup(x => x.getActiveConnections()).returns(() => [connection]);
connectionManagementService.setup(x => x.addSavedPassword(TypeMoq.It.isAny())).returns(() => new Promise<ConnectionProfile>((resolve) => {
resolve(connection);
}));
connectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => isConnectedReturnValue);
let objectExplorerSession = {
success: true,
sessionId: '1234',
rootNode: {
nodePath: 'testServerName\tables',
nodeType: NodeType.Folder,
label: 'Tables',
isLeaf: false,
metadata: null,
nodeSubType: '',
nodeStatus: '',
errorMessage: ''
},
errorMessage: ''
};
let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null, undefined, undefined);
tablesNode.connection = connection;
tablesNode.session = objectExplorerSession;
let table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined);
let table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined);
tablesNode.children = [table1Node, table2Node];
let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Loose, connectionManagementService.object);
objectExplorerService.callBase = true;
objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => tablesNode);
objectExplorerService.setup(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([table1Node, table2Node]));
let dataSource = new ServerTreeDataSource(objectExplorerService.object, connectionManagementService.object, undefined);
let tree = TypeMoq.Mock.ofType(Tree, TypeMoq.MockBehavior.Loose, $('div'), { dataSource });
tree.callBase = true;
tree.setup(x => x.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve(null));
tree.setup(x => x.expand(TypeMoq.It.isAny())).returns(() => Promise.resolve(null));
tree.setup(x => x.collapse(TypeMoq.It.isAny())).returns(() => Promise.resolve(null));
let connectionAction: RefreshAction = new RefreshAction(RefreshAction.ID,
RefreshAction.LABEL,
tree.object,
connection,
connectionManagementService.object,
objectExplorerService.object,
undefined,
logService);
connectionAction.run().then((value) => {
connectionManagementService.verify(x => x.isConnected(undefined, TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
objectExplorerService.verify(x => x.getObjectExplorerNode(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
objectExplorerService.verify(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
tree.verify(x => x.refresh(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
tree.verify(x => x.expand(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
}).then(() => done(), (err) => {
done(err);
});
});
test('RefreshConnectionAction - refresh should not be called if connection status is not connect', (done) => {
let isConnectedReturnValue: boolean = false;
let sqlProvider = {
providerId: mssqlProviderName,
displayName: 'MSSQL',
connectionOptions: []
};
capabilitiesService.capabilities[mssqlProviderName] = { connection: sqlProvider };
let connection = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'testServerName',
databaseName: 'testDatabaseName',
authenticationType: 'inetgrated',
password: 'test',
userName: 'testUsername',
groupId: undefined,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: 'testID'
});
let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined);
conProfGroup.connections = [connection];
let connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.callBase = true;
connectionManagementService.setup(x => x.getConnectionGroups()).returns(() => [conProfGroup]);
connectionManagementService.setup(x => x.getActiveConnections()).returns(() => [connection]);
connectionManagementService.setup(x => x.addSavedPassword(TypeMoq.It.isAny())).returns(() => new Promise<ConnectionProfile>((resolve) => {
resolve(connection);
}));
connectionManagementService.setup(x => x.isConnected(undefined, TypeMoq.It.isAny())).returns(() => isConnectedReturnValue);
let objectExplorerSession = {
success: true,
sessionId: '1234',
rootNode: {
nodePath: 'testServerName\tables',
nodeType: NodeType.Folder,
label: 'Tables',
isLeaf: false,
metadata: null,
nodeSubType: '',
nodeStatus: '',
errorMessage: ''
},
errorMessage: ''
};
let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null, undefined, undefined);
tablesNode.connection = connection;
tablesNode.session = objectExplorerSession;
let table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined);
let table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined, undefined);
tablesNode.children = [table1Node, table2Node];
let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Loose, connectionManagementService.object);
objectExplorerService.callBase = true;
objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => tablesNode);
objectExplorerService.setup(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([table1Node, table2Node]));
let dataSource = new ServerTreeDataSource(objectExplorerService.object, connectionManagementService.object, undefined);
let tree = TypeMoq.Mock.ofType(Tree, TypeMoq.MockBehavior.Loose, $('div'), { dataSource });
tree.callBase = true;
tree.setup(x => x.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve(null));
tree.setup(x => x.expand(TypeMoq.It.isAny())).returns(() => Promise.resolve(null));
let connectionAction: RefreshAction = new RefreshAction(RefreshAction.ID,
RefreshAction.LABEL,
tree.object,
connection,
connectionManagementService.object,
objectExplorerService.object,
undefined,
logService);
connectionAction.run().then((value) => {
connectionManagementService.verify(x => x.isConnected(undefined, TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
objectExplorerService.verify(x => x.getObjectExplorerNode(TypeMoq.It.isAny()), TypeMoq.Times.exactly(0));
objectExplorerService.verify(x => x.refreshTreeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(0));
tree.verify(x => x.refresh(TypeMoq.It.isAny()), TypeMoq.Times.exactly(0));
tree.verify(x => x.expand(TypeMoq.It.isAny()), TypeMoq.Times.exactly(0));
}).then(() => done(), (err) => done(err));
});
});

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* 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 'sql/platform/connection/common/interfaces';
import { ServerTreeView } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeView';
import { ConnectionManagementService } from 'sql/workbench/services/connection/browser/connectionManagementService';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import * as TypeMoq from 'typemoq';
import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService';
suite('ServerTreeView onAddConnectionProfile handler tests', () => {
let serverTreeView: ServerTreeView;
let mockTree: TypeMoq.Mock<Tree>;
let mockRefreshTreeMethod: TypeMoq.Mock<Function>;
let capabilitiesService = new TestCapabilitiesService();
setup(() => {
let instantiationService = new TestInstantiationService();
let mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, {}, {}, new TestStorageService());
mockConnectionManagementService.setup(x => x.getConnectionGroups()).returns(x => []);
mockConnectionManagementService.setup(x => x.hasRegisteredServers()).returns(() => true);
serverTreeView = new ServerTreeView(mockConnectionManagementService.object, instantiationService, undefined, undefined, undefined, undefined, capabilitiesService);
let tree = <Tree>{
clearSelection() { },
getSelection() { },
select(selection) { },
reveal(reveal) { }
};
mockTree = TypeMoq.Mock.ofInstance(tree);
(serverTreeView as any)._tree = mockTree.object;
mockRefreshTreeMethod = TypeMoq.Mock.ofType(Function);
mockRefreshTreeMethod.setup(x => x()).returns(() => Promise.resolve());
(serverTreeView as any).refreshTree = mockRefreshTreeMethod.object;
mockTree.setup(x => x.clearSelection());
mockTree.setup(x => x.select(TypeMoq.It.isAny()));
});
async function runAddConnectionProfileHandler(oldSelection, newProfile): Promise<void> {
mockTree.setup(x => x.getSelection()).returns(() => [oldSelection] || []);
return (serverTreeView as any).handleAddConnectionProfile(newProfile);
}
test('onAddConnectionProfile handler selects the new profile when no profile is already selected', async () => {
let newProfile = <IConnectionProfile>{
id: 'test_connection'
};
await runAddConnectionProfileHandler(undefined, newProfile);
mockRefreshTreeMethod.verify(x => x(), TypeMoq.Times.once());
mockTree.verify(x => x.clearSelection(), TypeMoq.Times.never());
mockTree.verify(x => x.select(TypeMoq.It.is(profile => profile === newProfile)), TypeMoq.Times.once());
});
test('onAddConnectionProfile handler selects the new profile when a different profile is already selected', async () => {
let oldProfile = <IConnectionProfile>{
id: 'old_connection'
};
let newProfile = <IConnectionProfile>{
id: 'test_connection'
};
await runAddConnectionProfileHandler(oldProfile, newProfile);
mockRefreshTreeMethod.verify(x => x(), TypeMoq.Times.once());
mockTree.verify(x => x.clearSelection(), TypeMoq.Times.once());
mockTree.verify(x => x.select(TypeMoq.It.is(profile => profile === newProfile)), TypeMoq.Times.once());
});
test('onAddConnectionProfile handler does not clear the selection when the new profile is already selected', async () => {
let selectionId = 'test_connection';
let oldProfile = <IConnectionProfile>{
id: selectionId
};
let newProfile = <IConnectionProfile>{
id: selectionId
};
await runAddConnectionProfileHandler(oldProfile, newProfile);
mockRefreshTreeMethod.verify(x => x(), TypeMoq.Times.once());
mockTree.verify(x => x.clearSelection(), TypeMoq.Times.never());
mockTree.verify(x => x.select(TypeMoq.It.isAny()), TypeMoq.Times.never());
});
test('onAddConnectionProfile handler does not clear the previously selected profile if there is no new one', async () => {
let oldProfile = <IConnectionProfile>{
id: 'test_connection'
};
await runAddConnectionProfileHandler(oldProfile, undefined);
mockRefreshTreeMethod.verify(x => x(), TypeMoq.Times.once());
mockTree.verify(x => x.clearSelection(), TypeMoq.Times.never());
mockTree.verify(x => x.select(TypeMoq.It.isAny()), TypeMoq.Times.never());
});
test('The tree refreshes when new capabilities are registered', () => {
capabilitiesService.fireCapabilitiesRegistered(undefined);
mockRefreshTreeMethod.verify(x => x(), TypeMoq.Times.once());
});
});