SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

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

View 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 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>

View File

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

View 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 { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
let explorerSchema: IJSONSchema = {
type: 'object',
};
registerDashboardWidget('explorer-widget', '', explorerSchema);

View 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.
*--------------------------------------------------------------------------------------------*/
explorer-widget .list-row .icon {
padding: 10px;
}
explorer-widget .list-row {
display: flex;
flex-direction: row;
align-items: center;
}

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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.']
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.
*--------------------------------------------------------------------------------------------*/
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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'
}
]
}
]
},
];

View File

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

View File

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

View File

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

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

View 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 { 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);