Refresh master with initial release/0.24 snapshot (#332)

* Initial port of release/0.24 source code

* Fix additional headers

* Fix a typo in launch.json
This commit is contained in:
Karl Burtram
2017-12-15 15:38:57 -08:00
committed by GitHub
parent 271b3a0b82
commit 6ad0df0e3e
7118 changed files with 107999 additions and 56466 deletions

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Button as vsButton, IButtonOptions, IButtonStyles as vsIButtonStyles } from 'vs/base/browser/ui/button/button';
import * as DOM from 'vs/base/browser/dom';
import { Color } from 'vs/base/common/color';
export interface IButtonStyles extends vsIButtonStyles {
buttonFocusOutline?: Color;
}
export class Button extends vsButton {
private buttonFocusOutline: Color;
constructor(container: any, options?: IButtonOptions) {
super(container, options);
this.buttonFocusOutline = null;
this.$el.on(DOM.EventType.FOCUS, (e) => {
this.$el.style('outline-color', this.buttonFocusOutline ? this.buttonFocusOutline.toString() : null);
this.$el.style('outline-width', '1px');
});
}
public style(styles: IButtonStyles): void {
super.style(styles);
this.buttonFocusOutline = styles.buttonFocusOutline;
}
public set title(value: string) {
this.$el.title(value);
}
}

View File

@@ -2,53 +2,85 @@
* 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/base/browser/ui/checkbox/media/checkbox';
import { Checkbox as vsCheckbox, ICheckboxOpts, ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
import { Color } from 'vs/base/common/color';
const defaultOpts = {
inputActiveOptionBorder: Color.fromHex('#007ACC'),
actionClassName: ' sql-checkbox'
};
import Event, { Emitter } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Widget } from 'vs/base/browser/ui/widget';
/**
* Extends Checkbox to include Carbon checkbox icon and styling.
*/
export class Checkbox extends vsCheckbox {
private _inputActiveOptionBorder: Color;
export interface ICheckboxOptions {
label: string;
enabled?: boolean;
checked?: boolean;
onChange?: (val: boolean) => void;
}
constructor(opts: ICheckboxOpts) {
super({
actionClassName: opts.actionClassName + defaultOpts.actionClassName,
title: opts.title,
isChecked: opts.isChecked,
onChange: opts.onChange,
onKeyDown: opts.onKeyDown,
inputActiveOptionBorder: opts.inputActiveOptionBorder
export class Checkbox extends Widget {
private _el: HTMLInputElement;
private _label: HTMLSpanElement;
private _onChange = new Emitter<boolean>();
public readonly onChange: Event<boolean> = this._onChange.event;
constructor(container: HTMLElement, opts: ICheckboxOptions) {
super();
this._el = document.createElement('input');
this._el.type = 'checkbox';
this.onchange(this._el, e => {
this._onChange.fire(this.checked);
});
this._inputActiveOptionBorder = opts.inputActiveOptionBorder ? opts.inputActiveOptionBorder : defaultOpts.inputActiveOptionBorder;
this.onkeydown(this._el, e => {
if (e.equals(KeyCode.Enter)) {
this.checked = !this.checked;
e.stopPropagation();
}
});
this._label = document.createElement('span');
this.label = opts.label;
this.enabled = opts.enabled || true;
this.checked = opts.checked || false;
if (opts.onChange) {
this.onChange(opts.onChange);
}
container.appendChild(this._el);
container.appendChild(this._label);
}
public enable(): void {
super.enable();
this.domNode.classList.remove('disabled');
public set label(val: string) {
this._label.innerText = val;
}
public set enabled(val: boolean) {
this._el.disabled = !val;
}
public get enabled(): boolean {
return !this._el.disabled;
}
public set checked(val: boolean) {
this._el.checked = val;
}
public get checked(): boolean {
return this._el.checked;
}
public focus(): void {
this._el.focus();
}
public disable(): void {
super.disable();
this.domNode.classList.add('disabled');
this.enabled = false;
}
public style(styles: ICheckboxStyles): void {
if (styles.inputActiveOptionBorder) {
this._inputActiveOptionBorder = styles.inputActiveOptionBorder;
}
this.applyStyles();
public enable(): void {
this.enabled = true;
}
protected applyStyles(): void {
if (this.domNode) {
this.domNode.style.borderColor = this._inputActiveOptionBorder ? this._inputActiveOptionBorder.toString(): defaultOpts.inputActiveOptionBorder.toString();
}
}
}
}

View File

@@ -1,71 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Event, { Emitter } from 'vs/base/common/event';
import * as DOM from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export interface ICheckboxOptions {
label: string;
enabled?: boolean;
checked?: boolean;
}
export class Checkbox extends Disposable {
private _el: HTMLInputElement;
private _label: HTMLSpanElement;
private _onChange = new Emitter<boolean>();
public readonly onChange: Event<boolean> = this._onChange.event;
constructor(container: HTMLElement, opts: ICheckboxOptions) {
super();
this._el = document.createElement('input');
this._el.type = 'checkbox';
this._register(DOM.addDisposableListener(this._el, DOM.EventType.CHANGE, e => {
this._onChange.fire(e);
}));
this._register(DOM.addStandardDisposableListener(this._el, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
if (e.equals(KeyCode.Enter)) {
this.checked = !this.checked;
e.stopPropagation();
}
}));
this._label = document.createElement('span');
this.label = opts.label;
this.enabled = opts.enabled;
this.checked = opts.checked;
container.appendChild(this._el);
container.appendChild(this._label);
}
public set label(val: string) {
this._label.innerText = val;
}
public set enabled(val: boolean) {
this._el.disabled = !val;
}
public get enabled(): boolean {
return !this._el.disabled;
}
public set checked(val: boolean) {
this._el.checked = val;
}
public get checked(): boolean {
return this._el.checked;
}
}

View File

@@ -11,13 +11,14 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { IAction } from 'vs/base/common/actions';
import { Button } from 'vs/base/browser/ui/button/button';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { EventType as GestureEventType } from 'vs/base/browser/touch';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Button } from 'sql/base/browser/ui/button/button';
import { attachButtonStyler } from 'sql/common/theme/styler';
export interface IDropdownStyles {
backgroundColor?: Color;
foregroundColor?: Color;
@@ -46,6 +47,14 @@ export class DropdownList extends Dropdown {
this._action.run();
this.hide();
}));
this.toDispose.push(DOM.addDisposableListener(button.getElement(), DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter)) {
e.stopPropagation();
this._action.run();
this.hide();
}
}));
attachButtonStyler(button, this._themeService);
}

View File

