mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
29
src/sql/parts/dashboard/common/actions.ts
Normal file
29
src/sql/parts/dashboard/common/actions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
export class RefreshWidgetAction extends Action {
|
||||
|
||||
public static ID = 'refreshWidget';
|
||||
public static LABEL = nls.localize('refreshWidget', 'Refresh');
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
private refreshFn: () => void
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): TPromise<boolean> {
|
||||
try {
|
||||
this.refreshFn();
|
||||
return TPromise.as(true);
|
||||
} catch (e) {
|
||||
return TPromise.as(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/sql/parts/dashboard/common/componentHost.directive.ts
Normal file
13
src/sql/parts/dashboard/common/componentHost.directive.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Directive, ViewContainerRef, Inject, forwardRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[component-host]',
|
||||
})
|
||||
export class ComponentHostDirective {
|
||||
constructor( @Inject(forwardRef(() => ViewContainerRef)) public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
||||
16
src/sql/parts/dashboard/common/dashboardPage.component.html
Normal file
16
src/sql/parts/dashboard/common/dashboardPage.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div #propertyContainer>
|
||||
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="margin-left: 10px; margin-right: 10px; height: 90px; display: block">
|
||||
</dashboard-widget-wrapper>
|
||||
</div>
|
||||
<div>
|
||||
<div [ngGrid]="gridConfig">
|
||||
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
|
||||
</dashboard-widget-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
300
src/sql/parts/dashboard/common/dashboardPage.component.ts
Normal file
300
src/sql/parts/dashboard/common/dashboardPage.component.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ViewChild, ElementRef, ViewChildren, QueryList } from '@angular/core';
|
||||
import { NgGridConfig } from 'angular2-grid';
|
||||
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { Severity } from 'vs/platform/message/common/message';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
|
||||
*/
|
||||
function isNumberArray(value: any): value is number[] {
|
||||
return types.isArray(value) && (<any[]>value).every(elem => types.isNumber(elem));
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-page',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html')),
|
||||
host: {
|
||||
class: 'dashboard-page'
|
||||
}
|
||||
})
|
||||
export abstract class DashboardPage {
|
||||
|
||||
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.
|
||||
};
|
||||
private _themeDispose: IDisposable;
|
||||
|
||||
@ViewChild('propertyContainer', { read: ElementRef }) private propertyContainer: ElementRef;
|
||||
@ViewChild('properties') private _properties: DashboardWidgetWrapper;
|
||||
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
|
||||
|
||||
// a set of config modifiers
|
||||
private readonly _configModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
|
||||
this.removeEmpty,
|
||||
this.initExtensionConfigs,
|
||||
this.validateGridConfig,
|
||||
this.addProvider,
|
||||
this.addEdition,
|
||||
this.addContext,
|
||||
this.filterWidgets
|
||||
];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface
|
||||
) { }
|
||||
|
||||
protected init() {
|
||||
if (!this.dashboardService.connectionManagementService.connectionInfo) {
|
||||
this.dashboardService.messageService.show(Severity.Warning, nls.localize('missingConnectionInfo', 'No connection information could be found for this dashboard'));
|
||||
} else {
|
||||
let tempWidgets = this.dashboardService.getSettings(this.context).widgets;
|
||||
let properties = this.getProperties();
|
||||
this._configModifiers.forEach((cb) => {
|
||||
tempWidgets = cb.apply(this, [tempWidgets]);
|
||||
properties = properties ? cb.apply(this, [properties]) : undefined;
|
||||
});
|
||||
this.widgets = tempWidgets;
|
||||
this.propertiesWidget = properties ? properties[0] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected baseInit(): void {
|
||||
let self = this;
|
||||
self._themeDispose = self.dashboardService.themeService.onDidColorThemeChange((event: IColorTheme) => {
|
||||
self.updateTheme(event);
|
||||
});
|
||||
self.updateTheme(self.dashboardService.themeService.getColorTheme());
|
||||
|
||||
}
|
||||
|
||||
protected baseDestroy(): void {
|
||||
if (this._themeDispose) {
|
||||
this._themeDispose.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract propertiesWidget: WidgetConfig;
|
||||
protected abstract get context(): string;
|
||||
|
||||
/**
|
||||
* Returns a filtered version of the widgets passed based on edition and provider
|
||||
* @param config widgets to filter
|
||||
*/
|
||||
private filterWidgets(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
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);
|
||||
}).filter((item) => {
|
||||
if (item.edition) {
|
||||
if (edition) {
|
||||
return this.stringCompare(isNumberArray(item.edition) ? item.edition.map(item => item.toString()) : item.edition.toString(), edition.toString());
|
||||
} else {
|
||||
this.dashboardService.messageService.show(Severity.Warning, nls.localize('providerMissingEdition', 'Widget filters based on edition, but the provider does not have an edition'));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a compare against the val passed in and the compare string
|
||||
* @param val string or array of strings to compare the compare value to; if array, it will compare each val in the array
|
||||
* @param compare value to compare to
|
||||
*/
|
||||
private stringCompare(val: string | Array<string>, compare: string): boolean {
|
||||
if (types.isUndefinedOrNull(val)) {
|
||||
return true;
|
||||
} else if (types.isString(val)) {
|
||||
return val === compare;
|
||||
} else if (types.isStringArray(val)) {
|
||||
return val.some(item => item === compare);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provider to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add provider onto
|
||||
*/
|
||||
protected addProvider(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let provider = this.dashboardService.connectionManagementService.connectionInfo.providerId;
|
||||
return config.map((item) => {
|
||||
if (item.provider === undefined) {
|
||||
item.provider = provider;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the edition to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add edition onto
|
||||
*/
|
||||
protected addEdition(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let connectionInfo: ConnectionManagementInfo = this.dashboardService.connectionManagementService.connectionInfo;
|
||||
let edition = connectionInfo.serverInfo.engineEditionId;
|
||||
return config.map((item) => {
|
||||
if (item.edition === undefined) {
|
||||
item.edition = edition;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the context to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add context to
|
||||
*/
|
||||
protected addContext(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let context = this.context;
|
||||
return config.map((item) => {
|
||||
if (item.context === undefined) {
|
||||
item.context = context;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates configs to make sure nothing will error out and returns the modified widgets
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
protected removeEmpty(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.filter(widget => {
|
||||
return !types.isUndefinedOrNull(widget);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates configs to make sure nothing will error out and returns the modified widgets
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
protected validateGridConfig(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.map((widget) => {
|
||||
if (widget.gridItemConfig === undefined) {
|
||||
widget.gridItemConfig = {};
|
||||
}
|
||||
return widget;
|
||||
});
|
||||
}
|
||||
|
||||
protected initExtensionConfigs(configurations: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let widgetRegistry = <IInsightRegistry>Registry.as(Extensions.InsightContribution);
|
||||
return configurations.map((config) => {
|
||||
if (config.widget && Object.keys(config.widget).length === 1) {
|
||||
let key = Object.keys(config.widget)[0];
|
||||
let insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
|
||||
if (insightConfig !== undefined) {
|
||||
// Setup the default properties for this extension if needed
|
||||
if (!config.provider && insightConfig.provider) {
|
||||
config.provider = insightConfig.provider;
|
||||
}
|
||||
if (!config.name && insightConfig.name) {
|
||||
config.name = insightConfig.name;
|
||||
}
|
||||
if (!config.edition && insightConfig.edition) {
|
||||
config.edition = insightConfig.edition;
|
||||
}
|
||||
if (!config.gridItemConfig && insightConfig.gridItemConfig) {
|
||||
config.gridItemConfig = {
|
||||
sizex: insightConfig.gridItemConfig.x,
|
||||
sizey: insightConfig.gridItemConfig.y
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
private getProperties(): Array<WidgetConfig> {
|
||||
let properties = this.dashboardService.getSettings(this.context).properties;
|
||||
if (types.isUndefinedOrNull(properties)) {
|
||||
return [this.propertiesWidget];
|
||||
} else if (types.isBoolean(properties)) {
|
||||
return properties ? [this.propertiesWidget] : [];
|
||||
} else if (types.isArray(properties)) {
|
||||
return properties.map((item) => {
|
||||
let retVal = Object.assign({}, this.propertiesWidget);
|
||||
retVal.edition = item.edition;
|
||||
retVal.provider = item.provider;
|
||||
retVal.widget = { 'properties-widget': { properties: item.properties } };
|
||||
return retVal;
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let propsEl: HTMLElement = this.propertyContainer.nativeElement;
|
||||
let widgetShadowColor = theme.getColor(colors.widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
// Box shadow on bottom only.
|
||||
// The below settings fill the shadow across the whole page
|
||||
propsEl.style.boxShadow = `-5px 5px 10px -5px ${widgetShadowColor}`;
|
||||
propsEl.style.marginRight = '-10px';
|
||||
propsEl.style.marginBottom = '5px';
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(refreshConfig: boolean = false): void {
|
||||
if (refreshConfig) {
|
||||
this.init();
|
||||
if (this._properties) {
|
||||
this._properties.refresh();
|
||||
}
|
||||
} else {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/sql/parts/dashboard/common/dashboardWidget.ts
Normal file
38
src/sql/parts/dashboard/common/dashboardWidget.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { NgGridItemConfig } from 'angular2-grid';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
|
||||
export interface IDashboardWidget {
|
||||
actions: Array<Action>;
|
||||
actionsContext?: any;
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
export const WIDGET_CONFIG = new InjectionToken<WidgetConfig>('widget_config');
|
||||
|
||||
export interface WidgetConfig {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
context: string;
|
||||
provider: string | Array<string>;
|
||||
edition: number | Array<number>;
|
||||
gridItemConfig?: NgGridItemConfig;
|
||||
widget: Object;
|
||||
background_color?: string;
|
||||
border?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: string;
|
||||
padding?:string;
|
||||
}
|
||||
|
||||
export abstract class DashboardWidget {
|
||||
protected _config: WidgetConfig;
|
||||
|
||||
get actions(): Array<Action> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="display: flex; flex-flow: column; overflow: hidden; height: 100%; width: 100%">
|
||||
|
||||
<div #header>
|
||||
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex;flex: 0 0; padding: 3px 0 3px 0">
|
||||
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
|
||||
<span *ngIf="_config.name" style="margin-left: 5px">{{_config.name}}</span>
|
||||
<div *ngIf="_actions" (click)="onActionsClick($event)" style="float: right; margin-right: 5px; margin-left: auto; padding: 10px" class="icon toggle-more"></div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template component-host>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -0,0 +1,215 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
|
||||
import {
|
||||
Component, Input, Inject, forwardRef, ComponentFactoryResolver, AfterContentInit, ViewChild,
|
||||
ElementRef, OnInit, ChangeDetectorRef, OnDestroy, ReflectiveInjector, Injector, Type, ComponentRef
|
||||
} from '@angular/core';
|
||||
|
||||
import { ComponentHostDirective } from './componentHost.directive';
|
||||
import { WidgetConfig, WIDGET_CONFIG, IDashboardWidget } from './dashboardWidget';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import * as ACTIONS from './actions';
|
||||
|
||||
/* Widgets */
|
||||
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
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 { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
const componentMap: { [x: string]: Type<IDashboardWidget> } = {
|
||||
'properties-widget': PropertiesWidgetComponent,
|
||||
'explorer-widget': ExplorerWidget,
|
||||
'tasks-widget': TasksWidget,
|
||||
'insights-widget': InsightsWidget
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-widget-wrapper',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardWidgetWrapper.component.html'))
|
||||
})
|
||||
export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestroy {
|
||||
@Input() private _config: WidgetConfig;
|
||||
private _themeDispose: IDisposable;
|
||||
private _actions: Array<Action>;
|
||||
private _component: IDashboardWidget;
|
||||
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => Injector)) private _injector: Injector
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
let self = this;
|
||||
self._themeDispose = self._bootstrap.themeService.onDidColorThemeChange((event: IColorTheme) => {
|
||||
self.updateTheme(event);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.updateTheme(this._bootstrap.themeService.getColorTheme());
|
||||
this.loadWidget();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._themeDispose.dispose();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._component && this._component.refresh) {
|
||||
this._component.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private loadWidget(): void {
|
||||
if (Object.keys(this._config.widget).length !== 1) {
|
||||
error('Exactly 1 widget must be defined per space');
|
||||
return;
|
||||
}
|
||||
let key = Object.keys(this._config.widget)[0];
|
||||
let selector = this.getOrCreateSelector(key);
|
||||
if (selector === undefined) {
|
||||
error('Could not find selector', key);
|
||||
return;
|
||||
}
|
||||
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
|
||||
|
||||
let viewContainerRef = this.componentHost.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
let injector = ReflectiveInjector.resolveAndCreate([{ provide: WIDGET_CONFIG, useValue: this._config }], this._injector);
|
||||
let componentRef: ComponentRef<IDashboardWidget>;
|
||||
try {
|
||||
componentRef = viewContainerRef.createComponent(componentFactory, 0, injector);
|
||||
this._component = componentRef.instance;
|
||||
let actions = componentRef.instance.actions;
|
||||
if (componentRef.instance.refresh) {
|
||||
actions.push(this._bootstrap.instantiationService.createInstance(ACTIONS.RefreshWidgetAction, ACTIONS.RefreshWidgetAction.ID, ACTIONS.RefreshWidgetAction.LABEL, componentRef.instance.refresh));
|
||||
}
|
||||
if (actions !== undefined && actions.length > 0) {
|
||||
this._actions = actions;
|
||||
this._changeref.detectChanges();
|
||||
}
|
||||
} catch (e) {
|
||||
error('Error rendering widget', key, e);
|
||||
return;
|
||||
}
|
||||
let el = <HTMLElement>componentRef.location.nativeElement;
|
||||
|
||||
// set widget styles to conform to its box
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.flex = '1 1 auto';
|
||||
el.style.position = 'relative';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the selector for a given key, and if none is defined tries
|
||||
* to load it from the widget registry and configure as needed
|
||||
*
|
||||
* @private
|
||||
* @param {string} key
|
||||
* @returns {Type<IDashboardWidget>}
|
||||
* @memberof DashboardWidgetWrapper
|
||||
*/
|
||||
private getOrCreateSelector(key: string): Type<IDashboardWidget> {
|
||||
let selector = componentMap[key];
|
||||
if (selector === undefined) {
|
||||
// Load the widget from the registry
|
||||
let widgetRegistry = <IInsightRegistry>Registry.as(Extensions.InsightContribution);
|
||||
let insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
|
||||
if (insightConfig === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// Save the widget for future use
|
||||
selector = componentMap['insights-widget'];
|
||||
this._config.widget['insights-widget'] = insightConfig;
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
//tslint:disable-next-line
|
||||
private onActionsClick(e: any) {
|
||||
let anchor = { x: e.pageX + 1, y: e.pageY };
|
||||
this._bootstrap.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => TPromise.as(this._actions),
|
||||
getActionsContext: () => this._component.actionsContext
|
||||
});
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let el = <HTMLElement>this._ref.nativeElement;
|
||||
let headerEl: HTMLElement = this.header.nativeElement;
|
||||
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
|
||||
let backgroundColor = theme.getColor(colors.editorBackground, true);
|
||||
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
|
||||
// TODO: highContrastBorder does not exist, how to handle?
|
||||
let border = theme.getColor(colors.contrastBorder, true);
|
||||
|
||||
if (this._config.background_color) {
|
||||
backgroundColor = theme.getColor(this._config.background_color);
|
||||
}
|
||||
|
||||
if (this._config.border === 'none') {
|
||||
borderColor = undefined;
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
el.style.backgroundColor = backgroundColor.toString();
|
||||
}
|
||||
|
||||
if (foregroundColor) {
|
||||
el.style.color = foregroundColor.toString();
|
||||
}
|
||||
|
||||
let borderString = undefined;
|
||||
if (border) {
|
||||
borderString = border.toString();
|
||||
el.style.borderColor = borderString;
|
||||
el.style.borderWidth = '1px';
|
||||
el.style.borderStyle = 'solid';
|
||||
} else if (borderColor) {
|
||||
borderString = borderColor.toString();
|
||||
el.style.border = '3px solid ' + borderColor.toString();
|
||||
} else {
|
||||
el.style.border = 'none';
|
||||
}
|
||||
|
||||
if (borderString) {
|
||||
headerEl.style.backgroundColor = borderString;
|
||||
} else {
|
||||
headerEl.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
if (this._config.fontSize) {
|
||||
headerEl.style.fontSize = this._config.fontSize;
|
||||
}
|
||||
if (this._config.fontWeight) {
|
||||
headerEl.style.fontWeight = this._config.fontWeight;
|
||||
}
|
||||
if (this._config.padding) {
|
||||
headerEl.style.padding = this._config.padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/sql/parts/dashboard/common/interfaces.ts
Normal file
14
src/sql/parts/dashboard/common/interfaces.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export enum Conditional {
|
||||
'equals',
|
||||
'notEquals',
|
||||
'greaterThanOrEquals',
|
||||
'greaterThan',
|
||||
'lessThanOrEquals',
|
||||
'lessThan',
|
||||
'always'
|
||||
};
|
||||
12
src/sql/parts/dashboard/dashboard.component.html
Normal file
12
src/sql/parts/dashboard/dashboard.component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: scroll; width: 100%; height: 100%">
|
||||
<div #header style="margin-bottom: 5px">
|
||||
<breadcrumb></breadcrumb>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
58
src/sql/parts/dashboard/dashboard.component.ts
Normal file
58
src/sql/parts/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { DashboardServiceInterface } from './services/dashboardServiceInterface.service';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
|
||||
export const DASHBOARD_SELECTOR: string = 'dashboard-component';
|
||||
|
||||
@Component({
|
||||
selector: DASHBOARD_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./dashboard.component.html'))
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
private _subs: Array<IDisposable> = new Array();
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrapService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
let self = this;
|
||||
self._subs.push(self._bootstrapService.themeService.onDidColorThemeChange(e => self.updateTheme(e)));
|
||||
self.updateTheme(self._bootstrapService.themeService.getColorTheme());
|
||||
let profile: IConnectionProfile = self._bootstrapService.getOriginalConnectionProfile();
|
||||
if (profile && (!profile.databaseName || Utils.isMaster(profile))) {
|
||||
// Route to the server page as this is the default database
|
||||
self._router.navigate(['server-dashboard']);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._subs.forEach((value) => {
|
||||
value.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let headerEl = <HTMLElement> this.header.nativeElement;
|
||||
headerEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
headerEl.style.borderBottomWidth = '1px';
|
||||
headerEl.style.borderBottomStyle = 'solid';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
106
src/sql/parts/dashboard/dashboard.module.ts
Normal file
106
src/sql/parts/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Inject, NgModule, forwardRef, ApplicationRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule, Routes, UrlSerializer } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgGridModule } from 'angular2-grid';
|
||||
import { ChartsModule } from 'ng2-charts/ng2-charts';
|
||||
|
||||
import CustomUrlSerializer from 'sql/common/urlSerializer';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
/* Services */
|
||||
import { BreadcrumbService } from 'sql/parts/dashboard/services/breadcrumb.service';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
|
||||
/* Directives */
|
||||
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
|
||||
|
||||
/* Base Components */
|
||||
import { DashboardComponent, DASHBOARD_SELECTOR } from 'sql/parts/dashboard/dashboard.component';
|
||||
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.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];
|
||||
|
||||
/* Pages */
|
||||
import { ServerDashboardPage } from 'sql/parts/dashboard/pages/serverDashboardPage.component';
|
||||
import { DatabaseDashboardPage } from 'sql/parts/dashboard/pages/databaseDashboardPage.component';
|
||||
let pageComponents = [ServerDashboardPage, DatabaseDashboardPage];
|
||||
|
||||
/* Widget Components */
|
||||
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
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];
|
||||
|
||||
/* Insights */
|
||||
let insightComponents = Registry.as<IInsightRegistry>(Extensions.InsightContribution).getAllCtors();
|
||||
|
||||
// Setup routes for various child components
|
||||
const appRoutes: Routes = [
|
||||
{ path: 'database-dashboard', component: DatabaseDashboardPage },
|
||||
{ path: 'server-dashboard', component: ServerDashboardPage },
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'database-dashboard',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{ path: '**', component: DatabaseDashboardPage }
|
||||
];
|
||||
|
||||
// Connection Dashboard main angular module
|
||||
@NgModule({
|
||||
declarations: [
|
||||
...baseComponents,
|
||||
...pageComponents,
|
||||
...widgetComponents,
|
||||
...insightComponents
|
||||
],
|
||||
// also for widgets
|
||||
entryComponents: [
|
||||
DashboardComponent,
|
||||
...widgetComponents,
|
||||
...insightComponents
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
NgGridModule,
|
||||
ChartsModule,
|
||||
RouterModule.forRoot(appRoutes)
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
{ provide: IBreadcrumbService, useClass: BreadcrumbService },
|
||||
DashboardServiceInterface,
|
||||
{ provide: UrlSerializer, useClass: CustomUrlSerializer }
|
||||
]
|
||||
})
|
||||
export class DashboardModule {
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factory = this._resolver.resolveComponentFactory(DashboardComponent);
|
||||
const uniqueSelector: string = this._bootstrapService.getUniqueSelector(DASHBOARD_SELECTOR);
|
||||
this._bootstrap.selector = uniqueSelector;
|
||||
(<any>factory).factory.selector = uniqueSelector;
|
||||
appRef.bootstrap(factory);
|
||||
}
|
||||
}
|
||||
22
src/sql/parts/dashboard/dashboardConfig.contribution.ts
Normal file
22
src/sql/parts/dashboard/dashboardConfig.contribution.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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';
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
const dashboardConfig: IConfigurationNode = {
|
||||
id: 'Dashboard',
|
||||
type: 'object',
|
||||
properties: {
|
||||
[DATABASE_DASHBOARD_PROPERTIES]: databaseDashboardPropertiesSchema,
|
||||
[SERVER_DASHBOARD_PROPERTIES]: serverDashboardPropertiesSchema,
|
||||
[DATABASE_DASHBOARD_SETTING]: databaseDashboardSettingSchema,
|
||||
[SERVER_DASHBOARD_SETTING]: serverDashboardSettingSchema
|
||||
}
|
||||
};
|
||||
|
||||
configurationRegistry.registerConfiguration(dashboardConfig);
|
||||
112
src/sql/parts/dashboard/dashboardEditor.ts
Normal file
112
src/sql/parts/dashboard/dashboardEditor.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Dimension, Builder, $ } from 'vs/base/browser/builder';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
|
||||
|
||||
import { DashboardInput } from './dashboardInput';
|
||||
import { DashboardModule } from './dashboard.module';
|
||||
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { DASHBOARD_SELECTOR } from 'sql/parts/dashboard/dashboard.component';
|
||||
|
||||
export class DashboardEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.connectiondashboard';
|
||||
private _dashboardContainer: HTMLElement;
|
||||
protected _input: DashboardInput;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IBootstrapService private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
super(DashboardEditor.ID, telemetryService, themeService);
|
||||
}
|
||||
|
||||
public get input(): DashboardInput {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the editor in the parent builder.
|
||||
*/
|
||||
public createEditor(parent: Builder): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
||||
*/
|
||||
public focus(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
|
||||
* To be called when the container of this editor changes size.
|
||||
*/
|
||||
public layout(dimension: Dimension): void {
|
||||
}
|
||||
|
||||
public setInput(input: DashboardInput, options: EditorOptions): TPromise<void> {
|
||||
if (this.input && this.input.matches(input)) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
const parentElement = this.getContainer().getHTMLElement();
|
||||
|
||||
super.setInput(input, options);
|
||||
|
||||
$(parentElement).empty();
|
||||
|
||||
if (!input.hasBootstrapped) {
|
||||
let container = DOM.$<HTMLElement>('.dashboardEditor');
|
||||
container.style.height = '100%';
|
||||
this._dashboardContainer = DOM.append(parentElement, container);
|
||||
this.input.container = this._dashboardContainer;
|
||||
return TPromise.wrap(input.initializedPromise.then(() => this.bootstrapAngular(input)));
|
||||
} else {
|
||||
this._dashboardContainer = DOM.append(parentElement, this.input.container);
|
||||
return TPromise.as<void>(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private bootstrapAngular(input: DashboardInput): void {
|
||||
// Get the bootstrap params and perform the bootstrap
|
||||
let params: DashboardComponentParams = {
|
||||
connection: input.connectionProfile,
|
||||
ownerUri: input.uri
|
||||
};
|
||||
|
||||
input.hasBootstrapped = true;
|
||||
|
||||
let uniqueSelector = this._bootstrapService.bootstrap(
|
||||
DashboardModule,
|
||||
this._dashboardContainer,
|
||||
DASHBOARD_SELECTOR,
|
||||
params,
|
||||
input);
|
||||
input.setUniqueSelector(uniqueSelector);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
extensions: ['.dashboard'],
|
||||
id: 'dashboard',
|
||||
});
|
||||
170
src/sql/parts/dashboard/dashboardInput.ts
Normal file
170
src/sql/parts/dashboard/dashboardInput.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { EditorInput, EditorModel } from 'vs/workbench/common/editor';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
|
||||
export class DashboardInput extends EditorInput {
|
||||
|
||||
private _uri: string;
|
||||
public static ID: string = 'workbench.editorinputs.connectiondashboardinputs';
|
||||
public static SCHEMA: string = 'sqldashboard';
|
||||
|
||||
private _initializedPromise: Thenable<void>;
|
||||
private _onConnectionChanged: IDisposable;
|
||||
|
||||
public get initializedPromise(): Thenable<void> {
|
||||
return this._initializedPromise;
|
||||
}
|
||||
|
||||
private _uniqueSelector: string;
|
||||
|
||||
public hasBootstrapped = false;
|
||||
// Holds the HTML content for the editor when the editor discards this input and loads another
|
||||
private _parentContainer: HTMLElement;
|
||||
|
||||
constructor(
|
||||
_connectionProfile: IConnectionProfile,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService model: IModelService
|
||||
) {
|
||||
super();
|
||||
// TODO; possible refactor
|
||||
// basically this is mimicing creating a "model" (the backing model for text for editors)
|
||||
// for dashboard, even though there is no backing text. We need this so that we can
|
||||
// tell the icon theme services that we are a dashboard resource, therefore loading the correct icon
|
||||
|
||||
// vscode has a comment that Mode's will eventually be removed (not sure the state of this comment)
|
||||
// so this might be able to be undone when that happens
|
||||
if (!model.getModel(this.getResource())) {
|
||||
model.createModel('', modeService.getMode('dashboard'), this.getResource());
|
||||
}
|
||||
this._initializedPromise = _connectionService.connectIfNotConnected(_connectionProfile, 'dashboard').then(
|
||||
u => {
|
||||
this._uri = u;
|
||||
let info = this._connectionService.getConnectionInfo(u);
|
||||
if (info) {
|
||||
this._onConnectionChanged = this._connectionService.onConnectionChanged(e => {
|
||||
if (e.connectionUri === u) {
|
||||
this._onDidChangeLabel.fire();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public setUniqueSelector(uniqueSelector: string): void {
|
||||
this._uniqueSelector = uniqueSelector;
|
||||
}
|
||||
|
||||
public getTypeId(): string {
|
||||
return UntitledEditorInput.ID;
|
||||
}
|
||||
|
||||
public getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'dashboard',
|
||||
path: '.dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
if (!this.connectionProfile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let name = this.connectionProfile.serverName;
|
||||
if (this.connectionProfile.databaseName
|
||||
&& !this.isMasterMssql()) {
|
||||
// Only add DB name if this is a non-default, non-master connection
|
||||
name = name + ':' + this.connectionProfile.databaseName;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private isMasterMssql(): boolean {
|
||||
return this.connectionProfile.providerName.toLowerCase() === 'mssql'
|
||||
&& this.connectionProfile.databaseName.toLowerCase() === 'master';
|
||||
}
|
||||
|
||||
public get uri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposeContainer();
|
||||
if (this._onConnectionChanged) {
|
||||
this._onConnectionChanged.dispose();
|
||||
}
|
||||
this._connectionService.disconnect(this._uri);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _disposeContainer() {
|
||||
if (!this._parentContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentNode = this._parentContainer.parentNode;
|
||||
if (parentNode) {
|
||||
parentNode.removeChild(this._parentContainer);
|
||||
this._parentContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
set container(container: HTMLElement) {
|
||||
this._disposeContainer();
|
||||
this._parentContainer = container;
|
||||
}
|
||||
|
||||
get container(): HTMLElement {
|
||||
return this._parentContainer;
|
||||
}
|
||||
|
||||
public supportsSplitEditor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public get connectionProfile(): IConnectionProfile {
|
||||
return this._connectionService.getConnectionProfile(this._uri);
|
||||
}
|
||||
|
||||
public resolve(refresh?: boolean): TPromise<EditorModel> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get hasInitialized(): boolean {
|
||||
return !!this._uniqueSelector;
|
||||
}
|
||||
|
||||
public get uniqueSelector(): string {
|
||||
return this._uniqueSelector;
|
||||
}
|
||||
|
||||
public matches(otherinput: any): boolean {
|
||||
return otherinput instanceof DashboardInput
|
||||
&& DashboardInput.profileMatches(this.connectionProfile, otherinput.connectionProfile);
|
||||
}
|
||||
|
||||
// similar to the default profile match but without databasename
|
||||
public static profileMatches(profile1: IConnectionProfile, profile2: IConnectionProfile): boolean {
|
||||
return profile1 && profile2
|
||||
&& profile1.providerName === profile2.providerName
|
||||
&& profile1.serverName === profile2.serverName
|
||||
&& profile1.userName === profile2.userName
|
||||
&& profile1.authenticationType === profile2.authenticationType
|
||||
&& profile1.groupFullName === profile2.groupFullName;
|
||||
}
|
||||
}
|
||||
@@ -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 { OnInit, Inject, forwardRef, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
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 * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDestroy {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('databasePageName', 'DATABASE DASHBOARD'),
|
||||
widget: {
|
||||
'properties-widget': undefined
|
||||
},
|
||||
context: 'database',
|
||||
background_color: colors.editorBackground,
|
||||
border: 'none',
|
||||
fontSize: '14px',
|
||||
fontWeight: '200',
|
||||
padding: '5px 0 0 0',
|
||||
provider: undefined,
|
||||
edition: undefined
|
||||
};
|
||||
|
||||
protected readonly context = 'database';
|
||||
private _dispose: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private _breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super(dashboardService);
|
||||
this._dispose.push(dashboardService.onUpdatePage(() => {
|
||||
this.refresh(true);
|
||||
this._cd.detectChanges();
|
||||
}));
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
|
||||
this.baseInit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._dispose = dispose(this._dispose);
|
||||
this.baseDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
|
||||
export const databaseDashboardPropertiesSchema: IJSONSchema = {
|
||||
description: nls.localize('dashboardDatabaseProperties', 'Enable or disable the properties widget'),
|
||||
default: true,
|
||||
oneOf: <IJSONSchema[]>[
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string'
|
||||
},
|
||||
edition: {
|
||||
type: 'number'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('dashboard.databaseproperties', 'Property values to show'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.databaseproperties.displayName', 'Display name of the property')
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.databaseproperties.value', 'Value in the Database Info Object')
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.databaseproperties.ignore', 'Specify specific values to ignore'),
|
||||
items: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
displayName: nls.localize('recoveryModel', 'Recovery Model'),
|
||||
value: 'recoveryModel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastDatabaseBackup', 'Last Database Backup'),
|
||||
value: 'lastBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastLogBackup', 'Last Log Backup'),
|
||||
value: 'lastLogBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
},
|
||||
widget: {
|
||||
type: 'object',
|
||||
properties: widgetRegistry.databaseWidgetSchema.properties,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
name: 'Tasks',
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 1
|
||||
},
|
||||
widget: {
|
||||
'tasks-widget': {}
|
||||
}
|
||||
},
|
||||
{
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 2
|
||||
},
|
||||
widget: {
|
||||
'explorer-widget': {}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
|
||||
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Inject, forwardRef, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
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 * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export class ServerDashboardPage extends DashboardPage implements OnInit, OnDestroy {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('serverPageName', 'SERVER DASHBOARD'),
|
||||
widget: {
|
||||
'properties-widget': undefined
|
||||
},
|
||||
context: 'server',
|
||||
background_color: colors.editorBackground,
|
||||
border: 'none',
|
||||
fontSize: '14px',
|
||||
fontWeight: '200',
|
||||
padding: '5px 0 0 0',
|
||||
provider: undefined,
|
||||
edition: undefined
|
||||
};
|
||||
|
||||
protected readonly context = 'server';
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef
|
||||
) {
|
||||
super(dashboardService);
|
||||
// revert back to default database
|
||||
this.dashboardService.connectionManagementService.changeDatabase('master').then(() => {
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = undefined;
|
||||
this.init();
|
||||
cd.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
|
||||
this.baseInit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.baseDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
|
||||
export interface IPropertiesConfig {
|
||||
edition: number | Array<number>;
|
||||
provider: string | Array<string>;
|
||||
properties: {
|
||||
displayName: string;
|
||||
value: string
|
||||
}[];
|
||||
}
|
||||
|
||||
export const serverDashboardPropertiesSchema: IJSONSchema = {
|
||||
description: nls.localize('dashboardServerProperties', 'Enable or disable the properties widget'),
|
||||
default: true,
|
||||
oneOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string'
|
||||
},
|
||||
edition: {
|
||||
type: 'number'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('dashboard.serverproperties', 'Property values to show'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.serverproperties.displayName', 'Display name of the property')
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.serverproperties.value', 'Value in the Server Info Object')
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('edition', 'Edition'),
|
||||
value: 'serverEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('computerName', 'Computer Name'),
|
||||
value: 'machineName'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('osVersion', 'OS Version'),
|
||||
value: 'osVersion'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let defaultVal = [
|
||||
{
|
||||
name: 'Tasks',
|
||||
widget: {
|
||||
'tasks-widget': {}
|
||||
},
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 2
|
||||
},
|
||||
widget: {
|
||||
'explorer-widget': {}
|
||||
}
|
||||
},
|
||||
{
|
||||
widget: {
|
||||
'backup-history-server-insight': {
|
||||
cacheId: '0c7cba8b-c87a-4bcc-ae54-2f40a5503a90'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
widget: {
|
||||
'all-database-size-server-insight': {
|
||||
cacheId: '1d7cba8b-c87a-4bcc-ae54-2f40a5503a90'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
default: defaultVal
|
||||
};
|
||||
|
||||
export const SERVER_DASHBOARD_SETTING = 'dashboard.server.widgets';
|
||||
export const SERVER_DASHBOARD_PROPERTIES = 'dashboard.server.properties';
|
||||
74
src/sql/parts/dashboard/services/breadcrumb.service.ts
Normal file
74
src/sql/parts/dashboard/services/breadcrumb.service.ts
Normal 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 { Injectable, forwardRef, Inject, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { DashboardServiceInterface } from './dashboardServiceInterface.service';
|
||||
import { MenuItem, IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export enum BreadcrumbClass {
|
||||
DatabasePage,
|
||||
ServerPage
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BreadcrumbService implements IBreadcrumbService, OnDestroy {
|
||||
public breadcrumbItem: Subject<MenuItem[]>;
|
||||
private itemBreadcrums: MenuItem[];
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _currentPage: BreadcrumbClass;
|
||||
|
||||
constructor( @Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface) {
|
||||
_bootstrap.onUpdatePage(() => {
|
||||
this.setBreadcrumbs(this._currentPage);
|
||||
});
|
||||
this.breadcrumbItem = new Subject<MenuItem[]>();
|
||||
}
|
||||
|
||||
public setBreadcrumbs(page: BreadcrumbClass) {
|
||||
this._currentPage = page;
|
||||
this.itemBreadcrums = [];
|
||||
let refList: MenuItem[] = this.getBreadcrumbsLink(page);
|
||||
this.breadcrumbItem.next(refList);
|
||||
}
|
||||
|
||||
private getBreadcrumbsLink(page: BreadcrumbClass): MenuItem[] {
|
||||
this.itemBreadcrums = [];
|
||||
let profile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
|
||||
this.itemBreadcrums.push({ label: nls.localize('homeCrumb', 'Home')});
|
||||
switch (page) {
|
||||
case BreadcrumbClass.DatabasePage:
|
||||
this.itemBreadcrums.push(this.getServerBreadcrumb(profile));
|
||||
this.itemBreadcrums.push(this.getDbBreadcrumb(profile));
|
||||
break;
|
||||
case BreadcrumbClass.ServerPage:
|
||||
this.itemBreadcrums.push(this.getServerBreadcrumb(profile));
|
||||
break;
|
||||
default:
|
||||
this.itemBreadcrums = [];
|
||||
}
|
||||
return this.itemBreadcrums;
|
||||
}
|
||||
|
||||
private getServerBreadcrumb(profile: ConnectionProfile): MenuItem {
|
||||
return { label: profile.serverName, routerLink: ['server-dashboard'] };
|
||||
}
|
||||
|
||||
private getDbBreadcrumb(profile: ConnectionProfile): MenuItem {
|
||||
return {
|
||||
label: profile.databaseName ? profile.databaseName : 'database-name',
|
||||
routerLink: ['database-dashboard']
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Node Modules */
|
||||
import { Injectable, Inject, forwardRef, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
/* SQL imports */
|
||||
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { IMetadataService } from 'sql/services/metadata/metadataService';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
import { IAdminService } from 'sql/parts/admin/common/adminService';
|
||||
import { IQueryManagementService } from 'sql/parts/query/common/queryManagement';
|
||||
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
|
||||
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
|
||||
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
|
||||
|
||||
import { ProviderMetadata, DatabaseInfo, SimpleExecuteResult } from 'data';
|
||||
|
||||
/* VS imports */
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { 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';
|
||||
|
||||
const DASHBOARD_SETTINGS = 'dashboard';
|
||||
|
||||
/* Wrapper for a metadata service that contains the uri string to use on each request */
|
||||
export class SingleConnectionMetadataService {
|
||||
|
||||
constructor(
|
||||
private _metadataService: IMetadataService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
get metadata(): Observable<ProviderMetadata> {
|
||||
return Observable.fromPromise(this._metadataService.getMetadata(this._uri));
|
||||
}
|
||||
|
||||
get databaseNames(): Observable<string[]> {
|
||||
return Observable.fromPromise(this._metadataService.getDatabaseNames(this._uri));
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper for a connection service that contains the uri string to use on each request */
|
||||
export class SingleConnectionManagementService {
|
||||
|
||||
constructor(
|
||||
private _connectionService: IConnectionManagementService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
public changeDatabase(name: string): Thenable<boolean> {
|
||||
return this._connectionService.changeDatabase(this._uri, name);
|
||||
}
|
||||
|
||||
public get connectionInfo(): ConnectionManagementInfo {
|
||||
return this._connectionService.getConnectionInfo(this._uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleAdminService {
|
||||
|
||||
constructor(
|
||||
private _adminService: IAdminService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
public get databaseInfo(): Observable<DatabaseInfo> {
|
||||
return Observable.fromPromise(this._adminService.getDatabaseInfo(this._uri));
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleQueryManagementService {
|
||||
constructor(
|
||||
private _queryManagementService: IQueryManagementService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
public runQueryAndReturn(queryString: string): Thenable<SimpleExecuteResult> {
|
||||
return this._queryManagementService.runQueryAndReturn(this._uri, queryString);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Providers a interface between a dashboard interface and the rest of carbon.
|
||||
Stores the uri and unique selector of a dashboard instance and uses that
|
||||
whenever a call to a carbon service needs this information, so that the widgets
|
||||
don't need to be aware of the uri or selector. Simplifies the initialization and
|
||||
usage of a widget.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DashboardServiceInterface implements OnDestroy {
|
||||
private _uniqueSelector: string;
|
||||
private _uri: string;
|
||||
private _bootstrapParams: DashboardComponentParams;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
/* Services */
|
||||
private _metadataService: SingleConnectionMetadataService;
|
||||
private _connectionManagementService: SingleConnectionManagementService;
|
||||
private _themeService: IWorkbenchThemeService;
|
||||
private _contextMenuService: IContextMenuService;
|
||||
private _instantiationService: IInstantiationService;
|
||||
private _adminService: SingleAdminService;
|
||||
private _queryManagementService: SingleQueryManagementService;
|
||||
private _configService: IConfigurationService;
|
||||
private _insightsDialogService: IInsightsDialogService;
|
||||
private _contextViewService: IContextViewService;
|
||||
private _messageService: IMessageService;
|
||||
private _workspaceContextService: IWorkspaceContextService;
|
||||
private _storageService: IStorageService;
|
||||
private _capabilitiesService: ICapabilitiesService;
|
||||
|
||||
private _updatePage = new Emitter<void>();
|
||||
public readonly onUpdatePage: Event<void> = this._updatePage.event;
|
||||
|
||||
constructor(
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
) {
|
||||
this._themeService = this._bootstrapService.themeService;
|
||||
this._contextMenuService = this._bootstrapService.contextMenuService;
|
||||
this._instantiationService = this._bootstrapService.instantiationService;
|
||||
this._configService = this._bootstrapService.configurationService;
|
||||
this._insightsDialogService = this._bootstrapService.insightsDialogService;
|
||||
this._contextViewService = this._bootstrapService.contextViewService;
|
||||
this._messageService = this._bootstrapService.messageService;
|
||||
this._workspaceContextService = this._bootstrapService.workspaceContextService;
|
||||
this._storageService = this._bootstrapService.storageService;
|
||||
this._capabilitiesService = this._bootstrapService.capabilitiesService;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach((item) => item.dispose());
|
||||
}
|
||||
|
||||
public get messageService(): IMessageService {
|
||||
return this._messageService;
|
||||
}
|
||||
|
||||
public get metadataService(): SingleConnectionMetadataService {
|
||||
return this._metadataService;
|
||||
}
|
||||
|
||||
public get connectionManagementService(): SingleConnectionManagementService {
|
||||
return this._connectionManagementService;
|
||||
}
|
||||
|
||||
public get themeService(): IWorkbenchThemeService {
|
||||
return this._themeService;
|
||||
}
|
||||
|
||||
public get contextMenuService(): IContextMenuService {
|
||||
return this._contextMenuService;
|
||||
}
|
||||
|
||||
public get instantiationService(): IInstantiationService {
|
||||
return this._instantiationService;
|
||||
}
|
||||
|
||||
public get adminService(): SingleAdminService {
|
||||
return this._adminService;
|
||||
}
|
||||
|
||||
public get queryManagementService(): SingleQueryManagementService {
|
||||
return this._queryManagementService;
|
||||
}
|
||||
|
||||
public get contextViewService(): IContextViewService {
|
||||
return this._contextViewService;
|
||||
}
|
||||
|
||||
public get workspaceContextService(): IWorkspaceContextService {
|
||||
return this._workspaceContextService;
|
||||
}
|
||||
|
||||
public get storageService(): IStorageService {
|
||||
return this._storageService;
|
||||
}
|
||||
|
||||
public get CapabilitiesService(): ICapabilitiesService {
|
||||
return this._capabilitiesService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selector for this dashboard instance, should only be set once
|
||||
*/
|
||||
public set selector(selector: string) {
|
||||
this._uniqueSelector = selector;
|
||||
this._getbootstrapParams();
|
||||
}
|
||||
|
||||
private _getbootstrapParams(): void {
|
||||
this._bootstrapParams = this._bootstrapService.getBootstrapParams(this._uniqueSelector);
|
||||
this.uri = this._bootstrapParams.ownerUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the uri for this dashboard instance, should only be set once
|
||||
* Inits all the services that depend on knowing a uri
|
||||
*/
|
||||
private set uri(uri: string) {
|
||||
this._uri = uri;
|
||||
this._metadataService = new SingleConnectionMetadataService(this._bootstrapService.metadataService, this._uri);
|
||||
this._connectionManagementService = new SingleConnectionManagementService(this._bootstrapService.connectionManagementService, this._uri);
|
||||
this._adminService = new SingleAdminService(this._bootstrapService.adminService, this._uri);
|
||||
this._queryManagementService = new SingleQueryManagementService(this._bootstrapService.queryManagementService, this._uri);
|
||||
this._disposables.push(toDisposableSubscription(this._bootstrapService.angularEventingService.onAngularEvent(this._uri, (event) => this.handleDashboardEvent(event))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying Uri for dashboard
|
||||
* In general don't use this, use specific services instances exposed publically
|
||||
*/
|
||||
public getUnderlyingUri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public getOriginalConnectionProfile(): IConnectionProfile {
|
||||
return this._bootstrapParams.connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings for given string
|
||||
* @param type string of setting to get from dashboard settings; i.e dashboard.{type}
|
||||
*/
|
||||
public getSettings(type: string): { widgets: Array<WidgetConfig>, properties: boolean | IPropertiesConfig[] } {
|
||||
let config = this._configService.getConfiguration(DASHBOARD_SETTINGS);
|
||||
return config[type];
|
||||
}
|
||||
|
||||
private handleDashboardEvent(event: AngularEventType): void {
|
||||
switch (event) {
|
||||
case AngularEventType.NAV_DATABASE:
|
||||
this.connectionManagementService.changeDatabase(this.connectionManagementService.connectionInfo.connectionProfile.databaseName).then(
|
||||
result => {
|
||||
if (result) {
|
||||
if (this._router.url === '/database-dashboard') {
|
||||
this._updatePage.fire();
|
||||
} else {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
}
|
||||
} else {
|
||||
this.messageService.show(Severity.Error, nls.localize('dashboard.changeDatabaseFailure', "Failed to change database"));
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.messageService.show(Severity.Error, nls.localize('dashboard.changeDatabaseFailure', "Failed to change database"));
|
||||
}
|
||||
);
|
||||
break;
|
||||
case AngularEventType.NAV_SERVER:
|
||||
this._router.navigate(['server-dashboard']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/sql/parts/dashboard/widgets/explorer/explorerActions.ts
Normal file
89
src/sql/parts/dashboard/widgets/explorer/explorerActions.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { IConnectionManagementService, MetadataType } from 'sql/parts/connection/common/connectionManagement';
|
||||
import {
|
||||
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction,
|
||||
BackupAction, BaseActionContext, ManageAction
|
||||
} from 'sql/workbench/common/actions';
|
||||
import { IDisasterRecoveryUiService } from 'sql/parts/disasterRecovery/common/interfaces';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
|
||||
export function GetExplorerActions(type: MetadataType, isCloud: boolean, dashboardService: DashboardServiceInterface): TPromise<IAction[]> {
|
||||
let actions: IAction[] = [];
|
||||
|
||||
// When context menu on database
|
||||
if (type === undefined) {
|
||||
actions.push(dashboardService.instantiationService.createInstance(DashboardNewQueryAction, DashboardNewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
|
||||
if (!isCloud) {
|
||||
actions.push(dashboardService.instantiationService.createInstance(DashboardBackupAction, DashboardBackupAction.ID, DashboardBackupAction.LABEL));
|
||||
}
|
||||
actions.push(dashboardService.instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
|
||||
return TPromise.as(actions);
|
||||
}
|
||||
|
||||
if (type === MetadataType.View || type === MetadataType.Table) {
|
||||
actions.push(dashboardService.instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
|
||||
}
|
||||
|
||||
if (type === MetadataType.Table) {
|
||||
actions.push(dashboardService.instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
|
||||
}
|
||||
|
||||
actions.push(dashboardService.instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
|
||||
|
||||
return TPromise.as(actions);
|
||||
}
|
||||
|
||||
export class DashboardBackupAction extends BackupAction {
|
||||
public static ID = 'dashboard.' + BackupAction.ID;
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IDisasterRecoveryUiService disasterRecoveryService: IDisasterRecoveryUiService,
|
||||
@IConnectionManagementService private connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(id, label, BackupAction.ICON, disasterRecoveryService, );
|
||||
}
|
||||
|
||||
run(actionContext: BaseActionContext): TPromise<boolean> {
|
||||
let self = this;
|
||||
// change database before performing action
|
||||
return new TPromise<boolean>((resolve, reject) => {
|
||||
self.connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
|
||||
actionContext.connInfo = self.connectionManagementService.getConnectionInfo(actionContext.uri);
|
||||
super.run(actionContext).then((result) => {
|
||||
resolve(result);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DashboardNewQueryAction extends NewQueryAction {
|
||||
public static ID = 'dashboard.' + NewQueryAction.ID;
|
||||
|
||||
run(actionContext: BaseActionContext): TPromise<boolean> {
|
||||
let self = this;
|
||||
// change database before performing action
|
||||
return new TPromise<boolean>((resolve, reject) => {
|
||||
self._connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
|
||||
actionContext.profile = self._connectionManagementService.getConnectionProfile(actionContext.uri);
|
||||
super.run(actionContext).then((result) => {
|
||||
resolve(result);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div class="explorer-widget" style="display: flex; flex-flow: column; position: absolute; height:100%; width:100%; padding: 10px; box-sizing: border-box">
|
||||
<div #input style="width: 100%"></div>
|
||||
<div style="flex: 1 1 auto; position: relative">
|
||||
<div #table style="position: absolute; height: 100%; width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,394 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/media/objectTypes/objecttypes';
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./media/explorerWidget';
|
||||
|
||||
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { BaseActionContext } from 'sql/workbench/common/actions';
|
||||
import { GetExplorerActions } from './explorerActions';
|
||||
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
|
||||
import { warn } from 'sql/base/common/log';
|
||||
import { MultipleRequestDelayer } from 'sql/base/common/async';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import * as nls from 'vs/nls';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { $, getContentHeight } from 'vs/base/browser/dom';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
|
||||
import { ObjectMetadata } from 'data';
|
||||
|
||||
export class ObjectMetadataWrapper implements ObjectMetadata {
|
||||
public metadataType: MetadataType;
|
||||
public metadataTypeName: string;
|
||||
public urn: string;
|
||||
public name: string;
|
||||
public schema: string;
|
||||
|
||||
constructor(from?: ObjectMetadata) {
|
||||
if (from) {
|
||||
this.metadataType = from.metadataType;
|
||||
this.metadataTypeName = from.metadataTypeName;
|
||||
this.urn = from.urn;
|
||||
this.name = from.name;
|
||||
this.schema = from.schema;
|
||||
}
|
||||
}
|
||||
|
||||
public matches(other: ObjectMetadataWrapper): boolean {
|
||||
if (!other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.metadataType === other.metadataType
|
||||
&& this.schema === other.schema
|
||||
&& this.name === other.name;
|
||||
}
|
||||
|
||||
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
|
||||
if (!objectMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
|
||||
}
|
||||
|
||||
|
||||
// custom sort : Table > View > Stored Procedures > Function
|
||||
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
|
||||
// compare the object type
|
||||
if (metadata1.metadataType < metadata2.metadataType) {
|
||||
return -1;
|
||||
} else if (metadata1.metadataType > metadata2.metadataType) {
|
||||
return 1;
|
||||
|
||||
// otherwise compare the schema
|
||||
} else {
|
||||
let schemaCompare: number = metadata1.schema && metadata2.schema
|
||||
? metadata1.schema.localeCompare(metadata2.schema)
|
||||
// schemas are not expected to be undefined, but if they are then compare using object names
|
||||
: 0;
|
||||
|
||||
if (schemaCompare !== 0) {
|
||||
return schemaCompare;
|
||||
|
||||
// otherwise compare the object name
|
||||
} else {
|
||||
return metadata1.name.localeCompare(metadata2.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare type ListResource = string | ObjectMetadataWrapper;
|
||||
|
||||
enum TemplateIds {
|
||||
STRING = 'string',
|
||||
METADATA = 'metadata'
|
||||
}
|
||||
|
||||
interface IListTemplate {
|
||||
icon?: HTMLElement;
|
||||
label: HTMLElement;
|
||||
}
|
||||
|
||||
class Delegate implements IDelegate<ListResource> {
|
||||
getHeight(element: ListResource): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: ListResource): string {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return TemplateIds.METADATA.toString();
|
||||
} else if (types.isString(element)) {
|
||||
return TemplateIds.STRING.toString();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StringRenderer implements IRenderer<string, IListTemplate> {
|
||||
public readonly templateId = TemplateIds.STRING.toString();
|
||||
|
||||
renderTemplate(container: HTMLElement): IListTemplate {
|
||||
let row = $('.list-row');
|
||||
let icon = $('.icon.database');
|
||||
let label = $('.label');
|
||||
row.appendChild(icon);
|
||||
row.appendChild(label);
|
||||
container.appendChild(row);
|
||||
return { icon, label };
|
||||
}
|
||||
|
||||
renderElement(element: string, index: number, templateData: IListTemplate): void {
|
||||
templateData.label.innerText = element;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IListTemplate): void {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataRenderer implements IRenderer<ObjectMetadataWrapper, IListTemplate> {
|
||||
public readonly templateId = TemplateIds.METADATA.toString();
|
||||
|
||||
renderTemplate(container: HTMLElement): IListTemplate {
|
||||
let row = $('.list-row');
|
||||
let icon = $('div');
|
||||
let label = $('.label');
|
||||
row.appendChild(icon);
|
||||
row.appendChild(label);
|
||||
container.appendChild(row);
|
||||
return { icon, label };
|
||||
}
|
||||
|
||||
renderElement(element: ObjectMetadataWrapper, index: number, templateData: IListTemplate): void {
|
||||
if (element && element) {
|
||||
switch (element.metadataType) {
|
||||
case MetadataType.Function:
|
||||
templateData.icon.className = 'icon scalarvaluedfunction';
|
||||
break;
|
||||
case MetadataType.SProc:
|
||||
templateData.icon.className = 'icon stored-procedure';
|
||||
break;
|
||||
case MetadataType.Table:
|
||||
templateData.icon.className = 'icon table';
|
||||
break;
|
||||
case MetadataType.View:
|
||||
templateData.icon.className = 'icon view';
|
||||
break;
|
||||
}
|
||||
|
||||
templateData.label.innerText = element.schema + '.' + element.name;
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IListTemplate): void {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'explorer-widget',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/explorer/explorerWidget.component.html'))
|
||||
})
|
||||
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
|
||||
|
||||
private _isCloud: boolean;
|
||||
private _tableData: ListResource[];
|
||||
private _disposables: Array<IDisposable> = [];
|
||||
private _input: InputBox;
|
||||
private _table: List<ListResource>;
|
||||
private _lastClickedItem: ListResource;
|
||||
private _filterDelayer = new Delayer<void>(200);
|
||||
private _dblClickDelayer = new MultipleRequestDelayer<void>(500);
|
||||
|
||||
@ViewChild('input') private _inputContainer: ElementRef;
|
||||
@ViewChild('table') private _tableContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
|
||||
) {
|
||||
super();
|
||||
this._isCloud = _bootstrap.connectionManagementService.connectionInfo.serverInfo.isCloud;
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let inputOptions: IInputOptions = {
|
||||
placeholder: this._config.context === 'database' ? nls.localize('seachObjects', 'Search by name of type (a:, t:, v:, f:, or sp:)') : nls.localize('searchDatabases', 'Search databases')
|
||||
};
|
||||
this._input = new InputBox(this._inputContainer.nativeElement, this._bootstrap.contextViewService, inputOptions);
|
||||
this._disposables.push(this._input.onDidChange(e => {
|
||||
this._filterDelayer.trigger(() => {
|
||||
this._table.splice(0, this._table.length, this._filterTable(e));
|
||||
});
|
||||
}));
|
||||
this._table = new List<ListResource>(this._tableContainer.nativeElement, new Delegate(), [new MetadataRenderer(), new StringRenderer()]);
|
||||
this._disposables.push(this._table.onContextMenu(e => {
|
||||
this.handleContextMenu(e.element, e.index, e.anchor);
|
||||
}));
|
||||
this._disposables.push(this._table.onSelectionChange(e => {
|
||||
if (e.elements.length > 0 && this._lastClickedItem === e.elements[0]) {
|
||||
this._dblClickDelayer.trigger(() => this.handleItemDoubleClick(e.elements[0]));
|
||||
} else {
|
||||
this._lastClickedItem = e.elements.length > 0 ? e.elements[0] : undefined;
|
||||
}
|
||||
}));
|
||||
this._table.layout(getContentHeight(this._tableContainer.nativeElement));
|
||||
this._disposables.push(this._input);
|
||||
this._disposables.push(attachInputBoxStyler(this._input, this._bootstrap.themeService));
|
||||
this._disposables.push(this._table);
|
||||
this._disposables.push(attachListStyler(this._table, this._bootstrap.themeService));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach(i => i.dispose());
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
if (this._config.context === 'database') {
|
||||
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
|
||||
data => {
|
||||
if (data) {
|
||||
this._tableData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
|
||||
this._tableData.sort(ObjectMetadataWrapper.sort);
|
||||
this._table.splice(0, this._table.length, this._tableData);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.objectError', "Unable to load objects");
|
||||
}
|
||||
)));
|
||||
} else {
|
||||
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
|
||||
data => {
|
||||
this._tableData = data;
|
||||
this._table.splice(0, this._table.length, this._tableData);
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.databaseError', "Unable to load databases");
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles action when an item is double clicked in the explorer widget
|
||||
* @param val If on server page, explorer objects will be strings representing databases;
|
||||
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
|
||||
*
|
||||
*/
|
||||
private handleItemDoubleClick(val: ListResource): void {
|
||||
if (types.isString(val)) {
|
||||
this._bootstrap.connectionManagementService.changeDatabase(val as string).then(result => {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles action when a item is clicked in the explorer widget
|
||||
* @param val If on server page, explorer objects will be strings representing databases;
|
||||
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
|
||||
* @param index Index of the value in the array the ngFor template is built from
|
||||
* @param event Click event
|
||||
*/
|
||||
private handleContextMenu(val: ListResource, index: number, anchor: HTMLElement | { x: number, y: number }): void {
|
||||
// event will exist if the context menu span was clicked
|
||||
if (event) {
|
||||
if (this._config.context === 'server') {
|
||||
let newProfile = <IConnectionProfile>Object.create(this._bootstrap.connectionManagementService.connectionInfo.connectionProfile);
|
||||
newProfile.databaseName = val as string;
|
||||
this._bootstrap.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => GetExplorerActions(undefined, this._isCloud, this._bootstrap),
|
||||
getActionsContext: () => {
|
||||
return <BaseActionContext>{
|
||||
uri: this._bootstrap.getUnderlyingUri(),
|
||||
profile: newProfile,
|
||||
connInfo: this._bootstrap.connectionManagementService.connectionInfo,
|
||||
databasename: val as string
|
||||
};
|
||||
}
|
||||
});
|
||||
} else if (this._config.context === 'database') {
|
||||
let object = val as ObjectMetadataWrapper;
|
||||
this._bootstrap.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => GetExplorerActions(object.metadataType, this._isCloud, this._bootstrap),
|
||||
getActionsContext: () => {
|
||||
return <BaseActionContext>{
|
||||
object: object,
|
||||
uri: this._bootstrap.getUnderlyingUri(),
|
||||
profile: this._bootstrap.connectionManagementService.connectionInfo.connectionProfile
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn('Unknown dashboard context: ', this._config.context);
|
||||
}
|
||||
}
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private _filterTable(val: string): ListResource[] {
|
||||
let items = this._tableData;
|
||||
if (!items) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// format filter string for clean filter, no white space and lower case
|
||||
let filterString = val.trim().toLowerCase();
|
||||
|
||||
// handle case when passed a string array
|
||||
if (types.isString(items[0])) {
|
||||
let _items = <string[]>items;
|
||||
return _items.filter(item => {
|
||||
return item.toLowerCase().includes(filterString);
|
||||
});
|
||||
}
|
||||
|
||||
// make typescript compiler happy
|
||||
let objectItems = items as ObjectMetadataWrapper[];
|
||||
|
||||
// determine is a filter is applied
|
||||
let metadataType: MetadataType;
|
||||
|
||||
if (val.includes(':')) {
|
||||
let filterArray = filterString.split(':');
|
||||
|
||||
if (filterArray.length > 2) {
|
||||
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
|
||||
} else {
|
||||
filterString = filterArray[1];
|
||||
}
|
||||
|
||||
switch (filterArray[0].toLowerCase()) {
|
||||
case 'v':
|
||||
metadataType = MetadataType.View;
|
||||
break;
|
||||
case 't':
|
||||
metadataType = MetadataType.Table;
|
||||
break;
|
||||
case 'sp':
|
||||
metadataType = MetadataType.SProc;
|
||||
break;
|
||||
case 'f':
|
||||
metadataType = MetadataType.Function;
|
||||
break;
|
||||
case 'a':
|
||||
return objectItems;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return objectItems.filter(item => {
|
||||
if (metadataType !== undefined) {
|
||||
return item.metadataType === metadataType && (item.schema + '.' + item.name).toLowerCase().includes(filterString);
|
||||
} else {
|
||||
return (item.schema + '.' + item.name).toLowerCase().includes(filterString);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 explorerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
registerDashboardWidget('explorer-widget', '', explorerSchema);
|
||||
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
explorer-widget .list-row .icon {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
explorer-widget .list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
37
src/sql/parts/dashboard/widgets/insights/actions.ts
Normal file
37
src/sql/parts/dashboard/widgets/insights/actions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { RunQueryOnConnectionMode, IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
|
||||
import { InsightActionContext } from 'sql/workbench/common/actions';
|
||||
|
||||
export class RunInsightQueryAction extends Action {
|
||||
public static ID = 'runQuery';
|
||||
public static LABEL = nls.localize('insights.runQuery', "Run Query");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService protected _queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService protected _connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(context: InsightActionContext): TPromise<boolean> {
|
||||
return new TPromise<boolean>((resolve, reject) => {
|
||||
TaskUtilities.newQuery(
|
||||
context.profile,
|
||||
this._connectionManagementService,
|
||||
this._queryEditorService,
|
||||
context.insight.query as string,
|
||||
RunQueryOnConnectionMode.executeQuery
|
||||
).then(() => resolve(true), () => resolve(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ViewContainerRef, forwardRef, AfterContentInit,
|
||||
ComponentFactoryResolver, ViewChild, OnDestroy, ChangeDetectorRef
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WIDGET_CONFIG, WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
|
||||
import { InsightAction, InsightActionContext } from 'sql/workbench/common/actions';
|
||||
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
|
||||
import { IInsightsConfig, IInsightsView } from './interfaces';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { insertValueRegex } from 'sql/parts/insights/browser/insightsDialogView';
|
||||
import { RunInsightQueryAction } from './actions';
|
||||
|
||||
import { SimpleExecuteResult } from 'data';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
@Component({
|
||||
selector: 'insights-widget',
|
||||
template: `
|
||||
<div *ngIf="error" style="text-align: center; padding-top: 20px">{{error}}</div>
|
||||
<div style="margin: 10px; width: calc(100% - 20px); height: calc(100% - 20px)">
|
||||
<ng-template component-host></ng-template>
|
||||
</div>`,
|
||||
styles: [':host { width: 100%; height: 100%}']
|
||||
})
|
||||
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit, OnDestroy {
|
||||
private insightConfig: IInsightsConfig;
|
||||
private queryObv: Observable<SimpleExecuteResult>;
|
||||
private _disposables: Array<IDisposable> = [];
|
||||
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
|
||||
|
||||
private _typeKey: string;
|
||||
private _init: boolean = false;
|
||||
|
||||
public error: string;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private dashboardService: DashboardServiceInterface,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ViewContainerRef)) private viewContainerRef: ViewContainerRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
this.insightConfig = <IInsightsConfig>this._config.widget['insights-widget'];
|
||||
|
||||
this._verifyConfig();
|
||||
|
||||
this._parseConfig().then(() => {
|
||||
if (!this._checkStorage()) {
|
||||
let promise = this._runQuery();
|
||||
this.queryObv = Observable.fromPromise(promise);
|
||||
promise.then(
|
||||
result => {
|
||||
if (this._init) {
|
||||
this._updateChild(result);
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(result));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if (this._init) {
|
||||
this.showError(error);
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.reject<SimpleExecuteResult>(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}, error => {
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init = true;
|
||||
if (this.queryObv) {
|
||||
this._disposables.push(toDisposableSubscription(this.queryObv.subscribe(
|
||||
result => {
|
||||
this._updateChild(result);
|
||||
},
|
||||
error => {
|
||||
this.showError(error);
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach(i => i.dispose());
|
||||
}
|
||||
|
||||
private showError(error: string): void {
|
||||
this.error = error;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
get actions(): Array<Action> {
|
||||
let actions: Array<Action> = [];
|
||||
if (this.insightConfig.details && (this.insightConfig.details.query || this.insightConfig.details.queryFile)) {
|
||||
actions.push(this.dashboardService.instantiationService.createInstance(InsightAction, InsightAction.ID, InsightAction.LABEL));
|
||||
}
|
||||
actions.push(this.dashboardService.instantiationService.createInstance(RunInsightQueryAction, RunInsightQueryAction.ID, RunInsightQueryAction.LABEL));
|
||||
return actions;
|
||||
}
|
||||
|
||||
get actionsContext(): InsightActionContext {
|
||||
return <InsightActionContext>{
|
||||
profile: this.dashboardService.connectionManagementService.connectionInfo.connectionProfile,
|
||||
insight: this.insightConfig
|
||||
};
|
||||
}
|
||||
|
||||
private _storeResult(result: SimpleExecuteResult): SimpleExecuteResult {
|
||||
if (this.insightConfig.cacheId) {
|
||||
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _checkStorage(): boolean {
|
||||
if (this.insightConfig.cacheId) {
|
||||
let storage = this.dashboardService.storageService.get(this._getStorageKey());
|
||||
if (storage) {
|
||||
if (this._init) {
|
||||
this._updateChild(JSON.parse(storage));
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public get refresh(): () => void {
|
||||
return this._refresh();
|
||||
}
|
||||
|
||||
public _refresh(): () => void {
|
||||
return () => {
|
||||
this._runQuery().then(
|
||||
result => this._updateChild(result),
|
||||
error => this.showError(error)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private _getStorageKey(): string {
|
||||
return `insights.${this.insightConfig.cacheId}.${this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.getOptionsKey()}`;
|
||||
}
|
||||
|
||||
private _runQuery(): Thenable<SimpleExecuteResult> {
|
||||
return this.dashboardService.queryManagementService.runQueryAndReturn(this.insightConfig.query as string).then(
|
||||
result => {
|
||||
return this._storeResult(result);
|
||||
},
|
||||
error => {
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateChild(result: SimpleExecuteResult): void {
|
||||
if (result.rowCount === 0) {
|
||||
this.showError(nls.localize('noResults', 'No results to show'));
|
||||
return;
|
||||
}
|
||||
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory<IInsightsView>(insightRegistry.getCtorFromId(this._typeKey));
|
||||
this.componentHost.viewContainerRef.clear();
|
||||
|
||||
let componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory);
|
||||
let componentInstance = componentRef.instance;
|
||||
componentInstance.data = { columns: result.columnInfo.map(item => item.columnName), rows: result.rows.map(row => row.map(item => item.displayValue)) };
|
||||
// check if the setter is defined
|
||||
componentInstance.config = this.insightConfig.type[this._typeKey];
|
||||
if (componentInstance.init) {
|
||||
componentInstance.init();
|
||||
}
|
||||
}
|
||||
|
||||
private _verifyConfig() {
|
||||
if (types.isUndefinedOrNull(this.insightConfig)) {
|
||||
throw new Error('Insight config must be defined');
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(this.insightConfig.type)) {
|
||||
throw new Error('An Insight type must be specified');
|
||||
}
|
||||
|
||||
if (Object.keys(this.insightConfig.type).length !== 1) {
|
||||
throw new Error('Exactly 1 insight type must be specified');
|
||||
}
|
||||
|
||||
if (!insightRegistry.getAllIds().includes(Object.keys(this.insightConfig.type)[0])) {
|
||||
throw new Error('The insight type must be a valid registered insight');
|
||||
}
|
||||
|
||||
if (!this.insightConfig.query && !this.insightConfig.queryFile) {
|
||||
throw new Error('No query was specified for this insight');
|
||||
}
|
||||
|
||||
if (!types.isStringArray(this.insightConfig.query)
|
||||
&& !types.isString(this.insightConfig.query)
|
||||
&& !types.isString(this.insightConfig.queryFile)) {
|
||||
throw new Error('Invalid query or queryfile specified');
|
||||
}
|
||||
}
|
||||
|
||||
private _parseConfig(): Thenable<void[]> {
|
||||
let promises: Array<Promise<void>> = [];
|
||||
|
||||
this._typeKey = Object.keys(this.insightConfig.type)[0];
|
||||
|
||||
if (types.isStringArray(this.insightConfig.query)) {
|
||||
this.insightConfig.query = this.insightConfig.query.join(' ');
|
||||
} else if (this.insightConfig.queryFile) {
|
||||
let filePath = this.insightConfig.queryFile;
|
||||
// check for workspace relative path
|
||||
let match = filePath.match(insertValueRegex);
|
||||
if (match && match.length > 0 && match[1] === 'workspaceRoot') {
|
||||
filePath = filePath.replace(match[0], '');
|
||||
filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
|
||||
}
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
pfs.readFile(filePath).then(
|
||||
buffer => {
|
||||
this.insightConfig.query = buffer.toString();
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { join } from 'path';
|
||||
|
||||
import { registerDashboardWidget, registerNonCustomDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
import { Extensions as InsightExtensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { IInsightsConfig } from './interfaces';
|
||||
import { insightsContribution, insightsSchema } from 'sql/parts/dashboard/widgets/insights/insightsWidgetSchemas';
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
|
||||
|
||||
interface IInsightTypeContrib {
|
||||
id: string;
|
||||
contrib: IInsightsConfig;
|
||||
}
|
||||
|
||||
registerDashboardWidget('insights-widget', '', insightsSchema);
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IInsightTypeContrib | IInsightTypeContrib[]>('insights', [], insightsContribution).setHandler(extensions => {
|
||||
|
||||
function handleCommand(insight: IInsightTypeContrib, extension: IExtensionPointUser<any>) {
|
||||
|
||||
if (insight.contrib.queryFile) {
|
||||
insight.contrib.queryFile = join(extension.description.extensionFolderPath, insight.contrib.queryFile);
|
||||
}
|
||||
|
||||
if (insight.contrib.details && insight.contrib.details.queryFile) {
|
||||
insight.contrib.details.queryFile = join(extension.description.extensionFolderPath, insight.contrib.details.queryFile);
|
||||
}
|
||||
|
||||
registerNonCustomDashboardWidget(insight.id, '', insight.contrib);
|
||||
insightRegistry.registerExtensionInsight(insight.id, insight.contrib);
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IInsightTypeContrib>(value)) {
|
||||
for (let command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsightRegistry, Extensions as InsightExtensions } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { ITaskRegistry, Extensions as TaskExtensions } from 'sql/platform/tasks/taskRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
|
||||
const taskRegistry = Registry.as<ITaskRegistry>(TaskExtensions.TaskContribution);
|
||||
|
||||
export const insightsSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('insightWidgetDescription', 'Adds a widget that can query a server or database and display the results in multiple ways - as a chart, summarized count, and more'),
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightIdDescription', 'Unique Identifier used for cacheing the results of the insight.')
|
||||
},
|
||||
type: {
|
||||
type: 'object',
|
||||
properties: insightRegistry.insightSchema.properties,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
},
|
||||
query: {
|
||||
type: ['string', 'array'],
|
||||
description: nls.localize('insightQueryDescription', 'SQL query to run. This should return exactly 1 resultset.')
|
||||
},
|
||||
queryFile: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightQueryFileDescription', '[Optional] path to a file that contains a query. Use if "query" is not set')
|
||||
},
|
||||
details: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: ['string', 'array']
|
||||
},
|
||||
queryFile: {
|
||||
type: 'string'
|
||||
},
|
||||
value: {
|
||||
type: 'string'
|
||||
},
|
||||
label: {
|
||||
type: ['string', 'object'],
|
||||
properties: {
|
||||
column: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
},
|
||||
state: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
if: {
|
||||
type: 'string',
|
||||
enum: ['equals', 'notEquals', 'greaterThanOrEquals', 'greaterThan', 'lessThanOrEquals', 'lessThan', 'always']
|
||||
},
|
||||
equals: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
types: {
|
||||
type: 'object',
|
||||
properties: taskRegistry.taskSchemas
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionDatabaseDescription', 'Target database for the action; can use the format "${columnName} to use a data driven column name.')
|
||||
},
|
||||
server: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionServerDescription', 'Target server for the action; can use the format "${columnName} to use a data driven column name.')
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionUserDescription', 'Target user for the action; can use the format "${columnName} to use a data driven column name.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const insightType: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('carbon.extension.contributes.insightType.id', 'Identifier of the insight'),
|
||||
type: 'string'
|
||||
},
|
||||
contrib: insightsSchema
|
||||
}
|
||||
};
|
||||
|
||||
export const insightsContribution: IJSONSchema = {
|
||||
description: nls.localize('carbon.extension.contributes.insights', "Contributes insights to the dashboard palette."),
|
||||
oneOf: [
|
||||
insightType,
|
||||
{
|
||||
type: 'array',
|
||||
items: insightType
|
||||
}
|
||||
]
|
||||
};
|
||||
60
src/sql/parts/dashboard/widgets/insights/interfaces.ts
Normal file
60
src/sql/parts/dashboard/widgets/insights/interfaces.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IStateCondition {
|
||||
condition: {
|
||||
if: string,
|
||||
equals?: string
|
||||
};
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IInsightsLabel {
|
||||
column: string;
|
||||
icon?: string;
|
||||
state?: Array<IStateCondition>;
|
||||
}
|
||||
|
||||
export interface IInsightsConfigDetails {
|
||||
query?: string | Array<string>;
|
||||
queryFile?: string;
|
||||
label?: string | IInsightsLabel;
|
||||
value?: string;
|
||||
actions?: {
|
||||
types: Array<string>;
|
||||
database?: string;
|
||||
server?: string;
|
||||
user?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IInsightData {
|
||||
columns: Array<string>;
|
||||
rows: Array<Array<string>>;
|
||||
}
|
||||
|
||||
export interface IInsightsView {
|
||||
data: IInsightData;
|
||||
config?: { [key: string]: any };
|
||||
init?: () => void;
|
||||
}
|
||||
|
||||
export interface ISize {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IInsightsConfig {
|
||||
cacheId?: string;
|
||||
type: any;
|
||||
name?: string;
|
||||
provider?: string;
|
||||
edition?: number | Array<number>;
|
||||
gridItemConfig?: ISize;
|
||||
query?: string | Array<string>;
|
||||
queryFile?: string;
|
||||
details?: IInsightsConfigDetails;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts/ng2-charts';
|
||||
|
||||
/* SQL Imports */
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
|
||||
import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { memoize, unmemoize } from 'sql/base/common/decorators';
|
||||
|
||||
/* VS Imports */
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
export enum ChartType {
|
||||
Bar = 'bar',
|
||||
Doughnut = 'doughnut',
|
||||
HorizontalBar = 'horizontalBar',
|
||||
Line = 'line',
|
||||
Pie = 'pie',
|
||||
TimeSeries = 'timeSeries',
|
||||
Scatter = 'scatter'
|
||||
}
|
||||
|
||||
export enum DataDirection {
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal'
|
||||
}
|
||||
|
||||
export enum LegendPosition {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export function customMixin(destination: any, source: any, overwrite?: boolean): any {
|
||||
if (types.isObject(source)) {
|
||||
mixin(destination, source, overwrite, customMixin);
|
||||
} else if (types.isArray(source)) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (destination[i]) {
|
||||
mixin(destination[i], source[i], overwrite, customMixin);
|
||||
} else {
|
||||
destination[i] = source[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination = source;
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export interface IDataSet {
|
||||
data: Array<number>;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IPointDataSet {
|
||||
data: Array<{ x: number | string, y: number }>;
|
||||
label?: string;
|
||||
fill: boolean;
|
||||
backgroundColor?: Color;
|
||||
}
|
||||
|
||||
export interface IChartConfig {
|
||||
colorMap?: { [column: string]: string };
|
||||
labelFirstColumn?: boolean;
|
||||
legendPosition?: LegendPosition;
|
||||
dataDirection?: DataDirection;
|
||||
columnsAsLabels?: boolean;
|
||||
}
|
||||
|
||||
export const defaultChartConfig: IChartConfig = {
|
||||
labelFirstColumn: false,
|
||||
columnsAsLabels: false,
|
||||
legendPosition: LegendPosition.Top,
|
||||
dataDirection: DataDirection.Vertical
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: ` <div style="display: block; width: 100%; height: 100%; position: relative">
|
||||
<canvas #canvas *ngIf="_isDataAvailable"
|
||||
baseChart
|
||||
[datasets]="chartData"
|
||||
[labels]="labels"
|
||||
[chartType]="chartType"
|
||||
[colors]="colors"
|
||||
[options]="_options"></canvas>
|
||||
</div>`
|
||||
})
|
||||
export abstract class ChartInsight implements IInsightsView, OnDestroy {
|
||||
private _isDataAvailable: boolean = false;
|
||||
private _options: any = {};
|
||||
|
||||
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
|
||||
|
||||
protected _defaultConfig = defaultChartConfig;
|
||||
protected _disposables: Array<IDisposable> = [];
|
||||
protected _config: IChartConfig;
|
||||
protected _data: IInsightData;
|
||||
|
||||
protected abstract get chartType(): ChartType;
|
||||
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService) { }
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach(item => item.dispose());
|
||||
}
|
||||
|
||||
init() {
|
||||
this._disposables.push(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
|
||||
// Note: must use a boolean to not render the canvas until all properties such as the labels and chart type are set.
|
||||
// This is because chart.js doesn't auto-update anything other than dataset when re-rendering so defaults are used
|
||||
// hence it's easier to not render until ready
|
||||
this.options = mixin(this.options, { maintainAspectRatio: false });
|
||||
this._isDataAvailable = true;
|
||||
this._changeRef.detectChanges();
|
||||
TelemetryUtils.addTelemetry(this._bootstrapService.telemetryService, TelemetryKeys.ChartCreated, { type: this.chartType });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options for the chart; handles rerendering the chart if needed
|
||||
*/
|
||||
public set options(options: any) {
|
||||
this._options = options;
|
||||
if (this._isDataAvailable) {
|
||||
this._options = mixin({}, mixin(this._options, { animation: { duration: 0 } }));
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public get options(): any {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
protected updateTheme(e: IColorTheme): void {
|
||||
let options = {
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
}
|
||||
}
|
||||
};
|
||||
this.options = mixin({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
// cheaper refresh but causes problems when change data for rerender
|
||||
this._chart.ngOnChanges({});
|
||||
}
|
||||
|
||||
public getCanvasData(): string {
|
||||
if (this._chart && this._chart.chart) {
|
||||
return this._chart.chart.toBase64Image();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
// unmemoize chart data as the data needs to be recalced
|
||||
unmemoize(this, 'chartData');
|
||||
unmemoize(this, 'labels');
|
||||
this._data = data;
|
||||
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
protected clearMemoize(): void {
|
||||
// unmemoize getters since their result can be changed by a new config
|
||||
unmemoize(this, 'getChartData');
|
||||
unmemoize(this, 'getLabels');
|
||||
unmemoize(this, 'colors');
|
||||
}
|
||||
|
||||
@Input() set config(config: IChartConfig) {
|
||||
this.clearMemoize();
|
||||
this._config = mixin(config, this._defaultConfig, false);
|
||||
this.legendPosition = this._config.legendPosition;
|
||||
if (this._isDataAvailable) {
|
||||
this._options = mixin({}, mixin(this._options, { animation: false }));
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/* Typescript does not allow you to access getters/setters for super classes.
|
||||
This is a workaround that allows us to still call base getter */
|
||||
@memoize
|
||||
protected getChartData(): Array<IDataSet> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
if (this._config.labelFirstColumn) {
|
||||
return this._data.rows.map((row) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)).slice(1),
|
||||
label: row[0]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return this._data.rows.map((row, i) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)),
|
||||
label: 'Series' + i
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this._config.columnsAsLabels) {
|
||||
return this._data.rows[0].map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i])),
|
||||
label: this._data.columns[i]
|
||||
};
|
||||
}).slice(1);
|
||||
} else {
|
||||
return this._data.rows[0].map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i])),
|
||||
label: 'Series' + i
|
||||
};
|
||||
}).slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
return this.getChartData();
|
||||
}
|
||||
|
||||
@memoize
|
||||
public getLabels(): Array<string> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
return this._data.columns;
|
||||
} else {
|
||||
return this._data.rows.map(row => row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
return this.getLabels();
|
||||
}
|
||||
|
||||
|
||||
@memoize
|
||||
private get colors(): { backgroundColor: string[] }[] {
|
||||
if (this._config && this._config.colorMap) {
|
||||
let backgroundColor = this.labels.map((item) => {
|
||||
return this._config.colorMap[item];
|
||||
});
|
||||
let colorsMap = { backgroundColor };
|
||||
return [colorsMap];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set legendPosition(input: LegendPosition) {
|
||||
let options = {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
};
|
||||
if (input === 'none') {
|
||||
options.legend.display = false;
|
||||
} else {
|
||||
options.legend.position = input;
|
||||
}
|
||||
this.options = mixin(this.options, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export const chartInsightSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('chartInsightDescription', 'Displays results of a query as a chart on the dashboard'),
|
||||
properties: {
|
||||
colorMap: {
|
||||
type: 'object',
|
||||
description: nls.localize('colorMapDescription', 'Maps "column name" -> color. for example add "column1": red to ensure this column uses a red color ')
|
||||
},
|
||||
legendPosition: {
|
||||
type: 'string',
|
||||
description: nls.localize('legendDescription', 'Indicates preferred position and visibility of the chart legend. These are the column names from your query, and map to the label of each chart entry'),
|
||||
default: 'none',
|
||||
enum: ['top', 'bottom', 'left', 'right', 'none']
|
||||
},
|
||||
labelFirstColumn: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('labelFirstColumnDescription', 'If dataDirection is horizontal, setting this to true uses the first columns value for the legend.'),
|
||||
default: false
|
||||
},
|
||||
columnsAsLabels: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('columnsAsLabels', 'If dataDirection is vertical, setting this to true will use the columns names for the legend.'),
|
||||
default: false
|
||||
},
|
||||
dataDirection: {
|
||||
type: 'string',
|
||||
description: nls.localize('dataDirectionDescription', 'Defines whether the data is read from a column (vertical) or a row (horizontal). For time series this is ignored as direction must be vertical.'),
|
||||
default: 'vertical',
|
||||
enum: ['vertical', 'horizontal'],
|
||||
enumDescriptions: ['When vertical, the first column is used to define the x-axis labels, with other columns expected to be numerical.', 'When horizontal, the column names are used as the x-axis labels.']
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartInsight, ChartType, customMixin } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
|
||||
|
||||
export default class BarChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Bar;
|
||||
|
||||
protected updateTheme(e: IColorTheme): void {
|
||||
super.updateTheme(e);
|
||||
let options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
ticks: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
gridLines: {
|
||||
color: e.getColor(editorLineNumbers)
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
ticks: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
gridLines: {
|
||||
color: e.getColor(editorLineNumbers)
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = mixin({}, mixin(this.options, options, true, customMixin));
|
||||
}
|
||||
}
|
||||
@@ -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 { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import BarChart from './barChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const barSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('bar', '', barSchema, BarChart);
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import PieChart from './pieChart.component';
|
||||
|
||||
export default class DoughnutChart extends PieChart {
|
||||
protected readonly chartType: ChartType = ChartType.Doughnut;
|
||||
}
|
||||
@@ -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 { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import DoughnutChart from './doughnutChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const doughnutChartSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('doughnut', '', doughnutChartSchema, DoughnutChart);
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import BarChart from './barChart.component';
|
||||
|
||||
export default class HorizontalBarChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.HorizontalBar;
|
||||
}
|
||||
@@ -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 { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import HorizontalBarChart from './horizontalBarChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const horizontalBarSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('horizontalBar', '', horizontalBarSchema, HorizontalBarChart);
|
||||
@@ -0,0 +1,98 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartType, customMixin, IChartConfig, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import BarChart from './barChart.component';
|
||||
import { memoize, unmemoize } from 'sql/base/common/decorators';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { clone } from 'vs/base/common/objects';
|
||||
|
||||
export enum DataType {
|
||||
Number = 'number',
|
||||
Point = 'point'
|
||||
}
|
||||
|
||||
export interface ILineConfig extends IChartConfig {
|
||||
dataType?: DataType;
|
||||
}
|
||||
|
||||
const defaultLineConfig = mixin(clone(defaultChartConfig), { dataType: 'number' }) as ILineConfig;
|
||||
|
||||
export default class LineChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.Line;
|
||||
protected _config: ILineConfig;
|
||||
protected _defaultConfig = defaultLineConfig;
|
||||
|
||||
public init() {
|
||||
if (this._config.dataType === DataType.Point) {
|
||||
this.addAxisLabels();
|
||||
}
|
||||
super.init();
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getChartData();
|
||||
} else {
|
||||
return this.getDataAsPoint();
|
||||
}
|
||||
}
|
||||
|
||||
protected clearMemoize() {
|
||||
super.clearMemoize();
|
||||
unmemoize(this, 'getDataAsPoint');
|
||||
}
|
||||
|
||||
@memoize
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
let dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
let legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: Number(row[1]), y: Number(row[2]) });
|
||||
}
|
||||
});
|
||||
return Object.values(dataSetMap);
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getLabels();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
let xLabel = this._data.columns[1] || 'x';
|
||||
let yLabel = this._data.columns[2] || 'y';
|
||||
let options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: xLabel
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: yLabel,
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = mixin(this.options, options, true, customMixin);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import LineChart from './lineChart.component';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
properties: {
|
||||
dataType: {
|
||||
type: 'string',
|
||||
description: nls.localize('dataTypeDescription', 'Indicates data property of a data set for a chart.'),
|
||||
default: 'number',
|
||||
enum: ['number', 'point'],
|
||||
enumDescriptions: ['Set "number" if the data values are contained in 1 column.', 'Set "point" if the data is an {x,y} combination requiring 2 columns for each value.']
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const lineSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('line', '', lineSchema, LineChart);
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartInsight, ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
|
||||
export default class PieChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Pie;
|
||||
}
|
||||
@@ -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 { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import PieChart from './pieChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const pieSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('pie', '', pieSchema, PieChart);
|
||||
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartType, defaultChartConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import LineChart, { ILineConfig } from './lineChart.component';
|
||||
|
||||
import { mixin, clone } from 'vs/base/common/objects';
|
||||
|
||||
const defaultScatterConfig = mixin(clone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;
|
||||
|
||||
export default class ScatterChart extends LineChart {
|
||||
protected readonly chartType: ChartType = ChartType.Scatter;
|
||||
protected _defaultConfig = defaultScatterConfig;
|
||||
}
|
||||
@@ -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 { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import ScatterChart from './scatterChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
};
|
||||
|
||||
const scatterSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('scatter', '', scatterSchema, ScatterChart);
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultChartConfig, IPointDataSet, ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import LineChart, { ILineConfig } from './lineChart.component';
|
||||
|
||||
import { mixin, clone } from 'vs/base/common/objects';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
|
||||
const defaultTimeSeriesConfig = mixin(clone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;
|
||||
|
||||
export default class TimeSeriesChart extends LineChart {
|
||||
protected _defaultConfig = defaultTimeSeriesConfig;
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
let xLabel = this.getLabels()[1] || 'x';
|
||||
let yLabel = this.getLabels()[2] || 'y';
|
||||
|
||||
let options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: xLabel
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: yLabel
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = Object.assign({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
let dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
let legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) });
|
||||
|
||||
if (this.chartType === ChartType.Scatter) {
|
||||
dataSetMap[legend].backgroundColor = Color.cyan;
|
||||
}
|
||||
}
|
||||
});
|
||||
return Object.values(dataSetMap);
|
||||
}
|
||||
}
|
||||
@@ -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 { mixin, clone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import TimeSeriesChart from './timeSeriesChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
};
|
||||
|
||||
const timeSeriesSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('timeSeries', '', timeSeriesSchema, TimeSeriesChart);
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef } from '@angular/core';
|
||||
|
||||
import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div style="margin-left: 5px" *ngFor="let label of _labels; let i = index">
|
||||
<span style="font-size: 20px">{{_values[i]}} </span>
|
||||
<span>{{_labels[i]}}</span>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export default class CountInsight implements IInsightsView {
|
||||
private _labels: Array<string>;
|
||||
private _values: Array<string>;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef) { }
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
this._labels = [];
|
||||
this._labels = data.columns;
|
||||
this._values = data.rows[0];
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
@@ -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 { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import CountInsight from './countInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
let countInsightSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('countInsightDescription', 'For each column in a resultset, displays the value in row 0 as a count followed by the column name. Supports "1 Healthy", "3 Unhealthy" for example, where "Healthy" is the column name and 1 is the value in row 1 cell 1')
|
||||
};
|
||||
|
||||
registerInsight('count', '', countInsightSchema, CountInsight);
|
||||
@@ -0,0 +1,80 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, OnInit, ElementRef } from '@angular/core';
|
||||
|
||||
import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
|
||||
interface IConfig {
|
||||
encoding?: string;
|
||||
imageFormat?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
encoding: 'hex',
|
||||
imageFormat: 'jpeg'
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngIf="hasData" #container style="display: block">
|
||||
<img #image src="{{source}}" >
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export default class ImageInsight implements IInsightsView, OnInit {
|
||||
private _rawSource: string;
|
||||
private _config: IConfig;
|
||||
|
||||
@ViewChild('image') private image: ElementRef;
|
||||
@ViewChild('container') private container: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit() {
|
||||
let size = Math.min(this.container.nativeElement.parentElement.parentElement.offsetHeight, this.container.nativeElement.parentElement.parentElement.offsetWidth);
|
||||
this.image.nativeElement.style.width = size + 'px';
|
||||
this.image.nativeElement.style.height = size + 'px';
|
||||
}
|
||||
|
||||
@Input() set config(config: { [key: string]: any }) {
|
||||
this._config = mixin(config, defaultConfig, false);
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
let self = this;
|
||||
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
|
||||
self._rawSource = data.rows[0][0];
|
||||
} else {
|
||||
this._rawSource = '';
|
||||
}
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public get hasData(): boolean {
|
||||
return this._rawSource && this._rawSource !== '';
|
||||
}
|
||||
|
||||
public get source(): string {
|
||||
let img = this._rawSource;
|
||||
if (this._config.encoding === 'hex') {
|
||||
img = ImageInsight._hexToBase64(img);
|
||||
}
|
||||
return `data:image/${this._config.imageFormat};base64,${img}`;
|
||||
}
|
||||
|
||||
private static _hexToBase64(hexVal: string) {
|
||||
|
||||
if (hexVal.startsWith('0x')) {
|
||||
hexVal = hexVal.slice(2);
|
||||
}
|
||||
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
|
||||
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import ImageInsight from './imageInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
let imageInsightSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('imageInsightDescription', 'Displays an image, for example one returned by an R query using ggplot2'),
|
||||
properties: {
|
||||
imageFormat: {
|
||||
type: 'string',
|
||||
description: nls.localize('imageFormatDescription', 'What format is expected - is this a JPEG, PNG or other format?'),
|
||||
default: 'jpeg',
|
||||
enum: ['jpeg', 'png']
|
||||
},
|
||||
encoding: {
|
||||
type: 'string',
|
||||
description: nls.localize('encodingDescription', 'Is this encoded as hex, base64 or some other format?'),
|
||||
default: 'hex',
|
||||
enum: ['hex', 'base64']
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
registerInsight('image', '', imageInsightSchema, ImageInsight);
|
||||
108
src/sql/parts/dashboard/widgets/properties/propertiesJson.ts
Normal file
108
src/sql/parts/dashboard/widgets/properties/propertiesJson.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProviderProperties } from './propertiesWidget.component';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
let azureEditionDisplayName = nls.localize('azureEdition', 'Edition');
|
||||
let azureType = nls.localize('azureType', 'Type');
|
||||
|
||||
export const properties: Array<ProviderProperties> = [
|
||||
{
|
||||
provider: 'MSSQL',
|
||||
flavors: [
|
||||
{
|
||||
flavor: 'on_prem',
|
||||
condition: {
|
||||
field: 'isCloud',
|
||||
operator: '!=',
|
||||
value: true
|
||||
},
|
||||
databaseProperties: [
|
||||
{
|
||||
displayName: nls.localize('recoveryModel', 'Recovery Model'),
|
||||
value: 'recoveryModel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastDatabaseBackup', 'Last Database Backup'),
|
||||
value: 'lastBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastLogBackup', 'Last Log Backup'),
|
||||
value: 'lastLogBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
],
|
||||
serverProperties: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('edition', 'Edition'),
|
||||
value: 'serverEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('computerName', 'Computer Name'),
|
||||
value: 'machineName'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('osVersion', 'OS Version'),
|
||||
value: 'osVersion'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
flavor: 'cloud',
|
||||
condition: {
|
||||
field: 'isCloud',
|
||||
operator: '==',
|
||||
value: true
|
||||
},
|
||||
databaseProperties: [
|
||||
{
|
||||
displayName: azureEditionDisplayName,
|
||||
value: 'azureEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('serviceLevelObjective', 'Pricing Tier'),
|
||||
value: 'serviceLevelObjective'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
],
|
||||
serverProperties: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: azureType,
|
||||
value: 'serverEdition'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div #parent style="position: absolute; height: 100%; width: 100%; margin: 10px 0px 10px 0px; ">
|
||||
<div [style.margin-right.px]="_clipped ? 30 : 0" [style.width]="_clipped ? 94 + '%' : '100%'" style="overflow: hidden">
|
||||
<span #child style="white-space : nowrap; width: fit-content">
|
||||
<ng-template ngFor let-item [ngForOf]="properties">
|
||||
<span style="margin-left: 10px; display: inline-block;">
|
||||
<div style="font-size: 11px; font-weight: lighter">{{item.displayName}}</div>
|
||||
<div>{{item.value}}</div>
|
||||
</span>
|
||||
</ng-template>
|
||||
</span>
|
||||
</div>
|
||||
<span *ngIf="_clipped" style="position: absolute; right: 0; top: 0; padding-top: 5px; padding-right: 14px; z-index: 2">
|
||||
...
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,265 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ElementRef, OnDestroy, ViewChild } from '@angular/core';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
|
||||
import { error } from 'sql/base/common/log';
|
||||
|
||||
import { properties } from './propertiesJson';
|
||||
|
||||
import { DatabaseInfo, ServerInfo } from 'data';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { EventType, addDisposableListener } from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export interface PropertiesConfig {
|
||||
properties: Array<Property>;
|
||||
}
|
||||
|
||||
export interface FlavorProperties {
|
||||
flavor: string;
|
||||
condition?: {
|
||||
field: string;
|
||||
operator: '==' | '<=' | '>=' | '!=';
|
||||
value: string | boolean;
|
||||
};
|
||||
databaseProperties: Array<Property>;
|
||||
serverProperties: Array<Property>;
|
||||
}
|
||||
|
||||
export interface ProviderProperties {
|
||||
provider: string;
|
||||
flavors: Array<FlavorProperties>;
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
displayName: string;
|
||||
value: string;
|
||||
ignore?: Array<string>;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface DisplayProperty {
|
||||
displayName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'properties-widget',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/properties/propertiesWidget.component.html'))
|
||||
})
|
||||
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
|
||||
private _connection: ConnectionManagementInfo;
|
||||
private _databaseInfo: DatabaseInfo;
|
||||
private _clipped: boolean;
|
||||
private _disposables: Array<IDisposable> = [];
|
||||
private properties: Array<DisplayProperty>;
|
||||
private _hasInit = false;
|
||||
|
||||
@ViewChild('child', { read: ElementRef }) private _child: ElementRef;
|
||||
@ViewChild('parent', { read: ElementRef }) private _parent: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
consoleError?: ((message?: any, ...optionalParams: any[]) => void)
|
||||
) {
|
||||
super();
|
||||
if (consoleError) {
|
||||
this.consoleError = consoleError;
|
||||
}
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._hasInit = true;
|
||||
this._disposables.push(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach(i => i.dispose());
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this._connection = this._bootstrap.connectionManagementService.connectionInfo;
|
||||
this._disposables.push(toDisposableSubscription(this._bootstrap.adminService.databaseInfo.subscribe(data => {
|
||||
this._databaseInfo = data;
|
||||
this._changeRef.detectChanges();
|
||||
this.parseProperties();
|
||||
if (this._hasInit) {
|
||||
this.handleClipping();
|
||||
}
|
||||
}, error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.properties.error', "Unable to load dashboard properties");
|
||||
})));
|
||||
}
|
||||
|
||||
private handleClipping(): void {
|
||||
if (this._child.nativeElement.offsetWidth > this._parent.nativeElement.offsetWidth) {
|
||||
this._clipped = true;
|
||||
} else {
|
||||
this._clipped = false;
|
||||
}
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private parseProperties() {
|
||||
let provider = this._config.provider;
|
||||
|
||||
let propertyArray: Array<Property>;
|
||||
|
||||
// if config exists use that, otherwise use default
|
||||
if (this._config.widget['properties-widget'] && this._config.widget['properties-widget'].properties) {
|
||||
let config = <PropertiesConfig>this._config.widget['properties-widget'];
|
||||
propertyArray = config.properties;
|
||||
} else {
|
||||
let propertiesConfig: Array<ProviderProperties> = properties;
|
||||
// ensure we have a properties file
|
||||
if (!Array.isArray(propertiesConfig)) {
|
||||
this.consoleError('Could not load properties JSON');
|
||||
return;
|
||||
}
|
||||
|
||||
// filter the properties provided based on provider name
|
||||
let providerPropertiesArray = propertiesConfig.filter((item) => {
|
||||
return item.provider === provider;
|
||||
});
|
||||
|
||||
// Error handling on provider
|
||||
if (providerPropertiesArray.length === 0) {
|
||||
this.consoleError('Could not locate properties for provider: ', provider);
|
||||
return;
|
||||
} else if (providerPropertiesArray.length > 1) {
|
||||
this.consoleError('Found multiple property definitions for provider ', provider);
|
||||
return;
|
||||
}
|
||||
|
||||
let providerProperties = providerPropertiesArray[0];
|
||||
|
||||
let flavor: FlavorProperties;
|
||||
|
||||
// find correct flavor
|
||||
if (providerProperties.flavors.length === 1) {
|
||||
flavor = providerProperties.flavors[0];
|
||||
} else if (providerProperties.flavors.length === 0) {
|
||||
this.consoleError('No flavor definitions found for "', provider,
|
||||
'. If there are not multiple flavors of this provider, add one flavor without a condition');
|
||||
return;
|
||||
} else {
|
||||
let flavorArray = providerProperties.flavors.filter((item) => {
|
||||
let condition = this._connection.serverInfo[item.condition.field];
|
||||
switch (item.condition.operator) {
|
||||
case '==':
|
||||
return condition === item.condition.value;
|
||||
case '!=':
|
||||
return condition !== item.condition.value;
|
||||
case '>=':
|
||||
return condition >= item.condition.value;
|
||||
case '<=':
|
||||
return condition <= item.condition.value;
|
||||
default:
|
||||
this.consoleError('Could not parse operator: "', item.condition.operator,
|
||||
'" on item "', item, '"');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flavorArray.length === 0) {
|
||||
this.consoleError('Could not determine flavor');
|
||||
return;
|
||||
} else if (flavorArray.length > 1) {
|
||||
this.consoleError('Multiple flavors matched correctly for this provider', provider);
|
||||
return;
|
||||
}
|
||||
|
||||
flavor = flavorArray[0];
|
||||
}
|
||||
|
||||
// determine what context we should be pulling from
|
||||
if (this._config.context === 'database') {
|
||||
if (!Array.isArray(flavor.databaseProperties)) {
|
||||
this.consoleError('flavor', flavor.flavor, ' does not have a definition for database properties');
|
||||
}
|
||||
|
||||
if (!Array.isArray(flavor.serverProperties)) {
|
||||
this.consoleError('flavor', flavor.flavor, ' does not have a definition for server properties');
|
||||
}
|
||||
|
||||
propertyArray = flavor.databaseProperties;
|
||||
} else {
|
||||
if (!Array.isArray(flavor.serverProperties)) {
|
||||
this.consoleError('flavor', flavor.flavor, ' does not have a definition for server properties');
|
||||
}
|
||||
|
||||
propertyArray = flavor.serverProperties;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let infoObject: ServerInfo | {};
|
||||
if (this._config.context === 'database') {
|
||||
if (this._databaseInfo && this._databaseInfo.options) {
|
||||
infoObject = this._databaseInfo.options;
|
||||
}
|
||||
} else {
|
||||
infoObject = this._connection.serverInfo;
|
||||
}
|
||||
|
||||
// iterate over properties and display them
|
||||
this.properties = [];
|
||||
for (let i = 0; i < propertyArray.length; i++) {
|
||||
let property = propertyArray[i];
|
||||
let assignProperty = {};
|
||||
let propertyObject = this.getValueOrDefault<string>(infoObject, property.value, property.default || '--');
|
||||
|
||||
// make sure the value we got shouldn't be ignored
|
||||
if (property.ignore !== undefined && propertyObject !== '--') {
|
||||
for (let j = 0; j < property.ignore.length; j++) {
|
||||
// set to default value if we should be ignoring it's value
|
||||
if (propertyObject === property.ignore[0]) {
|
||||
propertyObject = property.default || '--';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assignProperty['displayName'] = property.displayName;
|
||||
assignProperty['value'] = propertyObject;
|
||||
this.properties.push(<DisplayProperty>assignProperty);
|
||||
}
|
||||
|
||||
if (this._hasInit) {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private getValueOrDefault<T>(infoObject: ServerInfo | {}, propertyValue: string, defaultVal?: any): T {
|
||||
let val: T = undefined;
|
||||
if (infoObject) {
|
||||
val = infoObject[propertyValue];
|
||||
}
|
||||
if (types.isUndefinedOrNull(val)) {
|
||||
val = defaultVal;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// overwrittable console.error for testing
|
||||
private consoleError(message?: any, ...optionalParams: any[]): void {
|
||||
error(message, optionalParams);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div class="task-widget">
|
||||
<ng-template ngFor let-task [ngForOf]="_actions" let-i="index">
|
||||
<div (click)="runTask(task)"
|
||||
style="position: absolute; cursor: pointer; display: flex; flex-flow: row; align-items: center; text-align: center"
|
||||
class="task-tile"
|
||||
[style.height.px]="_size"
|
||||
[style.width.px]="_size"
|
||||
[style.transform]="calculateTransform(i)">
|
||||
<div style="flex: 1 1 auto; display: flex; flex-flow: column; align-items: center">
|
||||
<span [ngClass]="['icon', task.icon]" style="padding: 15px"></span>
|
||||
<div>{{task.label}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
117
src/sql/parts/dashboard/widgets/tasks/tasksWidget.component.ts
Normal file
117
src/sql/parts/dashboard/widgets/tasks/tasksWidget.component.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
|
||||
/* Node Modules */
|
||||
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
/* SQL imports */
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { ITaskRegistry, Extensions, ActionICtor } from 'sql/platform/tasks/taskRegistry';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { ITaskActionContext } from 'sql/workbench/common/actions';
|
||||
|
||||
/* VS imports */
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant, ICssStyleCollector, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
interface IConfig {
|
||||
tasks: Array<Object>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tasks-widget',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/tasks/tasksWidget.component.html'))
|
||||
})
|
||||
export class TasksWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
|
||||
private _size: number = 100;
|
||||
private _margins: number = 10;
|
||||
private _rows: number = 2;
|
||||
private _isAzure = false;
|
||||
private _themeDispose: IDisposable;
|
||||
private _actions: Array<Action> = [];
|
||||
private _profile: IConnectionProfile;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => DomSanitizer)) private _sanitizer: DomSanitizer,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig
|
||||
) {
|
||||
super();
|
||||
this._profile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
|
||||
let registry = Registry.as<ITaskRegistry>(Extensions.TaskContribution);
|
||||
let tasksConfig = <IConfig>Object.values(this._config.widget)[0];
|
||||
let connInfo = this._bootstrap.connectionManagementService.connectionInfo;
|
||||
if (tasksConfig.tasks) {
|
||||
Object.keys(tasksConfig.tasks).forEach((item) => {
|
||||
if (registry.idToCtorMap[item]) {
|
||||
let ctor = registry.idToCtorMap[item];
|
||||
this._actions.push(this._bootstrap.instantiationService.createInstance(ctor, ctor.ID, ctor.LABEL, ctor.ICON));
|
||||
} else {
|
||||
this._bootstrap.messageService.show(Severity.Warning, nls.localize('missingTask', 'Could not find task {0}; are you missing an extension?', item));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let actions = Object.values(registry.idToCtorMap).map((item: ActionICtor) => {
|
||||
|
||||
let action = this._bootstrap.instantiationService.createInstance(item, item.ID, item.LABEL, item.ICON);
|
||||
if (this._bootstrap.CapabilitiesService.isFeatureAvailable(action, connInfo)) {
|
||||
return action;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
this._actions = actions.filter(x => x !== undefined);
|
||||
}
|
||||
|
||||
this._isAzure = connInfo.serverInfo.isCloud;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._themeDispose = registerThemingParticipant(this.registerThemeing);
|
||||
}
|
||||
|
||||
private registerThemeing(theme: ITheme, collector: ICssStyleCollector) {
|
||||
let contrastBorder = theme.getColor(colors.contrastBorder);
|
||||
let sideBarColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND);
|
||||
if (contrastBorder) {
|
||||
let contrastBorderString = contrastBorder.toString();
|
||||
collector.addRule(`.task-widget .task-tile { border: 1px solid ${contrastBorderString} }`);
|
||||
} else {
|
||||
let sideBarColorString = sideBarColor.toString();
|
||||
collector.addRule(`.task-widget .task-tile { background-color: ${sideBarColorString} }`);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._themeDispose.dispose();
|
||||
}
|
||||
|
||||
//tslint:disable-next-line
|
||||
private calculateTransform(index: number): string {
|
||||
let marginy = (1 + (index % this._rows)) * this._margins;
|
||||
let marginx = (1 + (Math.floor(index / 2))) * this._margins;
|
||||
let posx = (this._size * (Math.floor(index / 2))) + marginx;
|
||||
let posy = (this._size * (index % this._rows)) + marginy;
|
||||
return 'translate(' + posx + 'px, ' + posy + 'px)';
|
||||
}
|
||||
|
||||
public runTask(task: Action) {
|
||||
let context: ITaskActionContext = {
|
||||
profile: this._profile
|
||||
};
|
||||
task.run(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
import { Extensions as TaskExtensions, ITaskRegistry } from 'sql/platform/tasks/taskRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
let taskRegistry = <ITaskRegistry>Registry.as(TaskExtensions.TaskContribution);
|
||||
|
||||
let tasksSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tasks: {
|
||||
type: 'object',
|
||||
properties: taskRegistry.taskSchemas
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
registerDashboardWidget('tasks-widget', '', tasksSchema);
|
||||
Reference in New Issue
Block a user