Refresh master with initial release/0.24 snapshot (#332)

* Initial port of release/0.24 source code

* Fix additional headers

* Fix a typo in launch.json
This commit is contained in:
Karl Burtram
2017-12-15 15:38:57 -08:00
committed by GitHub
parent 271b3a0b82
commit 6ad0df0e3e
7118 changed files with 107999 additions and 56466 deletions

View File

@@ -1,89 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { IConnectionManagementService, MetadataType } from 'sql/parts/connection/common/connectionManagement';
import {
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction,
BackupAction, BaseActionContext, ManageAction
} from 'sql/workbench/common/actions';
import { IDisasterRecoveryUiService } from 'sql/parts/disasterRecovery/common/interfaces';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction } from 'vs/base/common/actions';
export function GetExplorerActions(type: MetadataType, isCloud: boolean, dashboardService: DashboardServiceInterface): TPromise<IAction[]> {
let actions: IAction[] = [];
// When context menu on database
if (type === undefined) {
actions.push(dashboardService.instantiationService.createInstance(DashboardNewQueryAction, DashboardNewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
if (!isCloud) {
actions.push(dashboardService.instantiationService.createInstance(DashboardBackupAction, DashboardBackupAction.ID, DashboardBackupAction.LABEL));
}
actions.push(dashboardService.instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
return TPromise.as(actions);
}
if (type === MetadataType.View || type === MetadataType.Table) {
actions.push(dashboardService.instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
}
if (type === MetadataType.Table) {
actions.push(dashboardService.instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
}
actions.push(dashboardService.instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
return TPromise.as(actions);
}
export class DashboardBackupAction extends BackupAction {
public static ID = 'dashboard.' + BackupAction.ID;
constructor(
id: string, label: string,
@IDisasterRecoveryUiService disasterRecoveryService: IDisasterRecoveryUiService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService
) {
super(id, label, BackupAction.ICON, disasterRecoveryService, );
}
run(actionContext: BaseActionContext): TPromise<boolean> {
let self = this;
// change database before performing action
return new TPromise<boolean>((resolve, reject) => {
self.connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
actionContext.connInfo = self.connectionManagementService.getConnectionInfo(actionContext.uri);
super.run(actionContext).then((result) => {
resolve(result);
});
},
() => {
resolve(false);
});
});
}
}
export class DashboardNewQueryAction extends NewQueryAction {
public static ID = 'dashboard.' + NewQueryAction.ID;
run(actionContext: BaseActionContext): TPromise<boolean> {
let self = this;
// change database before performing action
return new TPromise<boolean>((resolve, reject) => {
self._connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
actionContext.profile = self._connectionManagementService.getConnectionProfile(actionContext.uri);
super.run(actionContext).then((result) => {
resolve(result);
});
},
() => {
resolve(false);
});
});
}
}

View File

@@ -0,0 +1,392 @@
/*---------------------------------------------------------------------------------------------
* 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/parts/connection/common/interfaces';
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
import { SingleConnectionManagementService } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import {
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction, ScriptExecuteAction, ScriptAlterAction,
BackupAction, ManageActionContext, BaseActionContext, ManageAction, RestoreAction
} from 'sql/workbench/common/actions';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import * as Constants from 'sql/parts/connection/common/constants';
import { ObjectMetadata } from 'data';
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { generateUuid } from 'vs/base/common/uuid';
import { $ } from 'vs/base/browser/dom';
export class ObjectMetadataWrapper implements ObjectMetadata {
public metadataType: MetadataType;
public metadataTypeName: string;
public urn: string;
public name: string;
public schema: string;
constructor(from?: ObjectMetadata) {
if (from) {
this.metadataType = from.metadataType;
this.metadataTypeName = from.metadataTypeName;
this.urn = from.urn;
this.name = from.name;
this.schema = from.schema;
}
}
public matches(other: ObjectMetadataWrapper): boolean {
if (!other) {
return false;
}
return this.metadataType === other.metadataType
&& this.schema === other.schema
&& this.name === other.name;
}
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
if (!objectMetadata) {
return undefined;
}
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
}
// custom sort : Table > View > Stored Procedures > Function
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
// compare the object type
if (metadata1.metadataType < metadata2.metadataType) {
return -1;
} else if (metadata1.metadataType > metadata2.metadataType) {
return 1;
// otherwise compare the schema
} else {
let schemaCompare: number = metadata1.schema && metadata2.schema
? metadata1.schema.localeCompare(metadata2.schema)
// schemas are not expected to be undefined, but if they are then compare using object names
: 0;
if (schemaCompare !== 0) {
return schemaCompare;
// otherwise compare the object name
} else {
return metadata1.name.localeCompare(metadata2.name);
}
}
}
}
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 {
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 _contextMenuService: IContextMenuService,
private _capabilitiesService: ICapabilitiesService,
private _instantiationService: IInstantiationService
) {
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 {
let context: ManageActionContext | BaseActionContext;
if (element instanceof ObjectMetadataWrapper) {
context = {
object: element,
profile: this._connectionService.connectionInfo.connectionProfile
};
} else {
context = {
profile: element,
uri: this._uri
};
}
this._contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => GetExplorerActions(element, this._instantiationService, this._capabilitiesService, this._connectionService.connectionInfo),
getActionsContext: () => context
});
return true;
}
private handleItemDoubleClick(element: IConnectionProfile): void {
this._connectionService.changeDatabase(element.databaseName).then(result => {
this._router.navigate(['database-dashboard']);
});
}
}
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 {
if (element instanceof ExplorerModel) {
return TPromise.as(this._data);
} else {
return TPromise.as(undefined);
}
}
public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise {
if (element instanceof ExplorerModel) {
return TPromise.as(undefined);
} else {
return TPromise.as(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 {
let row = $('.list-row');
let 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 stored-procedure';
break;
case MetadataType.Table:
templateData.icon.className = 'icon table';
break;
case MetadataType.View:
templateData.icon.className = 'icon view';
break;
}
templateData.label.innerText = element.schema + '.' + element.name;
} else {
templateData.label.innerText = element.databaseName;
}
}
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;
}
let filterString = this._filterString.trim().toLowerCase();
return element.databaseName.toLowerCase().includes(filterString);
}
// 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.includes(':')) {
let filterArray = filterString.split(':');
if (filterArray.length > 2) {
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
} else {
filterString = filterArray[1];
}
switch (filterArray[0].toLowerCase()) {
case 'v':
metadataType = MetadataType.View;
break;
case 't':
metadataType = MetadataType.Table;
break;
case 'sp':
metadataType = MetadataType.SProc;
break;
case 'f':
metadataType = MetadataType.Function;
break;
case 'a':
return true;
default:
break;
}
}
if (metadataType !== undefined) {
return element.metadataType === metadataType && (element.schema + '.' + element.name).toLowerCase().includes(filterString);
} else {
return (element.schema + '.' + element.name).toLowerCase().includes(filterString);
}
}
public set filterString(val: string) {
this._filterString = val;
}
}
function GetExplorerActions(element: TreeResource, instantiationService: IInstantiationService, capabilitiesService: ICapabilitiesService, info: ConnectionManagementInfo): TPromise<IAction[]> {
let actions: IAction[] = [];
if (element instanceof ObjectMetadataWrapper) {
if (element.metadataType === MetadataType.View || element.metadataType === MetadataType.Table) {
actions.push(instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
}
if (element.metadataType === MetadataType.Table) {
actions.push(instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
}
if (element.metadataType === MetadataType.SProc && info.connectionProfile.providerName === Constants.mssqlProviderName) {
actions.push(instantiationService.createInstance(ScriptExecuteAction, ScriptExecuteAction.ID, ScriptExecuteAction.LABEL));
}
if ((element.metadataType === MetadataType.SProc || element.metadataType === MetadataType.Function || element.metadataType === MetadataType.View)
&& info.connectionProfile.providerName === Constants.mssqlProviderName) {
actions.push(instantiationService.createInstance(ScriptAlterAction, ScriptAlterAction.ID, ScriptAlterAction.LABEL));
}
} else {
actions.push(instantiationService.createInstance(NewQueryAction, NewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
let action: IAction = instantiationService.createInstance(RestoreAction, RestoreAction.ID, RestoreAction.LABEL, RestoreAction.ICON);
if (capabilitiesService.isFeatureAvailable(action, info)) {
actions.push(action);
}
action = instantiationService.createInstance(BackupAction, BackupAction.ID, BackupAction.LABEL, BackupAction.ICON);
if (capabilitiesService.isFeatureAvailable(action, info)) {
actions.push(action);
}
actions.push(instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
return TPromise.as(actions);
}
actions.push(instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
return TPromise.as(actions);
}

View File

@@ -9,4 +9,4 @@
<div style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%"></div>
</div>
</div>
</div>

View File

@@ -7,196 +7,41 @@ import 'vs/css!sql/media/objectTypes/objecttypes';
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./media/explorerWidget';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Router } from '@angular/router';
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { BaseActionContext } from 'sql/workbench/common/actions';
import { GetExplorerActions } from './explorerActions';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { warn } from 'sql/base/common/log';
import { MultipleRequestDelayer } from 'sql/base/common/async';
import { ExplorerFilter, ExplorerRenderer, ExplorerDataSource, ExplorerController, ObjectMetadataWrapper, ExplorerModel } from './explorerTree';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IDisposable } from 'vs/base/common/lifecycle';
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler';
import * as nls from 'vs/nls';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import * as types from 'vs/base/common/types';
import { $, getContentHeight } from 'vs/base/browser/dom';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { getContentHeight } from 'vs/base/browser/dom';
import { Delayer } from 'vs/base/common/async';
import { ObjectMetadata } from 'data';
export class ObjectMetadataWrapper implements ObjectMetadata {
public metadataType: MetadataType;
public metadataTypeName: string;
public urn: string;
public name: string;
public schema: string;
constructor(from?: ObjectMetadata) {
if (from) {
this.metadataType = from.metadataType;
this.metadataTypeName = from.metadataTypeName;
this.urn = from.urn;
this.name = from.name;
this.schema = from.schema;
}
}
public matches(other: ObjectMetadataWrapper): boolean {
if (!other) {
return false;
}
return this.metadataType === other.metadataType
&& this.schema === other.schema
&& this.name === other.name;
}
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
if (!objectMetadata) {
return undefined;
}
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
}
// custom sort : Table > View > Stored Procedures > Function
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
// compare the object type
if (metadata1.metadataType < metadata2.metadataType) {
return -1;
} else if (metadata1.metadataType > metadata2.metadataType) {
return 1;
// otherwise compare the schema
} else {
let schemaCompare: number = metadata1.schema && metadata2.schema
? metadata1.schema.localeCompare(metadata2.schema)
// schemas are not expected to be undefined, but if they are then compare using object names
: 0;
if (schemaCompare !== 0) {
return schemaCompare;
// otherwise compare the object name
} else {
return metadata1.name.localeCompare(metadata2.name);
}
}
}
}
declare type ListResource = string | ObjectMetadataWrapper;
enum TemplateIds {
STRING = 'string',
METADATA = 'metadata'
}
interface IListTemplate {
icon?: HTMLElement;
label: HTMLElement;
}
class Delegate implements IDelegate<ListResource> {
getHeight(element: ListResource): number {
return 22;
}
getTemplateId(element: ListResource): string {
if (element instanceof ObjectMetadataWrapper) {
return TemplateIds.METADATA.toString();
} else if (types.isString(element)) {
return TemplateIds.STRING.toString();
} else {
return '';
}
}
}
class StringRenderer implements IRenderer<string, IListTemplate> {
public readonly templateId = TemplateIds.STRING.toString();
renderTemplate(container: HTMLElement): IListTemplate {
let row = $('.list-row');
let icon = $('.icon.database');
let label = $('.label');
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
renderElement(element: string, index: number, templateData: IListTemplate): void {
templateData.label.innerText = element;
}
disposeTemplate(templateData: IListTemplate): void {
// no op
}
}
class MetadataRenderer implements IRenderer<ObjectMetadataWrapper, IListTemplate> {
public readonly templateId = TemplateIds.METADATA.toString();
renderTemplate(container: HTMLElement): IListTemplate {
let row = $('.list-row');
let icon = $('div');
let label = $('.label');
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
renderElement(element: ObjectMetadataWrapper, index: number, templateData: IListTemplate): void {
if (element && element) {
switch (element.metadataType) {
case MetadataType.Function:
templateData.icon.className = 'icon scalarvaluedfunction';
break;
case MetadataType.SProc:
templateData.icon.className = 'icon stored-procedure';
break;
case MetadataType.Table:
templateData.icon.className = 'icon table';
break;
case MetadataType.View:
templateData.icon.className = 'icon view';
break;
}
templateData.label.innerText = element.schema + '.' + element.name;
}
}
disposeTemplate(templateData: IListTemplate): void {
// no op
}
}
@Component({
selector: 'explorer-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/explorer/explorerWidget.component.html'))
})
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
private _isCloud: boolean;
private _tableData: ListResource[];
private _disposables: Array<IDisposable> = [];
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit {
private _input: InputBox;
private _table: List<ListResource>;
private _lastClickedItem: ListResource;
private _tree: Tree;
private _filterDelayer = new Delayer<void>(200);
private _dblClickDelayer = new MultipleRequestDelayer<void>(500);
private _treeController = new ExplorerController(
this._bootstrap.getUnderlyingUri(),
this._bootstrap.connectionManagementService,
this._router,
this._bootstrap.contextMenuService,
this._bootstrap.capabilitiesService,
this._bootstrap.instantiationService
);
private _treeRenderer = new ExplorerRenderer();
private _treeDataSource = new ExplorerDataSource();
private _treeFilter = new ExplorerFilter();
@ViewChild('input') private _inputContainer: ElementRef;
@ViewChild('table') private _tableContainer: ElementRef;
@@ -209,7 +54,6 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
) {
super();
this._isCloud = _bootstrap.connectionManagementService.connectionInfo.serverInfo.isCloud;
this.init();
}
@@ -218,41 +62,34 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
placeholder: this._config.context === 'database' ? nls.localize('seachObjects', 'Search by name of type (a:, t:, v:, f:, or sp:)') : nls.localize('searchDatabases', 'Search databases')
};
this._input = new InputBox(this._inputContainer.nativeElement, this._bootstrap.contextViewService, inputOptions);
this._disposables.push(this._input.onDidChange(e => {
this._register(this._input.onDidChange(e => {
this._filterDelayer.trigger(() => {
this._table.splice(0, this._table.length, this._filterTable(e));
this._treeFilter.filterString = e;
this._tree.refresh();
});
}));
this._table = new List<ListResource>(this._tableContainer.nativeElement, new Delegate(), [new MetadataRenderer(), new StringRenderer()]);
this._disposables.push(this._table.onContextMenu(e => {
this.handleContextMenu(e.element, e.index, e.anchor);
}));
this._disposables.push(this._table.onSelectionChange(e => {
if (e.elements.length > 0 && this._lastClickedItem === e.elements[0]) {
this._dblClickDelayer.trigger(() => this.handleItemDoubleClick(e.elements[0]));
} else {
this._lastClickedItem = e.elements.length > 0 ? e.elements[0] : undefined;
}
}));
this._table.layout(getContentHeight(this._tableContainer.nativeElement));
this._disposables.push(this._input);
this._disposables.push(attachInputBoxStyler(this._input, this._bootstrap.themeService));
this._disposables.push(this._table);
this._disposables.push(attachListStyler(this._table, this._bootstrap.themeService));
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
});
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
this._register(this._input);
this._register(attachInputBoxStyler(this._input, this._bootstrap.themeService));
this._register(this._tree);
this._register(attachListStyler(this._tree, this._bootstrap.themeService));
}
private init(): void {
if (this._config.context === 'database') {
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
this._register(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
data => {
if (data) {
this._tableData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
this._tableData.sort(ObjectMetadataWrapper.sort);
this._table.splice(0, this._table.length, this._tableData);
let objectData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
objectData.sort(ObjectMetadataWrapper.sort);
this._treeDataSource.data = objectData;
this._tree.setInput(new ExplorerModel());
}
},
error => {
@@ -260,10 +97,16 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
}
)));
} else {
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
let currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
this._register(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
data => {
this._tableData = data;
this._table.splice(0, this._table.length, this._tableData);
let profileData = data.map(d => {
let profile = new ConnectionProfile(currentProfile.serverCapabilities, 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");
@@ -272,123 +115,7 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
}
}
/**
* Handles action when an item is double clicked in the explorer widget
* @param val If on server page, explorer objects will be strings representing databases;
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
*
*/
private handleItemDoubleClick(val: ListResource): void {
if (types.isString(val)) {
this._bootstrap.connectionManagementService.changeDatabase(val as string).then(result => {
this._router.navigate(['database-dashboard']);
});
}
}
/**
* Handles action when a item is clicked in the explorer widget
* @param val If on server page, explorer objects will be strings representing databases;
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
* @param index Index of the value in the array the ngFor template is built from
* @param event Click event
*/
private handleContextMenu(val: ListResource, index: number, anchor: HTMLElement | { x: number, y: number }): void {
// event will exist if the context menu span was clicked
if (event) {
if (this._config.context === 'server') {
let newProfile = <IConnectionProfile>Object.create(this._bootstrap.connectionManagementService.connectionInfo.connectionProfile);
newProfile.databaseName = val as string;
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => GetExplorerActions(undefined, this._isCloud, this._bootstrap),
getActionsContext: () => {
return <BaseActionContext>{
uri: this._bootstrap.getUnderlyingUri(),
profile: newProfile,
connInfo: this._bootstrap.connectionManagementService.connectionInfo,
databasename: val as string
};
}
});
} else if (this._config.context === 'database') {
let object = val as ObjectMetadataWrapper;
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => GetExplorerActions(object.metadataType, this._isCloud, this._bootstrap),
getActionsContext: () => {
return <BaseActionContext>{
object: object,
uri: this._bootstrap.getUnderlyingUri(),
profile: this._bootstrap.connectionManagementService.connectionInfo.connectionProfile
};
}
});
} else {
warn('Unknown dashboard context: ', this._config.context);
}
}
this._changeRef.detectChanges();
}
private _filterTable(val: string): ListResource[] {
let items = this._tableData;
if (!items) {
return items;
}
// format filter string for clean filter, no white space and lower case
let filterString = val.trim().toLowerCase();
// handle case when passed a string array
if (types.isString(items[0])) {
let _items = <string[]>items;
return _items.filter(item => {
return item.toLowerCase().includes(filterString);
});
}
// make typescript compiler happy
let objectItems = items as ObjectMetadataWrapper[];
// determine is a filter is applied
let metadataType: MetadataType;
if (val.includes(':')) {
let filterArray = filterString.split(':');
if (filterArray.length > 2) {
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
} else {
filterString = filterArray[1];
}
switch (filterArray[0].toLowerCase()) {
case 'v':
metadataType = MetadataType.View;
break;
case 't':
metadataType = MetadataType.Table;
break;
case 'sp':
metadataType = MetadataType.SProc;
break;
case 'f':
metadataType = MetadataType.Function;
break;
case 'a':
return objectItems;
default:
break;
}
}
return objectItems.filter(item => {
if (metadataType !== undefined) {
return item.metadataType === metadataType && (item.schema + '.' + item.name).toLowerCase().includes(filterString);
} else {
return (item.schema + '.' + item.name).toLowerCase().includes(filterString);
}
});
public refresh(): void {
this.init();
}
}