@@ -6,15 +6,15 @@
import 'vs/css!./media/dropdownList';
import { ToggleDropdownAction } from './actions';
import { DropdownDataSource, DropdownFilter, DropdownModel, DropdownRenderer, DropdownController } from './dropdownTree';
import { IContextViewProvider, ContextView } from 'vs/base/browser/ui/contextview/contextview';
import { mixin } from 'vs/base/common/objects';
import { Builder, $ } from 'vs/base/browser/builder';
import { InputBox, IInputBoxStyles } from 'sql/base/browser/ui/inputBox/inputBox';
import { IMessage, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { List, IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import * as DOM from 'vs/base/browser/dom';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Disposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
@@ -22,6 +22,7 @@ import * as nls from 'vs/nls';
import Event, { Emitter } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
export interface IDropdownOptions extends IDropdownStyles {
/**
@@ -72,47 +73,20 @@ interface TableTemplate {
label: HTMLElement;
}
class Delegate implements IDelegate<ListResource> {
getHeight = (): number => 22;
getTemplateId(element: ListResource): string {
return 'string';
}
}
class Renderer implements IRenderer<ListResource, TableTemplate> {
static TEMPLATE_ID = 'string';
get templateId(): string { return Renderer.TEMPLATE_ID; }
renderTemplate(container: HTMLElement): TableTemplate {
const row = $('div.list-row').style('height', '22px').style('padding-left', '5px').getHTMLElement();
DOM.append(container, row);
const label = $('span.label').style('margin', 'auto').getHTMLElement();
DOM.append(row, label);
return { label };
}
renderElement(resource: ListResource, index: number, template: TableTemplate): void {
template.label.innerText = resource.label;
}
disposeTemplate(template: TableTemplate): void {
// noop
}
}
export class Dropdown extends Disposable {
private $el: Builder;
private $input: Builder;
private $list: Builder;
private $treeContainer: Builder;
private _input: InputBox;
private _list: List<ListResource>;
private _values: string[];
private _tree: Tree;
private _options: IDropdownOptions;
private _toggleAction: ToggleDropdownAction;
// we have to create our own contextview since otherwise inputbox will override ours
private _contextView: ContextView;
private _dataSource = new DropdownDataSource();
private _filter = new DropdownFilter();
private _renderer = new DropdownRenderer();
private _controller = new DropdownController();
private _onBlur = this._register(new Emitter<void>());
public onBlur: Event<void> = this._onBlur.event;
@@ -132,13 +106,16 @@ export class Dropdown extends Disposable {
super();
this._contextView = new ContextView(document.body);
this._options = mixin(opt, defaults, false) as IDropdownOptions;
this._values = this._options.values;
this.$el = $('.dropdown').style('width', '100%').appendTo(container);
this.$input = $('.dropdown-input').style('width', '100%').appendTo(this.$el);
this.$list = $('.dropdown-list');
this.$treeContainer = $('.dropdown-tree');
this._toggleAction = new ToggleDropdownAction(() => this._showList());
this._toggleAction = new ToggleDropdownAction(() => {
this._showList();
this._tree.DOMFocus();
this._tree.focusFirst();
});
this._input = new InputBox(this.$input.getHTMLElement(), contextViewService, {
validationOptions: {
@@ -154,12 +131,12 @@ export class Dropdown extends Disposable {
}));
this._register(DOM.addDisposableListener(this._input.inputElement, DOM.EventType.BLUR, () => {
if (!this._list.isDOMFocused) {
if (!this._tree.isDOMFocused()) {
this._onBlur.fire();
}
}));
this._register(DOM.addStandardDisposableListener(this._input.inputElement, DOM.EventType.KEY_UP, (e: StandardKeyboardEvent) => {
this._register(DOM.addStandardDisposableListener(this._input.inputElement, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
switch (e.keyCode) {
case KeyCode.Enter:
if (this._input.validate()) {
@@ -168,7 +145,7 @@ export class Dropdown extends Disposable {
e.stopPropagation();
break;
case KeyCode.Escape:
if (this.$list.getHTMLElement().parentElement) {
if (this.$treeContainer.getHTMLElement().parentElement) {
this._input.validate();
this._onBlur.fire();
this._contextView.hide();
@@ -182,44 +159,55 @@ export class Dropdown extends Disposable {
e.stopPropagation();
break;
case KeyCode.DownArrow:
if (!this.$list.getHTMLElement().parentElement) {
if (!this.$treeContainer.getHTMLElement().parentElement) {
this._showList();
}
this._list.getHTMLElement().focus();
this._tree.DOMFocus();
this._tree.focusFirst();
e.stopPropagation();
e.preventDefault();
break;
}
}));
this._list = new List(this.$list.getHTMLElement(), new Delegate(), [new Renderer()]);
if (this._values) {
this._list.splice(0, this._list.length, this._values.map(i => { return { label: i }; }));
let height = this._list.length * 22 > this._options.maxHeight ? this._options.maxHeight : this._list.length * 22;
this.$list.style('height', height + 'px').style('width', DOM.getContentWidth(this.$input.getHTMLElement()) + 'px');
}
this._tree = new Tree(this.$treeContainer.getHTMLElement(), {
dataSource: this._dataSource,
filter: this._filter,
renderer: this._renderer,
controller: this._controller
});
this._list.onSelectionChange(e => {
if (e.elements.length === 1) {
this.value = e.elements[0].label;
this._onValueChange.fire(e.elements[0].label);
this._contextView.hide();
}
this.values = this._options.values;
this._controller.onSelectionChange(e => {
this.value = e.value;
this._onValueChange.fire(e.value);
this._input.focus();
this._contextView.hide();
});
this._input.onDidChange(e => {
if (this._values) {
this._list.splice(0, this._list.length, this._values.filter(i => i.includes(e)).map(i => { return { label: i }; }));
let height = this._list.length * 22 > this._options.maxHeight ? this._options.maxHeight : this._list.length * 22;
this.$list.style('height', height + 'px').style('width', DOM.getContentWidth(this.$input.getHTMLElement()) + 'px');
this._list.layout(parseInt(this.$list.style('height')));
if (this._dataSource.options) {
this._filter.filterString = e;
let filteredLength = this._dataSource.options.reduce((p, i) => {
if (this._filter.isVisible(undefined, i)) {
return p + 1;
} else {
return p;
}
}, 0);
let height = filteredLength * this._renderer.getHeight(undefined, undefined) > this._options.maxHeight ? this._options.maxHeight : filteredLength * this._renderer.getHeight(undefined, undefined);
this.$treeContainer.style('height', height + 'px').style('width', DOM.getContentWidth(this.$input.getHTMLElement()) + 'px');
this._tree.layout(parseInt(this.$treeContainer.style('height')));
this._tree.refresh();
}
});
this._register(this._contextView);
this._register(this.$el);
this._register(this.$input);
this._register(this.$list);
this._register(this._list);
this._register(this.$treeContainer);
this._register(this._tree);
this._register(this._input);
this._register(this._contextView);
}
@@ -230,12 +218,12 @@ export class Dropdown extends Disposable {
this._contextView.show({
getAnchor: () => this.$input.getHTMLElement(),
render: container => {
this.$list.appendTo(container);
this._list.layout(parseInt(this.$list.style('height')));
this.$treeContainer.appendTo(container);
this._tree.layout(parseInt(this.$treeContainer.style('height')));
return { dispose: () => { } };
},
onDOMEvent: (e, activeElement) => {
if (!DOM.isAncestor(activeElement, this.$el.getHTMLElement())) {
if (!DOM.isAncestor(activeElement, this.$el.getHTMLElement()) && !DOM.isAncestor(activeElement, this.$treeContainer.getHTMLElement())) {
this._input.validate();
this._onBlur.fire();
this._contextView.hide();
@@ -246,12 +234,15 @@ export class Dropdown extends Disposable {
}
public set values(vals: string[]) {
this._values = vals;
this._list.splice(0, this._list.length, this._values.map(i => { return { label: i }; }));
let height = this._list.length * 22 > this._options.maxHeight ? this._options.maxHeight : this._list.length * 22;
this.$list.style('height', height + 'px').style('width', DOM.getContentWidth(this.$input.getHTMLElement()) + 'px');
this._list.layout(parseInt(this.$list.style('height')));
this._input.validate();
if (vals) {
this._filter.filterString = '';
this._dataSource.options = vals.map(i => { return { value: i }; });
let height = this._dataSource.options.length * 22 > this._options.maxHeight ? this._options.maxHeight : this._dataSource.options.length * 22;
this.$treeContainer.style('height', height + 'px').style('width', DOM.getContentWidth(this.$input.getHTMLElement()) + 'px');
this._tree.layout(parseInt(this.$treeContainer.style('height')));
this._tree.setInput(new DropdownModel());
this._input.validate();
}
}
public get value(): string {
@@ -272,14 +263,14 @@ export class Dropdown extends Disposable {
}
style(style: IListStyles & IInputBoxStyles & IDropdownStyles) {
this._list.style(style);
this._tree.style(style);
this._input.style(style);
this.$list.style('background-color', style.contextBackground.toString());
this.$list.style('outline', `1px solid ${style.contextBorder || this._options.contextBorder}`);
this.$treeContainer.style('background-color', style.contextBackground.toString());
this.$treeContainer.style('outline', `1px solid ${style.contextBorder || this._options.contextBorder}`);
}
private _inputValidator(value: string): IMessage {
if (this._values && !this._values.includes(value)) {
if (this._dataSource.options && !this._dataSource.options.find(i => i.value === value)) {
if (this._options.strictSelection) {
return {
content: this._options.errorMessage,

View File

@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { generateUuid } from 'vs/base/common/uuid';
import * as DOM from 'vs/base/browser/dom';
import { $ } from 'vs/base/browser/builder';
import Event, { Emitter } from 'vs/base/common/event';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export interface Template {
label: HTMLElement;
}
export interface Resource {
value: string;
}
export class DropdownModel {
public static ID = generateUuid();
}
export class DropdownRenderer implements tree.IRenderer {
public getHeight(tree: tree.ITree, element: Resource): number {
return 22;
}
public getTemplateId(tree: tree.ITree, element: Resource): string {
return '';
}
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement) {
const row = $('div.list-row').style('height', '22px').style('padding-left', '5px').getHTMLElement();
DOM.append(container, row);
const label = $('span.label').style('margin', 'auto').getHTMLElement();
DOM.append(row, label);
return { label };
}
public renderElement(tree: tree.ITree, element: Resource, templateId: string, templateData: Template): void {
templateData.label.innerText = element.value;
}
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: Template): void {
// no op
}
}
export class DropdownDataSource implements tree.IDataSource {
public options: Array<Resource>;
public getId(tree: tree.ITree, element: Resource | DropdownModel): string {
if (element instanceof DropdownModel) {
return DropdownModel.ID;
} else {
return (element as Resource).value;
}
}
public hasChildren(tree: tree.ITree, element: Resource | DropdownModel): boolean {
if (element instanceof DropdownModel) {
return true;
} else {
return false;
}
}
public getChildren(tree: tree.ITree, element: Resource | DropdownModel): Promise<any, any> {
if (element instanceof DropdownModel) {
return TPromise.as(this.options);
} else {
return TPromise.as(undefined);
}
}
public getParent(tree: tree.ITree, element: Resource | DropdownModel): Promise<any, any> {
if (element instanceof DropdownModel) {
return TPromise.as(undefined);
} else {
return TPromise.as(new DropdownModel());
}
}
}
export class DropdownFilter extends TreeDefaults.DefaultFilter {
public filterString: string;
public isVisible(tree: tree.ITree, element: Resource): boolean {
return element.value.includes(this.filterString);
}
}
export class DropdownController extends TreeDefaults.DefaultController {
private _onSelectionChange = new Emitter<Resource>();
public readonly onSelectionChange: Event<Resource> = this._onSelectionChange.event;
constructor() {
super();
}
protected onLeftClick(tree: tree.ITree, element: any, eventish: TreeDefaults.ICancelableEvent, origin: string): boolean {
let response = super.onLeftClick(tree, element, eventish, origin);
if (response) {
this._onSelectionChange.fire(tree.getSelection()[0]);
}
return response;
}
protected onEnter(tree: tree.ITree, event: IKeyboardEvent): boolean {
let response = super.onEnter(tree, event);
if (response) {
this._onSelectionChange.fire(tree.getSelection()[0]);
}
return response;
}
}

View File

@@ -21,3 +21,7 @@
.dropdown .monaco-action-bar .action-item {
margin: 0;
}
.dropdown-tree .list-row {
margin-left: -33px;
}

View File

@@ -4,16 +4,15 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox';
import * as lifecycle from 'vs/base/common/lifecycle';
import { SelectBox, ISelectBoxStyles } from 'vs/base/browser/ui/selectBox/selectBox';
import { Color } from 'vs/base/common/color';
import { ISelectBoxStyles } from 'vs/base/browser/ui/selectBox/selectBox';
import { IMessage, MessageType, defaultOpts } from 'vs/base/browser/ui/inputbox/inputBox';
import * as dom from 'vs/base/browser/dom';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IContextViewProvider, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { RenderOptions, renderFormattedText, renderText } from 'vs/base/browser/htmlContentRenderer';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
const $ = dom.$;
@@ -33,14 +32,12 @@ export interface IListBoxStyles {
* Extends SelectBox to allow multiple selection and adding/remove items dynamically
*/
export class ListBox extends SelectBox {
private _toDispose2: lifecycle.IDisposable[];
private enabledSelectBackground: Color;
private enabledSelectForeground: Color;
private enabledSelectBorder: Color;
private disabledSelectBackground: Color;
private disabledSelectForeground: Color;
private disabledSelectBorder: Color;
private keyC = 33;
private inputValidationInfoBorder: Color;
private inputValidationInfoBackground: Color;
@@ -53,7 +50,7 @@ export class ListBox extends SelectBox {
private contextViewProvider: IContextViewProvider;
private isValid: boolean;
constructor(options: string[], selectedOption: string, contextViewProvider: IContextViewProvider) {
constructor(options: string[], selectedOption: string, contextViewProvider: IContextViewProvider, private _clipboardService: IClipboardService) {
super(options, 0);
this.contextViewProvider = contextViewProvider;
this.isValid = true;
@@ -64,10 +61,7 @@ export class ListBox extends SelectBox {
this.selectElement.style['width'] = 'inherit';
this.selectElement.style['min-width'] = '100%';
this._toDispose2 = [];
this._toDispose2.push(dom.addStandardDisposableListener(this.selectElement, 'keydown', (e) => {
this.onKeyDown(e)
}));
this._register(dom.addStandardDisposableListener(this.selectElement, dom.EventType.KEY_DOWN, e => this.onKeyDown(e)));
this.enabledSelectBackground = this.selectBackground;
this.enabledSelectForeground = this.selectForeground;
@@ -88,11 +82,11 @@ export class ListBox extends SelectBox {
}
public style(styles: IListBoxStyles): void {
var superStyle: ISelectBoxStyles = {
let superStyle: ISelectBoxStyles = {
selectBackground: styles.selectBackground,
selectForeground: styles.selectForeground,
selectBorder: styles.selectBorder
}
};
super.style(superStyle);
this.enabledSelectBackground = this.selectBackground;
this.enabledSelectForeground = this.selectForeground;
@@ -123,8 +117,8 @@ export class ListBox extends SelectBox {
}
public get selectedOptions(): string[] {
var selected = [];
for (var i = 0; i < this.selectElement.selectedOptions.length; i++ ) {
let selected = [];
for (let i = 0; i < this.selectElement.selectedOptions.length; i++) {
selected.push(this.selectElement.selectedOptions[i].innerHTML);
}
return selected;
@@ -136,13 +130,13 @@ export class ListBox extends SelectBox {
// Remove selected options
public remove(): void {
var indexes = [];
for (var i = 0; i < this.selectElement.selectedOptions.length; i++ ) {
let indexes = [];
for (let i = 0; i < this.selectElement.selectedOptions.length; i++) {
indexes.push(this.selectElement.selectedOptions[i].index);
}
indexes.sort((a, b) => b-a);
indexes.sort((a, b) => b - a);
for (var i = 0; i < indexes.length; i++) {
for (let i = 0; i < indexes.length; i++) {
this.selectElement.remove(indexes[i]);
this.options.splice(indexes[i], 1);
}
@@ -155,27 +149,22 @@ export class ListBox extends SelectBox {
// Allow copy to clipboard
public onKeyDown(event: IKeyboardEvent): void {
if (this.selectedOptions.length > 0)
{
var key = event.keyCode;
var ctrlOrCmd = event.ctrlKey || event.metaKey;
if (this.selectedOptions.length > 0) {
let key = event.keyCode;
let ctrlOrCmd = event.ctrlKey || event.metaKey;
if (ctrlOrCmd && key === this.keyC) {
var textToCopy = this.selectedOptions[0];
for (var i = 1; i < this.selectedOptions.length; i++) {
textToCopy = textToCopy + ', ' + this.selectedOptions[i];
}
if (ctrlOrCmd && key === KeyCode.KEY_C) {
let textToCopy = this.selectedOptions[0];
for (let i = 1; i < this.selectedOptions.length; i++) {
textToCopy = textToCopy + ', ' + this.selectedOptions[i];
}
// Copy to clipboard
WorkbenchUtils.executeCopy(textToCopy);
event.stopPropagation();
}
}
}
// Copy to clipboard
this._clipboardService.writeText(textToCopy);
public dispose(): void {
this._toDispose2 = lifecycle.dispose(this._toDispose2);
super.dispose();
event.stopPropagation();
}
}
}
public enable(): void {

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Button } from 'sql/base/browser/ui/button/button';
import { Builder } from 'vs/base/browser/builder';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Button } from 'vs/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import * as data from 'data';
import * as types from 'vs/base/common/types';
import * as data from 'data';
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string): Builder {
let cellContainer: Builder;
container.element('tr', {}, (rowContainer) => {
@@ -47,35 +47,11 @@ export function appendRowLink(container: Builder, label: string, labelClass: str
return new Builder(rowButton.getElement());
}
export function createCheckBox(container: Builder, label: string, checkboxClass: string, isChecked: boolean, onCheck?: (viaKeyboard: boolean) => void): Checkbox {
let checkbox = new Checkbox({
actionClassName: checkboxClass,
title: label,
isChecked: isChecked,
onChange: (viaKeyboard) => {
if (onCheck) {
onCheck(viaKeyboard);
}
}
});
container.getHTMLElement().appendChild(checkbox.domNode);
container.div({}, (labelContainer) => {
labelContainer.innerHtml(label);
});
return checkbox;
}
export function appendInputSelectBox(container: Builder, selectBox: SelectBox): SelectBox {
selectBox.render(container.getHTMLElement());
return selectBox;
}
export function isNullOrWhiteSpace(value: string): boolean {
// returns true if the string is null or contains white space/tab chars only
return !value || value.trim().length === 0;
}
export function getBooleanValueFromStringOrBoolean(value: any): boolean {
if (types.isBoolean(value)) {
return value;

View File

@@ -154,11 +154,7 @@
width: 100px;
}
.vs-dark.monaco-shell .modal.flyout-dialog .footer-button a.monaco-button.monaco-text-button {
outline-color: #8e8c8c;
}
.modal.flyout-dialog .footer-button a.monaco-button.monaco-text-button:focus {
.vs .monaco-text-button:focus {
outline-width: 1px;
}

View File

@@ -7,17 +7,17 @@ import 'vs/css!./media/modal';
import { IThemable } from 'vs/platform/theme/common/styler';
import { Color } from 'vs/base/common/color';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { KeyCode } from 'vs/base/common/keyCodes';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { mixin } from 'vs/base/common/objects';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Builder, $, withElementById } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Button } from 'sql/base/browser/ui/button/button';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
@@ -64,6 +64,12 @@ export abstract class Modal extends Disposable implements IThemable {
private _errorMessage: Builder;
private _spinnerElement: HTMLElement;
private _errorIconElement: HTMLElement;
private _focusableElements: NodeListOf<Element>;
private _firstFocusableElement: HTMLElement;
private _lastFocusableElement: HTMLElement;
private _focusedElementBeforeOpen: HTMLElement;
private _dialogForeground: Color;
private _dialogBorder: Color;
private _dialogHeaderAndFooterBackground: Color;
@@ -102,6 +108,7 @@ export abstract class Modal extends Disposable implements IThemable {
* Set the dialog to have wide layout dynamically.
* Temporary solution to render file browser as wide or narrow layout.
* This will be removed once backup dialog is changed to wide layout.
* (hyoshi - 10/2/2017 tracked by https://github.com/Microsoft/carbon/issues/1836)
*/
public setWide(isWide: boolean): void {
if (this._builder.hasClass('wide') && isWide === false) {
@@ -244,12 +251,42 @@ export abstract class Modal extends Disposable implements IThemable {
this.hide();
}
private handleBackwardTab(e: KeyboardEvent) {
if (this._firstFocusableElement && this._lastFocusableElement && document.activeElement === this._firstFocusableElement) {
e.preventDefault();
this._lastFocusableElement.focus();
}
}
private handleForwardTab(e: KeyboardEvent) {
if (this._firstFocusableElement && this._lastFocusableElement && document.activeElement === this._lastFocusableElement) {
e.preventDefault();
this._firstFocusableElement.focus();
}
}
/**
* Set focusable elements in the modal dialog
*/
public setFocusableElements() {
this._focusableElements = this._builder.getHTMLElement().querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
if (this._focusableElements && this._focusableElements.length > 0) {
this._firstFocusableElement = <HTMLElement>this._focusableElements[0];
this._lastFocusableElement = <HTMLElement>this._focusableElements[this._focusableElements.length - 1];
}
this._focusedElementBeforeOpen = <HTMLElement>document.activeElement;
}
/**
* Shows the modal and attaches key listeners
*/
protected show() {
this._modalShowingContext.get().push(this._staticKey);
this._builder.appendTo(withElementById(this._partService.getWorkbenchElementId()).getHTMLElement().parentElement);
this.setFocusableElements();
this._keydownListener = DOM.addDisposableListener(document, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let context = this._modalShowingContext.get();
if (context[context.length - 1] === this._staticKey) {
@@ -258,6 +295,10 @@ export abstract class Modal extends Disposable implements IThemable {
this.onAccept(event);
} else if (event.equals(KeyCode.Escape)) {
this.onClose(event);
} else if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
this.handleBackwardTab(e);
} else if (event.equals(KeyCode.Tab)) {
this.handleForwardTab(e);
}
}
});
@@ -281,6 +322,9 @@ export abstract class Modal extends Disposable implements IThemable {
this._footerButtons.forEach(button => button.applyStyles());
this._modalShowingContext.get().pop();
this._builder.offDOM();
if (this._focusedElementBeforeOpen) {
this._focusedElementBeforeOpen.focus();
}
this._keydownListener.dispose();
this._resizeListener.dispose();
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ModalDialogClosed, { name: this._name });

View File

@@ -6,16 +6,15 @@
'use strict';
import 'vs/css!./media/optionsDialog';
import { Button } from 'sql/base/browser/ui/button/button';
import { FixedCollapsibleView } from 'sql/platform/views/fixedCollapsibleView';
import * as DialogHelper from './dialogHelper';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IModalOptions, Modal } from './modal';
import * as OptionsDialogHelper from './optionsDialogHelper';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { attachButtonStyler, attachModalDialogStyler } from 'sql/common/theme/styler';
import * as data from 'data';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import Event, { Emitter } from 'vs/base/common/event';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
@@ -27,13 +26,14 @@ import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/theme
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import * as styler from 'vs/platform/theme/common/styler';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { SplitView, CollapsibleState } from 'vs/base/browser/ui/splitview/splitview';
import { SplitView, CollapsibleState } from 'sql/base/browser/ui/splitview/splitview';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { Widget } from 'vs/base/browser/ui/widget';
class CategoryView extends FixedCollapsibleView {
export class CategoryView extends FixedCollapsibleView {
private _treecontainer: HTMLElement;
private _collapsed: CollapsibleState;
constructor(private viewTitle: string, private _bodyContainer: HTMLElement, collapsed: boolean, initialBodySize: number, headerSize: number) {
super(
initialBodySize,
@@ -43,6 +43,7 @@ class CategoryView extends FixedCollapsibleView {
initialState: collapsed ? CollapsibleState.COLLAPSED : CollapsibleState.EXPANDED,
ariaHeaderLabel: viewTitle
});
this._collapsed = collapsed ? CollapsibleState.COLLAPSED : CollapsibleState.EXPANDED;
}
public renderHeader(container: HTMLElement): void {
@@ -54,6 +55,7 @@ class CategoryView extends FixedCollapsibleView {
this._treecontainer = document.createElement('div');
container.appendChild(this._treecontainer);
this._treecontainer.appendChild(this._bodyContainer);
this.changeState(this._collapsed);
}
public layoutBody(size: number): void {
@@ -102,13 +104,13 @@ export class OptionsDialog extends Modal {
attachModalDialogStyler(this, this._themeService);
if (this.backButton) {
this.backButton.addListener('click', () => this.cancel());
styler.attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND });
attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND });
}
this._okButton = this.addFooterButton(this.okLabel, () => this.ok());
this._closeButton = this.addFooterButton(this.cancelLabel, () => this.cancel());
// Theme styler
styler.attachButtonStyler(this._okButton, this._themeService);
styler.attachButtonStyler(this._closeButton, this._themeService);
attachButtonStyler(this._okButton, this._themeService);
attachButtonStyler(this._closeButton, this._themeService);
let self = this;
this._register(self._themeService.onDidColorThemeChange(e => self.updateTheme(e)));
self.updateTheme(self._themeService.getColorTheme());

View File

@@ -6,11 +6,14 @@
import { IThemable } from 'vs/platform/theme/common/styler';
import * as objects from 'vs/base/common/objects';
import Event, { Emitter } from 'vs/base/common/event';
import { Dimension, Builder } from 'vs/base/browser/builder';
import { addDisposableListener, EventType } from 'vs/base/browser/dom';
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner } from 'vs/base/common/actions';
import { Dimension, $, Builder } from 'vs/base/browser/builder';
import { EventType } from 'vs/base/browser/dom';
import { IAction } from 'vs/base/common/actions';
import { IActionOptions, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import './panelStyles';
import { Disposable } from 'vs/base/common/lifecycle';
export interface IPanelStyles {
@@ -28,45 +31,41 @@ export interface IPanelTab {
}
interface IInternalPanelTab extends IPanelTab {
header: HTMLElement;
label: HTMLElement;
header: Builder;
label: Builder;
}
export type PanelTabIdentifier = string;
export class TabbedPanel implements IThemable {
export class TabbedPanel extends Disposable implements IThemable {
private _tabMap = new Map<PanelTabIdentifier, IInternalPanelTab>();
private _shownTab: PanelTabIdentifier;
public readonly headersize = 35;
private _header: HTMLElement;
private _tabList: HTMLElement;
private $header: Builder;
private $tabList: Builder;
private $body: Builder;
private $parent: Builder;
private _actionbar: ActionBar;
private _body: HTMLElement;
private _currentDimensions: Dimension;
private _collapsed = false;
private _parent: HTMLElement;
private _onTabChange = new Emitter<PanelTabIdentifier>();
public onTabChange: Event<PanelTabIdentifier> = this._onTabChange.event;
constructor(private container: HTMLElement) {
this._parent = document.createElement('div');
this._parent.className = 'tabbedPanel';
container.appendChild(this._parent);
this._header = document.createElement('div');
this._header.className = 'composite title';
this._tabList = document.createElement('div');
this._tabList.className = 'tabList';
this._tabList.style.height = this.headersize + 'px';
this._header.appendChild(this._tabList);
let actionbarcontainer = document.createElement('div');
actionbarcontainer.className = 'title-actions';
super();
this.$parent = this._register($('.tabbedPanel'));
this.$parent.appendTo(container);
this.$header = $('.composite.title');
this.$tabList = $('.tabList');
this.$tabList.style('height', this.headersize + 'px');
this.$header.append(this.$tabList);
let actionbarcontainer = $('.title-actions');
this._actionbar = new ActionBar(actionbarcontainer);
this._header.appendChild(actionbarcontainer);
this._parent.appendChild(this._header);
this._body = document.createElement('div');
this._body.className = 'tabBody';
this._parent.appendChild(this._body);
this.$header.append(actionbarcontainer);
this.$parent.append(this.$header);
this.$body = $('tabBody');
this.$parent.append(this.$body);
}
public pushTab(tab: IPanelTab): PanelTabIdentifier {
@@ -88,14 +87,20 @@ export class TabbedPanel implements IThemable {
}
private _createTab(tab: IInternalPanelTab): void {
let tabElement = document.createElement('div');
tabElement.className = 'tab';
let tabLabel = document.createElement('a');
tabLabel.className = 'tabLabel';
tabLabel.innerText = tab.title;
tabElement.appendChild(tabLabel);
addDisposableListener(tabElement, EventType.CLICK, (e) => this.showTab(tab.identifier));
this._tabList.appendChild(tabElement);
let tabElement = $('.tab');
tabElement.attr('tabindex', '0');
let tabLabel = $('a.tabLabel');
tabLabel.safeInnerHtml(tab.title);
tabElement.append(tabLabel);
tabElement.on(EventType.CLICK, e => this.showTab(tab.identifier));
tabElement.on(EventType.KEY_DOWN, (e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter)) {
this.showTab(tab.identifier);
e.stopImmediatePropagation();
}
});
this.$tabList.append(tabElement);
tab.header = tabElement;
tab.label = tabLabel;
}
@@ -106,14 +111,14 @@ export class TabbedPanel implements IThemable {
}
if (this._shownTab) {
this._tabMap.get(this._shownTab).label.classList.remove('active');
this._tabMap.get(this._shownTab).label.removeClass('active');
}
this._shownTab = id;
new Builder(this._body).empty();
this.$body.clearChildren();
let tab = this._tabMap.get(this._shownTab);
tab.label.classList.add('active');
tab.view.render(this._body);
tab.label.addClass('active');
tab.view.render(this.$body.getHTMLElement());
this._onTabChange.fire(id);
if (this._currentDimensions) {
this._layoutCurrentTab(new Dimension(this._currentDimensions.width, this._currentDimensions.height - this.headersize));
@@ -121,7 +126,7 @@ export class TabbedPanel implements IThemable {
}
public removeTab(tab: PanelTabIdentifier) {
this._tabMap.get(tab).header.remove();
this._tabMap.get(tab).header.destroy();
this._tabMap.delete(tab);
}
@@ -131,9 +136,9 @@ export class TabbedPanel implements IThemable {
public layout(dimension: Dimension): void {
this._currentDimensions = dimension;
this._header.style.width = dimension.width + 'px';
this._body.style.width = dimension.width + 'px';
this._body.style.height = (dimension.height - this.headersize) + 'px';
this.$header.style('width', dimension.width + 'px');
this.$body.style('width', dimension.width + 'px');
this.$body.style('height', (dimension.height - this.headersize) + 'px');
this._layoutCurrentTab(new Dimension(dimension.width, dimension.height - this.headersize));
}
@@ -154,9 +159,9 @@ export class TabbedPanel implements IThemable {
this._collapsed = val === false ? false : true;
if (this.collapsed) {
this._body.remove();
this.$body.offDOM();
} else {
this._parent.appendChild(this._body);
this.$parent.append(this.$body);
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>

After

Width:  |  Height:  |  Size: 131 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>

After

Width:  |  Height:  |  Size: 131 B

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-split-view {
position: relative;
}
.monaco-split-view > .split-view-view {
overflow: hidden;
}
.monaco-split-view.vertical > .split-view-view {
width: 100%;
}
.monaco-split-view.horizontal > .split-view-view {
height: 100%;
}
.monaco-split-view > .split-view-view > .header {
position: relative;
line-height: 22px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
padding-left: 20px;
overflow: hidden;
display: flex;
}
.monaco-split-view > .split-view-view > .header.hide {
display: none;
}
/* Bold font style does not go well with CJK fonts */
.monaco-split-view:lang(zh-Hans) > .split-view-view > .header,
.monaco-split-view:lang(zh-Hant) > .split-view-view > .header,
.monaco-split-view:lang(ja) > .split-view-view > .header,
.monaco-split-view:lang(ko) > .split-view-view > .header { font-weight: normal; }
.monaco-split-view > .split-view-view > .header.collapsible {
cursor: pointer;
}
.monaco-split-view > .split-view-view > .header.collapsible {
background-image: url('arrow-collapse.svg');
background-position: 2px center;
background-repeat: no-repeat;
}
.monaco-split-view > .split-view-view > .header.collapsible:not(.collapsed) {
background-image: url('arrow-expand.svg');
background-position: 2px center;
background-repeat: no-repeat;
}
.vs-dark .monaco-split-view > .split-view-view > .header.collapsible {
background-image: url('arrow-collapse-dark.svg');
}
.vs-dark .monaco-split-view > .split-view-view > .header.collapsible:not(.collapsed) {
background-image: url('arrow-expand-dark.svg');
background-position: 2px center;
background-repeat: no-repeat;
}
/* Animation */
.monaco-split-view.animated > .split-view-view {
transition-duration: 0.15s;
-webkit-transition-duration: 0.15s;
-moz-transition-duration: 0.15s;
transition-timing-function: ease-out;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
}
.monaco-split-view.vertical.animated > .split-view-view {
transition-property: height;
-webkit-transition-property: height;
-moz-transition-property: height;
}
.monaco-split-view.horizontal.animated > .split-view-view {
transition-property: width;
-webkit-transition-property: width;
-moz-transition-property: width;
}
.hc-black .split-view-view .action-label {
background: none;
}
.hc-black .split-view-view > .header .action-label:before {
top: 4px !important;
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
import 'vs/css!vs/base/browser/ui/checkbox/checkbox';
import { mixin } from 'vs/base/common/objects';
import * as nls from 'vs/nls';
import { ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
import { Color } from 'vs/base/common/color';
import * as strings from 'vs/base/common/strings';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
export interface ICheckboxSelectColumnOptions extends Slick.PluginOptions, ICheckboxStyles {
columnId?: string;
@@ -21,24 +21,23 @@ const defaultOptions: ICheckboxSelectColumnOptions = {
columnId: '_checkbox_selector',
cssClass: null,
toolTip: nls.localize('selectDeselectAll', 'Select/Deselect All'),
width: 30,
inputActiveOptionBorder: Color.fromHex('#007ACC')
width: 30
};
const checkBoxTemplate = `<div style="display: flex; align-items: center; flex-direction: column">
<div style="border-color: {0}" role="checkbox" aria-checked="{1}" aria-label="" class="custom-checkbox sql-checkbox {2}"></div>
</div>`;
const checkboxTemplate = `
<div style="display: flex; align-items: center; flex-direction: column">
<input type="checkbox" {0}>
</div>
`;
export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
private _options: ICheckboxSelectColumnOptions;
private _grid: Slick.Grid<T>;
private _handler = new Slick.EventHandler();
private _selectedRowsLookup = {};
private _checkboxTemplate: string;
constructor(options?: Slick.PluginOptions) {
constructor(options?: ICheckboxSelectColumnOptions) {
this._options = mixin(options, defaultOptions, false);
this.applyStyles();
}
public init(grid: Slick.Grid<T>): void {
@@ -74,11 +73,11 @@ export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
if (!this._options.title) {
if (selectedRows.length && selectedRows.length === this._grid.getDataLength()) {
this._grid.updateColumnHeader(this._options.columnId,
strings.format(this._checkboxTemplate, 'true', 'checked'),
strings.format(checkboxTemplate, 'checked'),
this._options.toolTip);
} else {
this._grid.updateColumnHeader(this._options.columnId,
strings.format(this._checkboxTemplate, 'true', 'unchecked'),
strings.format(checkboxTemplate, ''),
this._options.toolTip);
}
}
@@ -94,12 +93,22 @@ export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
e.preventDefault();
e.stopImmediatePropagation();
}
} else {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter)) {
// clicking on a row select checkbox
if (this._grid.getColumns()[args.cell].id === this._options.columnId) {
this.toggleRowSelection(args.row);
e.stopPropagation();
e.stopImmediatePropagation();
}
}
}
}
private handleClick(e: Event, args: Slick.OnClickEventArgs<T>): void {
// clicking on a row select checkbox
if (this._grid.getColumns()[args.cell].id === this._options.columnId && $(e.target).is('.custom-checkbox')) {
if (this._grid.getColumns()[args.cell].id === this._options.columnId && $(e.target).is('input[type="checkbox"]')) {
// if editing, try to commit
if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) {
e.preventDefault();
@@ -122,7 +131,7 @@ export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
}
private handleHeaderClick(e: Event, args: Slick.OnHeaderClickEventArgs<T>): void {
if (!this._options.title && args.column.id === this._options.columnId && $(e.target).is('.custom-checkbox')) {
if (!this._options.title && args.column.id === this._options.columnId && $(e.target).is('input[type="checkbox"]')) {
// if editing, try to commit
if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) {
e.preventDefault();
@@ -130,19 +139,19 @@ export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
return;
}
if ($(e.target).is('.unchecked')) {
if ($(e.target).is('input[checked]')) {
let rows = [];
for (let i = 0; i < this._grid.getDataLength(); i++) {
rows.push(i);
}
this._grid.setSelectedRows(rows);
this._grid.updateColumnHeader(this._options.columnId,
strings.format(this._checkboxTemplate, 'true', 'checked'),
strings.format(checkboxTemplate, 'checked'),
this._options.toolTip);
} else {
this._grid.setSelectedRows([]);
this._grid.updateColumnHeader(this._options.columnId,
strings.format(this._checkboxTemplate, 'true', 'unchecked'), this._options.toolTip);
strings.format(checkboxTemplate, ''), this._options.toolTip);
e.stopPropagation();
e.stopImmediatePropagation();
}
@@ -152,7 +161,7 @@ export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
public getColumnDefinition(): Slick.Column<T> {
return {
id: this._options.columnId,
name: this._options.title || strings.format(this._checkboxTemplate, 'true', 'unchecked'),
name: this._options.title || strings.format(checkboxTemplate, ''),
toolTip: this._options.toolTip,
field: 'sel',
width: this._options.width,
@@ -166,24 +175,9 @@ export class CheckboxSelectColumn<T> implements Slick.Plugin<T> {
private checkboxSelectionFormatter(row, cell, value, columnDef: Slick.Column<T>, dataContext): string {
if (dataContext) {
return this._selectedRowsLookup[row]
? strings.format(this._checkboxTemplate, 'true', 'checked')
: strings.format(this._checkboxTemplate, 'true', 'unchecked');
? strings.format(checkboxTemplate, 'checked')
: strings.format(checkboxTemplate, '');
}
return null;
}
public style(styles: ICheckboxStyles): void {
if (styles.inputActiveOptionBorder) {
this._options.inputActiveOptionBorder = styles.inputActiveOptionBorder;
}
this.applyStyles();
}
protected applyStyles(): void {
this._checkboxTemplate = strings.format(checkBoxTemplate, this._options.inputActiveOptionBorder.toString(), '{0}', '{1}');
if (this._grid) {
this._grid.invalidateAllRows();
this._grid.render();
}
}
}
}

View File

@@ -82,6 +82,10 @@ export class Table<T extends Slick.SlickData> implements IThemable {
this._grid.setColumns(columns);
}
public get grid(): Slick.Grid<T> {
return this._grid;
}
setData(data: Array<T>);
setData(data: TableDataView<T>);
setData(data: Array<T> | TableDataView<T>) {
@@ -204,11 +208,11 @@ export class Table<T extends Slick.SlickData> implements IThemable {
}
if (styles.listFocusBackground) {
content.push(`.monaco-table .${this._idPrefix} .slick-row .focused { background-color: ${styles.listFocusBackground}; }`);
content.push(`.monaco-table .${this._idPrefix} .slick-row .active { background-color: ${styles.listFocusBackground}; }`);
}
if (styles.listFocusForeground) {
content.push(`.monaco-table .${this._idPrefix} .slick-row .focused { color: ${styles.listFocusForeground}; }`);
content.push(`.monaco-table .${this._idPrefix} .slick-row .active { color: ${styles.listFocusForeground}; }`);
}
if (styles.listActiveSelectionBackground) {

View File

@@ -5,9 +5,12 @@
import { Table } from './table';
import { TableDataView } from './tableDataView';
import { View, Orientation, AbstractCollapsibleView, HeaderView, IViewOptions, ICollapsibleViewOptions } from 'vs/base/browser/ui/splitview/splitview';
import { View, Orientation, AbstractCollapsibleView, HeaderView, ICollapsibleViewOptions, IViewOptions, CollapsibleState } from 'sql/base/browser/ui/splitview/splitview';
import { $ } from 'vs/base/browser/builder';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as DOM from 'vs/base/browser/dom';
import * as lifecycle from 'vs/base/common/lifecycle';
export class TableBasicView<T> extends View {
private _table: Table<T>;
@@ -84,6 +87,7 @@ export class TableHeaderView<T> extends HeaderView {
export class TableCollapsibleView<T> extends AbstractCollapsibleView {
private _table: Table<T>;
private _container: HTMLElement;
private _headerTabListener: lifecycle.IDisposable;
constructor(
private _viewTitle: string,
@@ -98,6 +102,29 @@ export class TableCollapsibleView<T> extends AbstractCollapsibleView {
this._table = new Table<T>(this._container, data, columns, tableOpts);
}
public render(container: HTMLElement, orientation: Orientation): void {
super.render(container, orientation);
this._headerTabListener = DOM.addDisposableListener(this.header, DOM.EventType.KEY_DOWN, (e) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Tab) && this.state === CollapsibleState.EXPANDED) {
let element = this._table.getSelectedRows();
if (!element || element.length === 0) {
this._table.setSelectedRows([0]);
this._table.setActiveCell(0, 1);
e.stopImmediatePropagation();
}
}
});
}
public dispose(): void {
if (this._headerTabListener) {
this._headerTabListener.dispose();
this._headerTabListener = null;
}
super.dispose();
}
public addContainerClass(className: string) {
this._container.classList.add(className);
}

View File

@@ -63,6 +63,15 @@
background-image: url('query-plan-inverse.svg');
}
.vs .icon.actualQueryPlan {
background-image: url('query-plan.svg');
}
.vs-dark .icon.actualQueryPlan,
.hc-black .icon.actualQueryPlan {
background-image: url('query-plan-inverse.svg');
}
.vs .icon.createInsight {
background-image: url('create_insight.svg');
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.custom-view-tree-node-item {
display: flex;
height: 22px;
line-height: 22px;
}
.custom-view-tree-node-item > .custom-view-tree-node-item-icon {
background-size: 16px;
background-position: left center;
background-repeat: no-repeat;
padding-right: 6px;
width: 16px;
height: 22px;
-webkit-font-smoothing: antialiased;
}
.custom-view-tree-node-item > .custom-view-tree-node-item-label {
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
}

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 * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IThemable } from 'vs/platform/theme/common/styler';
import * as errors from 'vs/base/common/errors';
import { $ } from 'vs/base/browser/builder';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { prepareActions } from 'vs/workbench/browser/actions';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { AbstractCollapsibleView, CollapsibleState, IView as IBaseView, SplitView, ViewSizing } from 'sql/base/browser/ui/splitview/splitview';
export interface IViewOptions {
id: string;
name: string;
actionRunner: IActionRunner;
collapsed: boolean;
}
export interface IViewConstructorSignature {
new(initialSize: number, options: IViewOptions, ...services: { _serviceBrand: any; }[]): IView;
}
export interface IView extends IBaseView, IThemable {
id: string;
name: string;
getHeaderElement(): HTMLElement;
create(): TPromise<void>;
setVisible(visible: boolean): TPromise<void>;
isVisible(): boolean;
getActions(): IAction[];
getSecondaryActions(): IAction[];
getActionItem(action: IAction): IActionItem;
getActionsContext(): any;
showHeader(): boolean;
hideHeader(): boolean;
focusBody(): void;
isExpanded(): boolean;
expand(): void;
collapse(): void;
getOptimalWidth(): number;
shutdown(): void;
}
export interface ICollapsibleViewOptions extends IViewOptions {
ariaHeaderLabel?: string;
sizing: ViewSizing;
initialBodySize?: number;
}
export abstract class CollapsibleView extends AbstractCollapsibleView implements IView {
readonly id: string;
readonly name: string;
protected treeContainer: HTMLElement;
protected tree: ITree;
protected toDispose: IDisposable[];
protected toolBar: ToolBar;
protected actionRunner: IActionRunner;
protected isDisposed: boolean;
private _isVisible: boolean;
private dragHandler: DelayedDragHandler;
constructor(
initialSize: number,
options: ICollapsibleViewOptions,
protected keybindingService: IKeybindingService,
protected contextMenuService: IContextMenuService
) {
super(initialSize, {
ariaHeaderLabel: options.ariaHeaderLabel,
sizing: options.sizing,
bodySize: options.initialBodySize ? options.initialBodySize : 4 * 22,
initialState: options.collapsed ? CollapsibleState.COLLAPSED : CollapsibleState.EXPANDED,
});
this.id = options.id;
this.name = options.name;
this.actionRunner = options.actionRunner;
this.toDispose = [];
}
protected changeState(state: CollapsibleState): void {
this.updateTreeVisibility(this.tree, state === CollapsibleState.EXPANDED);
super.changeState(state);
}
get draggableLabel(): string { return this.name; }
public create(): TPromise<void> {
return TPromise.as(null);
}
getHeaderElement(): HTMLElement {
return this.header;
}
public renderHeader(container: HTMLElement): void {
// Tool bar
this.toolBar = new ToolBar($('div.actions').appendTo(container).getHTMLElement(), this.contextMenuService, {
orientation: ActionsOrientation.HORIZONTAL,
actionItemProvider: (action) => this.getActionItem(action),
ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.name),
getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id)
});
this.toolBar.actionRunner = this.actionRunner;
this.updateActions();
// Expand on drag over
this.dragHandler = new DelayedDragHandler(container, () => {
if (!this.isExpanded()) {
this.expand();
}
});
}
protected updateActions(): void {
this.toolBar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions()))();
this.toolBar.context = this.getActionsContext();
}
protected renderViewTree(container: HTMLElement): HTMLElement {
const treeContainer = document.createElement('div');
container.appendChild(treeContainer);
return treeContainer;
}
public getViewer(): ITree {
return this.tree;
}
public isVisible(): boolean {
return this._isVisible;
}
public setVisible(visible: boolean): TPromise<void> {
if (this._isVisible !== visible) {
this._isVisible = visible;
this.updateTreeVisibility(this.tree, visible && this.state === CollapsibleState.EXPANDED);
}
return TPromise.as(null);
}
public focusBody(): void {
this.focusTree();
}
protected reveal(element: any, relativeTop?: number): TPromise<void> {
if (!this.tree) {
return TPromise.as(null); // return early if viewlet has not yet been created
}
return this.tree.reveal(element, relativeTop);
}
public layoutBody(size: number): void {
if (this.tree) {
this.treeContainer.style.height = size + 'px';
this.tree.layout(size);
}
}
public getActions(): IAction[] {
return [];
}
public getSecondaryActions(): IAction[] {
return [];
}
public getActionItem(action: IAction): IActionItem {
return null;
}
public getActionsContext(): any {
return undefined;
}
public shutdown(): void {
// Subclass to implement
}
public getOptimalWidth(): number {
return 0;
}
public dispose(): void {
this.isDisposed = true;
this.treeContainer = null;
if (this.tree) {
this.tree.dispose();
}
if (this.dragHandler) {
this.dragHandler.dispose();
}
this.toDispose = dispose(this.toDispose);
if (this.toolBar) {
this.toolBar.dispose();
}
super.dispose();
}
private updateTreeVisibility(tree: ITree, isVisible: boolean): void {
if (!tree) {
return;
}
if (isVisible) {
$(tree.getHTMLElement()).show();
} else {
$(tree.getHTMLElement()).hide(); // make sure the tree goes out of the tabindex world by hiding it
}
if (isVisible) {
tree.onVisible();
} else {
tree.onHidden();
}
}
private focusTree(): void {
if (!this.tree) {
return; // return early if viewlet has not yet been created
}
// Make sure the current selected element is revealed
const selection = this.tree.getSelection();
if (selection.length > 0) {
this.reveal(selection[0], 0.5).done(null, errors.onUnexpectedError);
}
// Pass Focus to Viewer
this.tree.DOMFocus();
}
}
export interface IViewletViewOptions extends IViewOptions {
viewletSettings: object;
}
export interface IViewState {
collapsed: boolean;
size: number | undefined;
isHidden: boolean;
order: number;
}

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import Event from 'vs/base/common/event';
import { Command } from 'vs/editor/common/modes';
export type TreeViewItemHandleArg = {
$treeViewId: string,
$treeItemHandle: number
};
export enum TreeItemCollapsibleState {
None = 0,
Collapsed = 1,
Expanded = 2
}
export interface ITreeItem {
handle: number;
label: string;
icon?: string;
iconDark?: string;
contextValue?: string;
command?: Command;
children?: ITreeItem[];
collapsibleState?: TreeItemCollapsibleState;
}
export interface ITreeViewDataProvider {
onDidChange: Event<ITreeItem[] | undefined | null>;
onDispose: Event<void>;
getElements(): TPromise<ITreeItem[]>;
getChildren(element: ITreeItem): TPromise<ITreeItem[]>;
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Subscription } from 'rxjs/Subscription';
import { IDisposable } from 'vs/base/common/lifecycle';
export function subscriptionToDisposable(sub: Subscription): IDisposable {
return {
dispose: sub.unsubscribe
};
}

114
src/sql/base/common/map.ts Normal file
View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
// --- trie'ish datastructure
class Node<E> {
element?: E;
readonly children = new Map<string, Node<E>>();
}
/**
* A trie map that allows for fast look up when keys are substrings
* to the actual search keys (dir/subdir-problem).
*/
export class TrieMap<E> {
static PathSplitter = (s: string) => s.split(/[\\/]/).filter(s => !!s);
private readonly _splitter: (s: string) => string[];
private _root = new Node<E>();
constructor(splitter: (s: string) => string[] = TrieMap.PathSplitter) {
this._splitter = s => splitter(s).filter(s => Boolean(s));
}
insert(path: string, element: E): void {
const parts = this._splitter(path);
let i = 0;
// find insertion node
let node = this._root;
for (; i < parts.length; i++) {
let child = node.children.get(parts[i]);
if (child) {
node = child;
continue;
}
break;
}
// create new nodes
let newNode: Node<E>;
for (; i < parts.length; i++) {
newNode = new Node<E>();
node.children.set(parts[i], newNode);
node = newNode;
}
node.element = element;
}
lookUp(path: string): E {
const parts = this._splitter(path);
let { children } = this._root;
let node: Node<E>;
for (const part of parts) {
node = children.get(part);
if (!node) {
return undefined;
}
children = node.children;
}
return node.element;
}
findSubstr(path: string): E {
const parts = this._splitter(path);
let lastNode: Node<E>;
let { children } = this._root;
for (const part of parts) {
const node = children.get(part);
if (!node) {
break;
}
if (node.element) {
lastNode = node;
}
children = node.children;
}
// return the last matching node
// that had an element
if (lastNode) {
return lastNode.element;
}
return undefined;
}
findSuperstr(path: string): TrieMap<E> {
const parts = this._splitter(path);
let { children } = this._root;
let node: Node<E>;
for (const part of parts) {
node = children.get(part);
if (!node) {
return undefined;
}
children = node.children;
}
const result = new TrieMap<E>(this._splitter);
result._root = node;
return result;
}
}

View File

@@ -20,7 +20,7 @@ export function mixin(destination: any, source: any, overwrite: boolean = true,
if (Types.isObject(destination[key]) && Types.isObject(source[key])) {
mixin(destination[key], source[key], overwrite, fn);
} else if(fn) {
fn(destination[key], source[key], overwrite);
destination[key] = fn(destination[key], source[key], overwrite);
} else {
destination[key] = source[key];
}

View File

@@ -1,103 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as electron from 'electron';
import * as urlLib from 'url';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { warn } from 'sql/base/common/log';
export class ProxyOAuthHandler {
_serviceBrand: any;
private disposables: IDisposable[] = [];
constructor( @IWindowsMainService private windowsService: IWindowsMainService) {
let self = this;
electron.ipcMain.on('oauth', (event, args) => {
self.onOAuthRequest(event, args);
});
}
private static sendOAuthResponse(event, eventId: string, error: any, code: string) {
let args = {
eventId: eventId,
error: error,
code: code
};
event.sender.send('oauth-reply', args);
}
private onOAuthRequest(event, args) {
// Verify the arguments are correct
if (!args || args['eventId'] === undefined) {
warn('Received OAuth request with invalid arguments');
return;
}
let eventId: string = args['eventId'];
let url: string = args['url'];
let silent: boolean = args['silent'];
let windowConfig = {
show: !silent,
width: 568,
height: 720,
resizable: false,
center: false,
alwaysOnTop: true,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: false
}
};
let authWindow = new electron.BrowserWindow(windowConfig);
// TODO: Determine if we need to do the fancy logic that devdiv did to get around buggy AAD calls
// Define a function that will parse the redirect URI
// NOTE: This is defined like this b/c we need access to a lot of scope
function onCallback(webEvent, url: string) {
let query: any = urlLib.parse(url, true).query;
let code: string = query.code;
let error: string = query.error;
if (error !== undefined) {
// We received an error
ProxyOAuthHandler.sendOAuthResponse(event, eventId, error, null);
} else if (code) {
// We received a successful authorization code
ProxyOAuthHandler.sendOAuthResponse(event, eventId, null, code);
} else {
// We didn't get a code or error, so let this redirect continue on
return;
}
// We got an error or a code, so we should close the window without redirecting
authWindow.removeAllListeners('closed');
authWindow.close();
webEvent.preventDefault();
}
// Remove all 'will-navigate' events since VS Code prevents navigation by default
authWindow.webContents.removeAllListeners('will-navigate');
// Setup event handlers
// closed -> user closed the window
// will-navigate, did-get-redirect-request -> OAuth redirected back to the redirect URL
authWindow.on('closed', () => { ProxyOAuthHandler.sendOAuthResponse(event, eventId, 'User cancelled authentication', null); });
authWindow.webContents.on('will-navigate', (event, url) => { onCallback(event, url); });
authWindow.webContents.on('did-get-redirect-request', (event, oldUrl, newUrl) => { onCallback(event, newUrl); });
// Load the URL
authWindow.loadURL(url);
}
public dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -12,4 +12,4 @@ export const SerializationDisabled = 'Saving results into different format disab
* Feature names
*/
export const RestoreFeatureName = 'restore';
export const BackupFeatureName = 'backup';
export const BackupFeatureName = 'backup';

View File

@@ -8,7 +8,7 @@ import * as os from 'os';
import URI from 'vs/base/common/uri';
import { UNTITLED_SCHEMA } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
export const FILE_SCHEMA: string = 'file';
@@ -40,6 +40,10 @@ export function resolveFilePath(uri: string, filePath: string, rootPath: string)
}
export function getRootPath(contextService: IWorkspaceContextService): string {
return contextService.hasWorkspace() && contextService.getWorkspace().roots[0]
? contextService.getWorkspace().roots[0].fsPath : undefined;
let isWorkspace = contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
if (isWorkspace) {
return contextService.getWorkspace().folders[0].uri.fsPath;
}
return undefined;
}

View File

@@ -38,3 +38,4 @@ export const Profiler = 'Profiler';
export const ServerGroups = 'ServerGroups';
export const Accounts = 'Accounts';
export const FireWallRule = 'FirewallRule';
export const AutoOAuth = 'AutoOAuth';

View File

@@ -75,4 +75,4 @@ export function addTelemetry(
resolve();
}
});
}
}

View File

@@ -10,4 +10,5 @@ import * as nls from 'vs/nls';
export const tableHeaderBackground = registerColor('table.headerBackground', { dark: new Color(new RGBA(51, 51, 52)), light: new Color(new RGBA(245, 245, 245)), hc: null }, nls.localize('tableHeaderBackground', 'Table header background color'));
export const tableHeaderForeground = registerColor('table.headerForeground', { dark: new Color(new RGBA(229, 229, 229)), light: new Color(new RGBA(16, 16, 16)), hc: null }, nls.localize('tableHeaderForeground', 'Table header foreground color'));
export const disabledInputBackground = registerColor('input.disabled.background', { dark: '#444444', light: '#dcdcdc', hc: Color.black }, nls.localize('disabledInputBoxBackground', "Disabled Input box background."));
export const disabledInputForeground = registerColor('input.disabled.foreground', { dark: '#888888', light: '#888888', hc: foreground }, nls.localize('disabledInputBoxForeground', "Disabled Input box foreground."));
export const disabledInputForeground = registerColor('input.disabled.foreground', { dark: '#888888', light: '#888888', hc: foreground }, nls.localize('disabledInputBoxForeground', "Disabled Input box foreground."));
export const buttonFocusOutline = registerColor('button.focusOutline', { dark: '#eaeaea', light: '#666666', hc: null }, nls.localize('buttonFocusOutline', "Button outline color when focused."));

View File

@@ -225,3 +225,18 @@ export function attachEditableDropdownStyler(widget: IThemable, themeService: IT
contextBorder: (style && style.contextBorder) || cr.inputBorder
}, widget);
}
export function attachButtonStyler(widget: IThemable, themeService: IThemeService, style?: {
buttonForeground?: cr.ColorIdentifier,
buttonBackground?: cr.ColorIdentifier,
buttonHoverBackground?: cr.ColorIdentifier,
buttonFocusOutline?: cr.ColorIdentifier
}): IDisposable {
return attachStyler(themeService, {
buttonForeground: (style && style.buttonForeground) || cr.buttonForeground,
buttonBackground: (style && style.buttonBackground) || cr.buttonBackground,
buttonHoverBackground: (style && style.buttonHoverBackground) || cr.buttonHoverBackground,
buttonBorder: cr.contrastBorder,
buttonFocusOutline: (style && style.buttonFocusOutline) || sqlcolors.buttonFocusOutline
}, widget);
}

115
src/sql/data.d.ts vendored
View File

@@ -15,7 +15,8 @@ declare module 'data' {
/**
* An [event](#Event) which fires when the specific flavor of a language used in DMP
* connections has changed. And example is for a SQL connection, the flavor changes.
* connections has changed. And example is for a SQL connection, the flavor changes
* to MSSQL
*/
export const onDidChangeLanguageFlavor: vscode.Event<DidChangeLanguageFlavorParams>;
}
@@ -396,6 +397,16 @@ declare module 'data' {
getViewInfo(connectionUri: string, metadata: ObjectMetadata): Thenable<ColumnMetadata[]>;
}
export enum ScriptOperation {
Select = 0,
Create = 1,
Insert = 2,
Update = 3,
Delete = 4,
Execute = 5,
Alter = 6
}
export interface ScriptingResult {
operationId: string;
script: string;
@@ -409,15 +420,8 @@ declare module 'data' {
}
export interface ScriptingProvider {
scriptAsSelect(connectionUri: string, metadata: ObjectMetadata, paramDetails: ScriptingParamDetails): Thenable<ScriptingResult>;
scriptAsCreate(connectionUri: string, metadata: ObjectMetadata, paramDetails: ScriptingParamDetails): Thenable<ScriptingResult>;
scriptAsInsert(connectionUri: string, metadata: ObjectMetadata, paramDetails: ScriptingParamDetails): Thenable<ScriptingResult>;
scriptAsUpdate(connectionUri: string, metadata: ObjectMetadata, paramDetails: ScriptingParamDetails): Thenable<ScriptingResult>;
scriptAsDelete(connectionUri: string, metadata: ObjectMetadata, paramDetails: ScriptingParamDetails): Thenable<ScriptingResult>;
scriptAsOperation(connectionUri: string, operation: ScriptOperation, metadata: ObjectMetadata, paramDetails: ScriptingParamDetails): Thenable<ScriptingResult>;
registerOnScriptingComplete(handler: (scriptingCompleteResult: ScriptingCompleteResult) => any);
}
@@ -464,6 +468,8 @@ declare module 'data' {
taskServicesProvider: TaskServicesProvider;
fileBrowserProvider: FileBrowserProvider;
profilerProvider: ProfilerProvider;
}
/**
@@ -505,7 +511,7 @@ declare module 'data' {
export interface QueryProvider {
handle: number;
// TODO replace this temporary queryType field to detect "MSSQL" vs "Other" with a standard definition for supported platform
// TODO replace this temporary queryType field with a standard definition for supported platform
queryType: string;
cancelQuery(ownerUri: string): Thenable<QueryCancelResult>;
runQuery(ownerUri: string, selection: ISelectionData, runOptions?: ExecutionPlanOptions): Thenable<void>;
@@ -1023,12 +1029,14 @@ declare module 'data' {
errorMessage: string;
}
export interface IProfilerProvider {
export interface ProfilerProvider {
startSession(sessionId: string): Thenable<boolean>;
stopSession(sessionId: string): Thenable<boolean>;
pauseSession(sessionId: string): Thenable<boolean>;
connectSession(sessionId: string): Thenable<boolean>;
disconnectSession(sessionId: string): Thenable<boolean>;
registerOnSessionEventsAvailable(handler: (response: ProfilerSessionEvents) => any);
}
export interface IProfilerTableRow {
@@ -1045,6 +1053,32 @@ declare module 'data' {
data: IProfilerTableRow;
}
/**
* Profiler Event
*/
export interface ProfilerEvent {
/**
* Event class name
*/
name: string;
/**
* Event timestamp
*/
timestamp: string;
/**
* Event values
*/
values: {};
}
export interface ProfilerSessionEvents {
sessionId: string;
events: ProfilerEvent[];
}
// File browser interfaces -----------------------------------------------------------------------
export interface FileBrowserProvider {
@@ -1100,29 +1134,28 @@ declare module 'data' {
export function registerAccountProvider(providerMetadata: AccountProviderMetadata, provider: AccountProvider): vscode.Disposable;
/**
* Performs OAuth via the account management service and returns the resulting authorization code
* @param {string} url URL to load to begin OAuth
* @param {boolean} silent Whether or not to show the browser, use false when doing initial
* login, true when doing subsequent auth requests
* @return {Thenable<string>} Promise to return the authorization code, rejects on failure
* Launches a flyout dialog that will display the information on how to complete device
* code OAuth login to the user. Only one flyout can be opened at once and each must be closed
* by calling {@link endAutoOAuthDeviceCode}.
* @param {string} providerId ID of the provider that's requesting the flyout be opened
* @param {string} title
* @param {string} message
* @param {string} userCode
* @param {string} uri
*/
export function performOAuthAuthorization(url: string, silent: boolean): Thenable<string>;
}
// - ACCOUNT DATATYPES /////////////////////////////////////////////////
/**
* Image to display for an account
*/
export interface AccountContextualLogo {
/**
* Image to display on light theme
*/
light: string;
export function beginAutoOAuthDeviceCode(providerId: string, title: string, message: string, userCode: string, uri: string): Thenable<void>;
/**
* Image to display on dark theme
* Closes the flyout dialog opened by {@link beginAutoOAuthDeviceCode}
*/
dark: string;
export function endAutoOAuthDeviceCode(): void;
/**
* Notifies the account management service that an account has updated (usually due to the
* account going stale).
* @param {Account} updatedAccount Account object with updated properties
*/
export function accountUpdated(updatedAccount: Account): void;
}
/**
@@ -1135,10 +1168,9 @@ declare module 'data' {
contextualDisplayName: string;
/**
* Contents of the logo to display alongside the account. Indicates the context of the
* account provider (eg, Work/School vs Microsoft Account)
*/
contextualLogo: AccountContextualLogo;
accountType: string;
/**
* A display name that identifies the account, such as "user@contoso.com".
@@ -1192,6 +1224,18 @@ declare module 'data' {
}
// - ACCOUNT PROVIDER //////////////////////////////////////////////////
/**
* Error to be used when the user has cancelled the prompt or refresh methods. When
* AccountProvider.refresh or AccountProvider.prompt are rejected with this error, the error
* will not be reported to the user.
*/
export interface UserCancelledSignInError extends Error {
/**
* Type guard for differentiating user cancelled sign in errors from other errors
*/
userCancelledSignIn: boolean;
}
/**
* Represents a provider of accounts.
*/
@@ -1254,6 +1298,13 @@ declare module 'data' {
* @param accountKey - Key that uniquely identifies the account to clear
*/
clear(accountKey: AccountKey): Thenable<void>;
/**
* Called from the account management service when the user has cancelled an auto OAuth
* authorization process. Implementations should use this to cancel any polling process
* and call the end OAuth method.
*/
autoOAuthCancelled(): Thenable<void>;
}
// Resource provider interfaces -----------------------------------------------------------------------

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
/* Activity Bar - connection */
.monaco-workbench > .activitybar > .content > .monaco-action-bar .action-label.connectionViewlet {
.monaco-workbench > .activitybar .monaco-action-bar .action-label.connectionViewlet {
-webkit-mask: url('icons/server_page_inverse.svg') no-repeat 50% 50%;
-webkit-mask-size: 25px 25px;
}
/* Activity Bar - task history */
.monaco-workbench > .activitybar > .content > .monaco-action-bar .action-label.taskHistoryViewlet {
.monaco-workbench > .activitybar .monaco-action-bar .action-label.taskHistoryViewlet {
-webkit-mask: url('icons/run_history_inverse.svg') no-repeat 50% 50%;
-webkit-mask-size: 25px 25px;
}

View File

@@ -81,9 +81,9 @@
content: url("status_success.svg");
}
.vs .icon.cancelled,
.vs-dark .icon.cancelled,
.hc-black .icon.cancelled {
.vs .icon.canceled,
.vs-dark .icon.canceled,
.hc-black .icon.canceled {
content: url("status_cancelled.svg");
}
@@ -99,9 +99,9 @@
.vs .icon.scriptToClipboard,
.vs-dark .icon.scriptToClipboard,
.hc-black .icon.scriptToClipboard {
content: url('script_to_clipboard.svg');
width: auto !important;
height: 25px;
background-image: url('script_to_clipboard.svg');
background-repeat: no-repeat;
background-position: 2px center;
}
.vs .icon.close {
@@ -171,4 +171,13 @@
.vs .icon.configure-dashboard {
background: url('configdashboard.svg') center center no-repeat;
}
}
.hc-black .icon.edit,
.vs-dark .icon.edit {
background: url('edit_inverse.svg') center center no-repeat;
}
.vs .icon.edit {
background: url('edit.svg') center center no-repeat;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 4.28l-11.673 11.72h-4.327v-4.406l11.477-11.594h.308l4.215 4.237v.043z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M14.598 4.25l-1.688 1.75-3-3 1.688-1.75 3 3zm-5.688-.25l-7 7 3 3 7-7-3-3zm-7.91 8.09v2.91h2.91l-2.91-2.91z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#2d2d30;} .icon-vs-out{fill:#2d2d30;} .icon-vs-bg{fill:#c5c5c5;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 4.28l-11.673 11.72h-4.327v-4.406l11.477-11.594h.308l4.215 4.237v.043z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M14.598 4.25l-1.688 1.75-3-3 1.688-1.75 3 3zm-5.688-.25l-7 7 3 3 7-7-3-3zm-7.91 8.09v2.91h2.91l-2.91-2.91z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -213,12 +213,24 @@
background: url("Key_PrimaryKey.svg") center center no-repeat;
}
.vs .icon.index_primarykey,
.vs-dark .icon.index_primarykey,
.hc-black .icon.index_primarykey {
background: url("Key_PrimaryKey.svg") center center no-repeat;
}
.vs .icon.key_uniquekey,
.vs-dark .icon.key_uniquekey,
.hc-black .icon.key_uniquekey {
background: url("Key_UniqueKey.svg") center center no-repeat;
}
.vs .icon.index_uniquekey,
.vs-dark .icon.index_uniquekey,
.hc-black .icon.index_uniquekey {
background: url("Key_UniqueKey.svg") center center no-repeat;
}
.vs .icon.masterkey,
.vs-dark .icon.masterkey,
.hc-black .icon.masterkey {

View File

@@ -8,15 +8,14 @@
import 'vs/css!./media/accountDialog';
import 'vs/css!sql/parts/accountManagement/common/media/accountActions';
import * as DOM from 'vs/base/browser/dom';
import { SplitView } from 'vs/base/browser/ui/splitview/splitview';
import { SplitView } from 'sql/base/browser/ui/splitview/splitview';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IListService } from 'vs/platform/list/browser/listService';
import { Button } from 'vs/base/browser/ui/button/button';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import Event, { Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachListStyler, attachButtonStyler } from 'vs/platform/theme/common/styler';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { ActionRunner } from 'vs/base/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -25,8 +24,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as data from 'data';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { AccountViewModel } from 'sql/parts/accountManagement/accountDialog/accountViewModel';
import { AddAccountAction } from 'sql/parts/accountManagement/common/accountActions';
import { AccountListRenderer, AccountListDelegate } from 'sql/parts/accountManagement/common/accountListRenderer';
@@ -67,7 +67,7 @@ export class AccountDialog extends Modal {
@IContextKeyService contextKeyService: IContextKeyService
) {
super(
localize('linkedAccounts', 'Linked Accounts'),
localize('linkedAccounts', 'Linked accounts'),
TelemetryKeys.Accounts,
partService,
telemetryService,
@@ -81,7 +81,7 @@ export class AccountDialog extends Modal {
this._actionRunner = new ActionRunner();
// Setup the event emitters
this._onAddAccountErrorEmitter = new Emitter<string>();
this._onAddAccountErrorEmitter = new Emitter<string>();
this._onCloseEmitter = new Emitter<void>();
// Create the view model and wire up the events

View File

@@ -18,8 +18,6 @@ import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
export class AccountListStatusbarItem implements IStatusbarItem {
private _rootElement: HTMLElement;
private _iconElement: HTMLElement;
private _toDispose: IDisposable[];
private _manageLinkedAccountAction: IAction;
@@ -32,11 +30,11 @@ export class AccountListStatusbarItem implements IStatusbarItem {
public render(container: HTMLElement): IDisposable {
// Create root element for account list
this._rootElement = append(container, $('.linked-account-staus'));
this._rootElement.title = ManageLinkedAccountAction.LABEL;
this._rootElement.onclick = () => this._onClick();
this._iconElement = append(this._rootElement, $('a.linked-account-status-selection'));
const rootElement = append(container, $('.linked-account-staus'));
const accountElement = append(rootElement, $('a.linked-account-status-selection'));
accountElement.title = ManageLinkedAccountAction.LABEL;
accountElement.onclick = () => this._onClick();
append(accountElement, $('.linked-account-icon'));
return combinedDisposable(this._toDispose);
}

View File

@@ -3,9 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.statusbar-item .linked-account-staus a.linked-account-status-selection {
background: url('accounts_statusbar_inverse.svg') center center no-repeat;
background-size: 12px;
height: 12px !important;
.statusbar-item .linked-account-staus a.linked-account-status-selection .linked-account-icon {
-webkit-mask: url('accounts_statusbar_inverse.svg') no-repeat 50% 50%;
-webkit-mask-size: 12px;
background-color: rgb(255, 255, 255);
width: 12px;
height: 22px;
}
.statusbar-item .linked-account-staus a.linked-account-status-selection {
padding: 0 5px 0 5px;
}

View File

@@ -34,6 +34,7 @@ export class AccountPicker extends Disposable {
private _refreshContainer: HTMLElement;
private _listContainer: HTMLElement;
private _dropdown: DropdownList;
private _refreshAccountAction: RefreshAccountAction;
// EVENTING ////////////////////////////////////////////////////////////
private _addAccountCompleteEmitter: Emitter<void>;
@@ -62,13 +63,6 @@ export class AccountPicker extends Disposable {
this._addAccountStartEmitter = new Emitter<void>();
this._onAccountSelectionChangeEvent = new Emitter<data.Account>();
// Create an account list
let delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
let accountRenderer = new AccountPickerListRenderer();
this._listContainer = DOM.$('div.account-list-container');
this._accountList = new List<data.Account>(this._listContainer, delegate, [accountRenderer]);
this._register(attachListStyler(this._accountList, this._themeService));
// Create the view model, wire up the events, and initialize with baseline data
this.viewModel = this._instantiationService.createInstance(AccountPickerViewModel, this._providerId);
this.viewModel.updateAccountListEvent(arg => {
@@ -76,8 +70,6 @@ export class AccountPicker extends Disposable {
this.updateAccountList(arg.accountList);
}
});
this.createAccountPickerComponent();
}
// PUBLIC METHODS //////////////////////////////////////////////////////
@@ -88,7 +80,18 @@ export class AccountPicker extends Disposable {
DOM.append(container, this._rootElement);
}
private createAccountPickerComponent() {
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Create account picker component
*/
public createAccountPickerComponent() {
// Create an account list
let delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
let accountRenderer = new AccountPickerListRenderer();
this._listContainer = DOM.$('div.account-list-container');
this._accountList = new List<data.Account>(this._listContainer, delegate, [accountRenderer]);
this._register(attachListStyler(this._accountList, this._themeService));
this._rootElement = DOM.$('div.account-picker-container');
// Create a dropdown for account picker
@@ -116,7 +119,8 @@ export class AccountPicker extends Disposable {
this._refreshContainer = DOM.append(this._rootElement, DOM.$('div.refresh-container'));
DOM.append(this._refreshContainer, DOM.$('div.icon warning'));
let actionBar = new ActionBar(this._refreshContainer, { animated: false });
actionBar.push(new RefreshAccountAction(RefreshAccountAction.ID, RefreshAccountAction.LABEL), { icon: false, label: true });
this._refreshAccountAction = this._instantiationService.createInstance(RefreshAccountAction);
actionBar.push(this._refreshAccountAction, { icon: false, label: true });
if (this._accountList.length > 0) {
this._accountList.setSelection([0]);
@@ -146,10 +150,12 @@ export class AccountPicker extends Disposable {
private onAccountSelectionChange(account: data.Account) {
this.viewModel.selectedAccount = account;
if (account && account.isStale) {
this._refreshAccountAction.account = account;
new Builder(this._refreshContainer).show();
} else {
new Builder(this._refreshContainer).hide();
}
this._onAccountSelectionChangeEvent.fire(account);
}
@@ -170,9 +176,9 @@ export class AccountPicker extends Disposable {
const badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
const label = DOM.append(row, DOM.$('div.label'));
icon.className = 'icon';
// Set the account icon
icon.style.background = `url('data:${account.displayInfo.contextualLogo.light}')`;
icon.classList.add('icon', account.displayInfo.accountType);
// TODO: Pick between the light and dark logo
label.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.contextualDisplayName + ')';

View File

@@ -54,6 +54,7 @@ export class AccountPickerService implements IAccountPickerService {
// TODO: expand support to multiple providers
const providerId: string = 'azurePublicCloud';
this._accountPicker = this._instantiationService.createInstance(AccountPicker, providerId);
this._accountPicker.createAccountPickerComponent();
}
this._accountPicker.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());

View File

@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/autoOAuthDialog';
import { Builder, $ } from 'vs/base/browser/builder';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IPartService } from 'vs/workbench/services/part/common/partService';
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 { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export class AutoOAuthDialog extends Modal {
private _copyAndOpenButton: Button;
private _closeButton: Button;
private _userCodeInputBox: InputBox;
private _websiteInputBox: InputBox;
private _descriptionElement: HTMLElement;
// EVENTING ////////////////////////////////////////////////////////////
private _onHandleAddAccount = new Emitter<void>();
public get onHandleAddAccount(): Event<void> { return this._onHandleAddAccount.event; }
private _onCancel = new Emitter<void>();
public get onCancel(): Event<void> { return this._onCancel.event; }
private _onCloseEvent = new Emitter<void>();
public get onCloseEvent(): Event<void> { return this._onCloseEvent.event; }
constructor(
@IPartService partService: IPartService,
@IThemeService private _themeService: IThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(
'',
TelemetryKeys.AutoOAuth,
partService,
telemetryService,
contextKeyService,
{
isFlyout: true,
hasBackButton: true,
hasSpinner: true
}
);
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
this.backButton.addListener('click', () => this.cancel());
this._register(attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }));
this._copyAndOpenButton = this.addFooterButton(localize('copyAndOpen', 'Copy & Open'), () => this.addAccount());
this._closeButton = this.addFooterButton(localize('cancel', 'Cancel'), () => this.cancel());
this.registerListeners();
this._userCodeInputBox.disable();
this._websiteInputBox.disable();
}
protected layout(height?: number): void {
// NO OP
}
protected renderBody(container: HTMLElement) {
$().div({ class: 'auto-oauth-description-section new-section' }, (descriptionContainer) => {
this._descriptionElement = descriptionContainer.getHTMLElement();
});
let addAccountSection;
$().div({ class: 'auto-oauth-info-section new-section' }, (addAccountContainer) => {
addAccountSection = addAccountContainer.getHTMLElement();
this._userCodeInputBox = this.createInputBoxHelper(addAccountContainer, localize('userCode', 'User code'));
this._websiteInputBox = this.createInputBoxHelper(addAccountContainer, localize('website', 'Website'));
});
new Builder(container).div({ class: 'auto-oauth-dialog' }, (builder) => {
builder.append(this._descriptionElement);
builder.append(addAccountSection);
});
}
private createInputBoxHelper(container: Builder, label: string): InputBox {
let inputBox: InputBox;
container.div({ class: 'dialog-input-section' }, (inputContainer) => {
inputContainer.div({ class: 'dialog-label' }, (labelContainer) => {
labelContainer.innerHtml(label);
});
inputContainer.div({ class: 'dialog-input' }, (inputCellContainer) => {
inputBox = new InputBox(inputCellContainer.getHTMLElement(), this._contextViewService);
});
});
return inputBox;
}
private registerListeners(): void {
// Theme styler
this._register(attachButtonStyler(this._copyAndOpenButton, this._themeService));
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(attachInputBoxStyler(this._userCodeInputBox, this._themeService));
this._register(attachInputBoxStyler(this._websiteInputBox, this._themeService));
}
/* Overwrite escape key behavior */
protected onClose() {
this.cancel();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.addAccount();
}
private addAccount() {
if (this._copyAndOpenButton.enabled) {
this._copyAndOpenButton.enabled = false;
this.showSpinner();
this._onHandleAddAccount.fire();
}
}
public cancel() {
this._onCancel.fire();
}
public close() {
this._copyAndOpenButton.enabled = true;
this._onCloseEvent.fire();
this.hideSpinner();
this.hide();
}
public open(title: string, message: string, userCode: string, uri: string) {
// Update dialog
this.title = title;
this._descriptionElement.innerText = message;
this._userCodeInputBox.value = userCode;
this._websiteInputBox.value = uri;
this.show();
this._copyAndOpenButton.focus();
}
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { AutoOAuthDialog } from 'sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialog';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
export class AutoOAuthDialogController {
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _autoOAuthDialog: AutoOAuthDialog;
private _providerId: string;
private _userCode: string;
private _uri: string;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IAccountManagementService private _accountManagementService: IAccountManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) {
this._providerId = null;
}
/**
* Open auto OAuth dialog
*/
public openAutoOAuthDialog(providerId: string, title: string, message: string, userCode: string, uri: string): Thenable<void> {
if (this._providerId !== null) {
// If a oauth flyout is already open, return an error
let errorMessage = localize('oauthFlyoutIsAlreadyOpen', 'Cannot start auto OAuth. An auto OAuth is already in progress.');
this._errorMessageService.showDialog(Severity.Error, '', errorMessage);
return Promise.reject(new Error('Auto OAuth dialog already open'));
}
// Create a new dialog if one doesn't exist
if (!this._autoOAuthDialog) {
this._autoOAuthDialog = this._instantiationService.createInstance(AutoOAuthDialog);
this._autoOAuthDialog.onHandleAddAccount(this.handleOnAddAccount, this);
this._autoOAuthDialog.onCancel(this.handleOnCancel, this);
this._autoOAuthDialog.onCloseEvent(this.handleOnClose, this);
this._autoOAuthDialog.render();
}
this._userCode = userCode;
this._uri = uri;
// Open the dialog
this._autoOAuthDialog.open(title, message, userCode, uri);
this._providerId = providerId;
return Promise.resolve();
}
/**
* Close auto OAuth dialog
*/
public closeAutoOAuthDialog(): void {
this._autoOAuthDialog.close();
this._providerId = null;
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private handleOnCancel(): void {
this._accountManagementService.cancelAutoOAuthDeviceCode(this._providerId);
}
private handleOnClose(): void {
this._providerId = null;
}
private handleOnAddAccount(): void {
this._accountManagementService.copyUserCodeAndOpenBrowser(this._userCode, this._uri);
}
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.auto-oauth-dialog {
padding: 15px
}
.modal .auto-oauth-dialog .new-section {
padding-bottom: 30px;
}
.modal .auto-oauth-dialog .dialog-input-section {
display: flex;
padding-left: 15px;
padding-right: 15px;
padding-top: 10px;
padding-bottom: 10px;
}
.modal .auto-oauth-dialog .dialog-input-section .dialog-label {
flex: 0 0 100px;
align-self: center;
}
.modal .auto-oauth-dialog .dialog-input-section .dialog-input {
flex: 1 1 auto;
}

View File

@@ -36,8 +36,6 @@ export class AddAccountAction extends Action {
constructor(
private _providerId: string,
@IMessageService private _messageService: IMessageService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IAccountManagementService private _accountManagementService: IAccountManagementService
) {
super(AddAccountAction.ID, AddAccountAction.LABEL);
@@ -99,21 +97,25 @@ export class RemoveAccountAction extends Action {
type: 'question'
};
if (!this._messageService.confirm(confirm)) {
return TPromise.as(false);
}
let confirmPromise = this._messageService.confirm(confirm);
return new TPromise((resolve, reject) => {
self._accountManagementService.removeAccount(self._account.key)
.then(
(result) => { resolve(result); },
(err) => {
// Must handle here as this is an independent action
self._errorMessageService.showDialog(Severity.Error,
localize('removeAccountFailed', 'Failed to remove account'), err);
resolve(false);
}
);
return confirmPromise.then(confirmation => {
if (!confirmation.confirmed) {
return TPromise.as(false);
} else {
return new TPromise((resolve, reject) => {
self._accountManagementService.removeAccount(self._account.key)
.then(
(result) => { resolve(result); },
(err) => {
// Must handle here as this is an independent action
self._errorMessageService.showDialog(Severity.Error,
localize('removeAccountFailed', 'Failed to remove account'), err);
resolve(false);
}
);
});
}
});
}
}
@@ -144,15 +146,31 @@ export class ApplyFilterAction extends Action {
export class RefreshAccountAction extends Action {
public static ID = 'account.refresh';
public static LABEL = localize('refreshAccount', 'Reenter your credentials');
public account: data.Account;
constructor(
id: string,
label: string
@IAccountManagementService private _accountManagementService: IAccountManagementService
) {
super(id, label, 'refresh-account-action icon refresh');
super(RefreshAccountAction.ID, RefreshAccountAction.LABEL, 'refresh-account-action icon refresh');
}
public run(): TPromise<boolean> {
// Todo: refresh the account
return TPromise.as(true);
let self = this;
return new TPromise((resolve, reject) => {
if (self.account) {
self._accountManagementService.refreshAccount(self.account)
.then(
() => {
resolve(true);
},
err => {
error(`Error while refreshing account: ${err}`);
reject(err);
}
);
} else {
let errorMessage = localize('NoAccountToRefresh', 'There is no account to refresh');
reject(errorMessage);
}
});
}
}

View File

@@ -13,7 +13,7 @@ import { ActionBar, IActionOptions } from 'vs/base/browser/ui/actionbar/actionba
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RemoveAccountAction, ApplyFilterAction, RefreshAccountAction } from 'sql/parts/accountManagement/common/accountActions';
import { RemoveAccountAction, RefreshAccountAction } from 'sql/parts/accountManagement/common/accountActions';
import * as data from 'data';
@@ -66,9 +66,7 @@ export class AccountPickerListRenderer implements IRenderer<data.Account, Accoun
public renderElement(account: data.Account, index: number, templateData: AccountListTemplate): void {
// Set the account icon
templateData.icon.classList.add('account-logo');
templateData.icon.style.background = `url('data:${account.displayInfo.contextualLogo.light}')`;
// TODO: Pick between the light and dark logo
templateData.icon.classList.add('account-logo', account.displayInfo.accountType);
templateData.contextualDisplayName.innerText = account.displayInfo.contextualDisplayName;
templateData.displayName.innerText = account.displayInfo.displayName;
@@ -114,9 +112,12 @@ export class AccountListRenderer extends AccountPickerListRenderer {
let actionOptions: IActionOptions = { icon: true, label: false };
if (account.isStale) {
templateData.actions.push(new RefreshAccountAction(RefreshAccountAction.ID, RefreshAccountAction.LABEL), actionOptions);
let refreshAction = this._instantiationService.createInstance(RefreshAccountAction);
refreshAction.account = account;
templateData.actions.push(refreshAction, actionOptions);
} else {
templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
// Todo: Will show filter action when API/GUI for filtering is implemented (#3022, #3024)
// templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
}
let removeAction = this._instantiationService.createInstance(RemoveAccountAction, account);

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { join } from 'path';
import { createCSSRule } from 'vs/base/browser/dom';
import URI from 'vs/base/common/uri';
import { ManageLinkedAccountAction } from 'sql/parts/accountManagement/accountListStatusbar/accountListStatusbarItem';
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
ManageLinkedAccountAction,
ManageLinkedAccountAction.ID,
ManageLinkedAccountAction.LABEL
),
ManageLinkedAccountAction.LABEL
);
export interface IAccountContrib {
id: string;
icon?: IUserFriendlyIcon;
}
export type IUserFriendlyIcon = string | { light: string; dark: string; };
const account: IJSONSchema = {
type: 'object',
properties: {
id: {
description: localize('carbon.extension.contributes.account.id', 'Identifier of the account type'),
type: 'string'
},
icon: {
description: localize('carbon.extension.contributes.account.icon', '(Optional) Icon which is used to represent the accpunt in the UI. Either a file path or a themable configuration'),
anyOf: [{
type: 'string'
},
{
type: 'object',
properties: {
light: {
description: localize('carbon.extension.contributes.account.icon.light', 'Icon path when a light theme is used'),
type: 'string'
},
dark: {
description: localize('carbon.extension.contributes.account.icon.dark', 'Icon path when a dark theme is used'),
type: 'string'
}
}
}]
}
}
};
export const accountsContribution: IJSONSchema = {
description: localize('carbon.extension.contributes.account', "Contributes icons to account provider."),
oneOf: [
account,
{
type: 'array',
items: account
}
]
};
ExtensionsRegistry.registerExtensionPoint<IAccountContrib | IAccountContrib[]>('account-type', [], accountsContribution).setHandler(extensions => {
function handleCommand(account: IAccountContrib, extension: IExtensionPointUser<any>) {
let { icon, id } = account;
let iconClass: string;
if (icon) {
iconClass = id;
if (typeof icon === 'string') {
const path = join(extension.description.extensionFolderPath, icon);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(path).toString()}")`);
} else {
const light = join(extension.description.extensionFolderPath, icon.light);
const dark = join(extension.description.extensionFolderPath, icon.dark);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(light).toString()}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(dark).toString()}")`);
}
}
}
for (let extension of extensions) {
const { value } = extension;
if (Array.isArray<IAccountContrib>(value)) {
for (let command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});

View File

@@ -6,7 +6,6 @@
'use strict';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IResourceProviderService, IHandleFirewallRuleResult } from 'sql/parts/accountManagement/common/interfaces';
@@ -26,7 +25,6 @@ export class ResourceProviderService implements IResourceProviderService {
constructor(
@ITelemetryService private _telemetryService: ITelemetryService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IEnvironmentService private _environmentService: IEnvironmentService
) {
}
@@ -66,39 +64,33 @@ export class ResourceProviderService implements IResourceProviderService {
* Handle a firewall rule
*/
public handleFirewallRule(errorCode: number, errorMessage: string, connectionTypeId: string): Promise<IHandleFirewallRuleResult> {
if (!this._environmentService.isBuilt) {
let self = this;
return new Promise<IHandleFirewallRuleResult>((resolve, reject) => {
let handleFirewallRuleResult: IHandleFirewallRuleResult;
let promises = [];
if (self._providers) {
for (let key in self._providers) {
let provider = self._providers[key];
promises.push(provider.handleFirewallRule(errorCode, errorMessage, connectionTypeId)
.then(response => {
if (response.result) {
handleFirewallRuleResult = { canHandleFirewallRule: response.result, ipAddress: response.ipAddress, resourceProviderId: key };
}
},
() => { /* Swallow failures at getting accounts, we'll just hide that provider */
}));
}
let self = this;
return new Promise<IHandleFirewallRuleResult>((resolve, reject) => {
let handleFirewallRuleResult: IHandleFirewallRuleResult;
let promises = [];
if (self._providers) {
for (let key in self._providers) {
let provider = self._providers[key];
promises.push(provider.handleFirewallRule(errorCode, errorMessage, connectionTypeId)
.then(response => {
if (response.result) {
handleFirewallRuleResult = { canHandleFirewallRule: response.result, ipAddress: response.ipAddress, resourceProviderId: key };
}
},
() => { /* Swallow failures at getting accounts, we'll just hide that provider */
}));
}
}
Promise.all(promises).then(() => {
if (handleFirewallRuleResult) {
resolve(handleFirewallRuleResult);
} else {
handleFirewallRuleResult = { canHandleFirewallRule: false, ipAddress: undefined, resourceProviderId: undefined };
resolve(handleFirewallRuleResult);
}
});
Promise.all(promises).then(() => {
if (handleFirewallRuleResult) {
resolve(handleFirewallRuleResult);
} else {
handleFirewallRuleResult = { canHandleFirewallRule: false, ipAddress: undefined, resourceProviderId: undefined };
resolve(handleFirewallRuleResult);
}
});
} else {
return new Promise<IHandleFirewallRuleResult>((resolve, reject) => {
resolve({ canHandleFirewallRule: false, ipAddress: undefined, resourceProviderId: undefined });
});
}
});
}
/**

View File

@@ -8,24 +8,23 @@
import 'vs/css!./media/firewallRuleDialog';
import { Builder, $ } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import Event, { Emitter } from 'vs/base/common/event';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { localize } from 'vs/nls';
import { buttonBackground } from 'vs/platform/theme/common/colorRegistry';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { attachButtonStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import * as data from 'data';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { FirewallRuleViewModel } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleViewModel';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { IAccountPickerService } from 'sql/parts/accountManagement/common/interfaces';
import * as TelemetryKeys from 'sql/common/telemetryKeys';

View File

@@ -10,7 +10,7 @@ import { localize } from 'vs/nls';
import * as data from 'data';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IConnectionManagementService, IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { FirewallRuleDialog } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
import { IResourceProviderService } from 'sql/parts/accountManagement/common/interfaces';
@@ -29,7 +29,6 @@ export class FirewallRuleDialogController {
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@IAccountManagementService private _accountManagementService: IAccountManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IExtensionGalleryService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
@@ -12,16 +12,16 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import { DashboardEditor } from 'sql/parts/dashboard/dashboardEditor';
import { DashboardInput } from 'sql/parts/dashboard/dashboardInput';
import { AddServerGroupAction, AddServerAction } from 'sql/parts/registeredServer/viewlet/connectionTreeAction';
import { ClearRecentConnectionsAction } from 'sql/parts/connection/common/connectionActions';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService';
import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorDescriptor } from 'vs/workbench/browser/editor';
import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService';
import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
import { AddServerGroupAction, AddServerAction } from 'sql/parts/registeredServer/viewlet/connectionTreeAction';
// Singletons
registerSingleton(IExtensionGalleryService, ExtensionGalleryService);
@@ -30,10 +30,9 @@ registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
// Connection Dashboard registration
const dashboardEditorDescriptor = new EditorDescriptor(
DashboardEditor,
DashboardEditor.ID,
'Dashboard',
'sql/parts/dashboard/dashboardEditor',
'DashboardEditor'
'Dashboard'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
@@ -50,7 +49,6 @@ actionRegistry.registerWorkbenchAction(
),
ClearRecentConnectionsAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
AddServerGroupAction,
@@ -82,8 +80,8 @@ configurationRegistry.registerConfiguration({
},
'sql.defaultEngine': {
'type': 'string',
'description': localize('sql.defaultEngineDescription', 'Default SQL Engine to use. This drives default language provider in .sql files and the default to use when creating a new connection.'),
'description': localize('sql.defaultEngineDescription', 'Default SQL Engine to use. This drives default language provider in .sql files and the default to use when creating a new connection. Valid option is currently MSSQL'),
'default': 'MSSQL'
},
}
});
});

View File

@@ -3,57 +3,84 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import Event, { Emitter } from 'vs/base/common/event';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
/**
* Locates the active editor and calls runQuery() on the editor if it is a QueryEditor.
* Workbench action to clear the recent connnections list
*/
export class ClearRecentConnectionsAction extends Action {
public static ID = 'clearRecentConnectionsAction';
public static LABEL = nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List');
public static ID = 'clearRecentConnectionsAction';
public static LABEL = nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List');
constructor(
id: string,
label: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IMessageService private _messageService: IMessageService,
@IQuickOpenService private _quickOpenService: IQuickOpenService
) {
super(id, label);
this.enabled = true;
}
public run(): TPromise<void> {
let self = this;
return self.promptToClearRecentConnectionsList().then(result => {
if (result) {
self._connectionManagementService.clearRecentConnectionsList();
self._messageService.show(Severity.Info, nls.localize('ClearedRecentConnections', 'Recent connections list cleared'));
}
});
}
private promptToClearRecentConnectionsList(): TPromise<boolean> {
const self = this;
return new TPromise<boolean>((resolve, reject) => {
let choices: { key, value }[] = [
{ key: nls.localize('yes', 'Yes'), value: true },
{ key: nls.localize('no', 'No'), value: false }
];
self._quickOpenService.pick(choices.map(x => x.key), { placeHolder: nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List'), ignoreFocusLost: true }).then((choice) => {
let confirm = choices.find(x => x.key === choice);
resolve(confirm && confirm.value);
});
});
}
}
/**
* Action to delete one recently used connection from the MRU
*/
export class ClearSingleRecentConnectionAction extends Action {
public static ID = 'clearSingleRecentConnectionAction';
public static LABEL = nls.localize('delete', 'Delete');
private _onRecentConnectionRemoved = new Emitter<void>();
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
constructor(
id: string,
label: string,
@IInstantiationService private _instantiationService: IInstantiationService,
private _connectionProfile: IConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IMessageService private _messageService: IMessageService,
@IQuickOpenService private _quickOpenService: IQuickOpenService
) {
super(id, label);
this.enabled = true;
}
public run(): TPromise<void> {
let self = this;
return self.promptToClearRecentConnectionsList().then(result => {
if (result) {
self._connectionManagementService.clearRecentConnectionsList();
self._messageService.show(Severity.Info, nls.localize('ClearedRecentConnections', 'Recent connections list cleared'));
}
return new TPromise<void>((resolve, reject) => {
resolve(this._connectionManagementService.clearRecentConnection(this._connectionProfile));
this._onRecentConnectionRemoved.fire();
});
}
private promptToClearRecentConnectionsList(): TPromise<boolean> {
const self = this;
return new TPromise<boolean>((resolve, reject) => {
let choices: { key, value }[] = [
{ key: nls.localize('yes', 'Yes'), value: true },
{ key: nls.localize('no', 'No'), value: false }
];
self._quickOpenService.pick(choices.map(x => x.key), { placeHolder: nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List'), ignoreFocusLost: true }).then((choice) => {
let confirm = choices.find(x => x.key === choice);
resolve(confirm && confirm.value);
});
});
}
}
}

View File

@@ -9,9 +9,9 @@ import * as Utils from './utils';
import { IConnectionProfile, IConnectionProfileStore } from './interfaces';
import { IConnectionConfig } from './iconnectionConfig';
import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionProfileGroup';
import { IConfigurationEditingService, ConfigurationTarget, IConfigurationValue } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ConfigurationEditingService, IConfigurationValue, IConfigurationValue as TConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IConfigurationValue as TConfigurationValue } from 'vs/platform/configuration/common/configuration';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConnectionProfile } from './connectionProfile';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import * as data from 'data';
@@ -30,22 +30,25 @@ export interface ISaveGroupResult {
export class ConnectionConfig implements IConnectionConfig {
private _providerCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
private _providerCachedCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
/**
* Constructor.
*/
public constructor(
private _configurationEditService: IConfigurationEditingService,
private _configurationEditService: ConfigurationEditingService,
private _workspaceConfigurationService: IWorkspaceConfigurationService,
private _capabilitiesService: ICapabilitiesService,
private _cachedMetadata?: data.DataProtocolServerCapabilities[]
cachedMetadata?: data.DataProtocolServerCapabilities[]
) {
this._providerCapabilitiesMap = {};
this._providerCachedCapabilitiesMap = {};
this.setCachedMetadata(cachedMetadata);
}
public setCachedMetadata(cachedMetaData: data.DataProtocolServerCapabilities[]): void {
this._cachedMetadata = cachedMetaData;
public setCachedMetadata(cachedMetadata: data.DataProtocolServerCapabilities[]): void {
if (cachedMetadata) {
cachedMetadata.forEach(item => {
this.updateCapabilitiesCache(item.providerName, item);
});
}
}
/**
@@ -58,7 +61,6 @@ export class ConnectionConfig implements IConnectionConfig {
let workspaceGroups = this.getConfiguration(Constants.connectionGroupsArrayName).workspace as IConnectionProfileGroup[];
if (userGroups) {
if (workspaceGroups) {
userGroups = userGroups.filter(x => workspaceGroups.find(f => this.isSameGroupName(f, x)) === undefined);
allGroups = allGroups.concat(workspaceGroups);
@@ -74,43 +76,44 @@ export class ConnectionConfig implements IConnectionConfig {
return allGroups;
}
private updateCapabilitiesCache(providerName: string, providerCapabilities: data.DataProtocolServerCapabilities): void {
if (providerName && providerCapabilities) {
this._providerCapabilitiesMap[providerName] = providerCapabilities;
}
}
private getCapabilitiesFromCache(providerName: string): data.DataProtocolServerCapabilities {
if (providerName in this._providerCapabilitiesMap) {
return this._providerCapabilitiesMap[providerName];
}
return undefined;
}
/**
* Returns the capabilities for given provider name. First tries to get it from capabilitiesService and if it's not registered yet,
* Gets the data from the metadata stored in the config
* @param providerName Provider Name
*/
public getCapabilities(providerName: string): data.DataProtocolServerCapabilities {
let result: data.DataProtocolServerCapabilities;
if (providerName in this._providerCapabilitiesMap) {
result = this._providerCapabilitiesMap[providerName];
let result: data.DataProtocolServerCapabilities = this.getCapabilitiesFromCache(providerName);
if (result) {
return result;
} else {
let capabilities = this._capabilitiesService.getCapabilities();
if (capabilities) {
let providerCapabilities = capabilities.find(c => c.providerName === providerName);
if (providerCapabilities) {
this._providerCapabilitiesMap[providerName] = providerCapabilities;
result = providerCapabilities;
this.updateCapabilitiesCache(providerName, providerCapabilities);
return providerCapabilities;
} else {
return undefined;
}
}
}
if (!result && this._cachedMetadata) {
if (providerName in this._providerCachedCapabilitiesMap) {
result = this._providerCachedCapabilitiesMap[providerName];
} else {
let metaDataFromConfig = this._cachedMetadata;
if (metaDataFromConfig) {
let providerCapabilities = metaDataFromConfig.find(m => m.providerName === providerName);
this._providerCachedCapabilitiesMap[providerName] = providerCapabilities;
result = providerCapabilities;
}
return undefined;
}
}
return result;
}
/**
* Add a new connection to the connection config.
*/
@@ -118,13 +121,13 @@ export class ConnectionConfig implements IConnectionConfig {
return new Promise<IConnectionProfile>((resolve, reject) => {
if (profile.saveProfile) {
this.addGroupFromProfile(profile).then(groupId => {
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
if (!profiles) {
profiles = [];
}
let providerCapabilities = this.getCapabilities(profile.providerName);
let connectionProfile = this.getConnectionProfileInstance(profile, groupId);
let connectionProfile = this.getConnectionProfileInstance(profile, groupId, providerCapabilities);
let newProfile = ConnectionProfile.convertToProfileStore(providerCapabilities, connectionProfile);
// Remove the profile if already set
@@ -151,9 +154,8 @@ export class ConnectionConfig implements IConnectionConfig {
});
}
private getConnectionProfileInstance(profile: IConnectionProfile, groupId: string): ConnectionProfile {
private getConnectionProfileInstance(profile: IConnectionProfile, groupId: string, providerCapabilities: data.DataProtocolServerCapabilities): ConnectionProfile {
let connectionProfile = profile as ConnectionProfile;
let providerCapabilities = this.getCapabilities(profile.providerName);
if (connectionProfile === undefined) {
connectionProfile = new ConnectionProfile(providerCapabilities, profile);
}
@@ -170,7 +172,7 @@ export class ConnectionConfig implements IConnectionConfig {
if (profile.groupId && profile.groupId !== Utils.defaultGroupId) {
resolve(profile.groupId);
} else {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let result = this.saveGroup(groups, profile.groupFullName, undefined, undefined);
groups = result.groups;
@@ -192,7 +194,7 @@ export class ConnectionConfig implements IConnectionConfig {
if (profileGroup.id) {
resolve(profileGroup.id);
} else {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let sameNameGroup = groups ? groups.find(group => group.name === profileGroup.name) : undefined;
if (sameNameGroup) {
let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists.");
@@ -221,7 +223,7 @@ export class ConnectionConfig implements IConnectionConfig {
profiles = <IConnectionProfileStore[]>configs.workspace;
}
if (profiles) {
if(this.fixConnectionIds(profiles)) {
if (this.fixConnectionIds(profiles)) {
this.writeConfiguration(Constants.connectionsArrayName, profiles, configTarget);
}
} else {
@@ -245,9 +247,9 @@ export class ConnectionConfig implements IConnectionConfig {
profile.id = generateUuid();
changed = true;
}
if (profile.id in idsCache) {
profile.id = generateUuid();
changed = true;
if (profile.id in idsCache) {
profile.id = generateUuid();
changed = true;
}
idsCache[profile.id] = true;
}
@@ -298,7 +300,7 @@ export class ConnectionConfig implements IConnectionConfig {
*/
public deleteConnection(profile: ConnectionProfile): Promise<void> {
// Get all connections in the settings
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
// Remove the profile from the connections
profiles = profiles.filter(value => {
let providerCapabilities = this.getCapabilities(value.providerName);
@@ -320,7 +322,7 @@ export class ConnectionConfig implements IConnectionConfig {
// Add selected group to subgroups list
subgroups.push(group);
// Get all connections in the settings
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
// Remove the profiles from the connections
profiles = profiles.filter(value => {
let providerCapabilities = this.getCapabilities(value.providerName);
@@ -329,7 +331,7 @@ export class ConnectionConfig implements IConnectionConfig {
});
// Get all groups in the settings
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
// Remove subgroups in the settings
groups = groups.filter((grp) => {
return !subgroups.some((item) => item.id === grp.id);
@@ -347,7 +349,7 @@ export class ConnectionConfig implements IConnectionConfig {
* Moves the source group under the target group.
*/
public changeGroupIdForConnectionGroup(source: ConnectionProfileGroup, target: ConnectionProfileGroup): Promise<void> {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
groups = groups.map(g => {
if (g.id === source.id) {
g.parentId = target.id;
@@ -372,8 +374,8 @@ export class ConnectionConfig implements IConnectionConfig {
*/
private changeGroupIdForConnectionInSettings(profile: ConnectionProfile, newGroupID: string, target: ConfigurationTarget = ConfigurationTarget.USER): Promise<void> {
return new Promise<void>((resolve, reject) => {
let profiles = target === ConfigurationTarget.USER ? this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user :
this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).workspace;
let profiles = target === ConfigurationTarget.USER ? this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user :
this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).workspace;
if (profiles) {
let providerCapabilities = this.getCapabilities(profile.providerName);
if (profile.parent && profile.parent.id === Constants.unsavedGroupId) {
@@ -429,7 +431,7 @@ export class ConnectionConfig implements IConnectionConfig {
}
public editGroup(source: ConnectionProfileGroup): Promise<void> {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let sameNameGroup = groups ? groups.find(group => group.name === source.name && group.id !== source.id) : undefined;
if (sameNameGroup) {
let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists.");
@@ -510,10 +512,9 @@ export class ConnectionConfig implements IConnectionConfig {
* @param parsedSettingsFile an object representing the parsed contents of the settings file.
* @returns the set of connection profiles found in the parsed settings file.
*/
private getConfiguration(key: string): TConfigurationValue<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]> {
let configs: TConfigurationValue<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]>;
configs = this._workspaceConfigurationService.lookup<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]>(key);
private getConfiguration(key: string): any {
let configs: any;
configs = this._workspaceConfigurationService.inspect<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]>(key);
return configs;
}
@@ -532,7 +533,7 @@ export class ConnectionConfig implements IConnectionConfig {
value: profiles
};
this._configurationEditService.writeConfiguration(target, configValue).then(result => {
this._workspaceConfigurationService.reloadConfiguration().then(() => {
this._workspaceConfigurationService.reloadConfiguration().then(() => {
resolve();
});
}, (error => {

View File

@@ -9,12 +9,12 @@ import { IViewlet } from 'vs/workbench/common/viewlet';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import Event from 'vs/base/common/event';
import { IAction } from 'vs/base/common/actions';
import Severity from 'vs/base/common/severity';
import data = require('data');
import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import Severity from 'vs/base/common/severity';
import { ISelectionData } from 'data';
import { ConnectionManagementInfo } from './connectionManagementInfo';
export const VIEWLET_ID = 'workbench.view.connections';
@@ -57,6 +57,7 @@ export interface IConnectionResult {
connected: boolean;
errorMessage: string;
errorCode: number;
callStack: string;
errorHandled?: boolean;
}
@@ -85,7 +86,7 @@ export interface IConnectionManagementService {
/**
* Opens the connection dialog to create new connection
*/
showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, error?: string): Promise<void>;
showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise<void>;
/**
* Opens the add server group dialog
@@ -135,6 +136,8 @@ export interface IConnectionManagementService {
clearRecentConnectionsList(): void;
clearRecentConnection(connectionProfile: IConnectionProfile) : void;
getActiveConnections(): ConnectionProfile[];
saveProfileGroup(profile: IConnectionProfileGroup): Promise<string>;
@@ -253,7 +256,7 @@ export interface IConnectionManagementService {
export const IConnectionDialogService = createDecorator<IConnectionDialogService>('connectionDialogService');
export interface IConnectionDialogService {
_serviceBrand: any;
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, error?: string): Thenable<void>;
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, connectionResult?: IConnectionResult): Thenable<void>;
}
export interface IServerGroupDialogCallbacks {
@@ -270,7 +273,7 @@ export interface IServerGroupController {
export const IErrorMessageService = createDecorator<IErrorMessageService>('errorMessageService');
export interface IErrorMessageService {
_serviceBrand: any;
showDialog(severity: Severity, headerTitle: string, message: string): void;
showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]): void;
}
export enum ServiceOptionType {
@@ -295,14 +298,15 @@ export enum RunQueryOnConnectionMode {
none = 0,
executeQuery = 1,
executeCurrentQuery = 2,
estimatedQueryPlan = 3
estimatedQueryPlan = 3,
actualQueryPlan = 4
}
export interface INewConnectionParams {
connectionType: ConnectionType;
input?: IConnectableInput;
runQueryOnCompletion?: RunQueryOnConnectionMode;
querySelection?: ISelectionData;
querySelection?: data.ISelectionData;
showDashboard?: boolean;
}

View File

@@ -29,7 +29,7 @@ export class ConnectionManagementInfo {
/**
* Callback for when a connection notification is received.
*/
public connectHandler: (result: boolean, errorMessage?: string, errorCode?: number) => void;
public connectHandler: (result: boolean, errorMessage?: string, errorCode?: number, callStack?: string) => void;
/**
* Information about the SQL Server instance.

View File

@@ -43,7 +43,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionProfileGroup';
import { IConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import Event, { Emitter } from 'vs/base/common/event';
@@ -78,6 +78,8 @@ export class ConnectionManagementService implements IConnectionManagementService
private _connectionGlobalStatus: ConnectionGlobalStatus;
private _configurationEditService: ConfigurationEditingService;
constructor(
private _connectionMemento: Memento,
private _connectionStore: ConnectionStore,
@@ -89,7 +91,6 @@ export class ConnectionManagementService implements IConnectionManagementService
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
@IStorageService private _storageService: IStorageService,
@ITelemetryService private _telemetryService: ITelemetryService,
@IConfigurationEditingService private _configurationEditService: IConfigurationEditingService,
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
@ICredentialsService private _credentialsService: ICredentialsService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@@ -100,13 +101,17 @@ export class ConnectionManagementService implements IConnectionManagementService
@IViewletService private _viewletService: IViewletService,
@IAngularEventingService private _angularEventing: IAngularEventingService
) {
if (this._instantiationService) {
this._configurationEditService = this._instantiationService.createInstance(ConfigurationEditingService);
}
// _connectionMemento and _connectionStore are in constructor to enable this class to be more testable
if (!this._connectionMemento) {
this._connectionMemento = new Memento('ConnectionManagement');
}
if (!this._connectionStore) {
this._connectionStore = new ConnectionStore(_storageService, this._connectionMemento,
_configurationEditService, this._workspaceConfigurationService, this._credentialsService, this._capabilitiesService);
this._configurationEditService, this._workspaceConfigurationService, this._credentialsService, this._capabilitiesService);
}
this._connectionStatusManager = new ConnectionStatusManager(this._capabilitiesService);
@@ -196,7 +201,7 @@ export class ConnectionManagementService implements IConnectionManagementService
// show the Registered Server viewlet
let startupConfig = this._workspaceConfigurationService.getConfiguration('startup');
if (startupConfig) {
let showServerViewlet = <boolean>startupConfig['alwaysShowServersView'];
let showServerViewlet = <boolean>startupConfig['alwaysShowServersView'];
if (showServerViewlet) {
// only show the Servers viewlet if there isn't another active viewlet
if (!this._viewletService.getActiveViewlet()) {
@@ -212,7 +217,7 @@ export class ConnectionManagementService implements IConnectionManagementService
* @param params Include the uri, type of connection
* @param model the existing connection profile to create a new one from
*/
public showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, error?: string): Promise<void> {
public showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise<void> {
let self = this;
return new Promise<void>((resolve, reject) => {
if (!params) {
@@ -221,7 +226,7 @@ export class ConnectionManagementService implements IConnectionManagementService
if (!model && params.input && params.input.uri) {
model = this._connectionStatusManager.getConnectionProfile(params.input.uri);
}
self._connectionDialogService.showDialog(self, params, model, error).then(() => {
self._connectionDialogService.showDialog(self, params, model, connectionResult).then(() => {
resolve();
}, dialogError => {
warn('failed to open the connection dialog. error: ' + dialogError);
@@ -301,7 +306,7 @@ export class ConnectionManagementService implements IConnectionManagementService
}
// If the password is required and still not loaded show the dialog
if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, errorCode: undefined }, options));
resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
} else {
// Try to connect
this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
@@ -340,7 +345,7 @@ export class ConnectionManagementService implements IConnectionManagementService
runQueryOnCompletion: RunQueryOnConnectionMode.none,
showDashboard: options.showDashboard
};
this.showConnectionDialog(params, connection, connectionResult.errorMessage).then(() => {
this.showConnectionDialog(params, connection, connectionResult).then(() => {
resolve(connectionResult);
}).catch(err => {
reject(err);
@@ -643,6 +648,10 @@ export class ConnectionManagementService implements IConnectionManagementService
return this._connectionStore.clearRecentlyUsed();
}
public clearRecentConnection(connectionProfile: IConnectionProfile) : void {
this._connectionStore.removeConnectionToMemento(connectionProfile, Constants.recentConnections);
}
public getActiveConnections(): ConnectionProfile[] {
return this._connectionStore.getActiveConnections();
}
@@ -825,6 +834,9 @@ export class ConnectionManagementService implements IConnectionManagementService
return new Promise<data.ListDatabasesResult>((resolve, reject) => {
let provider = this._providers[providerId];
provider.listDatabases(uri).then(result => {
if (result && result.databaseNames) {
result.databaseNames.sort();
}
resolve(result);
}, error => {
reject(error);
@@ -845,9 +857,9 @@ export class ConnectionManagementService implements IConnectionManagementService
/**
* Add a connection to the active connections list.
*/
private tryAddActiveConnection(connectionManagementInfo: ConnectionManagementInfo, newConnection: IConnectionProfile): void {
private tryAddActiveConnection(connectionManagementInfo: ConnectionManagementInfo, newConnection: IConnectionProfile, isConnectionToDefaultDb: boolean): void {
if (newConnection) {
this._connectionStore.addActiveConnection(newConnection)
this._connectionStore.addActiveConnection(newConnection, isConnectionToDefaultDb)
.then(() => {
connectionManagementInfo.connectHandler(true);
}, err => {
@@ -881,6 +893,10 @@ export class ConnectionManagementService implements IConnectionManagementService
let connection = this._connectionStatusManager.onConnectionComplete(info);
if (info.connectionId) {
let isConnectionToDefaultDb = false;
if (connection.connectionProfile && (!connection.connectionProfile.databaseName || connection.connectionProfile.databaseName.trim() === '')) {
isConnectionToDefaultDb = true;
}
if (info.connectionSummary && info.connectionSummary.databaseName) {
this._connectionStatusManager.updateDatabaseName(info);
}
@@ -889,14 +905,14 @@ export class ConnectionManagementService implements IConnectionManagementService
connection.connectHandler(true);
let activeConnection = connection.connectionProfile;
self.tryAddActiveConnection(connection, activeConnection);
self.tryAddActiveConnection(connection, activeConnection, isConnectionToDefaultDb);
self.addTelemetryForConnection(connection);
} else {
connection.connectHandler(false, info.messages, info.errorNumber);
}
if (this._connectionStatusManager.isDefaultTypeUri(info.ownerUri)) {
this._connectionGlobalStatus.setStatusToConnected(info.connectionSummary);
if (self._connectionStatusManager.isDefaultTypeUri(info.ownerUri)) {
self._connectionGlobalStatus.setStatusToConnected(info.connectionSummary);
}
} else {
connection.connectHandler(false, info.errorMessage, info.errorNumber, info.messages);
}
}
@@ -1004,18 +1020,18 @@ export class ConnectionManagementService implements IConnectionManagementService
this._capabilitiesService.onCapabilitiesReady().then(() => {
let connectionInfo = this._connectionStatusManager.addConnection(connection, uri);
// Setup the handler for the connection complete notification to call
connectionInfo.connectHandler = ((connectResult, errorMessage, errorCode) => {
connectionInfo.connectHandler = ((connectResult, errorMessage, errorCode, callStack) => {
let connectionMngInfo = this._connectionStatusManager.findConnection(uri);
if (connectionMngInfo && connectionMngInfo.deleted) {
this._connectionStatusManager.deleteConnection(uri);
resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, errorHandled: true });
resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, callStack: undefined, errorHandled: true });
} else {
if (errorMessage) {
// Connection to the server failed
this._connectionStatusManager.deleteConnection(uri);
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode });
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack });
} else {
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode });
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack });
}
}
});

View File

@@ -79,6 +79,26 @@ export class ConnectionProfileGroup implements IConnectionProfileGroup {
return false;
}
public get hasValidConnections(): boolean {
if (this.connections) {
let invalidConnections = this.connections.find(c => c.serverCapabilities === undefined);
if (invalidConnections !== undefined) {
return false;
} else {
let childrenAreValid: boolean = true;
this.children.forEach(element => {
let isChildValid = element.hasValidConnections;
if (!isChildValid) {
childrenAreValid = false;
}
});
return childrenAreValid;
}
} else {
return true;
}
}
public getChildren(): any {
let allChildren = [];

View File

@@ -155,9 +155,9 @@ export class ConnectionStatusManager {
}
/**
* Tries to find an existing connection that's mapped with given the ownerUri
* Tries to find an existing connection that's mapped with the given ownerUri
* The purpose for this method is to find the connection given the ownerUri and find the original uri assigned to it. most of the times should be the same.
* Only if the db name in the original uri is different than when connection is complete, we need to use the original uri
* Only if the db name in the original uri is different when connection is complete, we need to use the original uri
* Returns the generated ownerUri for the connection profile if not existing connection found
* @param ownerUri connection owner uri to find an existing connection
* @param purpose purpose for the connection

View File

@@ -15,11 +15,11 @@ import { ConnectionConfig } from './connectionConfig';
import { Memento, Scope as MementoScope } from 'vs/workbench/common/memento';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionProfileGroup';
import { IConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import * as data from 'data';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const MAX_CONNECTIONS_DEFAULT = 25;
@@ -37,24 +37,21 @@ export class ConnectionStore {
constructor(
private _storageService: IStorageService,
private _context: Memento,
private _configurationEditService: IConfigurationEditingService,
private _configurationEditService: ConfigurationEditingService,
private _workspaceConfigurationService: IWorkspaceConfigurationService,
private _credentialService: ICredentialsService,
private _capabilitiesService: ICapabilitiesService,
private _connectionConfig?: IConnectionConfig
) {
if (_context) {
this._memento = this._context.getMemento(this._storageService, MementoScope.GLOBAL);
}
this._groupIdToFullNameMap = {};
this._groupFullNameToIdMap = {};
if (!this._connectionConfig) {
let cachedServerCapabilities = this.getCachedServerCapabilities();
this._connectionConfig = new ConnectionConfig(this._configurationEditService,
this._workspaceConfigurationService, this._capabilitiesService, cachedServerCapabilities);
this._connectionConfig.setCachedMetadata(cachedServerCapabilities);
}
}
@@ -115,17 +112,6 @@ export class ConnectionStore {
}
}
/**
* Gets all connection profiles stored in the user settings
* Profiles from workspace will be included if getWorkspaceProfiles is passed as true
* Note: connections will not include password value
*
* @returns {IConnectionProfile[]}
*/
public getProfiles(getWorkspaceProfiles: boolean): IConnectionProfile[] {
return this.loadProfiles(getWorkspaceProfiles);
}
public addSavedPassword(credentialsItem: IConnectionProfile): Promise<{ profile: IConnectionProfile, savedCred: boolean }> {
let self = this;
return new Promise<{ profile: IConnectionProfile, savedCred: boolean }>((resolve, reject) => {
@@ -314,12 +300,15 @@ export class ConnectionStore {
* @param {IConnectionCredentials} conn the connection to add
* @returns {Promise<void>} a Promise that returns when the connection was saved
*/
public addActiveConnection(conn: IConnectionProfile): Promise<void> {
if(this.getActiveConnections().some(existingConn => existingConn.id === conn.id)) {
public addActiveConnection(conn: IConnectionProfile, isConnectionToDefaultDb: boolean = false): Promise<void> {
if (this.getActiveConnections().some(existingConn => existingConn.id === conn.id)) {
return Promise.resolve(undefined);
} else {
return this.addConnectionToMemento(conn, Constants.activeConnections, undefined, conn.savePassword).then(() => {
let maxConnections = this.getMaxRecentConnectionsCount();
if (isConnectionToDefaultDb) {
conn.databaseName = '';
}
return this.addConnectionToMemento(conn, Constants.recentConnections, maxConnections);
});
}
@@ -330,8 +319,7 @@ export class ConnectionStore {
return new Promise<void>((resolve, reject) => {
// Get all profiles
let configValues = self.getConnectionsFromMemento(mementoKey);
let configToSave: IConnectionProfile[] = this.addToConnectionList(conn, configValues, mementoKey === Constants.recentConnections);
let configToSave = this.addToConnectionList(conn, configValues);
if (maxConnections) {
// Remove last element if needed
if (configToSave.length > maxConnections) {
@@ -349,24 +337,6 @@ export class ConnectionStore {
});
}
private isSameConnectionProfileNoGroup(profile1: IConnectionProfile, profile2: IConnectionProfile): boolean {
// both are undefined
if (!profile1 && !profile2) {
return true;
}
// only one is undefined
if (!profile1 || !profile2) {
return false;
}
// compare all the connection's "identity" properties
return equalsIgnoreCase(profile1.serverName, profile2.serverName) &&
equalsIgnoreCase(profile1.databaseName, profile2.databaseName) &&
equalsIgnoreCase(profile1.userName, profile2.userName) &&
profile1.authenticationType === profile2.authenticationType &&
profile1.providerName === profile2.providerName;
}
public removeConnectionToMemento(conn: IConnectionProfile, mementoKey: string): Promise<void> {
const self = this;
return new Promise<void>((resolve, reject) => {
@@ -388,25 +358,18 @@ export class ConnectionStore {
return this.convertConfigValuesToConnectionProfiles(configValues);
}
private addToConnectionList(conn: IConnectionProfile, list: ConnectionProfile[], isRecentConnections: boolean): IConnectionProfile[] {
private addToConnectionList(conn: IConnectionProfile, list: ConnectionProfile[]): IConnectionProfile[] {
let savedProfile: ConnectionProfile = this.getProfileWithoutPassword(conn);
// Remove the connection from the list if it already exists
if (isRecentConnections) {
// recent connections should use a different comparison the server viewlet for managing connection list
list = list.filter(value => {
return !(this.isSameConnectionProfileNoGroup(value, savedProfile));
});
} else {
list = list.filter(value => {
let equal = value && value.getConnectionInfoId() === savedProfile.getConnectionInfoId();
if (equal && savedProfile.saveProfile) {
equal = value.groupId === savedProfile.groupId ||
ConnectionProfileGroup.sameGroupName(value.groupFullName, savedProfile.groupFullName);
}
return !equal;
});
}
list = list.filter(value => {
let equal = value && value.getConnectionInfoId() === savedProfile.getConnectionInfoId();
if (equal && savedProfile.saveProfile) {
equal = value.groupId === savedProfile.groupId ||
ConnectionProfileGroup.sameGroupName(value.groupFullName, savedProfile.groupFullName);
}
return !equal;
});
list.unshift(savedProfile);
@@ -527,11 +490,6 @@ export class ConnectionStore {
return result;
}
private loadProfiles(loadWorkspaceProfiles: boolean): IConnectionProfile[] {
let connections: IConnectionProfile[] = this._connectionConfig.getConnections(loadWorkspaceProfiles);
return connections;
}
private getMaxRecentConnectionsCount(): number {
let config = this._workspaceConfigurationService.getConfiguration(Constants.sqlConfigSectionName);

View File

@@ -25,3 +25,10 @@ export const mssqlProviderName = 'MSSQL';
export const applicationName = 'sqlops';
export const defaultEngine = 'defaultEngine';
export const passwordChars = '***************';
/* authentication types */
export const sqlLogin = 'SqlLogin';
export const integrated = 'Integrated';

View File

@@ -6,7 +6,7 @@
'use strict';
import { IConnectionManagementService, ConnectionOptionSpecialType } from 'sql/parts/connection/common/connectionManagement';
import { IConnectionComponentCallbacks, IConnectionComponentController, IConnectionResult } from 'sql/parts/connection/connectionDialog/connectionDialogService';
import { IConnectionComponentCallbacks, IConnectionComponentController, IConnectionValidateResult } from 'sql/parts/connection/connectionDialog/connectionDialogService';
import { ConnectionWidget } from 'sql/parts/connection/connectionDialog/connectionWidget';
import { AdvancedPropertiesController } from 'sql/parts/connection/connectionDialog/advancedPropertiesController';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
@@ -119,7 +119,7 @@ export class ConnectionController implements IConnectionComponentController {
this._connectionWidget.focusOnOpen();
}
public validateConnection(): IConnectionResult {
public validateConnection(): IConnectionValidateResult {
return { isValid: this._connectionWidget.connect(this._model), connection: this._model };
}

View File

@@ -7,7 +7,7 @@
import {
IConnectionDialogService, IConnectionManagementService, IErrorMessageService,
ConnectionType, INewConnectionParams, IConnectionCompletionOptions
ConnectionType, INewConnectionParams, IConnectionCompletionOptions, IConnectionResult
} from 'sql/parts/connection/common/connectionManagement';
import { ConnectionDialogWidget, OnShowUIResponse } from 'sql/parts/connection/connectionDialog/connectionDialogWidget';
import { ConnectionController } from 'sql/parts/connection/connectionDialog/connectionController';
@@ -24,10 +24,16 @@ import { IPartService } from 'vs/workbench/services/part/common/partService';
import { withElementById } from 'vs/base/browser/builder';
import { TPromise } from 'vs/base/common/winjs.base';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as platform from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { Action, IAction } from 'vs/base/common/actions';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import * as types from 'vs/base/common/types';
export interface IConnectionResult {
export interface IConnectionValidateResult {
isValid: boolean;
connection: IConnectionProfile;
}
@@ -42,7 +48,7 @@ export interface IConnectionComponentCallbacks {
export interface IConnectionComponentController {
showUiComponent(container: HTMLElement): void;
initDialog(model: IConnectionProfile): void;
validateConnection(): IConnectionResult;
validateConnection(): IConnectionValidateResult;
fillInConnectionInputs(connectionInfo: IConnectionProfile): void;
handleOnConnecting(): void;
handleResetConnection(): void;
@@ -65,14 +71,17 @@ export class ConnectionDialogService implements IConnectionDialogService {
private _providerTypes: string[];
private _currentProviderType: string = 'Microsoft SQL Server';
private _connecting: boolean = false;
private _connectionErrorTitle = localize('connectionError', 'Connection Error');
private _connectionErrorTitle = localize('connectionError', 'Connection error');
constructor(
@IPartService private _partService: IPartService,
@IInstantiationService private _instantiationService: IInstantiationService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
@IWindowsService private _windowsService: IWindowsService,
@IClipboardService private _clipboardService: IClipboardService,
@ICommandService private _commandService: ICommandService
) {
this._capabilitiesMaps = {};
this._providerNameToDisplayNameMap = {};
@@ -172,13 +181,13 @@ export class ConnectionDialogService implements IConnectionDialogService {
} else if (connectionResult && connectionResult.errorHandled) {
this._connectionDialog.resetConnection();
} else {
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage);
this._connectionDialog.resetConnection();
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack);
}
}).catch(err => {
this._connecting = false;
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, err);
this._connectionDialog.resetConnection();
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, err);
});
}
@@ -277,7 +286,7 @@ export class ConnectionDialogService implements IConnectionDialogService {
connectionManagementService: IConnectionManagementService,
params: INewConnectionParams,
model?: IConnectionProfile,
error?: string): Thenable<void> {
connectionResult?: IConnectionResult): Thenable<void> {
this._connectionManagementService = connectionManagementService;
this._params = params;
@@ -303,8 +312,8 @@ export class ConnectionDialogService implements IConnectionDialogService {
}
resolve(this.showDialogWithModel().then(() => {
if (error && error !== '') {
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, error);
if (connectionResult && connectionResult.errorMessage) {
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack);
}
}));
}, err => reject(err));
@@ -333,6 +342,33 @@ export class ConnectionDialogService implements IConnectionDialogService {
}
private getCurrentProviderName(): string {
return 'MSSQL';
return Object.keys(this._providerNameToDisplayNameMap).find(providerName => {
return this._currentProviderType === this._providerNameToDisplayNameMap[providerName];
});
}
private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): void {
// Kerberos errors are currently very hard to understand, so adding handling of these to solve the common scenario
// note that ideally we would have an extensible service to handle errors by error code and provider, but for now
// this solves the most common "hard error" that we've noticed
const helpLink = 'https://aka.ms/sqlopskerberos';
let actions: IAction[] = [];
if (!platform.isWindows && types.isString(message) && message.toLowerCase().includes('kerberos') && message.toLowerCase().includes('kinit')) {
message = [
localize('kerberosErrorStart', "Connection failed due to Kerberos error."),
localize('kerberosHelpLink', "&nbsp;Help configuring Kerberos is available at ") + helpLink,
localize('kerberosKinit', "&nbsp;If you have previously connected you may need to re-run kinit.")
].join('<br/>');
actions.push(new Action('Kinit', 'Run kinit', null, true, () => {
this._connectionDialog.close();
this._clipboardService.writeText('kinit\r');
this._commandService.executeCommand('workbench.action.terminal.focus').then(resolve => {
return this._commandService.executeCommand('workbench.action.terminal.paste');
}).then(resolve => null, reject => null);
return null;
}));
}
this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions);
}
}

View File

@@ -4,7 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/connectionDialog';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import nls = require('vs/nls');
import { Button } from 'sql/base/browser/ui/button/button';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { TPromise } from 'vs/base/common/winjs.base';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { Modal } from 'sql/base/browser/ui/modal/modal';
@@ -14,51 +17,37 @@ import { TreeCreationUtils } from 'sql/parts/registeredServer/viewlet/treeCreati
import { TreeUpdateUtils } from 'sql/parts/registeredServer/viewlet/treeUpdateUtils';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import * as styler from 'vs/platform/theme/common/styler';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import Event, { Emitter } from 'vs/base/common/event';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { RecentConnectionTreeController, RecentConnectionActionsProvider } from 'sql/parts/connection/connectionDialog/recentConnectionTreeController';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, IConfirmation } from 'vs/platform/message/common/message';
export interface OnShowUIResponse {
selectedProviderType: string;
container: HTMLElement;
}
class TreeController extends DefaultController {
constructor(private clickcb: (element: any, eventish: ICancelableEvent, origin: string) => void) {
super();
}
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
this.clickcb(element, eventish, origin);
return super.onLeftClick(tree, element, eventish, origin);
}
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
super.onEnter(tree, event);
this.clickcb(tree.getSelection()[0], event, 'keyboard');
return true;
}
}
export class ConnectionDialogWidget extends Modal {
private _bodyBuilder: Builder;
private _recentConnectionBuilder: Builder;
private _noRecentConnectionBuilder: Builder;
private _dividerBuilder: Builder;
private _connectButton: Button;
private _closeButton: Button;
private _providerTypeSelectBox: SelectBox;
private _newConnectionParams: INewConnectionParams;
private _recentConnectionTree: ITree;
private $connectionUIContainer: Builder;
@@ -89,16 +78,20 @@ export class ConnectionDialogWidget extends Modal {
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
@IPartService _partService: IPartService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IMessageService private _messageService: IMessageService
) {
super(localize('connection', 'Connection'), TelemetryKeys.Connection, _partService, telemetryService, contextKeyService, { hasSpinner: true, hasErrors: true });
}
protected renderBody(container: HTMLElement): void {
this._bodyBuilder = new Builder(container);
this._providerTypeSelectBox = new SelectBox(this.providerTypeOptions, this.selectedProviderType);
this._bodyBuilder.div({ class: 'connection-recent', id: 'recentConnection' }, (builder) => {
this._recentConnectionBuilder = new Builder(builder.getHTMLElement());
this._noRecentConnectionBuilder = new Builder(builder.getHTMLElement());
this.createRecentConnections();
this._recentConnectionBuilder.hide();
});
@@ -108,16 +101,10 @@ export class ConnectionDialogWidget extends Modal {
});
this._bodyBuilder.div({ class: 'connection-type' }, (modelTableContent) => {
// add SQL Server label to Connection Dialog until we support multiple connection providers
let sqlServerName = localize('microsoftSqlServer', "Microsoft SQL Server");
modelTableContent.div({ class: 'server-name-label' }, (nameLabel) => {
nameLabel.innerHtml(sqlServerName);
});
//let connectTypeLabel = localize('connectType', 'Connection type');
let connectTypeLabel = localize('connectType', 'Connection type');
modelTableContent.element('table', { class: 'connection-table-content' }, (tableContainer) => {
// DialogHelper.appendInputSelectBox(
// DialogHelper.appendRow(tableContainer, connectTypeLabel, 'connection-label', 'connection-input'), this._providerTypeSelectBox);
DialogHelper.appendInputSelectBox(
DialogHelper.appendRow(tableContainer, connectTypeLabel, 'connection-label', 'connection-input'), this._providerTypeSelectBox);
});
});
@@ -141,7 +128,7 @@ export class ConnectionDialogWidget extends Modal {
this._connectButton.enabled = false;
this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel());
this.registerListeners();
this.onProviderTypeSelected('MSSQL');
this.onProviderTypeSelected(this._providerTypeSelectBox.value);
}
// Update theming that is specific to connection flyout body
@@ -156,9 +143,14 @@ export class ConnectionDialogWidget extends Modal {
}
private registerListeners(): void {
this._register(styler.attachButtonStyler(this._connectButton, this._themeService));
this._register(styler.attachButtonStyler(this._closeButton, this._themeService));
// Theme styler
this._register(styler.attachSelectBoxStyler(this._providerTypeSelectBox, this._themeService));
this._register(attachButtonStyler(this._connectButton, this._themeService));
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(this._providerTypeSelectBox.onDidSelect(selectedProviderType => {
this.onProviderTypeSelected(selectedProviderType.selected);
}));
}
private onProviderTypeSelected(selectedProviderType: string) {
@@ -171,6 +163,7 @@ export class ConnectionDialogWidget extends Modal {
private connect(element?: IConnectionProfile): void {
if (this._connectButton.enabled) {
this._connectButton.enabled = false;
this._providerTypeSelectBox.disable();
this.showSpinner();
this._onConnect.fire(element);
}
@@ -198,13 +191,37 @@ export class ConnectionDialogWidget extends Modal {
this.hide();
}
private createRecentConnections() {
private clearRecentConnectionList(): TPromise<boolean> {
let confirm: IConfirmation = {
message: nls.localize('clearRecentConnectionMessage', 'Are you sure you want to delete all the connections from the list?'),
primaryButton: localize('yes', 'Yes'),
secondaryButton: localize('no', 'No'),
type: 'question'
};
return this._messageService.confirm(confirm).then(confirmation => {
if (!confirmation.confirmed) {
return TPromise.as(false);
} else {
this._connectionManagementService.clearRecentConnectionsList();
this.open(false);
return TPromise.as(true);
}
});
}
private createRecentConnectionList(): void {
this._recentConnectionBuilder.div({ class: 'connection-recent-content' }, (recentConnectionContainer) => {
let recentHistoryLabel = localize('recentHistory', 'Recent history');
recentConnectionContainer.div({ class: 'connection-history-label' }, (recentTitle) => {
recentTitle.innerHtml(recentHistoryLabel);
recentConnectionContainer.div({ class: 'recent-titles-container'}, (container) => {
container.div({ class: 'connection-history-label' }, (recentTitle) => {
recentTitle.innerHtml(recentHistoryLabel);
});
container.div({ class: 'search-action clear-search-results'}, (clearSearchIcon) => {
clearSearchIcon.on('click', () => this.clearRecentConnectionList());
});
});
recentConnectionContainer.div({ class: 'server-explorer-viewlet' }, (divContainer: Builder) => {
divContainer.div({ class: 'explorer-servers' }, (treeContainer: Builder) => {
let leftClick = (element: any, eventish: ICancelableEvent, origin: string) => {
@@ -212,9 +229,16 @@ export class ConnectionDialogWidget extends Modal {
if (element instanceof ConnectionProfile) {
this.onRecentConnectionClick({ payload: { origin: origin, originalEvent: eventish } }, element);
}
};
let controller = new TreeController(leftClick);
let actionProvider = this._instantiationService.createInstance(RecentConnectionActionsProvider, this._instantiationService, this._connectionManagementService,
this._messageService);
let controller = new RecentConnectionTreeController(leftClick, actionProvider, this._connectionManagementService, this._contextMenuService);
actionProvider.onRecentConnectionRemoved(() => {
this.open(this._connectionManagementService.getRecentConnections().length > 0);
});
controller.onRecentConnectionRemoved(() => {
this.open(this._connectionManagementService.getRecentConnections().length > 0);
})
this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer.getHTMLElement(), this._instantiationService, controller);
// Theme styler
@@ -225,6 +249,20 @@ export class ConnectionDialogWidget extends Modal {
});
}
private createRecentConnections() {
this.createRecentConnectionList();
this._noRecentConnectionBuilder.div({ class: 'connection-recent-content' }, (noRecentConnectionContainer) => {
let recentHistoryLabel = localize('recentHistory', 'Recent history');
noRecentConnectionContainer.div({ class: 'connection-history-label' }, (recentTitle) => {
recentTitle.innerHtml(recentHistoryLabel);
});
let noRecentHistoryLabel = localize('noRecentConnections', 'No Recent Connections');
noRecentConnectionContainer.div({ class: 'no-recent-connections' }, (noRecentTitle) => {
noRecentTitle.innerHtml(noRecentHistoryLabel);
});
});
}
private onRecentConnectionClick(event: any, element: IConnectionProfile) {
let isMouseOrigin = event.payload && (event.payload.origin === 'mouse');
let isDoubleClick = isMouseOrigin && event.payload.originalEvent && event.payload.originalEvent.detail === 2;
@@ -244,14 +282,15 @@ export class ConnectionDialogWidget extends Modal {
public open(recentConnections: boolean) {
this.show();
if (recentConnections) {
this._noRecentConnectionBuilder.hide();
this._recentConnectionBuilder.show();
TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this._connectionManagementService);
// call layout with view height
this.layout();
} else {
this._recentConnectionBuilder.hide();
this._noRecentConnectionBuilder.show();
}
TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this._connectionManagementService);
// call layout with view height
this.layout();
this.initDialog();
}
@@ -284,6 +323,7 @@ export class ConnectionDialogWidget extends Modal {
public resetConnection(): void {
this.hideSpinner();
this._connectButton.enabled = true;
this._providerTypeSelectBox.enable();
this._onResetConnection.fire();
}
@@ -296,6 +336,7 @@ export class ConnectionDialogWidget extends Modal {
}
public updateProvider(displayName: string) {
this.onProviderTypeSelected('MSSQL');
this._providerTypeSelectBox.selectWithOptionName(displayName);
this.onProviderTypeSelected(displayName);
}
}

View File

@@ -7,10 +7,10 @@
import 'vs/css!./media/sqlConnection';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { Button } from 'sql/base/browser/ui/button/button';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Checkbox } from 'sql/base/browser/ui/checkbox/defaultCheckbox';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { IConnectionComponentCallbacks } from 'sql/parts/connection/connectionDialog/connectionDialogService';
@@ -21,11 +21,12 @@ import * as Constants from 'sql/parts/connection/common/constants';
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import * as styler from 'vs/platform/theme/common/styler';
import { attachInputBoxStyler } from 'sql/common/theme/styler';
import { attachInputBoxStyler, attachButtonStyler } from 'sql/common/theme/styler';
import * as DOM from 'vs/base/browser/dom';
import data = require('data');
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { localize } from 'vs/nls';
import { OS, OperatingSystem } from 'vs/base/common/platform';
export class ConnectionWidget {
private _builder: Builder;
@@ -36,6 +37,7 @@ export class ConnectionWidget {
private _databaseNameInputBox: InputBox;
private _userNameInputBox: InputBox;
private _passwordInputBox: InputBox;
private _password: string;
private _rememberPasswordCheckBox: Checkbox;
private _advancedButton: Button;
private _callbacks: IConnectionComponentCallbacks;
@@ -43,9 +45,10 @@ export class ConnectionWidget {
private _toDispose: lifecycle.IDisposable[];
private _optionsMaps: { [optionType: number]: data.ConnectionOption };
private _tableContainer: Builder;
private _focusedBeforeHandleOnConnection: HTMLElement;
private _providerName: string;
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
[Constants.mssqlProviderName]: [new AuthenticationType('Integrated', false), new AuthenticationType('SqlLogin', true)]
[Constants.mssqlProviderName]: [new AuthenticationType(Constants.integrated, false), new AuthenticationType(Constants.sqlLogin, true)]
};
private _saveProfile: boolean;
public DefaultServerGroup: IConnectionProfileGroup = {
@@ -84,7 +87,14 @@ export class ConnectionWidget {
}
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
this._authTypeSelectBox = authTypeOption ? new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue) : undefined;
if(authTypeOption) {
if (OS === OperatingSystem.Windows) {
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.integrated);
} else {
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.sqlLogin);
}
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue);
}
this._providerName = providerName;
}
@@ -134,6 +144,7 @@ export class ConnectionWidget {
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input');
this._passwordInputBox = new InputBox(passwordBuilder.getHTMLElement(), this._contextViewService);
this._passwordInputBox.inputElement.type = 'password';
this._password = '';
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', false);
@@ -201,13 +212,14 @@ export class ConnectionWidget {
this._toDispose.push(attachInputBoxStyler(this._userNameInputBox, this._themeService));
this._toDispose.push(attachInputBoxStyler(this._passwordInputBox, this._themeService));
this._toDispose.push(styler.attachSelectBoxStyler(this._serverGroupSelectBox, this._themeService));
this._toDispose.push(styler.attachButtonStyler(this._advancedButton, this._themeService));
this._toDispose.push(attachButtonStyler(this._advancedButton, this._themeService));
if (this._authTypeSelectBox) {
// Theme styler
this._toDispose.push(styler.attachSelectBoxStyler(this._authTypeSelectBox, this._themeService));
this._toDispose.push(this._authTypeSelectBox.onDidSelect(selectedAuthType => {
this.onAuthTypeSelected(selectedAuthType.selected);
this.setConnectButton();
}));
}
@@ -218,6 +230,14 @@ export class ConnectionWidget {
this._toDispose.push(this._serverNameInputBox.onDidChange(serverName => {
this.serverNameChanged(serverName);
}));
this._toDispose.push(this._userNameInputBox.onDidChange(userName => {
this.setConnectButton();
}));
this._toDispose.push(this._passwordInputBox.onDidChange(passwordInput => {
this._password = passwordInput;
}));
}
private onGroupSelected(selectedGroup: string) {
@@ -230,6 +250,17 @@ export class ConnectionWidget {
}
}
private setConnectButton() : void {
let authDisplayName: string = this.getAuthTypeDisplayName(this.authenticationType);
let authType: AuthenticationType = this.getMatchingAuthType(authDisplayName);
let showUsernameAndPassword: boolean = true;
if(authType) {
showUsernameAndPassword = authType.showUsernameAndPassword;
}
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
this._callbacks.onSetConnectButton(!!this.serverName);
}
private onAuthTypeSelected(selectedAuthType: string) {
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
if (!currentAuthType.showUsernameAndPassword) {
@@ -239,6 +270,7 @@ export class ConnectionWidget {
this._passwordInputBox.hideMessage();
this._userNameInputBox.value = '';
this._passwordInputBox.value = '';
this._password = '';
this._rememberPasswordCheckBox.checked = false;
this._rememberPasswordCheckBox.enabled = false;
@@ -250,7 +282,7 @@ export class ConnectionWidget {
}
private serverNameChanged(serverName: string) {
this._callbacks.onSetConnectButton(!!serverName);
this.setConnectButton();
if (serverName.toLocaleLowerCase().includes('database.windows.net')) {
this._callbacks.onSetAzureTimeOut();
}
@@ -289,10 +321,10 @@ export class ConnectionWidget {
public fillInConnectionInputs(connectionInfo: IConnectionProfile) {
if (connectionInfo) {
this._serverNameInputBox.value = this.getModelValue(connectionInfo.serverName);
this._callbacks.onSetConnectButton(!!connectionInfo.serverName);
this._databaseNameInputBox.value = this.getModelValue(connectionInfo.databaseName);
this._userNameInputBox.value = this.getModelValue(connectionInfo.userName);
this._passwordInputBox.value = this.getModelValue(connectionInfo.password);
this._passwordInputBox.value = connectionInfo.password ? Constants.passwordChars : '';
this._password = this.getModelValue(connectionInfo.password);
this._saveProfile = connectionInfo.saveProfile;
let groupName: string;
if (this._saveProfile) {
@@ -321,18 +353,26 @@ export class ConnectionWidget {
if (this._authTypeSelectBox) {
this.onAuthTypeSelected(this._authTypeSelectBox.value);
}
// Disable connect button if -
// 1. Authentication type is SQL Login and no username is provided
// 2. No server name is provided
this.setConnectButton();
}
}
private getAuthTypeDisplayName(authTypeName: string) {
var displayName: string;
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
authTypeOption.categoryValues.forEach(c => {
if (c.name === authTypeName) {
displayName = c.displayName;
}
});
if(authTypeOption) {
authTypeOption.categoryValues.forEach(c => {
if (c.name === authTypeName) {
displayName = c.displayName;
}
});
}
return displayName;
}
@@ -348,6 +388,7 @@ export class ConnectionWidget {
}
public handleOnConnecting(): void {
this._focusedBeforeHandleOnConnection = <HTMLElement>document.activeElement;
this._advancedButton.enabled = false;
this._serverGroupSelectBox.disable();
@@ -378,6 +419,10 @@ export class ConnectionWidget {
this._passwordInputBox.enable();
this._rememberPasswordCheckBox.enabled = true;
}
if (this._focusedBeforeHandleOnConnection) {
this._focusedBeforeHandleOnConnection.focus();
}
}
public get serverName(): string {
@@ -393,7 +438,7 @@ export class ConnectionWidget {
}
public get password(): string {
return this._passwordInputBox.value;
return this._password;
}
public get authenticationType(): string {
@@ -467,7 +512,8 @@ export class ConnectionWidget {
}
private getMatchingAuthType(displayName: string): AuthenticationType {
return this._authTypeMap[this._providerName].find(authType => this.getAuthTypeDisplayName(authType.name) === displayName);
const authType = this._authTypeMap[this._providerName];
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType.name) === displayName) : undefined;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#1E1E1E" d="M4.222 0h-2.222v.479c-.526.648-.557 1.57-.043 2.269l.043.059v3.203l-.4.296-.053.053c-.353.352-.547.822-.547 1.321s.194.967.549 1.32c.134.134.288.236.451.322v6.678h14v-16h-11.778z"/><path fill="#E8E8E8" d="M10.798 7l-1.83-2h6.032v2h-4.202zm-2.292-6h-3.207l1.337 1.52 1.87-1.52zm-5.506 8.531v1.469h12v-2h-10.813l-.024.021c-.3.299-.716.479-1.163.51zm0 5.469h12v-2h-12v2zm3.323-8h.631l-.347-.266-.284.266zm8.677-4v-2h-3.289l-1.743 2h5.032z"/><path fill="#F48771" d="M7.246 4.6l2.856-3.277-.405-.002-3.176 2.581-2.607-2.962c-.336-.221-.786-.2-1.082.096-.308.306-.319.779-.069 1.12l2.83 2.444-3.339 2.466c-.339.338-.339.887 0 1.225.339.337.888.337 1.226 0l3.063-2.867 3.33 2.555h.466l-3.093-3.379z"/></svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M4.222 0h-2.222v.479c-.526.648-.557 1.57-.043 2.269l.043.059v3.203l-.4.296-.053.053c-.353.352-.547.822-.547 1.321s.194.967.549 1.32c.134.134.288.236.451.322v6.678h14v-16h-11.778z"/><path fill="#424242" d="M10.798 7l-1.83-2h6.032v2h-4.202zm-2.292-6h-3.207l1.337 1.52 1.87-1.52zm-5.506 8.531v1.469h12v-2h-10.813l-.024.021c-.3.299-.716.479-1.163.51zm0 5.469h12v-2h-12v2zm3.323-8h.631l-.347-.266-.284.266zm8.677-4v-2h-3.289l-1.743 2h5.032z"/><path fill="#A1260D" d="M7.246 4.6l2.856-3.277-.405-.002-3.176 2.581-2.607-2.962c-.336-.221-.786-.2-1.082.096-.308.306-.319.779-.069 1.12l2.83 2.444-3.339 2.466c-.339.338-.339.887 0 1.225.339.337.888.337 1.226 0l3.063-2.867 3.33 2.555h.466l-3.093-3.379z"/></svg>

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -20,13 +20,22 @@
overflow-y: auto;
}
.connection-history-label {
font-size: 15px;
.no-recent-connections {
font-size: 12px;
text-align: left;
display: block;
padding-top: 5px;
}
.server-name-label {
.connection-history-label {
font-size: 15px;
margin-bottom: 15px;
display: inline;
text-align: left;
}
.recent-titles-container {
display: flex;
justify-content: space-between;
}
.connection-provider-info {
@@ -46,4 +55,17 @@
.connection-type {
margin: 15px;
overflow-y: hidden;
}
.search-action.clear-search-results {
background: url('clear-search-results.svg') center right no-repeat;
width: 10%;
cursor: pointer;
}
.vs-dark .search-action.clear-search-results,
.hc-black .search-action.clear-search-results {
background: url('clear-search-results-dark.svg') center right no-repeat;
width: 10%;
cursor: pointer;
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ClearSingleRecentConnectionAction } from 'sql/parts/connection/common/connectionActions';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IAction } from 'vs/base/common/actions';
import Event, { Emitter } from 'vs/base/common/event';
import { IMessageService } from 'vs/platform/message/common/message';
import mouse = require('vs/base/browser/mouseEvent');
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
export class RecentConnectionActionsProvider extends ContributableActionProvider {
private _onRecentConnectionRemoved = new Emitter<void>();
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IMessageService private _messageService: IMessageService,
) {
super();
}
private getRecentConnectionActions(tree: ITree, element: any): IAction[] {
let actions: IAction[] = [];
let clearSingleConnectionAction = this._instantiationService.createInstance(ClearSingleRecentConnectionAction, ClearSingleRecentConnectionAction.ID,
ClearSingleRecentConnectionAction.LABEL,<IConnectionProfile>element);
clearSingleConnectionAction.onRecentConnectionRemoved(() => this._onRecentConnectionRemoved.fire());
actions.push(clearSingleConnectionAction);
return actions;
}
public hasActions(tree: ITree, element: any): boolean {
return element instanceof ConnectionProfile;
}
/**
* Return actions given an element in the tree
*/
public getActions(tree: ITree, element: any): TPromise<IAction[]> {
if (element instanceof ConnectionProfile) {
return TPromise.as(this.getRecentConnectionActions(tree, element));
}
return TPromise.as([]);
}
}
export class RecentConnectionsActionsContext {
public connectionProfile: ConnectionProfile;
public container: HTMLElement;
public tree: ITree;
}
export class RecentConnectionTreeController extends DefaultController {
private _onRecentConnectionRemoved = new Emitter<void>();
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
constructor(
private clickcb: (element: any, eventish: ICancelableEvent, origin: string) => void,
private actionProvider: RecentConnectionActionsProvider,
private _connectionManagementService: IConnectionManagementService,
@IContextMenuService private _contextMenuService: IContextMenuService
) {
super();
}
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
this.clickcb(element, eventish, origin);
return super.onLeftClick(tree, element, eventish, origin);
}
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
super.onEnter(tree, event);
this.clickcb(tree.getSelection()[0], event, 'keyboard');
return true;
}
protected onRightClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
this.clickcb(element, eventish, origin);
this.onContextMenu(tree, element, event);
return true;
}
public onMouseDown(tree: ITree, element: any, event: mouse.IMouseEvent, origin: string = 'mouse'): boolean {
if (event.leftButton || event.middleButton) {
return this.onLeftClick(tree, element, event, origin);
} else {
return this.onRightClick(tree, element, event);
}
}
public onKeyDown(tree: ITree, event: IKeyboardEvent): boolean {
if (event.keyCode === 20) {
let element = tree.getFocus();
if (element instanceof ConnectionProfile) {
this._connectionManagementService.clearRecentConnection(element);
this._onRecentConnectionRemoved.fire();
return true;
}
}
return super.onKeyDown(tree, event);
}
public onContextMenu(tree: ITree, element: any, event: any): boolean {
var actionContext: any;
if (element instanceof ConnectionProfile) {
actionContext = new RecentConnectionsActionsContext();
actionContext.container = event.target;
actionContext.connectionProfile = <ConnectionProfile>element;
actionContext.tree = tree;
} else {
actionContext = element;
}
let anchor = { x: event.x + 1, y: event.y };
this._contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this.actionProvider.getActions(tree, element),
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
tree.DOMFocus();
}
},
getActionsContext: () => (actionContext)
});
return true;
}
}

View File

@@ -2,28 +2,112 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { Action, IAction } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IAngularEventingService, AngularEventType } from 'sql/services/angularEventing/angularEventingService';
export class RefreshWidgetAction extends Action {
export class EditDashboardAction extends Action {
public static ID = 'refreshWidget';
public static LABEL = nls.localize('refreshWidget', 'Refresh');
private static readonly ID = 'editDashboard';
private static readonly EDITLABEL = nls.localize('editDashboard', "Edit");
private static readonly EXITLABEL = nls.localize('editDashboardExit', "Exit");
private static readonly ICON = 'edit';
private _state = 0;
constructor(
id: string, label: string,
private refreshFn: () => void
private editFn: () => void,
private context: any //this
) {
super(id, label);
super(EditDashboardAction.ID, EditDashboardAction.EDITLABEL, EditDashboardAction.ICON);
}
run(): TPromise<boolean> {
try {
this.refreshFn();
this.editFn.apply(this.context);
this.toggleLabel();
return TPromise.as(true);
} catch (e) {
return TPromise.as(false);
}
}
private toggleLabel(): void {
if (this._state === 0) {
this.label = EditDashboardAction.EXITLABEL;
this._state = 1;
} else {
this.label = EditDashboardAction.EDITLABEL;
this._state = 0;
}
}
}
export class RefreshWidgetAction extends Action {
private static readonly ID = 'refreshWidget';
private static readonly LABEL = nls.localize('refreshWidget', 'Refresh');
private static readonly ICON = 'refresh';
constructor(
private refreshFn: () => void,
private context: any // this
) {
super(RefreshWidgetAction.ID, RefreshWidgetAction.LABEL, RefreshWidgetAction.ICON);
}
run(): TPromise<boolean> {
try {
this.refreshFn.apply(this.context);
return TPromise.as(true);
} catch (e) {
return TPromise.as(false);
}
}
}
export class ToggleMoreWidgetAction extends Action {
private static readonly ID = 'toggleMore';
private static readonly LABEL = nls.localize('toggleMore', 'Toggle More');
private static readonly ICON = 'toggle-more';
constructor(
private _actions: Array<IAction>,
private _context: any,
@IContextMenuService private _contextMenuService: IContextMenuService
) {
super(ToggleMoreWidgetAction.ID, ToggleMoreWidgetAction.LABEL, ToggleMoreWidgetAction.ICON);
}
run(context: StandardKeyboardEvent): TPromise<boolean> {
this._contextMenuService.showContextMenu({
getAnchor: () => context.target,
getActions: () => TPromise.as(this._actions),
getActionsContext: () => this._context
});
return TPromise.as(true);
}
}
export class DeleteWidgetAction extends Action {
private static readonly ID = 'deleteWidget';
private static readonly LABEL = nls.localize('deleteWidget', "Delete Widget");
private static readonly ICON = 'close';
constructor(
private _widgetId,
private _uri,
@IAngularEventingService private angularEventService: IAngularEventingService
) {
super(DeleteWidgetAction.ID, DeleteWidgetAction.LABEL, DeleteWidgetAction.ICON);
}
run(): TPromise<boolean> {
this.angularEventService.sendAngularEvent(this._uri, AngularEventType.DELETE_WIDGET, { id: this._widgetId });
return TPromise.as(true);
}
}

View File

@@ -4,13 +4,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div #propertyContainer>
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="margin-left: 10px; margin-right: 10px; height: 90px; display: block">
</dashboard-widget-wrapper>
</div>
<div>
<div [ngGrid]="gridConfig">
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
</dashboard-widget-wrapper>
<div #scrollContainer style="height: 100%">
<div #scrollable style="position: relative">
<div #propertiesContainer>
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="padding-left: 10px; padding-right: 10px; height: 90px; display: block">
</dashboard-widget-wrapper>
</div>
<div [ngGrid]="gridConfig">
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
</dashboard-widget-wrapper>
</div>
</div>
</div>
</div>

View File

@@ -3,22 +3,33 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList } from '@angular/core';
import { NgGridConfig } from 'angular2-grid';
import 'vs/css!./dashboardPage';
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { NgGridConfig, NgGrid, NgGridItem } from 'angular2-grid';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
import { subscriptionToDisposable } from 'sql/base/common/lifecycle';
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
import { Registry } from 'vs/platform/registry/common/platform';
import * as types from 'vs/base/common/types';
import { Severity } from 'vs/platform/message/common/message';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { addDisposableListener, getContentHeight, EventType } from 'vs/base/browser/dom';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
import * as themeColors from 'vs/workbench/common/theme';
import { generateUuid } from 'vs/base/common/uuid';
import * as objects from 'vs/base/common/objects';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
/**
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
@@ -27,14 +38,56 @@ function isNumberArray(value: any): value is number[] {
return types.isArray(value) && (<any[]>value).every(elem => types.isNumber(elem));
}
/**
* Sorting function for dashboard widgets
* In order of priority;
* If neither have defined grid positions, they are equivalent
* If a has a defined grid position and b does not; a should come first
* If both have defined grid positions and have the same row; the one with the smaller col position should come first
* If both have defined grid positions but different rows (it doesn't really matter in this case) the lowers row should come first
*/
function configSorter(a, b): number {
if ((!a.gridItemConfig || !a.gridItemConfig.col)
&& (!b.gridItemConfig || !b.gridItemConfig.col)) {
return 0;
} else if (!a.gridItemConfig || !a.gridItemConfig.col) {
return 1;
} else if (!b.gridItemConfig || !b.gridItemConfig.col) {
return -1;
} else if (a.gridItemConfig.row === b.gridItemConfig.row) {
if (a.gridItemConfig.col < b.gridItemConfig.col) {
return -1;
}
if (a.gridItemConfig.col === b.gridItemConfig.col) {
return 0;
}
if (a.gridItemConfig.col > b.gridItemConfig.col) {
return 1;
}
} else {
if (a.gridItemConfig.row < b.gridItemConfig.row) {
return -1;
}
if (a.gridItemConfig.row === b.gridItemConfig.row) {
return 0;
}
if (a.gridItemConfig.row > b.gridItemConfig.row) {
return 1;
}
}
return void 0; // this should never be reached
}
@Component({
selector: 'dashboard-page',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html')),
host: {
class: 'dashboard-page'
}
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html'))
})
export abstract class DashboardPage {
export abstract class DashboardPage extends Disposable implements OnDestroy {
protected SKELETON_WIDTH = 5;
protected widgets: Array<WidgetConfig> = [];
@@ -60,55 +113,124 @@ export abstract class DashboardPage {
'prefer_new': false, // When adding new items, will use that items position ahead of existing items
'limit_to_screen': true, // When resizing the screen, with this true and auto_resize false, the grid will re-arrange to fit the screen size. Please note, at present this only works with cascade direction up.
};
private _themeDispose: IDisposable;
private _originalConfig: WidgetConfig[];
private _editDispose: Array<IDisposable> = [];
private _scrollableElement: ScrollableElement;
private _widgetConfigLocation: string;
private _propertiesConfigLocation: string;
@ViewChild('propertyContainer', { read: ElementRef }) private propertyContainer: ElementRef;
@ViewChild('properties') private _properties: DashboardWidgetWrapper;
@ViewChild(NgGrid) private _grid: NgGrid;
@ViewChild('scrollable', { read: ElementRef }) private _scrollable: ElementRef;
@ViewChild('scrollContainer', { read: ElementRef }) private _scrollContainer: ElementRef;
@ViewChild('propertiesContainer', { read: ElementRef }) private _propertiesContainer: ElementRef;
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
@ViewChildren(NgGridItem) private _items: QueryList<NgGridItem>;
// a set of config modifiers
private readonly _configModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
this.removeEmpty,
this.initExtensionConfigs,
this.validateGridConfig,
this.addProvider,
this.addEdition,
this.addContext,
this.filterWidgets
];
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
this.validateGridConfig
];
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface
) { }
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
protected init() {
if (!this.dashboardService.connectionManagementService.connectionInfo) {
this.dashboardService.messageService.show(Severity.Warning, nls.localize('missingConnectionInfo', 'No connection information could be found for this dashboard'));
} else {
let tempWidgets = this.dashboardService.getSettings(this.context).widgets;
let tempWidgets = this.dashboardService.getSettings<Array<WidgetConfig>>([this.context, 'widgets'].join('.'));
this._widgetConfigLocation = 'default';
this._originalConfig = objects.clone(tempWidgets);
let properties = this.getProperties();
this._configModifiers.forEach((cb) => {
tempWidgets = cb.apply(this, [tempWidgets]);
properties = properties ? cb.apply(this, [properties]) : undefined;
});
this._gridModifiers.forEach(cb => {
tempWidgets = cb.apply(this, [tempWidgets]);
});
this.widgets = tempWidgets;
this.propertiesWidget = properties ? properties[0] : undefined;
}
}
protected baseInit(): void {
let self = this;
self._themeDispose = self.dashboardService.themeService.onDidColorThemeChange((event: IColorTheme) => {
self.updateTheme(event);
ngAfterViewInit(): void {
this._register(this.dashboardService.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.dashboardService.themeService.getColorTheme());
let container = this._scrollContainer.nativeElement as HTMLElement;
let scrollable = this._scrollable.nativeElement as HTMLElement;
container.removeChild(scrollable);
this._scrollableElement = new ScrollableElement(scrollable, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false
});
self.updateTheme(self.dashboardService.themeService.getColorTheme());
this._scrollableElement.onScroll(e => {
scrollable.style.bottom = e.scrollTop + 'px';
});
container.appendChild(this._scrollableElement.getDomNode());
let initalHeight = getContentHeight(scrollable);
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
this._register(addDisposableListener(window, EventType.RESIZE, () => {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
}));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => {
let currentheight = getContentHeight(scrollable);
if (initalHeight !== currentheight) {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
}
}, 100);
}
private updateTheme(theme: IColorTheme): void {
let el = this._propertiesContainer.nativeElement as HTMLElement;
let border = theme.getColor(colors.contrastBorder, true);
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
if (border) {
el.style.borderColor = border.toString();
el.style.borderBottomWidth = '1px';
el.style.borderBottomStyle = 'solid';
} else if (borderColor) {
el.style.borderBottom = '1px solid ' + borderColor.toString();
} else {
el.style.border = 'none';
}
}
protected baseDestroy(): void {
if (this._themeDispose) {
this._themeDispose.dispose();
}
ngOnDestroy() {
this.dispose();
}
protected abstract propertiesWidget: WidgetConfig;
@@ -215,10 +337,14 @@ export abstract class DashboardPage {
* @param config Array of widgets to validate
*/
protected validateGridConfig(config: WidgetConfig[]): Array<WidgetConfig> {
return config.map((widget) => {
return config.map((widget, index) => {
if (widget.gridItemConfig === undefined) {
widget.gridItemConfig = {};
}
const id = generateUuid();
widget.gridItemConfig.payload = { id };
widget.id = id;
this._originalConfig[index].id = id;
return widget;
});
}
@@ -246,6 +372,12 @@ export abstract class DashboardPage {
sizey: insightConfig.gridItemConfig.y
};
}
if (config.gridItemConfig && !config.gridItemConfig.sizex && insightConfig.gridItemConfig && insightConfig.gridItemConfig.x) {
config.gridItemConfig.sizex = insightConfig.gridItemConfig.x;
}
if (config.gridItemConfig && !config.gridItemConfig.sizey && insightConfig.gridItemConfig && insightConfig.gridItemConfig.y) {
config.gridItemConfig.sizey = insightConfig.gridItemConfig.y;
}
}
}
return config;
@@ -253,7 +385,8 @@ export abstract class DashboardPage {
}
private getProperties(): Array<WidgetConfig> {
let properties = this.dashboardService.getSettings(this.context).properties;
let properties = this.dashboardService.getSettings<IPropertiesConfig[]>([this.context, 'properties'].join('.'));
this._propertiesConfigLocation = 'default';
if (types.isUndefinedOrNull(properties)) {
return [this.propertiesWidget];
} else if (types.isBoolean(properties)) {
@@ -271,18 +404,6 @@ export abstract class DashboardPage {
}
}
private updateTheme(theme: IColorTheme): void {
let propsEl: HTMLElement = this.propertyContainer.nativeElement;
let widgetShadowColor = theme.getColor(colors.widgetShadow);
if (widgetShadowColor) {
// Box shadow on bottom only.
// The below settings fill the shadow across the whole page
propsEl.style.boxShadow = `-5px 5px 10px -5px ${widgetShadowColor}`;
propsEl.style.marginRight = '-10px';
propsEl.style.marginBottom = '5px';
}
}
public refresh(refreshConfig: boolean = false): void {
if (refreshConfig) {
this.init();
@@ -297,4 +418,85 @@ export abstract class DashboardPage {
}
}
}
public enableEdit(): void {
if (this._grid.dragEnable) {
this._grid.disableDrag();
this._grid.disableResize();
this._editDispose.forEach(i => i.dispose());
this._widgets.forEach(i => {
if (i.id) {
i.disableEdit();
}
});
this._editDispose = [];
} else {
this._grid.enableResize();
this._grid.enableDrag();
this._editDispose.push(this.dashboardService.onDeleteWidget(e => {
let index = this.widgets.findIndex(i => i.id === e);
this.widgets.splice(index, 1);
index = this._originalConfig.findIndex(i => i.id === e);
this._originalConfig.splice(index, 1);
this._rewriteConfig();
this._cd.detectChanges();
}));
this._editDispose.push(subscriptionToDisposable(this._grid.onResizeStop.subscribe((e: NgGridItem) => {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this._scrollable.nativeElement),
height: getContentHeight(this._scrollContainer.nativeElement)
});
let event = e.getEventOutput();
let config = this._originalConfig.find(i => i.id === event.payload.id);
if (!config.gridItemConfig) {
config.gridItemConfig = {};
}
config.gridItemConfig.sizex = e.sizex;
config.gridItemConfig.sizey = e.sizey;
let component = this._widgets.find(i => i.id === event.payload.id);
component.layout();
this._rewriteConfig();
})));
this._editDispose.push(subscriptionToDisposable(this._grid.onDragStop.subscribe((e: NgGridItem) => {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this._scrollable.nativeElement),
height: getContentHeight(this._scrollContainer.nativeElement)
});
let event = e.getEventOutput();
this._items.forEach(i => {
let config = this._originalConfig.find(j => j.id === i.getEventOutput().payload.id);
if ((config.gridItemConfig && config.gridItemConfig.col) || config.id === event.payload.id) {
if (!config.gridItemConfig) {
config.gridItemConfig = {};
}
config.gridItemConfig.col = i.col;
config.gridItemConfig.row = i.row;
}
});
this._originalConfig.sort(configSorter);
this._rewriteConfig();
})));
this._widgets.forEach(i => {
if (i.id) {
i.enableEdit();
}
});
}
}
private _rewriteConfig(): void {
let writeableConfig = objects.clone(this._originalConfig);
writeableConfig.forEach(i => {
delete i.id;
});
let target: ConfigurationTarget = ConfigurationTarget.USER;
this.dashboardService.writeSettings(this.context, writeableConfig, target);
}
}

View File

@@ -3,7 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export * from 'rangy';
// add the text range to rangy
import 'rangy/lib/rangy-textrange';
dashboard-page {
height: 100%;
width: 100%;
position: absolute;
}

View File

@@ -2,19 +2,22 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InjectionToken } from '@angular/core';
import { InjectionToken, OnDestroy } from '@angular/core';
import { NgGridItemConfig } from 'angular2-grid';
import { Action } from 'vs/base/common/actions';
import { Disposable } from 'vs/base/common/lifecycle';
export interface IDashboardWidget {
actions: Array<Action>;
actionsContext?: any;
refresh?: () => void;
layout?: () => void;
}
export const WIDGET_CONFIG = new InjectionToken<WidgetConfig>('widget_config');
export interface WidgetConfig {
id?: string; // used to track the widget lifespan operations
name?: string;
icon?: string;
context: string;
@@ -26,13 +29,17 @@ export interface WidgetConfig {
border?: string;
fontSize?: string;
fontWeight?: string;
padding?:string;
padding?: string;
}
export abstract class DashboardWidget {
export abstract class DashboardWidget extends Disposable implements OnDestroy {
protected _config: WidgetConfig;
get actions(): Array<Action> {
return [];
}
}
ngOnDestroy() {
this.dispose();
}
}

View File

@@ -7,12 +7,12 @@
<div style="display: flex; flex-flow: column; overflow: hidden; height: 100%; width: 100%">
<div #header>
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex;flex: 0 0; padding: 3px 0 3px 0">
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex; flex: 0 0; padding: 3px 0 3px 0; flex-direction: row-reverse; justify-content: space-between">
<span #actionbar style="flex: 0 0 auto; align-self: end"></span>
<span *ngIf="_config.name" style="margin-left: 5px">{{_config.name}}</span>
<div *ngIf="_actions" (click)="onActionsClick($event)" style="float: right; margin-right: 5px; margin-left: auto; padding: 10px" class="icon toggle-more"></div>
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
</div>
</div>
<ng-template component-host>
</ng-template>
</div>
</div>

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./dashboardWidgetWrapper';
import {
Component, Input, Inject, forwardRef, ComponentFactoryResolver, AfterContentInit, ViewChild,
@@ -13,7 +14,7 @@ import { ComponentHostDirective } from './componentHost.directive';
import { WidgetConfig, WIDGET_CONFIG, IDashboardWidget } from './dashboardWidget';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { error } from 'sql/base/common/log';
import * as ACTIONS from './actions';
import { RefreshWidgetAction, ToggleMoreWidgetAction, DeleteWidgetAction } from './actions';
/* Widgets */
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
@@ -28,8 +29,8 @@ import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeS
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as themeColors from 'vs/workbench/common/theme';
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { Registry } from 'vs/platform/registry/common/platform';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
const componentMap: { [x: string]: Type<IDashboardWidget> } = {
'properties-widget': PropertiesWidgetComponent,
@@ -47,8 +48,10 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
private _themeDispose: IDisposable;
private _actions: Array<Action>;
private _component: IDashboardWidget;
private _actionbar: ActionBar;
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
constructor(
@@ -69,6 +72,11 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
ngAfterContentInit() {
this.updateTheme(this._bootstrap.themeService.getColorTheme());
this.loadWidget();
this._changeref.detectChanges();
this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
if (this._actions) {
this._actionbar.push(this._bootstrap.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, this._component.actionsContext), { icon: true, label: false });
}
}
ngOnDestroy() {
@@ -81,6 +89,24 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
}
}
public layout(): void {
if (this._component && this._component.layout) {
this._component.layout();
}
}
public get id(): string {
return this._config.id;
}
public enableEdit(): void {
this._actionbar.push(this._bootstrap.instantiationService.createInstance(DeleteWidgetAction, this._config.id, this._bootstrap.getUnderlyingUri()), { icon: true, label: false });
}
public disableEdit(): void {
this._actionbar.pull(this._actionbar.length() - 1);
}
private loadWidget(): void {
if (Object.keys(this._config.widget).length !== 1) {
error('Exactly 1 widget must be defined per space');
@@ -105,7 +131,7 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
this._component = componentRef.instance;
let actions = componentRef.instance.actions;
if (componentRef.instance.refresh) {
actions.push(this._bootstrap.instantiationService.createInstance(ACTIONS.RefreshWidgetAction, ACTIONS.RefreshWidgetAction.ID, ACTIONS.RefreshWidgetAction.LABEL, componentRef.instance.refresh));
actions.push(new RefreshWidgetAction(componentRef.instance.refresh, componentRef.instance));
}
if (actions !== undefined && actions.length > 0) {
this._actions = actions;
@@ -148,23 +174,12 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
return selector;
}
//tslint:disable-next-line
private onActionsClick(e: any) {
let anchor = { x: e.pageX + 1, y: e.pageY };
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(this._actions),
getActionsContext: () => this._component.actionsContext
});
}
private updateTheme(theme: IColorTheme): void {
let el = <HTMLElement>this._ref.nativeElement;
let headerEl: HTMLElement = this.header.nativeElement;
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
let backgroundColor = theme.getColor(colors.editorBackground, true);
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
// TODO: highContrastBorder does not exist, how to handle?
let border = theme.getColor(colors.contrastBorder, true);
if (this._config.background_color) {

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-widget-wrapper .action-label {
padding: 7px;
}

View File

@@ -4,9 +4,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: scroll; width: 100%; height: 100%">
<div #header style="margin-bottom: 5px">
<breadcrumb></breadcrumb>
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #header class="header" style="margin-bottom: 5px; flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center">
<div style="flex: 1 1 auto">
<breadcrumb></breadcrumb>
</div>
<div style="flex: 0 0 auto" #actionBar>
</div>
</div>
<router-outlet></router-outlet>
</div>
<div style="flex: 1 1 auto; position: relative">
<router-outlet (activate)="onActivate($event)"></router-outlet>
</div>
</div>

View File

@@ -3,16 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dashboard';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { DashboardServiceInterface } from './services/dashboardServiceInterface.service';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import * as Utils from 'sql/parts/connection/common/utils';
import { RefreshWidgetAction, EditDashboardAction } from 'sql/parts/dashboard/common/actions';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as themeColors from 'vs/workbench/common/theme';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
export const DASHBOARD_SELECTOR: string = 'dashboard-component';
@@ -22,7 +27,11 @@ export const DASHBOARD_SELECTOR: string = 'dashboard-component';
})
export class DashboardComponent implements OnInit, OnDestroy {
private _subs: Array<IDisposable> = new Array();
private _currentPage: DashboardPage;
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
@ViewChild('actionBar', { read: ElementRef }) private actionbarContainer: ElementRef;
private actionbar: ActionBar;
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrapService: DashboardServiceInterface,
@@ -31,13 +40,21 @@ export class DashboardComponent implements OnInit, OnDestroy {
) { }
ngOnInit() {
let self = this;
self._subs.push(self._bootstrapService.themeService.onDidColorThemeChange(e => self.updateTheme(e)));
self.updateTheme(self._bootstrapService.themeService.getColorTheme());
let profile: IConnectionProfile = self._bootstrapService.getOriginalConnectionProfile();
this._subs.push(this._bootstrapService.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
let profile: IConnectionProfile = this._bootstrapService.getOriginalConnectionProfile();
this.actionbar = new ActionBar(this.actionbarContainer.nativeElement);
this.actionbar.push(new RefreshWidgetAction(this.refresh, this), {
icon: true,
label: false,
});
this.actionbar.push(new EditDashboardAction(this.edit, this), {
icon: true,
label: false,
});
if (profile && (!profile.databaseName || Utils.isMaster(profile))) {
// Route to the server page as this is the default database
self._router.navigate(['server-dashboard']);
this._router.navigate(['server-dashboard']);
}
}
@@ -48,11 +65,23 @@ export class DashboardComponent implements OnInit, OnDestroy {
}
private updateTheme(theme: IColorTheme): void {
let headerEl = <HTMLElement> this.header.nativeElement;
let headerEl = <HTMLElement>this.header.nativeElement;
headerEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
headerEl.style.borderBottomWidth = '1px';
headerEl.style.borderBottomStyle = 'solid';
}
onActivate(page: DashboardPage) {
this._currentPage = page;
}
refresh(): void {
if (this._currentPage) {
this._currentPage.refresh();
}
}
edit(): void {
this._currentPage.enableEdit();
}
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.dashboardEditor .header .monaco-action-bar .action-label {
padding: 8px;
}
.dashboardEditor .header .monaco-action-bar .action-item {
margin-right: 5px;
}

View File

@@ -11,7 +11,6 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { DashboardInput } from './dashboardInput';
import { DashboardModule } from './dashboard.module';
@@ -66,7 +65,7 @@ export class DashboardEditor extends BaseEditor {
super.setInput(input, options);
$(parentElement).empty();
$(parentElement).clearChildren();
if (!input.hasBootstrapped) {
let container = DOM.$<HTMLElement>('.dashboardEditor');
@@ -105,8 +104,3 @@ export class DashboardEditor extends BaseEditor {
super.dispose();
}
}
ModesRegistry.registerLanguage({
extensions: ['.dashboard'],
id: 'dashboard',
});

View File

@@ -76,7 +76,7 @@ export class DashboardInput extends EditorInput {
public getResource(): URI {
return URI.from({
scheme: 'dashboard',
path: '.dashboard'
path: 'dashboard'
});
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Inject, forwardRef, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
@@ -12,10 +12,9 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDestroy {
export class DatabaseDashboardPage extends DashboardPage implements OnInit {
protected propertiesWidget: WidgetConfig = {
name: nls.localize('databasePageName', 'DATABASE DASHBOARD'),
widget: {
@@ -32,15 +31,15 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDe
};
protected readonly context = 'database';
private _dispose: IDisposable[] = [];
constructor(
@Inject(forwardRef(() => IBreadcrumbService)) private _breadcrumbService: IBreadcrumbService,
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(dashboardService);
this._dispose.push(dashboardService.onUpdatePage(() => {
super(dashboardService, el, _cd);
this._register(dashboardService.onUpdatePage(() => {
this.refresh(true);
this._cd.detectChanges();
}));
@@ -49,11 +48,5 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDe
ngOnInit() {
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
this.baseInit();
}
ngOnDestroy() {
this._dispose = dispose(this._dispose);
this.baseDestroy();
}
}

View File

@@ -120,6 +120,12 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
},
sizey: {
type: 'number'
},
col: {
type: 'number'
},
row: {
type: 'number'
}
}
},
@@ -155,4 +161,4 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
};
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Inject, forwardRef, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
@@ -14,7 +14,7 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
export class ServerDashboardPage extends DashboardPage implements OnInit, OnDestroy {
export class ServerDashboardPage extends DashboardPage implements OnInit {
protected propertiesWidget: WidgetConfig = {
name: nls.localize('serverPageName', 'SERVER DASHBOARD'),
widget: {
@@ -35,9 +35,10 @@ export class ServerDashboardPage extends DashboardPage implements OnInit, OnDest
constructor(
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(dashboardService);
super(dashboardService, el, cd);
// revert back to default database
this.dashboardService.connectionManagementService.changeDatabase('master').then(() => {
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = undefined;
@@ -49,10 +50,5 @@ export class ServerDashboardPage extends DashboardPage implements OnInit, OnDest
ngOnInit() {
this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
this.baseInit();
}
ngOnDestroy() {
this.baseDestroy();
}
}

View File

@@ -17,12 +17,10 @@ import { ConnectionManagementInfo } from 'sql/parts/connection/common/connection
import { IAdminService } from 'sql/parts/admin/common/adminService';
import { IQueryManagementService } from 'sql/parts/query/common/queryManagement';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
import { AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
import { ProviderMetadata, DatabaseInfo, SimpleExecuteResult } from 'data';
@@ -31,7 +29,8 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConfigurationEditingService, IConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService'
import { IMessageService } from 'vs/platform/message/common/message';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IStorageService } from 'vs/platform/storage/common/storage';
@@ -127,10 +126,14 @@ export class DashboardServiceInterface implements OnDestroy {
private _workspaceContextService: IWorkspaceContextService;
private _storageService: IStorageService;
private _capabilitiesService: ICapabilitiesService;
private _configurationEditingService: ConfigurationEditingService;
private _updatePage = new Emitter<void>();
public readonly onUpdatePage: Event<void> = this._updatePage.event;
private _onDeleteWidget = new Emitter<string>();
public readonly onDeleteWidget: Event<string> = this._onDeleteWidget.event;
constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
@Inject(forwardRef(() => Router)) private _router: Router,
@@ -145,6 +148,7 @@ export class DashboardServiceInterface implements OnDestroy {
this._workspaceContextService = this._bootstrapService.workspaceContextService;
this._storageService = this._bootstrapService.storageService;
this._capabilitiesService = this._bootstrapService.capabilitiesService;
this._configurationEditingService = this._bootstrapService.configurationEditorService;
}
ngOnDestroy() {
@@ -155,6 +159,10 @@ export class DashboardServiceInterface implements OnDestroy {
return this._messageService;
}
public get configurationEditingService(): ConfigurationEditingService {
return this._configurationEditingService;
}
public get metadataService(): SingleConnectionMetadataService {
return this._metadataService;
}
@@ -195,7 +203,7 @@ export class DashboardServiceInterface implements OnDestroy {
return this._storageService;
}
public get CapabilitiesService(): ICapabilitiesService {
public get capabilitiesService(): ICapabilitiesService {
return this._capabilitiesService;
}
@@ -241,13 +249,17 @@ export class DashboardServiceInterface implements OnDestroy {
* Get settings for given string
* @param type string of setting to get from dashboard settings; i.e dashboard.{type}
*/
public getSettings(type: string): { widgets: Array<WidgetConfig>, properties: boolean | IPropertiesConfig[] } {
let config = this._configService.getConfiguration(DASHBOARD_SETTINGS);
return config[type];
public getSettings<T>(type: string): T {
let config = this._configService.getValue<T>([DASHBOARD_SETTINGS, type].join('.'));
return config;
}
private handleDashboardEvent(event: AngularEventType): void {
switch (event) {
public writeSettings(key: string, value: any, target: ConfigurationTarget) {
this._configurationEditingService.writeConfiguration(target, { key: DASHBOARD_SETTINGS + '.' + key + '.widgets', value });
}
private handleDashboardEvent(event: IAngularEvent): void {
switch (event.event) {
case AngularEventType.NAV_DATABASE:
this.connectionManagementService.changeDatabase(this.connectionManagementService.connectionInfo.connectionProfile.databaseName).then(
result => {
@@ -269,6 +281,8 @@ export class DashboardServiceInterface implements OnDestroy {
case AngularEventType.NAV_SERVER:
this._router.navigate(['server-dashboard']);
break;
case AngularEventType.DELETE_WIDGET:
this._onDeleteWidget.fire(event.payload.id);
}
}
}

View File

@@ -1,89 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { IConnectionManagementService, MetadataType } from 'sql/parts/connection/common/connectionManagement';
import {
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction,
BackupAction, BaseActionContext, ManageAction
} from 'sql/workbench/common/actions';
import { IDisasterRecoveryUiService } from 'sql/parts/disasterRecovery/common/interfaces';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction } from 'vs/base/common/actions';
export function GetExplorerActions(type: MetadataType, isCloud: boolean, dashboardService: DashboardServiceInterface): TPromise<IAction[]> {
let actions: IAction[] = [];
// When context menu on database
if (type === undefined) {
actions.push(dashboardService.instantiationService.createInstance(DashboardNewQueryAction, DashboardNewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
if (!isCloud) {
actions.push(dashboardService.instantiationService.createInstance(DashboardBackupAction, DashboardBackupAction.ID, DashboardBackupAction.LABEL));
}
actions.push(dashboardService.instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
return TPromise.as(actions);
}
if (type === MetadataType.View || type === MetadataType.Table) {
actions.push(dashboardService.instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
}
if (type === MetadataType.Table) {
actions.push(dashboardService.instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
}
actions.push(dashboardService.instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
return TPromise.as(actions);
}
export class DashboardBackupAction extends BackupAction {
public static ID = 'dashboard.' + BackupAction.ID;
constructor(
id: string, label: string,
@IDisasterRecoveryUiService disasterRecoveryService: IDisasterRecoveryUiService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService
) {
super(id, label, BackupAction.ICON, disasterRecoveryService, );
}
run(actionContext: BaseActionContext): TPromise<boolean> {
let self = this;
// change database before performing action
return new TPromise<boolean>((resolve, reject) => {
self.connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
actionContext.connInfo = self.connectionManagementService.getConnectionInfo(actionContext.uri);
super.run(actionContext).then((result) => {
resolve(result);
});
},
() => {
resolve(false);
});
});
}
}
export class DashboardNewQueryAction extends NewQueryAction {
public static ID = 'dashboard.' + NewQueryAction.ID;
run(actionContext: BaseActionContext): TPromise<boolean> {
let self = this;
// change database before performing action
return new TPromise<boolean>((resolve, reject) => {
self._connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
actionContext.profile = self._connectionManagementService.getConnectionProfile(actionContext.uri);
super.run(actionContext).then((result) => {
resolve(result);
});
},
() => {
resolve(false);
});
});
}
}

View File

@@ -0,0 +1,392 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Router } from '@angular/router';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
import { SingleConnectionManagementService } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import {
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction, ScriptExecuteAction, ScriptAlterAction,
BackupAction, ManageActionContext, BaseActionContext, ManageAction, RestoreAction
} from 'sql/workbench/common/actions';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import * as Constants from 'sql/parts/connection/common/constants';
import { ObjectMetadata } from 'data';
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { generateUuid } from 'vs/base/common/uuid';
import { $ } from 'vs/base/browser/dom';
export class ObjectMetadataWrapper implements ObjectMetadata {
public metadataType: MetadataType;
public metadataTypeName: string;
public urn: string;
public name: string;
public schema: string;
constructor(from?: ObjectMetadata) {
if (from) {
this.metadataType = from.metadataType;
this.metadataTypeName = from.metadataTypeName;
this.urn = from.urn;
this.name = from.name;
this.schema = from.schema;
}
}
public matches(other: ObjectMetadataWrapper): boolean {
if (!other) {
return false;
}
return this.metadataType === other.metadataType
&& this.schema === other.schema
&& this.name === other.name;
}
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
if (!objectMetadata) {
return undefined;
}
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
}
// custom sort : Table > View > Stored Procedures > Function
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
// compare the object type
if (metadata1.metadataType < metadata2.metadataType) {
return -1;
} else if (metadata1.metadataType > metadata2.metadataType) {
return 1;
// otherwise compare the schema
} else {
let schemaCompare: number = metadata1.schema && metadata2.schema
? metadata1.schema.localeCompare(metadata2.schema)
// schemas are not expected to be undefined, but if they are then compare using object names
: 0;
if (schemaCompare !== 0) {
return schemaCompare;
// otherwise compare the object name
} else {
return metadata1.name.localeCompare(metadata2.name);
}
}
}
}
export declare type TreeResource = IConnectionProfile | ObjectMetadataWrapper;
// Empty class just for tree input
export class ExplorerModel {
public static readonly id = generateUuid();
}
export class ExplorerController extends TreeDefaults.DefaultController {
constructor(
// URI for the dashboard for managing, should look into some other way of doing this
private _uri,
private _connectionService: SingleConnectionManagementService,
private _router: Router,
private _contextMenuService: IContextMenuService,
private _capabilitiesService: ICapabilitiesService,
private _instantiationService: IInstantiationService
) {
super();
}
protected onLeftClick(tree: tree.ITree, element: TreeResource, event: IMouseEvent, origin: string = 'mouse'): boolean {
const payload = { origin: origin };
const isDoubleClick = (origin === 'mouse' && event.detail === 2);
// Cancel Event
const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
if (!isMouseDown) {
event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
}
event.stopPropagation();
tree.setFocus(element, payload);
if (!(element instanceof ObjectMetadataWrapper) && isDoubleClick) {
event.preventDefault(); // focus moves to editor, we need to prevent default
this.handleItemDoubleClick(element);
} else {
tree.setFocus(element, payload);
tree.setSelection([element], payload);
}
return true;
}
public onContextMenu(tree: tree.ITree, element: TreeResource, event: tree.ContextMenuEvent): boolean {
let context: ManageActionContext | BaseActionContext;
if (element instanceof ObjectMetadataWrapper) {
context = {
object: element,
profile: this._connectionService.connectionInfo.connectionProfile
};
} else {
context = {
profile: element,
uri: this._uri
};
}
this._contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => GetExplorerActions(element, this._instantiationService, this._capabilitiesService, this._connectionService.connectionInfo),
getActionsContext: () => context
});
return true;
}
private handleItemDoubleClick(element: IConnectionProfile): void {
this._connectionService.changeDatabase(element.databaseName).then(result => {
this._router.navigate(['database-dashboard']);
});
}
}
export class ExplorerDataSource implements tree.IDataSource {
private _data: TreeResource[];
public getId(tree: tree.ITree, element: TreeResource | ExplorerModel): string {
if (element instanceof ObjectMetadataWrapper) {
return element.urn || element.schema + element.name;
} else if (element instanceof ExplorerModel) {
return ExplorerModel.id;
} else {
return (element as IConnectionProfile).getOptionsKey();
}
}
public hasChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): boolean {
if (element instanceof ExplorerModel) {
return true;
} else {
return false;
}
}
public getChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise {
if (element instanceof ExplorerModel) {
return TPromise.as(this._data);
} else {
return TPromise.as(undefined);
}
}
public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise {
if (element instanceof ExplorerModel) {
return TPromise.as(undefined);
} else {
return TPromise.as(new ExplorerModel());
}
}
public set data(data: TreeResource[]) {
this._data = data;
}
}
enum TEMPLATEIDS {
profile = 'profile',
object = 'object'
}
export interface IListTemplate {
icon?: HTMLElement;
label: HTMLElement;
}
export class ExplorerRenderer implements tree.IRenderer {
public getHeight(tree: tree.ITree, element: TreeResource): number {
return 22;
}
public getTemplateId(tree: tree.ITree, element: TreeResource): string {
if (element instanceof ObjectMetadataWrapper) {
return TEMPLATEIDS.object;
} else {
return TEMPLATEIDS.profile;
}
}
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
let row = $('.list-row');
let label = $('.label');
let icon: HTMLElement;
if (templateId === TEMPLATEIDS.object) {
icon = $('div');
} else {
icon = $('.icon.database');
}
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
public renderElement(tree: tree.ITree, element: TreeResource, templateId: string, templateData: IListTemplate): void {
if (element instanceof ObjectMetadataWrapper) {
switch (element.metadataType) {
case MetadataType.Function:
templateData.icon.className = 'icon scalarvaluedfunction';
break;
case MetadataType.SProc:
templateData.icon.className = 'icon stored-procedure';
break;
case MetadataType.Table:
templateData.icon.className = 'icon table';
break;
case MetadataType.View:
templateData.icon.className = 'icon view';
break;
}
templateData.label.innerText = element.schema + '.' + element.name;
} else {
templateData.label.innerText = element.databaseName;
}
}
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
// no op
}
}
export class ExplorerFilter implements tree.IFilter {
private _filterString: string;
public isVisible(tree: tree.ITree, element: TreeResource): boolean {
if (element instanceof ObjectMetadataWrapper) {
return this._doIsVisibleObjectMetadata(element);
} else {
return this._doIsVisibleConnectionProfile(element);
}
}
// apply filter to databasename of the profile
private _doIsVisibleConnectionProfile(element: IConnectionProfile): boolean {
if (!this._filterString) {
return true;
}
let filterString = this._filterString.trim().toLowerCase();
return element.databaseName.toLowerCase().includes(filterString);
}
// apply filter for objectmetadatawrapper
// could be improved by pre-processing the filter string
private _doIsVisibleObjectMetadata(element: ObjectMetadataWrapper): boolean {
if (!this._filterString) {
return true;
}
// freeze filter string for edge cases
let filterString = this._filterString.trim().toLowerCase();
// determine if a filter is applied
let metadataType: MetadataType;
if (filterString.includes(':')) {
let filterArray = filterString.split(':');
if (filterArray.length > 2) {
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
} else {
filterString = filterArray[1];
}
switch (filterArray[0].toLowerCase()) {
case 'v':
metadataType = MetadataType.View;
break;
case 't':
metadataType = MetadataType.Table;
break;
case 'sp':
metadataType = MetadataType.SProc;
break;
case 'f':
metadataType = MetadataType.Function;
break;
case 'a':
return true;
default:
break;
}
}
if (metadataType !== undefined) {
return element.metadataType === metadataType && (element.schema + '.' + element.name).toLowerCase().includes(filterString);
} else {
return (element.schema + '.' + element.name).toLowerCase().includes(filterString);
}
}
public set filterString(val: string) {
this._filterString = val;
}
}
function GetExplorerActions(element: TreeResource, instantiationService: IInstantiationService, capabilitiesService: ICapabilitiesService, info: ConnectionManagementInfo): TPromise<IAction[]> {
let actions: IAction[] = [];
if (element instanceof ObjectMetadataWrapper) {
if (element.metadataType === MetadataType.View || element.metadataType === MetadataType.Table) {
actions.push(instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
}
if (element.metadataType === MetadataType.Table) {
actions.push(instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
}
if (element.metadataType === MetadataType.SProc && info.connectionProfile.providerName === Constants.mssqlProviderName) {
actions.push(instantiationService.createInstance(ScriptExecuteAction, ScriptExecuteAction.ID, ScriptExecuteAction.LABEL));
}
if ((element.metadataType === MetadataType.SProc || element.metadataType === MetadataType.Function || element.metadataType === MetadataType.View)
&& info.connectionProfile.providerName === Constants.mssqlProviderName) {
actions.push(instantiationService.createInstance(ScriptAlterAction, ScriptAlterAction.ID, ScriptAlterAction.LABEL));
}
} else {
actions.push(instantiationService.createInstance(NewQueryAction, NewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
let action: IAction = instantiationService.createInstance(RestoreAction, RestoreAction.ID, RestoreAction.LABEL, RestoreAction.ICON);
if (capabilitiesService.isFeatureAvailable(action, info)) {
actions.push(action);
}
action = instantiationService.createInstance(BackupAction, BackupAction.ID, BackupAction.LABEL, BackupAction.ICON);
if (capabilitiesService.isFeatureAvailable(action, info)) {
actions.push(action);
}
actions.push(instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
return TPromise.as(actions);
}
actions.push(instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
return TPromise.as(actions);
}

View File

@@ -9,4 +9,4 @@
<div style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%"></div>
</div>
</div>
</div>

View File

@@ -7,196 +7,41 @@ import 'vs/css!sql/media/objectTypes/objecttypes';
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./media/explorerWidget';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Router } from '@angular/router';
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { BaseActionContext } from 'sql/workbench/common/actions';
import { GetExplorerActions } from './explorerActions';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { warn } from 'sql/base/common/log';
import { MultipleRequestDelayer } from 'sql/base/common/async';
import { ExplorerFilter, ExplorerRenderer, ExplorerDataSource, ExplorerController, ObjectMetadataWrapper, ExplorerModel } from './explorerTree';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IDisposable } from 'vs/base/common/lifecycle';
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler';
import * as nls from 'vs/nls';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import * as types from 'vs/base/common/types';
import { $, getContentHeight } from 'vs/base/browser/dom';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { getContentHeight } from 'vs/base/browser/dom';
import { Delayer } from 'vs/base/common/async';
import { ObjectMetadata } from 'data';
export class ObjectMetadataWrapper implements ObjectMetadata {
public metadataType: MetadataType;
public metadataTypeName: string;
public urn: string;
public name: string;
public schema: string;
constructor(from?: ObjectMetadata) {
if (from) {
this.metadataType = from.metadataType;
this.metadataTypeName = from.metadataTypeName;
this.urn = from.urn;
this.name = from.name;
this.schema = from.schema;
}
}
public matches(other: ObjectMetadataWrapper): boolean {
if (!other) {
return false;
}
return this.metadataType === other.metadataType
&& this.schema === other.schema
&& this.name === other.name;
}
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
if (!objectMetadata) {
return undefined;
}
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
}
// custom sort : Table > View > Stored Procedures > Function
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
// compare the object type
if (metadata1.metadataType < metadata2.metadataType) {
return -1;
} else if (metadata1.metadataType > metadata2.metadataType) {
return 1;
// otherwise compare the schema
} else {
let schemaCompare: number = metadata1.schema && metadata2.schema
? metadata1.schema.localeCompare(metadata2.schema)
// schemas are not expected to be undefined, but if they are then compare using object names
: 0;
if (schemaCompare !== 0) {
return schemaCompare;
// otherwise compare the object name
} else {
return metadata1.name.localeCompare(metadata2.name);
}
}
}
}
declare type ListResource = string | ObjectMetadataWrapper;
enum TemplateIds {
STRING = 'string',
METADATA = 'metadata'
}
interface IListTemplate {
icon?: HTMLElement;
label: HTMLElement;
}
class Delegate implements IDelegate<ListResource> {
getHeight(element: ListResource): number {
return 22;
}
getTemplateId(element: ListResource): string {
if (element instanceof ObjectMetadataWrapper) {
return TemplateIds.METADATA.toString();
} else if (types.isString(element)) {
return TemplateIds.STRING.toString();
} else {
return '';
}
}
}
class StringRenderer implements IRenderer<string, IListTemplate> {
public readonly templateId = TemplateIds.STRING.toString();
renderTemplate(container: HTMLElement): IListTemplate {
let row = $('.list-row');
let icon = $('.icon.database');
let label = $('.label');
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
renderElement(element: string, index: number, templateData: IListTemplate): void {
templateData.label.innerText = element;
}
disposeTemplate(templateData: IListTemplate): void {
// no op
}
}
class MetadataRenderer implements IRenderer<ObjectMetadataWrapper, IListTemplate> {
public readonly templateId = TemplateIds.METADATA.toString();
renderTemplate(container: HTMLElement): IListTemplate {
let row = $('.list-row');
let icon = $('div');
let label = $('.label');
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
renderElement(element: ObjectMetadataWrapper, index: number, templateData: IListTemplate): void {
if (element && element) {
switch (element.metadataType) {
case MetadataType.Function:
templateData.icon.className = 'icon scalarvaluedfunction';
break;
case MetadataType.SProc:
templateData.icon.className = 'icon stored-procedure';
break;
case MetadataType.Table:
templateData.icon.className = 'icon table';
break;
case MetadataType.View:
templateData.icon.className = 'icon view';
break;
}
templateData.label.innerText = element.schema + '.' + element.name;
}
}
disposeTemplate(templateData: IListTemplate): void {
// no op
}
}
@Component({
selector: 'explorer-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/explorer/explorerWidget.component.html'))
})
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
private _isCloud: boolean;
private _tableData: ListResource[];
private _disposables: Array<IDisposable> = [];
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit {
private _input: InputBox;
private _table: List<ListResource>;
private _lastClickedItem: ListResource;
private _tree: Tree;
private _filterDelayer = new Delayer<void>(200);
private _dblClickDelayer = new MultipleRequestDelayer<void>(500);
private _treeController = new ExplorerController(
this._bootstrap.getUnderlyingUri(),
this._bootstrap.connectionManagementService,
this._router,
this._bootstrap.contextMenuService,
this._bootstrap.capabilitiesService,
this._bootstrap.instantiationService
);
private _treeRenderer = new ExplorerRenderer();
private _treeDataSource = new ExplorerDataSource();
private _treeFilter = new ExplorerFilter();
@ViewChild('input') private _inputContainer: ElementRef;
@ViewChild('table') private _tableContainer: ElementRef;
@@ -209,7 +54,6 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
) {
super();
this._isCloud = _bootstrap.connectionManagementService.connectionInfo.serverInfo.isCloud;
this.init();
}
@@ -218,41 +62,34 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
placeholder: this._config.context === 'database' ? nls.localize('seachObjects', 'Search by name of type (a:, t:, v:, f:, or sp:)') : nls.localize('searchDatabases', 'Search databases')
};
this._input = new InputBox(this._inputContainer.nativeElement, this._bootstrap.contextViewService, inputOptions);
this._disposables.push(this._input.onDidChange(e => {
this._register(this._input.onDidChange(e => {
this._filterDelayer.trigger(() => {
this._table.splice(0, this._table.length, this._filterTable(e));
this._treeFilter.filterString = e;
this._tree.refresh();
});
}));
this._table = new List<ListResource>(this._tableContainer.nativeElement, new Delegate(), [new MetadataRenderer(), new StringRenderer()]);
this._disposables.push(this._table.onContextMenu(e => {
this.handleContextMenu(e.element, e.index, e.anchor);
}));
this._disposables.push(this._table.onSelectionChange(e => {
if (e.elements.length > 0 && this._lastClickedItem === e.elements[0]) {
this._dblClickDelayer.trigger(() => this.handleItemDoubleClick(e.elements[0]));
} else {
this._lastClickedItem = e.elements.length > 0 ? e.elements[0] : undefined;
}
}));
this._table.layout(getContentHeight(this._tableContainer.nativeElement));
this._disposables.push(this._input);
this._disposables.push(attachInputBoxStyler(this._input, this._bootstrap.themeService));
this._disposables.push(this._table);
this._disposables.push(attachListStyler(this._table, this._bootstrap.themeService));
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
});
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
this._register(this._input);
this._register(attachInputBoxStyler(this._input, this._bootstrap.themeService));
this._register(this._tree);
this._register(attachListStyler(this._tree, this._bootstrap.themeService));
}
private init(): void {
if (this._config.context === 'database') {
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
this._register(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
data => {
if (data) {
this._tableData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
this._tableData.sort(ObjectMetadataWrapper.sort);
this._table.splice(0, this._table.length, this._tableData);
let objectData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
objectData.sort(ObjectMetadataWrapper.sort);
this._treeDataSource.data = objectData;
this._tree.setInput(new ExplorerModel());
}
},
error => {
@@ -260,10 +97,16 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
}
)));
} else {
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
let currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
this._register(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
data => {
this._tableData = data;
this._table.splice(0, this._table.length, this._tableData);
let profileData = data.map(d => {
let profile = new ConnectionProfile(currentProfile.serverCapabilities, currentProfile);
profile.databaseName = d;
return profile;
});
this._treeDataSource.data = profileData;
this._tree.setInput(new ExplorerModel());
},
error => {
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.databaseError', "Unable to load databases");
@@ -272,123 +115,7 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
}
}
/**
* Handles action when an item is double clicked in the explorer widget
* @param val If on server page, explorer objects will be strings representing databases;
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
*
*/
private handleItemDoubleClick(val: ListResource): void {
if (types.isString(val)) {
this._bootstrap.connectionManagementService.changeDatabase(val as string).then(result => {
this._router.navigate(['database-dashboard']);
});
}
}
/**
* Handles action when a item is clicked in the explorer widget
* @param val If on server page, explorer objects will be strings representing databases;
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
* @param index Index of the value in the array the ngFor template is built from
* @param event Click event
*/
private handleContextMenu(val: ListResource, index: number, anchor: HTMLElement | { x: number, y: number }): void {
// event will exist if the context menu span was clicked
if (event) {
if (this._config.context === 'server') {
let newProfile = <IConnectionProfile>Object.create(this._bootstrap.connectionManagementService.connectionInfo.connectionProfile);
newProfile.databaseName = val as string;
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => GetExplorerActions(undefined, this._isCloud, this._bootstrap),
getActionsContext: () => {
return <BaseActionContext>{
uri: this._bootstrap.getUnderlyingUri(),
profile: newProfile,
connInfo: this._bootstrap.connectionManagementService.connectionInfo,
databasename: val as string
};
}
});
} else if (this._config.context === 'database') {
let object = val as ObjectMetadataWrapper;
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => GetExplorerActions(object.metadataType, this._isCloud, this._bootstrap),
getActionsContext: () => {
return <BaseActionContext>{
object: object,
uri: this._bootstrap.getUnderlyingUri(),
profile: this._bootstrap.connectionManagementService.connectionInfo.connectionProfile
};
}
});
} else {
warn('Unknown dashboard context: ', this._config.context);
}
}
this._changeRef.detectChanges();
}
private _filterTable(val: string): ListResource[] {
let items = this._tableData;
if (!items) {
return items;
}
// format filter string for clean filter, no white space and lower case
let filterString = val.trim().toLowerCase();
// handle case when passed a string array
if (types.isString(items[0])) {
let _items = <string[]>items;
return _items.filter(item => {
return item.toLowerCase().includes(filterString);
});
}
// make typescript compiler happy
let objectItems = items as ObjectMetadataWrapper[];
// determine is a filter is applied
let metadataType: MetadataType;
if (val.includes(':')) {
let filterArray = filterString.split(':');
if (filterArray.length > 2) {
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
} else {
filterString = filterArray[1];
}
switch (filterArray[0].toLowerCase()) {
case 'v':
metadataType = MetadataType.View;
break;
case 't':
metadataType = MetadataType.Table;
break;
case 'sp':
metadataType = MetadataType.SProc;
break;
case 'f':
metadataType = MetadataType.Function;
break;
case 'a':
return objectItems;
default:
break;
}
}
return objectItems.filter(item => {
if (metadataType !== undefined) {
return item.metadataType === metadataType && (item.schema + '.' + item.name).toLowerCase().includes(filterString);
} else {
return (item.schema + '.' + item.name).toLowerCase().includes(filterString);
}
});
public refresh(): void {
this.init();
}
}

View File

@@ -11,4 +11,5 @@ explorer-widget .list-row {
display: flex;
flex-direction: row;
align-items: center;
}
margin-left: -33px;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import {
Component, Inject, ViewContainerRef, forwardRef, AfterContentInit,
ComponentFactoryResolver, ViewChild, OnDestroy, ChangeDetectorRef
ComponentFactoryResolver, ViewChild, ChangeDetectorRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
@@ -15,39 +15,45 @@ import { InsightAction, InsightActionContext } from 'sql/workbench/common/action
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { IInsightsConfig, IInsightsView } from './interfaces';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { insertValueRegex } from 'sql/parts/insights/browser/insightsDialogView';
import { insertValueRegex } from 'sql/parts/insights/common/interfaces';
import { RunInsightQueryAction } from './actions';
import { SimpleExecuteResult } from 'data';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Action } from 'vs/base/common/actions';
import * as types from 'vs/base/common/types';
import * as pfs from 'vs/base/node/pfs';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { WorkbenchState } from 'vs/platform/workspace/common/workspace';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
interface IStorageResult {
date: string;
results: SimpleExecuteResult;
}
@Component({
selector: 'insights-widget',
template: `
<div *ngIf="error" style="text-align: center; padding-top: 20px">{{error}}</div>
<div *ngIf="lastUpdated" style="font-style: italic; font-size: 80%; margin-left: 5px">{{lastUpdated}}</div>
<div style="margin: 10px; width: calc(100% - 20px); height: calc(100% - 20px)">
<ng-template component-host></ng-template>
</div>`,
styles: [':host { width: 100%; height: 100%}']
styles: [':host { width: 100%; height: 100% }']
})
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit, OnDestroy {
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit {
private insightConfig: IInsightsConfig;
private queryObv: Observable<SimpleExecuteResult>;
private _disposables: Array<IDisposable> = [];
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
private _typeKey: string;
private _init: boolean = false;
public error: string;
public lastUpdated: string;
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
@@ -90,7 +96,7 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
ngAfterContentInit() {
this._init = true;
if (this.queryObv) {
this._disposables.push(toDisposableSubscription(this.queryObv.subscribe(
this._register(toDisposableSubscription(this.queryObv.subscribe(
result => {
this._updateChild(result);
},
@@ -101,10 +107,6 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
}
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
}
private showError(error: string): void {
this.error = error;
this._cd.detectChanges();
@@ -128,7 +130,11 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
private _storeResult(result: SimpleExecuteResult): SimpleExecuteResult {
if (this.insightConfig.cacheId) {
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(result));
let store: IStorageResult = {
date: new Date().toString(),
results: result
};
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(store));
}
return result;
}
@@ -137,8 +143,12 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
if (this.insightConfig.cacheId) {
let storage = this.dashboardService.storageService.get(this._getStorageKey());
if (storage) {
let storedResult: IStorageResult = JSON.parse(storage);
let date = new Date(storedResult.date);
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", date.toLocaleTimeString(), date.toLocaleDateString());
if (this._init) {
this._updateChild(JSON.parse(storage));
this._updateChild(storedResult.results);
this._cd.detectChanges();
} else {
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
}
@@ -151,17 +161,11 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
return false;
}
public get refresh(): () => void {
return this._refresh();
}
public _refresh(): () => void {
return () => {
this._runQuery().then(
result => this._updateChild(result),
error => this.showError(error)
);
};
public refresh(): void {
this._runQuery().then(
result => this._updateChild(result),
error => this.showError(error)
);
}
private _getStorageKey(): string {
@@ -192,7 +196,10 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
let componentInstance = componentRef.instance;
componentInstance.data = { columns: result.columnInfo.map(item => item.columnName), rows: result.rows.map(row => row.map(item => item.displayValue)) };
// check if the setter is defined
componentInstance.config = this.insightConfig.type[this._typeKey];
if (componentInstance.setConfig) {
componentInstance.setConfig(this.insightConfig.type[this._typeKey]);
}
if (componentInstance.init) {
componentInstance.init();
}
@@ -239,7 +246,28 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
let match = filePath.match(insertValueRegex);
if (match && match.length > 0 && match[1] === 'workspaceRoot') {
filePath = filePath.replace(match[0], '');
filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
//filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
switch (this.dashboardService.workspaceContextService.getWorkbenchState()) {
case WorkbenchState.FOLDER:
filePath = this.dashboardService.workspaceContextService.getWorkspace().folders[0].toResource(filePath).fsPath;
break;
case WorkbenchState.WORKSPACE:
let filePathArray = filePath.split('/');
// filter out empty sections
filePathArray = filePathArray.filter(i => !!i);
let folder = this.dashboardService.workspaceContextService.getWorkspace().folders.find(i => i.name === filePathArray[0]);
if (!folder) {
return Promise.reject<void[]>(new Error(`Could not find workspace folder ${filePathArray[0]}`));
}
// remove the folder name from the filepath
filePathArray.shift();
// rejoin the filepath after doing the work to find the right folder
filePath = '/' + filePathArray.join('/');
filePath = folder.toResource(filePath).fsPath;
break;
}
}
promises.push(new Promise((resolve, reject) => {
pfs.readFile(filePath).then(
@@ -256,4 +284,4 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
return Promise.all(promises);
}
}
}

View File

@@ -4,19 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { IInsightRegistry, Extensions as InsightExtensions } from 'sql/platform/dashboard/common/insightRegistry';
import { ITaskRegistry, Extensions as TaskExtensions } from 'sql/platform/tasks/taskRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
const taskRegistry = Registry.as<ITaskRegistry>(TaskExtensions.TaskContribution);
export const insightsSchema: IJSONSchema = {
type: 'object',
description: nls.localize('insightWidgetDescription', 'Adds a widget that can query a server or database and display the results in multiple ways - as a chart, summarized count, and more'),
properties: {
id: {
cacheId: {
type: 'string',
description: nls.localize('insightIdDescription', 'Unique Identifier used for cacheing the results of the insight.')
},
@@ -87,8 +85,11 @@ export const insightsSchema: IJSONSchema = {
type: 'object',
properties: {
types: {
type: 'object',
properties: taskRegistry.taskSchemas
description: nls.localize('actionTypes', "Which actions to use"),
type: 'array',
items: {
type: 'string'
}
},
database: {
type: 'string',

View File

@@ -38,7 +38,7 @@ export interface IInsightData {
export interface IInsightsView {
data: IInsightData;
config?: { [key: string]: any };
setConfig?: (config: { [key: string]: any }) => void;
init?: () => void;
}

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, ViewChild } from '@angular/core';
import { BaseChartDirective } from 'ng2-charts/ng2-charts';
/* SQL Imports */
@@ -18,7 +18,7 @@ import * as colors from 'vs/platform/theme/common/colorRegistry';
import { mixin } from 'sql/base/common/objects';
import { Color } from 'vs/base/common/color';
import * as types from 'vs/base/common/types';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
export enum ChartType {
@@ -99,14 +99,13 @@ export const defaultChartConfig: IChartConfig = {
[options]="_options"></canvas>
</div>`
})
export abstract class ChartInsight implements IInsightsView, OnDestroy {
export abstract class ChartInsight extends Disposable implements IInsightsView {
private _isDataAvailable: boolean = false;
private _options: any = {};
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
protected _defaultConfig = defaultChartConfig;
protected _disposables: Array<IDisposable> = [];
protected _config: IChartConfig;
protected _data: IInsightData;
@@ -116,14 +115,13 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService) { }
ngOnDestroy() {
this._disposables.forEach(item => item.dispose());
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService
) {
super();
}
init() {
this._disposables.push(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._register(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
// Note: must use a boolean to not render the canvas until all properties such as the labels and chart type are set.
// This is because chart.js doesn't auto-update anything other than dataset when re-rendering so defaults are used
@@ -150,10 +148,12 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
}
protected updateTheme(e: IColorTheme): void {
let foregroundColor = e.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let options = {
legend: {
labels: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
}
}
};
@@ -189,7 +189,7 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
unmemoize(this, 'colors');
}
@Input() set config(config: IChartConfig) {
public setConfig(config: IChartConfig) {
this.clearMemoize();
this._config = mixin(config, this._defaultConfig, false);
this.legendPosition = this._config.legendPosition;

Some files were not shown because too many files have changed in this diff Show More