Merge dashboardCommandCenter to master (#697)

* Initial work of adding tab in the dashboard (#526)

* refactor dashboard to have the home tab

* formatting

* fix grid layout issue

* fix initailize issue in database dashboard

* Add action bar to the panel and add close tab to the dashboard (#562)

* add action bar to the panel and add close tab to the dashboard

* formatting

* Tab contribution (#564)

* added contrib

* disabled edit for extensions; fixed new name for insights contrib

* fix merge issue

* move file

* formatting

* fix builds

* moving imports

* Expand on tab contrib (#581)

* added contrib

* disabled edit for extensions; fixed new name for insights contrib

* fix merge issue

* move file

* formatting

* fix builds

* adding to contrib

* updated contrib

* format

* moving imports

* updated contribution to map to current design

* implemented actually using provider and edition filtering

*  Refactor and fix issues in close tab and add the placeholder for pin tab (#588)

* refactor and fix issues in close tab and add the placeholder for pin tab

* formatting

* remove the redundant code

* add clear all tabs in dashboard page init

* Initial work for adding a feature tab dialog (#594)

* initial work for add new dashboard tab

* formatting

* fix add panel action issue

* fix breaking change

* fix issues and tab and panels

* formatting

* minor fix

* address comments

* Add tab status to add extension tab dialog (#610)

* add tab status to add extension tab dialog

* add tab status to add extension tab dialog

* rename add feature tab action

* address comments

* Webview widget (#618)

* getting closer

* webview widget now works

* fix problem with rerendering webview

* formatting

* ensure that webview only shows up for extensions

* formatting

* comments

* fix more compile issues

* Change dashboard page init (#640)

* changed init of serverpage

* formatting

* Webview tab (#638)

* getting closer

* webview widget now works

* fix problem with rerendering webview

* formatting

* ensure that webview only shows up for extensions

* formatting

* comments

* fix more compile issues

* refacting stuff

* added inital webview tab

* piped through messaging and tested

* Implement pin/unpin feature and always on tabs (#629)

* implement pin/unpin feature

* fix issue where insight can't be loaded after reopen

* fix tab look and feel

* implement always show tabs

* make AddFeatureTabAction to track always show and pinned tabs

* formatting

* make dashboard tabs looks like the UX design

* load always show before pinned tab

* fix regression in panel for restore and connection dialog

* fix merge conflict

* don't worry about no widgets if its a webview (#656)

* expose the dashboard server info when a webview is rendering (#644)

* Fix few issues in dashboard command center (#655)

* fix reloading insight wigets and create new tab when there is no extension

* show possible tabIDs in the setting file

* formatting

* address comment

* fix import name

* fixes problem with size of webview widget being wrong (#654)

*  Refactor tab contribution to support content type (#685)

* refactor tab contribution to support content type

* formatting

* address comment

* fix rendering tab issue (#694)

* Add layout option to panel for supporting horizontal and vertical navigation bar  (#700)

* Add left navigation panel for inner tab in the dashboard

* add layout option in panel

* remove panel option in dashboard Page
This commit is contained in:
Abbie Petchtes
2018-02-15 10:27:47 -08:00
committed by GitHub
parent dfc212369a
commit b61fbc806b
72 changed files with 2535 additions and 372 deletions

View File

@@ -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<boolean> {
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<IDashboardTab>,
private _openedTabs: Array<IDashboardTab>,
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<boolean> {
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 = <IDashboardTab[]>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;
}
}
}

View File

@@ -5,14 +5,18 @@
*--------------------------------------------------------------------------------------------*/
-->
<div #scrollContainer style="height: 100%">
<div #scrollable style="position: relative">
<div #propertiesContainer>
<div #scrollable style="position: relative; display: flex; flex-direction: column; height: 100%">
<div style="flex: 0 0 auto" #propertiesContainer>
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="padding-left: 10px; padding-right: 10px; height: 90px; display: block">
</dashboard-widget-wrapper>
</div>
<div [ngGrid]="gridConfig">
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
</dashboard-widget-wrapper>
</div>
<panel style="flex: 1 1 auto; position: relative" class="dashboard-panel" (onTabChange)="handleTabChange($event)" (onTabClose)="handleTabClose($event)" [actions]="panelActions">
<tab *ngFor="let tab of tabs" [title]="tab.title" class="fullsize" [identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions">
<dashboard-webview-tab *ngIf="getContentType(tab) === 'webview-tab'" [tab]="tab">
</dashboard-webview-tab>
<dashboard-widget-tab *ngIf="getContentType(tab) === 'widgets-tab'" [tab]="tab">
</dashboard-widget-tab>
</tab>
</panel>
</div>
</div>

View File

@@ -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<IDashboardRegistry>(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) && (<any[]>value).every(elem => types.isNumber(elem));
}
/**
* Sorting function for dashboard widgets
* In order of priority;
* If neither have defined grid positions, they are equivalent
* If a has a defined grid position and b does not; a should come first
* If both have defined grid positions and have the same row; the one with the smaller col position should come first
* If both have defined grid positions but different rows (it doesn't really matter in this case) the lowers row should come first
*/
function configSorter(a, b): number {
if ((!a.gridItemConfig || !a.gridItemConfig.col)
&& (!b.gridItemConfig || !b.gridItemConfig.col)) {
return 0;
} else if (!a.gridItemConfig || !a.gridItemConfig.col) {
return 1;
} else if (!b.gridItemConfig || !b.gridItemConfig.col) {
return -1;
} else if (a.gridItemConfig.row === b.gridItemConfig.row) {
if (a.gridItemConfig.col < b.gridItemConfig.col) {
return -1;
}
if (a.gridItemConfig.col === b.gridItemConfig.col) {
return 0;
}
if (a.gridItemConfig.col > b.gridItemConfig.col) {
return 1;
}
} else {
if (a.gridItemConfig.row < b.gridItemConfig.row) {
return -1;
}
if (a.gridItemConfig.row === b.gridItemConfig.row) {
return 0;
}
if (a.gridItemConfig.row > b.gridItemConfig.row) {
return 1;
}
}
return void 0; // this should never be reached
}
@Component({
selector: 'dashboard-page',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html'))
@@ -90,43 +59,31 @@ function configSorter(a, b): number {
export abstract class DashboardPage extends Disposable implements OnDestroy {
protected SKELETON_WIDTH = 5;
protected widgets: Array<WidgetConfig> = [];
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<TabConfig> = [];
private _originalConfig: WidgetConfig[];
private _editDispose: Array<IDisposable> = [];
private _scrollableElement: ScrollableElement;
private _widgetConfigLocation: string;
private _propertiesConfigLocation: string;
protected panelActions: Action[];
private _tabsDispose: Array<IDisposable> = [];
private _pinnedTabs: Array<PinConfig> = [];
@ViewChild('properties') private _properties: DashboardWidgetWrapper;
@ViewChild(NgGrid) private _grid: NgGrid;
@ViewChild('scrollable', { read: ElementRef }) private _scrollable: ElementRef;
@ViewChild('scrollContainer', { read: ElementRef }) private _scrollContainer: ElementRef;
@ViewChild('propertiesContainer', { read: ElementRef }) private _propertiesContainer: ElementRef;
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
@ViewChildren(NgGridItem) private _items: QueryList<NgGridItem>;
@ViewChildren(DashboardTab) private _tabs: QueryList<DashboardTab>;
@ViewChild(PanelComponent) private _panel: PanelComponent;
private _editEnabled = new Emitter<boolean>();
public readonly editEnabled: Event<boolean> = 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<WidgetConfig>) => Array<WidgetConfig>> = [
@@ -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<WidgetConfig>) => Array<WidgetConfig>> = [
@@ -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<Array<WidgetConfig>>([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<Array<PinConfig>>([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 = <WidgetConfig[]>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<WidgetConfig> {
private filterConfigs<T extends { provider?: string | string[], edition?: number | number[] }>(config: T[]): Array<T> {
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 });
}
}

View File

@@ -8,3 +8,8 @@ dashboard-page {
width: 100%;
position: absolute;
}
dashboard-page .monaco-scrollable-element {
height: 100%;
width: 100%;
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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;
}

View File

@@ -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;
}
`);
}
});

View File

@@ -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<IDashboardTabContrib | IDashboardTabContrib[]>('dashboard.tabs', [], tabContributionSchema).setHandler(extensions => {
function handleCommand(tab: IDashboardTabContrib, extension: IExtensionPointUser<any>) {
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<IDashboardTabContrib>(value)) {
for (let command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});

View File

@@ -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<Action>;
@@ -32,6 +33,19 @@ export interface WidgetConfig {
padding?: string;
}
export interface TabConfig extends IDashboardTab {
context: string;
originalConfig: Array<WidgetConfig>;
editable: boolean;
canClose: boolean;
actions?: Array<Action>;
}
export interface PinConfig {
tabId: string;
isPinned?: boolean;
}
export abstract class DashboardWidget extends Disposable implements OnDestroy {
protected _config: WidgetConfig;

View File

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

View File

@@ -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<IDashboardWidget> } = {
'properties-widget': PropertiesWidgetComponent,
'explorer-widget': ExplorerWidget,
'tasks-widget': TasksWidget,
'insights-widget': InsightsWidget
'insights-widget': InsightsWidget,
'webview-widget': WebviewWidget
};
@Component({

View File

@@ -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'
};
}
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<void>;
public enableEdit(): void {
// no op
}
}

View File

@@ -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;
}
}

View File

@@ -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<IInsightRegistry>(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: '/' },

View File

@@ -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<IConfigurationRegistry>(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
}
};

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
export const INewDashboardTabDialogService = createDecorator<INewDashboardTabDialogService>('addNewDashboardTabService');
export interface INewDashboardTabDialogService {
_serviceBrand: any;
showDialog(dashboardTabs: Array<IDashboardTab>, openedTabs: Array<IDashboardTab>, uri: string): void;
}

View File

@@ -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;
}

View File

@@ -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<IDashboardUITab> {
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<IDashboardUITab, ExtensionListTemplate> {
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<IDashboardUITab>;
private _extensionTabView: FixedListView<IDashboardUITab>;
private _splitView: SplitView;
private _container: HTMLElement;
private _viewModel: NewDashboardTabViewModel;
// EVENTING ////////////////////////////////////////////////////////////
private _onAddTabs: Emitter<Array<IDashboardUITab>>;
public get onAddTabs(): Event<Array<IDashboardUITab>> { return this._onAddTabs.event; }
private _onCancel: Emitter<void>;
public get onCancel(): Event<void> { 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<IDashboardUITab[]>();
this._onCancel = new Emitter<void>();
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<IDashboardUITab>(extensionTabViewContainer, delegate, [extensionTabRenderer]);
this._extensionTabView = new FixedListView<IDashboardUITab>(
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 = <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<IDashboardTab>, openedTabs: Array<IDashboardTab>) {
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();
}
}

View File

@@ -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<IDashboardTab>, openedTabs: Array<IDashboardTab>, 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<IDashboardUITab>): void {
let selectedTabs = selectedUiTabs.map(tab => tab.tabConfig);
this._angularEventService.sendAngularEvent(this._uri, AngularEventType.NEW_TABS, { dashboardTabs: selectedTabs });
this._addNewTabDialog.close();
}
private handleOnCancel(): void { }
}

View File

@@ -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<IDashboardUITab[]>;
public get updateTabListEvent(): Event<IDashboardUITab[]> { return this._updateTabListEmitter.event; }
constructor() {
// Create event emitters
this._updateTabListEmitter = new Emitter<IDashboardUITab[]>();
}
public updateDashboardTabs(dashboardTabs: Array<IDashboardTab>, openedTabs: Array<IDashboardTab>) {
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);
}
}

View File

@@ -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 = <IDashboardWidgetRegistry>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';

View File

@@ -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);
}
}

View File

@@ -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 = <IDashboardWidgetRegistry>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: <IJSONSchema>{
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';

View File

@@ -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<boolean>;
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();
});
}
}

View File

@@ -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 = <IDashboardWidgetRegistry>Registry.as(Extensions.DashboardWidgetContribution);
import { generateDashboardWidgetSchema, generateDashboardTabSchema } from 'sql/parts/dashboard/pages/dashboardPageContribution';
export interface IPropertiesConfig {
edition: number | Array<number>;
@@ -111,53 +108,18 @@ let defaultVal = [
export const serverDashboardSettingSchema: IJSONSchema = {
type: ['array'],
description: nls.localize('dashboardServer', 'Customizes the server dashboard page'),
items: <IJSONSchema>{
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';
export const SERVER_DASHBOARD_PROPERTIES = 'dashboard.server.properties';
export const SERVER_DASHBOARD_TABS = 'dashboard.server.tabs';

View File

@@ -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<void>();
public readonly onUpdatePage: Event<void> = this._updatePage.event;
@@ -135,6 +142,15 @@ export class DashboardServiceInterface implements OnDestroy {
private _onDeleteWidget = new Emitter<string>();
public readonly onDeleteWidget: Event<string> = this._onDeleteWidget.event;
private _onPinUnpinTab = new Emitter<PinConfig>();
public readonly onPinUnpinTab: Event<PinConfig> = this._onPinUnpinTab.event;
private _onAddNewTabs = new Emitter<Array<IDashboardTab>>();
public readonly onAddNewTabs: Event<Array<IDashboardTab>> = this._onAddNewTabs.event;
private _onCloseTab = new Emitter<string>();
public readonly onCloseTab: Event<string> = 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);
}
}
}

View File

@@ -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<void>();
public readonly onResize: Event<void> = this._onResize.event;
private _onMessage = new Emitter<string>();
public readonly onMessage: Event<string> = 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();
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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.
*--------------------------------------------------------------------------------------------*/
-->
<div [ngGrid]="gridConfig" *ngIf="widgets" >
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
</dashboard-widget-wrapper>
</div>

View File

@@ -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<void>();
public readonly onResize: Event<void> = 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<IDisposable> = [];
@ViewChild(NgGrid) private _grid: NgGrid;
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
@ViewChildren(NgGridItem) private _items: QueryList<NgGridItem>;
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);
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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);

View File

@@ -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%;
}

View File

@@ -21,7 +21,7 @@ interface IInsightTypeContrib {
registerDashboardWidget('insights-widget', '', insightsSchema);
ExtensionsRegistry.registerExtensionPoint<IInsightTypeContrib | IInsightTypeContrib[]>('insights', [], insightsContribution).setHandler(extensions => {
ExtensionsRegistry.registerExtensionPoint<IInsightTypeContrib | IInsightTypeContrib[]>('dashboard.insights', [], insightsContribution).setHandler(extensions => {
function handleCommand(insight: IInsightTypeContrib, extension: IExtensionPointUser<any>) {

View File

@@ -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: '<div></div>'
})
export class WebviewWidget extends DashboardWidget implements IDashboardWidget, OnInit, IDashboardWebview {
private _id: string;
private _webview: Webview;
private _html: string;
private _onMessage = new Emitter<string>();
public readonly onMessage: Event<string> = 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();
}
}

View File

@@ -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 });

View File

@@ -1,5 +1,6 @@
.fullsize {
height: 100%;
width: 100%;
}
.headersVisible > .fullsize {