View File

@@ -11,4 +11,5 @@ explorer-widget .list-row {
display: flex;
flex-direction: row;
align-items: center;
}
margin-left: -33px;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import {
Component, Inject, ViewContainerRef, forwardRef, AfterContentInit,
ComponentFactoryResolver, ViewChild, OnDestroy, ChangeDetectorRef
ComponentFactoryResolver, ViewChild, ChangeDetectorRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
@@ -15,39 +15,45 @@ import { InsightAction, InsightActionContext } from 'sql/workbench/common/action
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { IInsightsConfig, IInsightsView } from './interfaces';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { insertValueRegex } from 'sql/parts/insights/browser/insightsDialogView';
import { insertValueRegex } from 'sql/parts/insights/common/interfaces';
import { RunInsightQueryAction } from './actions';
import { SimpleExecuteResult } from 'data';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Action } from 'vs/base/common/actions';
import * as types from 'vs/base/common/types';
import * as pfs from 'vs/base/node/pfs';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { WorkbenchState } from 'vs/platform/workspace/common/workspace';
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 style="margin: 10px; width: calc(100% - 20px); height: calc(100% - 20px)">
<ng-template component-host></ng-template>
</div>`,
styles: [':host { width: 100%; height: 100%}']
styles: [':host { width: 100%; height: 100% }']
})
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit, OnDestroy {
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit {
private insightConfig: IInsightsConfig;
private queryObv: Observable<SimpleExecuteResult>;
private _disposables: Array<IDisposable> = [];
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
private _typeKey: string;
private _init: boolean = false;
public error: string;
public lastUpdated: string;
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
@@ -90,7 +96,7 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
ngAfterContentInit() {
this._init = true;
if (this.queryObv) {
this._disposables.push(toDisposableSubscription(this.queryObv.subscribe(
this._register(toDisposableSubscription(this.queryObv.subscribe(
result => {
this._updateChild(result);
},
@@ -101,10 +107,6 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
}
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
}
private showError(error: string): void {
this.error = error;
this._cd.detectChanges();
@@ -128,7 +130,11 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
private _storeResult(result: SimpleExecuteResult): SimpleExecuteResult {
if (this.insightConfig.cacheId) {
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(result));
let store: IStorageResult = {
date: new Date().toString(),
results: result
};
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(store));
}
return result;
}
@@ -137,8 +143,12 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
if (this.insightConfig.cacheId) {
let storage = this.dashboardService.storageService.get(this._getStorageKey());
if (storage) {
let storedResult: IStorageResult = JSON.parse(storage);
let date = new Date(storedResult.date);
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", date.toLocaleTimeString(), date.toLocaleDateString());
if (this._init) {
this._updateChild(JSON.parse(storage));
this._updateChild(storedResult.results);
this._cd.detectChanges();
} else {
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
}
@@ -151,17 +161,11 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
return false;
}
public get refresh(): () => void {
return this._refresh();
}
public _refresh(): () => void {
return () => {
this._runQuery().then(
result => this._updateChild(result),
error => this.showError(error)
);
};
public refresh(): void {
this._runQuery().then(
result => this._updateChild(result),
error => this.showError(error)
);
}
private _getStorageKey(): string {
@@ -192,7 +196,10 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
let componentInstance = componentRef.instance;
componentInstance.data = { columns: result.columnInfo.map(item => item.columnName), rows: result.rows.map(row => row.map(item => item.displayValue)) };
// check if the setter is defined
componentInstance.config = this.insightConfig.type[this._typeKey];
if (componentInstance.setConfig) {
componentInstance.setConfig(this.insightConfig.type[this._typeKey]);
}
if (componentInstance.init) {
componentInstance.init();
}
@@ -239,7 +246,28 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
let match = filePath.match(insertValueRegex);
if (match && match.length > 0 && match[1] === 'workspaceRoot') {
filePath = filePath.replace(match[0], '');
filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
//filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
switch (this.dashboardService.workspaceContextService.getWorkbenchState()) {
case WorkbenchState.FOLDER:
filePath = this.dashboardService.workspaceContextService.getWorkspace().folders[0].toResource(filePath).fsPath;
break;
case WorkbenchState.WORKSPACE:
let filePathArray = filePath.split('/');
// filter out empty sections
filePathArray = filePathArray.filter(i => !!i);
let folder = this.dashboardService.workspaceContextService.getWorkspace().folders.find(i => i.name === filePathArray[0]);
if (!folder) {
return Promise.reject<void[]>(new Error(`Could not find workspace folder ${filePathArray[0]}`));
}
// remove the folder name from the filepath
filePathArray.shift();
// rejoin the filepath after doing the work to find the right folder
filePath = '/' + filePathArray.join('/');
filePath = folder.toResource(filePath).fsPath;
break;
}
}
promises.push(new Promise((resolve, reject) => {
pfs.readFile(filePath).then(
@@ -256,4 +284,4 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
return Promise.all(promises);
}
}
}

View File

@@ -4,19 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { IInsightRegistry, Extensions as InsightExtensions } from 'sql/platform/dashboard/common/insightRegistry';
import { ITaskRegistry, Extensions as TaskExtensions } from 'sql/platform/tasks/taskRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
const taskRegistry = Registry.as<ITaskRegistry>(TaskExtensions.TaskContribution);
export const insightsSchema: IJSONSchema = {
type: 'object',
description: nls.localize('insightWidgetDescription', 'Adds a widget that can query a server or database and display the results in multiple ways - as a chart, summarized count, and more'),
properties: {
id: {
cacheId: {
type: 'string',
description: nls.localize('insightIdDescription', 'Unique Identifier used for cacheing the results of the insight.')
},
@@ -87,8 +85,11 @@ export const insightsSchema: IJSONSchema = {
type: 'object',
properties: {
types: {
type: 'object',
properties: taskRegistry.taskSchemas
description: nls.localize('actionTypes', "Which actions to use"),
type: 'array',
items: {
type: 'string'
}
},
database: {
type: 'string',

View File

@@ -38,7 +38,7 @@ export interface IInsightData {
export interface IInsightsView {
data: IInsightData;
config?: { [key: string]: any };
setConfig?: (config: { [key: string]: any }) => void;
init?: () => void;
}

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, ViewChild } from '@angular/core';
import { BaseChartDirective } from 'ng2-charts/ng2-charts';
/* SQL Imports */
@@ -18,7 +18,7 @@ import * as colors from 'vs/platform/theme/common/colorRegistry';
import { mixin } from 'sql/base/common/objects';
import { Color } from 'vs/base/common/color';
import * as types from 'vs/base/common/types';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
export enum ChartType {
@@ -99,14 +99,13 @@ export const defaultChartConfig: IChartConfig = {
[options]="_options"></canvas>
</div>`
})
export abstract class ChartInsight implements IInsightsView, OnDestroy {
export abstract class ChartInsight extends Disposable implements IInsightsView {
private _isDataAvailable: boolean = false;
private _options: any = {};
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
protected _defaultConfig = defaultChartConfig;
protected _disposables: Array<IDisposable> = [];
protected _config: IChartConfig;
protected _data: IInsightData;
@@ -116,14 +115,13 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService) { }
ngOnDestroy() {
this._disposables.forEach(item => item.dispose());
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService
) {
super();
}
init() {
this._disposables.push(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._register(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
// Note: must use a boolean to not render the canvas until all properties such as the labels and chart type are set.
// This is because chart.js doesn't auto-update anything other than dataset when re-rendering so defaults are used
@@ -150,10 +148,12 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
}
protected updateTheme(e: IColorTheme): void {
let foregroundColor = e.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let options = {
legend: {
labels: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
}
}
};
@@ -189,7 +189,7 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
unmemoize(this, 'colors');
}
@Input() set config(config: IChartConfig) {
public setConfig(config: IChartConfig) {
this.clearMemoize();
this._config = mixin(config, this._defaultConfig, false);
this.legendPosition = this._config.legendPosition;

View File

@@ -3,40 +3,145 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChartInsight, ChartType, customMixin } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { ChartInsight, ChartType, customMixin, IChartConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { mixin } from 'sql/base/common/objects';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
export 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;
public setConfig(config: IBarChartConfig): void {
let options = {};
if (config.xAxisMax) {
let opts = {
scales: {
xAxes: [{
display: true,
ticks: {
max: config.xAxisMax
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.xAxisMin) {
let opts = {
scales: {
xAxes: [{
display: true,
ticks: {
min: config.xAxisMin
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.xAxisLabel) {
let opts = {
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: config.xAxisLabel
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.yAxisMax) {
let opts = {
scales: {
yAxes: [{
display: true,
ticks: {
max: config.yAxisMax
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.yAxisMin) {
let opts = {
scales: {
yAxes: [{
display: true,
ticks: {
max: config.yAxisMin
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.yAxisLabel) {
let 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: IColorTheme): void {
super.updateTheme(e);
let foregroundColor = e.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let gridLinesColor = e.getColor(editorLineNumbers);
let gridLines = gridLinesColor ? gridLinesColor.toString() : null;
let options = {
scales: {
xAxes: [{
scaleLabel: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
ticks: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
gridLines: {
color: e.getColor(editorLineNumbers)
color: gridLines
}
}],
yAxes: [{
scaleLabel: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
ticks: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
gridLines: {
color: e.getColor(editorLineNumbers)
color: gridLines
}
}]
}

View File

@@ -4,13 +4,40 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import BarChart from './barChart.component';
const properties: IJSONSchema = {
export const properties: IJSONSchema = {
properties: {
yAxisMin: {
type: 'number',
description: nls.localize('yAxisMin', "Minumum value of the y axis")
},
yAxisMax: {
type: 'number',
description: nls.localize('yAxisMax', "Maximum value of the y axis")
},
yAxisLabel: {
type: 'string',
description: nls.localize('yAxisLabel', "Label for the y axis")
},
xAxisMin: {
type: 'number',
description: nls.localize('xAxisMin', "Minumum value of the x axis")
},
xAxisMax: {
type: 'number',
description: nls.localize('xAxisMax', "Maximum value of the x axis")
},
xAxisLabel: {
type: 'string',
description: nls.localize('xAxisLabel', "Label for the x axis")
}
}
};
const barSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;

View File

@@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import HorizontalBarChart from './horizontalBarChart.component';
@@ -13,6 +14,6 @@ const properties: IJSONSchema = {
};
const horizontalBarSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
const horizontalBarSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('horizontalBar', '', horizontalBarSchema, HorizontalBarChart);

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChartType, customMixin, IChartConfig, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import BarChart from './barChart.component';
import { ChartType, customMixin, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import BarChart, { IBarChartConfig } from './barChart.component';
import { memoize, unmemoize } from 'sql/base/common/decorators';
import { mixin } from 'sql/base/common/objects';
import { clone } from 'vs/base/common/objects';
@@ -14,7 +14,7 @@ export enum DataType {
Point = 'point'
}
export interface ILineConfig extends IChartConfig {
export interface ILineConfig extends IBarChartConfig {
dataType?: DataType;
}
@@ -69,8 +69,8 @@ export default class LineChart extends BarChart {
}
protected addAxisLabels(): void {
let xLabel = this._data.columns[1] || 'x';
let yLabel = this._data.columns[2] || 'y';
let xLabel = this._config.xAxisLabel || this._data.columns[1] || 'x';
let yLabel = this._config.yAxisLabel || this._data.columns[2] || 'y';
let options = {
scales: {
xAxes: [{

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import LineChart from './lineChart.component';
import * as nls from 'vs/nls';
const properties: IJSONSchema = {
properties: {
dataType: {
@@ -23,6 +23,6 @@ const properties: IJSONSchema = {
}
};
export const lineSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
export const lineSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('line', '', lineSchema, LineChart);

View File

@@ -4,14 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import ScatterChart from './scatterChart.component';
const properties: IJSONSchema = {
};
const scatterSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
const scatterSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('scatter', '', scatterSchema, ScatterChart);

View File

@@ -15,8 +15,8 @@ export default class TimeSeriesChart extends LineChart {
protected _defaultConfig = defaultTimeSeriesConfig;
protected addAxisLabels(): void {
let xLabel = this.getLabels()[1] || 'x';
let yLabel = this.getLabels()[2] || 'y';
let xLabel = this._config.xAxisLabel || this.getLabels()[1] || 'x';
let yLabel = this._config.yAxisLabel || this.getLabels()[2] || 'y';
let options = {
scales: {

View File

@@ -4,14 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import TimeSeriesChart from './timeSeriesChart.component';
const properties: IJSONSchema = {
};
const timeSeriesSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
const timeSeriesSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('timeSeries', '', timeSeriesSchema, TimeSeriesChart);

View File

@@ -14,4 +14,4 @@ let countInsightSchema: IJSONSchema = {
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);
registerInsight('count', '', countInsightSchema, CountInsight);

View File

@@ -76,5 +76,4 @@ export default class ImageInsight implements IInsightsView, OnInit {
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')));
}
}
}

View File

@@ -28,4 +28,4 @@ let imageInsightSchema: IJSONSchema = {
}
};
registerInsight('image', '', imageInsightSchema, ImageInsight);
registerInsight('image', '', imageInsightSchema, ImageInsight);

View File

@@ -104,5 +104,5 @@ export const properties: Array<ProviderProperties> = [
]
}
]
},
}
];

View File

@@ -4,18 +4,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div #parent style="position: absolute; height: 100%; width: 100%; margin: 10px 0px 10px 0px; ">
<div [style.margin-right.px]="_clipped ? 30 : 0" [style.width]="_clipped ? 94 + '%' : '100%'" style="overflow: hidden">
<span #child style="white-space : nowrap; width: fit-content">
<ng-template ngFor let-item [ngForOf]="properties">
<span style="margin-left: 10px; display: inline-block;">
<div style="font-size: 11px; font-weight: lighter">{{item.displayName}}</div>
<div>{{item.value}}</div>
</span>
</ng-template>
</span>
</div>
<span *ngIf="_clipped" style="position: absolute; right: 0; top: 0; padding-top: 5px; padding-right: 14px; z-index: 2">
...
</span>
<div #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; font-weight: lighter">{{item.displayName}}</div>
<div>{{item.value}}</div>
</span>
</ng-template>
</span>
</div>
<span *ngIf="_clipped" style="position: absolute; right: 0; top: 0; padding-top: 5px; padding-right: 14px; z-index: 2">
...
</span>
</div>

View File

@@ -3,22 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ElementRef, ViewChild } from '@angular/core';
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { error } from 'sql/base/common/log';
import { properties } from './propertiesJson';
import { IDashboardRegistry, Extensions as DashboardExtensions } from 'sql/platform/dashboard/common/dashboardRegistry';
import { DatabaseInfo, ServerInfo } from 'data';
import { IDisposable } from 'vs/base/common/lifecycle';
import { EventType, addDisposableListener } from 'vs/base/browser/dom';
import * as types from 'vs/base/common/types';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
export interface PropertiesConfig {
properties: Array<Property>;
@@ -47,6 +46,8 @@ export interface Property {
default?: string;
}
const dashboardRegistry = Registry.as<IDashboardRegistry>(DashboardExtensions.DashboardContributions);
export interface DisplayProperty {
displayName: string;
value: string;
@@ -56,11 +57,10 @@ export interface DisplayProperty {
selector: 'properties-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/properties/propertiesWidget.component.html'))
})
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit {
private _connection: ConnectionManagementInfo;
private _databaseInfo: DatabaseInfo;
private _clipped: boolean;
private _disposables: Array<IDisposable> = [];
private properties: Array<DisplayProperty>;
private _hasInit = false;
@@ -83,21 +83,17 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
ngOnInit() {
this._hasInit = true;
this._disposables.push(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
this._register(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
this._changeRef.detectChanges();
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
}
public refresh(): void {
this.init();
}
private init(): void {
this._connection = this._bootstrap.connectionManagementService.connectionInfo;
this._disposables.push(toDisposableSubscription(this._bootstrap.adminService.databaseInfo.subscribe(data => {
this._register(toDisposableSubscription(this._bootstrap.adminService.databaseInfo.subscribe(data => {
this._databaseInfo = data;
this._changeRef.detectChanges();
this.parseProperties();
@@ -128,29 +124,13 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
let config = <PropertiesConfig>this._config.widget['properties-widget'];
propertyArray = config.properties;
} else {
let propertiesConfig: Array<ProviderProperties> = properties;
// ensure we have a properties file
if (!Array.isArray(propertiesConfig)) {
this.consoleError('Could not load properties JSON');
let providerProperties = dashboardRegistry.getProperties(provider as string);
if (!providerProperties) {
this.consoleError('No property definitions found for provider', provider);
return;
}
// filter the properties provided based on provider name
let providerPropertiesArray = propertiesConfig.filter((item) => {
return item.provider === provider;
});
// Error handling on provider
if (providerPropertiesArray.length === 0) {
this.consoleError('Could not locate properties for provider: ', provider);
return;
} else if (providerPropertiesArray.length > 1) {
this.consoleError('Found multiple property definitions for provider ', provider);
return;
}
let providerProperties = providerPropertiesArray[0];
let flavor: FlavorProperties;
// find correct flavor
@@ -262,4 +242,4 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
private consoleError(message?: any, ...optionalParams: any[]): void {
error(message, optionalParams);
}
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
tasks-widget .tile-container {
position: relative;
display: flex;
flex-flow: column;
flex-wrap: wrap;
}
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:last-of-type {
margin-right: 10px;
}
tasks-widget .task-tile > div {
flex: 1 1 auto;
display: flex;
flex-flow: column;
align-items: center;
}
tasks-widget .task-tile .icon {
padding: 15px;
}

View File

@@ -4,18 +4,5 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="task-widget">
<ng-template ngFor let-task [ngForOf]="_actions" let-i="index">
<div (click)="runTask(task)"
style="position: absolute; cursor: pointer; display: flex; flex-flow: row; align-items: center; text-align: center"
class="task-tile"
[style.height.px]="_size"
[style.width.px]="_size"
[style.transform]="calculateTransform(i)">
<div style="flex: 1 1 auto; display: flex; flex-flow: column; align-items: center">
<span [ngClass]="['icon', task.icon]" style="padding: 15px"></span>
<div>{{task.label}}</div>
</div>
</div>
</ng-template>
</div>
<div #container style="position: absolute; height: 100%; width: 100%">
</div>

View File

@@ -3,20 +3,20 @@
* 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, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, OnInit, ElementRef } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
/* SQL imports */
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ITaskRegistry, Extensions, ActionICtor } from 'sql/platform/tasks/taskRegistry';
import { ITaskRegistry, Extensions, TaskAction } from 'sql/platform/tasks/taskRegistry';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { ITaskActionContext } from 'sql/workbench/common/actions';
import { BaseActionContext } from 'sql/workbench/common/actions';
/* VS imports */
import { IDisposable } from 'vs/base/common/lifecycle';
import * as themeColors from 'vs/workbench/common/theme';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant, ICssStyleCollector, ITheme } from 'vs/platform/theme/common/themeService';
@@ -24,6 +24,11 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Action } from 'vs/base/common/actions';
import Severity from 'vs/base/common/severity';
import * as nls from 'vs/nls';
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 { $, Builder } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
interface IConfig {
tasks: Array<Object>;
@@ -33,14 +38,14 @@ interface IConfig {
selector: 'tasks-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/tasks/tasksWidget.component.html'))
})
export class TasksWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
private _size: number = 100;
private _margins: number = 10;
private _rows: number = 2;
private _isAzure = false;
private _themeDispose: IDisposable;
private _actions: Array<Action> = [];
export class TasksWidget extends DashboardWidget implements IDashboardWidget, OnInit {
private _size: number = 98;
private _tasks: Array<TaskAction> = [];
private _profile: IConnectionProfile;
private _scrollableElement: ScrollableElement;
private $container: Builder;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
@@ -52,35 +57,77 @@ export class TasksWidget extends DashboardWidget implements IDashboardWidget, On
this._profile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
let registry = Registry.as<ITaskRegistry>(Extensions.TaskContribution);
let tasksConfig = <IConfig>Object.values(this._config.widget)[0];
let connInfo = this._bootstrap.connectionManagementService.connectionInfo;
let taskIds: Array<string>;
if (tasksConfig.tasks) {
Object.keys(tasksConfig.tasks).forEach((item) => {
if (registry.idToCtorMap[item]) {
let ctor = registry.idToCtorMap[item];
this._actions.push(this._bootstrap.instantiationService.createInstance(ctor, ctor.ID, ctor.LABEL, ctor.ICON));
} else {
this._bootstrap.messageService.show(Severity.Warning, nls.localize('missingTask', 'Could not find task {0}; are you missing an extension?', item));
}
});
taskIds = Object.keys(tasksConfig.tasks);
} else {
let actions = Object.values(registry.idToCtorMap).map((item: ActionICtor) => {
let action = this._bootstrap.instantiationService.createInstance(item, item.ID, item.LABEL, item.ICON);
if (this._bootstrap.CapabilitiesService.isFeatureAvailable(action, connInfo)) {
return action;
} else {
return undefined;
}
});
this._actions = actions.filter(x => x !== undefined);
taskIds = registry.ids;
}
this._isAzure = connInfo.serverInfo.isCloud;
let ctorMap = registry.idToCtorMap;
this._tasks = taskIds.map(id => {
let ctor = ctorMap[id];
if (ctor) {
let action = this._bootstrap.instantiationService.createInstance(ctor, ctor.ID, ctor.LABEL, ctor.ICON);
if (this._bootstrap.capabilitiesService.isFeatureAvailable(action, this._bootstrap.connectionManagementService.connectionInfo)) {
return action;
}
} else {
this._bootstrap.messageService.show(Severity.Warning, nls.localize('missingTask', 'Could not find task {0}; are you missing an extension?', id));
}
return undefined;
}).filter(a => !types.isUndefinedOrNull(a));
}
ngOnInit() {
this._themeDispose = registerThemingParticipant(this.registerThemeing);
this._register(registerThemingParticipant(this.registerThemeing));
this._computeContainer();
this._tasks.map(a => {
this.$container.append(this._createTile(a));
});
this._scrollableElement = this._register(new ScrollableElement(this.$container.getHTMLElement(), {
horizontal: ScrollbarVisibility.Auto,
vertical: ScrollbarVisibility.Hidden,
scrollYToX: true,
useShadows: false
}));
this._scrollableElement.onScroll(e => {
this.$container.getHTMLElement().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.$container.getHTMLElement()) + 18 // right padding
});
}
private _computeContainer(): void {
let height = DOM.getContentHeight(this._container.nativeElement);
let tilesHeight = Math.floor(height / (this._size + 10));
let width = (this._size + 18) * Math.ceil(this._tasks.length / tilesHeight);
if (!this.$container) {
this.$container = $('.tile-container');
this._register(this.$container);
}
this.$container.style('height', height + 'px').style('width', width + 'px');
}
private _createTile(action: TaskAction): HTMLElement {
let label = $('div').safeInnerHtml(action.label);
let icon = $('span.icon').addClass(action.icon);
let innerTile = $('div').append(icon).append(label);
let tile = $('div.task-tile').style('height', this._size + 'px').style('width', this._size + 'px');
tile.append(innerTile);
tile.on(DOM.EventType.CLICK, () => this.runTask(action));
return tile.getHTMLElement();
}
private registerThemeing(theme: ITheme, collector: ICssStyleCollector) {
@@ -88,30 +135,26 @@ export class TasksWidget extends DashboardWidget implements IDashboardWidget, On
let sideBarColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND);
if (contrastBorder) {
let contrastBorderString = contrastBorder.toString();
collector.addRule(`.task-widget .task-tile { border: 1px solid ${contrastBorderString} }`);
collector.addRule(`tasks-widget .task-tile { border: 1px solid ${contrastBorderString} }`);
} else {
let sideBarColorString = sideBarColor.toString();
collector.addRule(`.task-widget .task-tile { background-color: ${sideBarColorString} }`);
collector.addRule(`tasks-widget .task-tile { background-color: ${sideBarColorString} }`);
}
}
ngOnDestroy() {
this._themeDispose.dispose();
}
//tslint:disable-next-line
private calculateTransform(index: number): string {
let marginy = (1 + (index % this._rows)) * this._margins;
let marginx = (1 + (Math.floor(index / 2))) * this._margins;
let posx = (this._size * (Math.floor(index / 2))) + marginx;
let posy = (this._size * (index % this._rows)) + marginy;
return 'translate(' + posx + 'px, ' + posy + 'px)';
}
public runTask(task: Action) {
let context: ITaskActionContext = {
let context: BaseActionContext = {
profile: this._profile
};
task.run(context);
}
public layout(): void {
this._computeContainer();
// Update scrollbar
this._scrollableElement.setScrollDimensions({
width: DOM.getContentWidth(this._container.nativeElement),
scrollWidth: DOM.getContentWidth(this.$container.getHTMLElement()) + 18 // right padding
});
}
}