Code Layering dashboard (#4883)

* move dashboard to workbench

* revert xlf file changes

* 💄

* 💄

* add back removed functions
This commit is contained in:
Anthony Dresser
2019-04-09 00:26:57 -07:00
committed by GitHub
parent 9e9164c4ee
commit 8bdcc3267a
145 changed files with 543 additions and 535 deletions

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* 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/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { registerContainer, generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/workbench/parts/dashboard/containers/dashboardNavSection.contribution';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
import { WEBVIEW_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWebviewContainer.contribution';
const containerTypes = [
WIDGETS_CONTAINER,
GRID_CONTAINER,
WEBVIEW_CONTAINER,
NAV_SECTION
];
export type IUserFriendlyIcon = string | { light: string; dark: string; };
export interface IDashboardContainerContrib {
id: string;
container: object;
}
const containerSchema: IJSONSchema = {
type: 'object',
properties: {
id: {
type: 'string',
description: localize('azdata.extension.contributes.dashboard.container.id', "Unique identifier for this container.")
},
container: {
description: localize('azdata.extension.contributes.dashboard.container.container', "The container that will be displayed in the tab."),
type: 'object',
properties: generateContainerTypeSchemaProperties()
}
}
};
const containerContributionSchema: IJSONSchema = {
description: localize('azdata.extension.contributes.containers', "Contributes a single or multiple dashboard containers for users to add to their dashboard."),
oneOf: [
containerSchema,
{
type: 'array',
items: containerSchema
}
]
};
ExtensionsRegistry.registerExtensionPoint<IDashboardContainerContrib | IDashboardContainerContrib[]>({ extensionPoint: 'dashboard.containers', jsonSchema: containerContributionSchema }).setHandler(extensions => {
function handleCommand(dashboardContainer: IDashboardContainerContrib, extension: IExtensionPointUser<any>) {
const { id, container } = dashboardContainer;
if (!id) {
extension.collector.error(localize('dashboardContainer.contribution.noIdError', 'No id in dashboard container specified for extension.'));
return;
}
if (!container) {
extension.collector.error(localize('dashboardContainer.contribution.noContainerError', 'No container in dashboard container specified for extension.'));
return;
}
if (Object.keys(container).length !== 1) {
extension.collector.error(localize('dashboardContainer.contribution.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.'));
return;
}
let result = true;
const containerkey = Object.keys(container)[0];
const containerValue = Object.values(container)[0];
const containerTypeFound = containerTypes.find(c => (c === containerkey));
if (!containerTypeFound) {
extension.collector.error(localize('dashboardTab.contribution.unKnownContainerType', 'Unknown container type defines in dashboard container for extension.'));
return;
}
switch (containerkey) {
case WIDGETS_CONTAINER:
result = validateWidgetContainerContribution(extension, containerValue);
break;
case GRID_CONTAINER:
result = validateGridContainerContribution(extension, containerValue);
break;
case NAV_SECTION:
result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
break;
}
if (result) {
registerContainer({ id, container });
}
}
for (const extension of extensions) {
const { value } = extension;
if (Array.isArray<IDashboardContainerContrib>(value)) {
for (const command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardControlHostContainer';
import { Component, forwardRef, Input, AfterContentInit, ViewChild, OnChanges } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { ControlHostContent } from 'sql/workbench/parts/dashboard/contents/controlHostContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({
selector: 'dashboard-controlhost-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardControlHostContainer) }],
template: `
<controlhost-content [webviewId]="tab.id">
</controlhost-content>
`
})
export class DashboardControlHostContainer extends DashboardTab implements AfterContentInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(ControlHostContent) private _hostContent: ControlHostContent;
constructor() {
super();
}
ngAfterContentInit(): void {
this._register(this._hostContent.onResize(() => {
this._onResize.fire();
}));
const container = <any>this.tab.container;
if (container['controlhost-container'] && container['controlhost-container'].type) {
this._hostContent.setControlType(container['controlhost-container'].type);
}
}
public layout(): void {
this._hostContent.layout();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public refresh(): void {
this._hostContent.refresh();
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const CONTROLHOST_CONTAINER = 'controlhost-container';
let webviewSchema: IJSONSchema = {
type: 'null',
description: nls.localize('dashboard.container.controlhost', "The controlhost that will be displayed in this tab."),
default: null
};
registerContainerType(CONTROLHOST_CONTAINER, webviewSchema);
registerNavSectionContainerType(CONTROLHOST_CONTAINER, 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-controlhost-container {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardErrorContainer';
import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Event, Emitter } from 'vs/base/common/event';
import * as nls from 'vs/nls';
@Component({
selector: 'dashboard-error-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardErrorContainer) }],
template: `
<div class="error-container">
<div class="icon globalError">
</div>
<div class="error-message" #errorMessage>
</div>
</div>
`
})
export class DashboardErrorContainer extends DashboardTab implements AfterViewInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild('errorMessage', { read: ElementRef }) private _errorMessageContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
ngAfterViewInit() {
const errorMessage = this._errorMessageContainer.nativeElement as HTMLElement;
errorMessage.innerText = nls.localize('dashboardNavSection_loadTabError', 'The "{0}" section has invalid content. Please contact extension owner.', this.tab.title);
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return false;
}
public layout() {
}
public refresh(): void {
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-error-container {
height: 100%;
width: 100%;
}
dashboard-error-container .error-container {
padding: 6px;
background: #D02E00;
color: white;
}
dashboard-error-container .error-container .icon.globalError {
height: 16px;
width: 16px;
float: left;
padding-right: 15px;
}

View File

@@ -0,0 +1,28 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div>
<table class="grid-table">
<tr *ngFor="let row of rows" class="grid-table-row">
<ng-container *ngFor="let col of cols">
<ng-container *ngIf="getContent(row,col) !== undefined">
<td class="table-cell" [colSpan]=getColspan(row,col) [rowSpan]=getRowspan(row,col)
[width]="getWidgetWidth(row,col)" [height]="getWidgetHeight(row,col)">
<dashboard-widget-wrapper *ngIf="isWidget(row,col)" [_config]="getWidgetContent(row,col)"
style="position:absolute;" [style.width]="getWidgetWidth(row,col)"
[style.height]="getWidgetHeight(row,col)">
</dashboard-widget-wrapper>
<webview-content *ngIf="isWebview(row,col)" [webviewId]="getWebviewId(row,col)">
</webview-content>
</td>
</ng-container>
</ng-container>
</tr>
</table>
</div>

View File

@@ -0,0 +1,221 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardGridContainer';
import { Component, Inject, Input, forwardRef, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { TabConfig, WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { DashboardWidgetWrapper } from 'sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { WebviewContent } from 'sql/workbench/parts/dashboard/contents/webviewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Event, Emitter } from 'vs/base/common/event';
export interface GridCellConfig {
id?: string;
row?: number;
col?: number;
colspan?: string | number;
rowspan?: string | number;
}
export interface GridWidgetConfig extends GridCellConfig, WidgetConfig {
}
export interface GridWebviewConfig extends GridCellConfig {
webview: {
id?: string;
};
}
@Component({
selector: 'dashboard-grid-container',
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/containers/dashboardGridContainer.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardGridContainer) }]
})
export class DashboardGridContainer extends DashboardTab implements OnDestroy {
@Input() private tab: TabConfig;
private _contents: GridCellConfig[];
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
private cellWidth: number = 270;
private cellHeight: number = 270;
protected SKELETON_WIDTH = 5;
protected rows: number[];
protected cols: number[];
protected getContent(row: number, col: number): GridCellConfig {
const widget = this._contents.filter(w => w.row === row && w.col === col);
return widget ? widget[0] : undefined;
}
protected getWidgetContent(row: number, col: number): GridWidgetConfig {
const content = this.getContent(row, col);
if (content) {
const widgetConfig = <GridWidgetConfig>content;
if (widgetConfig && widgetConfig.widget) {
return widgetConfig;
}
}
return undefined;
}
protected getWebviewContent(row: number, col: number): GridWebviewConfig {
const content = this.getContent(row, col);
if (content) {
const webviewConfig = <GridWebviewConfig>content;
if (webviewConfig && webviewConfig.webview) {
return webviewConfig;
}
}
return undefined;
}
protected isWidget(row: number, col: number): boolean {
const widgetConfig = this.getWidgetContent(row, col);
return widgetConfig !== undefined;
}
protected isWebview(row: number, col: number): boolean {
const webview = this.getWebviewContent(row, col);
return webview !== undefined;
}
protected getWebviewId(row: number, col: number): string {
const widgetConfig = this.getWebviewContent(row, col);
if (widgetConfig && widgetConfig.webview) {
return widgetConfig.webview.id;
}
return undefined;
}
protected getColspan(row: number, col: number): string {
const content = this.getContent(row, col);
let colspan: string = '1';
if (content && content.colspan) {
colspan = this.convertToNumber(content.colspan, this.cols.length).toString();
}
return colspan;
}
protected getRowspan(row: number, col: number): string {
const content = this.getContent(row, col);
if (content && (content.rowspan)) {
return this.convertToNumber(content.rowspan, this.rows.length).toString();
} else {
return '1';
}
}
protected getWidgetWidth(row: number, col: number): string {
const colspan = this.getColspan(row, col);
const columnCount = this.convertToNumber(colspan, this.cols.length);
return columnCount * this.cellWidth + 'px';
}
protected getWidgetHeight(row: number, col: number): string {
const rowspan = this.getRowspan(row, col);
const rowCount = this.convertToNumber(rowspan, this.rows.length);
return rowCount * this.cellHeight + 'px';
}
private convertToNumber(value: string | number, maxNumber: number): number {
if (!value) {
return 1;
}
if (value === '*') {
return maxNumber;
}
try {
return +value;
} catch {
return 1;
}
}
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
@ViewChildren(WebviewContent) private _webViews: QueryList<WebviewContent>;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
protected init() {
}
ngOnInit() {
if (this.tab.container) {
this._contents = Object.values(this.tab.container)[0];
this._contents.forEach(widget => {
if (!widget.row) {
widget.row = 0;
}
if (!widget.col) {
widget.col = 0;
}
if (!widget.colspan) {
widget.colspan = '1';
}
if (!widget.rowspan) {
widget.rowspan = '1';
}
});
this.rows = this.createIndexes(this._contents.map(w => w.row));
this.cols = this.createIndexes(this._contents.map(w => w.col));
}
}
private createIndexes(indexes: number[]) {
const max = Math.max(...indexes) + 1;
return Array(max).fill(0).map((x, i) => i);
}
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();
});
}
if (this._webViews) {
this._webViews.forEach(item => {
item.layout();
});
}
}
public refresh(): void {
if (this._widgets) {
this._widgets.forEach(item => {
item.refresh();
});
}
}
public enableEdit(): void {
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { generateDashboardGridLayoutSchema } from 'sql/workbench/parts/dashboard/pages/dashboardPageContribution';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const GRID_CONTAINER = 'grid-container';
let gridContainersSchema: IJSONSchema = {
type: 'array',
description: nls.localize('dashboard.container.gridtab.items', "The list of widgets or webviews that will be displayed in this tab."),
items: generateDashboardGridLayoutSchema(undefined, true)
};
registerContainerType(GRID_CONTAINER, gridContainersSchema);
registerNavSectionContainerType(GRID_CONTAINER, gridContainersSchema);
export function validateGridContainerContribution(extension: IExtensionPointUser<any>, gridConfigs: object[]): boolean {
let result = true;
gridConfigs.forEach(widgetConfig => {
const allKeys = Object.keys(widgetConfig);
const widgetOrWebviewKey = allKeys.find(key => key === 'widget' || key === 'webview');
if (!widgetOrWebviewKey) {
result = false;
extension.collector.error(nls.localize('gridContainer.invalidInputs', 'widgets or webviews are expected inside widgets-container for extension.'));
return;
}
});
return result;
}

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* 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: auto;
width: auto;
}
.grid-table {
border-spacing: 5px;
}
.grid-table-row {
width: auto;
clear: both;
}
.table-cell {
vertical-align: top;
padding: 7px;
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardHomeContainer';
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild, ContentChild } from '@angular/core';
import { DashboardWidgetContainer } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.component';
import { WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularEventType, IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
import { DashboardWidgetWrapper } from 'sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums';
@Component({
selector: 'dashboard-home-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardHomeContainer) }],
template: `
<div class="fullsize" style="display: flex; flex-direction: column">
<div scrollable [horizontalScroll]="ScrollbarVisibility.Hidden" [verticalScroll]="ScrollbarVisibility.Auto">
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
</dashboard-widget-wrapper>
<widget-content style="flex: 1" [scrollContent]="false" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
</div>
</div>
`
})
export class DashboardHomeContainer extends DashboardWidgetContainer {
@Input() private properties: WidgetConfig;
@ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper;
@ContentChild(ScrollableDirective) private _scrollable;
private ScrollbarVisibility = ScrollbarVisibility;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: DashboardServiceInterface,
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(IAngularEventingService) private angularEventingService: IAngularEventingService
) {
super(_cd);
}
ngAfterContentInit() {
const collapsedVal = this.dashboardService.getSettings<string>(`${this.properties.context}.properties`);
if (collapsedVal === 'collapsed') {
this._propertiesClass.collapsed = true;
}
this.angularEventingService.onAngularEvent(this.dashboardService.getUnderlyingUri(), event => {
if (event.event === AngularEventType.COLLAPSE_WIDGET && this._propertiesClass && event.payload === this._propertiesClass.guid) {
this._propertiesClass.collapsed = !this._propertiesClass.collapsed;
this._cd.detectChanges();
this._configurationService.updateValue(`dashboard.${this.properties.context}.properties`,
this._propertiesClass.collapsed ? 'collapsed' : true, ConfigurationTarget.USER);
}
});
}
public layout() {
super.layout();
if (this._scrollable) {
this._scrollable.layout();
}
}
}

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-home-tab {
height: 100%;
width: 100%;
display: block;
}

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.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dashboardWebviewContainer';
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({
selector: 'dashboard-modelview-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardModelViewContainer) }],
template: `
<modelview-content [modelViewId]="tab.id">
</modelview-content>
`
})
export class DashboardModelViewContainer extends DashboardTab implements AfterContentInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent;
constructor() {
super();
}
ngAfterContentInit(): void {
this._register(this._modelViewContent.onResize(() => {
this._onResize.fire();
}));
}
public layout(): void {
this._modelViewContent.layout();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public refresh(): void {
// no op
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const MODELVIEW_CONTAINER = 'modelview-container';
let modelviewSchema: IJSONSchema = {
type: 'null',
description: nls.localize('dashboard.container.modelview', "The model-backed view that will be displayed in this tab."),
default: null
};
registerContainerType(MODELVIEW_CONTAINER, modelviewSchema);
registerNavSectionContainerType(MODELVIEW_CONTAINER, modelviewSchema);

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-modelview-container {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,23 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<panel [options]="panelOpt" style="flex: 1 1 auto;" class="dashboard-panel">
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize"
[identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions" [iconClass]="tab.iconClass">
<ng-template>
<dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab">
</dashboard-webview-container>
<dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab">
</dashboard-widget-container>
<dashboard-modelview-container *ngIf="getContentType(tab) === 'modelview-container'" [tab]="tab">
</dashboard-modelview-container>
<dashboard-grid-container *ngIf="getContentType(tab) === 'grid-container'" [tab]="tab">
</dashboard-grid-container>
<dashboard-error-container *ngIf="getContentType(tab) === 'error-container'" [tab]="tab">
</dashboard-error-container>
</ng-template>
</tab>
</panel>

View File

@@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardNavSection';
import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, OnChanges, AfterContentInit } from '@angular/core';
import { CommonServiceInterface, SingleConnectionManagementService } from 'sql/services/common/commonServiceInterface.service';
import { WidgetConfig, TabConfig, NavSectionConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { DashboardTab, IConfigModifierCollection } from 'sql/workbench/parts/dashboard/common/interfaces';
import { WIDGETS_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
import * as dashboardHelper from 'sql/workbench/parts/dashboard/common/dashboardHelper';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@Component({
selector: 'dashboard-nav-section',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardNavSection) }],
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/containers/dashboardNavSection.component.html'))
})
export class DashboardNavSection extends DashboardTab implements OnDestroy, OnChanges, AfterContentInit, IConfigModifierCollection {
@Input() private tab: TabConfig;
protected tabs: Array<TabConfig> = [];
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
// tslint:disable-next-line:no-unused-variable
private readonly panelOpt: IPanelOptions = {
layout: NavigationBarLayout.vertical
};
// a set of config modifiers
private readonly _configModifiers: Array<(item: Array<WidgetConfig>, collection: IConfigModifierCollection, context: string) => Array<WidgetConfig>> = [
dashboardHelper.removeEmpty,
dashboardHelper.initExtensionConfigs,
dashboardHelper.addProvider,
dashboardHelper.addEdition,
dashboardHelper.addContext,
dashboardHelper.filterConfigs
];
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>, originalConfig: Array<WidgetConfig>) => Array<WidgetConfig>> = [
dashboardHelper.validateGridConfig
];
@ViewChildren(TabChild) private _tabs: QueryList<DashboardTab>;
@ViewChild(PanelComponent) private _panel: PanelComponent;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
ngOnChanges() {
this.tabs = [];
let navSectionContainers: NavSectionConfig[] = [];
if (this.tab.container) {
navSectionContainers = Object.values(this.tab.container)[0];
let hasIcon = true;
navSectionContainers.forEach(navSection => {
if (!navSection.iconClass) {
hasIcon = false;
}
});
this.panelOpt.showIcon = hasIcon;
this.loadNewTabs(navSectionContainers);
}
}
ngAfterContentInit(): void {
if (this._tabs) {
this._tabs.forEach(tabContent => {
this._register(tabContent.onResize(() => {
this._onResize.fire();
}));
});
}
}
ngOnDestroy() {
this.dispose();
}
private loadNewTabs(dashboardTabs: NavSectionConfig[]) {
if (dashboardTabs && dashboardTabs.length > 0) {
dashboardTabs.map(v => {
const containerResult = dashboardHelper.getDashboardContainer(v.container);
if (!containerResult.result) {
return { id: v.id, title: v.title, container: { 'error-container': undefined } };
}
const key = Object.keys(containerResult.container)[0];
if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) {
let configs = <WidgetConfig[]>Object.values(containerResult.container)[0];
this._configModifiers.forEach(cb => {
configs = cb.apply(this, [configs, this, this.tab.context]);
});
this._gridModifiers.forEach(cb => {
configs = cb.apply(this, [configs]);
});
if (key === WIDGETS_CONTAINER) {
return { id: v.id, title: v.title, container: { 'widgets-container': configs }, iconClass: v.iconClass };
} else {
return { id: v.id, title: v.title, container: { 'grid-container': configs }, iconClass: v.iconClass };
}
}
return { id: v.id, title: v.title, container: containerResult.container, iconClass: v.iconClass };
}).map(v => {
const config = v as TabConfig;
config.context = this.tab.context;
config.editable = false;
config.canClose = false;
this.addNewTab(config);
return config;
});
}
}
private addNewTab(tab: TabConfig): void {
const existedTab = this.tabs.find(i => i.id === tab.id);
if (!existedTab) {
this.tabs.push(tab);
this._cd.detectChanges();
}
}
protected getContentType(tab: TabConfig): string {
return tab.container ? Object.keys(tab.container)[0] : '';
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public layout() {
const activeTabId = this._panel.getActiveTab;
const localtab = this._tabs.find(i => i.id === activeTabId);
this._cd.detectChanges();
localtab.layout();
}
public refresh(): void {
if (this._tabs) {
this._tabs.forEach(tabContent => {
tabContent.refresh();
});
}
}
public enableEdit(): void {
if (this._tabs) {
this._tabs.forEach(tabContent => {
tabContent.enableEdit();
});
}
}
public get connectionManagementService(): SingleConnectionManagementService {
return this.dashboardService.connectionManagementService;
}
public get contextKeyService(): IContextKeyService {
return this.dashboardService.scopedContextKeyService;
}
}

View File

@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { join } from 'path';
import { createCSSRule } from 'vs/base/browser/dom';
import { URI } from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { NavSectionConfig, IUserFriendlyIcon } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
export const NAV_SECTION = 'nav-section';
const navSectionContainerSchema: IJSONSchema = {
type: 'object',
properties: {
id: {
type: 'string',
description: nls.localize('dashboard.container.left-nav-bar.id', "Unique identifier for this nav section. Will be passed to the extension for any requests.")
},
icon: {
description: nls.localize('dashboard.container.left-nav-bar.icon', '(Optional) Icon which is used to represent this nav section in the UI. Either a file path or a themeable configuration'),
anyOf: [{
type: 'string'
},
{
type: 'object',
properties: {
light: {
description: nls.localize('dashboard.container.left-nav-bar.icon.light', 'Icon path when a light theme is used'),
type: 'string'
},
dark: {
description: nls.localize('dashboard.container.left-nav-bar.icon.dark', 'Icon path when a dark theme is used'),
type: 'string'
}
}
}]
},
title: {
type: 'string',
description: nls.localize('dashboard.container.left-nav-bar.title', "Title of the nav section to show the user.")
},
container: {
description: nls.localize('dashboard.container.left-nav-bar.container', "The container that will be displayed in this nav section."),
type: 'object',
properties: generateNavSectionContainerTypeSchemaProperties()
}
}
};
const NavSectionSchema: IJSONSchema = {
type: 'array',
description: nls.localize('dashboard.container.left-nav-bar', "The list of dashboard containers that will be displayed in this navigation section."),
items: navSectionContainerSchema
};
registerContainerType(NAV_SECTION, NavSectionSchema);
function isValidIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): boolean {
if (typeof icon === 'undefined') {
return false;
}
if (typeof icon === 'string') {
return true;
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
return true;
}
extension.collector.error(nls.localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
return false;
}
const ids = new IdGenerator('contrib-dashboardNavSection-icon-');
function createCSSRuleForIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): string {
let iconClass: string;
if (icon) {
iconClass = ids.nextId();
if (typeof icon === 'string') {
const path = join(extension.description.extensionLocation.fsPath, icon);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(path).toString()}")`);
} else {
const light = join(extension.description.extensionLocation.fsPath, icon.light);
const dark = join(extension.description.extensionLocation.fsPath, icon.dark);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(light).toString()}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(dark).toString()}")`);
}
}
return iconClass;
}
export function validateNavSectionContributionAndRegisterIcon(extension: IExtensionPointUser<any>, navSectionConfigs: NavSectionConfig[]): boolean {
let result = true;
navSectionConfigs.forEach(section => {
if (!section.title) {
result = false;
extension.collector.error(nls.localize('navSection.missingTitle_error', 'No title in nav section specified for extension.'));
}
if (!section.container) {
result = false;
extension.collector.error(nls.localize('navSection.missingContainer_error', 'No container in nav section specified for extension.'));
}
if (Object.keys(section.container).length !== 1) {
result = false;
extension.collector.error(nls.localize('navSection.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.'));
}
if (isValidIcon(section.icon, extension)) {
section.iconClass = createCSSRuleForIcon(section.icon, extension);
}
const containerKey = Object.keys(section.container)[0];
const containerValue = Object.values(section.container)[0];
switch (containerKey) {
case WIDGETS_CONTAINER:
result = result && validateWidgetContainerContribution(extension, containerValue);
break;
case GRID_CONTAINER:
result = result && validateGridContainerContribution(extension, containerValue);
break;
case NAV_SECTION:
result = false;
extension.collector.error(nls.localize('navSection.invalidContainer_error', 'NAV_SECTION within NAV_SECTION is an invalid container for extension.'));
break;
}
});
return result;
}

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-nav-section {
height: 100%;
width: 100%;
display: block;
}

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.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dashboardWebviewContainer';
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { WebviewContent } from 'sql/workbench/parts/dashboard/contents/webviewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({
selector: 'dashboard-webview-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWebviewContainer) }],
template: `
<webview-content [webviewId]="tab.id">
</webview-content>
`
})
export class DashboardWebviewContainer extends DashboardTab implements AfterContentInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(WebviewContent) private _webviewContent: WebviewContent;
constructor() {
super();
}
ngAfterContentInit(): void {
this._register(this._webviewContent.onResize(() => {
this._onResize.fire();
}));
}
public layout(): void {
this._webviewContent.layout();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public refresh(): void {
// no op
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const WEBVIEW_CONTAINER = 'webview-container';
let webviewSchema: IJSONSchema = {
type: 'null',
description: nls.localize('dashboard.container.webview', "The webview that will be displayed in this tab."),
default: null
};
registerContainerType(WEBVIEW_CONTAINER, webviewSchema);
registerNavSectionContainerType(WEBVIEW_CONTAINER, 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-container {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardWidgetContainer';
import { Component, Inject, Input, forwardRef, ViewChild, OnDestroy, ChangeDetectorRef, AfterContentInit } from '@angular/core';
import { TabConfig, WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { WidgetContent } from 'sql/workbench/parts/dashboard/contents/widgetContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Event, Emitter } from 'vs/base/common/event';
@Component({
selector: 'dashboard-widget-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWidgetContainer) }],
template: `
<widget-content [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
`
})
export class DashboardWidgetContainer extends DashboardTab implements OnDestroy, AfterContentInit {
@Input() protected tab: TabConfig;
protected widgets: WidgetConfig[];
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(WidgetContent) protected _widgetContent: WidgetContent;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
ngOnInit() {
if (this.tab.container) {
this.widgets = Object.values(this.tab.container)[0];
this._cd.detectChanges();
}
}
ngAfterContentInit(): void {
this._register(this._widgetContent.onResize(() => {
this._onResize.fire();
}));
}
ngOnDestroy() {
this.dispose();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public layout() {
this._widgetContent.layout();
}
public refresh(): void {
this._widgetContent.refresh();
}
public enableEdit(): void {
this._widgetContent.enableEdit();
}
}

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { generateDashboardWidgetSchema } from 'sql/workbench/parts/dashboard/pages/dashboardPageContribution';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const WIDGETS_CONTAINER = 'widgets-container';
const widgetsSchema: IJSONSchema = {
type: 'array',
description: nls.localize('dashboard.container.widgets', "The list of widgets that will be displayed in this tab."),
items: generateDashboardWidgetSchema(undefined, true)
};
registerContainerType(WIDGETS_CONTAINER, widgetsSchema);
registerNavSectionContainerType(WIDGETS_CONTAINER, widgetsSchema);
export function validateWidgetContainerContribution(extension: IExtensionPointUser<any>, WidgetConfigs: object[]): boolean {
let result = true;
WidgetConfigs.forEach(widgetConfig => {
const allKeys = Object.keys(widgetConfig);
const widgetKey = allKeys.find(key => key === 'widget');
if (!widgetKey) {
result = false;
extension.collector.error(nls.localize('widgetContainer.invalidInputs', 'The list of widgets is expected inside widgets-container for extension.'));
}
});
return result;
}

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-widget-container {
height: 100%;
width: 100%;
}