mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-30 09:35:39 -05:00
move code from parts to contrib (#8319)
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { MetadataType } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { SingleConnectionManagementService, CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { ManageActionContext, BaseActionContext } from 'sql/workbench/browser/actions';
|
||||
|
||||
import * as tree from 'vs/base/parts/tree/browser/tree';
|
||||
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext';
|
||||
import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper';
|
||||
|
||||
export declare type TreeResource = IConnectionProfile | ObjectMetadataWrapper;
|
||||
|
||||
// Empty class just for tree input
|
||||
export class ExplorerModel {
|
||||
public static readonly id = generateUuid();
|
||||
}
|
||||
|
||||
export class ExplorerController extends TreeDefaults.DefaultController {
|
||||
private readonly contextKey = new ItemContextKey(this.contextKeyService);
|
||||
|
||||
constructor(
|
||||
// URI for the dashboard for managing, should look into some other way of doing this
|
||||
private _uri,
|
||||
private _connectionService: SingleConnectionManagementService,
|
||||
private _router: Router,
|
||||
private readonly bootStrapService: CommonServiceInterface,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@IEditorProgressService private readonly progressService: IEditorProgressService,
|
||||
@IMenuService private readonly menuService: IMenuService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: tree.ITree, element: TreeResource, event: IMouseEvent, origin: string = 'mouse'): boolean {
|
||||
const payload = { origin: origin };
|
||||
const isDoubleClick = (origin === 'mouse' && event.detail === 2);
|
||||
// Cancel Event
|
||||
const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
|
||||
|
||||
if (!isMouseDown) {
|
||||
event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
tree.setFocus(element, payload);
|
||||
|
||||
if (!(element instanceof ObjectMetadataWrapper) && isDoubleClick) {
|
||||
event.preventDefault(); // focus moves to editor, we need to prevent default
|
||||
this.handleItemDoubleClick(element);
|
||||
} else {
|
||||
tree.setFocus(element, payload);
|
||||
tree.setSelection([element], payload);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onContextMenu(tree: tree.ITree, element: TreeResource, event: tree.ContextMenuEvent): boolean {
|
||||
this.contextKey.set({
|
||||
resource: element,
|
||||
providerName: this.bootStrapService.connectionManagementService.connectionInfo.providerId,
|
||||
isCloud: this.bootStrapService.connectionManagementService.connectionInfo.serverInfo.isCloud,
|
||||
engineEdition: this.bootStrapService.connectionManagementService.connectionInfo.serverInfo.engineEditionId
|
||||
});
|
||||
|
||||
let context: ManageActionContext | BaseActionContext;
|
||||
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
context = {
|
||||
object: element,
|
||||
profile: this._connectionService.connectionInfo.connectionProfile
|
||||
};
|
||||
} else {
|
||||
context = {
|
||||
profile: element,
|
||||
uri: this._uri
|
||||
};
|
||||
}
|
||||
|
||||
const menu = this.menuService.createMenu(MenuId.ExplorerWidgetContext, this.contextKeyService);
|
||||
const primary: IAction[] = [];
|
||||
const secondary: IAction[] = [];
|
||||
const result = { primary, secondary };
|
||||
createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline');
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => { return { x: event.posx, y: event.posy }; },
|
||||
getActions: () => result.secondary,
|
||||
getActionsContext: () => context
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private handleItemDoubleClick(element: IConnectionProfile): void {
|
||||
this.progressService.showWhile(this._connectionService.changeDatabase(element.databaseName).then(result => {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
}));
|
||||
}
|
||||
|
||||
protected onEnter(tree: tree.ITree, event: IKeyboardEvent): boolean {
|
||||
const result = super.onEnter(tree, event);
|
||||
if (result) {
|
||||
const focus = tree.getFocus();
|
||||
if (focus && !(focus instanceof ObjectMetadataWrapper)) {
|
||||
this._connectionService.changeDatabase(focus.databaseName).then(result => {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplorerDataSource implements tree.IDataSource {
|
||||
private _data: TreeResource[];
|
||||
|
||||
public getId(tree: tree.ITree, element: TreeResource | ExplorerModel): string {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return element.urn || element.schema + element.name;
|
||||
} else if (element instanceof ExplorerModel) {
|
||||
return ExplorerModel.id;
|
||||
} else {
|
||||
return (element as IConnectionProfile).getOptionsKey();
|
||||
}
|
||||
}
|
||||
|
||||
public hasChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): boolean {
|
||||
if (element instanceof ExplorerModel) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public getChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise<TreeResource[]> {
|
||||
if (element instanceof ExplorerModel) {
|
||||
return Promise.resolve(this._data);
|
||||
} else {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise<ExplorerModel> {
|
||||
if (element instanceof ExplorerModel) {
|
||||
return Promise.resolve(undefined);
|
||||
} else {
|
||||
return Promise.resolve(new ExplorerModel());
|
||||
}
|
||||
}
|
||||
|
||||
public set data(data: TreeResource[]) {
|
||||
this._data = data;
|
||||
}
|
||||
}
|
||||
|
||||
enum TEMPLATEIDS {
|
||||
profile = 'profile',
|
||||
object = 'object'
|
||||
}
|
||||
|
||||
export interface IListTemplate {
|
||||
icon?: HTMLElement;
|
||||
label: HTMLElement;
|
||||
}
|
||||
|
||||
export class ExplorerRenderer implements tree.IRenderer {
|
||||
public getHeight(tree: tree.ITree, element: TreeResource): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
public getTemplateId(tree: tree.ITree, element: TreeResource): string {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return TEMPLATEIDS.object;
|
||||
} else {
|
||||
return TEMPLATEIDS.profile;
|
||||
}
|
||||
}
|
||||
|
||||
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
|
||||
const row = $('.list-row');
|
||||
const label = $('.label');
|
||||
|
||||
let icon: HTMLElement;
|
||||
if (templateId === TEMPLATEIDS.object) {
|
||||
icon = $('div');
|
||||
} else {
|
||||
icon = $('.icon.database');
|
||||
}
|
||||
|
||||
row.appendChild(icon);
|
||||
row.appendChild(label);
|
||||
container.appendChild(row);
|
||||
|
||||
return { icon, label };
|
||||
}
|
||||
|
||||
public renderElement(tree: tree.ITree, element: TreeResource, templateId: string, templateData: IListTemplate): void {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
switch (element.metadataType) {
|
||||
case MetadataType.Function:
|
||||
templateData.icon.className = 'icon scalarvaluedfunction';
|
||||
break;
|
||||
case MetadataType.SProc:
|
||||
templateData.icon.className = 'icon storedprocedure';
|
||||
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;
|
||||
} else {
|
||||
templateData.label.innerText = element.databaseName;
|
||||
}
|
||||
templateData.label.title = templateData.label.innerText;
|
||||
}
|
||||
|
||||
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
|
||||
// no op
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ExplorerFilter implements tree.IFilter {
|
||||
private _filterString: string;
|
||||
|
||||
public isVisible(tree: tree.ITree, element: TreeResource): boolean {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return this._doIsVisibleObjectMetadata(element);
|
||||
} else {
|
||||
return this._doIsVisibleConnectionProfile(element);
|
||||
}
|
||||
}
|
||||
|
||||
// apply filter to databasename of the profile
|
||||
private _doIsVisibleConnectionProfile(element: IConnectionProfile): boolean {
|
||||
if (!this._filterString) {
|
||||
return true;
|
||||
}
|
||||
const filterString = this._filterString.trim().toLowerCase();
|
||||
return element.databaseName.toLowerCase().indexOf(filterString) > -1;
|
||||
}
|
||||
|
||||
// apply filter for objectmetadatawrapper
|
||||
// could be improved by pre-processing the filter string
|
||||
private _doIsVisibleObjectMetadata(element: ObjectMetadataWrapper): boolean {
|
||||
if (!this._filterString) {
|
||||
return true;
|
||||
}
|
||||
// freeze filter string for edge cases
|
||||
let filterString = this._filterString.trim().toLowerCase();
|
||||
|
||||
// determine if a filter is applied
|
||||
let metadataType: MetadataType;
|
||||
|
||||
if (filterString.indexOf(':') > -1) {
|
||||
const 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 true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataType !== undefined) {
|
||||
return element.metadataType === metadataType && (element.schema + '.' + element.name).toLowerCase().indexOf(filterString) > -1;
|
||||
} else {
|
||||
return (element.schema + '.' + element.name).toLowerCase().indexOf(filterString) > -1;
|
||||
}
|
||||
}
|
||||
|
||||
public set filterString(val: string) {
|
||||
this._filterString = val;
|
||||
}
|
||||
}
|
||||
@@ -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 { ManageAction, ManageActionContext } from 'sql/workbench/browser/actions';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IAngularEventingService } from 'sql/platform/angularEventing/browser/angularEventingService';
|
||||
import { ExecuteCommandAction } from 'vs/platform/actions/common/actions';
|
||||
|
||||
export class ExplorerManageAction extends ManageAction {
|
||||
public static readonly ID = 'explorerwidget.manage';
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||
@IAngularEventingService angularEventingService: IAngularEventingService,
|
||||
) {
|
||||
super(id, label, connectionManagementService, angularEventingService);
|
||||
}
|
||||
|
||||
public run(actionContext: ManageActionContext): Promise<boolean> {
|
||||
const promise = super.run(actionContext);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomExecuteCommandAction extends ExecuteCommandAction {
|
||||
run(context: ManageActionContext): Promise<any> {
|
||||
return super.run(context.profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { MetadataType } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper';
|
||||
|
||||
export declare type ContextResource = IConnectionProfile | ObjectMetadataWrapper;
|
||||
|
||||
export interface IContextValue {
|
||||
resource: ContextResource;
|
||||
providerName: string;
|
||||
isCloud: boolean;
|
||||
engineEdition: number;
|
||||
}
|
||||
|
||||
export class ItemContextKey extends Disposable implements IContextKey<IContextValue> {
|
||||
|
||||
static readonly ItemType = new RawContextKey<string>('itemType', undefined);
|
||||
static readonly Item = new RawContextKey<IContextValue>('item', undefined);
|
||||
static readonly ConnectionProvider = new RawContextKey<string>('provider', undefined);
|
||||
static readonly IsCloud = new RawContextKey<boolean>('isCloud', undefined);
|
||||
static readonly EngineEdition = new RawContextKey<number>('engineEdition', undefined);
|
||||
|
||||
private _itemTypeKey: IContextKey<string>;
|
||||
private _itemKey: IContextKey<IContextValue>;
|
||||
private _connectionProviderKey: IContextKey<string>;
|
||||
private _isCloudKey: IContextKey<boolean>;
|
||||
private _engineEditionKey: IContextKey<number>;
|
||||
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._itemTypeKey = ItemContextKey.ItemType.bindTo(contextKeyService);
|
||||
this._itemKey = ItemContextKey.Item.bindTo(contextKeyService);
|
||||
this._connectionProviderKey = ItemContextKey.ConnectionProvider.bindTo(contextKeyService);
|
||||
this._isCloudKey = ItemContextKey.IsCloud.bindTo(contextKeyService);
|
||||
this._engineEditionKey = ItemContextKey.EngineEdition.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
set(value: IContextValue) {
|
||||
this._itemKey.set(value);
|
||||
this._connectionProviderKey.set(value.providerName.toLowerCase());
|
||||
this._isCloudKey.set(value.isCloud);
|
||||
this._engineEditionKey.set(value.engineEdition);
|
||||
if (value.resource instanceof ObjectMetadataWrapper) {
|
||||
switch (value.resource.metadataType) {
|
||||
case MetadataType.Function:
|
||||
this._itemTypeKey.set('function');
|
||||
break;
|
||||
case MetadataType.SProc:
|
||||
this._itemTypeKey.set('sproc');
|
||||
break;
|
||||
case MetadataType.Table:
|
||||
this._itemTypeKey.set('table');
|
||||
break;
|
||||
case MetadataType.View:
|
||||
this._itemTypeKey.set('view');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this._itemTypeKey.set('database');
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._itemTypeKey.reset();
|
||||
this._itemKey.reset();
|
||||
this._connectionProviderKey.reset();
|
||||
this._isCloudKey.reset();
|
||||
this._engineEditionKey.reset();
|
||||
}
|
||||
|
||||
get(): IContextValue | undefined {
|
||||
return this._itemKey.get();
|
||||
}
|
||||
}
|
||||
@@ -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,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { ExplorerFilter, ExplorerRenderer, ExplorerDataSource, ExplorerController, ExplorerModel } from './explorerTree';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
|
||||
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 { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { getContentHeight } from 'vs/base/browser/dom';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { subscriptionToDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper';
|
||||
|
||||
@Component({
|
||||
selector: 'explorer-widget',
|
||||
templateUrl: decodeURI(require.toUrl('./explorerWidget.component.html'))
|
||||
})
|
||||
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit {
|
||||
private _input: InputBox;
|
||||
private _tree: Tree;
|
||||
private _filterDelayer = new Delayer<void>(200);
|
||||
private _treeController = this.instantiationService.createInstance(ExplorerController,
|
||||
this._bootstrap.getUnderlyingUri(),
|
||||
this._bootstrap.connectionManagementService,
|
||||
this._router,
|
||||
this._bootstrap
|
||||
);
|
||||
private _treeRenderer = new ExplorerRenderer();
|
||||
private _treeDataSource = new ExplorerDataSource();
|
||||
private _treeFilter = new ExplorerFilter();
|
||||
|
||||
private _inited = false;
|
||||
|
||||
@ViewChild('input') private _inputContainer: ElementRef;
|
||||
@ViewChild('table') private _tableContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private readonly _bootstrap: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private readonly _router: Router,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ElementRef)) private readonly _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private readonly themeService: IWorkbenchThemeService,
|
||||
@Inject(IContextViewService) private readonly contextViewService: IContextViewService,
|
||||
@Inject(IInstantiationService) private readonly instantiationService: IInstantiationService,
|
||||
@Inject(ICapabilitiesService) private readonly capabilitiesService: ICapabilitiesService
|
||||
) {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._inited = true;
|
||||
|
||||
const placeholderLabel = this._config.context === 'database' ? nls.localize('seachObjects', "Search by name of type (a:, t:, v:, f:, or sp:)") : nls.localize('searchDatabases', "Search databases");
|
||||
|
||||
const inputOptions: IInputOptions = {
|
||||
placeholder: placeholderLabel,
|
||||
ariaLabel: placeholderLabel
|
||||
};
|
||||
this._input = new InputBox(this._inputContainer.nativeElement, this.contextViewService, inputOptions);
|
||||
this._register(this._input.onDidChange(e => {
|
||||
this._filterDelayer.trigger(() => {
|
||||
this._treeFilter.filterString = e;
|
||||
this._tree.refresh();
|
||||
});
|
||||
}));
|
||||
this._tree = new Tree(this._tableContainer.nativeElement, {
|
||||
controller: this._treeController,
|
||||
dataSource: this._treeDataSource,
|
||||
filter: this._treeFilter,
|
||||
renderer: this._treeRenderer
|
||||
}, { horizontalScrollMode: ScrollbarVisibility.Auto });
|
||||
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
|
||||
this._register(this._input);
|
||||
this._register(attachInputBoxStyler(this._input, this.themeService));
|
||||
this._register(this._tree);
|
||||
this._register(attachListStyler(this._tree, this.themeService));
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
if (this._config.context === 'database') {
|
||||
this._register(subscriptionToDisposable(this._bootstrap.metadataService.metadata.subscribe(
|
||||
data => {
|
||||
if (data) {
|
||||
const objectData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
|
||||
objectData.sort(ObjectMetadataWrapper.sort);
|
||||
this._treeDataSource.data = objectData;
|
||||
this._tree.setInput(new ExplorerModel());
|
||||
}
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.objectError', "Unable to load objects");
|
||||
}
|
||||
)));
|
||||
} else {
|
||||
const currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
|
||||
this._register(subscriptionToDisposable(this._bootstrap.metadataService.databaseNames.subscribe(
|
||||
data => {
|
||||
// Handle the case where there is no metadata service
|
||||
data = data || [];
|
||||
const profileData = data.map(d => {
|
||||
const profile = new ConnectionProfile(this.capabilitiesService, currentProfile);
|
||||
profile.databaseName = d;
|
||||
return profile;
|
||||
});
|
||||
this._treeDataSource.data = profileData;
|
||||
this._tree.setInput(new ExplorerModel());
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.databaseError', "Unable to load databases");
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (this._inited) {
|
||||
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/browser/widgetRegistry';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { ExplorerManageAction } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeActions';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext';
|
||||
|
||||
const explorerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
registerDashboardWidget('explorer-widget', '', explorerSchema);
|
||||
|
||||
CommandsRegistry.registerCommand(ExplorerManageAction.ID, (accessor, context) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
instantiationService.createInstance(ExplorerManageAction, ExplorerManageAction.ID, ExplorerManageAction.LABEL).run(context);
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerWidgetContext, {
|
||||
command: {
|
||||
id: ExplorerManageAction.ID,
|
||||
title: ExplorerManageAction.LABEL
|
||||
},
|
||||
when: ItemContextKey.ItemType.isEqualTo('database'),
|
||||
order: 1
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
explorer-widget .list-row .icon {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
explorer-widget .list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: -33px;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MetadataType } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ObjectMetadata } from 'azdata';
|
||||
|
||||
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 {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { InsightActionContext } from 'sql/workbench/browser/actions';
|
||||
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class RunInsightQueryAction extends Action {
|
||||
public static ID = 'runQuery';
|
||||
public static LABEL = nls.localize('insights.runQuery', "Run Query");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(context: InsightActionContext): Promise<boolean> {
|
||||
return this.instantiationService.invokeFunction(openNewQuery, context.profile, undefined, RunQueryOnConnectionMode.executeQuery).then(() => true, () => false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, AfterContentInit,
|
||||
ComponentFactoryResolver, ViewChild, ChangeDetectorRef, Injector
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WIDGET_CONFIG, WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { ComponentHostDirective } from 'sql/workbench/contrib/dashboard/browser/core/componentHost.directive';
|
||||
import { InsightAction, InsightActionContext } from 'sql/workbench/browser/actions';
|
||||
import { Extensions, IInsightRegistry, IInsightsConfig, IInsightsView, getWidgetAutoRefreshState } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { resolveQueryFilePath } from 'sql/workbench/services/insights/common/insightsUtils';
|
||||
|
||||
import { RunInsightQueryAction } from './actions';
|
||||
|
||||
import { SimpleExecuteResult } from 'azdata';
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IntervalTimer, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { subscriptionToDisposable } from 'sql/base/browser/lifecycle';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
interface IStorageResult {
|
||||
date: string;
|
||||
results: SimpleExecuteResult;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'insights-widget',
|
||||
template: `
|
||||
<div *ngIf="error" style="text-align: center; padding-top: 20px">{{error}}</div>
|
||||
<div *ngIf="lastUpdated" style="font-style: italic; font-size: 80%; margin-left: 5px">{{lastUpdated}}</div>
|
||||
<div *ngIf="autoRefreshStatus" style="font-style: italic; font-size: 80%; margin-left: 5px">{{autoRefreshStatus}}</div>
|
||||
<div style="margin: 10px; width: calc(100% - 20px); height: calc(100% - 20px)">
|
||||
<ng-template component-host></ng-template>
|
||||
<loading-spinner [loading]="_loading"></loading-spinner>
|
||||
</div>`,
|
||||
styles: [':host { width: 100%; height: 100% }']
|
||||
})
|
||||
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit {
|
||||
private insightConfig: IInsightsConfig;
|
||||
private queryObv: Observable<SimpleExecuteResult>;
|
||||
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
|
||||
|
||||
private _typeKey: string;
|
||||
private _init: boolean = false;
|
||||
public _loading: boolean = true;
|
||||
private _intervalTimer: IntervalTimer;
|
||||
|
||||
public error: string;
|
||||
public lastUpdated: string;
|
||||
public autoRefreshStatus: string;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private dashboardService: CommonServiceInterface,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => Injector)) private _injector: Injector,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(IStorageService) private storageService: IStorageService,
|
||||
@Inject(IConfigurationService) private readonly _configurationService: IConfigurationService,
|
||||
@Inject(IFileService) private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this.insightConfig = <IInsightsConfig>this._config.widget['insights-widget'];
|
||||
|
||||
this._verifyConfig();
|
||||
|
||||
this._parseConfig().then(() => {
|
||||
if (!this._checkStorage()) {
|
||||
const promise = this._runQuery();
|
||||
this.queryObv = Observable.fromPromise(promise);
|
||||
const cancelablePromise = createCancelablePromise(() => {
|
||||
return promise.then(
|
||||
result => {
|
||||
this._loading = false;
|
||||
if (this._init) {
|
||||
this._updateChild(result);
|
||||
this.setupInterval();
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(result));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this._loading = false;
|
||||
if (isPromiseCanceledError(error)) {
|
||||
return;
|
||||
}
|
||||
if (this._init) {
|
||||
this.showError(error);
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(error));
|
||||
}
|
||||
}
|
||||
).then(() => this._cd.detectChanges());
|
||||
});
|
||||
this._register(toDisposable(() => cancelablePromise.cancel()));
|
||||
}
|
||||
}, error => {
|
||||
this._loading = false;
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init = true;
|
||||
if (this.queryObv) {
|
||||
this._register(subscriptionToDisposable(this.queryObv.subscribe(
|
||||
result => {
|
||||
this._loading = false;
|
||||
this._updateChild(result);
|
||||
this.setupInterval();
|
||||
},
|
||||
error => {
|
||||
this._loading = false;
|
||||
this.showError(error);
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
private setupInterval(): void {
|
||||
if (this.insightConfig.autoRefreshInterval) {
|
||||
this._intervalTimer = new IntervalTimer();
|
||||
this._register(this._intervalTimer);
|
||||
this._intervalTimer.cancelAndSet(() => {
|
||||
const autoRefresh = getWidgetAutoRefreshState(this.insightConfig.id, this.actionsContext.profile.id);
|
||||
this.updateAutoRefreshStatus(autoRefresh);
|
||||
if (!autoRefresh) {
|
||||
return;
|
||||
}
|
||||
this.refresh();
|
||||
}, this.insightConfig.autoRefreshInterval * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private updateAutoRefreshStatus(autoRefreshOn: boolean): void {
|
||||
let newState = autoRefreshOn ? '' : nls.localize('insights.autoRefreshOffState', "Auto Refresh: OFF");
|
||||
if (this.autoRefreshStatus !== newState) {
|
||||
this.autoRefreshStatus = newState;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private showError(error: string): void {
|
||||
this.error = error;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
get actions(): Array<Action> {
|
||||
const actions: Array<Action> = [];
|
||||
if (this.insightConfig.details && (this.insightConfig.details.query || this.insightConfig.details.queryFile)) {
|
||||
actions.push(this.instantiationService.createInstance(InsightAction, InsightAction.ID, InsightAction.LABEL));
|
||||
}
|
||||
actions.push(this.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) {
|
||||
const currentTime = new Date();
|
||||
const store: IStorageResult = {
|
||||
date: currentTime.toString(),
|
||||
results: result
|
||||
};
|
||||
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", currentTime.toLocaleTimeString(), currentTime.toLocaleDateString());
|
||||
this._cd.detectChanges();
|
||||
this.storageService.store(this._getStorageKey(), JSON.stringify(store), StorageScope.GLOBAL);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _checkStorage(): boolean {
|
||||
if (this.insightConfig.cacheId) {
|
||||
const storage = this.storageService.get(this._getStorageKey(), StorageScope.GLOBAL);
|
||||
if (storage) {
|
||||
const storedResult: IStorageResult = JSON.parse(storage);
|
||||
const date = new Date(storedResult.date);
|
||||
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", date.toLocaleTimeString(), date.toLocaleDateString());
|
||||
this._loading = false;
|
||||
if (this._init) {
|
||||
this._updateChild(storedResult.results);
|
||||
this.setupInterval();
|
||||
this._cd.detectChanges();
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
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(): Promise<SimpleExecuteResult> {
|
||||
return Promise.resolve(this.dashboardService.queryManagementService.runQueryAndReturn(this.insightConfig.query as string).then(
|
||||
result => {
|
||||
return this._storeResult(result);
|
||||
},
|
||||
error => {
|
||||
throw error;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private _updateChild(result: SimpleExecuteResult): void {
|
||||
this.componentHost.viewContainerRef.clear();
|
||||
this.error = undefined;
|
||||
this._cd.detectChanges();
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
this.showError(nls.localize('noResults', "No results to show"));
|
||||
return;
|
||||
}
|
||||
|
||||
const componentFactory = this._componentFactoryResolver.resolveComponentFactory<IInsightsView>(insightRegistry.getCtorFromId(this._typeKey));
|
||||
|
||||
const componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory, 0, this._injector);
|
||||
const componentInstance = componentRef.instance;
|
||||
|
||||
// check if the setter is defined
|
||||
if (componentInstance.setConfig) {
|
||||
componentInstance.setConfig(this.insightConfig.type[this._typeKey]);
|
||||
}
|
||||
componentInstance.data = { columns: result.columnInfo.map(item => item.columnName), rows: result.rows.map(row => row.map(item => (item.invariantCultureDisplayValue === null || item.invariantCultureDisplayValue === undefined) ? item.displayValue : item.invariantCultureDisplayValue)) };
|
||||
|
||||
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().some(x => x === 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 (this.insightConfig.autoRefreshInterval && !types.isNumber(this.insightConfig.autoRefreshInterval)) {
|
||||
throw new Error('Auto Refresh Interval must be a number if specified');
|
||||
}
|
||||
|
||||
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 async _parseConfig(): Promise<void> {
|
||||
this._typeKey = Object.keys(this.insightConfig.type)[0];
|
||||
|
||||
// When the editor.accessibilitySupport setting is on, we will force the chart type to be table.
|
||||
// so that the information is accessible to the user.
|
||||
// count chart type is already a text based chart, we don't have to apply this rule for it.
|
||||
const isAccessibilitySupportOn = this._configurationService.getValue('editor.accessibilitySupport') === 'on';
|
||||
if (isAccessibilitySupportOn && this._typeKey !== 'count') {
|
||||
this._typeKey = 'table';
|
||||
}
|
||||
|
||||
if (types.isStringArray(this.insightConfig.query)) {
|
||||
this.insightConfig.query = this.insightConfig.query.join(' ');
|
||||
} else if (this.insightConfig.queryFile) {
|
||||
const filePath = await this.instantiationService.invokeFunction(resolveQueryFilePath, this.insightConfig.queryFile);
|
||||
|
||||
this.insightConfig.query = (await this.fileService.readFile(URI.file(filePath))).value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'vs/base/common/path';
|
||||
|
||||
import { registerDashboardWidget, registerNonCustomDashboardWidget } from 'sql/platform/dashboard/browser/widgetRegistry';
|
||||
import { Extensions as InsightExtensions, IInsightRegistry, setWidgetAutoRefreshState } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { IInsightTypeContrib } from './interfaces';
|
||||
import { insightsContribution, insightsSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/insightsWidgetSchemas';
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
|
||||
|
||||
registerDashboardWidget('insights-widget', '', insightsSchema);
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IInsightTypeContrib | IInsightTypeContrib[]>({ extensionPoint: 'dashboard.insights', jsonSchema: insightsContribution }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(insight: IInsightTypeContrib, extension: IExtensionPointUser<any>) {
|
||||
|
||||
if (insight.contrib.queryFile) {
|
||||
insight.contrib.queryFile = join(extension.description.extensionLocation.fsPath, insight.contrib.queryFile);
|
||||
}
|
||||
|
||||
if (insight.contrib.details && insight.contrib.details.queryFile) {
|
||||
insight.contrib.details.queryFile = join(extension.description.extensionLocation.fsPath, insight.contrib.details.queryFile);
|
||||
}
|
||||
insight.contrib.id = insight.id;
|
||||
registerNonCustomDashboardWidget(insight.id, '', insight.contrib);
|
||||
insightRegistry.registerExtensionInsight(insight.id, insight.contrib);
|
||||
}
|
||||
|
||||
for (const extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IInsightTypeContrib>(value)) {
|
||||
for (const command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('azdata.widget.setAutoRefreshState',
|
||||
function (accessor: ServicesAccessor, widgetId: string, connectionId: string, autoRefresh: boolean) {
|
||||
setWidgetAutoRefreshState(widgetId, connectionId, autoRefresh);
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/browser/insightRegistry';
|
||||
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);
|
||||
|
||||
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: {
|
||||
cacheId: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightIdDescription', "Unique Identifier used for caching 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")
|
||||
},
|
||||
autoRefreshInterval: {
|
||||
type: 'number',
|
||||
description: nls.localize('insightAutoRefreshIntervalDescription', "[Optional] Auto refresh interval in minutes, if not set, there will be no auto refresh")
|
||||
},
|
||||
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: {
|
||||
description: nls.localize('actionTypes', "Which actions to use"),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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 { IInsightsConfig } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
|
||||
export interface IInsightTypeContrib {
|
||||
id: string;
|
||||
contrib: IInsightsConfig;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import * as chartjs from 'chart.js';
|
||||
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { defaultChartConfig, IChartConfig, IDataSet } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { IInsightData, IPointDataSet } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { IInsightsView } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { ChartType, LegendPosition } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { createMemoizer } from 'vs/base/common/decorators';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
@Component({
|
||||
template: ` <div style="display: block; width: 100%; height: 100%; position: relative">
|
||||
<canvas #canvas *ngIf="_isDataAvailable && _hasInit"
|
||||
baseChart
|
||||
[datasets]="chartData"
|
||||
[labels]="labels"
|
||||
[chartType]="chartType"
|
||||
[colors]="colors"
|
||||
[options]="_options"></canvas>
|
||||
<div *ngIf="_hasError">{{CHART_ERROR_MESSAGE}}</div>
|
||||
</div>`
|
||||
})
|
||||
export abstract class ChartInsight extends Disposable implements IInsightsView {
|
||||
protected static readonly MEMOIZER = createMemoizer();
|
||||
|
||||
private _isDataAvailable: boolean = false;
|
||||
protected _hasInit: boolean = false;
|
||||
protected _hasError: boolean = false;
|
||||
private _options: any = {};
|
||||
|
||||
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
|
||||
|
||||
protected _defaultConfig = defaultChartConfig;
|
||||
protected _config: IChartConfig;
|
||||
protected _data: IInsightData;
|
||||
|
||||
protected readonly CHART_ERROR_MESSAGE = nls.localize('chartErrorMessage', "Chart cannot be displayed with the given data");
|
||||
|
||||
protected abstract get chartType(): ChartType;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) private themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) private _telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._register(this.themeService.onThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this.themeService.getTheme());
|
||||
// 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._hasInit = true;
|
||||
this._hasError = false;
|
||||
try {
|
||||
this._changeRef.detectChanges();
|
||||
} catch (err) {
|
||||
this._hasInit = false;
|
||||
this._hasError = true;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Shell, TelemetryKeys.ChartCreated)
|
||||
.withAdditionalProperties({ type: this.chartType })
|
||||
.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: ITheme): void {
|
||||
const foregroundColor = e.getColor(colors.editorForeground);
|
||||
const foreground = foregroundColor ? foregroundColor.toString() : null;
|
||||
const backgroundColor = e.getColor(colors.editorBackground);
|
||||
const background = backgroundColor ? backgroundColor.toString() : null;
|
||||
|
||||
const options = {
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: foreground
|
||||
}
|
||||
},
|
||||
viewArea: {
|
||||
backgroundColor: background
|
||||
}
|
||||
};
|
||||
this.options = mixin({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
// cheaper refresh but causes problems when change data for rerender
|
||||
if (this._chart) {
|
||||
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
|
||||
ChartInsight.MEMOIZER.clear();
|
||||
this._data = this.filterToTopNData(data);
|
||||
if (isValidData(data)) {
|
||||
this._isDataAvailable = true;
|
||||
}
|
||||
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private filterToTopNData(data: IInsightData): IInsightData {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
return {
|
||||
columns: this.getTopNData(data.columns),
|
||||
rows: data.rows.map((row) => {
|
||||
return this.getTopNData(row);
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
columns: data.columns,
|
||||
rows: data.rows.slice(0, this._config.showTopNData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getTopNData(data: any[]): any[] {
|
||||
if (this._config.showTopNData) {
|
||||
if (this._config.dataDirection === 'horizontal' && this._config.labelFirstColumn) {
|
||||
return data.slice(0, this._config.showTopNData + 1);
|
||||
} else {
|
||||
return data.slice(0, this._config.showTopNData);
|
||||
}
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
protected clearMemoize(): void {
|
||||
// unmemoize getters since their result can be changed by a new config
|
||||
ChartInsight.MEMOIZER.clear();
|
||||
}
|
||||
|
||||
public setConfig(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.
|
||||
his is a workaround that allows us to still call base getter */
|
||||
@ChartInsight.MEMOIZER
|
||||
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].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i + 1])),
|
||||
label: this._data.columns[i + 1]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return this._data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i + 1])),
|
||||
label: 'Series' + (i + 1)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
return this.getChartData();
|
||||
}
|
||||
|
||||
@ChartInsight.MEMOIZER
|
||||
public getLabels(): Array<string> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
if (this._config.labelFirstColumn) {
|
||||
return this._data.columns.slice(1);
|
||||
} else {
|
||||
return this._data.columns;
|
||||
}
|
||||
} else {
|
||||
return this._data.rows.map(row => row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
return this.getLabels();
|
||||
}
|
||||
|
||||
|
||||
@ChartInsight.MEMOIZER
|
||||
public get colors(): { backgroundColor: string[] }[] {
|
||||
if (this._config && this._config.colorMap) {
|
||||
const backgroundColor = this.labels.map((item) => {
|
||||
return this._config.colorMap[item];
|
||||
});
|
||||
const colorsMap = { backgroundColor };
|
||||
return [colorsMap];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set legendPosition(input: LegendPosition) {
|
||||
const options = {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
};
|
||||
if (input === 'none') {
|
||||
options.legend.display = false;
|
||||
} else {
|
||||
options.legend.position = input;
|
||||
}
|
||||
this.options = mixin(this.options, options);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidData(data: IInsightData): boolean {
|
||||
if (types.isUndefinedOrNull(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(data.columns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(data.rows)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
chartjs.Chart.pluginService.register({
|
||||
beforeDraw: function (chart) {
|
||||
if ((chart.config.options as any).viewArea && (chart.config.options as any).viewArea.backgroundColor) {
|
||||
let ctx = (chart as any).chart.ctx;
|
||||
ctx.fillStyle = (chart.config.options as any).viewArea.backgroundColor;
|
||||
ctx.fillRect(0, 0, (chart as any).chart.width, (chart as any).chart.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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.']
|
||||
},
|
||||
showTopNData: {
|
||||
type: 'number',
|
||||
description: nls.localize('showTopNData', "If showTopNData is set, showing only top N data in the chart.")
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LegendPosition, DataDirection } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export interface IDataSet {
|
||||
data: Array<number>;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IChartConfig {
|
||||
colorMap?: { [column: string]: string };
|
||||
labelFirstColumn?: boolean;
|
||||
legendPosition?: LegendPosition;
|
||||
dataDirection?: DataDirection;
|
||||
columnsAsLabels?: boolean;
|
||||
showTopNData?: number;
|
||||
}
|
||||
|
||||
export const defaultChartConfig: IChartConfig = {
|
||||
labelFirstColumn: true,
|
||||
columnsAsLabels: true,
|
||||
legendPosition: LegendPosition.Top,
|
||||
dataDirection: DataDirection.Vertical
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartInsight } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.component';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { IChartConfig } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { customMixin } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export interface IBarChartConfig extends IChartConfig {
|
||||
yAxisMin: number;
|
||||
yAxisMax: number;
|
||||
yAxisLabel: string;
|
||||
xAxisMin: number;
|
||||
xAxisMax: number;
|
||||
xAxisLabel: string;
|
||||
}
|
||||
|
||||
export default class BarChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Bar;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
|
||||
public setConfig(config: IBarChartConfig): void {
|
||||
let options = {};
|
||||
if (config.xAxisMax) {
|
||||
const opts = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
max: config.xAxisMax
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.xAxisMin) {
|
||||
const opts = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
min: config.xAxisMin
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.xAxisLabel) {
|
||||
const opts = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: config.xAxisLabel
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.yAxisMax) {
|
||||
const opts = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
max: config.yAxisMax
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.yAxisMin) {
|
||||
const opts = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
min: config.yAxisMin
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.yAxisLabel) {
|
||||
const opts = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: config.yAxisLabel
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
this.options = mixin({}, mixin(this.options, options, true, customMixin));
|
||||
super.setConfig(config);
|
||||
}
|
||||
|
||||
protected updateTheme(e: ITheme): void {
|
||||
super.updateTheme(e);
|
||||
const foregroundColor = e.getColor(colors.editorForeground);
|
||||
const foreground = foregroundColor ? foregroundColor.toString() : null;
|
||||
const gridLinesColor = e.getColor(editorLineNumbers);
|
||||
const gridLines = gridLinesColor ? gridLinesColor.toString() : null;
|
||||
const options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = mixin({}, mixin(this.options, options, true, customMixin));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import BarChart from './barChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
properties: {
|
||||
yAxisMin: {
|
||||
type: 'number',
|
||||
description: nls.localize('yAxisMin', "Minimum value of the y axis")
|
||||
},
|
||||
yAxisMax: {
|
||||
type: 'number',
|
||||
description: nls.localize('yAxisMax', "Maximum value of the y axis")
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: 'string',
|
||||
description: nls.localize('barchart.yAxisLabel', "Label for the y axis")
|
||||
},
|
||||
xAxisMin: {
|
||||
type: 'number',
|
||||
description: nls.localize('xAxisMin', "Minimum value of the x axis")
|
||||
},
|
||||
xAxisMax: {
|
||||
type: 'number',
|
||||
description: nls.localize('xAxisMax', "Maximum value of the x axis")
|
||||
},
|
||||
xAxisLabel: {
|
||||
type: 'string',
|
||||
description: nls.localize('barchart.xAxisLabel', "Label for the x axis")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const barChartSchema = mixin(deepClone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('bar', '', barChartSchema, BarChart);
|
||||
@@ -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 PieChart from './pieChart.component';
|
||||
import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
export default class DoughnutChart extends PieChart {
|
||||
protected readonly chartType: ChartType = ChartType.Doughnut;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import DoughnutChart from './doughnutChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const doughnutChartSchema = mixin(deepClone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('doughnut', '', doughnutChartSchema, DoughnutChart);
|
||||
@@ -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 BarChart from './barChart.component';
|
||||
import { forwardRef, Inject, ChangeDetectorRef } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export default class HorizontalBarChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.HorizontalBar;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import HorizontalBarChart from './horizontalBarChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const horizontalBarSchema = mixin(deepClone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('horizontalBar', '', horizontalBarSchema, HorizontalBarChart);
|
||||
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
|
||||
import BarChart, { IBarChartConfig } from './barChart.component';
|
||||
import { defaultChartConfig, IDataSet } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/interfaces';
|
||||
import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IPointDataSet } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { DataType, ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
export interface ILineConfig extends IBarChartConfig {
|
||||
dataType?: DataType;
|
||||
}
|
||||
|
||||
const defaultLineConfig = mixin(deepClone(defaultChartConfig), { dataType: 'number' }) as ILineConfig;
|
||||
|
||||
export default class LineChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.Line;
|
||||
protected _config: ILineConfig;
|
||||
protected _defaultConfig = defaultLineConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
|
||||
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();
|
||||
LineChart.MEMOIZER.clear();
|
||||
}
|
||||
|
||||
@LineChart.MEMOIZER
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
const dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
const 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 values(dataSetMap);
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getLabels();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
const xLabel = this._config.xAxisLabel || this._data.columns[1] || 'x';
|
||||
const yLabel = this._config.yAxisLabel || this._data.columns[2] || 'y';
|
||||
const options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: xLabel
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: yLabel,
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
// @SQLTODO
|
||||
this.options = mixin(this.options, options, true);
|
||||
}
|
||||
}
|
||||
@@ -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 { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import LineChart from './lineChart.component';
|
||||
|
||||
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(deepClone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('line', '', lineSchema, LineChart);
|
||||
@@ -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 { ChartInsight } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.component';
|
||||
import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export default class PieChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Pie;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import PieChart from './pieChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const pieSchema = mixin(deepClone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('pie', '', pieSchema, PieChart);
|
||||
@@ -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 LineChart, { ILineConfig } from './lineChart.component';
|
||||
import { defaultChartConfig } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
const defaultScatterConfig = mixin(deepClone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;
|
||||
|
||||
export default class ScatterChart extends LineChart {
|
||||
protected readonly chartType: ChartType = ChartType.Scatter;
|
||||
protected _defaultConfig = defaultScatterConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import ScatterChart from './scatterChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
};
|
||||
|
||||
const scatterSchema = mixin(deepClone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('scatter', '', scatterSchema, ScatterChart);
|
||||
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import LineChart, { ILineConfig } from './lineChart.component';
|
||||
import { defaultChartConfig } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import { mixin, deepClone, assign } from 'vs/base/common/objects';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ChangeDetectorRef, Inject, forwardRef } from '@angular/core';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IPointDataSet } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
const defaultTimeSeriesConfig = mixin(deepClone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;
|
||||
|
||||
export default class TimeSeriesChart extends LineChart {
|
||||
protected _defaultConfig = defaultTimeSeriesConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(IThemeService) themeService: IThemeService,
|
||||
@Inject(IAdsTelemetryService) telemetryService: IAdsTelemetryService
|
||||
) {
|
||||
super(_changeRef, themeService, telemetryService);
|
||||
}
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
const xLabel = this._config.xAxisLabel || this.getLabels()[1] || 'x';
|
||||
const yLabel = this._config.yAxisLabel || this.getLabels()[2] || 'y';
|
||||
|
||||
const 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 = assign({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
const dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
const 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.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
return values(dataSetMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin, deepClone } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/contrib/dashboard/browser/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import TimeSeriesChart from './timeSeriesChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
};
|
||||
|
||||
const timeSeriesSchema = mixin(deepClone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('timeSeries', '', timeSeriesSchema, TimeSeriesChart);
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IInsightData } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { IInsightsView } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
|
||||
@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 {
|
||||
protected _labels: Array<string>;
|
||||
protected _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,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/browser/insightRegistry';
|
||||
|
||||
import CountInsight from './countInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const 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 { mixin } from 'vs/base/common/objects';
|
||||
import { IInsightData } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { IInsightsView } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
|
||||
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 = defaultConfig;
|
||||
|
||||
@ViewChild('image') private image: ElementRef;
|
||||
@ViewChild('container') private container: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit() {
|
||||
const 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) {
|
||||
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
|
||||
this._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 (startsWith(hexVal, '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(' ').map(v => Number(v))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/browser/insightRegistry';
|
||||
|
||||
import ImageInsight from './imageInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const 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);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, forwardRef, ElementRef, OnInit } from '@angular/core';
|
||||
|
||||
import { getContentHeight, getContentWidth, Dimension } from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
import { IInsightData } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { IInsightsView } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
|
||||
@Component({
|
||||
template: ''
|
||||
})
|
||||
export default class TableInsight extends Disposable implements IInsightsView, OnInit {
|
||||
private table: Table<any>;
|
||||
private dataView: TableDataView<any>;
|
||||
private columns: Slick.Column<any>[];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) private _elementRef: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
||||
) {
|
||||
super();
|
||||
this._elementRef.nativeElement.className = 'slickgridContainer';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.createTable();
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
if (!this.dataView) {
|
||||
this.dataView = new TableDataView();
|
||||
if (this.table) {
|
||||
this.table.setData(this.dataView);
|
||||
}
|
||||
}
|
||||
|
||||
this.dataView.clear();
|
||||
this.dataView.push(transformData(data.rows, data.columns));
|
||||
this.columns = transformColumns(data.columns);
|
||||
|
||||
if (this.table) {
|
||||
this.table.columns = this.columns;
|
||||
} else if (this._elementRef && this._elementRef.nativeElement) {
|
||||
this.createTable();
|
||||
}
|
||||
}
|
||||
|
||||
layout() {
|
||||
if (this.table) {
|
||||
this.table.layout(new Dimension(getContentWidth(this._elementRef.nativeElement), getContentHeight(this._elementRef.nativeElement)));
|
||||
}
|
||||
}
|
||||
|
||||
private createTable() {
|
||||
if (!this.table) {
|
||||
this.table = new Table(this._elementRef.nativeElement, { dataProvider: this.dataView, columns: this.columns }, { showRowNumber: true });
|
||||
this.table.setSelectionModel(new CellSelectionModel());
|
||||
this._register(attachTableStyler(this.table, this.themeService));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transformData(rows: string[][], columns: string[]): { [key: string]: string }[] {
|
||||
return rows.map(row => {
|
||||
const object: { [key: string]: string } = {};
|
||||
row.forEach((val, index) => {
|
||||
object[columns[index]] = val;
|
||||
});
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
return columns.map(col => {
|
||||
return <Slick.Column<any>>{
|
||||
name: col,
|
||||
id: col,
|
||||
field: col
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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 { registerInsight } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
|
||||
import TableInsight from './tableInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const tableInsightSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('tableInsightDescription', "Displays the results in a simple table")
|
||||
};
|
||||
|
||||
registerInsight('table', '', tableInsightSchema, TableInsight);
|
||||
@@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
|
||||
const azureEditionDisplayName = nls.localize('azureEdition', "Edition");
|
||||
const azureType = nls.localize('azureType', "Type");
|
||||
|
||||
export const properties: Array<ProviderProperties> = [
|
||||
{
|
||||
provider: mssqlProviderName,
|
||||
flavors: [
|
||||
{
|
||||
flavor: 'on_prem',
|
||||
conditions: [
|
||||
{
|
||||
field: 'isCloud',
|
||||
operator: '!=',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
field: 'engineEditionId',
|
||||
operator: '!=',
|
||||
value: '11'
|
||||
}
|
||||
],
|
||||
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',
|
||||
conditions: [
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
flavor: 'on_demand',
|
||||
conditions: [
|
||||
{
|
||||
field: 'engineEditionId',
|
||||
operator: '==',
|
||||
value: '11'
|
||||
}
|
||||
],
|
||||
databaseProperties: [
|
||||
{
|
||||
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%;">
|
||||
<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;">{{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,271 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, ViewChild } from '@angular/core';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import { IDashboardRegistry, Extensions as DashboardExtensions } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry';
|
||||
|
||||
import { DatabaseInfo, ServerInfo } from 'azdata';
|
||||
|
||||
import { EventType, addDisposableListener } from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { subscriptionToDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
|
||||
export interface PropertiesConfig {
|
||||
properties: Array<Property>;
|
||||
}
|
||||
|
||||
export interface FlavorProperties {
|
||||
flavor: string;
|
||||
condition?: ConditionProperties;
|
||||
conditions?: Array<ConditionProperties>;
|
||||
databaseProperties: Array<Property>;
|
||||
serverProperties: Array<Property>;
|
||||
}
|
||||
|
||||
export interface ConditionProperties {
|
||||
field: string;
|
||||
operator: '==' | '<=' | '>=' | '!=';
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
export interface ProviderProperties {
|
||||
provider: string;
|
||||
flavors: Array<FlavorProperties>;
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
displayName: string;
|
||||
value: string;
|
||||
ignore?: Array<string>;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
const dashboardRegistry = Registry.as<IDashboardRegistry>(DashboardExtensions.DashboardContributions);
|
||||
|
||||
export interface DisplayProperty {
|
||||
displayName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'properties-widget',
|
||||
templateUrl: decodeURI(require.toUrl('./propertiesWidget.component.html'))
|
||||
})
|
||||
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit {
|
||||
private _connection: ConnectionManagementInfo;
|
||||
private _databaseInfo: DatabaseInfo;
|
||||
public _clipped: boolean;
|
||||
private properties: Array<DisplayProperty>;
|
||||
private _hasInit = false;
|
||||
|
||||
@ViewChild('child', { read: ElementRef }) private _child: ElementRef;
|
||||
@ViewChild('parent', { read: ElementRef }) private _parent: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrap: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(ILogService) private logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._hasInit = true;
|
||||
this._register(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this._connection = this._bootstrap.connectionManagementService.connectionInfo;
|
||||
this._register(subscriptionToDisposable(this._bootstrap.adminService.databaseInfo.subscribe(data => {
|
||||
this._databaseInfo = data;
|
||||
this._changeRef.detectChanges();
|
||||
this.parseProperties();
|
||||
if (this._hasInit) {
|
||||
this.handleClipping();
|
||||
}
|
||||
}, error => {
|
||||
if (this._bootstrap.connectionManagementService.connectionInfo.serverInfo.engineEditionId !== DatabaseEngineEdition.SqlOnDemand) {
|
||||
(<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() {
|
||||
const 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) {
|
||||
const config = <PropertiesConfig>this._config.widget['properties-widget'];
|
||||
propertyArray = config.properties;
|
||||
} else {
|
||||
const providerProperties = dashboardRegistry.getProperties(provider as string);
|
||||
|
||||
if (!providerProperties) {
|
||||
this.logService.error('No property definitions found for provider', provider);
|
||||
return;
|
||||
}
|
||||
|
||||
let flavor: FlavorProperties;
|
||||
|
||||
// find correct flavor
|
||||
if (providerProperties.flavors.length === 1) {
|
||||
flavor = providerProperties.flavors[0];
|
||||
} else if (providerProperties.flavors.length === 0) {
|
||||
this.logService.error('No flavor definitions found for "', provider,
|
||||
'. If there are not multiple flavors of this provider, add one flavor without a condition');
|
||||
return;
|
||||
} else {
|
||||
const flavorArray = providerProperties.flavors.filter((item) => {
|
||||
|
||||
// For backward compatibility we are supporting array of conditions and single condition.
|
||||
// If nothing is specified, we return false.
|
||||
if (item.conditions) {
|
||||
let conditionResult = true;
|
||||
for (let i = 0; i < item.conditions.length; i++) {
|
||||
conditionResult = conditionResult && this.getConditionResult(item, item.conditions[i]);
|
||||
}
|
||||
|
||||
return conditionResult;
|
||||
}
|
||||
else if (item.condition) {
|
||||
return this.getConditionResult(item, item.condition);
|
||||
}
|
||||
else {
|
||||
this.logService.error('No condition was specified.');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flavorArray.length === 0) {
|
||||
this.logService.error('Could not determine flavor');
|
||||
return;
|
||||
} else if (flavorArray.length > 1) {
|
||||
this.logService.error('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.logService.error('flavor', flavor.flavor, ' does not have a definition for database properties');
|
||||
}
|
||||
|
||||
if (!Array.isArray(flavor.serverProperties)) {
|
||||
this.logService.error('flavor', flavor.flavor, ' does not have a definition for server properties');
|
||||
}
|
||||
|
||||
propertyArray = flavor.databaseProperties;
|
||||
} else {
|
||||
if (!Array.isArray(flavor.serverProperties)) {
|
||||
this.logService.error('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++) {
|
||||
const property = propertyArray[i];
|
||||
const 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 getConditionResult(item: FlavorProperties, conditionItem: ConditionProperties): boolean {
|
||||
let condition = this._connection.serverInfo[conditionItem.field];
|
||||
|
||||
// If we need to compare strings, then we should ensure that condition is string
|
||||
// Otherwise tripple equals/unequals would return false values
|
||||
if (typeof conditionItem.value === 'string') {
|
||||
condition = condition.toString();
|
||||
}
|
||||
|
||||
switch (conditionItem.operator) {
|
||||
case '==':
|
||||
return condition === conditionItem.value;
|
||||
case '!=':
|
||||
return condition !== conditionItem.value;
|
||||
case '>=':
|
||||
return condition >= conditionItem.value;
|
||||
case '<=':
|
||||
return condition <= conditionItem.value;
|
||||
default:
|
||||
this.logService.error('Could not parse operator: "', conditionItem.operator,
|
||||
'" on item "', item, '"');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
tasks-widget .tile-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
tasks-widget .task-tile {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
tasks-widget .task-tile > div {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
tasks-widget .task-tile .codicon {
|
||||
padding: 15px;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div #container style="position: absolute; height: 100%; width: 100%">
|
||||
</div>
|
||||
@@ -0,0 +1,178 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./media/taskWidget';
|
||||
|
||||
/* Node Modules */
|
||||
import { Component, Inject, forwardRef, ViewChild, OnInit, ElementRef } from '@angular/core';
|
||||
|
||||
/* SQL imports */
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
|
||||
/* VS imports */
|
||||
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 * as types from 'vs/base/common/types';
|
||||
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { MenuRegistry, ICommandAction } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { TaskRegistry } from 'sql/platform/tasks/browser/tasksRegistry';
|
||||
|
||||
interface ITask {
|
||||
name: string;
|
||||
when: string;
|
||||
}
|
||||
|
||||
const selector = 'tasks-widget';
|
||||
|
||||
@Component({
|
||||
selector,
|
||||
templateUrl: decodeURI(require.toUrl('./tasksWidget.component.html'))
|
||||
})
|
||||
export class TasksWidget extends DashboardWidget implements IDashboardWidget, OnInit {
|
||||
private _size: number = 98;
|
||||
private _tasks: Array<ICommandAction> = [];
|
||||
private _profile: IConnectionProfile;
|
||||
private _scrollableElement: ScrollableElement;
|
||||
private _tileContainer: HTMLElement;
|
||||
static readonly ICON_PATH_TO_CSS_RULES: Map<string /* path*/, string /* CSS rule */> = new Map<string, string>();
|
||||
|
||||
private _inited = false;
|
||||
|
||||
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private readonly _bootstrap: CommonServiceInterface,
|
||||
@Inject(ICommandService) private readonly commandService: ICommandService,
|
||||
@Inject(IContextKeyService) readonly contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
this._profile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
|
||||
const tasksConfig = this._config.widget[selector] as Array<string | ITask>;
|
||||
let tasks = TaskRegistry.getTasks();
|
||||
|
||||
if (types.isArray(tasksConfig) && tasksConfig.length > 0) {
|
||||
tasks = tasksConfig.map(i => {
|
||||
if (types.isString(i)) {
|
||||
if (tasks.some(x => x === i)) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
if (tasks.some(x => x === i.name) && contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(i.when))) {
|
||||
return i.name;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}).filter(i => !!i);
|
||||
}
|
||||
|
||||
this._tasks = tasks.map(i => MenuRegistry.getCommand(i)).filter(v => !!v);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._inited = true;
|
||||
this._register(registerThemingParticipant(this.registerThemeing));
|
||||
this._computeContainer();
|
||||
|
||||
this._tasks.map(a => {
|
||||
this._tileContainer.append(this._createTile(a));
|
||||
});
|
||||
|
||||
this._scrollableElement = this._register(new ScrollableElement(this._tileContainer, {
|
||||
horizontal: ScrollbarVisibility.Auto,
|
||||
vertical: ScrollbarVisibility.Hidden,
|
||||
scrollYToX: true,
|
||||
useShadows: false
|
||||
}));
|
||||
|
||||
this._scrollableElement.onScroll(e => {
|
||||
this._tileContainer.style.right = e.scrollLeft + 'px';
|
||||
});
|
||||
|
||||
(this._container.nativeElement as HTMLElement).appendChild(this._scrollableElement.getDomNode());
|
||||
|
||||
// Update scrollbar
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
width: DOM.getContentWidth(this._container.nativeElement),
|
||||
scrollWidth: DOM.getContentWidth(this._tileContainer) + 18 // right padding
|
||||
});
|
||||
}
|
||||
|
||||
private _computeContainer(): void {
|
||||
const height = DOM.getContentHeight(this._container.nativeElement);
|
||||
const tilesHeight = Math.floor(height / (this._size + 10));
|
||||
const width = (this._size + 18) * Math.ceil(this._tasks.length / tilesHeight);
|
||||
if (!this._tileContainer) {
|
||||
this._tileContainer = DOM.$('.tile-container');
|
||||
}
|
||||
this._tileContainer.style.height = height + 'px';
|
||||
this._tileContainer.style.width = width + 'px';
|
||||
}
|
||||
|
||||
private _createTile(action: ICommandAction): HTMLElement {
|
||||
const label = DOM.$('div');
|
||||
label.innerText = types.isString(action.title) ? action.title : action.title.value;
|
||||
const tile = DOM.$('.task-tile');
|
||||
tile.style.height = this._size + 'px';
|
||||
tile.style.width = this._size + 'px';
|
||||
const innerTile = DOM.$('div');
|
||||
|
||||
const iconClassName = TaskRegistry.getOrCreateTaskIconClassName(action);
|
||||
if (iconClassName) {
|
||||
const icon = DOM.$('span.codicon');
|
||||
DOM.addClass(icon, iconClassName);
|
||||
innerTile.append(icon);
|
||||
}
|
||||
innerTile.append(label);
|
||||
tile.append(innerTile);
|
||||
tile.setAttribute('tabindex', '0');
|
||||
this._register(DOM.addDisposableListener(tile, DOM.EventType.CLICK, () => this.runTask(action)));
|
||||
this._register(DOM.addDisposableListener(tile, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Enter)) {
|
||||
this.runTask(action);
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}));
|
||||
return tile;
|
||||
}
|
||||
|
||||
private registerThemeing(theme: ITheme, collector: ICssStyleCollector) {
|
||||
const contrastBorder = theme.getColor(colors.contrastBorder);
|
||||
const sideBarColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND);
|
||||
if (contrastBorder) {
|
||||
const contrastBorderString = contrastBorder.toString();
|
||||
collector.addRule(`tasks-widget .task-tile { border: 1px solid ${contrastBorderString} }`);
|
||||
} else {
|
||||
const sideBarColorString = sideBarColor.toString();
|
||||
collector.addRule(`tasks-widget .task-tile { background-color: ${sideBarColorString} }`);
|
||||
}
|
||||
}
|
||||
|
||||
public runTask(task: ICommandAction) {
|
||||
this.commandService.executeCommand(task.id, this._profile);
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (this._inited) {
|
||||
this._computeContainer();
|
||||
// Update scrollbar
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
width: DOM.getContentWidth(this._container.nativeElement),
|
||||
scrollWidth: DOM.getContentWidth(this._tileContainer) + 18 // right padding
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { registerDashboardWidget } from 'sql/platform/dashboard/browser/widgetRegistry';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { TaskRegistry } from 'sql/platform/tasks/browser/tasksRegistry';
|
||||
|
||||
const singleTaskSchema: IJSONSchema = {
|
||||
type: 'string',
|
||||
enum: TaskRegistry.getTasks()
|
||||
};
|
||||
|
||||
const tasksSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [
|
||||
singleTaskSchema,
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: singleTaskSchema,
|
||||
when: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
TaskRegistry.onTaskRegistered(e => {
|
||||
singleTaskSchema.enum.push(e);
|
||||
});
|
||||
|
||||
registerDashboardWidget('tasks-widget', '', tasksSchema);
|
||||
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, OnInit, ElementRef } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/contrib/dashboard/browser/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { IDashboardWebview, IDashboardViewService } from 'sql/platform/dashboard/browser/dashboardViewService';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { WebviewElement, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
|
||||
interface IWebviewWidgetConfig {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const selector = 'webview-widget';
|
||||
|
||||
@Component({
|
||||
selector: selector,
|
||||
template: '<div></div>'
|
||||
})
|
||||
export class WebviewWidget extends DashboardWidget implements IDashboardWidget, OnInit, IDashboardWebview {
|
||||
|
||||
private _id: string;
|
||||
private _webview: WebviewElement;
|
||||
private _html: string;
|
||||
private _onMessage = new Emitter<string>();
|
||||
public readonly onMessage: Event<string> = this._onMessage.event;
|
||||
private _onMessageDisposable: IDisposable;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private readonly _dashboardService: DashboardServiceInterface,
|
||||
@Inject(WIDGET_CONFIG) protected readonly _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ElementRef)) private readonly _el: ElementRef,
|
||||
@Inject(IDashboardViewService) private readonly dashboardViewService: IDashboardViewService,
|
||||
@Inject(IWebviewService) private readonly webviewService: IWebviewService
|
||||
) {
|
||||
super();
|
||||
this._id = (_config.widget[selector] as IWebviewWidgetConfig).id;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dashboardViewService.registerWebview(this);
|
||||
this._createWebview();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public setHtml(html: string): void {
|
||||
this._html = html;
|
||||
if (this._webview) {
|
||||
this._webview.html = html;
|
||||
}
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get connection(): azdata.connection.Connection {
|
||||
const currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
const connection: azdata.connection.Connection = {
|
||||
providerName: currentConnection.providerName,
|
||||
connectionId: currentConnection.id,
|
||||
options: currentConnection.options
|
||||
};
|
||||
return connection;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get serverInfo(): azdata.ServerInfo {
|
||||
return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
// no op
|
||||
}
|
||||
|
||||
public sendMessage(message: string): void {
|
||||
if (this._webview) {
|
||||
this._webview.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private _createWebview(): void {
|
||||
if (this._webview) {
|
||||
this._webview.dispose();
|
||||
}
|
||||
if (this._onMessageDisposable) {
|
||||
this._onMessageDisposable.dispose();
|
||||
}
|
||||
|
||||
this._webview = this.webviewService.createWebview(this.id,
|
||||
{},
|
||||
{
|
||||
allowScripts: true,
|
||||
});
|
||||
|
||||
this._webview.mountTo(this._el.nativeElement);
|
||||
this._onMessageDisposable = this._webview.onMessage(e => {
|
||||
this._onMessage.fire(e);
|
||||
});
|
||||
if (this._html) {
|
||||
this._webview.html = this._html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerDashboardWidget } from 'sql/platform/dashboard/browser/widgetRegistry';
|
||||
|
||||
const webviewSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
registerDashboardWidget('webview-widget', '', webviewSchema, undefined, { extensionOnly: true });
|
||||
Reference in New Issue
Block a user