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:
@@ -2,28 +2,112 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IAngularEventingService, AngularEventType } from 'sql/services/angularEventing/angularEventingService';
|
||||
|
||||
export class RefreshWidgetAction extends Action {
|
||||
export class EditDashboardAction extends Action {
|
||||
|
||||
public static ID = 'refreshWidget';
|
||||
public static LABEL = nls.localize('refreshWidget', 'Refresh');
|
||||
private static readonly ID = 'editDashboard';
|
||||
private static readonly EDITLABEL = nls.localize('editDashboard', "Edit");
|
||||
private static readonly EXITLABEL = nls.localize('editDashboardExit', "Exit");
|
||||
private static readonly ICON = 'edit';
|
||||
|
||||
private _state = 0;
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
private refreshFn: () => void
|
||||
private editFn: () => void,
|
||||
private context: any //this
|
||||
) {
|
||||
super(id, label);
|
||||
super(EditDashboardAction.ID, EditDashboardAction.EDITLABEL, EditDashboardAction.ICON);
|
||||
}
|
||||
|
||||
run(): TPromise<boolean> {
|
||||
try {
|
||||
this.refreshFn();
|
||||
this.editFn.apply(this.context);
|
||||
this.toggleLabel();
|
||||
return TPromise.as(true);
|
||||
} catch (e) {
|
||||
return TPromise.as(false);
|
||||
}
|
||||
}
|
||||
|
||||
private toggleLabel(): void {
|
||||
if (this._state === 0) {
|
||||
this.label = EditDashboardAction.EXITLABEL;
|
||||
this._state = 1;
|
||||
} else {
|
||||
this.label = EditDashboardAction.EDITLABEL;
|
||||
this._state = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RefreshWidgetAction extends Action {
|
||||
|
||||
private static readonly ID = 'refreshWidget';
|
||||
private static readonly LABEL = nls.localize('refreshWidget', 'Refresh');
|
||||
private static readonly ICON = 'refresh';
|
||||
|
||||
constructor(
|
||||
private refreshFn: () => void,
|
||||
private context: any // this
|
||||
) {
|
||||
super(RefreshWidgetAction.ID, RefreshWidgetAction.LABEL, RefreshWidgetAction.ICON);
|
||||
}
|
||||
|
||||
run(): TPromise<boolean> {
|
||||
try {
|
||||
this.refreshFn.apply(this.context);
|
||||
return TPromise.as(true);
|
||||
} catch (e) {
|
||||
return TPromise.as(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ToggleMoreWidgetAction extends Action {
|
||||
|
||||
private static readonly ID = 'toggleMore';
|
||||
private static readonly LABEL = nls.localize('toggleMore', 'Toggle More');
|
||||
private static readonly ICON = 'toggle-more';
|
||||
|
||||
constructor(
|
||||
private _actions: Array<IAction>,
|
||||
private _context: any,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService
|
||||
) {
|
||||
super(ToggleMoreWidgetAction.ID, ToggleMoreWidgetAction.LABEL, ToggleMoreWidgetAction.ICON);
|
||||
}
|
||||
|
||||
run(context: StandardKeyboardEvent): TPromise<boolean> {
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => context.target,
|
||||
getActions: () => TPromise.as(this._actions),
|
||||
getActionsContext: () => this._context
|
||||
});
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteWidgetAction extends Action {
|
||||
private static readonly ID = 'deleteWidget';
|
||||
private static readonly LABEL = nls.localize('deleteWidget', "Delete Widget");
|
||||
private static readonly ICON = 'close';
|
||||
|
||||
constructor(
|
||||
private _widgetId,
|
||||
private _uri,
|
||||
@IAngularEventingService private angularEventService: IAngularEventingService
|
||||
) {
|
||||
super(DeleteWidgetAction.ID, DeleteWidgetAction.LABEL, DeleteWidgetAction.ICON);
|
||||
}
|
||||
|
||||
run(): TPromise<boolean> {
|
||||
this.angularEventService.sendAngularEvent(this._uri, AngularEventType.DELETE_WIDGET, { id: this._widgetId });
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div #propertyContainer>
|
||||
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="margin-left: 10px; margin-right: 10px; height: 90px; display: block">
|
||||
</dashboard-widget-wrapper>
|
||||
</div>
|
||||
<div>
|
||||
<div [ngGrid]="gridConfig">
|
||||
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
|
||||
</dashboard-widget-wrapper>
|
||||
<div #scrollContainer style="height: 100%">
|
||||
<div #scrollable style="position: relative">
|
||||
<div #propertiesContainer>
|
||||
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="padding-left: 10px; padding-right: 10px; height: 90px; display: block">
|
||||
</dashboard-widget-wrapper>
|
||||
</div>
|
||||
<div [ngGrid]="gridConfig">
|
||||
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
|
||||
</dashboard-widget-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,22 +3,33 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList } from '@angular/core';
|
||||
import { NgGridConfig } from 'angular2-grid';
|
||||
import 'vs/css!./dashboardPage';
|
||||
|
||||
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { NgGridConfig, NgGrid, NgGridItem } from 'angular2-grid';
|
||||
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
|
||||
import { subscriptionToDisposable } from 'sql/base/common/lifecycle';
|
||||
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { Severity } from 'vs/platform/message/common/message';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { addDisposableListener, getContentHeight, EventType } from 'vs/base/browser/dom';
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
|
||||
@@ -27,14 +38,56 @@ function isNumberArray(value: any): value is number[] {
|
||||
return types.isArray(value) && (<any[]>value).every(elem => types.isNumber(elem));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting function for dashboard widgets
|
||||
* In order of priority;
|
||||
* If neither have defined grid positions, they are equivalent
|
||||
* If a has a defined grid position and b does not; a should come first
|
||||
* If both have defined grid positions and have the same row; the one with the smaller col position should come first
|
||||
* If both have defined grid positions but different rows (it doesn't really matter in this case) the lowers row should come first
|
||||
*/
|
||||
function configSorter(a, b): number {
|
||||
if ((!a.gridItemConfig || !a.gridItemConfig.col)
|
||||
&& (!b.gridItemConfig || !b.gridItemConfig.col)) {
|
||||
return 0;
|
||||
} else if (!a.gridItemConfig || !a.gridItemConfig.col) {
|
||||
return 1;
|
||||
} else if (!b.gridItemConfig || !b.gridItemConfig.col) {
|
||||
return -1;
|
||||
} else if (a.gridItemConfig.row === b.gridItemConfig.row) {
|
||||
if (a.gridItemConfig.col < b.gridItemConfig.col) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.gridItemConfig.col === b.gridItemConfig.col) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a.gridItemConfig.col > b.gridItemConfig.col) {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
if (a.gridItemConfig.row < b.gridItemConfig.row) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.gridItemConfig.row === b.gridItemConfig.row) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a.gridItemConfig.row > b.gridItemConfig.row) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return void 0; // this should never be reached
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-page',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html')),
|
||||
host: {
|
||||
class: 'dashboard-page'
|
||||
}
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html'))
|
||||
})
|
||||
export abstract class DashboardPage {
|
||||
export abstract class DashboardPage extends Disposable implements OnDestroy {
|
||||
|
||||
protected SKELETON_WIDTH = 5;
|
||||
protected widgets: Array<WidgetConfig> = [];
|
||||
@@ -60,55 +113,124 @@ export abstract class DashboardPage {
|
||||
'prefer_new': false, // When adding new items, will use that items position ahead of existing items
|
||||
'limit_to_screen': true, // When resizing the screen, with this true and auto_resize false, the grid will re-arrange to fit the screen size. Please note, at present this only works with cascade direction up.
|
||||
};
|
||||
private _themeDispose: IDisposable;
|
||||
private _originalConfig: WidgetConfig[];
|
||||
private _editDispose: Array<IDisposable> = [];
|
||||
private _scrollableElement: ScrollableElement;
|
||||
|
||||
private _widgetConfigLocation: string;
|
||||
private _propertiesConfigLocation: string;
|
||||
|
||||
@ViewChild('propertyContainer', { read: ElementRef }) private propertyContainer: ElementRef;
|
||||
@ViewChild('properties') private _properties: DashboardWidgetWrapper;
|
||||
@ViewChild(NgGrid) private _grid: NgGrid;
|
||||
@ViewChild('scrollable', { read: ElementRef }) private _scrollable: ElementRef;
|
||||
@ViewChild('scrollContainer', { read: ElementRef }) private _scrollContainer: ElementRef;
|
||||
@ViewChild('propertiesContainer', { read: ElementRef }) private _propertiesContainer: ElementRef;
|
||||
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
|
||||
@ViewChildren(NgGridItem) private _items: QueryList<NgGridItem>;
|
||||
|
||||
// a set of config modifiers
|
||||
private readonly _configModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
|
||||
this.removeEmpty,
|
||||
this.initExtensionConfigs,
|
||||
this.validateGridConfig,
|
||||
this.addProvider,
|
||||
this.addEdition,
|
||||
this.addContext,
|
||||
this.filterWidgets
|
||||
];
|
||||
|
||||
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
|
||||
this.validateGridConfig
|
||||
];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface
|
||||
) { }
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
if (!this.dashboardService.connectionManagementService.connectionInfo) {
|
||||
this.dashboardService.messageService.show(Severity.Warning, nls.localize('missingConnectionInfo', 'No connection information could be found for this dashboard'));
|
||||
} else {
|
||||
let tempWidgets = this.dashboardService.getSettings(this.context).widgets;
|
||||
let tempWidgets = this.dashboardService.getSettings<Array<WidgetConfig>>([this.context, 'widgets'].join('.'));
|
||||
this._widgetConfigLocation = 'default';
|
||||
this._originalConfig = objects.clone(tempWidgets);
|
||||
let properties = this.getProperties();
|
||||
this._configModifiers.forEach((cb) => {
|
||||
tempWidgets = cb.apply(this, [tempWidgets]);
|
||||
properties = properties ? cb.apply(this, [properties]) : undefined;
|
||||
});
|
||||
this._gridModifiers.forEach(cb => {
|
||||
tempWidgets = cb.apply(this, [tempWidgets]);
|
||||
});
|
||||
this.widgets = tempWidgets;
|
||||
this.propertiesWidget = properties ? properties[0] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected baseInit(): void {
|
||||
let self = this;
|
||||
self._themeDispose = self.dashboardService.themeService.onDidColorThemeChange((event: IColorTheme) => {
|
||||
self.updateTheme(event);
|
||||
ngAfterViewInit(): void {
|
||||
this._register(this.dashboardService.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.dashboardService.themeService.getColorTheme());
|
||||
let container = this._scrollContainer.nativeElement as HTMLElement;
|
||||
let scrollable = this._scrollable.nativeElement as HTMLElement;
|
||||
container.removeChild(scrollable);
|
||||
this._scrollableElement = new ScrollableElement(scrollable, {
|
||||
horizontal: ScrollbarVisibility.Hidden,
|
||||
vertical: ScrollbarVisibility.Auto,
|
||||
useShadows: false
|
||||
});
|
||||
self.updateTheme(self.dashboardService.themeService.getColorTheme());
|
||||
|
||||
this._scrollableElement.onScroll(e => {
|
||||
scrollable.style.bottom = e.scrollTop + 'px';
|
||||
});
|
||||
|
||||
container.appendChild(this._scrollableElement.getDomNode());
|
||||
let initalHeight = getContentHeight(scrollable);
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(scrollable),
|
||||
height: getContentHeight(container)
|
||||
});
|
||||
|
||||
this._register(addDisposableListener(window, EventType.RESIZE, () => {
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(scrollable),
|
||||
height: getContentHeight(container)
|
||||
});
|
||||
}));
|
||||
|
||||
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
|
||||
setTimeout(() => {
|
||||
let currentheight = getContentHeight(scrollable);
|
||||
if (initalHeight !== currentheight) {
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(scrollable),
|
||||
height: getContentHeight(container)
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let el = this._propertiesContainer.nativeElement as HTMLElement;
|
||||
let border = theme.getColor(colors.contrastBorder, true);
|
||||
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
|
||||
|
||||
if (border) {
|
||||
el.style.borderColor = border.toString();
|
||||
el.style.borderBottomWidth = '1px';
|
||||
el.style.borderBottomStyle = 'solid';
|
||||
} else if (borderColor) {
|
||||
el.style.borderBottom = '1px solid ' + borderColor.toString();
|
||||
} else {
|
||||
el.style.border = 'none';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected baseDestroy(): void {
|
||||
if (this._themeDispose) {
|
||||
this._themeDispose.dispose();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
protected abstract propertiesWidget: WidgetConfig;
|
||||
@@ -215,10 +337,14 @@ export abstract class DashboardPage {
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
protected validateGridConfig(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.map((widget) => {
|
||||
return config.map((widget, index) => {
|
||||
if (widget.gridItemConfig === undefined) {
|
||||
widget.gridItemConfig = {};
|
||||
}
|
||||
const id = generateUuid();
|
||||
widget.gridItemConfig.payload = { id };
|
||||
widget.id = id;
|
||||
this._originalConfig[index].id = id;
|
||||
return widget;
|
||||
});
|
||||
}
|
||||
@@ -246,6 +372,12 @@ export abstract class DashboardPage {
|
||||
sizey: insightConfig.gridItemConfig.y
|
||||
};
|
||||
}
|
||||
if (config.gridItemConfig && !config.gridItemConfig.sizex && insightConfig.gridItemConfig && insightConfig.gridItemConfig.x) {
|
||||
config.gridItemConfig.sizex = insightConfig.gridItemConfig.x;
|
||||
}
|
||||
if (config.gridItemConfig && !config.gridItemConfig.sizey && insightConfig.gridItemConfig && insightConfig.gridItemConfig.y) {
|
||||
config.gridItemConfig.sizey = insightConfig.gridItemConfig.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
@@ -253,7 +385,8 @@ export abstract class DashboardPage {
|
||||
}
|
||||
|
||||
private getProperties(): Array<WidgetConfig> {
|
||||
let properties = this.dashboardService.getSettings(this.context).properties;
|
||||
let properties = this.dashboardService.getSettings<IPropertiesConfig[]>([this.context, 'properties'].join('.'));
|
||||
this._propertiesConfigLocation = 'default';
|
||||
if (types.isUndefinedOrNull(properties)) {
|
||||
return [this.propertiesWidget];
|
||||
} else if (types.isBoolean(properties)) {
|
||||
@@ -271,18 +404,6 @@ export abstract class DashboardPage {
|
||||
}
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let propsEl: HTMLElement = this.propertyContainer.nativeElement;
|
||||
let widgetShadowColor = theme.getColor(colors.widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
// Box shadow on bottom only.
|
||||
// The below settings fill the shadow across the whole page
|
||||
propsEl.style.boxShadow = `-5px 5px 10px -5px ${widgetShadowColor}`;
|
||||
propsEl.style.marginRight = '-10px';
|
||||
propsEl.style.marginBottom = '5px';
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(refreshConfig: boolean = false): void {
|
||||
if (refreshConfig) {
|
||||
this.init();
|
||||
@@ -297,4 +418,85 @@ export abstract class DashboardPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
if (this._grid.dragEnable) {
|
||||
this._grid.disableDrag();
|
||||
this._grid.disableResize();
|
||||
this._editDispose.forEach(i => i.dispose());
|
||||
this._widgets.forEach(i => {
|
||||
if (i.id) {
|
||||
i.disableEdit();
|
||||
}
|
||||
});
|
||||
this._editDispose = [];
|
||||
} else {
|
||||
this._grid.enableResize();
|
||||
this._grid.enableDrag();
|
||||
this._editDispose.push(this.dashboardService.onDeleteWidget(e => {
|
||||
let index = this.widgets.findIndex(i => i.id === e);
|
||||
this.widgets.splice(index, 1);
|
||||
|
||||
index = this._originalConfig.findIndex(i => i.id === e);
|
||||
this._originalConfig.splice(index, 1);
|
||||
|
||||
this._rewriteConfig();
|
||||
this._cd.detectChanges();
|
||||
}));
|
||||
this._editDispose.push(subscriptionToDisposable(this._grid.onResizeStop.subscribe((e: NgGridItem) => {
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(this._scrollable.nativeElement),
|
||||
height: getContentHeight(this._scrollContainer.nativeElement)
|
||||
});
|
||||
let event = e.getEventOutput();
|
||||
let config = this._originalConfig.find(i => i.id === event.payload.id);
|
||||
|
||||
if (!config.gridItemConfig) {
|
||||
config.gridItemConfig = {};
|
||||
}
|
||||
config.gridItemConfig.sizex = e.sizex;
|
||||
config.gridItemConfig.sizey = e.sizey;
|
||||
|
||||
let component = this._widgets.find(i => i.id === event.payload.id);
|
||||
|
||||
component.layout();
|
||||
this._rewriteConfig();
|
||||
})));
|
||||
this._editDispose.push(subscriptionToDisposable(this._grid.onDragStop.subscribe((e: NgGridItem) => {
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(this._scrollable.nativeElement),
|
||||
height: getContentHeight(this._scrollContainer.nativeElement)
|
||||
});
|
||||
let event = e.getEventOutput();
|
||||
this._items.forEach(i => {
|
||||
let config = this._originalConfig.find(j => j.id === i.getEventOutput().payload.id);
|
||||
if ((config.gridItemConfig && config.gridItemConfig.col) || config.id === event.payload.id) {
|
||||
if (!config.gridItemConfig) {
|
||||
config.gridItemConfig = {};
|
||||
}
|
||||
config.gridItemConfig.col = i.col;
|
||||
config.gridItemConfig.row = i.row;
|
||||
}
|
||||
});
|
||||
this._originalConfig.sort(configSorter);
|
||||
|
||||
this._rewriteConfig();
|
||||
})));
|
||||
this._widgets.forEach(i => {
|
||||
if (i.id) {
|
||||
i.enableEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _rewriteConfig(): void {
|
||||
let writeableConfig = objects.clone(this._originalConfig);
|
||||
|
||||
writeableConfig.forEach(i => {
|
||||
delete i.id;
|
||||
});
|
||||
let target: ConfigurationTarget = ConfigurationTarget.USER;
|
||||
this.dashboardService.writeSettings(this.context, writeableConfig, target);
|
||||
}
|
||||
}
|
||||
|
||||
10
src/sql/parts/dashboard/common/dashboardPage.css
Normal file
10
src/sql/parts/dashboard/common/dashboardPage.css
Normal file
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-page {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -2,19 +2,22 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { InjectionToken, OnDestroy } from '@angular/core';
|
||||
import { NgGridItemConfig } from 'angular2-grid';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface IDashboardWidget {
|
||||
actions: Array<Action>;
|
||||
actionsContext?: any;
|
||||
refresh?: () => void;
|
||||
layout?: () => void;
|
||||
}
|
||||
|
||||
export const WIDGET_CONFIG = new InjectionToken<WidgetConfig>('widget_config');
|
||||
|
||||
export interface WidgetConfig {
|
||||
id?: string; // used to track the widget lifespan operations
|
||||
name?: string;
|
||||
icon?: string;
|
||||
context: string;
|
||||
@@ -26,13 +29,17 @@ export interface WidgetConfig {
|
||||
border?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: string;
|
||||
padding?:string;
|
||||
padding?: string;
|
||||
}
|
||||
|
||||
export abstract class DashboardWidget {
|
||||
export abstract class DashboardWidget extends Disposable implements OnDestroy {
|
||||
protected _config: WidgetConfig;
|
||||
|
||||
get actions(): Array<Action> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<div style="display: flex; flex-flow: column; overflow: hidden; height: 100%; width: 100%">
|
||||
|
||||
<div #header>
|
||||
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex;flex: 0 0; padding: 3px 0 3px 0">
|
||||
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
|
||||
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex; flex: 0 0; padding: 3px 0 3px 0; flex-direction: row-reverse; justify-content: space-between">
|
||||
<span #actionbar style="flex: 0 0 auto; align-self: end"></span>
|
||||
<span *ngIf="_config.name" style="margin-left: 5px">{{_config.name}}</span>
|
||||
<div *ngIf="_actions" (click)="onActionsClick($event)" style="float: right; margin-right: 5px; margin-left: auto; padding: 10px" class="icon toggle-more"></div>
|
||||
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template component-host>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 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!./dashboardWidgetWrapper';
|
||||
|
||||
import {
|
||||
Component, Input, Inject, forwardRef, ComponentFactoryResolver, AfterContentInit, ViewChild,
|
||||
@@ -13,7 +14,7 @@ import { ComponentHostDirective } from './componentHost.directive';
|
||||
import { WidgetConfig, WIDGET_CONFIG, IDashboardWidget } from './dashboardWidget';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import * as ACTIONS from './actions';
|
||||
import { RefreshWidgetAction, ToggleMoreWidgetAction, DeleteWidgetAction } from './actions';
|
||||
|
||||
/* Widgets */
|
||||
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
@@ -28,8 +29,8 @@ import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeS
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
const componentMap: { [x: string]: Type<IDashboardWidget> } = {
|
||||
'properties-widget': PropertiesWidgetComponent,
|
||||
@@ -47,8 +48,10 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
|
||||
private _themeDispose: IDisposable;
|
||||
private _actions: Array<Action>;
|
||||
private _component: IDashboardWidget;
|
||||
private _actionbar: ActionBar;
|
||||
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
|
||||
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
|
||||
|
||||
constructor(
|
||||
@@ -69,6 +72,11 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
|
||||
ngAfterContentInit() {
|
||||
this.updateTheme(this._bootstrap.themeService.getColorTheme());
|
||||
this.loadWidget();
|
||||
this._changeref.detectChanges();
|
||||
this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
|
||||
if (this._actions) {
|
||||
this._actionbar.push(this._bootstrap.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, this._component.actionsContext), { icon: true, label: false });
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -81,6 +89,24 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
|
||||
}
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (this._component && this._component.layout) {
|
||||
this._component.layout();
|
||||
}
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._config.id;
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
this._actionbar.push(this._bootstrap.instantiationService.createInstance(DeleteWidgetAction, this._config.id, this._bootstrap.getUnderlyingUri()), { icon: true, label: false });
|
||||
}
|
||||
|
||||
public disableEdit(): void {
|
||||
this._actionbar.pull(this._actionbar.length() - 1);
|
||||
}
|
||||
|
||||
private loadWidget(): void {
|
||||
if (Object.keys(this._config.widget).length !== 1) {
|
||||
error('Exactly 1 widget must be defined per space');
|
||||
@@ -105,7 +131,7 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
|
||||
this._component = componentRef.instance;
|
||||
let actions = componentRef.instance.actions;
|
||||
if (componentRef.instance.refresh) {
|
||||
actions.push(this._bootstrap.instantiationService.createInstance(ACTIONS.RefreshWidgetAction, ACTIONS.RefreshWidgetAction.ID, ACTIONS.RefreshWidgetAction.LABEL, componentRef.instance.refresh));
|
||||
actions.push(new RefreshWidgetAction(componentRef.instance.refresh, componentRef.instance));
|
||||
}
|
||||
if (actions !== undefined && actions.length > 0) {
|
||||
this._actions = actions;
|
||||
@@ -148,23 +174,12 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
|
||||
return selector;
|
||||
}
|
||||
|
||||
//tslint:disable-next-line
|
||||
private onActionsClick(e: any) {
|
||||
let anchor = { x: e.pageX + 1, y: e.pageY };
|
||||
this._bootstrap.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => TPromise.as(this._actions),
|
||||
getActionsContext: () => this._component.actionsContext
|
||||
});
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let el = <HTMLElement>this._ref.nativeElement;
|
||||
let headerEl: HTMLElement = this.header.nativeElement;
|
||||
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
|
||||
let backgroundColor = theme.getColor(colors.editorBackground, true);
|
||||
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
|
||||
// TODO: highContrastBorder does not exist, how to handle?
|
||||
let border = theme.getColor(colors.contrastBorder, true);
|
||||
|
||||
if (this._config.background_color) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-widget-wrapper .action-label {
|
||||
padding: 7px;
|
||||
}
|
||||
@@ -4,9 +4,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: scroll; width: 100%; height: 100%">
|
||||
<div #header style="margin-bottom: 5px">
|
||||
<breadcrumb></breadcrumb>
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div #header class="header" style="margin-bottom: 5px; flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center">
|
||||
<div style="flex: 1 1 auto">
|
||||
<breadcrumb></breadcrumb>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto" #actionBar>
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<div style="flex: 1 1 auto; position: relative">
|
||||
<router-outlet (activate)="onActivate($event)"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboard';
|
||||
|
||||
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { DashboardServiceInterface } from './services/dashboardServiceInterface.service';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
import { RefreshWidgetAction, EditDashboardAction } from 'sql/parts/dashboard/common/actions';
|
||||
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
export const DASHBOARD_SELECTOR: string = 'dashboard-component';
|
||||
|
||||
@@ -22,7 +27,11 @@ export const DASHBOARD_SELECTOR: string = 'dashboard-component';
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
private _subs: Array<IDisposable> = new Array();
|
||||
private _currentPage: DashboardPage;
|
||||
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
@ViewChild('actionBar', { read: ElementRef }) private actionbarContainer: ElementRef;
|
||||
private actionbar: ActionBar;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrapService: DashboardServiceInterface,
|
||||
@@ -31,13 +40,21 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
let self = this;
|
||||
self._subs.push(self._bootstrapService.themeService.onDidColorThemeChange(e => self.updateTheme(e)));
|
||||
self.updateTheme(self._bootstrapService.themeService.getColorTheme());
|
||||
let profile: IConnectionProfile = self._bootstrapService.getOriginalConnectionProfile();
|
||||
this._subs.push(this._bootstrapService.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
|
||||
let profile: IConnectionProfile = this._bootstrapService.getOriginalConnectionProfile();
|
||||
this.actionbar = new ActionBar(this.actionbarContainer.nativeElement);
|
||||
this.actionbar.push(new RefreshWidgetAction(this.refresh, this), {
|
||||
icon: true,
|
||||
label: false,
|
||||
});
|
||||
this.actionbar.push(new EditDashboardAction(this.edit, this), {
|
||||
icon: true,
|
||||
label: false,
|
||||
});
|
||||
if (profile && (!profile.databaseName || Utils.isMaster(profile))) {
|
||||
// Route to the server page as this is the default database
|
||||
self._router.navigate(['server-dashboard']);
|
||||
this._router.navigate(['server-dashboard']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +65,23 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let headerEl = <HTMLElement> this.header.nativeElement;
|
||||
let headerEl = <HTMLElement>this.header.nativeElement;
|
||||
headerEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
headerEl.style.borderBottomWidth = '1px';
|
||||
headerEl.style.borderBottomStyle = 'solid';
|
||||
|
||||
}
|
||||
|
||||
onActivate(page: DashboardPage) {
|
||||
this._currentPage = page;
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
if (this._currentPage) {
|
||||
this._currentPage.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
this._currentPage.enableEdit();
|
||||
}
|
||||
}
|
||||
|
||||
12
src/sql/parts/dashboard/dashboard.css
Normal file
12
src/sql/parts/dashboard/dashboard.css
Normal file
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.dashboardEditor .header .monaco-action-bar .action-label {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dashboardEditor .header .monaco-action-bar .action-item {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
|
||||
|
||||
import { DashboardInput } from './dashboardInput';
|
||||
import { DashboardModule } from './dashboard.module';
|
||||
@@ -66,7 +65,7 @@ export class DashboardEditor extends BaseEditor {
|
||||
|
||||
super.setInput(input, options);
|
||||
|
||||
$(parentElement).empty();
|
||||
$(parentElement).clearChildren();
|
||||
|
||||
if (!input.hasBootstrapped) {
|
||||
let container = DOM.$<HTMLElement>('.dashboardEditor');
|
||||
@@ -105,8 +104,3 @@ export class DashboardEditor extends BaseEditor {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
extensions: ['.dashboard'],
|
||||
id: 'dashboard',
|
||||
});
|
||||
@@ -76,7 +76,7 @@ export class DashboardInput extends EditorInput {
|
||||
public getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'dashboard',
|
||||
path: '.dashboard'
|
||||
path: 'dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Inject, forwardRef, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
|
||||
@@ -12,10 +12,9 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
|
||||
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDestroy {
|
||||
export class DatabaseDashboardPage extends DashboardPage implements OnInit {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('databasePageName', 'DATABASE DASHBOARD'),
|
||||
widget: {
|
||||
@@ -32,15 +31,15 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDe
|
||||
};
|
||||
|
||||
protected readonly context = 'database';
|
||||
private _dispose: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private _breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef
|
||||
) {
|
||||
super(dashboardService);
|
||||
this._dispose.push(dashboardService.onUpdatePage(() => {
|
||||
super(dashboardService, el, _cd);
|
||||
this._register(dashboardService.onUpdatePage(() => {
|
||||
this.refresh(true);
|
||||
this._cd.detectChanges();
|
||||
}));
|
||||
@@ -49,11 +48,5 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDe
|
||||
|
||||
ngOnInit() {
|
||||
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
|
||||
this.baseInit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._dispose = dispose(this._dispose);
|
||||
this.baseDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
|
||||
},
|
||||
sizey: {
|
||||
type: 'number'
|
||||
},
|
||||
col: {
|
||||
type: 'number'
|
||||
},
|
||||
row: {
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -155,4 +161,4 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
|
||||
};
|
||||
|
||||
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
|
||||
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
|
||||
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Inject, forwardRef, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
|
||||
@@ -14,7 +14,7 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export class ServerDashboardPage extends DashboardPage implements OnInit, OnDestroy {
|
||||
export class ServerDashboardPage extends DashboardPage implements OnInit {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('serverPageName', 'SERVER DASHBOARD'),
|
||||
widget: {
|
||||
@@ -35,9 +35,10 @@ export class ServerDashboardPage extends DashboardPage implements OnInit, OnDest
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef
|
||||
) {
|
||||
super(dashboardService);
|
||||
super(dashboardService, el, cd);
|
||||
// revert back to default database
|
||||
this.dashboardService.connectionManagementService.changeDatabase('master').then(() => {
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = undefined;
|
||||
@@ -49,10 +50,5 @@ export class ServerDashboardPage extends DashboardPage implements OnInit, OnDest
|
||||
ngOnInit() {
|
||||
this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
|
||||
this.baseInit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.baseDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,10 @@ import { ConnectionManagementInfo } from 'sql/parts/connection/common/connection
|
||||
import { IAdminService } from 'sql/parts/admin/common/adminService';
|
||||
import { IQueryManagementService } from 'sql/parts/query/common/queryManagement';
|
||||
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
|
||||
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
|
||||
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
|
||||
import { AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
|
||||
|
||||
import { ProviderMetadata, DatabaseInfo, SimpleExecuteResult } from 'data';
|
||||
|
||||
@@ -31,7 +29,8 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationEditingService, IConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService'
|
||||
import { IMessageService } from 'vs/platform/message/common/message';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
@@ -127,10 +126,14 @@ export class DashboardServiceInterface implements OnDestroy {
|
||||
private _workspaceContextService: IWorkspaceContextService;
|
||||
private _storageService: IStorageService;
|
||||
private _capabilitiesService: ICapabilitiesService;
|
||||
private _configurationEditingService: ConfigurationEditingService;
|
||||
|
||||
private _updatePage = new Emitter<void>();
|
||||
public readonly onUpdatePage: Event<void> = this._updatePage.event;
|
||||
|
||||
private _onDeleteWidget = new Emitter<string>();
|
||||
public readonly onDeleteWidget: Event<string> = this._onDeleteWidget.event;
|
||||
|
||||
constructor(
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@@ -145,6 +148,7 @@ export class DashboardServiceInterface implements OnDestroy {
|
||||
this._workspaceContextService = this._bootstrapService.workspaceContextService;
|
||||
this._storageService = this._bootstrapService.storageService;
|
||||
this._capabilitiesService = this._bootstrapService.capabilitiesService;
|
||||
this._configurationEditingService = this._bootstrapService.configurationEditorService;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -155,6 +159,10 @@ export class DashboardServiceInterface implements OnDestroy {
|
||||
return this._messageService;
|
||||
}
|
||||
|
||||
public get configurationEditingService(): ConfigurationEditingService {
|
||||
return this._configurationEditingService;
|
||||
}
|
||||
|
||||
public get metadataService(): SingleConnectionMetadataService {
|
||||
return this._metadataService;
|
||||
}
|
||||
@@ -195,7 +203,7 @@ export class DashboardServiceInterface implements OnDestroy {
|
||||
return this._storageService;
|
||||
}
|
||||
|
||||
public get CapabilitiesService(): ICapabilitiesService {
|
||||
public get capabilitiesService(): ICapabilitiesService {
|
||||
return this._capabilitiesService;
|
||||
}
|
||||
|
||||
@@ -241,13 +249,17 @@ export class DashboardServiceInterface implements OnDestroy {
|
||||
* Get settings for given string
|
||||
* @param type string of setting to get from dashboard settings; i.e dashboard.{type}
|
||||
*/
|
||||
public getSettings(type: string): { widgets: Array<WidgetConfig>, properties: boolean | IPropertiesConfig[] } {
|
||||
let config = this._configService.getConfiguration(DASHBOARD_SETTINGS);
|
||||
return config[type];
|
||||
public getSettings<T>(type: string): T {
|
||||
let config = this._configService.getValue<T>([DASHBOARD_SETTINGS, type].join('.'));
|
||||
return config;
|
||||
}
|
||||
|
||||
private handleDashboardEvent(event: AngularEventType): void {
|
||||
switch (event) {
|
||||
public writeSettings(key: string, value: any, target: ConfigurationTarget) {
|
||||
this._configurationEditingService.writeConfiguration(target, { key: DASHBOARD_SETTINGS + '.' + key + '.widgets', value });
|
||||
}
|
||||
|
||||
private handleDashboardEvent(event: IAngularEvent): void {
|
||||
switch (event.event) {
|
||||
case AngularEventType.NAV_DATABASE:
|
||||
this.connectionManagementService.changeDatabase(this.connectionManagementService.connectionInfo.connectionProfile.databaseName).then(
|
||||
result => {
|
||||
@@ -269,6 +281,8 @@ export class DashboardServiceInterface implements OnDestroy {
|
||||
case AngularEventType.NAV_SERVER:
|
||||
this._router.navigate(['server-dashboard']);
|
||||
break;
|
||||
case AngularEventType.DELETE_WIDGET:
|
||||
this._onDeleteWidget.fire(event.payload.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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