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 @@ +new_16x16 \ 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 @@ +new_inverse_16x16 \ 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 @@ +pin \ 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 @@ +pin_inverse \ 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 @@ +unpin \ 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 @@ +unpin_inverse \ 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 */