diff --git a/extensions/insights-default/package.json b/extensions/insights-default/package.json
index 975a1b4924..1ba5fc4ac0 100644
--- a/extensions/insights-default/package.json
+++ b/extensions/insights-default/package.json
@@ -6,7 +6,7 @@
"vscode": "*"
},
"contributes": {
- "insights": [
+ "dashboard.insights": [
{
"id": "query-data-store-db-insight",
"contrib": {
diff --git a/src/sql/base/browser/ui/modal/webViewDialog.ts b/src/sql/base/browser/ui/modal/webViewDialog.ts
index 31c6e0f11f..862fcf4d4c 100644
--- a/src/sql/base/browser/ui/modal/webViewDialog.ts
+++ b/src/sql/base/browser/ui/modal/webViewDialog.ts
@@ -80,7 +80,7 @@ export class WebViewDialog extends Modal {
}
public set headerTitle(value: string) {
- this._headerTitle = value
+ this._headerTitle = value;
}
public get headerTitle(): string {
diff --git a/src/sql/base/browser/ui/panel/media/panel.css b/src/sql/base/browser/ui/panel/media/panel.css
index d465d6c2c4..5276c38ebe 100644
--- a/src/sql/base/browser/ui/panel/media/panel.css
+++ b/src/sql/base/browser/ui/panel/media/panel.css
@@ -5,7 +5,7 @@
.tabbedPanel {
border-top-color: rgba(128, 128, 128, 0.35);
- border-top-width: 1;
+ border-top-width: 1px;
border-top-style: solid;
box-sizing: border-box;
}
@@ -19,15 +19,14 @@
margin: 0 auto;
padding: 0;
justify-content: flex-start;
- flex-flow: row;
line-height: 35px;
}
-.tabbedPanel .tabList > .tab {
+.tabbedPanel .tabList .tab {
cursor: pointer;
}
-.tabbedPanel .tabList > .tab > .tabLabel {
+.tabbedPanel .tabList .tab .tabLabel {
text-transform: uppercase;
margin-left: 16px;
margin-right: 16px;
@@ -55,4 +54,30 @@
.composite.title ~ tab.fullsize > :first-child {
height: calc(100% - 38px);
+}
+
+.tabbedPanel .title-actions .panel-actions .actions-container {
+ justify-content: flex-start;
+}
+
+.tabbedPanel.vertical {
+ display: flex;
+}
+
+.tabbedPanel.vertical > .title {
+ flex: 0 0 auto;
+ flex-direction: column;
+ height: 100%;
+}
+
+.tabbedPanel.vertical > .tab-content {
+ flex: 1;
+}
+
+.tabbedPanel.vertical > .title > .tabList {
+ flex-flow: column;
+}
+
+.tabbedPanel.horizontal > .title > .tabList {
+ flex-flow: row;
}
\ No newline at end of file
diff --git a/src/sql/base/browser/ui/panel/panel.component.ts b/src/sql/base/browser/ui/panel/panel.component.ts
index 072a1392f5..32bec2aaa5 100644
--- a/src/sql/base/browser/ui/panel/panel.component.ts
+++ b/src/sql/base/browser/ui/panel/panel.component.ts
@@ -3,11 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { Component, ContentChildren, QueryList, AfterContentInit, Inject, forwardRef, NgZone, OnInit, Input } from '@angular/core';
+import { Component, ContentChildren, QueryList, AfterContentInit, Inject, forwardRef, NgZone, OnInit, Input, EventEmitter, Output, ViewChild, ElementRef, OnChanges, OnDestroy, ViewChildren, AfterViewInit } from '@angular/core';
import { TabComponent } from './tab.component';
+import { TabHeaderComponent } from './tabHeader.component';
import './panelStyles';
+import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
+import { Action } from 'vs/base/common/actions';
import * as types from 'vs/base/common/types';
import { mixin } from 'vs/base/common/objects';
@@ -16,40 +19,65 @@ export interface IPanelOptions {
* Whether or not to show the tabs if there is only one tab present
*/
showTabsWhenOne?: boolean;
+ layout?: NavigationBarLayout;
+}
+
+export enum NavigationBarLayout {
+ horizontal = 0,
+ vertical = 1
}
const defaultOptions: IPanelOptions = {
- showTabsWhenOne: true
+ showTabsWhenOne: true,
+ layout: NavigationBarLayout.horizontal
};
+const verticalLayout = 'vertical';
+const horizontalLayout = 'horizontal';
+
+let idPool = 0;
+
@Component({
selector: 'panel',
template: `
-
+
`
})
-export class PanelComponent implements AfterContentInit, OnInit {
+export class PanelComponent implements AfterContentInit, OnInit, OnChanges, OnDestroy, AfterViewInit {
@Input() public options: IPanelOptions;
+ @Input() public actions: Array
;
@ContentChildren(TabComponent) private _tabs: QueryList;
- private _activeTab: TabComponent;
+ @ViewChildren(TabHeaderComponent) private _headerTabs: QueryList;
+ @Output() public onTabChange = new EventEmitter();
+ @Output() public onTabClose = new EventEmitter();
+
+ private _activeTab: TabComponent;
+ private _actionbar: ActionBar;
+ private _mru: TabComponent[];
+
+ @ViewChild('panelActionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
+ @ViewChild('tabbedPanel', { read: ElementRef }) private _tabbedPanelRef: ElementRef;
constructor( @Inject(forwardRef(() => NgZone)) private _zone: NgZone) { }
ngOnInit(): void {
this.options = mixin(this.options || {}, defaultOptions, false);
+ this._mru = [];
}
ngAfterContentInit(): void {
@@ -59,11 +87,38 @@ export class PanelComponent implements AfterContentInit, OnInit {
}
}
+ ngAfterViewInit(): void {
+ if (this.options.layout === NavigationBarLayout.horizontal) {
+ (this._tabbedPanelRef.nativeElement).classList.add(horizontalLayout);
+ } else {
+ (this._tabbedPanelRef.nativeElement).classList.add(verticalLayout);
+ }
+ }
+
+ ngOnChanges(): void {
+ if (this._actionbarRef && !this._actionbar) {
+ this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
+ }
+ if (this.actions && this._actionbar) {
+ this._actionbar.clear();
+ this._actionbar.push(this.actions, { icon: true, label: false });
+ }
+ }
+
+ ngOnDestroy() {
+ if (this._actionbar) {
+ this._actionbar.dispose();
+ }
+ if (this.actions && this.actions.length > 0) {
+ this.actions.forEach((action) => action.dispose());
+ }
+ }
+
/**
* Select a tab based on index (unrecommended)
* @param index index of tab in the html
*/
- selectTab(index: number)
+ selectTab(index: number);
/**
* Select a tab based on the identifier that was passed into the tab
* @param identifier specified identifer of the tab
@@ -85,14 +140,67 @@ export class PanelComponent implements AfterContentInit, OnInit {
tab = this._tabs.find(i => i.identifier === input);
}
+ // since we need to compare identifiers in this next step we are going to go through and make sure all tabs have one
+ this._tabs.forEach(i => {
+ if (!i.identifier) {
+ i.identifier = 'tabIndex_' + idPool++;
+ }
+ });
+
+ if (this._activeTab && tab === this._activeTab) {
+ this.onTabChange.emit(tab);
+ return;
+ }
+
this._zone.run(() => {
if (this._activeTab) {
this._activeTab.active = false;
}
this._activeTab = tab;
+ this.setMostRecentlyUsed(tab);
this._activeTab.active = true;
+
+ // Make the tab header focus on the new selected tab
+ let activeTabHeader = this._headerTabs.find(i => i.tab === this._activeTab);
+ if (activeTabHeader) {
+ activeTabHeader.focusOnTabHeader();
+ }
+
+ this.onTabChange.emit(tab);
});
}
}
+
+ private findAndRemoveTabFromMRU(tab: TabComponent): void {
+ let mruIndex = this._mru.findIndex(i => i === tab);
+
+ if (mruIndex !== -1) {
+ // Remove old index
+ this._mru.splice(mruIndex, 1);
+ }
+ }
+
+ private setMostRecentlyUsed(tab: TabComponent): void {
+ this.findAndRemoveTabFromMRU(tab);
+
+ // Set tab to front
+ this._mru.unshift(tab);
+ }
+
+ /**
+ * Close a tab
+ * @param tab tab to close
+ */
+ closeTab(tab: TabComponent) {
+ this.onTabClose.emit(tab);
+
+ // remove the closed tab from mru
+ this.findAndRemoveTabFromMRU(tab);
+
+ // Open the most recent tab
+ if (this._mru.length > 0) {
+ this.selectTab(this._mru[0]);
+ }
+ }
}
diff --git a/src/sql/base/browser/ui/panel/panel.module.ts b/src/sql/base/browser/ui/panel/panel.module.ts
index 92d15ff75b..164806670b 100644
--- a/src/sql/base/browser/ui/panel/panel.module.ts
+++ b/src/sql/base/browser/ui/panel/panel.module.ts
@@ -6,11 +6,12 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TabComponent } from './tab.component';
+import { TabHeaderComponent } from './tabHeader.component';
import { PanelComponent } from './panel.component';
@NgModule({
imports: [CommonModule],
exports: [TabComponent, PanelComponent],
- declarations: [TabComponent, PanelComponent]
+ declarations: [TabComponent, TabHeaderComponent, PanelComponent]
})
export class PanelModule { }
\ No newline at end of file
diff --git a/src/sql/base/browser/ui/panel/panel.ts b/src/sql/base/browser/ui/panel/panel.ts
index 8fc374f4b1..390a76dc0c 100644
--- a/src/sql/base/browser/ui/panel/panel.ts
+++ b/src/sql/base/browser/ui/panel/panel.ts
@@ -87,21 +87,23 @@ export class TabbedPanel extends Disposable implements IThemable {
}
private _createTab(tab: IInternalPanelTab): void {
+ let tabHeaderElement = $('.tab-header');
+ tabHeaderElement.attr('tabindex', '0');
let tabElement = $('.tab');
- tabElement.attr('tabindex', '0');
+ tabHeaderElement.append(tabElement);
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) => {
+ tabHeaderElement.on(EventType.CLICK, e => this.showTab(tab.identifier));
+ tabHeaderElement.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;
+ this.$tabList.append(tabHeaderElement);
+ tab.header = tabHeaderElement;
tab.label = tabLabel;
}
@@ -112,12 +114,14 @@ export class TabbedPanel extends Disposable implements IThemable {
if (this._shownTab) {
this._tabMap.get(this._shownTab).label.removeClass('active');
+ this._tabMap.get(this._shownTab).header.removeClass('active');
}
this._shownTab = id;
this.$body.clearChildren();
let tab = this._tabMap.get(this._shownTab);
tab.label.addClass('active');
+ tab.header.addClass('active');
tab.view.render(this.$body.getHTMLElement());
this._onTabChange.fire(id);
if (this._currentDimensions) {
diff --git a/src/sql/base/browser/ui/panel/panelStyles.ts b/src/sql/base/browser/ui/panel/panelStyles.ts
index ffdc18b0c6..532ec0f998 100644
--- a/src/sql/base/browser/ui/panel/panelStyles.ts
+++ b/src/sql/base/browser/ui/panel/panelStyles.ts
@@ -15,11 +15,15 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const titleActiveBorder = theme.getColor(PANEL_ACTIVE_TITLE_BORDER);
if (titleActive || titleActiveBorder) {
collector.addRule(`
- .tabbedPanel > .title > .tabList > .tab:hover .tabLabel,
- .tabbedPanel > .title > .tabList > .tab .tabLabel.active {
+ .tabbedPanel > .title > .tabList .tab:hover .tabLabel,
+ .tabbedPanel > .title > .tabList .tab .tabLabel.active {
color: ${titleActive};
border-bottom-color: ${titleActiveBorder};
}
+
+ .tabbedPanel > .title > .tabList .tab-header.active {
+ outline: none;
+ }
`);
}
@@ -27,7 +31,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const titleInactive = theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND);
if (titleInactive) {
collector.addRule(`
- .tabbedPanel > .title > .tabList > .tab .tabLabel {
+ .tabbedPanel > .title > .tabList .tab .tabLabel {
color: ${titleInactive};
}
`);
@@ -37,7 +41,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const focusBorderColor = theme.getColor(focusBorder);
if (focusBorderColor) {
collector.addRule(`
- .tabbedPanel > .title > .tabList > .tab .tabLabel:focus {
+ .tabbedPanel > .title > .tabList .tab .tabLabel:focus {
color: ${titleActive};
border-bottom-color: ${focusBorderColor} !important;
border-bottom: 1px solid;
@@ -49,20 +53,17 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
// Styling with Outline color (e.g. high contrast theme)
const outline = theme.getColor(activeContrastBorder);
if (outline) {
- const outline = theme.getColor(activeContrastBorder);
-
collector.addRule(`
- .tabbedPanel > .title > .tabList > .tab .tabLabel.active,
- .tabbedPanel > .title > .tabList > .tab .tabLabel:hover {
+ .tabbedPanel > .title > .tabList .tab-header.active,
+ .tabbedPanel > .title > .tabList .tab-header:hover {
outline-color: ${outline};
outline-width: 1px;
outline-style: solid;
- border-bottom: none;
padding-bottom: 0;
- outline-offset: 3px;
+ outline-offset: -5px;
}
- .tabbedPanel > .title > .tabList > .tab .tabLabel:hover:not(.active) {
+ .tabbedPanel > .title > .tabList .tab-header:hover:not(.active) {
outline-style: dashed;
}
`);
diff --git a/src/sql/base/browser/ui/panel/tab.component.ts b/src/sql/base/browser/ui/panel/tab.component.ts
index 8b2ecf50c4..2da6c112d2 100644
--- a/src/sql/base/browser/ui/panel/tab.component.ts
+++ b/src/sql/base/browser/ui/panel/tab.component.ts
@@ -2,7 +2,9 @@
* 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, ContentChild } from '@angular/core';
+import { Component, Input, ContentChild, OnDestroy } from '@angular/core';
+
+import { Action } from 'vs/base/common/actions';
export abstract class TabChild {
public abstract layout(): void;
@@ -16,9 +18,11 @@ export abstract class TabChild {
`
})
-export class TabComponent {
+export class TabComponent implements OnDestroy {
@ContentChild(TabChild) private _child: TabChild;
@Input() public title: string;
+ @Input() public canClose: boolean;
+ @Input() public actions: Array
;
public _active = false;
@Input() public identifier: string;
@@ -32,4 +36,11 @@ export class TabComponent {
public get active(): boolean {
return this._active;
}
+
+ ngOnDestroy() {
+ if (this.actions && this.actions.length > 0) {
+ this.actions.forEach((action) => action.dispose());
+ }
+ }
+
}
diff --git a/src/sql/base/browser/ui/panel/tabActions.ts b/src/sql/base/browser/ui/panel/tabActions.ts
new file mode 100644
index 0000000000..6b461290fa
--- /dev/null
+++ b/src/sql/base/browser/ui/panel/tabActions.ts
@@ -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.
+ *--------------------------------------------------------------------------------------------*/
+import { Action } from 'vs/base/common/actions';
+import * as nls from 'vs/nls';
+import { TPromise } from 'vs/base/common/winjs.base';
+
+export class CloseTabAction extends Action {
+ private static readonly ID = 'closeTab';
+ private static readonly LABEL = nls.localize('closeTab', "Close");
+ private static readonly ICON = 'close';
+
+ constructor(
+ private closeFn: () => void,
+ private context: any // this
+ ) {
+ super(CloseTabAction.ID, CloseTabAction.LABEL, CloseTabAction.ICON);
+ }
+
+ run(): TPromise {
+ try {
+ this.closeFn.apply(this.context);
+ return TPromise.as(true);
+ } catch (e) {
+ return TPromise.as(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/sql/base/browser/ui/panel/tabHeader.component.ts b/src/sql/base/browser/ui/panel/tabHeader.component.ts
new file mode 100644
index 0000000000..cbb4442095
--- /dev/null
+++ b/src/sql/base/browser/ui/panel/tabHeader.component.ts
@@ -0,0 +1,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/media/icons/common-icons';
+import 'vs/css!./tabHeader';
+
+import { Component, AfterContentInit, OnDestroy, Input, Output, ElementRef, ViewChild, EventEmitter } from '@angular/core';
+
+import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
+import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
+import { KeyCode } from 'vs/base/common/keyCodes';
+import * as DOM from 'vs/base/browser/dom';
+import { Disposable } from 'vs/base/common/lifecycle';
+
+import { TabComponent } from './tab.component';
+import { CloseTabAction } from './tabActions';
+
+@Component({
+ selector: 'tab-header',
+ template: `
+
+ `
+})
+export class TabHeaderComponent extends Disposable implements AfterContentInit, OnDestroy {
+ @Input() public tab: TabComponent;
+ @Output() public onSelectTab: EventEmitter = new EventEmitter();
+ @Output() public onCloseTab: EventEmitter = new EventEmitter();
+
+ private _actionbar: ActionBar;
+
+ @ViewChild('actionHeader', { read: ElementRef }) private _actionHeaderRef: ElementRef;
+ @ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
+ constructor() {
+ super();
+ }
+
+ ngAfterContentInit(): void {
+ this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
+ if (this.tab.actions) {
+ this._actionbar.push(this.tab.actions, { icon: true, label: false });
+ }
+ if (this.tab.canClose) {
+ let closeAction = this._register(new CloseTabAction(this.closeTab, this));
+ this._actionbar.push(closeAction, { icon: true, label: false });
+ }
+ }
+
+ ngOnDestroy() {
+ if (this._actionbar) {
+ this._actionbar.dispose();
+ }
+ this.dispose();
+ }
+
+ selectTab(tab: TabComponent) {
+ this.onSelectTab.emit(tab);
+ }
+
+ closeTab() {
+ this.onCloseTab.emit(this.tab);
+ }
+
+ focusOnTabHeader() {
+ let header = this._actionHeaderRef.nativeElement;
+ header.focus();
+ }
+
+ onKey(e: Event) {
+ if (DOM.isAncestor(e.target, this._actionHeaderRef.nativeElement) && e instanceof KeyboardEvent) {
+ let event = new StandardKeyboardEvent(e);
+ if (event.equals(KeyCode.Enter)) {
+ this.onSelectTab.emit(this.tab);
+ e.stopPropagation();
+ }
+ }
+ }
+}
diff --git a/src/sql/base/browser/ui/panel/tabHeader.css b/src/sql/base/browser/ui/panel/tabHeader.css
new file mode 100644
index 0000000000..bbf06dda0b
--- /dev/null
+++ b/src/sql/base/browser/ui/panel/tabHeader.css
@@ -0,0 +1,20 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+tab-header .action-item .action-label {
+ opacity: 0;
+ padding: 8px;
+ margin-top: 0.3em;
+}
+
+tab-header .action-item {
+ line-height: 1.4em;
+}
+
+tab-header .tab-header.active .action-label, /* always show it for active tab */
+tab-header .tab-header:hover .action-label, /* always show it on hover */
+tab-header .tab-header:focus .action-label { /* always show it on focus */
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/src/sql/common/telemetryKeys.ts b/src/sql/common/telemetryKeys.ts
index f51b350af5..bae25446f0 100644
--- a/src/sql/common/telemetryKeys.ts
+++ b/src/sql/common/telemetryKeys.ts
@@ -40,3 +40,4 @@ export const ServerGroups = 'ServerGroups';
export const Accounts = 'Accounts';
export const FireWallRule = 'FirewallRule';
export const AutoOAuth = 'AutoOAuth';
+export const AddNewDashboardTab = 'AddNewDashboardTab';
diff --git a/src/sql/data.d.ts b/src/sql/data.d.ts
index 06f6f1d8e3..e9e7fba7ff 100644
--- a/src/sql/data.d.ts
+++ b/src/sql/data.d.ts
@@ -1441,6 +1441,48 @@ declare module 'data' {
postMessage(message: any): Thenable;
}
+ export interface DashboardWebview {
+
+ /**
+ * Raised when the webview posts a message.
+ */
+ readonly onMessage: vscode.Event;
+
+ /**
+ * Raised when the webview closed.
+ */
+ readonly onClosed: vscode.Event;
+
+ /**
+ * Post a message to the webview.
+ *
+ * @param message Body of the message.
+ */
+ postMessage(message: any): Thenable;
+
+ /**
+ * The connection info for the dashboard the webview exists on
+ */
+ readonly connection: connection.Connection;
+
+ /**
+ * The info on the server for the webview dashboard
+ */
+ readonly serverInfo: ServerInfo;
+
+ /**
+ * Contents of the dialog body.
+ */
+ html: string;
+ }
+
+ export namespace dashboard {
+ /**
+ * Register a provider for a webview widget
+ */
+ export function registerWebviewProvider(widgetId: string, handler: (webview: DashboardWebview) => void): void;
+ }
+
export namespace window {
/**
* creates a dialog
diff --git a/src/sql/media/icons/common-icons.css b/src/sql/media/icons/common-icons.css
index aeae65e478..6dae2faa12 100644
--- a/src/sql/media/icons/common-icons.css
+++ b/src/sql/media/icons/common-icons.css
@@ -155,6 +155,15 @@
background: url('ellipsis.svg') center center no-repeat;
}
+.hc-black .icon.new,
+.vs-dark .icon.new {
+ background: url('new_inverse.svg') center center no-repeat;
+}
+
+.vs .icon.new {
+ background: url('new.svg') center center no-repeat;
+}
+
.hc-black .icon.new-query,
.vs-dark .icon.new-query {
background: url('newquery_inverse.svg') center center no-repeat;
@@ -181,3 +190,21 @@
.vs .icon.edit {
background: url('edit.svg') center center no-repeat;
}
+
+.hc-black .icon.pin,
+.vs-dark .icon.pin {
+ background: url('pin_inverse.svg') center center no-repeat;
+}
+
+.vs .icon.pin {
+ background: url('pin.svg') center center no-repeat;
+}
+
+.hc-black .icon.unpin,
+.vs-dark .icon.unpin {
+ background: url('unpin_inverse.svg') center center no-repeat;
+}
+
+.vs .icon.unpin {
+ background: url('unpin.svg') center center no-repeat;
+}
\ No newline at end of file
diff --git a/src/sql/media/icons/new.svg b/src/sql/media/icons/new.svg
new file mode 100644
index 0000000000..f76813b363
--- /dev/null
+++ b/src/sql/media/icons/new.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/media/icons/new_inverse.svg b/src/sql/media/icons/new_inverse.svg
new file mode 100644
index 0000000000..787edf63bd
--- /dev/null
+++ b/src/sql/media/icons/new_inverse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/media/icons/pin.svg b/src/sql/media/icons/pin.svg
new file mode 100644
index 0000000000..8f28228fa0
--- /dev/null
+++ b/src/sql/media/icons/pin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/media/icons/pin_inverse.svg b/src/sql/media/icons/pin_inverse.svg
new file mode 100644
index 0000000000..932c9cd0a7
--- /dev/null
+++ b/src/sql/media/icons/pin_inverse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/media/icons/unpin.svg b/src/sql/media/icons/unpin.svg
new file mode 100644
index 0000000000..68d31cb4e1
--- /dev/null
+++ b/src/sql/media/icons/unpin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/media/icons/unpin_inverse.svg b/src/sql/media/icons/unpin_inverse.svg
new file mode 100644
index 0000000000..84049b27fd
--- /dev/null
+++ b/src/sql/media/icons/unpin_inverse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/common/actions.ts b/src/sql/parts/dashboard/common/actions.ts
index 755040d3a0..6f0d368550 100644
--- a/src/sql/parts/dashboard/common/actions.ts
+++ b/src/sql/parts/dashboard/common/actions.ts
@@ -7,8 +7,12 @@ 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';
+import { IDisposable } from 'vs/base/common/lifecycle';
+import { IAngularEventingService, AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
+import { INewDashboardTabDialogService } from 'sql/parts/dashboard/newDashboardTabDialog/interface';
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
export class EditDashboardAction extends Action {
private static readonly ID = 'editDashboard';
@@ -111,3 +115,85 @@ export class DeleteWidgetAction extends Action {
return TPromise.as(true);
}
}
+
+export class PinUnpinTabAction extends Action {
+ private static readonly ID = 'pinTab';
+ private static readonly PINLABEL = nls.localize('clickToUnpin', "Click to unpin");
+ private static readonly UNPINLABEL = nls.localize('clickToPin', "Click to pin");
+ private static readonly PINICON = 'pin';
+ private static readonly UNPINICON = 'unpin';
+
+ constructor(
+ private _tabId: string,
+ private _uri: string,
+ private _isPinned: boolean,
+ @IAngularEventingService private angularEventService: IAngularEventingService
+ ) {
+ super(PinUnpinTabAction.ID, PinUnpinTabAction.PINLABEL, PinUnpinTabAction.PINICON);
+ this.updatePinStatus();
+ }
+
+ private updatePinStatus() {
+ if (this._isPinned) {
+ this.label = PinUnpinTabAction.PINLABEL;
+ this.class = PinUnpinTabAction.PINICON;
+ } else {
+ this.label = PinUnpinTabAction.UNPINLABEL;
+ this.class = PinUnpinTabAction.UNPINICON;
+ }
+ }
+
+ public run(): TPromise {
+ this._isPinned = !this._isPinned;
+ this.updatePinStatus();
+ this.angularEventService.sendAngularEvent(this._uri, AngularEventType.PINUNPIN_TAB, { tabId: this._tabId, isPinned: this._isPinned });
+ return TPromise.as(true);
+ }
+}
+
+export class AddFeatureTabAction extends Action {
+ private static readonly ID = 'openInstalledFeatures';
+ private static readonly LABEL = nls.localize('openInstalledFeatures', "Open installed features");
+ private static readonly ICON = 'new';
+
+ private _disposables: IDisposable[] = [];
+
+ constructor(
+ private _dashboardTabs: Array,
+ private _openedTabs: Array,
+ private _uri: string,
+ @INewDashboardTabDialogService private _newDashboardTabService: INewDashboardTabDialogService,
+ @IAngularEventingService private _angularEventService: IAngularEventingService
+ ) {
+ super(AddFeatureTabAction.ID, AddFeatureTabAction.LABEL, AddFeatureTabAction.ICON);
+ this._disposables.push(toDisposableSubscription(this._angularEventService.onAngularEvent(this._uri, (event) => this.handleDashboardEvent(event))));
+ }
+
+ run(): TPromise {
+ this._newDashboardTabService.showDialog(this._dashboardTabs, this._openedTabs, this._uri);
+ return TPromise.as(true);
+ }
+
+ dispose() {
+ super.dispose();
+ this._disposables.forEach((item) => item.dispose());
+ }
+
+ private handleDashboardEvent(event: IAngularEvent): void {
+ switch (event.event) {
+ case AngularEventType.NEW_TABS:
+ let openedTabs = event.payload.dashboardTabs;
+ openedTabs.forEach(tab => {
+ let existedTab = this._openedTabs.find(i => i === tab);
+ if (!existedTab) {
+ this._openedTabs.push(tab);
+ }
+ });
+ break;
+ case AngularEventType.CLOSE_TAB:
+ let index = this._openedTabs.findIndex(i => i.id === event.payload.id);
+ this._openedTabs.splice(index, 1);
+ break;
+ }
+ }
+}
diff --git a/src/sql/parts/dashboard/common/dashboardPage.component.html b/src/sql/parts/dashboard/common/dashboardPage.component.html
index 6fd1c036d5..0845ee98e1 100644
--- a/src/sql/parts/dashboard/common/dashboardPage.component.html
+++ b/src/sql/parts/dashboard/common/dashboardPage.component.html
@@ -5,14 +5,18 @@
*--------------------------------------------------------------------------------------------*/
-->
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/sql/parts/dashboard/common/dashboardPage.component.ts b/src/sql/parts/dashboard/common/dashboardPage.component.ts
index 52cd6f5fab..4e174acaa2 100644
--- a/src/sql/parts/dashboard/common/dashboardPage.component.ts
+++ b/src/sql/parts/dashboard/common/dashboardPage.component.ts
@@ -4,17 +4,27 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dashboardPage';
+import './dashboardPanelStyles';
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 { WidgetConfig, TabConfig, PinConfig } 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 { PanelComponent } from 'sql/base/browser/ui/panel/panel.component';
+import { subscriptionToDisposable } from 'sql/base/common/lifecycle';
+import { IDashboardRegistry, Extensions as DashboardExtensions, IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+import { PinUnpinTabAction, AddFeatureTabAction } from './actions';
+import { TabComponent } from 'sql/base/browser/ui/panel/tab.component';
+import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
+import { AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
+import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
+import { error } from 'sql/base/common/log';
+import { WIDGETS_TABS } from 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
+import { WEBVIEW_TABS } from 'sql/parts/dashboard/tabs/dashboardWebviewTab.contribution';
import { Registry } from 'vs/platform/registry/common/platform';
import * as types from 'vs/base/common/types';
@@ -28,9 +38,13 @@ 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 { generateUuid } from 'vs/base/common/uuid';
-import * as objects from 'sql/base/common/objects';
+import * as objects from 'vs/base/common/objects';
+import Event, { Emitter } from 'vs/base/common/event';
+import { Action } from 'vs/base/common/actions';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
+const dashboardRegistry = Registry.as
(DashboardExtensions.DashboardContributions);
+
/**
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
*/
@@ -38,51 +52,6 @@ function isNumberArray(value: any): value is number[] {
return types.isArray(value) && (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'))
@@ -90,43 +59,31 @@ function configSorter(a, b): number {
export abstract class DashboardPage extends Disposable implements OnDestroy {
protected SKELETON_WIDTH = 5;
- protected widgets: Array = [];
- protected gridConfig: NgGridConfig = {
- 'margins': [10], // The size of the margins of each item. Supports up to four values in the same way as CSS margins. Can be updated using setMargins()
- 'draggable': false, // Whether the items can be dragged. Can be updated using enableDrag()/disableDrag()
- 'resizable': false, // Whether the items can be resized. Can be updated using enableResize()/disableResize()
- 'max_cols': this.SKELETON_WIDTH, // The maximum number of columns allowed. Set to 0 for infinite. Cannot be used with max_rows
- 'max_rows': 0, // The maximum number of rows allowed. Set to 0 for infinite. Cannot be used with max_cols
- 'visible_cols': 0, // The number of columns shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_cols
- 'visible_rows': 0, // The number of rows shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_rows
- 'min_cols': 0, // The minimum number of columns allowed. Can be any number greater than or equal to 1.
- 'min_rows': 0, // The minimum number of rows allowed. Can be any number greater than or equal to 1.
- 'col_width': 250, // The width of each column
- 'row_height': 250, // The height of each row
- 'cascade': 'left', // The direction to cascade grid items ('up', 'right', 'down', 'left')
- 'min_width': 100, // The minimum width of an item. If greater than col_width, this will update the value of min_cols
- 'min_height': 100, // The minimum height of an item. If greater than row_height, this will update the value of min_rows
- 'fix_to_grid': false, // Fix all item movements to the grid
- 'auto_style': true, // Automatically add required element styles at run-time
- 'auto_resize': false, // Automatically set col_width/row_height so that max_cols/max_rows fills the screen. Only has effect is max_cols or max_rows is set
- 'maintain_ratio': false, // Attempts to maintain aspect ratio based on the colWidth/rowHeight values set in the config
- '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.
- };
+ protected tabs: Array = [];
+
private _originalConfig: WidgetConfig[];
- private _editDispose: Array = [];
private _scrollableElement: ScrollableElement;
private _widgetConfigLocation: string;
private _propertiesConfigLocation: string;
+ protected panelActions: Action[];
+ private _tabsDispose: Array = [];
+ private _pinnedTabs: Array = [];
+
@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;
- @ViewChildren(NgGridItem) private _items: QueryList;
+ @ViewChildren(DashboardTab) private _tabs: QueryList;
+ @ViewChild(PanelComponent) private _panel: PanelComponent;
+
+ private _editEnabled = new Emitter();
+ public readonly editEnabled: Event = this._editEnabled.event;
+
+
+ // tslint:disable:no-unused-variable
+ private readonly homeTabTitle: string = nls.localize('home', 'Home');
// a set of config modifiers
private readonly _configModifiers: Array<(item: Array) => Array> = [
@@ -135,7 +92,7 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
this.addProvider,
this.addEdition,
this.addContext,
- this.filterWidgets
+ this.filterConfigs
];
private readonly _gridModifiers: Array<(item: Array) => Array> = [
@@ -144,6 +101,7 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface,
+ @Inject(BOOTSTRAP_SERVICE_ID) protected bootstrapService: IBootstrapService,
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
@@ -156,7 +114,7 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
} else {
let tempWidgets = this.dashboardService.getSettings>([this.context, 'widgets'].join('.'));
this._widgetConfigLocation = 'default';
- this._originalConfig = objects.clone(tempWidgets);
+ this._originalConfig = objects.deepClone(tempWidgets);
let properties = this.getProperties();
this._configModifiers.forEach((cb) => {
tempWidgets = cb.apply(this, [tempWidgets]);
@@ -165,8 +123,10 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
this._gridModifiers.forEach(cb => {
tempWidgets = cb.apply(this, [tempWidgets]);
});
- this.widgets = tempWidgets;
this.propertiesWidget = properties ? properties[0] : undefined;
+
+ this.createTabs(tempWidgets);
+
}
}
@@ -189,15 +149,18 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
container.appendChild(this._scrollableElement.getDomNode());
let initalHeight = getContentHeight(scrollable);
this._scrollableElement.setScrollDimensions({
- scrollHeight: getContentHeight(scrollable),
+ scrollHeight: Math.max(getContentHeight(scrollable), getContentHeight(container)),
height: getContentHeight(container)
});
this._register(addDisposableListener(window, EventType.RESIZE, () => {
- this._scrollableElement.setScrollDimensions({
- scrollHeight: getContentHeight(scrollable),
- height: getContentHeight(container)
- });
+ // Todo: Need to set timeout because we have to make sure that the grids have already rearraged before the getContentHeight gets called.
+ setTimeout(() => {
+ this._scrollableElement.setScrollDimensions({
+ scrollHeight: Math.max(getContentHeight(scrollable), getContentHeight(container)),
+ height: getContentHeight(container)
+ });
+ }, 100);
}));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
@@ -205,13 +168,147 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
let currentheight = getContentHeight(scrollable);
if (initalHeight !== currentheight) {
this._scrollableElement.setScrollDimensions({
- scrollHeight: getContentHeight(scrollable),
+ scrollHeight: Math.max(getContentHeight(scrollable), getContentHeight(container)),
height: getContentHeight(container)
});
}
}, 100);
}
+ private createTabs(homeWidgets: WidgetConfig[]) {
+ // Clear all tabs
+ this.tabs = [];
+ this._pinnedTabs = [];
+ this._tabsDispose.forEach(i => i.dispose());
+ this._tabsDispose = [];
+
+ // Create home tab
+ let homeTab: TabConfig = {
+ id: 'homeTab',
+ publisher: undefined,
+ title: this.homeTabTitle,
+ content: { 'widgets-tab': homeWidgets },
+ context: this.context,
+ originalConfig: this._originalConfig,
+ editable: true,
+ canClose: false,
+ actions: []
+ };
+ this.addNewTab(homeTab);
+ this._panel.selectTab(homeTab.id);
+
+ let allTabs = this.filterConfigs(dashboardRegistry.tabs);
+
+ // Load always show tabs
+ let alwaysShowTabs = allTabs.filter(tab => tab.alwaysShow);
+ this.loadNewTabs(alwaysShowTabs);
+
+ // Load pinned tabs
+ this._pinnedTabs = this.dashboardService.getSettings>([this.context, 'tabs'].join('.'));
+ let pinnedDashboardTabs: IDashboardTab[] = [];
+ this._pinnedTabs.forEach(pinnedTab => {
+ let tab = allTabs.find(i => i.id === pinnedTab.tabId);
+ if (tab) {
+ pinnedDashboardTabs.push(tab);
+ }
+ });
+ this.loadNewTabs(pinnedDashboardTabs);
+
+ // Set panel actions
+ let openedTabs = [...pinnedDashboardTabs, ...alwaysShowTabs];
+ let addNewTabAction = this.dashboardService.instantiationService.createInstance(AddFeatureTabAction, allTabs, openedTabs, this.dashboardService.getUnderlyingUri());
+ this._tabsDispose.push(addNewTabAction);
+ this.panelActions = [addNewTabAction];
+ this._cd.detectChanges();
+
+ this._tabsDispose.push(this.dashboardService.onPinUnpinTab(e => {
+ if (e.isPinned) {
+ this._pinnedTabs.push(e);
+ } else {
+ let index = this._pinnedTabs.findIndex(i => i.tabId === e.tabId);
+ this._pinnedTabs.splice(index, 1);
+ }
+ this.rewriteConfig();
+ }));
+
+ this._tabsDispose.push(this.dashboardService.onAddNewTabs(e => {
+ this.loadNewTabs(e);
+ }));
+ }
+
+ private rewriteConfig(): void {
+ let writeableConfig = objects.deepClone(this._pinnedTabs);
+
+ writeableConfig.forEach(i => {
+ delete i.isPinned;
+ });
+ let target: ConfigurationTarget = ConfigurationTarget.USER;
+ this.dashboardService.writeSettings([this.context, 'tabs'].join('.'), writeableConfig, target);
+ }
+
+ private loadNewTabs(dashboardTabs: IDashboardTab[]) {
+ if (dashboardTabs && dashboardTabs.length > 0) {
+ let selectedTabs = dashboardTabs.map(v => {
+
+ if (Object.keys(v.content).length !== 1) {
+ error('Exactly 1 widget must be defined per space');
+ }
+
+ let key = Object.keys(v.content)[0];
+ if (key === WIDGETS_TABS) {
+ let configs = Object.values(v.content)[0];
+ this._configModifiers.forEach(cb => {
+ configs = cb.apply(this, [configs]);
+ });
+ this._gridModifiers.forEach(cb => {
+ configs = cb.apply(this, [configs]);
+ });
+ return { id: v.id, title: v.title, content: { 'widgets-tab': configs }, alwaysShow: v.alwaysShow };
+ }
+ return v;
+ }).map(v => {
+ let actions = [];
+ if (!v.alwaysShow) {
+ let pinnedTab = this._pinnedTabs.find(i => i.tabId === v.id);
+ actions.push(this.dashboardService.instantiationService.createInstance(PinUnpinTabAction, v.id, this.dashboardService.getUnderlyingUri(), !!pinnedTab));
+ }
+
+ let config = v as TabConfig;
+ config.context = this.context;
+ config.editable = false;
+ config.canClose = true;
+ config.actions = actions;
+ this.addNewTab(config);
+ return config;
+ });
+
+ // put this immediately on the stack so that is ran *after* the tab is rendered
+ setTimeout(() => {
+ this._panel.selectTab(selectedTabs.pop().id);
+ });
+ }
+ }
+
+
+ private getContentType(tab: TabConfig): string {
+ return tab.content ? Object.keys(tab.content)[0] : '';
+ }
+
+ private addNewTab(tab: TabConfig): void {
+ let existedTab = this.tabs.find(i => i.id === tab.id);
+ if (!existedTab) {
+ this.tabs.push(tab);
+ this._cd.detectChanges();
+ let tabComponents = this._tabs.find(i => i.id === tab.id);
+ this._register(tabComponents.onResize(() => {
+ this._scrollableElement.setScrollDimensions({
+ scrollHeight: Math.max(getContentHeight(this._scrollable.nativeElement), getContentHeight(this._scrollContainer.nativeElement)),
+ height: getContentHeight(this._scrollContainer.nativeElement)
+ });
+ }));
+ }
+ }
+
private updateTheme(theme: IColorTheme): void {
let el = this._propertiesContainer.nativeElement as HTMLElement;
let border = theme.getColor(colors.contrastBorder, true);
@@ -240,14 +337,18 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
* Returns a filtered version of the widgets passed based on edition and provider
* @param config widgets to filter
*/
- private filterWidgets(config: WidgetConfig[]): Array {
+ private filterConfigs(config: T[]): Array {
let connectionInfo: ConnectionManagementInfo = this.dashboardService.connectionManagementService.connectionInfo;
let edition = connectionInfo.serverInfo.engineEditionId;
let provider = connectionInfo.providerId;
// filter by provider
return config.filter((item) => {
- return this.stringCompare(item.provider, provider);
+ if (item.provider) {
+ return this.stringCompare(item.provider, provider);
+ } else {
+ return true;
+ }
}).filter((item) => {
if (item.edition) {
if (edition) {
@@ -407,96 +508,42 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
public refresh(refreshConfig: boolean = false): void {
if (refreshConfig) {
this.init();
- if (this._properties) {
- this._properties.refresh();
- }
+ this.refreshProperties();
} else {
- if (this._widgets) {
- this._widgets.forEach(item => {
- item.refresh();
+ this.refreshProperties();
+ if (this._tabs) {
+ this._tabs.forEach(tabContent => {
+ tabContent.refresh();
});
}
}
}
+ private refreshProperties(): void {
+ if (this._properties) {
+ this._properties.refresh();
+ }
+ }
+
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();
- }
+ if (this._tabs) {
+ this._tabs.forEach(tabContent => {
+ tabContent.enableEdit();
});
}
}
- private _rewriteConfig(): void {
- let writeableConfig = objects.clone(this._originalConfig);
+ public handleTabChange(tab: TabComponent): void {
+ let localtab = this._tabs.find(i => i.id === tab.identifier);
+ this._editEnabled.fire(localtab.editable);
+ this._cd.detectChanges();
+ localtab.layout();
+ }
- writeableConfig.forEach(i => {
- delete i.id;
- });
- let target: ConfigurationTarget = ConfigurationTarget.USER;
- this.dashboardService.writeSettings(this.context, writeableConfig, target);
+ public handleTabClose(tab: TabComponent): void {
+ let index = this.tabs.findIndex(i => i.id === tab.identifier);
+ this.tabs.splice(index, 1);
+ this._cd.detectChanges();
+ this.bootstrapService.angularEventingService.sendAngularEvent(this.dashboardService.getUnderlyingUri(), AngularEventType.CLOSE_TAB, { id: tab.identifier });
}
}
diff --git a/src/sql/parts/dashboard/common/dashboardPage.css b/src/sql/parts/dashboard/common/dashboardPage.css
index 703fe490b8..bd5f4a72a0 100644
--- a/src/sql/parts/dashboard/common/dashboardPage.css
+++ b/src/sql/parts/dashboard/common/dashboardPage.css
@@ -8,3 +8,8 @@ dashboard-page {
width: 100%;
position: absolute;
}
+
+dashboard-page .monaco-scrollable-element {
+ height: 100%;
+ width: 100%;
+}
diff --git a/src/sql/parts/dashboard/common/dashboardPanel.css b/src/sql/parts/dashboard/common/dashboardPanel.css
new file mode 100644
index 0000000000..de814cc76f
--- /dev/null
+++ b/src/sql/parts/dashboard/common/dashboardPanel.css
@@ -0,0 +1,19 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+panel.dashboard-panel > .tabbedPanel {
+ border-top-width: 0px;
+ border-top-style: solid;
+ box-sizing: border-box;
+}
+
+panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header .tab > .tabLabel.active {
+ border-bottom: 0px solid;
+}
+
+panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header {
+ box-sizing: border-box;
+ border: 1px solid transparent;
+}
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/common/dashboardPanelStyles.ts b/src/sql/parts/dashboard/common/dashboardPanelStyles.ts
new file mode 100644
index 0000000000..012ef32e90
--- /dev/null
+++ b/src/sql/parts/dashboard/common/dashboardPanelStyles.ts
@@ -0,0 +1,95 @@
+/*---------------------------------------------------------------------------------------------
+ * 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!./dashboardPanel';
+
+import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
+import { TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_ACTIVE_BORDER, TAB_INACTIVE_BACKGROUND, TAB_INACTIVE_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, TAB_BORDER } from 'vs/workbench/common/theme';
+import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
+
+registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
+
+ // Title Active
+ const tabActiveBackground = theme.getColor(TAB_ACTIVE_BACKGROUND);
+ const tabActiveForeground = theme.getColor(TAB_ACTIVE_FOREGROUND);
+ if (tabActiveBackground || tabActiveForeground) {
+ collector.addRule(`
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab:hover .tabLabel,
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab .tabLabel.active {
+ color: ${tabActiveForeground};
+ border-bottom: 0px solid;
+ }
+
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header.active {
+ background-color: ${tabActiveBackground};
+ }
+ `);
+ }
+
+ const activeTabBorderColor = theme.getColor(TAB_ACTIVE_BORDER);
+ if (activeTabBorderColor) {
+ collector.addRule(`
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header.active {
+ box-shadow: ${activeTabBorderColor} 0 -1px inset;
+ }
+ `);
+ }
+
+ // Title Inactive
+ const tabInactiveBackground = theme.getColor(TAB_INACTIVE_BACKGROUND);
+ const tabInactiveForeground = theme.getColor(TAB_INACTIVE_FOREGROUND);
+ if (tabInactiveBackground || tabInactiveForeground) {
+ collector.addRule(`
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab .tabLabel {
+ color: ${tabInactiveForeground};
+ }
+
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header {
+ background-color: ${tabInactiveBackground};
+ }
+ `);
+ }
+
+ // Panel title background
+ const panelTitleBackground = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND);
+ if (panelTitleBackground) {
+ collector.addRule(`
+ panel.dashboard-panel > .tabbedPanel > .title {
+ background-color: ${panelTitleBackground};
+ }
+ `);
+ }
+
+ // Panel title background
+ const tabBoarder = theme.getColor(TAB_BORDER);
+ if (tabBoarder) {
+ collector.addRule(`
+ panel.dashboard-panel > .tabbedPanel.horizontal > .title > .tabList .tab-header {
+ border-right-color: ${tabBoarder};
+ }
+
+ panel.dashboard-panel > .tabbedPanel.vertical > .title > .tabList .tab-header {
+ border-bottom-color: ${tabBoarder};
+ }
+ `);
+ }
+
+ // Styling with Outline color (e.g. high contrast theme)
+ const outline = theme.getColor(activeContrastBorder);
+ if (outline) {
+ collector.addRule(`
+ panel.dashboard-panel > .tabbedPanel > .title {
+ border-bottom-color: ${tabBoarder};
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ }
+
+ panel.dashboard-panel > .tabbedPanel.vertical > .title {
+ border-right-color: ${tabBoarder};
+ border-right-width: 1px;
+ border-right-style: solid;
+ }
+ `);
+ }
+});
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/common/dashboardTab.contribution.ts b/src/sql/parts/dashboard/common/dashboardTab.contribution.ts
new file mode 100644
index 0000000000..0368dc5b76
--- /dev/null
+++ b/src/sql/parts/dashboard/common/dashboardTab.contribution.ts
@@ -0,0 +1,116 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { IExtensionPointUser, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
+import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import { localize } from 'vs/nls';
+
+import { registerTab, generateTabContentSchemaProperties } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+export interface IDashboardTabContrib {
+ id: string;
+ title: string;
+ content: object;
+ description?: string;
+ provider?: string | string[];
+ edition?: number | number[];
+ alwaysShow?: boolean;
+}
+
+const tabSchema: IJSONSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ description: localize('sqlops.extension.contributes.dashboard.tab.id', "Unique identifier for this tab. Will be passed to the extension for any requests.")
+ },
+ title: {
+ type: 'string',
+ description: localize('sqlops.extension.contributes.dashboard.tab.title', "Title of the tab to show the user.")
+ },
+ description: {
+ description: localize('sqlops.extension.contributes.dashboard.tab.description', "Description of this tab that will be shown to the user."),
+ type: 'string'
+ },
+ provider: {
+ description: localize('sqlops.extension.contributes.dashboard.tab.provider', "Providers for which this tab should be allowed for."),
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ ]
+ },
+ edition: {
+ description: localize('sqlops.extension.contributes.dashboard.tab.edition', "Editions for which this tab should be allowed for."),
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'array',
+ items: {
+ type: 'number'
+ }
+ }
+ ]
+ },
+ content: {
+ description: localize('sqlops.extension.contributes.dashboard.tab.content', "The content that will be displayed in this tab."),
+ type: 'object',
+ properties: generateTabContentSchemaProperties()
+ },
+ alwaysShow: {
+ description: localize('sqlops.extension.contributes.dashboard.tab.alwaysShow', "Whether or not this tab should always be shown or only when the user adds it."),
+ type: 'boolean'
+ }
+ }
+};
+
+const tabContributionSchema: IJSONSchema = {
+ description: localize('sqlops.extension.contributes.tabs', "Contributes a single or multiple tabs for users to add to their dashboard."),
+ oneOf: [
+ tabSchema,
+ {
+ type: 'array',
+ items: tabSchema
+ }
+ ]
+};
+
+ExtensionsRegistry.registerExtensionPoint('dashboard.tabs', [], tabContributionSchema).setHandler(extensions => {
+
+ function handleCommand(tab: IDashboardTabContrib, extension: IExtensionPointUser) {
+ let { description, content, title, edition, provider, id, alwaysShow } = tab;
+ alwaysShow = alwaysShow || false;
+ let publisher = extension.description.publisher;
+ if (!title) {
+ extension.collector.error('No title specified for extension.');
+ return;
+ }
+ if (!description) {
+ extension.collector.warn('No description specified to show.');
+ }
+ if (!content) {
+ extension.collector.warn('No content specified to show.');
+ }
+ registerTab({ description, title, content, edition, provider, id, alwaysShow, publisher });
+ }
+
+ for (let extension of extensions) {
+ const { value } = extension;
+ if (Array.isArray(value)) {
+ for (let command of value) {
+ handleCommand(command, extension);
+ }
+ } else {
+ handleCommand(value, extension);
+ }
+ }
+});
diff --git a/src/sql/parts/dashboard/common/dashboardWidget.ts b/src/sql/parts/dashboard/common/dashboardWidget.ts
index 574c898fdb..457f1c2bb0 100644
--- a/src/sql/parts/dashboard/common/dashboardWidget.ts
+++ b/src/sql/parts/dashboard/common/dashboardWidget.ts
@@ -6,6 +6,7 @@ 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';
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
export interface IDashboardWidget {
actions: Array;
@@ -32,6 +33,19 @@ export interface WidgetConfig {
padding?: string;
}
+export interface TabConfig extends IDashboardTab {
+ context: string;
+ originalConfig: Array;
+ editable: boolean;
+ canClose: boolean;
+ actions?: Array;
+}
+
+export interface PinConfig {
+ tabId: string;
+ isPinned?: boolean;
+}
+
export abstract class DashboardWidget extends Disposable implements OnDestroy {
protected _config: WidgetConfig;
diff --git a/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.html b/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.html
index 1680dcb7d9..26a6340105 100644
--- a/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.html
+++ b/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.html
@@ -7,7 +7,7 @@
-
+
{{_config.name}}
diff --git a/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.ts b/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.ts
index 748e7b228a..aee2efd7ed 100644
--- a/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.ts
+++ b/src/sql/parts/dashboard/common/dashboardWidgetWrapper.component.ts
@@ -21,6 +21,7 @@ import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/propertie
import { ExplorerWidget } from 'sql/parts/dashboard/widgets/explorer/explorerWidget.component';
import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.component';
import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component';
+import { WebviewWidget } from 'sql/parts/dashboard/widgets/webview/webviewWidget.component';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
@@ -36,7 +37,8 @@ const componentMap: { [x: string]: Type
} = {
'properties-widget': PropertiesWidgetComponent,
'explorer-widget': ExplorerWidget,
'tasks-widget': TasksWidget,
- 'insights-widget': InsightsWidget
+ 'insights-widget': InsightsWidget,
+ 'webview-widget': WebviewWidget
};
@Component({
diff --git a/src/sql/parts/dashboard/common/interfaces.ts b/src/sql/parts/dashboard/common/interfaces.ts
index fa6acf998e..ca2400a32a 100644
--- a/src/sql/parts/dashboard/common/interfaces.ts
+++ b/src/sql/parts/dashboard/common/interfaces.ts
@@ -3,6 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import { Disposable } from 'vs/base/common/lifecycle';
+import Event from 'vs/base/common/event';
+
export enum Conditional {
'equals',
'notEquals',
@@ -11,4 +14,15 @@ export enum Conditional {
'lessThanOrEquals',
'lessThan',
'always'
-};
\ No newline at end of file
+}
+
+export abstract class DashboardTab extends Disposable {
+ public abstract layout(): void;
+ public abstract readonly id: string;
+ public abstract readonly editable: boolean;
+ public abstract refresh(): void;
+ public abstract readonly onResize: Event;
+ public enableEdit(): void {
+ // no op
+ }
+}
diff --git a/src/sql/parts/dashboard/dashboard.component.ts b/src/sql/parts/dashboard/dashboard.component.ts
index cf4fd768ce..4a9cef5330 100644
--- a/src/sql/parts/dashboard/dashboard.component.ts
+++ b/src/sql/parts/dashboard/dashboard.component.ts
@@ -32,6 +32,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
@ViewChild('actionBar', { read: ElementRef }) private actionbarContainer: ElementRef;
private actionbar: ActionBar;
+ private editAction: EditDashboardAction;
+ private editDisposable: IDisposable;
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrapService: DashboardServiceInterface,
@@ -48,7 +50,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
icon: true,
label: false,
});
- this.actionbar.push(new EditDashboardAction(this.edit, this), {
+ this.editAction = new EditDashboardAction(this.edit, this);
+ this.actionbar.push(this.editAction, {
icon: true,
label: false,
});
@@ -72,7 +75,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
}
onActivate(page: DashboardPage) {
+ if (this.editDisposable) {
+ this.editDisposable.dispose();
+ }
this._currentPage = page;
+ this.editDisposable = page.editEnabled(e => this.editEnabled = e, this);
}
refresh(): void {
@@ -84,4 +91,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
edit(): void {
this._currentPage.enableEdit();
}
+
+ set editEnabled(val: boolean) {
+ this.editAction.enabled = val;
+ }
}
diff --git a/src/sql/parts/dashboard/dashboard.module.ts b/src/sql/parts/dashboard/dashboard.module.ts
index e38be564fc..302e4747d1 100644
--- a/src/sql/parts/dashboard/dashboard.module.ts
+++ b/src/sql/parts/dashboard/dashboard.module.ts
@@ -27,9 +27,14 @@ import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost
/* Base Components */
import { DashboardComponent, DASHBOARD_SELECTOR } from 'sql/parts/dashboard/dashboard.component';
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
+import { DashboardWidgetTab } from 'sql/parts/dashboard/tabs/dashboardWidgetTab.component';
+import { DashboardWebviewTab } from 'sql/parts/dashboard/tabs/dashboardWebviewTab.component';
import { BreadcrumbComponent } from 'sql/base/browser/ui/breadcrumb/breadcrumb.component';
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
-let baseComponents = [DashboardComponent, DashboardWidgetWrapper, ComponentHostDirective, BreadcrumbComponent];
+let baseComponents = [DashboardComponent, DashboardWidgetWrapper, DashboardWebviewTab, DashboardWidgetTab, ComponentHostDirective, BreadcrumbComponent];
+
+/* Panel */
+import { PanelModule } from 'sql/base/browser/ui/panel/panel.module';
/* Pages */
import { ServerDashboardPage } from 'sql/parts/dashboard/pages/serverDashboardPage.component';
@@ -41,7 +46,14 @@ import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/propertie
import { ExplorerWidget } from 'sql/parts/dashboard/widgets/explorer/explorerWidget.component';
import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.component';
import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component';
-let widgetComponents = [PropertiesWidgetComponent, ExplorerWidget, TasksWidget, InsightsWidget];
+import { WebviewWidget } from 'sql/parts/dashboard/widgets/webview/webviewWidget.component';
+let widgetComponents = [
+ PropertiesWidgetComponent,
+ ExplorerWidget,
+ TasksWidget,
+ InsightsWidget,
+ WebviewWidget
+];
/* Insights */
let insightComponents = Registry.as(Extensions.InsightContribution).getAllCtors();
@@ -78,7 +90,8 @@ const appRoutes: Routes = [
FormsModule,
NgGridModule,
ChartsModule,
- RouterModule.forRoot(appRoutes)
+ RouterModule.forRoot(appRoutes),
+ PanelModule
],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
diff --git a/src/sql/parts/dashboard/dashboardConfig.contribution.ts b/src/sql/parts/dashboard/dashboardConfig.contribution.ts
index 3cd639f173..07a10ef677 100644
--- a/src/sql/parts/dashboard/dashboardConfig.contribution.ts
+++ b/src/sql/parts/dashboard/dashboardConfig.contribution.ts
@@ -4,18 +4,21 @@
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
-import { DATABASE_DASHBOARD_SETTING, DATABASE_DASHBOARD_PROPERTIES, databaseDashboardSettingSchema, databaseDashboardPropertiesSchema } from 'sql/parts/dashboard/pages/databaseDashboardPage.contribution';
-import { SERVER_DASHBOARD_SETTING, SERVER_DASHBOARD_PROPERTIES, serverDashboardSettingSchema, serverDashboardPropertiesSchema } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
+import { DASHBOARD_CONFIG_ID } from 'sql/parts/dashboard/pages/dashboardPageContribution';
+import { DATABASE_DASHBOARD_SETTING, DATABASE_DASHBOARD_PROPERTIES, DATABASE_DASHBOARD_TABS, databaseDashboardSettingSchema, databaseDashboardPropertiesSchema, databaseDashboardTabsSchema } from 'sql/parts/dashboard/pages/databaseDashboardPage.contribution';
+import { SERVER_DASHBOARD_SETTING, SERVER_DASHBOARD_PROPERTIES, SERVER_DASHBOARD_TABS, serverDashboardSettingSchema, serverDashboardPropertiesSchema, serverDashboardTabsSchema } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
const configurationRegistry = Registry.as(Extensions.Configuration);
const dashboardConfig: IConfigurationNode = {
- id: 'Dashboard',
+ id: DASHBOARD_CONFIG_ID,
type: 'object',
properties: {
[DATABASE_DASHBOARD_PROPERTIES]: databaseDashboardPropertiesSchema,
[SERVER_DASHBOARD_PROPERTIES]: serverDashboardPropertiesSchema,
[DATABASE_DASHBOARD_SETTING]: databaseDashboardSettingSchema,
- [SERVER_DASHBOARD_SETTING]: serverDashboardSettingSchema
+ [SERVER_DASHBOARD_SETTING]: serverDashboardSettingSchema,
+ [DATABASE_DASHBOARD_TABS]: databaseDashboardTabsSchema,
+ [SERVER_DASHBOARD_TABS]: serverDashboardTabsSchema
}
};
diff --git a/src/sql/parts/dashboard/newDashboardTabDialog/interface.ts b/src/sql/parts/dashboard/newDashboardTabDialog/interface.ts
new file mode 100644
index 0000000000..2e58c4561d
--- /dev/null
+++ b/src/sql/parts/dashboard/newDashboardTabDialog/interface.ts
@@ -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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+export const INewDashboardTabDialogService = createDecorator('addNewDashboardTabService');
+export interface INewDashboardTabDialogService {
+ _serviceBrand: any;
+ showDialog(dashboardTabs: Array, openedTabs: Array, uri: string): void;
+}
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/newDashboardTabDialog/media/newDashboardTabDialog.css b/src/sql/parts/dashboard/newDashboardTabDialog/media/newDashboardTabDialog.css
new file mode 100644
index 0000000000..8d0782c6a6
--- /dev/null
+++ b/src/sql/parts/dashboard/newDashboardTabDialog/media/newDashboardTabDialog.css
@@ -0,0 +1,57 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+.extension-view .monaco-split-view .split-view-view .header {
+ padding-right: 12px;
+}
+
+.extension-view .split-view-view .header .title {
+ display: flex;
+ justify-content: flex-start;
+ flex: 1 1 auto;
+}
+
+.extension-view .split-view-view .header .count-badge-wrapper {
+ justify-content: flex-end;
+}
+
+.extension-view .extensionTab-view .list-row {
+ padding: 15px;
+ display: flex;
+ align-items: flex-start;
+}
+
+.extension-view .extensionTab-view .list-row .extension-status-icon {
+ flex: 0 0 20px;
+ height: 16px;
+ width: 16px;
+}
+
+.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details {
+ flex: 1 1 auto;
+ margin-left: 5px;
+}
+
+.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details .title {
+ font-size: 15px;
+ font-weight: 700;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ white-space: pre-wrap;
+ padding-bottom: 5px;
+}
+
+.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details .description {
+ font-size: 13px;
+ overflow: hidden;
+ white-space: pre-wrap;
+ text-overflow: ellipsis;
+}
+
+.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details .publisher {
+ font-size: 90%;
+ padding-right: 6px;
+ opacity: .6;
+ font-weight: 600;
+}
diff --git a/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts
new file mode 100644
index 0000000000..cc6a45e676
--- /dev/null
+++ b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog.ts
@@ -0,0 +1,241 @@
+/*---------------------------------------------------------------------------------------------
+ * 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!sql/media/icons/common-icons';
+import 'vs/css!./media/newDashboardTabDialog';
+import * as DOM from 'vs/base/browser/dom';
+import { List } from 'vs/base/browser/ui/list/listWidget';
+import { IListService, ListService } from 'vs/platform/list/browser/listService';
+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 } from 'vs/platform/theme/common/styler';
+import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
+import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
+import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
+import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
+import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
+
+import { Button } from 'sql/base/browser/ui/button/button';
+import { Modal } from 'sql/base/browser/ui/modal/modal';
+import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
+import { FixedListView } from 'sql/platform/views/fixedListView';
+import * as TelemetryKeys from 'sql/common/telemetryKeys';
+import { SplitView } from 'sql/base/browser/ui/splitview/splitview';
+import { NewDashboardTabViewModel, IDashboardUITab } from 'sql/parts/dashboard/newDashboardTabDialog/newDashboardTabViewModel';
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+class ExtensionListDelegate implements IDelegate {
+
+ constructor(
+ private _height: number
+ ) {
+ }
+
+ public getHeight(element: IDashboardUITab): number {
+ return this._height;
+ }
+
+ public getTemplateId(element: IDashboardUITab): string {
+ return 'extensionListRenderer';
+ }
+}
+
+interface ExtensionListTemplate {
+ root: HTMLElement;
+ icon: HTMLElement;
+ title: HTMLElement;
+ description: HTMLElement;
+ publisher: HTMLElement;
+}
+
+class ExtensionListRenderer implements IRenderer {
+ public static TEMPLATE_ID = 'extensionListRenderer';
+ private static readonly OPENED_TAB_CLASS = 'success';
+ private static readonly ICON_CLASS = 'extension-status-icon icon';
+
+ public get templateId(): string {
+ return ExtensionListRenderer.TEMPLATE_ID;
+ }
+
+ public renderTemplate(container: HTMLElement): ExtensionListTemplate {
+ const tableTemplate: ExtensionListTemplate = Object.create(null);
+ tableTemplate.root = DOM.append(container, DOM.$('div.list-row.extensionTab-list'));
+ tableTemplate.icon = DOM.append(tableTemplate.root, DOM.$('div.icon'));
+ var titleContainer = DOM.append(tableTemplate.root, DOM.$('div.extension-details'));
+ tableTemplate.title = DOM.append(titleContainer, DOM.$('div.title'));
+ tableTemplate.description = DOM.append(titleContainer, DOM.$('div.description'));
+ tableTemplate.publisher = DOM.append(titleContainer, DOM.$('div.publisher'));
+ return tableTemplate;
+ }
+
+ public renderElement(dashboardTab: IDashboardUITab, index: number, templateData: ExtensionListTemplate): void {
+ templateData.icon.className = ExtensionListRenderer.ICON_CLASS;
+ if (dashboardTab.isOpened) {
+ templateData.icon.classList.add(ExtensionListRenderer.OPENED_TAB_CLASS);
+ }
+ templateData.title.innerText = dashboardTab.tabConfig.title;
+ templateData.description.innerText = dashboardTab.tabConfig.description;
+ templateData.publisher.innerText = dashboardTab.tabConfig.publisher;
+ }
+
+ public disposeTemplate(template: ExtensionListTemplate): void {
+ // noop
+ }
+}
+
+export class NewDashboardTabDialog extends Modal {
+ public static EXTENSIONLIST_HEIGHT = 101;
+
+ // MEMBER VARIABLES ////////////////////////////////////////////////////
+ private _addNewTabButton: Button;
+ private _cancelButton: Button;
+ private _extensionList: List;
+ private _extensionTabView: FixedListView;
+ private _splitView: SplitView;
+ private _container: HTMLElement;
+
+ private _viewModel: NewDashboardTabViewModel;
+
+ // EVENTING ////////////////////////////////////////////////////////////
+ private _onAddTabs: Emitter>;
+ public get onAddTabs(): Event> { return this._onAddTabs.event; }
+
+ private _onCancel: Emitter;
+ public get onCancel(): Event { return this._onCancel.event; }
+
+ constructor(
+ @IPartService partService: IPartService,
+ @IThemeService private _themeService: IThemeService,
+ @IListService private _listService: IListService,
+ @IInstantiationService private _instantiationService: IInstantiationService,
+ @IContextMenuService private _contextMenuService: IContextMenuService,
+ @IKeybindingService private _keybindingService: IKeybindingService,
+ @ITelemetryService telemetryService: ITelemetryService,
+ @IContextKeyService contextKeyService: IContextKeyService
+ ) {
+ super(
+ localize('openInstalledFeatures', 'Open installed features'),
+ TelemetryKeys.AddNewDashboardTab,
+ partService,
+ telemetryService,
+ contextKeyService,
+ { hasSpinner: true }
+ );
+
+ // Setup the event emitters
+ this._onAddTabs = new Emitter();
+ this._onCancel = new Emitter();
+
+ this._viewModel = new NewDashboardTabViewModel();
+ this._register(this._viewModel.updateTabListEvent(tabs => this.onUpdateTabList(tabs)));
+ }
+
+ // MODAL OVERRIDE METHODS //////////////////////////////////////////////
+ protected layout(height?: number): void {
+ // Ignore height as it's a subcomponent being laid out
+ this._splitView.layout(DOM.getContentHeight(this._container));
+ }
+
+ public render() {
+ super.render();
+ attachModalDialogStyler(this, this._themeService);
+
+ this._addNewTabButton = this.addFooterButton(localize('ok', 'OK'), () => this.addNewTabs());
+ this._cancelButton = this.addFooterButton(localize('cancel', 'Cancel'), () => this.cancel());
+ this.registerListeners();
+ }
+
+ protected renderBody(container: HTMLElement) {
+ this._container = container;
+ let viewBody = DOM.$('div.extension-view');
+ DOM.append(container, viewBody);
+ this._splitView = new SplitView(viewBody);
+
+ // Create a fixed list view for the account provider
+ let extensionTabViewContainer = DOM.$('.extensionTab-view');
+ let delegate = new ExtensionListDelegate(NewDashboardTabDialog.EXTENSIONLIST_HEIGHT);
+ let extensionTabRenderer = new ExtensionListRenderer();
+ this._extensionList = new List(extensionTabViewContainer, delegate, [extensionTabRenderer]);
+ this._extensionTabView = new FixedListView(
+ undefined,
+ false,
+ localize('allFeatures', 'All features'),
+ this._extensionList,
+ extensionTabViewContainer,
+ 22,
+ [],
+ undefined,
+ this._contextMenuService,
+ this._keybindingService,
+ this._themeService
+ );
+
+ // Append the list view to the split view
+ this._splitView.addView(this._extensionTabView);
+ this._register(attachListStyler(this._extensionList, this._themeService));
+
+ let listService = this._listService;
+ this._register(listService.register(this._extensionList));
+ this._splitView.layout(DOM.getContentHeight(this._container));
+ }
+
+ private registerListeners(): void {
+ // Theme styler
+ this._register(attachButtonStyler(this._cancelButton, this._themeService));
+ this._register(attachButtonStyler(this._addNewTabButton, this._themeService));
+ }
+
+ /* Overwrite escape key behavior */
+ protected onClose() {
+ this.cancel();
+ }
+
+ /* Overwrite enter key behavior */
+ protected onAccept() {
+ this.addNewTabs();
+ }
+
+ public close() {
+ this.hide();
+ }
+
+ private addNewTabs() {
+ if (this._addNewTabButton.enabled) {
+ let selectedTabs = this._extensionList.getSelectedElements();
+ this._onAddTabs.fire(selectedTabs);
+ }
+ }
+
+ public cancel() {
+ this.hide();
+ }
+
+ public open(dashboardTabs: Array, openedTabs: Array) {
+ this.show();
+ this._viewModel.updateDashboardTabs(dashboardTabs, openedTabs);
+ }
+
+ private onUpdateTabList(tabs: IDashboardUITab[]) {
+ this._extensionTabView.updateList(tabs);
+ this.layout();
+ if (this._extensionList.length > 0) {
+ this._extensionList.setSelection([0]);
+ this._addNewTabButton.enabled = true;
+ this._addNewTabButton.focus();
+ } else {
+ this._addNewTabButton.enabled = false;
+ this._cancelButton.focus();
+ }
+ }
+
+ public dispose(): void {
+ super.dispose();
+ }
+}
diff --git a/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialogService.ts b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialogService.ts
new file mode 100644
index 0000000000..92e8c83c11
--- /dev/null
+++ b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialogService.ts
@@ -0,0 +1,54 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { INewDashboardTabDialogService } from 'sql/parts/dashboard/newDashboardTabDialog/interface';
+import { NewDashboardTabDialog } from 'sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialog';
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+import { IAngularEventingService, AngularEventType } from 'sql/services/angularEventing/angularEventingService';
+import { IDashboardUITab } from 'sql/parts/dashboard/newDashboardTabDialog/newDashboardTabViewModel';
+
+import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+
+export class NewDashboardTabDialogService implements INewDashboardTabDialogService {
+ _serviceBrand: any;
+
+ // MEMBER VARIABLES ////////////////////////////////////////////////////
+ private _addNewTabDialog: NewDashboardTabDialog;
+ private _uri: string;
+
+ constructor(
+ @IAngularEventingService private _angularEventService: IAngularEventingService,
+ @IInstantiationService private _instantiationService: IInstantiationService
+ ) { }
+
+ /**
+ * Open account dialog
+ */
+ public showDialog(dashboardTabs: Array, openedTabs: Array, uri: string): void {
+ this._uri = uri;
+ let self = this;
+
+ // Create a new dialog if one doesn't exist
+ if (!this._addNewTabDialog) {
+ this._addNewTabDialog = this._instantiationService.createInstance(NewDashboardTabDialog);
+ this._addNewTabDialog.onCancel(() => { self.handleOnCancel(); });
+ this._addNewTabDialog.onAddTabs((selectedTabs) => { self.handleOnAddTabs(selectedTabs); });
+ this._addNewTabDialog.render();
+ }
+
+ // Open the dialog
+ this._addNewTabDialog.open(dashboardTabs, openedTabs);
+ }
+
+ // PRIVATE HELPERS /////////////////////////////////////////////////////
+ private handleOnAddTabs(selectedUiTabs: Array): void {
+ let selectedTabs = selectedUiTabs.map(tab => tab.tabConfig);
+ this._angularEventService.sendAngularEvent(this._uri, AngularEventType.NEW_TABS, { dashboardTabs: selectedTabs });
+ this._addNewTabDialog.close();
+ }
+
+ private handleOnCancel(): void { }
+}
diff --git a/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabViewModel.ts b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabViewModel.ts
new file mode 100644
index 0000000000..27d51f94ce
--- /dev/null
+++ b/src/sql/parts/dashboard/newDashboardTabDialog/newDashboardTabViewModel.ts
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 data from 'data';
+import Event, { Emitter } from 'vs/base/common/event';
+
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+
+export interface IDashboardUITab {
+ tabConfig: IDashboardTab;
+ isOpened?: boolean;
+}
+
+/**
+ * View model for new dashboard tab
+ */
+export class NewDashboardTabViewModel {
+
+ // EVENTING ///////////////////////////////////////////////////////
+ private _updateTabListEmitter: Emitter;
+ public get updateTabListEvent(): Event { return this._updateTabListEmitter.event; }
+
+
+ constructor() {
+ // Create event emitters
+ this._updateTabListEmitter = new Emitter();
+ }
+
+ public updateDashboardTabs(dashboardTabs: Array, openedTabs: Array) {
+ let tabList: IDashboardUITab[] = [];
+ dashboardTabs.forEach(tab => {
+ tabList.push({ tabConfig: tab });
+ });
+ openedTabs.forEach(tab => {
+ let uiTab = tabList.find(i => i.tabConfig === tab);
+ if (uiTab) {
+ uiTab.isOpened = true;
+ }
+ });
+ this._updateTabListEmitter.fire(tabList);
+ }
+}
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/pages/dashboardPageContribution.ts b/src/sql/parts/dashboard/pages/dashboardPageContribution.ts
new file mode 100644
index 0000000000..6e8814f222
--- /dev/null
+++ b/src/sql/parts/dashboard/pages/dashboardPageContribution.ts
@@ -0,0 +1,102 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { Extensions, IDashboardWidgetRegistry } from 'sql/platform/dashboard/common/widgetRegistry';
+
+import { Registry } from 'vs/platform/registry/common/platform';
+import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import { mixin } from 'vs/base/common/objects';
+import { localize } from 'vs/nls';
+
+let widgetRegistry = Registry.as(Extensions.DashboardWidgetContribution);
+
+export function generateDashboardWidgetSchema(type?: 'database' | 'server', extension?: boolean): IJSONSchema {
+ let schemas;
+ if (extension) {
+ let extensionSchemas = type === 'server' ? widgetRegistry.serverWidgetSchema.extensionProperties : type === 'database' ? widgetRegistry.databaseWidgetSchema.extensionProperties : widgetRegistry.allSchema.extensionProperties;
+ schemas = type === 'server' ? widgetRegistry.serverWidgetSchema.properties : type === 'database' ? widgetRegistry.databaseWidgetSchema.properties : widgetRegistry.allSchema.properties;
+ schemas = mixin(schemas, extensionSchemas, true);
+ } else {
+ schemas = type === 'server' ? widgetRegistry.serverWidgetSchema.properties : type === 'database' ? widgetRegistry.databaseWidgetSchema.properties : widgetRegistry.allSchema.properties;
+ }
+
+ return {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string'
+ },
+ icon: {
+ type: 'string'
+ },
+ provider: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ ]
+ },
+ edition: {
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'array',
+ items: {
+ type: 'number'
+ }
+ }
+ ]
+ },
+ gridItemConfig: {
+ type: 'object',
+ properties: {
+ sizex: {
+ type: 'number'
+ },
+ sizey: {
+ type: 'number'
+ },
+ col: {
+ type: 'number'
+ },
+ row: {
+ type: 'number'
+ }
+ }
+ },
+ widget: {
+ type: 'object',
+ properties: schemas,
+ minItems: 1,
+ maxItems: 1
+ }
+ }
+ };
+}
+
+export function generateDashboardTabSchema(type?: 'database' | 'server'): IJSONSchema {
+ return {
+ type: 'object',
+ properties: {
+ tabId: {
+ type: 'string',
+ description: localize('sqlops.extension.contributes.dashboard.tab.id', "Unique identifier for this tab. Will be passed to the extension for any requests."),
+ enum: [],
+ enumDescriptions: [],
+ errorMessage: localize('dashboardTabError', "Extension tab is unknown or not installed.")
+ }
+ }
+ };
+}
+
+export const DASHBOARD_CONFIG_ID = 'Dashboard';
+export const DASHBOARD_TABS_KEY_PROPERTY = 'tabId';
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/pages/databaseDashboardPage.component.ts b/src/sql/parts/dashboard/pages/databaseDashboardPage.component.ts
index ff228ae75f..ea8106c241 100644
--- a/src/sql/parts/dashboard/pages/databaseDashboardPage.component.ts
+++ b/src/sql/parts/dashboard/pages/databaseDashboardPage.component.ts
@@ -10,6 +10,7 @@ import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
+import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
@@ -34,19 +35,21 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit {
constructor(
@Inject(forwardRef(() => IBreadcrumbService)) private _breadcrumbService: IBreadcrumbService,
+ @Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService,
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
- super(dashboardService, el, _cd);
+ super(dashboardService, bootstrapService, el, _cd);
this._register(dashboardService.onUpdatePage(() => {
this.refresh(true);
this._cd.detectChanges();
}));
- this.init();
+
}
ngOnInit() {
+ this.init();
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
}
}
diff --git a/src/sql/parts/dashboard/pages/databaseDashboardPage.contribution.ts b/src/sql/parts/dashboard/pages/databaseDashboardPage.contribution.ts
index 63ca0224b2..9f8e5e3a58 100644
--- a/src/sql/parts/dashboard/pages/databaseDashboardPage.contribution.ts
+++ b/src/sql/parts/dashboard/pages/databaseDashboardPage.contribution.ts
@@ -3,12 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { Registry } from 'vs/platform/registry/common/platform';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
-import { Extensions, IDashboardWidgetRegistry } from 'sql/platform/dashboard/common/widgetRegistry';
import * as nls from 'vs/nls';
-
-let widgetRegistry = Registry.as(Extensions.DashboardWidgetContribution);
+import { generateDashboardWidgetSchema, generateDashboardTabSchema } from './dashboardPageContribution';
export const databaseDashboardPropertiesSchema: IJSONSchema = {
description: nls.localize('dashboardDatabaseProperties', 'Enable or disable the properties widget'),
@@ -85,58 +82,7 @@ export const databaseDashboardPropertiesSchema: IJSONSchema = {
export const databaseDashboardSettingSchema: IJSONSchema = {
type: ['array'],
description: nls.localize('dashboardDatabase', 'Customizes the database dashboard page'),
- items: {
- type: 'object',
- properties: {
- name: {
- type: 'string'
- },
- icon: {
- type: 'string'
- },
- provider: {
- anyOf: [
- 'string',
- {
- type: 'array',
- items: 'string'
- }
- ]
- },
- edition: {
- anyOf: [
- 'number',
- {
- type: 'array',
- items: 'number'
- }
- ]
- },
- gridItemConfig: {
- type: 'object',
- properties: {
- sizex: {
- type: 'number'
- },
- sizey: {
- type: 'number'
- },
- col: {
- type: 'number'
- },
- row: {
- type: 'number'
- }
- }
- },
- widget: {
- type: 'object',
- properties: widgetRegistry.databaseWidgetSchema.properties,
- minItems: 1,
- maxItems: 1
- }
- }
- },
+ items: generateDashboardWidgetSchema('database'),
default: [
{
name: 'Tasks',
@@ -160,5 +106,14 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
]
};
+export const databaseDashboardTabsSchema: IJSONSchema = {
+ type: ['array'],
+ description: nls.localize('dashboardDatabaseTabs', 'Customizes the database dashboard tabs'),
+ items: generateDashboardTabSchema('database'),
+ default: [
+ ]
+};
+
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
+export const DATABASE_DASHBOARD_TABS = 'dashboard.database.tabs';
diff --git a/src/sql/parts/dashboard/pages/serverDashboardPage.component.ts b/src/sql/parts/dashboard/pages/serverDashboardPage.component.ts
index 336c49df4c..80719ff325 100644
--- a/src/sql/parts/dashboard/pages/serverDashboardPage.component.ts
+++ b/src/sql/parts/dashboard/pages/serverDashboardPage.component.ts
@@ -10,6 +10,7 @@ import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
+import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
@@ -31,24 +32,26 @@ export class ServerDashboardPage extends DashboardPage implements OnInit {
};
protected readonly context = 'server';
+ private _letDashboardPromise: Thenable;
constructor(
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
+ @Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService,
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
- @Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
+ @Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
- super(dashboardService, el, cd);
+ super(dashboardService, bootstrapService, el, _cd);
// revert back to default database
- this.dashboardService.connectionManagementService.changeDatabase('master').then(() => {
- this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = undefined;
- this.init();
- cd.detectChanges();
- });
+ this._letDashboardPromise = this.dashboardService.connectionManagementService.changeDatabase('master');
}
ngOnInit() {
- this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
- this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
+ this._letDashboardPromise.then(() => {
+ this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
+ this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
+ this.init();
+ this._cd.detectChanges();
+ });
}
}
diff --git a/src/sql/parts/dashboard/pages/serverDashboardPage.contribution.ts b/src/sql/parts/dashboard/pages/serverDashboardPage.contribution.ts
index 01d72c1795..bbeeb211ab 100644
--- a/src/sql/parts/dashboard/pages/serverDashboardPage.contribution.ts
+++ b/src/sql/parts/dashboard/pages/serverDashboardPage.contribution.ts
@@ -2,12 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { Registry } from 'vs/platform/registry/common/platform';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
-import { Extensions, IDashboardWidgetRegistry } from 'sql/platform/dashboard/common/widgetRegistry';
import * as nls from 'vs/nls';
-
-let widgetRegistry = Registry.as(Extensions.DashboardWidgetContribution);
+import { generateDashboardWidgetSchema, generateDashboardTabSchema } from 'sql/parts/dashboard/pages/dashboardPageContribution';
export interface IPropertiesConfig {
edition: number | Array;
@@ -111,53 +108,18 @@ let defaultVal = [
export const serverDashboardSettingSchema: IJSONSchema = {
type: ['array'],
description: nls.localize('dashboardServer', 'Customizes the server dashboard page'),
- items: {
- type: 'object',
- properties: {
- name: {
- type: 'string'
- },
- icon: {
- type: 'string'
- },
- provider: {
- anyOf: [
- 'string',
- {
- type: 'array',
- items: 'string'
- }
- ]
- },
- edition: {
- anyOf: [
- 'number',
- {
- type: 'array',
- items: 'number'
- }
- ]
- },
- gridItemConfig: {
- type: 'object',
- properties: {
- sizex: {
- type: 'number'
- },
- sizey: {
- type: 'number'
- }
- }
- },
- widget: {
- type: 'object',
- properties: widgetRegistry.serverWidgetSchema.properties,
- maxItems: 1
- }
- }
- },
+ items: generateDashboardWidgetSchema('server'),
default: defaultVal
};
+export const serverDashboardTabsSchema: IJSONSchema = {
+ type: ['array'],
+ description: nls.localize('dashboardServerTabs', 'Customizes the Server dashboard tabs'),
+ items: generateDashboardTabSchema('server'),
+ default: [
+ ]
+};
+
export const SERVER_DASHBOARD_SETTING = 'dashboard.server.widgets';
-export const SERVER_DASHBOARD_PROPERTIES = 'dashboard.server.properties';
\ No newline at end of file
+export const SERVER_DASHBOARD_PROPERTIES = 'dashboard.server.properties';
+export const SERVER_DASHBOARD_TABS = 'dashboard.server.tabs';
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/services/dashboardServiceInterface.service.ts b/src/sql/parts/dashboard/services/dashboardServiceInterface.service.ts
index c12043f3ce..20de3c23bd 100644
--- a/src/sql/parts/dashboard/services/dashboardServiceInterface.service.ts
+++ b/src/sql/parts/dashboard/services/dashboardServiceInterface.service.ts
@@ -21,6 +21,8 @@ import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
+import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
+import { PinConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { ProviderMetadata, DatabaseInfo, SimpleExecuteResult } from 'data';
@@ -30,15 +32,18 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
-import { ConfigurationEditingService, IConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService'
+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';
import Event, { Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
import * as nls from 'vs/nls';
+import { IPartService } from 'vs/workbench/services/part/common/partService';
import { deepClone } from 'vs/base/common/objects';
+import { IDashboardWebviewService } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
+
const DASHBOARD_SETTINGS = 'dashboard';
/* Wrapper for a metadata service that contains the uri string to use on each request */
@@ -128,6 +133,8 @@ export class DashboardServiceInterface implements OnDestroy {
private _storageService: IStorageService;
private _capabilitiesService: ICapabilitiesService;
private _configurationEditingService: ConfigurationEditingService;
+ private _dashboardWebviewService: IDashboardWebviewService;
+ private _partService: IPartService;
private _updatePage = new Emitter();
public readonly onUpdatePage: Event = this._updatePage.event;
@@ -135,6 +142,15 @@ export class DashboardServiceInterface implements OnDestroy {
private _onDeleteWidget = new Emitter();
public readonly onDeleteWidget: Event = this._onDeleteWidget.event;
+ private _onPinUnpinTab = new Emitter();
+ public readonly onPinUnpinTab: Event = this._onPinUnpinTab.event;
+
+ private _onAddNewTabs = new Emitter>();
+ public readonly onAddNewTabs: Event> = this._onAddNewTabs.event;
+
+ private _onCloseTab = new Emitter();
+ public readonly onCloseTab: Event = this._onCloseTab.event;
+
constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
@Inject(forwardRef(() => Router)) private _router: Router,
@@ -150,6 +166,8 @@ export class DashboardServiceInterface implements OnDestroy {
this._storageService = this._bootstrapService.storageService;
this._capabilitiesService = this._bootstrapService.capabilitiesService;
this._configurationEditingService = this._bootstrapService.configurationEditorService;
+ this._dashboardWebviewService = this._bootstrapService.dashboardWebviewService;
+ this._partService = this._bootstrapService.partService;
}
ngOnDestroy() {
@@ -184,6 +202,14 @@ export class DashboardServiceInterface implements OnDestroy {
return this._instantiationService;
}
+ public get dashboardWebviewService(): IDashboardWebviewService {
+ return this._dashboardWebviewService;
+ }
+
+ public get partService(): IPartService {
+ return this._partService;
+ }
+
public get adminService(): SingleAdminService {
return this._adminService;
}
@@ -255,8 +281,8 @@ export class DashboardServiceInterface implements OnDestroy {
return deepClone(config);
}
- public writeSettings(key: string, value: any, target: ConfigurationTarget) {
- this._configurationEditingService.writeConfiguration(target, { key: DASHBOARD_SETTINGS + '.' + key + '.widgets', value });
+ public writeSettings(type: string, value: any, target: ConfigurationTarget) {
+ this._configurationEditingService.writeConfiguration(target, { key: [DASHBOARD_SETTINGS, type].join('.'), value });
}
private handleDashboardEvent(event: IAngularEvent): void {
@@ -284,6 +310,15 @@ export class DashboardServiceInterface implements OnDestroy {
break;
case AngularEventType.DELETE_WIDGET:
this._onDeleteWidget.fire(event.payload.id);
+ break;
+ case AngularEventType.PINUNPIN_TAB:
+ this._onPinUnpinTab.fire(event.payload);
+ break;
+ case AngularEventType.NEW_TABS:
+ this._onAddNewTabs.fire(event.payload.dashboardTabs);
+ break;
+ case AngularEventType.CLOSE_TAB:
+ this._onCloseTab.fire(event.payload.id);
}
}
}
diff --git a/src/sql/parts/dashboard/tabs/dashboardWebviewTab.component.ts b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.component.ts
new file mode 100644
index 0000000000..065e0e474d
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.component.ts
@@ -0,0 +1,126 @@
+/*---------------------------------------------------------------------------------------------
+* 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!./dashboardWebviewTab';
+
+import { Component, forwardRef, Input, OnInit, Inject, ChangeDetectorRef, ElementRef } from '@angular/core';
+
+import Event, { Emitter } from 'vs/base/common/event';
+import Webview from 'vs/workbench/parts/html/browser/webview';
+import { Parts } from 'vs/workbench/services/part/common/partService';
+import { IDisposable } from 'vs/base/common/lifecycle';
+
+import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
+import { TabConfig } from 'sql/parts/dashboard/common/dashboardWidget';
+import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
+import { IDashboardWebview } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
+
+import * as data from 'data';
+import { memoize } from 'vs/base/common/decorators';
+
+@Component({
+ template: '',
+ selector: 'dashboard-webview-tab',
+ providers: [{ provide: DashboardTab, useExisting: forwardRef(() => DashboardWebviewTab) }]
+})
+export class DashboardWebviewTab extends DashboardTab implements OnInit, IDashboardWebview {
+ @Input() private tab: TabConfig;
+
+ private _onResize = new Emitter();
+ public readonly onResize: Event = this._onResize.event;
+ private _onMessage = new Emitter();
+ public readonly onMessage: Event = this._onMessage.event;
+ private _onMessageDisposable: IDisposable;
+ private _webview: Webview;
+ private _html: string;
+
+ constructor(
+ @Inject(forwardRef(() => DashboardServiceInterface)) private _dashboardService: DashboardServiceInterface,
+ @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
+ @Inject(forwardRef(() => ElementRef)) private _el: ElementRef
+ ) {
+ super();
+ }
+
+ ngOnInit() {
+ this._dashboardService.dashboardWebviewService.registerWebview(this);
+ this._createWebview();
+ }
+
+ public layout(): void {
+ this._createWebview();
+ }
+
+ public get id(): string {
+ return this.tab.id;
+ }
+
+ public get editable(): boolean {
+ return this.tab.editable;
+ }
+
+ @memoize
+ public get connection(): data.connection.Connection {
+ let currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
+ let connection: data.connection.Connection = {
+ providerName: currentConnection.providerName,
+ connectionId: currentConnection.id,
+ options: currentConnection.options
+ };
+ return connection;
+ }
+
+ @memoize
+ public get serverInfo(): data.ServerInfo {
+ return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
+ }
+
+ public refresh(): void {
+ // no op
+ }
+
+ public setHtml(html: string): void {
+ this._html = html;
+ if (this._webview) {
+ this._webview.contents = [html];
+ this._webview.layout();
+ }
+ }
+
+ public sendMessage(message: string): void {
+ if (this._webview) {
+ this._webview.sendMessage(message);
+ }
+ }
+
+ private _createWebview(): void {
+ if (this._webview) {
+ this._webview.dispose();
+ }
+
+ if (this._onMessageDisposable) {
+ this._onMessageDisposable.dispose();
+ }
+
+ this._webview = new Webview(this._el.nativeElement,
+ this._dashboardService.partService.getContainer(Parts.EDITOR_PART),
+ this._dashboardService.contextViewService,
+ undefined,
+ undefined,
+ {
+ allowScripts: true,
+ enableWrappedPostMessage: true,
+ hideFind: true
+ }
+ );
+ this._onMessageDisposable = this._webview.onMessage(e => {
+ this._onMessage.fire(e);
+ });
+ this._webview.style(this._dashboardService.themeService.getTheme());
+ if (this._html) {
+ this._webview.contents = [this._html];
+ }
+ this._webview.layout();
+ }
+}
diff --git a/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts
new file mode 100644
index 0000000000..8b57d3e784
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts
@@ -0,0 +1,18 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import * as nls from 'vs/nls';
+
+import { registerTabContent } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+export const WEBVIEW_TABS = 'webview-tab';
+
+let webviewSchema: IJSONSchema = {
+ type: 'null',
+ description: nls.localize('dashboard.tab.widgets', "The list of widgets that will be displayed in this tab."),
+ default: null
+};
+
+registerTabContent(WEBVIEW_TABS, webviewSchema);
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/tabs/dashboardWebviewTab.css b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.css
new file mode 100644
index 0000000000..53fd33163f
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.css
@@ -0,0 +1,10 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+dashboard-webview-tab {
+ height: 100%;
+ width : 100%;
+ display: block;
+}
diff --git a/src/sql/parts/dashboard/tabs/dashboardWidgetTab.component.html b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.component.html
new file mode 100644
index 0000000000..93963222d7
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.component.html
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/src/sql/parts/dashboard/tabs/dashboardWidgetTab.component.ts b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.component.ts
new file mode 100644
index 0000000000..69e4e2cf94
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.component.ts
@@ -0,0 +1,228 @@
+/*---------------------------------------------------------------------------------------------
+ * 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!./dashboardWidgetTab';
+
+import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, EventEmitter, OnChanges } from '@angular/core';
+import { NgGridConfig, NgGrid, NgGridItem } from 'angular2-grid';
+
+import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
+import { TabConfig, WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
+import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
+import { subscriptionToDisposable } from 'sql/base/common/lifecycle';
+import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
+
+import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
+import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
+import * as objects from 'vs/base/common/objects';
+import Event, { Emitter } from 'vs/base/common/event';
+
+/**
+ * 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-widget-tab',
+ templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/tabs/dashboardWidgetTab.component.html')),
+ providers: [{ provide: DashboardTab, useExisting: forwardRef(() => DashboardWidgetTab) }]
+})
+export class DashboardWidgetTab extends DashboardTab implements OnDestroy, OnChanges {
+ @Input() private tab: TabConfig;
+ private widgets: WidgetConfig[];
+ private _onResize = new Emitter();
+ public readonly onResize: Event = this._onResize.event;
+
+ protected SKELETON_WIDTH = 5;
+ protected gridConfig: NgGridConfig = {
+ 'margins': [10], // The size of the margins of each item. Supports up to four values in the same way as CSS margins. Can be updated using setMargins()
+ 'draggable': false, // Whether the items can be dragged. Can be updated using enableDrag()/disableDrag()
+ 'resizable': false, // Whether the items can be resized. Can be updated using enableResize()/disableResize()
+ 'max_cols': this.SKELETON_WIDTH, // The maximum number of columns allowed. Set to 0 for infinite. Cannot be used with max_rows
+ 'max_rows': 0, // The maximum number of rows allowed. Set to 0 for infinite. Cannot be used with max_cols
+ 'visible_cols': 0, // The number of columns shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_cols
+ 'visible_rows': 0, // The number of rows shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_rows
+ 'min_cols': 0, // The minimum number of columns allowed. Can be any number greater than or equal to 1.
+ 'min_rows': 0, // The minimum number of rows allowed. Can be any number greater than or equal to 1.
+ 'col_width': 250, // The width of each column
+ 'row_height': 250, // The height of each row
+ 'cascade': 'left', // The direction to cascade grid items ('up', 'right', 'down', 'left')
+ 'min_width': 100, // The minimum width of an item. If greater than col_width, this will update the value of min_cols
+ 'min_height': 100, // The minimum height of an item. If greater than row_height, this will update the value of min_rows
+ 'fix_to_grid': false, // Fix all item movements to the grid
+ 'auto_style': true, // Automatically add required element styles at run-time
+ 'auto_resize': false, // Automatically set col_width/row_height so that max_cols/max_rows fills the screen. Only has effect is max_cols or max_rows is set
+ 'maintain_ratio': false, // Attempts to maintain aspect ratio based on the colWidth/rowHeight values set in the config
+ '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 _editDispose: Array = [];
+
+ @ViewChild(NgGrid) private _grid: NgGrid;
+ @ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList;
+ @ViewChildren(NgGridItem) private _items: QueryList;
+ constructor(
+ @Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface,
+ @Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
+ @Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
+ ) {
+ super();
+ }
+
+ protected init() {
+ }
+
+ ngOnChanges() {
+ if (this.tab.content) {
+ this.widgets = Object.values(this.tab.content)[0];
+ this._cd.detectChanges();
+ }
+ }
+
+ ngOnDestroy() {
+ this.dispose();
+ }
+
+ public get id(): string {
+ return this.tab.id;
+ }
+
+ public get editable(): boolean {
+ return this.tab.editable;
+ }
+
+ public layout() {
+ if (this._widgets) {
+ this._widgets.forEach(item => {
+ item.layout();
+ });
+ }
+ this._grid.triggerResize();
+ }
+
+ public refresh(): void {
+ if (this._widgets) {
+ this._widgets.forEach(item => {
+ item.refresh();
+ });
+ }
+ }
+
+ 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.tab.originalConfig.findIndex(i => i.id === e);
+ this.tab.originalConfig.splice(index, 1);
+
+ this._rewriteConfig();
+ this._cd.detectChanges();
+ }));
+ this._editDispose.push(subscriptionToDisposable(this._grid.onResizeStop.subscribe((e: NgGridItem) => {
+ this._onResize.fire();
+ let event = e.getEventOutput();
+ let config = this.tab.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._onResize.fire();
+ let event = e.getEventOutput();
+ this._items.forEach(i => {
+ let config = this.tab.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.tab.originalConfig.sort(configSorter);
+
+ this._rewriteConfig();
+ })));
+ this._widgets.forEach(i => {
+ if (i.id) {
+ i.enableEdit();
+ }
+ });
+ }
+ }
+
+ private _rewriteConfig(): void {
+ let writeableConfig = objects.deepClone(this.tab.originalConfig);
+
+ writeableConfig.forEach(i => {
+ delete i.id;
+ });
+ let target: ConfigurationTarget = ConfigurationTarget.USER;
+ this.dashboardService.writeSettings([this.tab.context, 'widgets'].join('.'), writeableConfig, target);
+ }
+}
diff --git a/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts
new file mode 100644
index 0000000000..3a34a04964
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts
@@ -0,0 +1,19 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import * as nls from 'vs/nls';
+
+import { generateDashboardWidgetSchema } from 'sql/parts/dashboard/pages/dashboardPageContribution';
+import { registerTabContent } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+export const WIDGETS_TABS = 'widgets-tab';
+
+let widgetsSchema: IJSONSchema = {
+ type: 'array',
+ description: nls.localize('dashboard.tab.content.widgets', "The list of widgets that will be displayed in this tab."),
+ items: generateDashboardWidgetSchema(undefined, true)
+};
+
+registerTabContent(WIDGETS_TABS, widgetsSchema);
diff --git a/src/sql/parts/dashboard/tabs/dashboardWidgetTab.css b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.css
new file mode 100644
index 0000000000..6bde0682e1
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.css
@@ -0,0 +1,9 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+dashboard-tab {
+ height: 100%;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/widgets/insights/insightsWidget.contribution.ts b/src/sql/parts/dashboard/widgets/insights/insightsWidget.contribution.ts
index e1ff3f22d0..b3a75aadcb 100644
--- a/src/sql/parts/dashboard/widgets/insights/insightsWidget.contribution.ts
+++ b/src/sql/parts/dashboard/widgets/insights/insightsWidget.contribution.ts
@@ -21,7 +21,7 @@ interface IInsightTypeContrib {
registerDashboardWidget('insights-widget', '', insightsSchema);
-ExtensionsRegistry.registerExtensionPoint('insights', [], insightsContribution).setHandler(extensions => {
+ExtensionsRegistry.registerExtensionPoint('dashboard.insights', [], insightsContribution).setHandler(extensions => {
function handleCommand(insight: IInsightTypeContrib, extension: IExtensionPointUser) {
diff --git a/src/sql/parts/dashboard/widgets/webview/webviewWidget.component.ts b/src/sql/parts/dashboard/widgets/webview/webviewWidget.component.ts
new file mode 100644
index 0000000000..37e6af5e9a
--- /dev/null
+++ b/src/sql/parts/dashboard/widgets/webview/webviewWidget.component.ts
@@ -0,0 +1,119 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ViewChild, ElementRef } from '@angular/core';
+
+import Webview from 'vs/workbench/parts/html/browser/webview';
+import { Parts } from 'vs/workbench/services/part/common/partService';
+import Event, { Emitter } from 'vs/base/common/event';
+import { IDisposable } from 'vs/base/common/lifecycle';
+import { memoize } from 'vs/base/common/decorators';
+
+import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
+import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
+import { IDashboardWebview } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
+
+import * as data from 'data';
+
+interface IWebviewWidgetConfig {
+ id: string;
+}
+
+const selector = 'webview-widget';
+
+@Component({
+ selector: selector,
+ template: ''
+})
+export class WebviewWidget extends DashboardWidget implements IDashboardWidget, OnInit, IDashboardWebview {
+
+ private _id: string;
+ private _webview: Webview;
+ private _html: string;
+ private _onMessage = new Emitter();
+ public readonly onMessage: Event = this._onMessage.event;
+ private _onMessageDisposable: IDisposable;
+
+ constructor(
+ @Inject(forwardRef(() => DashboardServiceInterface)) private _dashboardService: DashboardServiceInterface,
+ @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
+ @Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
+ @Inject(forwardRef(() => ElementRef)) private _el: ElementRef
+ ) {
+ super();
+ this._id = (_config.widget[selector] as IWebviewWidgetConfig).id;
+ }
+
+ ngOnInit() {
+ this._dashboardService.dashboardWebviewService.registerWebview(this);
+ this._createWebview();
+ }
+
+ public get id(): string {
+ return this._id;
+ }
+
+ public setHtml(html: string): void {
+ this._html = html;
+ if (this._webview) {
+ this._webview.contents = [html];
+ this._webview.layout();
+ }
+ }
+
+ @memoize
+ public get connection(): data.connection.Connection {
+ let currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
+ let connection: data.connection.Connection = {
+ providerName: currentConnection.providerName,
+ connectionId: currentConnection.id,
+ options: currentConnection.options
+ };
+ return connection;
+ }
+
+ @memoize
+ public get serverInfo(): data.ServerInfo {
+ return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
+ }
+
+ public layout(): void {
+ this._createWebview();
+ }
+
+ public sendMessage(message: string): void {
+ if (this._webview) {
+ this._webview.sendMessage(message);
+ }
+ }
+
+ private _createWebview(): void {
+ if (this._webview) {
+ this._webview.dispose();
+ }
+ if (this._onMessageDisposable) {
+ this._onMessageDisposable.dispose();
+ }
+ this._webview = new Webview(this._el.nativeElement,
+ this._dashboardService.partService.getContainer(Parts.EDITOR_PART),
+ this._dashboardService.contextViewService,
+ undefined,
+ undefined,
+ {
+ allowScripts: true,
+ enableWrappedPostMessage: true,
+ hideFind: true
+ }
+ );
+ this._onMessageDisposable = this._webview.onMessage(e => {
+ this._onMessage.fire(e);
+ });
+ this._webview.style(this._dashboardService.themeService.getTheme());
+ if (this._html) {
+ this._webview.contents = [this._html];
+ }
+ this._webview.layout();
+ }
+}
diff --git a/src/sql/parts/dashboard/widgets/webview/webviewWidget.contribution.ts b/src/sql/parts/dashboard/widgets/webview/webviewWidget.contribution.ts
new file mode 100644
index 0000000000..dbca3a1597
--- /dev/null
+++ b/src/sql/parts/dashboard/widgets/webview/webviewWidget.contribution.ts
@@ -0,0 +1,17 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import { registerDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
+
+let webviewSchema: IJSONSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string'
+ }
+ }
+};
+
+registerDashboardWidget('webview-widget', '', webviewSchema, undefined, { extensionOnly: true });
diff --git a/src/sql/parts/grid/media/flexbox.css b/src/sql/parts/grid/media/flexbox.css
index 6c364c1769..5c7bc0d190 100644
--- a/src/sql/parts/grid/media/flexbox.css
+++ b/src/sql/parts/grid/media/flexbox.css
@@ -1,5 +1,6 @@
.fullsize {
height: 100%;
+ width: 100%;
}
.headersVisible > .fullsize {
diff --git a/src/sql/platform/dashboard/common/dashboardRegistry.ts b/src/sql/platform/dashboard/common/dashboardRegistry.ts
index b4e86bcd42..b335a42ad0 100644
--- a/src/sql/platform/dashboard/common/dashboardRegistry.ts
+++ b/src/sql/platform/dashboard/common/dashboardRegistry.ts
@@ -4,23 +4,45 @@
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
-import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import { IConfigurationRegistry, Extensions as ConfigurationExtension } from 'vs/platform/configuration/common/configurationRegistry';
+import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
+import { deepClone } from 'vs/base/common/objects';
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
import { ProviderProperties } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
+import { DATABASE_DASHBOARD_TABS } from 'sql/parts/dashboard/pages/databaseDashboardPage.contribution';
+import { SERVER_DASHBOARD_TABS, SERVER_DASHBOARD_PROPERTIES } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
+import { DASHBOARD_CONFIG_ID, DASHBOARD_TABS_KEY_PROPERTY } from 'sql/parts/dashboard/pages/dashboardPageContribution';
export const Extensions = {
DashboardContributions: 'dashboard.contributions'
};
+export interface IDashboardTab {
+ id: string;
+ title: string;
+ publisher: string;
+ description?: string;
+ content?: object;
+ provider?: string | string[];
+ edition?: number | number[];
+ alwaysShow?: boolean;
+}
+
export interface IDashboardRegistry {
registerDashboardProvider(id: string, properties: ProviderProperties): void;
getProperties(id: string): ProviderProperties;
+ registerTab(tab: IDashboardTab): void;
+ tabs: Array;
+ tabContentSchemaProperties: IJSONSchemaMap;
}
class DashboardRegistry implements IDashboardRegistry {
private _properties = new Map();
+ private _tabs = new Array();
+ private _configurationRegistry = Registry.as(ConfigurationExtension.Configuration);
+ private _dashboardTabContentSchemaProperties: IJSONSchemaMap = {};
/**
* Register a dashboard widget
@@ -33,11 +55,57 @@ class DashboardRegistry implements IDashboardRegistry {
public getProperties(id: string): ProviderProperties {
return this._properties.get(id);
}
+
+ public registerTab(tab: IDashboardTab): void {
+ this._tabs.push(tab);
+ let dashboardConfig = this._configurationRegistry.getConfigurations().find(c => c.id === DASHBOARD_CONFIG_ID);
+
+ if (dashboardConfig) {
+ let dashboardDatabaseTabProperty = (dashboardConfig.properties[DATABASE_DASHBOARD_TABS].items).properties[DASHBOARD_TABS_KEY_PROPERTY];
+ dashboardDatabaseTabProperty.enum.push(tab.id);
+ dashboardDatabaseTabProperty.enumDescriptions.push(tab.description || '');
+
+ let dashboardServerTabProperty = (dashboardConfig.properties[SERVER_DASHBOARD_TABS].items).properties[DASHBOARD_TABS_KEY_PROPERTY];
+ dashboardServerTabProperty.enum.push(tab.id);
+ dashboardServerTabProperty.enumDescriptions.push(tab.description || '');
+
+ this._configurationRegistry.notifyConfigurationSchemaUpdated(dashboardConfig);
+ }
+ }
+
+ public get tabs(): Array {
+ return this._tabs;
+ }
+
+ /**
+ * Register a dashboard widget
+ * @param id id of the widget
+ * @param schema config schema of the widget
+ */
+ public registerTabContent(id: string, schema: IJSONSchema): void {
+ this._dashboardTabContentSchemaProperties[id] = schema;
+ }
+
+ public get tabContentSchemaProperties(): IJSONSchemaMap {
+ return deepClone(this._dashboardTabContentSchemaProperties);
+ }
}
const dashboardRegistry = new DashboardRegistry();
Registry.add(Extensions.DashboardContributions, dashboardRegistry);
+export function registerTab(tab: IDashboardTab): void {
+ dashboardRegistry.registerTab(tab);
+}
+
+export function registerTabContent(id: string, schema: IJSONSchema): void {
+ dashboardRegistry.registerTabContent(id, schema);
+}
+
+export function generateTabContentSchemaProperties(): IJSONSchemaMap {
+ return dashboardRegistry.tabContentSchemaProperties;
+}
+
const dashboardPropertiesPropertyContrib: IJSONSchema = {
description: nls.localize('dashboard.properties.property', "Defines a property to show on the dashboard"),
type: 'object',
@@ -134,4 +202,4 @@ ExtensionsRegistry.registerExtensionPoint(
export enum AngularEventType {
NAV_DATABASE,
NAV_SERVER,
- DELETE_WIDGET
+ DELETE_WIDGET,
+ PINUNPIN_TAB,
+ NEW_TABS,
+ CLOSE_TAB
}
export interface IDeleteWidgetPayload {
diff --git a/src/sql/services/bootstrap/bootstrapService.ts b/src/sql/services/bootstrap/bootstrapService.ts
index b8e1cdce7a..66a43f3bf6 100644
--- a/src/sql/services/bootstrap/bootstrapService.ts
+++ b/src/sql/services/bootstrap/bootstrapService.ts
@@ -22,6 +22,7 @@ import { ISqlOAuthService } from 'sql/common/sqlOAuthService';
import { IFileBrowserService, IFileBrowserDialogController } from 'sql/parts/fileBrowser/common/interfaces';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
+import { IDashboardWebviewService } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@@ -88,6 +89,7 @@ export interface IBootstrapService {
clipboardService: IClipboardService;
capabilitiesService: ICapabilitiesService;
configurationEditorService: ConfigurationEditingService;
+ dashboardWebviewService: IDashboardWebviewService;
/*
* Bootstraps the Angular module described. Components that need singleton services should inject the
diff --git a/src/sql/services/bootstrap/bootstrapServiceImpl.ts b/src/sql/services/bootstrap/bootstrapServiceImpl.ts
index ea3ee9ff3f..34d87c0abe 100644
--- a/src/sql/services/bootstrap/bootstrapServiceImpl.ts
+++ b/src/sql/services/bootstrap/bootstrapServiceImpl.ts
@@ -43,6 +43,7 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
+import { IDashboardWebviewService } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
export class BootstrapService implements IBootstrapService {
@@ -96,7 +97,8 @@ export class BootstrapService implements IBootstrapService {
@ITelemetryService public telemetryService: ITelemetryService,
@IStorageService public storageService: IStorageService,
@IClipboardService public clipboardService: IClipboardService,
- @ICapabilitiesService public capabilitiesService: ICapabilitiesService
+ @ICapabilitiesService public capabilitiesService: ICapabilitiesService,
+ @IDashboardWebviewService public dashboardWebviewService: IDashboardWebviewService
) {
this.configurationEditorService = this.instantiationService.createInstance(ConfigurationEditingService);
this._bootstrapParameterMap = new Map();
diff --git a/src/sql/services/dashboardWebview/common/dashboardWebviewService.ts b/src/sql/services/dashboardWebview/common/dashboardWebviewService.ts
new file mode 100644
index 0000000000..5fe8a7f684
--- /dev/null
+++ b/src/sql/services/dashboardWebview/common/dashboardWebviewService.ts
@@ -0,0 +1,30 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
+import Event from 'vs/base/common/event';
+
+import * as data from 'data';
+
+export const SERVICE_ID = 'dashboardWebviewService';
+
+export interface IDashboardWebview {
+ readonly id: string;
+ readonly connection: data.connection.Connection;
+ readonly serverInfo: data.ServerInfo;
+ setHtml(html: string): void;
+ onMessage: Event;
+ sendMessage(message: string);
+}
+
+export interface IDashboardWebviewService {
+ _serviceBrand: any;
+ onRegisteredWebview: Event;
+ registerWebview(widget: IDashboardWebview);
+}
+
+export const IDashboardWebviewService = createDecorator(SERVICE_ID);
diff --git a/src/sql/services/dashboardWebview/common/dashboardWebviewServiceImpl.ts b/src/sql/services/dashboardWebview/common/dashboardWebviewServiceImpl.ts
new file mode 100644
index 0000000000..f35e097b56
--- /dev/null
+++ b/src/sql/services/dashboardWebview/common/dashboardWebviewServiceImpl.ts
@@ -0,0 +1,21 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { IDashboardWebviewService, IDashboardWebview } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
+import Event, { Emitter } from 'vs/base/common/event';
+
+export class DashboardWebviewService implements IDashboardWebviewService {
+ _serviceBrand: any;
+
+ private _onRegisteredWebview = new Emitter();
+ public readonly onRegisteredWebview: Event = this._onRegisteredWebview.event;
+
+
+ public registerWebview(widget: IDashboardWebview) {
+ this._onRegisteredWebview.fire(widget);
+ }
+}
diff --git a/src/sql/workbench/api/node/extHostDashboardWebview.ts b/src/sql/workbench/api/node/extHostDashboardWebview.ts
new file mode 100644
index 0000000000..805be19159
--- /dev/null
+++ b/src/sql/workbench/api/node/extHostDashboardWebview.ts
@@ -0,0 +1,94 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { SqlMainContext, ExtHostDashboardWebviewsShape, MainThreadDashboardWebviewShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
+
+import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
+import { Emitter } from 'vs/base/common/event';
+import { deepClone } from 'vs/base/common/objects';
+
+import * as vscode from 'vscode';
+import * as data from 'data';
+
+class ExtHostDashboardWebview implements data.DashboardWebview {
+
+ private _html: string;
+ public onMessageEmitter = new Emitter();
+ public onClosedEmitter = new Emitter();
+
+ constructor(
+ private readonly _proxy: MainThreadDashboardWebviewShape,
+ private readonly _handle: number,
+ private readonly _connection: data.connection.Connection,
+ private readonly _serverInfo: data.ServerInfo
+ ) { }
+
+ public postMessage(message: any): Thenable {
+ return this._proxy.$sendMessage(this._handle, message);
+ }
+
+ public get onMessage(): vscode.Event {
+ return this.onMessageEmitter.event;
+ }
+
+ public get onClosed(): vscode.Event {
+ return this.onClosedEmitter.event;
+ }
+
+ public get connection(): data.connection.Connection {
+ return deepClone(this._connection);
+ }
+
+ public get serverInfo(): data.ServerInfo {
+ return deepClone(this._serverInfo);
+ }
+
+ get html(): string {
+ return this._html;
+ }
+
+ set html(value: string) {
+ if (this._html !== value) {
+ this._html = value;
+ this._proxy.$setHtml(this._handle, value);
+ }
+ }
+}
+
+export class ExtHostDashboardWebviews implements ExtHostDashboardWebviewsShape {
+ private readonly _proxy: MainThreadDashboardWebviewShape;
+
+ private readonly _webviews = new Map();
+ private readonly _handlers = new Map void>();
+
+ constructor(
+ mainContext: IMainContext
+ ) {
+ this._proxy = mainContext.get(SqlMainContext.MainThreadDashboardWebview);
+ }
+
+ $onMessage(handle: number, message: any): void {
+ const webview = this._webviews.get(handle);
+ webview.onMessageEmitter.fire(message);
+ }
+
+ $onClosed(handle: number): void {
+ const webview = this._webviews.get(handle);
+ webview.onClosedEmitter.fire();
+ this._webviews.delete(handle);
+ }
+
+ $registerProvider(widgetId: string, handler: (webview: data.DashboardWebview) => void): void {
+ this._handlers.set(widgetId, handler);
+ this._proxy.$registerProvider(widgetId);
+ }
+
+ $registerWidget(handle: number, id: string, connection: data.connection.Connection, serverInfo: data.ServerInfo): void {
+ let webview = new ExtHostDashboardWebview(this._proxy, handle, connection, serverInfo);
+ this._webviews.set(handle, webview);
+ this._handlers.get(id)(webview);
+ }
+}
diff --git a/src/sql/workbench/api/node/extHostModalDialog.ts b/src/sql/workbench/api/node/extHostModalDialog.ts
index 9fc85b81fa..089c3623e3 100644
--- a/src/sql/workbench/api/node/extHostModalDialog.ts
+++ b/src/sql/workbench/api/node/extHostModalDialog.ts
@@ -119,4 +119,4 @@ export class ExtHostModalDialogs implements ExtHostModalDialogsShape {
const webview = this._webviews.get(handle);
webview.onClosedEmitter.fire();
}
-}
\ No newline at end of file
+}
diff --git a/src/sql/workbench/api/node/mainThreadDashboardWebview.ts b/src/sql/workbench/api/node/mainThreadDashboardWebview.ts
new file mode 100644
index 0000000000..f77ca9183f
--- /dev/null
+++ b/src/sql/workbench/api/node/mainThreadDashboardWebview.ts
@@ -0,0 +1,53 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { MainThreadDashboardWebviewShape, SqlMainContext, ExtHostDashboardWebviewsShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
+import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
+import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
+import { IDashboardWebviewService, IDashboardWebview } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
+
+@extHostNamedCustomer(SqlMainContext.MainThreadDashboardWebview)
+export class MainThreadDashboardWebview implements MainThreadDashboardWebviewShape {
+
+ private static _handlePool = 0;
+ private readonly _proxy: ExtHostDashboardWebviewsShape;
+ private readonly _dialogs = new Map();
+
+ private knownWidgets = new Array();
+
+ constructor(
+ context: IExtHostContext,
+ @IDashboardWebviewService webviewService: IDashboardWebviewService
+ ) {
+ this._proxy = context.get(SqlExtHostContext.ExtHostDashboardWebviews);
+ webviewService.onRegisteredWebview(e => {
+ if (this.knownWidgets.includes(e.id)) {
+ let handle = MainThreadDashboardWebview._handlePool++;
+ this._dialogs.set(handle, e);
+ this._proxy.$registerWidget(handle, e.id, e.connection, e.serverInfo);
+ e.onMessage(e => {
+ this._proxy.$onMessage(handle, e);
+ });
+ }
+ });
+ }
+
+ public dispose(): void {
+ throw new Error("Method not implemented.");
+ }
+
+ $sendMessage(handle: number, message: string) {
+ this._dialogs.get(handle).sendMessage(message);
+ }
+
+ $setHtml(handle: number, value: string) {
+ this._dialogs.get(handle).setHtml(value);
+ }
+
+ $registerProvider(widgetId: string) {
+ this.knownWidgets.push(widgetId);
+ }
+}
diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts
index 49bdfb7689..b62fc8ef3c 100644
--- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts
+++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts
@@ -28,6 +28,7 @@ import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration
import { ExtHostModalDialogs } from 'sql/workbench/api/node/extHostModalDialog';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionApiFactory } from 'vs/workbench/api/node/extHost.api.impl';
+import { ExtHostDashboardWebviews } from 'sql/workbench/api/node/extHostDashboardWebview';
import { ExtHostConnectionManagement } from 'sql/workbench/api/node/extHostConnectionManagement';
export interface ISqlExtensionApiFactory {
@@ -56,6 +57,7 @@ export function createApiFactory(
const extHostSerializationProvider = threadService.set(SqlExtHostContext.ExtHostSerializationProvider, new ExtHostSerializationProvider(threadService));
const extHostResourceProvider = threadService.set(SqlExtHostContext.ExtHostResourceProvider, new ExtHostResourceProvider(threadService));
const extHostModalDialogs = threadService.set(SqlExtHostContext.ExtHostModalDialogs, new ExtHostModalDialogs(threadService));
+ const extHostWebviewWidgets = threadService.set(SqlExtHostContext.ExtHostDashboardWebviews, new ExtHostDashboardWebviews(threadService));
return {
vsCodeFactory: vsCodeFactory,
@@ -259,6 +261,12 @@ export function createApiFactory(
}
};
+ const dashboard = {
+ registerWebviewProvider(widgetId: string, handler: (webview: data.DashboardWebview) => void) {
+ extHostWebviewWidgets.$registerProvider(widgetId, handler);
+ }
+ };
+
return {
accounts,
connection,
@@ -273,7 +281,8 @@ export function createApiFactory(
TaskStatus: sqlExtHostTypes.TaskStatus,
TaskExecutionMode: sqlExtHostTypes.TaskExecutionMode,
ScriptOperation: sqlExtHostTypes.ScriptOperation,
- window
+ window,
+ dashboard
};
}
};
diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts
index 8c3b70dc38..f9cef6acf5 100644
--- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts
+++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts
@@ -15,6 +15,7 @@ import 'sql/workbench/api/node/mainThreadCredentialManagement';
import 'sql/workbench/api/node/mainThreadDataProtocol';
import 'sql/workbench/api/node/mainThreadSerializationProvider';
import 'sql/workbench/api/node/mainThreadResourceProvider';
+import 'sql/workbench/api/node/mainThreadDashboardWebview';
import './mainThreadAccountManagement';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts
index 9fdd025582..8623718cc7 100644
--- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts
+++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts
@@ -14,6 +14,7 @@ import * as data from 'data';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDisposable } from 'vs/base/common/lifecycle';
+
export abstract class ExtHostAccountManagementShape {
$autoOAuthCancelled(handle: number): Thenable { throw ni(); }
$clear(handle: number, accountKey: data.AccountKey): Thenable { throw ni(); }
@@ -420,6 +421,7 @@ export const SqlMainContext = {
MainThreadSerializationProvider: createMainId('MainThreadSerializationProvider'),
MainThreadResourceProvider: createMainId('MainThreadResourceProvider'),
MainThreadModalDialog: createMainId('MainThreadModalDialog'),
+ MainThreadDashboardWebview: createMainId('MainThreadDashboardWebview')
};
export const SqlExtHostContext = {
@@ -429,7 +431,8 @@ export const SqlExtHostContext = {
ExtHostDataProtocol: createExtId('ExtHostDataProtocol'),
ExtHostSerializationProvider: createExtId('ExtHostSerializationProvider'),
ExtHostResourceProvider: createExtId('ExtHostResourceProvider'),
- ExtHostModalDialogs: createExtId('ExtHostModalDialogs')
+ ExtHostModalDialogs: createExtId('ExtHostModalDialogs'),
+ ExtHostDashboardWebviews: createExtId('ExtHostDashboardWebviews')
};
export interface MainThreadModalDialogShape extends IDisposable {
@@ -440,7 +443,21 @@ export interface MainThreadModalDialogShape extends IDisposable {
$setHtml(handle: number, value: string): void;
$sendMessage(handle: number, value: any): Thenable;
}
+
export interface ExtHostModalDialogsShape {
$onMessage(handle: number, message: any): void;
$onClosed(handle: number): void;
-}
\ No newline at end of file
+}
+
+export interface ExtHostDashboardWebviewsShape {
+ $registerProvider(widgetId: string, handler: (webview: data.DashboardWebview) => void): void;
+ $onMessage(handle: number, message: any): void;
+ $onClosed(handle: number): void;
+ $registerWidget(handle: number, id: string, connection: data.connection.Connection, serverInfo: data.ServerInfo): void;
+}
+
+export interface MainThreadDashboardWebviewShape extends IDisposable {
+ $sendMessage(handle: number, message: string);
+ $registerProvider(widgetId: string);
+ $setHtml(handle: number, value: string);
+}
diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts
index 8d99eb8a62..791d3a64a2 100644
--- a/src/vs/workbench/electron-browser/workbench.ts
+++ b/src/vs/workbench/electron-browser/workbench.ts
@@ -127,6 +127,8 @@ import { IBackupService, IBackupUiService } from 'sql/parts/disasterRecovery/bac
import { BackupService, BackupUiService } from 'sql/parts/disasterRecovery/backup/common/backupServiceImp';
import { IRestoreDialogController, IRestoreService } from 'sql/parts/disasterRecovery/restore/common/restoreService';
import { RestoreService, RestoreDialogController } from 'sql/parts/disasterRecovery/restore/common/restoreServiceImpl';
+import { INewDashboardTabDialogService } from 'sql/parts/dashboard/newDashboardTabDialog/interface';
+import { NewDashboardTabDialogService } from 'sql/parts/dashboard/newDashboardTabDialog/newDashboardTabDialogService';
import { IFileBrowserService, IFileBrowserDialogController } from 'sql/parts/fileBrowser/common/interfaces';
import { FileBrowserService } from 'sql/parts/fileBrowser/common/fileBrowserService';
import { FileBrowserDialogController } from 'sql/parts/fileBrowser/fileBrowserDialogController';
@@ -143,6 +145,8 @@ import { ClipboardService as sqlClipboardService } from 'sql/platform/clipboard/
import { IResourceProviderService, IAccountPickerService } from 'sql/parts/accountManagement/common/interfaces';
import { ResourceProviderService } from 'sql/parts/accountManagement/common/resourceProviderService';
import { AccountPickerService } from 'sql/parts/accountManagement/accountPicker/accountPickerService';
+import { IDashboardWebviewService } from 'sql/services/dashboardWebview/common/dashboardWebviewService';
+import { DashboardWebviewService } from 'sql/services/dashboardWebview/common/dashboardWebviewServiceImpl';
export const MessagesVisibleContext = new RawContextKey('globalMessageVisible', false);
export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false);
@@ -667,10 +671,12 @@ export class Workbench implements IPartService {
this.toDispose.push(this.quickOpen);
this.toShutdown.push(this.quickOpen);
serviceCollection.set(IQuickOpenService, this.quickOpen);
-
+
// {{SQL CARBON EDIT}}
// SQL Tools services
+ serviceCollection.set(IDashboardWebviewService, this.instantiationService.createInstance(DashboardWebviewService));
serviceCollection.set(IAngularEventingService, this.instantiationService.createInstance(AngularEventingService));
+ serviceCollection.set(INewDashboardTabDialogService, this.instantiationService.createInstance(NewDashboardTabDialogService));
serviceCollection.set(ISqlOAuthService, this.instantiationService.createInstance(SqlOAuthService));
serviceCollection.set(sqlIClipboardService, this.instantiationService.createInstance(sqlClipboardService));
serviceCollection.set(ICapabilitiesService, this.instantiationService.createInstance(CapabilitiesService));
@@ -707,7 +713,7 @@ export class Workbench implements IPartService {
this.toDispose.push(connectionManagementService);
this.toShutdown.push(connectionManagementService);
this.toShutdown.push(accountManagementService);
-
+
// Contributed services
const contributedServices = getServices();
for (let contributedService of contributedServices) {
diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts
index 689991871c..b05f368c36 100644
--- a/src/vs/workbench/parts/html/browser/webview.ts
+++ b/src/vs/workbench/parts/html/browser/webview.ts
@@ -195,7 +195,7 @@ export default class Webview {
if (parent) {
// {{SQL CARBON EDIT}}
if (!this._options.hideFind) {
- parent.appendChild(this._webviewFindWidget.getDomNode());
+ parent.appendChild(this._webviewFindWidget.getDomNode());
}
parent.appendChild(this._webview);
}
@@ -215,8 +215,11 @@ export default class Webview {
if (this._webview.parentElement) {
this._webview.parentElement.removeChild(this._webview);
- const findWidgetDomNode = this._webviewFindWidget.getDomNode();
- findWidgetDomNode.parentElement.removeChild(findWidgetDomNode);
+ // {{SQL CARBON EDIT}}
+ if (!this._options.hideFind) {
+ const findWidgetDomNode = this._webviewFindWidget.getDomNode();
+ findWidgetDomNode.parentElement.removeChild(findWidgetDomNode);
+ }
}
}
diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts
index 9d972c8037..7d0aff5109 100644
--- a/src/vs/workbench/workbench.main.ts
+++ b/src/vs/workbench/workbench.main.ts
@@ -157,7 +157,12 @@ import 'sql/parts/dashboard/widgets/insights/views/imageInsight.contribution';
import 'sql/parts/dashboard/widgets/insights/insightsWidget.contribution';
import 'sql/parts/dashboard/widgets/explorer/explorerWidget.contribution';
import 'sql/parts/dashboard/widgets/tasks/tasksWidget.contribution';
+import 'sql/parts/dashboard/widgets/webview/webviewWidget.contribution';
import 'sql/parts/dashboard/dashboardConfig.contribution';
+/* Tabs */
+import 'sql/parts/dashboard/tabs/dashboardWebviewTab.contribution';
+import 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
+import 'sql/parts/dashboard/common/dashboardTab.contribution';
/* Tasks */
import 'sql/workbench/common/actions.contribution';
/* Extension Host */