mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
392
src/sql/parts/dashboard/widgets/explorer/explorerTree.ts
Normal file
392
src/sql/parts/dashboard/widgets/explorer/explorerTree.ts
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ explorer-widget .list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
margin-left: -33px;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface IInsightData {
|
||||
|
||||
export interface IInsightsView {
|
||||
data: IInsightData;
|
||||
config?: { [key: string]: any };
|
||||
setConfig?: (config: { [key: string]: any }) => void;
|
||||
init?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(' ')));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@ let imageInsightSchema: IJSONSchema = {
|
||||
}
|
||||
};
|
||||
|
||||
registerInsight('image', '', imageInsightSchema, ImageInsight);
|
||||
registerInsight('image', '', imageInsightSchema, ImageInsight);
|
||||
|
||||
@@ -104,5 +104,5 @@ export const properties: Array<ProviderProperties> = [
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
];
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
src/sql/parts/dashboard/widgets/tasks/media/taskWidget.css
Normal file
36
src/sql/parts/dashboard/widgets/tasks/media/taskWidget.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user