mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-20 09:35:38 -05:00
Code Layering dashboard (#4883)
* move dashboard to workbench * revert xlf file changes * 💄 * 💄 * add back removed functions
This commit is contained in:
@@ -7,7 +7,7 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IInsightsConfig } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
|
||||
import { IRestoreDialogController } from 'sql/platform/restore/common/restoreService';
|
||||
import { IAngularEventingService, AngularEventType } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
|
||||
@@ -17,12 +17,12 @@ import { IScriptingService } from 'sql/platform/scripting/common/scriptingServic
|
||||
import { EditDataInput } from 'sql/parts/editData/common/editDataInput';
|
||||
import { IAdminService } from 'sql/workbench/services/admin/common/adminService';
|
||||
import { IRestoreDialogController } from 'sql/platform/restore/common/restoreService';
|
||||
import { IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IInsightsConfig } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IInsightsDialogService } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { DashboardInput } from 'sql/parts/dashboard/dashboardInput';
|
||||
import { DashboardInput } from 'sql/workbench/parts/dashboard/dashboardInput';
|
||||
import { ProfilerInput } from 'sql/parts/profiler/editor/profilerInput';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
|
||||
|
||||
244
src/sql/workbench/parts/dashboard/common/actions.ts
Normal file
244
src/sql/workbench/parts/dashboard/common/actions.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
import { IAngularEventingService, AngularEventType, IAngularEvent } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
import { INewDashboardTabDialogService } from 'sql/workbench/services/dashboard/common/newDashboardTabDialog';
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
|
||||
|
||||
export class EditDashboardAction extends Action {
|
||||
|
||||
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(
|
||||
private editFn: () => void,
|
||||
private context: any //this
|
||||
) {
|
||||
super(EditDashboardAction.ID, EditDashboardAction.EDITLABEL, EditDashboardAction.ICON);
|
||||
}
|
||||
|
||||
run(): Promise<boolean> {
|
||||
try {
|
||||
this.editFn.apply(this.context);
|
||||
this.toggleLabel();
|
||||
return Promise.resolve(true);
|
||||
} catch (e) {
|
||||
return Promise.resolve(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(): Promise<boolean> {
|
||||
try {
|
||||
this.refreshFn.apply(this.context);
|
||||
return Promise.resolve(true);
|
||||
} catch (e) {
|
||||
return Promise.resolve(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): Promise<boolean> {
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => context.target,
|
||||
getActions: () => this._actions,
|
||||
getActionsContext: () => this._context
|
||||
});
|
||||
return Promise.resolve(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(): Promise<boolean> {
|
||||
this.angularEventService.sendAngularEvent(this._uri, AngularEventType.DELETE_WIDGET, { id: this._widgetId });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class PinUnpinTabAction extends Action {
|
||||
private static readonly ID = 'pinTab';
|
||||
private static readonly PINLABEL = nls.localize('clickToUnpin', "Click to unpin");
|
||||
private static readonly UNPINLABEL = nls.localize('clickToPin', "Click to pin");
|
||||
private static readonly PINICON = 'pin';
|
||||
private static readonly UNPINICON = 'unpin';
|
||||
|
||||
constructor(
|
||||
private _tabId: string,
|
||||
private _uri: string,
|
||||
private _isPinned: boolean,
|
||||
@IAngularEventingService private angularEventService: IAngularEventingService
|
||||
) {
|
||||
super(PinUnpinTabAction.ID, PinUnpinTabAction.PINLABEL, PinUnpinTabAction.PINICON);
|
||||
this.updatePinStatus();
|
||||
}
|
||||
|
||||
private updatePinStatus() {
|
||||
if (this._isPinned) {
|
||||
this.label = PinUnpinTabAction.PINLABEL;
|
||||
this.class = PinUnpinTabAction.PINICON;
|
||||
} else {
|
||||
this.label = PinUnpinTabAction.UNPINLABEL;
|
||||
this.class = PinUnpinTabAction.UNPINICON;
|
||||
}
|
||||
}
|
||||
|
||||
public run(): Promise<boolean> {
|
||||
this._isPinned = !this._isPinned;
|
||||
this.updatePinStatus();
|
||||
this.angularEventService.sendAngularEvent(this._uri, AngularEventType.PINUNPIN_TAB, { tabId: this._tabId, isPinned: this._isPinned });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddFeatureTabAction extends Action {
|
||||
private static readonly ID = 'openInstalledFeatures';
|
||||
private static readonly LABEL = nls.localize('addFeatureAction.openInstalledFeatures', "Open installed features");
|
||||
private static readonly ICON = 'new';
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private _dashboardTabs: Array<IDashboardTab>,
|
||||
private _openedTabs: Array<IDashboardTab>,
|
||||
private _uri: string,
|
||||
@INewDashboardTabDialogService private _newDashboardTabService: INewDashboardTabDialogService,
|
||||
@IAngularEventingService private _angularEventService: IAngularEventingService
|
||||
) {
|
||||
super(AddFeatureTabAction.ID, AddFeatureTabAction.LABEL, AddFeatureTabAction.ICON);
|
||||
this._disposables.push(toDisposableSubscription(this._angularEventService.onAngularEvent(this._uri, (event) => this.handleDashboardEvent(event))));
|
||||
}
|
||||
|
||||
run(): Promise<boolean> {
|
||||
this._newDashboardTabService.showDialog(this._dashboardTabs, this._openedTabs, this._uri);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._disposables.forEach((item) => item.dispose());
|
||||
}
|
||||
|
||||
private handleDashboardEvent(event: IAngularEvent): void {
|
||||
switch (event.event) {
|
||||
case AngularEventType.NEW_TABS:
|
||||
const openedTabs = <IDashboardTab[]>event.payload.dashboardTabs;
|
||||
openedTabs.forEach(tab => {
|
||||
const existedTab = this._openedTabs.find(i => i === tab);
|
||||
if (!existedTab) {
|
||||
this._openedTabs.push(tab);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case AngularEventType.CLOSE_TAB:
|
||||
const index = this._openedTabs.findIndex(i => i.id === event.payload.id);
|
||||
this._openedTabs.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CollapseWidgetAction extends Action {
|
||||
private static readonly ID = 'collapseWidget';
|
||||
private static readonly COLLPASE_LABEL = nls.localize('collapseWidget', "Collapse");
|
||||
private static readonly EXPAND_LABEL = nls.localize('expandWidget', "Expand");
|
||||
private static readonly COLLAPSE_ICON = 'maximize-panel-action';
|
||||
private static readonly EXPAND_ICON = 'minimize-panel-action';
|
||||
|
||||
constructor(
|
||||
private _uri: string,
|
||||
private _widgetUuid: string,
|
||||
private collpasedState: boolean,
|
||||
@IAngularEventingService private _angularEventService: IAngularEventingService
|
||||
) {
|
||||
super(
|
||||
CollapseWidgetAction.ID,
|
||||
collpasedState ? CollapseWidgetAction.EXPAND_LABEL : CollapseWidgetAction.COLLPASE_LABEL,
|
||||
collpasedState ? CollapseWidgetAction.EXPAND_ICON : CollapseWidgetAction.COLLAPSE_ICON
|
||||
);
|
||||
}
|
||||
|
||||
run(): Promise<boolean> {
|
||||
this._toggleState();
|
||||
this._angularEventService.sendAngularEvent(this._uri, AngularEventType.COLLAPSE_WIDGET, this._widgetUuid);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
private _toggleState(): void {
|
||||
this._updateState(!this.collpasedState);
|
||||
}
|
||||
|
||||
private _updateState(collapsed: boolean): void {
|
||||
if (collapsed === this.collpasedState) {
|
||||
return;
|
||||
}
|
||||
this.collpasedState = collapsed;
|
||||
this._setClass(this.collpasedState ? CollapseWidgetAction.EXPAND_ICON : CollapseWidgetAction.COLLAPSE_ICON);
|
||||
this._setLabel(this.collpasedState ? CollapseWidgetAction.EXPAND_LABEL : CollapseWidgetAction.COLLPASE_LABEL);
|
||||
}
|
||||
|
||||
public set state(collapsed: boolean) {
|
||||
this._updateState(collapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Directive, ViewContainerRef, Inject, forwardRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[component-host]',
|
||||
})
|
||||
export class ComponentHostDirective {
|
||||
constructor( @Inject(forwardRef(() => ViewContainerRef)) public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
||||
190
src/sql/workbench/parts/dashboard/common/dashboardHelper.ts
Normal file
190
src/sql/workbench/parts/dashboard/common/dashboardHelper.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { WIDGETS_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
|
||||
import { WEBVIEW_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWebviewContainer.contribution';
|
||||
import { MODELVIEW_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardModelViewContainer.contribution';
|
||||
import { CONTROLHOST_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardControlHostContainer.contribution';
|
||||
import { NAV_SECTION } from 'sql/workbench/parts/dashboard/containers/dashboardNavSection.contribution';
|
||||
import { IDashboardContainerRegistry, Extensions as DashboardContainerExtensions } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { SingleConnectionManagementService } from 'sql/services/common/commonServiceInterface.service';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
|
||||
const dashboardcontainerRegistry = Registry.as<IDashboardContainerRegistry>(DashboardContainerExtensions.dashboardContainerContributions);
|
||||
const containerTypes = [
|
||||
WIDGETS_CONTAINER,
|
||||
GRID_CONTAINER,
|
||||
WEBVIEW_CONTAINER,
|
||||
MODELVIEW_CONTAINER,
|
||||
CONTROLHOST_CONTAINER,
|
||||
NAV_SECTION
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates configs to make sure nothing will error out and returns the modified widgets
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
export function removeEmpty(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.filter(widget => {
|
||||
return !types.isUndefinedOrNull(widget);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates configs to make sure nothing will error out and returns the modified widgets
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
export function validateGridConfig(config: WidgetConfig[], originalConfig: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.map((widget, index) => {
|
||||
if (widget.gridItemConfig === undefined) {
|
||||
widget.gridItemConfig = {};
|
||||
}
|
||||
const id = generateUuid();
|
||||
widget.gridItemConfig.payload = { id };
|
||||
widget.id = id;
|
||||
if (originalConfig && originalConfig[index]) {
|
||||
originalConfig[index].id = id;
|
||||
}
|
||||
return widget;
|
||||
});
|
||||
}
|
||||
|
||||
export function initExtensionConfigs(configurations: WidgetConfig[]): Array<WidgetConfig> {
|
||||
const widgetRegistry = <IInsightRegistry>Registry.as(Extensions.InsightContribution);
|
||||
return configurations.map((config) => {
|
||||
if (config.widget && Object.keys(config.widget).length === 1) {
|
||||
const key = Object.keys(config.widget)[0];
|
||||
const insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
|
||||
if (insightConfig !== undefined) {
|
||||
// Setup the default properties for this extension if needed
|
||||
if (!config.when && insightConfig.when) {
|
||||
config.when = insightConfig.when;
|
||||
}
|
||||
if (!config.gridItemConfig && insightConfig.gridItemConfig) {
|
||||
config.gridItemConfig = {
|
||||
sizex: insightConfig.gridItemConfig.x,
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provider to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add provider onto
|
||||
*/
|
||||
export function addProvider<T extends { connectionManagementService: SingleConnectionManagementService }>(config: WidgetConfig[], collection: T): Array<WidgetConfig> {
|
||||
const provider = collection.connectionManagementService.connectionInfo.providerId;
|
||||
return config.map((item) => {
|
||||
if (item.provider === undefined) {
|
||||
item.provider = provider;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the edition to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add edition onto
|
||||
*/
|
||||
export function addEdition<T extends { connectionManagementService: SingleConnectionManagementService }>(config: WidgetConfig[], collection: DashboardServiceInterface): Array<WidgetConfig> {
|
||||
const connectionInfo: ConnectionManagementInfo = collection.connectionManagementService.connectionInfo;
|
||||
if (connectionInfo.serverInfo) {
|
||||
const edition = connectionInfo.serverInfo.engineEditionId;
|
||||
return config.map((item) => {
|
||||
if (item.edition === undefined) {
|
||||
item.edition = edition;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the context to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add context to
|
||||
*/
|
||||
export function addContext(config: WidgetConfig[], collection: any, context: string): Array<WidgetConfig> {
|
||||
return config.map((item) => {
|
||||
if (item.context === undefined) {
|
||||
item.context = context;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtered version of the widgets passed based on edition and provider
|
||||
* @param config widgets to filter
|
||||
*/
|
||||
export function filterConfigs<T extends { provider?: string | string[], when?: string }, K extends { contextKeyService: IContextKeyService }>(config: T[], collection: K): Array<T> {
|
||||
return config.filter((item) => {
|
||||
if (!hasCompatibleProvider(item.provider, collection.contextKeyService)) {
|
||||
return false;
|
||||
} else if (!item.when) {
|
||||
return true;
|
||||
} else {
|
||||
return collection.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(item.when));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the listed providers contain '*' indicating any provider will do, or that they are a match
|
||||
* for the currently scoped 'connectionProvider' context key.
|
||||
*/
|
||||
function hasCompatibleProvider(provider: string | string[], contextKeyService: IContextKeyService): boolean {
|
||||
let isCompatible = true;
|
||||
const connectionProvider = contextKeyService.getContextKeyValue<string>(Constants.connectionProviderContextKey);
|
||||
if (connectionProvider) {
|
||||
const providers = (provider instanceof Array) ? provider : [provider];
|
||||
const matchingProvider = providers.find((p) => p === connectionProvider || p === Constants.anyProviderName);
|
||||
isCompatible = (matchingProvider !== undefined);
|
||||
} // Else there's no connection context so skip the check
|
||||
return isCompatible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered container if it is specified as the key
|
||||
* @param container dashboard container
|
||||
*/
|
||||
export function getDashboardContainer(container: object): { result: boolean, message: string, container: object } {
|
||||
const key = Object.keys(container)[0];
|
||||
const containerTypeFound = containerTypes.find(c => (c === key));
|
||||
if (!containerTypeFound) {
|
||||
const dashboardContainer = dashboardcontainerRegistry.getRegisteredContainer(key);
|
||||
if (!dashboardContainer) {
|
||||
const errorMessage = nls.localize('unknownDashboardContainerError', '{0} is an unknown container.', key);
|
||||
error(errorMessage);
|
||||
return { result: false, message: errorMessage, container: undefined };
|
||||
} else {
|
||||
container = dashboardContainer.container;
|
||||
}
|
||||
}
|
||||
return { result: true, message: undefined, container: container };
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<panel class="dashboard-panel" (onTabChange)="handleTabChange($event)" (onTabClose)="handleTabClose($event)"
|
||||
[actions]="panelActions">
|
||||
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize"
|
||||
[identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions">
|
||||
<ng-template>
|
||||
<dashboard-home-container *ngIf="tab.id === 'homeTab'; else not_home" [properties]="propertiesWidget"
|
||||
[tab]="tab">
|
||||
</dashboard-home-container>
|
||||
<ng-template #not_home>
|
||||
<dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab">
|
||||
</dashboard-webview-container>
|
||||
<dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab">
|
||||
</dashboard-widget-container>
|
||||
<dashboard-modelview-container *ngIf="getContentType(tab) === 'modelview-container'" [tab]="tab">
|
||||
</dashboard-modelview-container>
|
||||
<dashboard-controlhost-container *ngIf="getContentType(tab) === 'controlhost-container'" [tab]="tab">
|
||||
</dashboard-controlhost-container>
|
||||
<dashboard-nav-section *ngIf="getContentType(tab) === 'nav-section'" [tab]="tab">
|
||||
</dashboard-nav-section>
|
||||
<dashboard-grid-container *ngIf="getContentType(tab) === 'grid-container'" [tab]="tab">
|
||||
</dashboard-grid-container>
|
||||
<dashboard-error-container *ngIf="getContentType(tab) === 'error-container'" [tab]="tab">
|
||||
</dashboard-error-container>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</tab>
|
||||
</panel>
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/workbench/parts/dashboard/common/dashboardPage';
|
||||
import 'sql/workbench/parts/dashboard/common/dashboardPanelStyles';
|
||||
|
||||
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface, SingleConnectionManagementService } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { WidgetConfig, TabConfig, TabSettingConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { IPropertiesConfig } from 'sql/workbench/parts/dashboard/pages/serverDashboardPage.contribution';
|
||||
import { PanelComponent } from 'sql/base/browser/ui/panel/panel.component';
|
||||
import { IDashboardRegistry, Extensions as DashboardExtensions, IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
import { PinUnpinTabAction, AddFeatureTabAction } from './actions';
|
||||
import { TabComponent, TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
import { AngularEventType, IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
import { DashboardTab, IConfigModifierCollection } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import * as dashboardHelper from 'sql/workbench/parts/dashboard/common/dashboardHelper';
|
||||
import { WIDGETS_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
const dashboardRegistry = Registry.as<IDashboardRegistry>(DashboardExtensions.DashboardContributions);
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-page',
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/common/dashboardPage.component.html'))
|
||||
})
|
||||
export abstract class DashboardPage extends AngularDisposable implements IConfigModifierCollection {
|
||||
|
||||
protected tabs: Array<TabConfig> = [];
|
||||
|
||||
private _originalConfig: WidgetConfig[];
|
||||
|
||||
private _widgetConfigLocation: string;
|
||||
private _propertiesConfigLocation: string;
|
||||
|
||||
protected panelActions: Action[];
|
||||
private _tabsDispose: Array<IDisposable> = [];
|
||||
private _tabSettingConfigs: Array<TabSettingConfig> = [];
|
||||
|
||||
@ViewChildren(TabChild) private _tabs: QueryList<DashboardTab>;
|
||||
@ViewChild(PanelComponent) private _panel: PanelComponent;
|
||||
|
||||
private _editEnabled = new Emitter<boolean>();
|
||||
public readonly editEnabled: Event<boolean> = this._editEnabled.event;
|
||||
|
||||
// tslint:disable:no-unused-variable
|
||||
private readonly homeTabTitle: string = nls.localize('home', 'Home');
|
||||
|
||||
// a set of config modifiers
|
||||
private readonly _configModifiers: Array<(item: Array<WidgetConfig>, collection: IConfigModifierCollection, context: string) => Array<WidgetConfig>> = [
|
||||
dashboardHelper.removeEmpty,
|
||||
dashboardHelper.initExtensionConfigs,
|
||||
dashboardHelper.addProvider,
|
||||
dashboardHelper.addEdition,
|
||||
dashboardHelper.addContext,
|
||||
dashboardHelper.filterConfigs
|
||||
];
|
||||
|
||||
public get connectionManagementService(): SingleConnectionManagementService {
|
||||
return this.dashboardService.connectionManagementService;
|
||||
}
|
||||
|
||||
public get contextKeyService(): IContextKeyService {
|
||||
return this.dashboardService.scopedContextKeyService;
|
||||
}
|
||||
|
||||
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>, originalConfig: Array<WidgetConfig>) => Array<WidgetConfig>> = [
|
||||
dashboardHelper.validateGridConfig
|
||||
];
|
||||
|
||||
protected abstract propertiesWidget: WidgetConfig;
|
||||
protected abstract get context(): string;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(INotificationService) private notificationService: INotificationService,
|
||||
@Inject(IAngularEventingService) private angularEventingService: IAngularEventingService,
|
||||
@Inject(IConfigurationService) private configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
this.dashboardService.dashboardContextKey.set(this.context);
|
||||
if (!this.dashboardService.connectionManagementService.connectionInfo) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('missingConnectionInfo', 'No connection information could be found for this dashboard')
|
||||
});
|
||||
} else {
|
||||
let tempWidgets = this.dashboardService.getSettings<Array<WidgetConfig>>([this.context, 'widgets'].join('.'));
|
||||
this._widgetConfigLocation = 'default';
|
||||
this._originalConfig = objects.deepClone(tempWidgets);
|
||||
let properties = this.getProperties();
|
||||
this._configModifiers.forEach((cb) => {
|
||||
tempWidgets = cb.apply(this, [tempWidgets, this, this.context]);
|
||||
properties = properties ? cb.apply(this, [properties, this, this.context]) : undefined;
|
||||
});
|
||||
this._gridModifiers.forEach(cb => {
|
||||
tempWidgets = cb.apply(this, [tempWidgets, this._originalConfig]);
|
||||
});
|
||||
this.propertiesWidget = properties ? properties[0] : undefined;
|
||||
|
||||
this.createTabs(tempWidgets);
|
||||
}
|
||||
}
|
||||
|
||||
private createTabs(homeWidgets: WidgetConfig[]) {
|
||||
// Clear all tabs
|
||||
this.tabs = [];
|
||||
this._tabSettingConfigs = [];
|
||||
this._tabsDispose.forEach(i => i.dispose());
|
||||
this._tabsDispose = [];
|
||||
|
||||
let allTabs = dashboardHelper.filterConfigs(dashboardRegistry.tabs, this);
|
||||
|
||||
// Before separating tabs into pinned / shown, ensure that the home tab is always set up as expected
|
||||
allTabs = this.setAndRemoveHomeTab(allTabs, homeWidgets);
|
||||
|
||||
// If preview features are disabled only show the home tab
|
||||
const extensionTabsEnabled = this.configurationService.getValue('workbench')['enablePreviewFeatures'];
|
||||
if (!extensionTabsEnabled) {
|
||||
allTabs = [];
|
||||
}
|
||||
|
||||
// Load tab setting configs
|
||||
this._tabSettingConfigs = this.dashboardService.getSettings<Array<TabSettingConfig>>([this.context, 'tabs'].join('.'));
|
||||
|
||||
const pinnedDashboardTabs: IDashboardTab[] = [];
|
||||
const alwaysShowTabs = allTabs.filter(tab => tab.alwaysShow);
|
||||
|
||||
this._tabSettingConfigs.forEach(config => {
|
||||
if (config.tabId && types.isBoolean(config.isPinned)) {
|
||||
const tab = allTabs.find(i => i.id === config.tabId);
|
||||
if (tab) {
|
||||
if (config.isPinned) {
|
||||
pinnedDashboardTabs.push(tab);
|
||||
} else {
|
||||
// overwrite always show if specify in user settings
|
||||
const index = alwaysShowTabs.findIndex(i => i.id === tab.id);
|
||||
alwaysShowTabs.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.loadNewTabs(pinnedDashboardTabs);
|
||||
this.loadNewTabs(alwaysShowTabs);
|
||||
|
||||
// Set panel actions
|
||||
const openedTabs = [...pinnedDashboardTabs, ...alwaysShowTabs];
|
||||
if (extensionTabsEnabled) {
|
||||
const addNewTabAction = this.instantiationService.createInstance(AddFeatureTabAction, allTabs, openedTabs, this.dashboardService.getUnderlyingUri());
|
||||
this._tabsDispose.push(addNewTabAction);
|
||||
this.panelActions = [addNewTabAction];
|
||||
} else {
|
||||
this.panelActions = [];
|
||||
}
|
||||
this._cd.detectChanges();
|
||||
|
||||
this._tabsDispose.push(this.dashboardService.onPinUnpinTab(e => {
|
||||
const tabConfig = this._tabSettingConfigs.find(i => i.tabId === e.tabId);
|
||||
if (tabConfig) {
|
||||
tabConfig.isPinned = e.isPinned;
|
||||
} else {
|
||||
this._tabSettingConfigs.push(e);
|
||||
}
|
||||
this.rewriteConfig();
|
||||
}));
|
||||
|
||||
this._tabsDispose.push(this.dashboardService.onAddNewTabs(e => {
|
||||
this.loadNewTabs(e, true);
|
||||
}));
|
||||
}
|
||||
|
||||
private setAndRemoveHomeTab(allTabs: IDashboardTab[], homeWidgets: WidgetConfig[]): IDashboardTab[] {
|
||||
const homeTabConfig: TabConfig = {
|
||||
id: 'homeTab',
|
||||
provider: Constants.anyProviderName,
|
||||
publisher: undefined,
|
||||
title: this.homeTabTitle,
|
||||
container: { 'widgets-container': homeWidgets },
|
||||
context: this.context,
|
||||
originalConfig: this._originalConfig,
|
||||
editable: true,
|
||||
canClose: false,
|
||||
actions: []
|
||||
};
|
||||
|
||||
const homeTabIndex = allTabs.findIndex((tab) => tab.isHomeTab === true);
|
||||
if (homeTabIndex !== undefined && homeTabIndex > -1) {
|
||||
// Have a tab: get its information and copy over to the home tab definition
|
||||
const homeTab = allTabs.splice(homeTabIndex, 1)[0];
|
||||
const tabConfig = this.initTabComponents(homeTab);
|
||||
homeTabConfig.id = tabConfig.id;
|
||||
homeTabConfig.container = tabConfig.container;
|
||||
}
|
||||
this.addNewTab(homeTabConfig);
|
||||
return allTabs;
|
||||
}
|
||||
|
||||
private rewriteConfig(): void {
|
||||
const writeableConfig = objects.deepClone(this._tabSettingConfigs);
|
||||
|
||||
const target: ConfigurationTarget = ConfigurationTarget.USER;
|
||||
this.dashboardService.writeSettings([this.context, 'tabs'].join('.'), writeableConfig, target);
|
||||
}
|
||||
|
||||
private loadNewTabs(dashboardTabs: IDashboardTab[], openLastTab: boolean = false) {
|
||||
if (dashboardTabs && dashboardTabs.length > 0) {
|
||||
const selectedTabs = dashboardTabs.map(v => this.initTabComponents(v)).map(v => {
|
||||
const actions = [];
|
||||
const tabSettingConfig = this._tabSettingConfigs.find(i => i.tabId === v.id);
|
||||
let isPinned = false;
|
||||
if (tabSettingConfig) {
|
||||
isPinned = tabSettingConfig.isPinned;
|
||||
} else if (v.alwaysShow) {
|
||||
isPinned = true;
|
||||
}
|
||||
actions.push(this.instantiationService.createInstance(PinUnpinTabAction, v.id, this.dashboardService.getUnderlyingUri(), isPinned));
|
||||
|
||||
const config = v as TabConfig;
|
||||
config.context = this.context;
|
||||
config.editable = false;
|
||||
config.canClose = true;
|
||||
config.actions = actions;
|
||||
this.addNewTab(config);
|
||||
return config;
|
||||
});
|
||||
|
||||
if (openLastTab) {
|
||||
// put this immediately on the stack so that is ran *after* the tab is rendered
|
||||
setTimeout(() => {
|
||||
const selectedLastTab = selectedTabs.pop();
|
||||
this._panel.selectTab(selectedLastTab.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initTabComponents(value: IDashboardTab): { id: string; title: string; container: object; alwaysShow: boolean; } {
|
||||
const containerResult = dashboardHelper.getDashboardContainer(value.container);
|
||||
if (!containerResult.result) {
|
||||
return { id: value.id, title: value.title, container: { 'error-container': undefined }, alwaysShow: value.alwaysShow };
|
||||
}
|
||||
const key = Object.keys(containerResult.container)[0];
|
||||
if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) {
|
||||
let configs = <WidgetConfig[]>Object.values(containerResult.container)[0];
|
||||
this._configModifiers.forEach(cb => {
|
||||
configs = cb.apply(this, [configs, this, this.context]);
|
||||
});
|
||||
this._gridModifiers.forEach(cb => {
|
||||
configs = cb.apply(this, [configs]);
|
||||
});
|
||||
if (key === WIDGETS_CONTAINER) {
|
||||
return { id: value.id, title: value.title, container: { 'widgets-container': configs }, alwaysShow: value.alwaysShow };
|
||||
}
|
||||
else {
|
||||
return { id: value.id, title: value.title, container: { 'grid-container': configs }, alwaysShow: value.alwaysShow };
|
||||
}
|
||||
}
|
||||
return { id: value.id, title: value.title, container: containerResult.container, alwaysShow: value.alwaysShow };
|
||||
}
|
||||
|
||||
protected getContentType(tab: TabConfig): string {
|
||||
return tab.container ? Object.keys(tab.container)[0] : '';
|
||||
}
|
||||
|
||||
private addNewTab(tab: TabConfig): void {
|
||||
const existedTab = this.tabs.find(i => i.id === tab.id);
|
||||
if (!existedTab) {
|
||||
this.tabs.push(tab);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private getProperties(): Array<WidgetConfig> {
|
||||
const properties = this.dashboardService.getSettings<IPropertiesConfig[] | string | boolean>([this.context, 'properties'].join('.'));
|
||||
this._propertiesConfigLocation = 'default';
|
||||
if (types.isUndefinedOrNull(properties)) {
|
||||
return [this.propertiesWidget];
|
||||
} else if (types.isBoolean(properties)) {
|
||||
return properties ? [this.propertiesWidget] : [];
|
||||
} else if (types.isString(properties) && properties === 'collapsed') {
|
||||
return [this.propertiesWidget];
|
||||
} else if (types.isArray(properties)) {
|
||||
return properties.map((item) => {
|
||||
const retVal = Object.assign({}, this.propertiesWidget);
|
||||
retVal.edition = item.edition;
|
||||
retVal.provider = item.provider;
|
||||
retVal.widget = { 'properties-widget': { properties: item.properties } };
|
||||
return retVal;
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(refreshConfig: boolean = false): void {
|
||||
if (refreshConfig) {
|
||||
this.init();
|
||||
} else {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
tabContent.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
tabContent.enableEdit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public handleTabChange(tab: TabComponent): void {
|
||||
this._cd.detectChanges();
|
||||
const localtab = this._tabs.find(i => i.id === tab.identifier);
|
||||
this._editEnabled.fire(localtab.editable);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
public handleTabClose(tab: TabComponent): void {
|
||||
const index = this.tabs.findIndex(i => i.id === tab.identifier);
|
||||
this.tabs.splice(index, 1);
|
||||
this.angularEventingService.sendAngularEvent(this.dashboardService.getUnderlyingUri(), AngularEventType.CLOSE_TAB, { id: tab.identifier });
|
||||
}
|
||||
}
|
||||
25
src/sql/workbench/parts/dashboard/common/dashboardPage.css
Normal file
25
src/sql/workbench/parts/dashboard/common/dashboardPage.css
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
|
||||
dashboard-page .monaco-scrollable-element {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
dashboard-page .dashboard-panel .tab-header .action-item .action-label.unpin,
|
||||
dashboard-page .dashboard-panel .tab-header .action-item .action-label.pin {
|
||||
padding: 6px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
dashboard-page .dashboard-panel .tab-header .action-item .action-label.close {
|
||||
padding: 5px;
|
||||
}
|
||||
24
src/sql/workbench/parts/dashboard/common/dashboardPanel.css
Normal file
24
src/sql/workbench/parts/dashboard/common/dashboardPanel.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel {
|
||||
border-top-width: 0px;
|
||||
border-top-style: solid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel > .title > .monaco-scrollable-element > .tabList .tab-header .tab > .tabLabel.active {
|
||||
border-bottom: 0px solid;
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel .tabList .tab .tabLabel {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel > .title > .title-actions,
|
||||
panel.dashboard-panel > .tabbedPanel > .title > .monaco-scrollable-element > .tabList .tab-header {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
115
src/sql/workbench/parts/dashboard/common/dashboardPanelStyles.ts
Normal file
115
src/sql/workbench/parts/dashboard/common/dashboardPanelStyles.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardPanel';
|
||||
|
||||
import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
|
||||
import {
|
||||
TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_ACTIVE_BORDER, TAB_INACTIVE_BACKGROUND,
|
||||
TAB_INACTIVE_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, TAB_BORDER, EDITOR_GROUP_BORDER
|
||||
} from 'vs/workbench/common/theme';
|
||||
import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
|
||||
|
||||
// Title Active
|
||||
const tabActiveBackground = theme.getColor(TAB_ACTIVE_BACKGROUND);
|
||||
const tabActiveForeground = theme.getColor(TAB_ACTIVE_FOREGROUND);
|
||||
if (tabActiveBackground || tabActiveForeground) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab:hover .tabLabel,
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab .tabLabel.active {
|
||||
color: ${tabActiveForeground};
|
||||
border-bottom: 0px solid;
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab-header.active {
|
||||
background-color: ${tabActiveBackground};
|
||||
outline-color: ${tabActiveBackground};
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel.horizontal > .title .tabList .tab-header.active {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel.vertical > .title .tabList .tab-header.active {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const activeTabBorderColor = theme.getColor(TAB_ACTIVE_BORDER);
|
||||
if (activeTabBorderColor) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab-header.active {
|
||||
box-shadow: ${activeTabBorderColor} 0 -1px inset;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Title Inactive
|
||||
const tabInactiveBackground = theme.getColor(TAB_INACTIVE_BACKGROUND);
|
||||
const tabInactiveForeground = theme.getColor(TAB_INACTIVE_FOREGROUND);
|
||||
if (tabInactiveBackground || tabInactiveForeground) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab .tabLabel {
|
||||
color: ${tabInactiveForeground};
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab-header {
|
||||
background-color: ${tabInactiveBackground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Panel title background
|
||||
const panelTitleBackground = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND);
|
||||
if (panelTitleBackground) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title {
|
||||
background-color: ${panelTitleBackground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Panel title background
|
||||
const tabBorder = theme.getColor(TAB_BORDER);
|
||||
if (tabBorder) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab-header {
|
||||
border-right-color: ${tabBorder};
|
||||
border-bottom-color: ${tabBorder};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// Styling with Outline color (e.g. high contrast theme)
|
||||
const outline = theme.getColor(activeContrastBorder);
|
||||
if (outline) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title {
|
||||
border-bottom-color: ${tabBorder};
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
panel.dashboard-panel > .tabbedPanel.vertical > .title {
|
||||
border-right-color: ${tabBorder};
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const divider = theme.getColor(EDITOR_GROUP_BORDER);
|
||||
if (divider) {
|
||||
collector.addRule(`
|
||||
panel.dashboard-panel > .tabbedPanel > .title .tabList .tab-header {
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import { registerTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
import { generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/workbench/parts/dashboard/containers/dashboardNavSection.contribution';
|
||||
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
|
||||
|
||||
export interface IDashboardTabContrib {
|
||||
id: string;
|
||||
title: string;
|
||||
container: object;
|
||||
provider: string | string[];
|
||||
when?: string;
|
||||
description?: string;
|
||||
alwaysShow?: boolean;
|
||||
isHomeTab?: boolean;
|
||||
}
|
||||
|
||||
const tabSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: localize('azdata.extension.contributes.dashboard.tab.id', "Unique identifier for this tab. Will be passed to the extension for any requests.")
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: localize('azdata.extension.contributes.dashboard.tab.title', "Title of the tab to show the user.")
|
||||
},
|
||||
description: {
|
||||
description: localize('azdata.extension.contributes.dashboard.tab.description', "Description of this tab that will be shown to the user."),
|
||||
type: 'string'
|
||||
},
|
||||
when: {
|
||||
description: localize('azdata.extension.contributes.tab.when', 'Condition which must be true to show this item'),
|
||||
type: 'string'
|
||||
},
|
||||
provider: {
|
||||
description: localize('azdata.extension.contributes.tab.provider', 'Defines the connection types this tab is compatible with. Defaults to "MSSQL" if not set'),
|
||||
type: ['string', 'array']
|
||||
|
||||
},
|
||||
container: {
|
||||
description: localize('azdata.extension.contributes.dashboard.tab.container', "The container that will be displayed in this tab."),
|
||||
type: 'object',
|
||||
properties: generateContainerTypeSchemaProperties()
|
||||
},
|
||||
alwaysShow: {
|
||||
description: localize('azdata.extension.contributes.dashboard.tab.alwaysShow', "Whether or not this tab should always be shown or only when the user adds it."),
|
||||
type: 'boolean'
|
||||
},
|
||||
isHomeTab: {
|
||||
description: localize('azdata.extension.contributes.dashboard.tab.isHomeTab', "Whether or not this tab should be used as the Home tab for a connection type."),
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tabContributionSchema: IJSONSchema = {
|
||||
description: localize('azdata.extension.contributes.tabs', "Contributes a single or multiple tabs for users to add to their dashboard."),
|
||||
oneOf: [
|
||||
tabSchema,
|
||||
{
|
||||
type: 'array',
|
||||
items: tabSchema
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IDashboardTabContrib | IDashboardTabContrib[]>({ extensionPoint: 'dashboard.tabs', jsonSchema: tabContributionSchema }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(tab: IDashboardTabContrib, extension: IExtensionPointUser<any>) {
|
||||
let { description, container, provider, title, when, id, alwaysShow, isHomeTab } = tab;
|
||||
|
||||
// If always show is not specified, set it to true by default.
|
||||
if (!types.isBoolean(alwaysShow)) {
|
||||
alwaysShow = true;
|
||||
}
|
||||
const publisher = extension.description.publisher;
|
||||
if (!title) {
|
||||
extension.collector.error(localize('dashboardTab.contribution.noTitleError', 'No title specified for extension.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
extension.collector.warn(localize('dashboardTab.contribution.noDescriptionWarning', 'No description specified to show.'));
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
extension.collector.error(localize('dashboardTab.contribution.noContainerError', 'No container specified for extension.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
// Use a default. Consider warning extension developers about this in the future if in development mode
|
||||
provider = Constants.mssqlProviderName;
|
||||
// Cannot be a home tab if it did not specify a provider
|
||||
isHomeTab = false;
|
||||
}
|
||||
|
||||
if (Object.keys(container).length !== 1) {
|
||||
extension.collector.error(localize('dashboardTab.contribution.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space'));
|
||||
return;
|
||||
}
|
||||
|
||||
let result = true;
|
||||
const containerkey = Object.keys(container)[0];
|
||||
const containerValue = Object.values(container)[0];
|
||||
|
||||
switch (containerkey) {
|
||||
case WIDGETS_CONTAINER:
|
||||
result = validateWidgetContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case GRID_CONTAINER:
|
||||
result = validateGridContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case NAV_SECTION:
|
||||
result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
registerTab({ description, title, container, provider, when, id, alwaysShow, publisher, isHomeTab });
|
||||
}
|
||||
}
|
||||
|
||||
for (const extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IDashboardTabContrib>(value)) {
|
||||
for (const command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
72
src/sql/workbench/parts/dashboard/common/dashboardWidget.ts
Normal file
72
src/sql/workbench/parts/dashboard/common/dashboardWidget.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
|
||||
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;
|
||||
provider: string | Array<string>;
|
||||
edition: number | Array<number>;
|
||||
when?: string;
|
||||
gridItemConfig?: NgGridItemConfig;
|
||||
widget: Object;
|
||||
background_color?: string;
|
||||
border?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: string;
|
||||
padding?: string;
|
||||
}
|
||||
|
||||
export interface TabConfig extends IDashboardTab {
|
||||
context: string;
|
||||
originalConfig: Array<WidgetConfig>;
|
||||
editable: boolean;
|
||||
canClose: boolean;
|
||||
actions?: Array<Action>;
|
||||
iconClass?: string;
|
||||
}
|
||||
|
||||
export type IUserFriendlyIcon = string | { light: string; dark: string; };
|
||||
|
||||
export interface NavSectionConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
iconClass?: string;
|
||||
icon?: IUserFriendlyIcon;
|
||||
container: object;
|
||||
}
|
||||
|
||||
export interface TabSettingConfig {
|
||||
tabId: string;
|
||||
isPinned: boolean;
|
||||
}
|
||||
|
||||
export abstract class DashboardWidget extends Disposable implements OnDestroy {
|
||||
protected _config: WidgetConfig;
|
||||
|
||||
get actions(): Array<Action> {
|
||||
return [];
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
45
src/sql/workbench/parts/dashboard/common/interfaces.ts
Normal file
45
src/sql/workbench/parts/dashboard/common/interfaces.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnDestroy } from '@angular/core';
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
import { SingleConnectionManagementService } from 'sql/services/common/commonServiceInterface.service';
|
||||
|
||||
export enum Conditional {
|
||||
'equals',
|
||||
'notEquals',
|
||||
'greaterThanOrEquals',
|
||||
'greaterThan',
|
||||
'lessThanOrEquals',
|
||||
'lessThan',
|
||||
'always'
|
||||
}
|
||||
|
||||
export abstract class DashboardTab extends TabChild implements OnDestroy {
|
||||
public abstract layout(): void;
|
||||
public abstract readonly id: string;
|
||||
public abstract readonly editable: boolean;
|
||||
public abstract refresh(): void;
|
||||
public abstract readonly onResize: Event<void>;
|
||||
public enableEdit(): void {
|
||||
// no op
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IConfigModifierCollection {
|
||||
connectionManagementService: SingleConnectionManagementService;
|
||||
contextKeyService: IContextKeyService;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { registerContainer, generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/workbench/parts/dashboard/containers/dashboardNavSection.contribution';
|
||||
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
|
||||
import { WEBVIEW_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWebviewContainer.contribution';
|
||||
|
||||
const containerTypes = [
|
||||
WIDGETS_CONTAINER,
|
||||
GRID_CONTAINER,
|
||||
WEBVIEW_CONTAINER,
|
||||
NAV_SECTION
|
||||
];
|
||||
|
||||
export type IUserFriendlyIcon = string | { light: string; dark: string; };
|
||||
|
||||
export interface IDashboardContainerContrib {
|
||||
id: string;
|
||||
container: object;
|
||||
}
|
||||
|
||||
const containerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: localize('azdata.extension.contributes.dashboard.container.id', "Unique identifier for this container.")
|
||||
},
|
||||
container: {
|
||||
description: localize('azdata.extension.contributes.dashboard.container.container', "The container that will be displayed in the tab."),
|
||||
type: 'object',
|
||||
properties: generateContainerTypeSchemaProperties()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const containerContributionSchema: IJSONSchema = {
|
||||
description: localize('azdata.extension.contributes.containers', "Contributes a single or multiple dashboard containers for users to add to their dashboard."),
|
||||
oneOf: [
|
||||
containerSchema,
|
||||
{
|
||||
type: 'array',
|
||||
items: containerSchema
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IDashboardContainerContrib | IDashboardContainerContrib[]>({ extensionPoint: 'dashboard.containers', jsonSchema: containerContributionSchema }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(dashboardContainer: IDashboardContainerContrib, extension: IExtensionPointUser<any>) {
|
||||
const { id, container } = dashboardContainer;
|
||||
if (!id) {
|
||||
extension.collector.error(localize('dashboardContainer.contribution.noIdError', 'No id in dashboard container specified for extension.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
extension.collector.error(localize('dashboardContainer.contribution.noContainerError', 'No container in dashboard container specified for extension.'));
|
||||
return;
|
||||
}
|
||||
if (Object.keys(container).length !== 1) {
|
||||
extension.collector.error(localize('dashboardContainer.contribution.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.'));
|
||||
return;
|
||||
}
|
||||
|
||||
let result = true;
|
||||
const containerkey = Object.keys(container)[0];
|
||||
const containerValue = Object.values(container)[0];
|
||||
|
||||
const containerTypeFound = containerTypes.find(c => (c === containerkey));
|
||||
if (!containerTypeFound) {
|
||||
extension.collector.error(localize('dashboardTab.contribution.unKnownContainerType', 'Unknown container type defines in dashboard container for extension.'));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (containerkey) {
|
||||
case WIDGETS_CONTAINER:
|
||||
result = validateWidgetContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case GRID_CONTAINER:
|
||||
result = validateGridContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case NAV_SECTION:
|
||||
result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
registerContainer({ id, container });
|
||||
}
|
||||
}
|
||||
|
||||
for (const extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IDashboardContainerContrib>(value)) {
|
||||
for (const command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardControlHostContainer';
|
||||
|
||||
import { Component, forwardRef, Input, AfterContentInit, ViewChild, OnChanges } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { ControlHostContent } from 'sql/workbench/parts/dashboard/contents/controlHostContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-controlhost-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardControlHostContainer) }],
|
||||
template: `
|
||||
<controlhost-content [webviewId]="tab.id">
|
||||
</controlhost-content>
|
||||
`
|
||||
})
|
||||
|
||||
export class DashboardControlHostContainer extends DashboardTab implements AfterContentInit {
|
||||
@Input() private tab: TabConfig;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(ControlHostContent) private _hostContent: ControlHostContent;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._hostContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
|
||||
const container = <any>this.tab.container;
|
||||
if (container['controlhost-container'] && container['controlhost-container'].type) {
|
||||
this._hostContent.setControlType(container['controlhost-container'].type);
|
||||
}
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._hostContent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this._hostContent.refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const CONTROLHOST_CONTAINER = 'controlhost-container';
|
||||
|
||||
let webviewSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('dashboard.container.controlhost', "The controlhost that will be displayed in this tab."),
|
||||
default: null
|
||||
};
|
||||
|
||||
registerContainerType(CONTROLHOST_CONTAINER, webviewSchema);
|
||||
registerNavSectionContainerType(CONTROLHOST_CONTAINER, webviewSchema);
|
||||
@@ -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-controlhost-container {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardErrorContainer';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ChangeDetectorRef, AfterViewInit } from '@angular/core';
|
||||
|
||||
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-error-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardErrorContainer) }],
|
||||
template: `
|
||||
<div class="error-container">
|
||||
<div class="icon globalError">
|
||||
</div>
|
||||
<div class="error-message" #errorMessage>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class DashboardErrorContainer extends DashboardTab implements AfterViewInit {
|
||||
@Input() private tab: TabConfig;
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild('errorMessage', { read: ElementRef }) private _errorMessageContainer: ElementRef;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
const errorMessage = this._errorMessageContainer.nativeElement as HTMLElement;
|
||||
errorMessage.innerText = nls.localize('dashboardNavSection_loadTabError', 'The "{0}" section has invalid content. Please contact extension owner.', this.tab.title);
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-error-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
dashboard-error-container .error-container {
|
||||
padding: 6px;
|
||||
background: #D02E00;
|
||||
color: white;
|
||||
}
|
||||
|
||||
dashboard-error-container .error-container .icon.globalError {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
float: left;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div>
|
||||
<table class="grid-table">
|
||||
<tr *ngFor="let row of rows" class="grid-table-row">
|
||||
<ng-container *ngFor="let col of cols">
|
||||
<ng-container *ngIf="getContent(row,col) !== undefined">
|
||||
<td class="table-cell" [colSpan]=getColspan(row,col) [rowSpan]=getRowspan(row,col)
|
||||
[width]="getWidgetWidth(row,col)" [height]="getWidgetHeight(row,col)">
|
||||
|
||||
<dashboard-widget-wrapper *ngIf="isWidget(row,col)" [_config]="getWidgetContent(row,col)"
|
||||
style="position:absolute;" [style.width]="getWidgetWidth(row,col)"
|
||||
[style.height]="getWidgetHeight(row,col)">
|
||||
</dashboard-widget-wrapper>
|
||||
|
||||
<webview-content *ngIf="isWebview(row,col)" [webviewId]="getWebviewId(row,col)">
|
||||
</webview-content>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,221 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardGridContainer';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { TabConfig, WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardWidgetWrapper } from 'sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component';
|
||||
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { WebviewContent } from 'sql/workbench/parts/dashboard/contents/webviewContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
export interface GridCellConfig {
|
||||
id?: string;
|
||||
row?: number;
|
||||
col?: number;
|
||||
colspan?: string | number;
|
||||
rowspan?: string | number;
|
||||
}
|
||||
|
||||
export interface GridWidgetConfig extends GridCellConfig, WidgetConfig {
|
||||
}
|
||||
|
||||
export interface GridWebviewConfig extends GridCellConfig {
|
||||
webview: {
|
||||
id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-grid-container',
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/containers/dashboardGridContainer.component.html')),
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardGridContainer) }]
|
||||
})
|
||||
export class DashboardGridContainer extends DashboardTab implements OnDestroy {
|
||||
@Input() private tab: TabConfig;
|
||||
private _contents: GridCellConfig[];
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
private cellWidth: number = 270;
|
||||
private cellHeight: number = 270;
|
||||
|
||||
protected SKELETON_WIDTH = 5;
|
||||
|
||||
protected rows: number[];
|
||||
protected cols: number[];
|
||||
|
||||
protected getContent(row: number, col: number): GridCellConfig {
|
||||
const widget = this._contents.filter(w => w.row === row && w.col === col);
|
||||
return widget ? widget[0] : undefined;
|
||||
}
|
||||
|
||||
protected getWidgetContent(row: number, col: number): GridWidgetConfig {
|
||||
const content = this.getContent(row, col);
|
||||
if (content) {
|
||||
const widgetConfig = <GridWidgetConfig>content;
|
||||
if (widgetConfig && widgetConfig.widget) {
|
||||
return widgetConfig;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getWebviewContent(row: number, col: number): GridWebviewConfig {
|
||||
const content = this.getContent(row, col);
|
||||
if (content) {
|
||||
const webviewConfig = <GridWebviewConfig>content;
|
||||
if (webviewConfig && webviewConfig.webview) {
|
||||
return webviewConfig;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
protected isWidget(row: number, col: number): boolean {
|
||||
const widgetConfig = this.getWidgetContent(row, col);
|
||||
return widgetConfig !== undefined;
|
||||
}
|
||||
|
||||
protected isWebview(row: number, col: number): boolean {
|
||||
const webview = this.getWebviewContent(row, col);
|
||||
return webview !== undefined;
|
||||
}
|
||||
|
||||
protected getWebviewId(row: number, col: number): string {
|
||||
const widgetConfig = this.getWebviewContent(row, col);
|
||||
if (widgetConfig && widgetConfig.webview) {
|
||||
return widgetConfig.webview.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getColspan(row: number, col: number): string {
|
||||
const content = this.getContent(row, col);
|
||||
let colspan: string = '1';
|
||||
if (content && content.colspan) {
|
||||
colspan = this.convertToNumber(content.colspan, this.cols.length).toString();
|
||||
}
|
||||
return colspan;
|
||||
}
|
||||
|
||||
protected getRowspan(row: number, col: number): string {
|
||||
const content = this.getContent(row, col);
|
||||
if (content && (content.rowspan)) {
|
||||
return this.convertToNumber(content.rowspan, this.rows.length).toString();
|
||||
} else {
|
||||
return '1';
|
||||
}
|
||||
}
|
||||
|
||||
protected getWidgetWidth(row: number, col: number): string {
|
||||
const colspan = this.getColspan(row, col);
|
||||
const columnCount = this.convertToNumber(colspan, this.cols.length);
|
||||
|
||||
return columnCount * this.cellWidth + 'px';
|
||||
}
|
||||
|
||||
protected getWidgetHeight(row: number, col: number): string {
|
||||
const rowspan = this.getRowspan(row, col);
|
||||
const rowCount = this.convertToNumber(rowspan, this.rows.length);
|
||||
|
||||
return rowCount * this.cellHeight + 'px';
|
||||
}
|
||||
|
||||
private convertToNumber(value: string | number, maxNumber: number): number {
|
||||
if (!value) {
|
||||
return 1;
|
||||
}
|
||||
if (value === '*') {
|
||||
return maxNumber;
|
||||
}
|
||||
try {
|
||||
return +value;
|
||||
} catch {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
|
||||
@ViewChildren(WebviewContent) private _webViews: QueryList<WebviewContent>;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.tab.container) {
|
||||
this._contents = Object.values(this.tab.container)[0];
|
||||
this._contents.forEach(widget => {
|
||||
if (!widget.row) {
|
||||
widget.row = 0;
|
||||
}
|
||||
if (!widget.col) {
|
||||
widget.col = 0;
|
||||
}
|
||||
if (!widget.colspan) {
|
||||
widget.colspan = '1';
|
||||
}
|
||||
if (!widget.rowspan) {
|
||||
widget.rowspan = '1';
|
||||
}
|
||||
});
|
||||
this.rows = this.createIndexes(this._contents.map(w => w.row));
|
||||
this.cols = this.createIndexes(this._contents.map(w => w.col));
|
||||
}
|
||||
}
|
||||
|
||||
private createIndexes(indexes: number[]) {
|
||||
const max = Math.max(...indexes) + 1;
|
||||
return Array(max).fill(0).map((x, i) => i);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.layout();
|
||||
});
|
||||
}
|
||||
if (this._webViews) {
|
||||
this._webViews.forEach(item => {
|
||||
item.layout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { generateDashboardGridLayoutSchema } from 'sql/workbench/parts/dashboard/pages/dashboardPageContribution';
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const GRID_CONTAINER = 'grid-container';
|
||||
|
||||
let gridContainersSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.container.gridtab.items', "The list of widgets or webviews that will be displayed in this tab."),
|
||||
items: generateDashboardGridLayoutSchema(undefined, true)
|
||||
};
|
||||
|
||||
registerContainerType(GRID_CONTAINER, gridContainersSchema);
|
||||
registerNavSectionContainerType(GRID_CONTAINER, gridContainersSchema);
|
||||
|
||||
export function validateGridContainerContribution(extension: IExtensionPointUser<any>, gridConfigs: object[]): boolean {
|
||||
let result = true;
|
||||
gridConfigs.forEach(widgetConfig => {
|
||||
const allKeys = Object.keys(widgetConfig);
|
||||
const widgetOrWebviewKey = allKeys.find(key => key === 'widget' || key === 'webview');
|
||||
if (!widgetOrWebviewKey) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('gridContainer.invalidInputs', 'widgets or webviews are expected inside widgets-container for extension.'));
|
||||
return;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-tab {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.grid-table {
|
||||
border-spacing: 5px;
|
||||
}
|
||||
|
||||
.grid-table-row {
|
||||
width: auto;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardHomeContainer';
|
||||
|
||||
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild, ContentChild } from '@angular/core';
|
||||
|
||||
import { DashboardWidgetContainer } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.component';
|
||||
import { WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { AngularEventType, IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
import { DashboardWidgetWrapper } from 'sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component';
|
||||
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-home-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardHomeContainer) }],
|
||||
template: `
|
||||
<div class="fullsize" style="display: flex; flex-direction: column">
|
||||
<div scrollable [horizontalScroll]="ScrollbarVisibility.Hidden" [verticalScroll]="ScrollbarVisibility.Auto">
|
||||
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
|
||||
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
|
||||
</dashboard-widget-wrapper>
|
||||
<widget-content style="flex: 1" [scrollContent]="false" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
|
||||
</widget-content>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class DashboardHomeContainer extends DashboardWidgetContainer {
|
||||
@Input() private properties: WidgetConfig;
|
||||
@ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper;
|
||||
@ContentChild(ScrollableDirective) private _scrollable;
|
||||
|
||||
private ScrollbarVisibility = ScrollbarVisibility;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: DashboardServiceInterface,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
|
||||
@Inject(IAngularEventingService) private angularEventingService: IAngularEventingService
|
||||
) {
|
||||
super(_cd);
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
const collapsedVal = this.dashboardService.getSettings<string>(`${this.properties.context}.properties`);
|
||||
if (collapsedVal === 'collapsed') {
|
||||
this._propertiesClass.collapsed = true;
|
||||
}
|
||||
this.angularEventingService.onAngularEvent(this.dashboardService.getUnderlyingUri(), event => {
|
||||
if (event.event === AngularEventType.COLLAPSE_WIDGET && this._propertiesClass && event.payload === this._propertiesClass.guid) {
|
||||
this._propertiesClass.collapsed = !this._propertiesClass.collapsed;
|
||||
this._cd.detectChanges();
|
||||
this._configurationService.updateValue(`dashboard.${this.properties.context}.properties`,
|
||||
this._propertiesClass.collapsed ? 'collapsed' : true, ConfigurationTarget.USER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public layout() {
|
||||
super.layout();
|
||||
if (this._scrollable) {
|
||||
this._scrollable.layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-home-tab {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardWebviewContainer';
|
||||
|
||||
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-modelview-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardModelViewContainer) }],
|
||||
template: `
|
||||
<modelview-content [modelViewId]="tab.id">
|
||||
</modelview-content>
|
||||
`
|
||||
})
|
||||
export class DashboardModelViewContainer extends DashboardTab implements AfterContentInit {
|
||||
@Input() private tab: TabConfig;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._modelViewContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._modelViewContent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const MODELVIEW_CONTAINER = 'modelview-container';
|
||||
|
||||
let modelviewSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('dashboard.container.modelview', "The model-backed view that will be displayed in this tab."),
|
||||
default: null
|
||||
};
|
||||
|
||||
registerContainerType(MODELVIEW_CONTAINER, modelviewSchema);
|
||||
registerNavSectionContainerType(MODELVIEW_CONTAINER, modelviewSchema);
|
||||
@@ -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-modelview-container {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<panel [options]="panelOpt" style="flex: 1 1 auto;" class="dashboard-panel">
|
||||
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize"
|
||||
[identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions" [iconClass]="tab.iconClass">
|
||||
<ng-template>
|
||||
<dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab">
|
||||
</dashboard-webview-container>
|
||||
<dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab">
|
||||
</dashboard-widget-container>
|
||||
<dashboard-modelview-container *ngIf="getContentType(tab) === 'modelview-container'" [tab]="tab">
|
||||
</dashboard-modelview-container>
|
||||
<dashboard-grid-container *ngIf="getContentType(tab) === 'grid-container'" [tab]="tab">
|
||||
</dashboard-grid-container>
|
||||
<dashboard-error-container *ngIf="getContentType(tab) === 'error-container'" [tab]="tab">
|
||||
</dashboard-error-container>
|
||||
</ng-template>
|
||||
</tab>
|
||||
</panel>
|
||||
@@ -0,0 +1,177 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardNavSection';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, OnChanges, AfterContentInit } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface, SingleConnectionManagementService } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { WidgetConfig, TabConfig, NavSectionConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
import { DashboardTab, IConfigModifierCollection } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { WIDGETS_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
|
||||
import * as dashboardHelper from 'sql/workbench/parts/dashboard/common/dashboardHelper';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-nav-section',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardNavSection) }],
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/containers/dashboardNavSection.component.html'))
|
||||
})
|
||||
export class DashboardNavSection extends DashboardTab implements OnDestroy, OnChanges, AfterContentInit, IConfigModifierCollection {
|
||||
@Input() private tab: TabConfig;
|
||||
protected tabs: Array<TabConfig> = [];
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private readonly panelOpt: IPanelOptions = {
|
||||
layout: NavigationBarLayout.vertical
|
||||
};
|
||||
|
||||
// a set of config modifiers
|
||||
private readonly _configModifiers: Array<(item: Array<WidgetConfig>, collection: IConfigModifierCollection, context: string) => Array<WidgetConfig>> = [
|
||||
dashboardHelper.removeEmpty,
|
||||
dashboardHelper.initExtensionConfigs,
|
||||
dashboardHelper.addProvider,
|
||||
dashboardHelper.addEdition,
|
||||
dashboardHelper.addContext,
|
||||
dashboardHelper.filterConfigs
|
||||
];
|
||||
|
||||
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>, originalConfig: Array<WidgetConfig>) => Array<WidgetConfig>> = [
|
||||
dashboardHelper.validateGridConfig
|
||||
];
|
||||
|
||||
@ViewChildren(TabChild) private _tabs: QueryList<DashboardTab>;
|
||||
@ViewChild(PanelComponent) private _panel: PanelComponent;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.tabs = [];
|
||||
let navSectionContainers: NavSectionConfig[] = [];
|
||||
if (this.tab.container) {
|
||||
navSectionContainers = Object.values(this.tab.container)[0];
|
||||
let hasIcon = true;
|
||||
navSectionContainers.forEach(navSection => {
|
||||
if (!navSection.iconClass) {
|
||||
hasIcon = false;
|
||||
}
|
||||
});
|
||||
this.panelOpt.showIcon = hasIcon;
|
||||
this.loadNewTabs(navSectionContainers);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
this._register(tabContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private loadNewTabs(dashboardTabs: NavSectionConfig[]) {
|
||||
if (dashboardTabs && dashboardTabs.length > 0) {
|
||||
dashboardTabs.map(v => {
|
||||
const containerResult = dashboardHelper.getDashboardContainer(v.container);
|
||||
if (!containerResult.result) {
|
||||
return { id: v.id, title: v.title, container: { 'error-container': undefined } };
|
||||
}
|
||||
|
||||
const key = Object.keys(containerResult.container)[0];
|
||||
if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) {
|
||||
let configs = <WidgetConfig[]>Object.values(containerResult.container)[0];
|
||||
this._configModifiers.forEach(cb => {
|
||||
configs = cb.apply(this, [configs, this, this.tab.context]);
|
||||
});
|
||||
this._gridModifiers.forEach(cb => {
|
||||
configs = cb.apply(this, [configs]);
|
||||
});
|
||||
if (key === WIDGETS_CONTAINER) {
|
||||
return { id: v.id, title: v.title, container: { 'widgets-container': configs }, iconClass: v.iconClass };
|
||||
|
||||
} else {
|
||||
return { id: v.id, title: v.title, container: { 'grid-container': configs }, iconClass: v.iconClass };
|
||||
}
|
||||
}
|
||||
return { id: v.id, title: v.title, container: containerResult.container, iconClass: v.iconClass };
|
||||
}).map(v => {
|
||||
const config = v as TabConfig;
|
||||
config.context = this.tab.context;
|
||||
config.editable = false;
|
||||
config.canClose = false;
|
||||
this.addNewTab(config);
|
||||
return config;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addNewTab(tab: TabConfig): void {
|
||||
const existedTab = this.tabs.find(i => i.id === tab.id);
|
||||
if (!existedTab) {
|
||||
this.tabs.push(tab);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
protected getContentType(tab: TabConfig): string {
|
||||
return tab.container ? Object.keys(tab.container)[0] : '';
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
const activeTabId = this._panel.getActiveTab;
|
||||
const localtab = this._tabs.find(i => i.id === activeTabId);
|
||||
this._cd.detectChanges();
|
||||
localtab.layout();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
tabContent.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
tabContent.enableEdit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get connectionManagementService(): SingleConnectionManagementService {
|
||||
return this.dashboardService.connectionManagementService;
|
||||
}
|
||||
|
||||
public get contextKeyService(): IContextKeyService {
|
||||
return this.dashboardService.scopedContextKeyService;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { join } from 'path';
|
||||
import { createCSSRule } from 'vs/base/browser/dom';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IdGenerator } from 'vs/base/common/idGenerator';
|
||||
|
||||
import { NavSectionConfig, IUserFriendlyIcon } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.contribution';
|
||||
|
||||
export const NAV_SECTION = 'nav-section';
|
||||
|
||||
const navSectionContainerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.container.left-nav-bar.id', "Unique identifier for this nav section. Will be passed to the extension for any requests.")
|
||||
},
|
||||
icon: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.icon', '(Optional) Icon which is used to represent this nav section in the UI. Either a file path or a themeable configuration'),
|
||||
anyOf: [{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.icon.light', 'Icon path when a light theme is used'),
|
||||
type: 'string'
|
||||
},
|
||||
dark: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.icon.dark', 'Icon path when a dark theme is used'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.container.left-nav-bar.title', "Title of the nav section to show the user.")
|
||||
},
|
||||
container: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.container', "The container that will be displayed in this nav section."),
|
||||
type: 'object',
|
||||
properties: generateNavSectionContainerTypeSchemaProperties()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const NavSectionSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.container.left-nav-bar', "The list of dashboard containers that will be displayed in this navigation section."),
|
||||
items: navSectionContainerSchema
|
||||
};
|
||||
|
||||
registerContainerType(NAV_SECTION, NavSectionSchema);
|
||||
|
||||
function isValidIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): boolean {
|
||||
if (typeof icon === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
if (typeof icon === 'string') {
|
||||
return true;
|
||||
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
|
||||
return true;
|
||||
}
|
||||
extension.collector.error(nls.localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const ids = new IdGenerator('contrib-dashboardNavSection-icon-');
|
||||
|
||||
function createCSSRuleForIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): string {
|
||||
let iconClass: string;
|
||||
if (icon) {
|
||||
iconClass = ids.nextId();
|
||||
if (typeof icon === 'string') {
|
||||
const path = join(extension.description.extensionLocation.fsPath, icon);
|
||||
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(path).toString()}")`);
|
||||
} else {
|
||||
const light = join(extension.description.extensionLocation.fsPath, icon.light);
|
||||
const dark = join(extension.description.extensionLocation.fsPath, icon.dark);
|
||||
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(light).toString()}")`);
|
||||
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(dark).toString()}")`);
|
||||
}
|
||||
}
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
export function validateNavSectionContributionAndRegisterIcon(extension: IExtensionPointUser<any>, navSectionConfigs: NavSectionConfig[]): boolean {
|
||||
let result = true;
|
||||
navSectionConfigs.forEach(section => {
|
||||
if (!section.title) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.missingTitle_error', 'No title in nav section specified for extension.'));
|
||||
}
|
||||
|
||||
if (!section.container) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.missingContainer_error', 'No container in nav section specified for extension.'));
|
||||
}
|
||||
|
||||
if (Object.keys(section.container).length !== 1) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.'));
|
||||
}
|
||||
|
||||
if (isValidIcon(section.icon, extension)) {
|
||||
section.iconClass = createCSSRuleForIcon(section.icon, extension);
|
||||
}
|
||||
|
||||
const containerKey = Object.keys(section.container)[0];
|
||||
const containerValue = Object.values(section.container)[0];
|
||||
|
||||
switch (containerKey) {
|
||||
case WIDGETS_CONTAINER:
|
||||
result = result && validateWidgetContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case GRID_CONTAINER:
|
||||
result = result && validateGridContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case NAV_SECTION:
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.invalidContainer_error', 'NAV_SECTION within NAV_SECTION is an invalid container for extension.'));
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -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-nav-section {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardWebviewContainer';
|
||||
|
||||
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { WebviewContent } from 'sql/workbench/parts/dashboard/contents/webviewContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-webview-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWebviewContainer) }],
|
||||
template: `
|
||||
<webview-content [webviewId]="tab.id">
|
||||
</webview-content>
|
||||
`
|
||||
})
|
||||
export class DashboardWebviewContainer extends DashboardTab implements AfterContentInit {
|
||||
@Input() private tab: TabConfig;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(WebviewContent) private _webviewContent: WebviewContent;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._webviewContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._webviewContent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const WEBVIEW_CONTAINER = 'webview-container';
|
||||
|
||||
let webviewSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('dashboard.container.webview', "The webview that will be displayed in this tab."),
|
||||
default: null
|
||||
};
|
||||
|
||||
registerContainerType(WEBVIEW_CONTAINER, webviewSchema);
|
||||
registerNavSectionContainerType(WEBVIEW_CONTAINER, webviewSchema);
|
||||
@@ -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-webview-container {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboardWidgetContainer';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, OnDestroy, ChangeDetectorRef, AfterContentInit } from '@angular/core';
|
||||
|
||||
import { TabConfig, WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
|
||||
import { WidgetContent } from 'sql/workbench/parts/dashboard/contents/widgetContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-widget-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWidgetContainer) }],
|
||||
template: `
|
||||
<widget-content [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
|
||||
</widget-content>
|
||||
`
|
||||
})
|
||||
export class DashboardWidgetContainer extends DashboardTab implements OnDestroy, AfterContentInit {
|
||||
@Input() protected tab: TabConfig;
|
||||
protected widgets: WidgetConfig[];
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(WidgetContent) protected _widgetContent: WidgetContent;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.tab.container) {
|
||||
this.widgets = Object.values(this.tab.container)[0];
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._widgetContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
this._widgetContent.layout();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this._widgetContent.refresh();
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
this._widgetContent.enableEdit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { generateDashboardWidgetSchema } from 'sql/workbench/parts/dashboard/pages/dashboardPageContribution';
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const WIDGETS_CONTAINER = 'widgets-container';
|
||||
|
||||
const widgetsSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.container.widgets', "The list of widgets that will be displayed in this tab."),
|
||||
items: generateDashboardWidgetSchema(undefined, true)
|
||||
};
|
||||
|
||||
registerContainerType(WIDGETS_CONTAINER, widgetsSchema);
|
||||
registerNavSectionContainerType(WIDGETS_CONTAINER, widgetsSchema);
|
||||
|
||||
export function validateWidgetContainerContribution(extension: IExtensionPointUser<any>, WidgetConfigs: object[]): boolean {
|
||||
let result = true;
|
||||
WidgetConfigs.forEach(widgetConfig => {
|
||||
const allKeys = Object.keys(widgetConfig);
|
||||
const widgetKey = allKeys.find(key => key === 'widget');
|
||||
if (!widgetKey) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('widgetContainer.invalidInputs', 'The list of widgets is expected inside widgets-container for extension.'));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-widget-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
|
||||
<agentview-component #agent *ngIf="(controlType) === 'agent'"></agentview-component>
|
||||
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./controlHostContent';
|
||||
|
||||
import { Component, forwardRef, Input, OnInit, Inject, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { AgentViewComponent } from '../../../../parts/jobManagement/agent/agentView.component';
|
||||
|
||||
@Component({
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/contents/controlHostContent.component.html')),
|
||||
selector: 'controlhost-content'
|
||||
})
|
||||
export class ControlHostContent {
|
||||
@Input() private webviewId: string;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
private _onMessage = new Emitter<string>();
|
||||
public readonly onMessage: Event<string> = this._onMessage.event;
|
||||
|
||||
private _onMessageDisposable: IDisposable;
|
||||
private _type: string;
|
||||
|
||||
/* Children components */
|
||||
@ViewChild('agent') private _agentViewComponent: AgentViewComponent;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
|
||||
) {
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._agentViewComponent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.webviewId;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get connection(): azdata.connection.Connection {
|
||||
const currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
const connection: azdata.connection.Connection = {
|
||||
providerName: currentConnection.providerName,
|
||||
connectionId: currentConnection.id,
|
||||
options: currentConnection.options
|
||||
};
|
||||
return connection;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get serverInfo(): azdata.ServerInfo {
|
||||
return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
|
||||
}
|
||||
|
||||
public setControlType(type: string): void {
|
||||
this._type = type;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public get controlType(): string {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this._agentViewComponent.refresh = true;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
controlhost-content {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="display: flex; flex-flow: column; overflow: hidden; height: 100%; width: 100%">
|
||||
|
||||
<div #header>
|
||||
<div 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>
|
||||
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template [ngIf]="!collapsed">
|
||||
<ng-template component-host>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -0,0 +1,276 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./dashboardWidgetWrapper';
|
||||
|
||||
import {
|
||||
Component, Input, Inject, forwardRef, ComponentFactoryResolver, ViewChild,
|
||||
ElementRef, OnInit, ChangeDetectorRef, ReflectiveInjector, Injector, Type, ComponentRef
|
||||
} from '@angular/core';
|
||||
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
|
||||
import { WidgetConfig, WIDGET_CONFIG, IDashboardWidget } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { RefreshWidgetAction, ToggleMoreWidgetAction, DeleteWidgetAction, CollapseWidgetAction } from 'sql/workbench/parts/dashboard/common/actions';
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
|
||||
/* Widgets */
|
||||
import { PropertiesWidgetComponent } from 'sql/workbench/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
import { ExplorerWidget } from 'sql/workbench/parts/dashboard/widgets/explorer/explorerWidget.component';
|
||||
import { TasksWidget } from 'sql/workbench/parts/dashboard/widgets/tasks/tasksWidget.component';
|
||||
import { InsightsWidget } from 'sql/workbench/parts/dashboard/widgets/insights/insightsWidget.component';
|
||||
import { WebviewWidget } from 'sql/workbench/parts/dashboard/widgets/webview/webviewWidget.component';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
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 { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
const componentMap: { [x: string]: Type<IDashboardWidget> } = {
|
||||
'properties-widget': PropertiesWidgetComponent,
|
||||
'explorer-widget': ExplorerWidget,
|
||||
'tasks-widget': TasksWidget,
|
||||
'insights-widget': InsightsWidget,
|
||||
'webview-widget': WebviewWidget
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-widget-wrapper',
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component.html'))
|
||||
})
|
||||
export class DashboardWidgetWrapper extends AngularDisposable implements OnInit {
|
||||
@Input() private _config: WidgetConfig;
|
||||
@Input() private collapsable = false;
|
||||
|
||||
private _collapseAction: CollapseWidgetAction;
|
||||
private _collapsed = false;
|
||||
|
||||
public get collapsed(): boolean {
|
||||
return this._collapsed;
|
||||
}
|
||||
|
||||
public set collapsed(val: boolean) {
|
||||
if (val === this._collapsed) {
|
||||
return;
|
||||
}
|
||||
this._collapsed = val;
|
||||
this._collapseAction.state = val;
|
||||
this._changeref.detectChanges();
|
||||
if (!val) {
|
||||
this.loadWidget();
|
||||
}
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get guid(): string {
|
||||
return generateUuid();
|
||||
}
|
||||
|
||||
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(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrap: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => Injector)) private _injector: Injector,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange((event: IColorTheme) => {
|
||||
this.updateTheme(event);
|
||||
}));
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
if (this.componentHost) {
|
||||
this.loadWidget();
|
||||
}
|
||||
this._changeref.detectChanges();
|
||||
this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
|
||||
if (this._actions) {
|
||||
if (this.collapsable) {
|
||||
this._collapseAction = this.instantiationService.createInstance(CollapseWidgetAction, this._bootstrap.getUnderlyingUri(), this.guid, this.collapsed);
|
||||
this._actionbar.push(this._collapseAction, { icon: true, label: false });
|
||||
}
|
||||
this._actionbar.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, this._component.actionsContext), { icon: true, label: false });
|
||||
}
|
||||
this.layout();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (!this.collapsed && this._component && this._component.refresh) {
|
||||
this._component.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (!this.collapsed && this._component && this._component.layout) {
|
||||
this._component.layout();
|
||||
}
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._config.id;
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
this._actionbar.push(this.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');
|
||||
return;
|
||||
}
|
||||
const key = Object.keys(this._config.widget)[0];
|
||||
const selector = this.getOrCreateSelector(key);
|
||||
if (selector === undefined) {
|
||||
error('Could not find selector', key);
|
||||
return;
|
||||
}
|
||||
|
||||
// If _config.name is not set, set it to _config.widget.name
|
||||
if (!this._config.name) {
|
||||
const widget = Object.values(this._config.widget)[0];
|
||||
if (widget.name) {
|
||||
this._config.name = widget.name;
|
||||
}
|
||||
}
|
||||
|
||||
const componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
|
||||
|
||||
const viewContainerRef = this.componentHost.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
const injector = ReflectiveInjector.resolveAndCreate([{ provide: WIDGET_CONFIG, useValue: this._config }], this._injector);
|
||||
let componentRef: ComponentRef<IDashboardWidget>;
|
||||
try {
|
||||
componentRef = viewContainerRef.createComponent(componentFactory, 0, injector);
|
||||
this._component = componentRef.instance;
|
||||
const actions = componentRef.instance.actions;
|
||||
if (componentRef.instance.refresh) {
|
||||
actions.push(new RefreshWidgetAction(this.refresh, this));
|
||||
}
|
||||
if (actions !== undefined && actions.length > 0) {
|
||||
this._actions = actions;
|
||||
this._changeref.detectChanges();
|
||||
}
|
||||
} catch (e) {
|
||||
error('Error rendering widget', key, e);
|
||||
return;
|
||||
}
|
||||
const el = <HTMLElement>componentRef.location.nativeElement;
|
||||
|
||||
// set widget styles to conform to its box
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.flex = '1 1 auto';
|
||||
el.style.position = 'relative';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the selector for a given key, and if none is defined tries
|
||||
* to load it from the widget registry and configure as needed
|
||||
*
|
||||
* @private
|
||||
* @param {string} key
|
||||
* @returns {Type<IDashboardWidget>}
|
||||
* @memberof DashboardWidgetWrapper
|
||||
*/
|
||||
private getOrCreateSelector(key: string): Type<IDashboardWidget> {
|
||||
let selector = componentMap[key];
|
||||
if (selector === undefined) {
|
||||
// Load the widget from the registry
|
||||
const widgetRegistry = <IInsightRegistry>Registry.as(Extensions.InsightContribution);
|
||||
const insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
|
||||
if (insightConfig === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// Save the widget for future use
|
||||
selector = componentMap['insights-widget'];
|
||||
delete this._config.widget[key];
|
||||
this._config.widget['insights-widget'] = insightConfig;
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
const el = <HTMLElement>this._ref.nativeElement;
|
||||
const headerEl: HTMLElement = this.header.nativeElement;
|
||||
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
|
||||
let backgroundColor = theme.getColor(colors.editorBackground, true);
|
||||
const foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
|
||||
const border = theme.getColor(colors.contrastBorder, true);
|
||||
|
||||
if (this._config.background_color) {
|
||||
backgroundColor = theme.getColor(this._config.background_color);
|
||||
}
|
||||
|
||||
if (this._config.border === 'none') {
|
||||
borderColor = undefined;
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
el.style.backgroundColor = backgroundColor.toString();
|
||||
}
|
||||
|
||||
if (foregroundColor) {
|
||||
el.style.color = foregroundColor.toString();
|
||||
}
|
||||
|
||||
let borderString = undefined;
|
||||
if (border) {
|
||||
borderString = border.toString();
|
||||
el.style.borderColor = borderString;
|
||||
el.style.borderWidth = '1px';
|
||||
el.style.borderStyle = 'solid';
|
||||
} else if (borderColor) {
|
||||
borderString = borderColor.toString();
|
||||
el.style.border = '3px solid ' + borderColor.toString();
|
||||
} else {
|
||||
el.style.border = 'none';
|
||||
}
|
||||
|
||||
if (borderString) {
|
||||
headerEl.style.backgroundColor = borderString;
|
||||
} else {
|
||||
headerEl.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
if (this._config.fontSize) {
|
||||
headerEl.style.fontSize = this._config.fontSize;
|
||||
}
|
||||
if (this._config.fontWeight) {
|
||||
headerEl.style.fontWeight = this._config.fontWeight;
|
||||
}
|
||||
if (this._config.padding) {
|
||||
headerEl.style.padding = this._config.padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./webviewContent';
|
||||
|
||||
import { Component, forwardRef, Input, OnInit, Inject, ElementRef } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { IDashboardWebview, IDashboardViewService } from 'sql/platform/dashboard/common/dashboardViewService';
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
selector: 'webview-content'
|
||||
})
|
||||
export class WebviewContent extends AngularDisposable implements OnInit, IDashboardWebview {
|
||||
@Input() private webviewId: string;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
private _onMessage = new Emitter<string>();
|
||||
public readonly onMessage: Event<string> = this._onMessage.event;
|
||||
|
||||
private _onMessageDisposable: IDisposable;
|
||||
private _webview: WebviewElement;
|
||||
private _html: string;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IDashboardViewService) private dashboardViewService: IDashboardViewService,
|
||||
@Inject(IWorkbenchLayoutService) private layoutService: IWorkbenchLayoutService,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dashboardViewService.registerWebview(this);
|
||||
this._createWebview();
|
||||
this._register(addDisposableListener(window, EventType.RESIZE, e => {
|
||||
this.layout();
|
||||
}));
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._webview.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.webviewId;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get connection(): azdata.connection.Connection {
|
||||
const currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
const connection: azdata.connection.Connection = {
|
||||
providerName: currentConnection.providerName,
|
||||
connectionId: currentConnection.id,
|
||||
options: currentConnection.options
|
||||
};
|
||||
return connection;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get serverInfo(): azdata.ServerInfo {
|
||||
return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
|
||||
}
|
||||
|
||||
public setHtml(html: string): void {
|
||||
this._html = html;
|
||||
if (this._webview) {
|
||||
this._webview.contents = html;
|
||||
this._webview.layout();
|
||||
}
|
||||
}
|
||||
|
||||
public sendMessage(message: string): void {
|
||||
if (this._webview) {
|
||||
this._webview.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private _createWebview(): void {
|
||||
if (this._webview) {
|
||||
this._webview.dispose();
|
||||
}
|
||||
|
||||
if (this._onMessageDisposable) {
|
||||
this._onMessageDisposable.dispose();
|
||||
}
|
||||
|
||||
this._webview = this.instantiationService.createInstance(WebviewElement,
|
||||
{},
|
||||
{
|
||||
allowScripts: true
|
||||
});
|
||||
|
||||
this._webview.mountTo(this._el.nativeElement);
|
||||
|
||||
this._onMessageDisposable = this._webview.onMessage(e => {
|
||||
this._onMessage.fire(e);
|
||||
});
|
||||
this._webview.style(this.themeService.getTheme());
|
||||
if (this._html) {
|
||||
this._webview.contents = this._html;
|
||||
}
|
||||
this._webview.layout();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
webview-content {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div class="scroll-container" #scrollContainer>
|
||||
<div class="scrollable" #scrollable>
|
||||
<div [ngGrid]="gridConfig" *ngIf="widgets" >
|
||||
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
|
||||
</dashboard-widget-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,272 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./widgetContent';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, ViewChildren, QueryList, ChangeDetectorRef, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import { NgGridConfig, NgGrid, NgGridItem } from 'angular2-grid';
|
||||
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardWidgetWrapper } from 'sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component';
|
||||
import { subscriptionToDisposable, AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { getContentHeight, addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
|
||||
/**
|
||||
* 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: 'widget-content',
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/contents/widgetContent.component.html'))
|
||||
})
|
||||
export class WidgetContent extends AngularDisposable implements AfterViewInit {
|
||||
@Input() private widgets: WidgetConfig[];
|
||||
@Input() private originalConfig: WidgetConfig[];
|
||||
@Input() private context: string;
|
||||
@Input() private scrollContent = true;
|
||||
|
||||
private _scrollableElement: ScrollableElement;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
protected SKELETON_WIDTH = 5;
|
||||
protected gridConfig: NgGridConfig = {
|
||||
'margins': [10], // The size of the margins of each item. Supports up to four values in the same way as CSS margins. Can be updated using setMargins()
|
||||
'draggable': false, // Whether the items can be dragged. Can be updated using enableDrag()/disableDrag()
|
||||
'resizable': false, // Whether the items can be resized. Can be updated using enableResize()/disableResize()
|
||||
'max_cols': this.SKELETON_WIDTH, // The maximum number of columns allowed. Set to 0 for infinite. Cannot be used with max_rows
|
||||
'max_rows': 0, // The maximum number of rows allowed. Set to 0 for infinite. Cannot be used with max_cols
|
||||
'visible_cols': 0, // The number of columns shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_cols
|
||||
'visible_rows': 0, // The number of rows shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_rows
|
||||
'min_cols': 0, // The minimum number of columns allowed. Can be any number greater than or equal to 1.
|
||||
'min_rows': 0, // The minimum number of rows allowed. Can be any number greater than or equal to 1.
|
||||
'col_width': 250, // The width of each column
|
||||
'row_height': 250, // The height of each row
|
||||
'cascade': 'left', // The direction to cascade grid items ('up', 'right', 'down', 'left')
|
||||
'min_width': 100, // The minimum width of an item. If greater than col_width, this will update the value of min_cols
|
||||
'min_height': 100, // The minimum height of an item. If greater than row_height, this will update the value of min_rows
|
||||
'fix_to_grid': false, // Fix all item movements to the grid
|
||||
'auto_style': true, // Automatically add required element styles at run-time
|
||||
'auto_resize': false, // Automatically set col_width/row_height so that max_cols/max_rows fills the screen. Only has effect is max_cols or max_rows is set
|
||||
'maintain_ratio': false, // Attempts to maintain aspect ratio based on the colWidth/rowHeight values set in the config
|
||||
'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 _editDispose: Array<IDisposable> = [];
|
||||
|
||||
@ViewChild(NgGrid) private _grid: NgGrid;
|
||||
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
|
||||
@ViewChildren(NgGridItem) private _items: QueryList<NgGridItem>;
|
||||
@ViewChild('scrollable', { read: ElementRef }) private _scrollable: ElementRef;
|
||||
@ViewChild('scrollContainer', { read: ElementRef }) private _scrollContainer: ElementRef;
|
||||
|
||||
protected dashboardService: DashboardServiceInterface;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected commonService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
this.dashboardService = commonService as DashboardServiceInterface;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.scrollContent) {
|
||||
const container = this._scrollContainer.nativeElement as HTMLElement;
|
||||
const scrollable = this._scrollable.nativeElement as HTMLElement;
|
||||
container.removeChild(scrollable);
|
||||
|
||||
this._scrollableElement = new ScrollableElement(scrollable, {
|
||||
horizontal: ScrollbarVisibility.Hidden,
|
||||
vertical: ScrollbarVisibility.Auto,
|
||||
useShadows: false
|
||||
});
|
||||
|
||||
this._scrollableElement.onScroll(e => {
|
||||
scrollable.style.bottom = e.scrollTop + 'px';
|
||||
});
|
||||
|
||||
container.appendChild(this._scrollableElement.getDomNode());
|
||||
const initalHeight = getContentHeight(scrollable);
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(scrollable),
|
||||
height: getContentHeight(container)
|
||||
});
|
||||
|
||||
this._register(addDisposableListener(window, EventType.RESIZE, () => {
|
||||
this.resetScrollDimensions();
|
||||
}));
|
||||
|
||||
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
|
||||
setTimeout(() => {
|
||||
const currentheight = getContentHeight(scrollable);
|
||||
if (initalHeight !== currentheight) {
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: currentheight,
|
||||
height: getContentHeight(container)
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
public layout() {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.layout();
|
||||
});
|
||||
}
|
||||
this._grid.triggerResize();
|
||||
if (this.scrollContent) {
|
||||
this.resetScrollDimensions();
|
||||
}
|
||||
}
|
||||
|
||||
private resetScrollDimensions() {
|
||||
const container = this._scrollContainer.nativeElement as HTMLElement;
|
||||
const scrollable = this._scrollable.nativeElement as HTMLElement;
|
||||
|
||||
this._scrollableElement.setScrollDimensions({
|
||||
scrollHeight: getContentHeight(scrollable),
|
||||
height: getContentHeight(container)
|
||||
});
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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._onResize.fire();
|
||||
const event = e.getEventOutput();
|
||||
const 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;
|
||||
|
||||
const component = this._widgets.find(i => i.id === event.payload.id);
|
||||
|
||||
component.layout();
|
||||
this._rewriteConfig();
|
||||
this.resetScrollDimensions();
|
||||
})));
|
||||
this._editDispose.push(subscriptionToDisposable(this._grid.onDragStop.subscribe((e: NgGridItem) => {
|
||||
this._onResize.fire();
|
||||
const event = e.getEventOutput();
|
||||
this._items.forEach(i => {
|
||||
const 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.resetScrollDimensions();
|
||||
})));
|
||||
this._widgets.forEach(i => {
|
||||
if (i.id) {
|
||||
i.enableEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _rewriteConfig(): void {
|
||||
const writeableConfig = objects.deepClone(this.originalConfig);
|
||||
|
||||
writeableConfig.forEach(i => {
|
||||
delete i.id;
|
||||
});
|
||||
const target: ConfigurationTarget = ConfigurationTarget.USER;
|
||||
this.dashboardService.writeSettings([this.context, 'widgets'].join('.'), writeableConfig, target);
|
||||
}
|
||||
}
|
||||
18
src/sql/workbench/parts/dashboard/contents/widgetContent.css
Normal file
18
src/sql/workbench/parts/dashboard/contents/widgetContent.css
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
widget-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
position: relative;
|
||||
}
|
||||
18
src/sql/workbench/parts/dashboard/dashboard.component.html
Normal file
18
src/sql/workbench/parts/dashboard/dashboard.component.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div #header class="header" style="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>
|
||||
<div style="flex: 1 1 auto; position: relative">
|
||||
<router-outlet (activate)="onActivate($event)"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
95
src/sql/workbench/parts/dashboard/dashboard.component.ts
Normal file
95
src/sql/workbench/parts/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dashboard';
|
||||
|
||||
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import * as Utils from 'sql/platform/connection/common/utils';
|
||||
import { RefreshWidgetAction, EditDashboardAction } from 'sql/workbench/parts/dashboard/common/actions';
|
||||
import { DashboardPage } from 'sql/workbench/parts/dashboard/common/dashboardPage.component';
|
||||
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
export const DASHBOARD_SELECTOR: string = 'dashboard-component';
|
||||
|
||||
@Component({
|
||||
selector: DASHBOARD_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./dashboard.component.html'))
|
||||
})
|
||||
export class DashboardComponent extends AngularDisposable implements OnInit {
|
||||
private _currentPage: DashboardPage;
|
||||
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
@ViewChild('actionBar', { read: ElementRef }) private actionbarContainer: ElementRef;
|
||||
private actionbar: ActionBar;
|
||||
private editAction: EditDashboardAction;
|
||||
private editDisposable: IDisposable;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
const 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.editAction = new EditDashboardAction(this.edit, this);
|
||||
this.actionbar.push(this.editAction, {
|
||||
icon: true,
|
||||
label: false,
|
||||
});
|
||||
if (profile && (!profile.databaseName || Utils.isMaster(profile))) {
|
||||
// Route to the server page as this is the default database
|
||||
this._router.navigate(['server-dashboard']);
|
||||
}
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
const 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) {
|
||||
if (this.editDisposable) {
|
||||
this.editDisposable.dispose();
|
||||
}
|
||||
this._currentPage = page;
|
||||
this.editDisposable = page.editEnabled(e => this.editEnabled = e, this);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
if (this._currentPage) {
|
||||
this._currentPage.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
this._currentPage.enableEdit();
|
||||
}
|
||||
|
||||
set editEnabled(val: boolean) {
|
||||
this.editAction.enabled = val;
|
||||
}
|
||||
}
|
||||
16
src/sql/workbench/parts/dashboard/dashboard.css
Normal file
16
src/sql/workbench/parts/dashboard/dashboard.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.dashboardEditor .header .monaco-action-bar .action-label {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dashboardEditor .header .monaco-action-bar .action-item {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.dashboardEditor .monaco-action-bar {
|
||||
overflow: visible;
|
||||
}
|
||||
180
src/sql/workbench/parts/dashboard/dashboard.module.ts
Normal file
180
src/sql/workbench/parts/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Inject, NgModule, forwardRef, ApplicationRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule, Routes, UrlSerializer, Router, NavigationEnd } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgGridModule } from 'angular2-grid';
|
||||
import { ChartsModule } from 'ng2-charts';
|
||||
|
||||
import CustomUrlSerializer from 'sql/base/node/urlSerializer';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { Extensions as ComponentExtensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
|
||||
import { IBootstrapParams, ISelector, providerIterator } from 'sql/services/bootstrap/bootstrapService';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
/* Telemetry */
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
|
||||
/* Services */
|
||||
import { BreadcrumbService } from 'sql/workbench/parts/dashboard/services/breadcrumb.service';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
|
||||
/* Directives */
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
|
||||
|
||||
/* Base Components */
|
||||
import { DashboardComponent } from 'sql/workbench/parts/dashboard/dashboard.component';
|
||||
import { DashboardWidgetWrapper } from 'sql/workbench/parts/dashboard/contents/dashboardWidgetWrapper.component';
|
||||
import { DashboardWidgetContainer } from 'sql/workbench/parts/dashboard/containers/dashboardWidgetContainer.component';
|
||||
import { DashboardGridContainer } from 'sql/workbench/parts/dashboard/containers/dashboardGridContainer.component';
|
||||
import { DashboardWebviewContainer } from 'sql/workbench/parts/dashboard/containers/dashboardWebviewContainer.component';
|
||||
import { DashboardModelViewContainer } from 'sql/workbench/parts/dashboard/containers/dashboardModelViewContainer.component';
|
||||
import { DashboardErrorContainer } from 'sql/workbench/parts/dashboard/containers/dashboardErrorContainer.component';
|
||||
import { DashboardNavSection } from 'sql/workbench/parts/dashboard/containers/dashboardNavSection.component';
|
||||
import { WidgetContent } from 'sql/workbench/parts/dashboard/contents/widgetContent.component';
|
||||
import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component';
|
||||
import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component';
|
||||
import { WebviewContent } from 'sql/workbench/parts/dashboard/contents/webviewContent.component';
|
||||
import { BreadcrumbComponent } from 'sql/base/browser/ui/breadcrumb/breadcrumb.component';
|
||||
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { DashboardHomeContainer } from 'sql/workbench/parts/dashboard/containers/dashboardHomeContainer.component';
|
||||
import { ControlHostContent } from 'sql/workbench/parts/dashboard/contents/controlHostContent.component';
|
||||
import { DashboardControlHostContainer } from 'sql/workbench/parts/dashboard/containers/dashboardControlHostContainer.component';
|
||||
import { JobsViewComponent } from 'sql/parts/jobManagement/views/jobsView.component';
|
||||
import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component';
|
||||
import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.component';
|
||||
import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component';
|
||||
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
|
||||
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component';
|
||||
import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component';
|
||||
import LoadingSpinner from 'sql/parts/modelComponents/loadingSpinner.component';
|
||||
|
||||
const baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer,
|
||||
DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent,
|
||||
ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer,
|
||||
JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, AlertsViewComponent, ProxiesViewComponent, OperatorsViewComponent,
|
||||
DashboardModelViewContainer, ModelComponentWrapper, Checkbox, EditableDropDown, SelectBox, InputBox, LoadingSpinner];
|
||||
|
||||
/* Panel */
|
||||
import { PanelModule } from 'sql/base/browser/ui/panel/panel.module';
|
||||
|
||||
import { ScrollableModule } from 'sql/base/browser/ui/scrollable/scrollable.module';
|
||||
|
||||
/* Pages */
|
||||
import { ServerDashboardPage } from 'sql/workbench/parts/dashboard/pages/serverDashboardPage.component';
|
||||
import { DatabaseDashboardPage } from 'sql/workbench/parts/dashboard/pages/databaseDashboardPage.component';
|
||||
|
||||
const pageComponents = [ServerDashboardPage, DatabaseDashboardPage];
|
||||
|
||||
/* Widget Components */
|
||||
import { PropertiesWidgetComponent } from 'sql/workbench/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
import { ExplorerWidget } from 'sql/workbench/parts/dashboard/widgets/explorer/explorerWidget.component';
|
||||
import { TasksWidget } from 'sql/workbench/parts/dashboard/widgets/tasks/tasksWidget.component';
|
||||
import { InsightsWidget } from 'sql/workbench/parts/dashboard/widgets/insights/insightsWidget.component';
|
||||
import { WebviewWidget } from 'sql/workbench/parts/dashboard/widgets/webview/webviewWidget.component';
|
||||
import { JobStepsViewComponent } from 'sql/parts/jobManagement/views/jobStepsView.component';
|
||||
import { IInstantiationService, _util } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
const widgetComponents = [
|
||||
PropertiesWidgetComponent,
|
||||
ExplorerWidget,
|
||||
TasksWidget,
|
||||
InsightsWidget,
|
||||
WebviewWidget
|
||||
];
|
||||
|
||||
/* Insights */
|
||||
const insightComponents = Registry.as<IInsightRegistry>(Extensions.InsightContribution).getAllCtors();
|
||||
|
||||
/* Model-backed components */
|
||||
const extensionComponents = Registry.as<IComponentRegistry>(ComponentExtensions.ComponentContribution).getAllCtors();
|
||||
|
||||
// Setup routes for various child components
|
||||
const appRoutes: Routes = [
|
||||
{ path: 'database-dashboard', component: DatabaseDashboardPage },
|
||||
{ path: 'server-dashboard', component: ServerDashboardPage },
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'database-dashboard',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{ path: '**', component: DatabaseDashboardPage }
|
||||
];
|
||||
|
||||
// Connection Dashboard main angular module
|
||||
export const DashboardModule = (params, selector: string, instantiationService: IInstantiationService): any => {
|
||||
@NgModule({
|
||||
declarations: [
|
||||
...baseComponents,
|
||||
...pageComponents,
|
||||
...widgetComponents,
|
||||
...insightComponents,
|
||||
...extensionComponents
|
||||
],
|
||||
// also for widgets
|
||||
entryComponents: [
|
||||
DashboardComponent,
|
||||
...widgetComponents,
|
||||
...insightComponents,
|
||||
...extensionComponents
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
NgGridModule,
|
||||
ChartsModule,
|
||||
RouterModule.forRoot(appRoutes),
|
||||
PanelModule,
|
||||
ScrollableModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
{ provide: IBreadcrumbService, useClass: BreadcrumbService },
|
||||
{ provide: CommonServiceInterface, useClass: DashboardServiceInterface },
|
||||
{ provide: UrlSerializer, useClass: CustomUrlSerializer },
|
||||
{ provide: IBootstrapParams, useValue: params },
|
||||
{ provide: ISelector, useValue: selector },
|
||||
...providerIterator(instantiationService)
|
||||
]
|
||||
})
|
||||
class ModuleClass {
|
||||
private navigations = 0;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(ITelemetryService) private telemetryService: ITelemetryService,
|
||||
@Inject(ISelector) private selector: string
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factory = this._resolver.resolveComponentFactory(DashboardComponent);
|
||||
(<any>factory).factory.selector = this.selector;
|
||||
appRef.bootstrap(factory);
|
||||
|
||||
this._router.events.subscribe(e => {
|
||||
if (e instanceof NavigationEnd) {
|
||||
this.navigations++;
|
||||
TelemetryUtils.addTelemetry(this.telemetryService, TelemetryKeys.DashboardNavigated, {
|
||||
numberOfNavigations: this.navigations
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ModuleClass;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { DASHBOARD_CONFIG_ID } from 'sql/workbench/parts/dashboard/pages/dashboardPageContribution';
|
||||
import { DATABASE_DASHBOARD_SETTING, DATABASE_DASHBOARD_PROPERTIES, DATABASE_DASHBOARD_TABS, databaseDashboardSettingSchema, databaseDashboardPropertiesSchema, databaseDashboardTabsSchema } from 'sql/workbench/parts/dashboard/pages/databaseDashboardPage.contribution';
|
||||
import { SERVER_DASHBOARD_SETTING, SERVER_DASHBOARD_PROPERTIES, SERVER_DASHBOARD_TABS, serverDashboardSettingSchema, serverDashboardPropertiesSchema, serverDashboardTabsSchema } from 'sql/workbench/parts/dashboard/pages/serverDashboardPage.contribution';
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
const dashboardConfig: IConfigurationNode = {
|
||||
id: DASHBOARD_CONFIG_ID,
|
||||
type: 'object',
|
||||
properties: {
|
||||
[DATABASE_DASHBOARD_PROPERTIES]: databaseDashboardPropertiesSchema,
|
||||
[SERVER_DASHBOARD_PROPERTIES]: serverDashboardPropertiesSchema,
|
||||
[DATABASE_DASHBOARD_SETTING]: databaseDashboardSettingSchema,
|
||||
[SERVER_DASHBOARD_SETTING]: serverDashboardSettingSchema,
|
||||
[DATABASE_DASHBOARD_TABS]: databaseDashboardTabsSchema,
|
||||
[SERVER_DASHBOARD_TABS]: serverDashboardTabsSchema
|
||||
}
|
||||
};
|
||||
|
||||
configurationRegistry.registerConfiguration(dashboardConfig);
|
||||
140
src/sql/workbench/parts/dashboard/dashboardEditor.ts
Normal file
140
src/sql/workbench/parts/dashboard/dashboardEditor.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
import { DashboardInput } from './dashboardInput';
|
||||
import { DashboardModule } from './dashboard.module';
|
||||
import { bootstrapAngular } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { IDashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { DASHBOARD_SELECTOR } from 'sql/workbench/parts/dashboard/dashboard.component';
|
||||
import { ConnectionContextKey } from 'sql/parts/connection/common/connectionContextKey';
|
||||
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { $ } from 'sql/base/browser/builder';
|
||||
|
||||
export class DashboardEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.connectiondashboard';
|
||||
private _dashboardContainer: HTMLElement;
|
||||
protected _input: DashboardInput;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IContextKeyService private _contextKeyService: IContextKeyService,
|
||||
@IDashboardService private _dashboardService: IDashboardService,
|
||||
@IConnectionManagementService private _connMan: IConnectionManagementService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(DashboardEditor.ID, telemetryService, themeService, storageService);
|
||||
}
|
||||
|
||||
public get input(): DashboardInput {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the editor in the parent element.
|
||||
*/
|
||||
public createEditor(parent: HTMLElement): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
||||
*/
|
||||
public focus(): void {
|
||||
|
||||
let profile: IConnectionProfile;
|
||||
if (this.input.connectionProfile instanceof ConnectionProfile) {
|
||||
profile = this.input.connectionProfile.toIConnectionProfile();
|
||||
} else {
|
||||
profile = this.input.connectionProfile;
|
||||
}
|
||||
const serverInfo = this._connMan.getConnectionInfo(this.input.uri).serverInfo;
|
||||
this._dashboardService.changeToDashboard({ profile, serverInfo });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
|
||||
* To be called when the container of this editor changes size.
|
||||
*/
|
||||
public layout(dimension: DOM.Dimension): void {
|
||||
this._dashboardService.layout(dimension);
|
||||
}
|
||||
|
||||
public setInput(input: DashboardInput, options: EditorOptions): Promise<void> {
|
||||
if (this.input && this.input.matches(input)) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const parentElement = this.getContainer();
|
||||
|
||||
super.setInput(input, options, CancellationToken.None);
|
||||
|
||||
$(parentElement).clearChildren();
|
||||
|
||||
if (!input.hasBootstrapped) {
|
||||
const container = DOM.$<HTMLElement>('.dashboardEditor');
|
||||
container.style.height = '100%';
|
||||
this._dashboardContainer = DOM.append(parentElement, container);
|
||||
this.input.container = this._dashboardContainer;
|
||||
return Promise.resolve(input.initializedPromise.then(() => this.bootstrapAngular(input)));
|
||||
} else {
|
||||
this._dashboardContainer = DOM.append(parentElement, this.input.container);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private bootstrapAngular(input: DashboardInput): void {
|
||||
// Get the bootstrap params and perform the bootstrap
|
||||
let profile: IConnectionProfile;
|
||||
if (input.connectionProfile instanceof ConnectionProfile) {
|
||||
profile = input.connectionProfile.toIConnectionProfile();
|
||||
} else {
|
||||
profile = this.input.connectionProfile;
|
||||
}
|
||||
const serverInfo = this._connMan.getConnectionInfo(this.input.uri).serverInfo;
|
||||
this._dashboardService.changeToDashboard({ profile, serverInfo });
|
||||
const scopedContextService = this._contextKeyService.createScoped(input.container);
|
||||
const connectionContextKey = new ConnectionContextKey(scopedContextService);
|
||||
connectionContextKey.set(input.connectionProfile);
|
||||
|
||||
const params: IDashboardComponentParams = {
|
||||
connection: input.connectionProfile,
|
||||
ownerUri: input.uri,
|
||||
scopedContextService,
|
||||
connectionContextKey
|
||||
};
|
||||
|
||||
input.hasBootstrapped = true;
|
||||
|
||||
const uniqueSelector = bootstrapAngular(this.instantiationService,
|
||||
DashboardModule,
|
||||
this._dashboardContainer,
|
||||
DASHBOARD_SELECTOR,
|
||||
params,
|
||||
input);
|
||||
input.setUniqueSelector(uniqueSelector);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
172
src/sql/workbench/parts/dashboard/dashboardInput.ts
Normal file
172
src/sql/workbench/parts/dashboard/dashboardInput.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EditorInput, EditorModel } from 'vs/workbench/common/editor';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
|
||||
export class DashboardInput extends EditorInput {
|
||||
|
||||
private _uri: string;
|
||||
public static ID: string = 'workbench.editorinputs.connectiondashboardinputs';
|
||||
public static SCHEMA: string = 'sqldashboard';
|
||||
|
||||
private _initializedPromise: Thenable<void>;
|
||||
private _onConnectionChanged: IDisposable;
|
||||
|
||||
public get initializedPromise(): Thenable<void> {
|
||||
return this._initializedPromise;
|
||||
}
|
||||
|
||||
private _uniqueSelector: string;
|
||||
|
||||
public hasBootstrapped = false;
|
||||
// Holds the HTML content for the editor when the editor discards this input and loads another
|
||||
private _parentContainer: HTMLElement;
|
||||
|
||||
constructor(
|
||||
_connectionProfile: IConnectionProfile,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService model: IModelService
|
||||
) {
|
||||
super();
|
||||
// TODO; possible refactor
|
||||
// basically this is mimicing creating a "model" (the backing model for text for editors)
|
||||
// for dashboard, even though there is no backing text. We need this so that we can
|
||||
// tell the icon theme services that we are a dashboard resource, therefore loading the correct icon
|
||||
|
||||
// vscode has a comment that Mode's will eventually be removed (not sure the state of this comment)
|
||||
// so this might be able to be undone when that happens
|
||||
if (!model.getModel(this.getResource())) {
|
||||
model.createModel('', modeService.create('dashboard'), this.getResource());
|
||||
}
|
||||
this._initializedPromise = _connectionService.connectIfNotConnected(_connectionProfile, 'dashboard').then(
|
||||
u => {
|
||||
this._uri = u;
|
||||
const info = this._connectionService.getConnectionInfo(u);
|
||||
if (info) {
|
||||
this._onConnectionChanged = this._connectionService.onConnectionChanged(e => {
|
||||
if (e.connectionUri === u) {
|
||||
this._onDidChangeLabel.fire();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public setUniqueSelector(uniqueSelector: string): void {
|
||||
this._uniqueSelector = uniqueSelector;
|
||||
}
|
||||
|
||||
public getTypeId(): string {
|
||||
return DashboardInput.ID;
|
||||
}
|
||||
|
||||
public getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'dashboard',
|
||||
path: 'dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
if (!this.connectionProfile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let name = this.connectionProfile.connectionName ? this.connectionProfile.connectionName : this.connectionProfile.serverName;
|
||||
if (this.connectionProfile.databaseName
|
||||
&& !this.isMasterMssql()) {
|
||||
// Only add DB name if this is a non-default, non-master connection
|
||||
name = name + ':' + this.connectionProfile.databaseName;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private isMasterMssql(): boolean {
|
||||
return this.connectionProfile.providerName.toLowerCase() === 'mssql'
|
||||
&& this.connectionProfile.databaseName.toLowerCase() === 'master';
|
||||
}
|
||||
|
||||
public get uri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposeContainer();
|
||||
if (this._onConnectionChanged) {
|
||||
this._onConnectionChanged.dispose();
|
||||
}
|
||||
this._connectionService.disconnect(this._uri);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _disposeContainer() {
|
||||
if (!this._parentContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentNode = this._parentContainer.parentNode;
|
||||
if (parentNode) {
|
||||
parentNode.removeChild(this._parentContainer);
|
||||
this._parentContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
set container(container: HTMLElement) {
|
||||
this._disposeContainer();
|
||||
this._parentContainer = container;
|
||||
}
|
||||
|
||||
get container(): HTMLElement {
|
||||
return this._parentContainer;
|
||||
}
|
||||
|
||||
public supportsSplitEditor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public get connectionProfile(): IConnectionProfile {
|
||||
return this._connectionService.getConnectionProfile(this._uri);
|
||||
}
|
||||
|
||||
public resolve(refresh?: boolean): Promise<EditorModel> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get hasInitialized(): boolean {
|
||||
return !!this._uniqueSelector;
|
||||
}
|
||||
|
||||
public get uniqueSelector(): string {
|
||||
return this._uniqueSelector;
|
||||
}
|
||||
|
||||
public matches(otherinput: any): boolean {
|
||||
return otherinput instanceof DashboardInput
|
||||
&& DashboardInput.profileMatches(this.connectionProfile, otherinput.connectionProfile);
|
||||
}
|
||||
|
||||
// similar to the default profile match but without databasename
|
||||
public static profileMatches(profile1: IConnectionProfile, profile2: IConnectionProfile): boolean {
|
||||
return profile1 && profile2
|
||||
&& profile1.providerName === profile2.providerName
|
||||
&& profile1.serverName === profile2.serverName
|
||||
&& profile1.userName === profile2.userName
|
||||
&& profile1.authenticationType === profile2.authenticationType
|
||||
&& profile1.groupFullName === profile2.groupFullName;
|
||||
}
|
||||
|
||||
public get tabColor(): string {
|
||||
return this._connectionService.getTabColorForUri(this.uri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Extensions, IDashboardWidgetRegistry } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
let widgetRegistry = <IDashboardWidgetRegistry>Registry.as(Extensions.DashboardWidgetContribution);
|
||||
|
||||
export function generateDashboardWidgetSchema(type?: 'database' | 'server', extension?: boolean): IJSONSchema {
|
||||
let schemas;
|
||||
if (extension) {
|
||||
const extensionSchemas = type === 'server' ? widgetRegistry.serverWidgetSchema.extensionProperties : type === 'database' ? widgetRegistry.databaseWidgetSchema.extensionProperties : widgetRegistry.allSchema.extensionProperties;
|
||||
schemas = type === 'server' ? widgetRegistry.serverWidgetSchema.properties : type === 'database' ? widgetRegistry.databaseWidgetSchema.properties : widgetRegistry.allSchema.properties;
|
||||
schemas = mixin(schemas, extensionSchemas, true);
|
||||
} else {
|
||||
schemas = type === 'server' ? widgetRegistry.serverWidgetSchema.properties : type === 'database' ? widgetRegistry.databaseWidgetSchema.properties : widgetRegistry.allSchema.properties;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
},
|
||||
when: {
|
||||
description: localize('azdata.extension.contributes.widget.when', 'Condition which must be true to show this item'),
|
||||
type: 'string'
|
||||
},
|
||||
gridItemConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sizex: {
|
||||
type: 'number'
|
||||
},
|
||||
sizey: {
|
||||
type: 'number'
|
||||
},
|
||||
col: {
|
||||
type: 'number'
|
||||
},
|
||||
row: {
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
},
|
||||
widget: {
|
||||
type: 'object',
|
||||
properties: schemas,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDashboardGridLayoutSchema(type?: 'database' | 'server', extension?: boolean): IJSONSchema {
|
||||
let schemas;
|
||||
if (extension) {
|
||||
const extensionSchemas = type === 'server' ? widgetRegistry.serverWidgetSchema.extensionProperties : type === 'database' ? widgetRegistry.databaseWidgetSchema.extensionProperties : widgetRegistry.allSchema.extensionProperties;
|
||||
schemas = type === 'server' ? widgetRegistry.serverWidgetSchema.properties : type === 'database' ? widgetRegistry.databaseWidgetSchema.properties : widgetRegistry.allSchema.properties;
|
||||
schemas = mixin(schemas, extensionSchemas, true);
|
||||
} else {
|
||||
schemas = type === 'server' ? widgetRegistry.serverWidgetSchema.properties : type === 'database' ? widgetRegistry.databaseWidgetSchema.properties : widgetRegistry.allSchema.properties;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: localize('dashboardpage.tabName', "The title of the container")
|
||||
},
|
||||
row: {
|
||||
type: 'number',
|
||||
description: localize('dashboardpage.rowNumber', "The row of the component in the grid")
|
||||
},
|
||||
rowspan: {
|
||||
type: ['string', 'number'],
|
||||
description: localize('dashboardpage.rowSpan', "The rowspan of the component in the grid. Default value is 1. Use '*' to set to number of rows in the grid.")
|
||||
},
|
||||
col: {
|
||||
type: 'number',
|
||||
description: localize('dashboardpage.colNumber', "The column of the component in the grid")
|
||||
},
|
||||
colspan: {
|
||||
type: ['string', 'number'],
|
||||
description: localize('dashboardpage.colspan', "The colspan of the component in the grid. Default value is 1. Use '*' to set to number of columns in the grid.")
|
||||
},
|
||||
widget: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: schemas,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
webview: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
when: {
|
||||
description: localize('azdata.extension.contributes.widget.when', 'Condition which must be true to show this item'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDashboardTabSchema(type?: 'database' | 'server'): IJSONSchema {
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tabId: {
|
||||
type: 'string',
|
||||
description: localize('azdata.extension.contributes.dashboardPage.tab.id', "Unique identifier for this tab. Will be passed to the extension for any requests."),
|
||||
enum: [],
|
||||
enumDescriptions: [],
|
||||
errorMessage: localize('dashboardTabError', "Extension tab is unknown or not installed.")
|
||||
},
|
||||
isPinned: {
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const DASHBOARD_CONFIG_ID = 'Dashboard';
|
||||
export const DASHBOARD_TABS_KEY_PROPERTY = 'tabId';
|
||||
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/workbench/parts/dashboard/common/dashboardPage.component';
|
||||
import { BreadcrumbClass } from 'sql/workbench/parts/dashboard/services/breadcrumb.service';
|
||||
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export class DatabaseDashboardPage extends DashboardPage implements OnInit {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('databasePageName', 'DATABASE DASHBOARD'),
|
||||
widget: {
|
||||
'properties-widget': undefined
|
||||
},
|
||||
context: 'database',
|
||||
background_color: colors.editorBackground,
|
||||
border: 'none',
|
||||
fontSize: '14px',
|
||||
padding: '5px 0 0 0',
|
||||
provider: undefined,
|
||||
edition: undefined
|
||||
};
|
||||
|
||||
protected readonly context = 'database';
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private _breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(IInstantiationService) instantiationService: IInstantiationService,
|
||||
@Inject(INotificationService) notificationService: INotificationService,
|
||||
@Inject(IAngularEventingService) angularEventingService: IAngularEventingService,
|
||||
@Inject(IConfigurationService) configurationService: IConfigurationService
|
||||
) {
|
||||
super(dashboardService, el, _cd, instantiationService, notificationService, angularEventingService, configurationService);
|
||||
this._register(dashboardService.onUpdatePage(() => {
|
||||
this.refresh(true);
|
||||
this._cd.detectChanges();
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.init();
|
||||
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { generateDashboardWidgetSchema, generateDashboardTabSchema } from './dashboardPageContribution';
|
||||
|
||||
export const databaseDashboardPropertiesSchema: IJSONSchema = {
|
||||
description: nls.localize('dashboardDatabaseProperties', 'Enable or disable the properties widget'),
|
||||
default: true,
|
||||
oneOf: <IJSONSchema[]>[
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'string',
|
||||
enum: ['collapsed']
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string'
|
||||
},
|
||||
edition: {
|
||||
type: 'number'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('dashboard.databaseproperties', 'Property values to show'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.databaseproperties.displayName', 'Display name of the property')
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.databaseproperties.value', 'Value in the Database Info Object')
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.databaseproperties.ignore', 'Specify specific values to ignore'),
|
||||
items: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
displayName: nls.localize('recoveryModel', 'Recovery Model'),
|
||||
value: 'recoveryModel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastDatabaseBackup', 'Last Database Backup'),
|
||||
value: 'lastBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastLogBackup', 'Last Log Backup'),
|
||||
value: 'lastLogBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const databaseDashboardSettingSchema: IJSONSchema = {
|
||||
type: ['array'],
|
||||
description: nls.localize('dashboardDatabase', 'Customizes the database dashboard page'),
|
||||
items: generateDashboardWidgetSchema('database'),
|
||||
default: [
|
||||
{
|
||||
name: 'Tasks',
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 1
|
||||
},
|
||||
widget: {
|
||||
'tasks-widget': [{ name: 'backup', when: '!mssql:iscloud' }, { name: 'restore', when: '!mssql:iscloud' }, 'configureDashboard', 'newQuery']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 2
|
||||
},
|
||||
widget: {
|
||||
'explorer-widget': {}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const databaseDashboardTabsSchema: IJSONSchema = {
|
||||
type: ['array'],
|
||||
description: nls.localize('dashboardDatabaseTabs', 'Customizes the database dashboard tabs'),
|
||||
items: generateDashboardTabSchema('database'),
|
||||
default: [
|
||||
]
|
||||
};
|
||||
|
||||
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
|
||||
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
|
||||
export const DATABASE_DASHBOARD_TABS = 'dashboard.database.tabs';
|
||||
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/workbench/parts/dashboard/common/dashboardPage.component';
|
||||
import { BreadcrumbClass } from 'sql/workbench/parts/dashboard/services/breadcrumb.service';
|
||||
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export class ServerDashboardPage extends DashboardPage implements OnInit {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('serverPageName', 'SERVER DASHBOARD'),
|
||||
widget: {
|
||||
'properties-widget': undefined
|
||||
},
|
||||
context: 'server',
|
||||
background_color: colors.editorBackground,
|
||||
border: 'none',
|
||||
fontSize: '14px',
|
||||
padding: '5px 0 0 0',
|
||||
provider: undefined,
|
||||
edition: undefined
|
||||
};
|
||||
|
||||
protected readonly context = 'server';
|
||||
private _letDashboardPromise: Promise<void>;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(IInstantiationService) instantiationService: IInstantiationService,
|
||||
@Inject(INotificationService) notificationService: INotificationService,
|
||||
@Inject(IAngularEventingService) angularEventingService: IAngularEventingService,
|
||||
@Inject(IConfigurationService) configurationService: IConfigurationService
|
||||
) {
|
||||
super(dashboardService, el, _cd, instantiationService, notificationService, angularEventingService, configurationService);
|
||||
|
||||
// special-case handling for MSSQL data provider
|
||||
const connInfo = this.dashboardService.connectionManagementService.connectionInfo;
|
||||
if (connInfo && connInfo.providerId === 'MSSQL') {
|
||||
// revert back to default database
|
||||
this._letDashboardPromise = this.dashboardService.connectionManagementService.changeDatabase('master').then();
|
||||
} else {
|
||||
this._letDashboardPromise = Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._letDashboardPromise.then(() => {
|
||||
this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
|
||||
this.init();
|
||||
this._cd.detectChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { generateDashboardWidgetSchema, generateDashboardTabSchema } from 'sql/workbench/parts/dashboard/pages/dashboardPageContribution';
|
||||
|
||||
export interface IPropertiesConfig {
|
||||
edition: number | Array<number>;
|
||||
provider: string | Array<string>;
|
||||
properties: {
|
||||
displayName: string;
|
||||
value: string
|
||||
}[];
|
||||
}
|
||||
|
||||
export const serverDashboardPropertiesSchema: IJSONSchema = {
|
||||
description: nls.localize('dashboardServerProperties', 'Enable or disable the properties widget'),
|
||||
default: true,
|
||||
oneOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'string',
|
||||
enum: ['collapsed']
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string'
|
||||
},
|
||||
edition: {
|
||||
type: 'number'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('dashboard.serverproperties', 'Property values to show'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.serverproperties.displayName', 'Display name of the property')
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.serverproperties.value', 'Value in the Server Info Object')
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('edition', 'Edition'),
|
||||
value: 'serverEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('computerName', 'Computer Name'),
|
||||
value: 'machineName'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('osVersion', 'OS Version'),
|
||||
value: 'osVersion'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const defaultVal = [
|
||||
{
|
||||
name: 'Tasks',
|
||||
widget: {
|
||||
'tasks-widget': [{ name: 'restore', when: '!mssql:iscloud' }, 'configureDashboard', 'newQuery']
|
||||
},
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 2
|
||||
},
|
||||
widget: {
|
||||
'explorer-widget': {}
|
||||
}
|
||||
},
|
||||
{
|
||||
widget: {
|
||||
'backup-history-server-insight': null
|
||||
}
|
||||
},
|
||||
{
|
||||
widget: {
|
||||
'all-database-size-server-insight': null
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const serverDashboardSettingSchema: IJSONSchema = {
|
||||
type: ['array'],
|
||||
description: nls.localize('dashboardServer', 'Customizes the server dashboard page'),
|
||||
items: generateDashboardWidgetSchema('server'),
|
||||
default: defaultVal
|
||||
};
|
||||
|
||||
export const serverDashboardTabsSchema: IJSONSchema = {
|
||||
type: ['array'],
|
||||
description: nls.localize('dashboardServerTabs', 'Customizes the Server dashboard tabs'),
|
||||
items: generateDashboardTabSchema('server'),
|
||||
default: []
|
||||
};
|
||||
|
||||
export const SERVER_DASHBOARD_SETTING = 'dashboard.server.widgets';
|
||||
export const SERVER_DASHBOARD_PROPERTIES = 'dashboard.server.properties';
|
||||
export const SERVER_DASHBOARD_TABS = 'dashboard.server.tabs';
|
||||
@@ -0,0 +1,69 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Injectable, forwardRef, Inject } from '@angular/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { DashboardServiceInterface } from './dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { MenuItem, IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export enum BreadcrumbClass {
|
||||
DatabasePage,
|
||||
ServerPage
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BreadcrumbService implements IBreadcrumbService {
|
||||
public breadcrumbItem: Subject<MenuItem[]>;
|
||||
private itemBreadcrums: MenuItem[];
|
||||
private _currentPage: BreadcrumbClass;
|
||||
|
||||
constructor(@Inject(forwardRef(() => CommonServiceInterface)) private commonService: DashboardServiceInterface) {
|
||||
this.commonService.onUpdatePage(() => {
|
||||
this.setBreadcrumbs(this._currentPage);
|
||||
});
|
||||
this.breadcrumbItem = new Subject<MenuItem[]>();
|
||||
}
|
||||
|
||||
public setBreadcrumbs(page: BreadcrumbClass) {
|
||||
this._currentPage = page;
|
||||
this.itemBreadcrums = [];
|
||||
const refList: MenuItem[] = this.getBreadcrumbsLink(page);
|
||||
this.breadcrumbItem.next(refList);
|
||||
}
|
||||
|
||||
private getBreadcrumbsLink(page: BreadcrumbClass): MenuItem[] {
|
||||
this.itemBreadcrums = [];
|
||||
const profile = this.commonService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
this.itemBreadcrums.push({ label: nls.localize('homeCrumb', 'Home') });
|
||||
switch (page) {
|
||||
case BreadcrumbClass.DatabasePage:
|
||||
this.itemBreadcrums.push(this.getServerBreadcrumb(profile));
|
||||
this.itemBreadcrums.push(this.getDbBreadcrumb(profile));
|
||||
break;
|
||||
case BreadcrumbClass.ServerPage:
|
||||
this.itemBreadcrums.push(this.getServerBreadcrumb(profile));
|
||||
break;
|
||||
default:
|
||||
this.itemBreadcrums = [];
|
||||
}
|
||||
return this.itemBreadcrums;
|
||||
}
|
||||
|
||||
private getServerBreadcrumb(profile: ConnectionProfile): MenuItem {
|
||||
return profile.connectionName ? { label: profile.connectionName, routerLink: ['server-dashboard'] } : { label: profile.serverName, routerLink: ['server-dashboard'] };
|
||||
}
|
||||
|
||||
private getDbBreadcrumb(profile: ConnectionProfile): MenuItem {
|
||||
return {
|
||||
label: profile.databaseName ? profile.databaseName : 'database-name',
|
||||
routerLink: ['database-dashboard']
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Node Modules */
|
||||
import { Injectable, Inject, forwardRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/* SQL imports */
|
||||
import { IDashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { IMetadataService } from 'sql/platform/metadata/common/metadataService';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IAdminService } from 'sql/workbench/services/admin/common/adminService';
|
||||
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
|
||||
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
|
||||
import { AngularEventType, IAngularEvent, IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
import { TabSettingConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
|
||||
/* VS imports */
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import * as nls from 'vs/nls';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
const DASHBOARD_SETTINGS = 'dashboard';
|
||||
|
||||
/*
|
||||
Providers a interface between a dashboard interface and the rest of carbon.
|
||||
Stores the uri and unique selector of a dashboard instance and uses that
|
||||
whenever a call to a carbon service needs this information, so that the widgets
|
||||
don't need to be aware of the uri or selector. Simplifies the initialization and
|
||||
usage of a widget.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DashboardServiceInterface extends CommonServiceInterface {
|
||||
|
||||
/* Static Services */
|
||||
|
||||
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;
|
||||
|
||||
private _onPinUnpinTab = new Emitter<TabSettingConfig>();
|
||||
public readonly onPinUnpinTab: Event<TabSettingConfig> = this._onPinUnpinTab.event;
|
||||
|
||||
private _onAddNewTabs = new Emitter<Array<IDashboardTab>>();
|
||||
public readonly onAddNewTabs: Event<Array<IDashboardTab>> = this._onAddNewTabs.event;
|
||||
|
||||
private _onCloseTab = new Emitter<string>();
|
||||
public readonly onCloseTab: Event<string> = this._onCloseTab.event;
|
||||
|
||||
private _dashboardContextKey = new RawContextKey<string>('dashboardContext', undefined);
|
||||
public dashboardContextKey: IContextKey<string>;
|
||||
|
||||
private _numberOfPageNavigations = 0;
|
||||
|
||||
constructor(
|
||||
@Inject(IMetadataService) metadataService: IMetadataService,
|
||||
@Inject(IConnectionManagementService) connectionManagementService: IConnectionManagementService,
|
||||
@Inject(IAdminService) adminService: IAdminService,
|
||||
@Inject(IQueryManagementService) queryManagementService: IQueryManagementService,
|
||||
@Inject(IBootstrapParams) params: IDashboardComponentParams,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(INotificationService) private _notificationService: INotificationService,
|
||||
@Inject(IAngularEventingService) private angularEventingService: IAngularEventingService,
|
||||
@Inject(IConfigurationService) private _configService: IConfigurationService
|
||||
) {
|
||||
super(params, metadataService, connectionManagementService, adminService, queryManagementService);
|
||||
// during testing there may not be params
|
||||
if (this._params) {
|
||||
this.dashboardContextKey = this._dashboardContextKey.bindTo(this.scopedContextKeyService);
|
||||
this._register(toDisposableSubscription(this.angularEventingService.onAngularEvent(this._uri, (event) => this.handleDashboardEvent(event))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of page navigation
|
||||
*/
|
||||
public getNumberOfPageNavigations(): number {
|
||||
return this._numberOfPageNavigations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle on page navigation
|
||||
*/
|
||||
public handlePageNavigation(): void {
|
||||
this._numberOfPageNavigations++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings for given string
|
||||
* @param type string of setting to get from dashboard settings; i.e dashboard.{type}
|
||||
*/
|
||||
public getSettings<T>(type: string): T {
|
||||
let config = this._configService.getValue<T>([DASHBOARD_SETTINGS, type].join('.'));
|
||||
return deepClone(config);
|
||||
}
|
||||
|
||||
public writeSettings(type: string, value: any, target: ConfigurationTarget) {
|
||||
this._configService.updateValue([DASHBOARD_SETTINGS, type].join('.'), value, target);
|
||||
}
|
||||
|
||||
private handleDashboardEvent(event: IAngularEvent): void {
|
||||
switch (event.event) {
|
||||
case AngularEventType.NAV_DATABASE:
|
||||
this.connectionManagementService.changeDatabase(this.connectionManagementService.connectionInfo.connectionProfile.databaseName).then(
|
||||
result => {
|
||||
if (result) {
|
||||
if (this._router.url === '/database-dashboard') {
|
||||
this._updatePage.fire();
|
||||
} else {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
}
|
||||
} else {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('dashboard.changeDatabaseFailure', "Failed to change database")
|
||||
});
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('dashboard.changeDatabaseFailure', "Failed to change database")
|
||||
});
|
||||
}
|
||||
);
|
||||
break;
|
||||
case AngularEventType.NAV_SERVER:
|
||||
this._router.navigate(['server-dashboard']);
|
||||
break;
|
||||
case AngularEventType.DELETE_WIDGET:
|
||||
this._onDeleteWidget.fire(event.payload.id);
|
||||
break;
|
||||
case AngularEventType.PINUNPIN_TAB:
|
||||
this._onPinUnpinTab.fire(event.payload);
|
||||
break;
|
||||
case AngularEventType.NEW_TABS:
|
||||
this._onAddNewTabs.fire(event.payload.dashboardTabs);
|
||||
break;
|
||||
case AngularEventType.CLOSE_TAB:
|
||||
this._onCloseTab.fire(event.payload.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { MetadataType, IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { SingleConnectionManagementService } from 'sql/services/common/commonServiceInterface.service';
|
||||
import {
|
||||
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction, ScriptExecuteAction, ScriptAlterAction,
|
||||
BackupAction, ManageActionContext, BaseActionContext, ManageAction, RestoreAction
|
||||
} from 'sql/workbench/common/actions';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
|
||||
import { IAngularEventingService } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
|
||||
import { ObjectMetadata } from 'azdata';
|
||||
|
||||
import * as tree from 'vs/base/parts/tree/browser/tree';
|
||||
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { ExecuteCommandAction } from 'vs/platform/actions/common/actions';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
|
||||
export class ObjectMetadataWrapper implements ObjectMetadata {
|
||||
public metadataType: MetadataType;
|
||||
public metadataTypeName: string;
|
||||
public urn: string;
|
||||
public name: string;
|
||||
public schema: string;
|
||||
|
||||
constructor(from?: ObjectMetadata) {
|
||||
if (from) {
|
||||
this.metadataType = from.metadataType;
|
||||
this.metadataTypeName = from.metadataTypeName;
|
||||
this.urn = from.urn;
|
||||
this.name = from.name;
|
||||
this.schema = from.schema;
|
||||
}
|
||||
}
|
||||
|
||||
public matches(other: ObjectMetadataWrapper): boolean {
|
||||
if (!other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.metadataType === other.metadataType
|
||||
&& this.schema === other.schema
|
||||
&& this.name === other.name;
|
||||
}
|
||||
|
||||
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
|
||||
if (!objectMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
|
||||
}
|
||||
|
||||
// custom sort : Table > View > Stored Procedures > Function
|
||||
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
|
||||
// compare the object type
|
||||
if (metadata1.metadataType < metadata2.metadataType) {
|
||||
return -1;
|
||||
} else if (metadata1.metadataType > metadata2.metadataType) {
|
||||
return 1;
|
||||
|
||||
// otherwise compare the schema
|
||||
} else {
|
||||
const schemaCompare: number = metadata1.schema && metadata2.schema
|
||||
? metadata1.schema.localeCompare(metadata2.schema)
|
||||
// schemas are not expected to be undefined, but if they are then compare using object names
|
||||
: 0;
|
||||
|
||||
if (schemaCompare !== 0) {
|
||||
return schemaCompare;
|
||||
|
||||
// otherwise compare the object name
|
||||
} else {
|
||||
return metadata1.name.localeCompare(metadata2.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
private _progressService: IProgressService
|
||||
) {
|
||||
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._progressService.showWhile(this._connectionService.changeDatabase(element.databaseName).then(result => {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
}));
|
||||
}
|
||||
|
||||
protected onEnter(tree: tree.ITree, event: IKeyboardEvent): boolean {
|
||||
const result = super.onEnter(tree, event);
|
||||
if (result) {
|
||||
const focus = tree.getFocus();
|
||||
if (focus && !(focus instanceof ObjectMetadataWrapper)) {
|
||||
this._connectionService.changeDatabase(focus.databaseName).then(result => {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplorerDataSource implements tree.IDataSource {
|
||||
private _data: TreeResource[];
|
||||
|
||||
public getId(tree: tree.ITree, element: TreeResource | ExplorerModel): string {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return element.urn || element.schema + element.name;
|
||||
} else if (element instanceof ExplorerModel) {
|
||||
return ExplorerModel.id;
|
||||
} else {
|
||||
return (element as IConnectionProfile).getOptionsKey();
|
||||
}
|
||||
}
|
||||
|
||||
public hasChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): boolean {
|
||||
if (element instanceof ExplorerModel) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public getChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise<TreeResource[]> {
|
||||
if (element instanceof ExplorerModel) {
|
||||
return Promise.resolve(this._data);
|
||||
} else {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise<ExplorerModel> {
|
||||
if (element instanceof ExplorerModel) {
|
||||
return Promise.resolve(undefined);
|
||||
} else {
|
||||
return Promise.resolve(new ExplorerModel());
|
||||
}
|
||||
}
|
||||
|
||||
public set data(data: TreeResource[]) {
|
||||
this._data = data;
|
||||
}
|
||||
}
|
||||
|
||||
enum TEMPLATEIDS {
|
||||
profile = 'profile',
|
||||
object = 'object'
|
||||
}
|
||||
|
||||
export interface IListTemplate {
|
||||
icon?: HTMLElement;
|
||||
label: HTMLElement;
|
||||
}
|
||||
|
||||
export class ExplorerRenderer implements tree.IRenderer {
|
||||
public getHeight(tree: tree.ITree, element: TreeResource): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
public getTemplateId(tree: tree.ITree, element: TreeResource): string {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return TEMPLATEIDS.object;
|
||||
} else {
|
||||
return TEMPLATEIDS.profile;
|
||||
}
|
||||
}
|
||||
|
||||
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
|
||||
const row = $('.list-row');
|
||||
const label = $('.label');
|
||||
|
||||
let icon: HTMLElement;
|
||||
if (templateId === TEMPLATEIDS.object) {
|
||||
icon = $('div');
|
||||
} else {
|
||||
icon = $('.icon.database');
|
||||
}
|
||||
|
||||
row.appendChild(icon);
|
||||
row.appendChild(label);
|
||||
container.appendChild(row);
|
||||
|
||||
return { icon, label };
|
||||
}
|
||||
|
||||
public renderElement(tree: tree.ITree, element: TreeResource, templateId: string, templateData: IListTemplate): void {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
switch (element.metadataType) {
|
||||
case MetadataType.Function:
|
||||
templateData.icon.className = 'icon scalarvaluedfunction';
|
||||
break;
|
||||
case MetadataType.SProc:
|
||||
templateData.icon.className = 'icon 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;
|
||||
}
|
||||
templateData.label.title = templateData.label.innerText;
|
||||
}
|
||||
|
||||
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
|
||||
// no op
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ExplorerFilter implements tree.IFilter {
|
||||
private _filterString: string;
|
||||
|
||||
public isVisible(tree: tree.ITree, element: TreeResource): boolean {
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
return this._doIsVisibleObjectMetadata(element);
|
||||
} else {
|
||||
return this._doIsVisibleConnectionProfile(element);
|
||||
}
|
||||
}
|
||||
|
||||
// apply filter to databasename of the profile
|
||||
private _doIsVisibleConnectionProfile(element: IConnectionProfile): boolean {
|
||||
if (!this._filterString) {
|
||||
return true;
|
||||
}
|
||||
const filterString = this._filterString.trim().toLowerCase();
|
||||
return element.databaseName.toLowerCase().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(':')) {
|
||||
const filterArray = filterString.split(':');
|
||||
|
||||
if (filterArray.length > 2) {
|
||||
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
|
||||
} else {
|
||||
filterString = filterArray[1];
|
||||
}
|
||||
|
||||
switch (filterArray[0].toLowerCase()) {
|
||||
case 'v':
|
||||
metadataType = MetadataType.View;
|
||||
break;
|
||||
case 't':
|
||||
metadataType = MetadataType.Table;
|
||||
break;
|
||||
case 'sp':
|
||||
metadataType = MetadataType.SProc;
|
||||
break;
|
||||
case 'f':
|
||||
metadataType = MetadataType.Function;
|
||||
break;
|
||||
case 'a':
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataType !== undefined) {
|
||||
return element.metadataType === metadataType && (element.schema + '.' + element.name).toLowerCase().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): IAction[] {
|
||||
const actions: IAction[] = [];
|
||||
|
||||
if (element instanceof ObjectMetadataWrapper) {
|
||||
if (element.metadataType === MetadataType.View || element.metadataType === MetadataType.Table) {
|
||||
actions.push(instantiationService.createInstance(ExplorerScriptSelectAction, 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(ExplorerScriptExecuteAction, 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(ExplorerScriptAlterAction, ScriptAlterAction.ID, ScriptAlterAction.LABEL));
|
||||
}
|
||||
} else {
|
||||
actions.push(instantiationService.createInstance(CustomExecuteCommandAction, NewQueryAction.ID, NewQueryAction.LABEL));
|
||||
|
||||
let action: IAction = instantiationService.createInstance(CustomExecuteCommandAction, RestoreAction.ID, RestoreAction.LABEL);
|
||||
if (capabilitiesService.isFeatureAvailable(action, info)) {
|
||||
actions.push(action);
|
||||
}
|
||||
|
||||
action = instantiationService.createInstance(CustomExecuteCommandAction, BackupAction.ID, BackupAction.LABEL);
|
||||
if (capabilitiesService.isFeatureAvailable(action, info)) {
|
||||
actions.push(action);
|
||||
}
|
||||
|
||||
actions.push(instantiationService.createInstance(ExplorerManageAction, ManageAction.ID, ManageAction.LABEL));
|
||||
return actions;
|
||||
}
|
||||
|
||||
actions.push(instantiationService.createInstance(ExplorerScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
class CustomExecuteCommandAction extends ExecuteCommandAction {
|
||||
run(context: ManageActionContext): Promise<any> {
|
||||
return super.run(context.profile);
|
||||
}
|
||||
}
|
||||
|
||||
class ExplorerScriptSelectAction extends ScriptSelectAction {
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||
@IScriptingService scriptingService: IScriptingService,
|
||||
@IProgressService private progressService: IProgressService
|
||||
) {
|
||||
super(id, label, queryEditorService, connectionManagementService, scriptingService);
|
||||
}
|
||||
|
||||
public run(actionContext: BaseActionContext): Promise<boolean> {
|
||||
const promise = super.run(actionContext);
|
||||
this.progressService.showWhile(promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
class ExplorerScriptCreateAction extends ScriptCreateAction {
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||
@IScriptingService scriptingService: IScriptingService,
|
||||
@IErrorMessageService errorMessageService: IErrorMessageService,
|
||||
@IProgressService private progressService: IProgressService
|
||||
) {
|
||||
super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
|
||||
}
|
||||
|
||||
public run(actionContext: BaseActionContext): Promise<boolean> {
|
||||
const promise = super.run(actionContext);
|
||||
this.progressService.showWhile(promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
class ExplorerScriptAlterAction extends ScriptAlterAction {
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||
@IScriptingService scriptingService: IScriptingService,
|
||||
@IErrorMessageService errorMessageService: IErrorMessageService,
|
||||
@IProgressService private progressService: IProgressService
|
||||
) {
|
||||
super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
|
||||
}
|
||||
|
||||
public run(actionContext: BaseActionContext): Promise<boolean> {
|
||||
const promise = super.run(actionContext);
|
||||
this.progressService.showWhile(promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
class ExplorerScriptExecuteAction extends ScriptExecuteAction {
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||
@IScriptingService scriptingService: IScriptingService,
|
||||
@IErrorMessageService errorMessageService: IErrorMessageService,
|
||||
@IProgressService private progressService: IProgressService
|
||||
) {
|
||||
super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
|
||||
}
|
||||
|
||||
public run(actionContext: BaseActionContext): Promise<boolean> {
|
||||
const promise = super.run(actionContext);
|
||||
this.progressService.showWhile(promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
class ExplorerManageAction extends ManageAction {
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
|
||||
@IAngularEventingService angularEventingService: IAngularEventingService,
|
||||
@IProgressService private _progressService: IProgressService
|
||||
) {
|
||||
super(id, label, connectionManagementService, angularEventingService);
|
||||
}
|
||||
|
||||
public run(actionContext: ManageActionContext): Promise<boolean> {
|
||||
const promise = super.run(actionContext);
|
||||
this._progressService.showWhile(promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div class="explorer-widget" style="display: flex; flex-flow: column; position: absolute; height:100%; width:100%; padding: 10px; box-sizing: border-box">
|
||||
<div #input style="width: 100%"></div>
|
||||
<div style="flex: 1 1 auto; position: relative">
|
||||
<div #table style="position: absolute; height: 100%; width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/media/objectTypes/objecttypes';
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./media/explorerWidget';
|
||||
|
||||
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
|
||||
import { ExplorerFilter, ExplorerRenderer, ExplorerDataSource, ExplorerController, ObjectMetadataWrapper, ExplorerModel } from './explorerTree';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
|
||||
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { getContentHeight } from 'vs/base/browser/dom';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
|
||||
@Component({
|
||||
selector: 'explorer-widget',
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/widgets/explorer/explorerWidget.component.html'))
|
||||
})
|
||||
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit {
|
||||
private _input: InputBox;
|
||||
private _tree: Tree;
|
||||
private _filterDelayer = new Delayer<void>(200);
|
||||
private _treeController = new ExplorerController(
|
||||
this._bootstrap.getUnderlyingUri(),
|
||||
this._bootstrap.connectionManagementService,
|
||||
this._router,
|
||||
this.contextMenuService,
|
||||
this.capabilitiesService,
|
||||
this.instantiationService,
|
||||
this.progressService
|
||||
);
|
||||
private _treeRenderer = new ExplorerRenderer();
|
||||
private _treeDataSource = new ExplorerDataSource();
|
||||
private _treeFilter = new ExplorerFilter();
|
||||
|
||||
private _inited = false;
|
||||
|
||||
@ViewChild('input') private _inputContainer: ElementRef;
|
||||
@ViewChild('table') private _tableContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrap: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
||||
@Inject(ICapabilitiesService) private capabilitiesService: ICapabilitiesService,
|
||||
@Inject(IProgressService) private progressService: IProgressService
|
||||
) {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._inited = true;
|
||||
|
||||
const placeholderLabel = this._config.context === 'database' ? nls.localize('seachObjects', 'Search by name of type (a:, t:, v:, f:, or sp:)') : nls.localize('searchDatabases', 'Search databases');
|
||||
|
||||
const inputOptions: IInputOptions = {
|
||||
placeholder: placeholderLabel,
|
||||
ariaLabel: placeholderLabel
|
||||
};
|
||||
this._input = new InputBox(this._inputContainer.nativeElement, this.contextViewService, inputOptions);
|
||||
this._register(this._input.onDidChange(e => {
|
||||
this._filterDelayer.trigger(() => {
|
||||
this._treeFilter.filterString = e;
|
||||
this._tree.refresh();
|
||||
});
|
||||
}));
|
||||
this._tree = new Tree(this._tableContainer.nativeElement, {
|
||||
controller: this._treeController,
|
||||
dataSource: this._treeDataSource,
|
||||
filter: this._treeFilter,
|
||||
renderer: this._treeRenderer
|
||||
}, { horizontalScrollMode: ScrollbarVisibility.Auto });
|
||||
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
|
||||
this._register(this._input);
|
||||
this._register(attachInputBoxStyler(this._input, this.themeService));
|
||||
this._register(this._tree);
|
||||
this._register(attachListStyler(this._tree, this.themeService));
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
if (this._config.context === 'database') {
|
||||
this._register(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
|
||||
data => {
|
||||
if (data) {
|
||||
const objectData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
|
||||
objectData.sort(ObjectMetadataWrapper.sort);
|
||||
this._treeDataSource.data = objectData;
|
||||
this._tree.setInput(new ExplorerModel());
|
||||
}
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.objectError', "Unable to load objects");
|
||||
}
|
||||
)));
|
||||
} else {
|
||||
const currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
|
||||
this._register(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
|
||||
data => {
|
||||
// Handle the case where there is no metadata service
|
||||
data = data || [];
|
||||
const profileData = data.map(d => {
|
||||
const profile = new ConnectionProfile(this.capabilitiesService, currentProfile);
|
||||
profile.databaseName = d;
|
||||
return profile;
|
||||
});
|
||||
this._treeDataSource.data = profileData;
|
||||
this._tree.setInput(new ExplorerModel());
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.databaseError', "Unable to load databases");
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (this._inited) {
|
||||
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
|
||||
const explorerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
registerDashboardWidget('explorer-widget', '', explorerSchema);
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
explorer-widget .list-row .icon {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
explorer-widget .list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: -33px;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { RunQueryOnConnectionMode, IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { InsightActionContext } from 'sql/workbench/common/actions';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export class RunInsightQueryAction extends Action {
|
||||
public static ID = 'runQuery';
|
||||
public static LABEL = nls.localize('insights.runQuery', "Run Query");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService protected _queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService protected _connectionManagementService: IConnectionManagementService,
|
||||
@IObjectExplorerService protected _objectExplorerService: IObjectExplorerService,
|
||||
@IEditorService protected _workbenchEditorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(context: InsightActionContext): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
TaskUtilities.newQuery(
|
||||
context.profile,
|
||||
this._connectionManagementService,
|
||||
this._queryEditorService,
|
||||
this._objectExplorerService,
|
||||
this._workbenchEditorService,
|
||||
context.insight.query as string,
|
||||
RunQueryOnConnectionMode.executeQuery
|
||||
).then(() => resolve(true), () => resolve(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
Component, Inject, forwardRef, AfterContentInit,
|
||||
ComponentFactoryResolver, ViewChild, ChangeDetectorRef, Injector
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WIDGET_CONFIG, WidgetConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
|
||||
import { InsightAction, InsightActionContext } from 'sql/workbench/common/actions';
|
||||
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
|
||||
import { IInsightsConfig, IInsightsView } from './interfaces';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { resolveQueryFilePath } from 'sql/workbench/services/insights/common/insightsUtils';
|
||||
|
||||
import { RunInsightQueryAction } from './actions';
|
||||
|
||||
import { SimpleExecuteResult } from 'azdata';
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IntervalTimer, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
|
||||
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>
|
||||
<loading-spinner [loading]="_loading"></loading-spinner>
|
||||
</div>`,
|
||||
styles: [':host { width: 100%; height: 100% }']
|
||||
})
|
||||
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit {
|
||||
private insightConfig: IInsightsConfig;
|
||||
private queryObv: Observable<SimpleExecuteResult>;
|
||||
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
|
||||
|
||||
private _typeKey: string;
|
||||
private _init: boolean = false;
|
||||
private _loading: boolean = true;
|
||||
private _intervalTimer: IntervalTimer;
|
||||
|
||||
public error: string;
|
||||
public lastUpdated: string;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private dashboardService: CommonServiceInterface,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => Injector)) private _injector: Injector,
|
||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||
@Inject(IStorageService) private storageService: IStorageService,
|
||||
@Inject(IWorkspaceContextService) private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@Inject(IConfigurationService) private readonly _configurationService: IConfigurationService,
|
||||
@Inject(IConfigurationResolverService) private readonly _configurationResolverService: IConfigurationResolverService
|
||||
) {
|
||||
super();
|
||||
this.insightConfig = <IInsightsConfig>this._config.widget['insights-widget'];
|
||||
|
||||
this._verifyConfig();
|
||||
|
||||
this._parseConfig().then(() => {
|
||||
if (!this._checkStorage()) {
|
||||
const promise = this._runQuery();
|
||||
this.queryObv = Observable.fromPromise(promise);
|
||||
const cancelablePromise = createCancelablePromise(() => {
|
||||
return promise.then(
|
||||
result => {
|
||||
this._loading = false;
|
||||
if (this._init) {
|
||||
this._updateChild(result);
|
||||
this.setupInterval();
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(result));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this._loading = false;
|
||||
if (isPromiseCanceledError(error)) {
|
||||
return;
|
||||
}
|
||||
if (this._init) {
|
||||
this.showError(error);
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(error));
|
||||
}
|
||||
}
|
||||
).then(() => this._cd.detectChanges());
|
||||
});
|
||||
this._register(toDisposable(() => cancelablePromise.cancel()));
|
||||
}
|
||||
}, error => {
|
||||
this._loading = false;
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init = true;
|
||||
if (this.queryObv) {
|
||||
this._register(toDisposableSubscription(this.queryObv.subscribe(
|
||||
result => {
|
||||
this._loading = false;
|
||||
this._updateChild(result);
|
||||
this.setupInterval();
|
||||
},
|
||||
error => {
|
||||
this._loading = false;
|
||||
this.showError(error);
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
private setupInterval(): void {
|
||||
if (this.insightConfig.autoRefreshInterval) {
|
||||
this._intervalTimer = new IntervalTimer();
|
||||
this._register(this._intervalTimer);
|
||||
this._intervalTimer.cancelAndSet(() => this.refresh(), this.insightConfig.autoRefreshInterval * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private showError(error: string): void {
|
||||
this.error = error;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
get actions(): Array<Action> {
|
||||
const actions: Array<Action> = [];
|
||||
if (this.insightConfig.details && (this.insightConfig.details.query || this.insightConfig.details.queryFile)) {
|
||||
actions.push(this.instantiationService.createInstance(InsightAction, InsightAction.ID, InsightAction.LABEL));
|
||||
}
|
||||
actions.push(this.instantiationService.createInstance(RunInsightQueryAction, RunInsightQueryAction.ID, RunInsightQueryAction.LABEL));
|
||||
return actions;
|
||||
}
|
||||
|
||||
get actionsContext(): InsightActionContext {
|
||||
return <InsightActionContext>{
|
||||
profile: this.dashboardService.connectionManagementService.connectionInfo.connectionProfile,
|
||||
insight: this.insightConfig
|
||||
};
|
||||
}
|
||||
|
||||
private _storeResult(result: SimpleExecuteResult): SimpleExecuteResult {
|
||||
if (this.insightConfig.cacheId) {
|
||||
const currentTime = new Date();
|
||||
const store: IStorageResult = {
|
||||
date: currentTime.toString(),
|
||||
results: result
|
||||
};
|
||||
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", currentTime.toLocaleTimeString(), currentTime.toLocaleDateString());
|
||||
this._cd.detectChanges();
|
||||
this.storageService.store(this._getStorageKey(), JSON.stringify(store), StorageScope.GLOBAL);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _checkStorage(): boolean {
|
||||
if (this.insightConfig.cacheId) {
|
||||
const storage = this.storageService.get(this._getStorageKey(), StorageScope.GLOBAL);
|
||||
if (storage) {
|
||||
const storedResult: IStorageResult = JSON.parse(storage);
|
||||
const date = new Date(storedResult.date);
|
||||
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", date.toLocaleTimeString(), date.toLocaleDateString());
|
||||
this._loading = false;
|
||||
if (this._init) {
|
||||
this._updateChild(storedResult.results);
|
||||
this.setupInterval();
|
||||
this._cd.detectChanges();
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this._runQuery().then(
|
||||
result => this._updateChild(result),
|
||||
error => this.showError(error)
|
||||
);
|
||||
}
|
||||
|
||||
private _getStorageKey(): string {
|
||||
return `insights.${this.insightConfig.cacheId}.${this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.getOptionsKey()}`;
|
||||
}
|
||||
|
||||
private _runQuery(): Promise<SimpleExecuteResult> {
|
||||
return Promise.resolve(this.dashboardService.queryManagementService.runQueryAndReturn(this.insightConfig.query as string).then(
|
||||
result => {
|
||||
return this._storeResult(result);
|
||||
},
|
||||
error => {
|
||||
throw error;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private _updateChild(result: SimpleExecuteResult): void {
|
||||
this.componentHost.viewContainerRef.clear();
|
||||
this.error = undefined;
|
||||
this._cd.detectChanges();
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
this.showError(nls.localize('noResults', 'No results to show'));
|
||||
return;
|
||||
}
|
||||
|
||||
const componentFactory = this._componentFactoryResolver.resolveComponentFactory<IInsightsView>(insightRegistry.getCtorFromId(this._typeKey));
|
||||
|
||||
const componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory, 0, this._injector);
|
||||
const componentInstance = componentRef.instance;
|
||||
|
||||
// check if the setter is defined
|
||||
if (componentInstance.setConfig) {
|
||||
componentInstance.setConfig(this.insightConfig.type[this._typeKey]);
|
||||
}
|
||||
componentInstance.data = { columns: result.columnInfo.map(item => item.columnName), rows: result.rows.map(row => row.map(item => (item.invariantCultureDisplayValue === null || item.invariantCultureDisplayValue === undefined) ? item.displayValue : item.invariantCultureDisplayValue)) };
|
||||
|
||||
if (componentInstance.init) {
|
||||
componentInstance.init();
|
||||
}
|
||||
}
|
||||
|
||||
private _verifyConfig() {
|
||||
if (types.isUndefinedOrNull(this.insightConfig)) {
|
||||
throw new Error('Insight config must be defined');
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(this.insightConfig.type)) {
|
||||
throw new Error('An Insight type must be specified');
|
||||
}
|
||||
|
||||
if (Object.keys(this.insightConfig.type).length !== 1) {
|
||||
throw new Error('Exactly 1 insight type must be specified');
|
||||
}
|
||||
|
||||
if (!insightRegistry.getAllIds().includes(Object.keys(this.insightConfig.type)[0])) {
|
||||
throw new Error('The insight type must be a valid registered insight');
|
||||
}
|
||||
|
||||
if (!this.insightConfig.query && !this.insightConfig.queryFile) {
|
||||
throw new Error('No query was specified for this insight');
|
||||
}
|
||||
|
||||
if (this.insightConfig.autoRefreshInterval && !types.isNumber(this.insightConfig.autoRefreshInterval)) {
|
||||
throw new Error('Auto Refresh Interval must be a number if specified');
|
||||
}
|
||||
|
||||
if (!types.isStringArray(this.insightConfig.query)
|
||||
&& !types.isString(this.insightConfig.query)
|
||||
&& !types.isString(this.insightConfig.queryFile)) {
|
||||
throw new Error('Invalid query or queryfile specified');
|
||||
}
|
||||
}
|
||||
|
||||
private async _parseConfig(): Promise<void> {
|
||||
this._typeKey = Object.keys(this.insightConfig.type)[0];
|
||||
|
||||
// When the editor.accessibilitySupport setting is on, we will force the chart type to be table.
|
||||
// so that the information is accessible to the user.
|
||||
// count chart type is already a text based chart, we don't have to apply this rule for it.
|
||||
const isAccessibilitySupportOn = this._configurationService.getValue('editor.accessibilitySupport') === 'on';
|
||||
if (isAccessibilitySupportOn && this._typeKey !== 'count') {
|
||||
this._typeKey = 'table';
|
||||
}
|
||||
|
||||
if (types.isStringArray(this.insightConfig.query)) {
|
||||
this.insightConfig.query = this.insightConfig.query.join(' ');
|
||||
} else if (this.insightConfig.queryFile) {
|
||||
const filePath = await resolveQueryFilePath(this.insightConfig.queryFile,
|
||||
this._workspaceContextService,
|
||||
this._configurationResolverService);
|
||||
|
||||
this.insightConfig.query = (await pfs.readFile(filePath)).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
import { registerDashboardWidget, registerNonCustomDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
import { Extensions as InsightExtensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { IInsightTypeContrib } from './interfaces';
|
||||
import { insightsContribution, insightsSchema } from 'sql/workbench/parts/dashboard/widgets/insights/insightsWidgetSchemas';
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
|
||||
|
||||
registerDashboardWidget('insights-widget', '', insightsSchema);
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IInsightTypeContrib | IInsightTypeContrib[]>({ extensionPoint: 'dashboard.insights', jsonSchema: insightsContribution }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(insight: IInsightTypeContrib, extension: IExtensionPointUser<any>) {
|
||||
|
||||
if (insight.contrib.queryFile) {
|
||||
insight.contrib.queryFile = join(extension.description.extensionLocation.fsPath, insight.contrib.queryFile);
|
||||
}
|
||||
|
||||
if (insight.contrib.details && insight.contrib.details.queryFile) {
|
||||
insight.contrib.details.queryFile = join(extension.description.extensionLocation.fsPath, insight.contrib.details.queryFile);
|
||||
}
|
||||
|
||||
registerNonCustomDashboardWidget(insight.id, '', insight.contrib);
|
||||
insightRegistry.registerExtensionInsight(insight.id, insight.contrib);
|
||||
}
|
||||
|
||||
for (const extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IInsightTypeContrib>(value)) {
|
||||
for (const command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsightRegistry, Extensions as InsightExtensions } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
|
||||
|
||||
export const insightsSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('insightWidgetDescription', 'Adds a widget that can query a server or database and display the results in multiple ways - as a chart, summarized count, and more'),
|
||||
properties: {
|
||||
cacheId: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightIdDescription', 'Unique Identifier used for cacheing the results of the insight.')
|
||||
},
|
||||
type: {
|
||||
type: 'object',
|
||||
properties: insightRegistry.insightSchema.properties,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
},
|
||||
query: {
|
||||
type: ['string', 'array'],
|
||||
description: nls.localize('insightQueryDescription', 'SQL query to run. This should return exactly 1 resultset.')
|
||||
},
|
||||
queryFile: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightQueryFileDescription', '[Optional] path to a file that contains a query. Use if "query" is not set')
|
||||
},
|
||||
autoRefreshInterval: {
|
||||
type: 'number',
|
||||
description: nls.localize('insightAutoRefreshIntervalDescription', '[Optional] Auto refresh interval in minutes, if not set, there will be no auto refresh')
|
||||
},
|
||||
details: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: ['string', 'array']
|
||||
},
|
||||
queryFile: {
|
||||
type: 'string'
|
||||
},
|
||||
value: {
|
||||
type: 'string'
|
||||
},
|
||||
label: {
|
||||
type: ['string', 'object'],
|
||||
properties: {
|
||||
column: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
},
|
||||
state: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
if: {
|
||||
type: 'string',
|
||||
enum: ['equals', 'notEquals', 'greaterThanOrEquals', 'greaterThan', 'lessThanOrEquals', 'lessThan', 'always']
|
||||
},
|
||||
equals: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
types: {
|
||||
description: nls.localize('actionTypes', "Which actions to use"),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionDatabaseDescription', 'Target database for the action; can use the format "${columnName}" to use a data driven column name.')
|
||||
},
|
||||
server: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionServerDescription', 'Target server for the action; can use the format "${columnName}" to use a data driven column name.')
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionUserDescription', 'Target user for the action; can use the format "${columnName}" to use a data driven column name.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const insightType: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('carbon.extension.contributes.insightType.id', 'Identifier of the insight'),
|
||||
type: 'string'
|
||||
},
|
||||
contrib: insightsSchema
|
||||
}
|
||||
};
|
||||
|
||||
export const insightsContribution: IJSONSchema = {
|
||||
description: nls.localize('carbon.extension.contributes.insights', "Contributes insights to the dashboard palette."),
|
||||
oneOf: [
|
||||
insightType,
|
||||
{
|
||||
type: 'array',
|
||||
items: insightType
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IStateCondition {
|
||||
condition: {
|
||||
if: string,
|
||||
equals?: string
|
||||
};
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IInsightsLabel {
|
||||
column: string;
|
||||
icon?: string;
|
||||
state?: Array<IStateCondition>;
|
||||
}
|
||||
|
||||
export interface IInsightsConfigDetails {
|
||||
query?: string | Array<string>;
|
||||
queryFile?: string;
|
||||
label?: string | IInsightsLabel;
|
||||
value?: string;
|
||||
actions?: {
|
||||
types: Array<string>;
|
||||
database?: string;
|
||||
server?: string;
|
||||
user?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IInsightData {
|
||||
columns: Array<string>;
|
||||
rows: Array<Array<string>>;
|
||||
}
|
||||
|
||||
export interface IInsightsView {
|
||||
data: IInsightData;
|
||||
setConfig?: (config: { [key: string]: any }) => void;
|
||||
init?: () => void;
|
||||
}
|
||||
|
||||
export interface ISize {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IInsightsConfig {
|
||||
cacheId?: string;
|
||||
type: any;
|
||||
name?: string;
|
||||
when?: string;
|
||||
gridItemConfig?: ISize;
|
||||
query?: string | Array<string>;
|
||||
queryFile?: string;
|
||||
details?: IInsightsConfigDetails;
|
||||
autoRefreshInterval?: number;
|
||||
}
|
||||
export interface IInsightTypeContrib {
|
||||
id: string;
|
||||
contrib: IInsightsConfig;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, ViewChild } from '@angular/core';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
|
||||
import { IInsightsView, IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { memoize, unmemoize } from 'sql/base/common/decorators';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { LegendPosition, ChartType, defaultChartConfig, IChartConfig, IDataSet, IPointDataSet } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
declare const Chart: any;
|
||||
|
||||
@Component({
|
||||
template: ` <div style="display: block; width: 100%; height: 100%; position: relative">
|
||||
<canvas #canvas *ngIf="_isDataAvailable && _hasInit"
|
||||
baseChart
|
||||
[datasets]="chartData"
|
||||
[labels]="labels"
|
||||
[chartType]="chartType"
|
||||
[colors]="colors"
|
||||
[options]="_options"></canvas>
|
||||
<div *ngIf="_hasError">{{CHART_ERROR_MESSAGE}}</div>
|
||||
</div>`
|
||||
})
|
||||
export abstract class ChartInsight extends Disposable implements IInsightsView {
|
||||
private _isDataAvailable: boolean = false;
|
||||
protected _hasInit: boolean = false;
|
||||
protected _hasError: boolean = false;
|
||||
private _options: any = {};
|
||||
|
||||
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
|
||||
|
||||
protected _defaultConfig = defaultChartConfig;
|
||||
protected _config: IChartConfig;
|
||||
protected _data: IInsightData;
|
||||
|
||||
protected readonly CHART_ERROR_MESSAGE = nls.localize('chartErrorMessage', 'Chart cannot be displayed with the given data');
|
||||
|
||||
protected abstract get chartType(): ChartType;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) private telemetryService: ITelemetryService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._register(this.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
// Note: must use a boolean to not render the canvas until all properties such as the labels and chart type are set.
|
||||
// This is because chart.js doesn't auto-update anything other than dataset when re-rendering so defaults are used
|
||||
// hence it's easier to not render until ready
|
||||
this.options = mixin(this.options, { maintainAspectRatio: false });
|
||||
this._hasInit = true;
|
||||
this._hasError = false;
|
||||
try {
|
||||
this._changeRef.detectChanges();
|
||||
} catch (err) {
|
||||
this._hasInit = false;
|
||||
this._hasError = true;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
TelemetryUtils.addTelemetry(this.telemetryService, TelemetryKeys.ChartCreated, { type: this.chartType });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options for the chart; handles rerendering the chart if needed
|
||||
*/
|
||||
public set options(options: any) {
|
||||
this._options = options;
|
||||
if (this._isDataAvailable) {
|
||||
this._options = mixin({}, mixin(this._options, { animation: { duration: 0 } }));
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public get options(): any {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
protected updateTheme(e: IColorTheme): void {
|
||||
const foregroundColor = e.getColor(colors.editorForeground);
|
||||
const foreground = foregroundColor ? foregroundColor.toString() : null;
|
||||
const backgroundColor = e.getColor(colors.editorBackground);
|
||||
const background = backgroundColor ? backgroundColor.toString() : null;
|
||||
|
||||
const options = {
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: foreground
|
||||
}
|
||||
},
|
||||
viewArea: {
|
||||
backgroundColor: background
|
||||
}
|
||||
};
|
||||
this.options = mixin({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
// cheaper refresh but causes problems when change data for rerender
|
||||
if (this._chart) {
|
||||
this._chart.ngOnChanges({});
|
||||
}
|
||||
}
|
||||
|
||||
public getCanvasData(): string {
|
||||
if (this._chart && this._chart.chart) {
|
||||
return this._chart.chart.toBase64Image();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
// unmemoize chart data as the data needs to be recalced
|
||||
unmemoize(this, 'chartData');
|
||||
unmemoize(this, 'labels');
|
||||
this._data = this.filterToTopNData(data);
|
||||
if (isValidData(data)) {
|
||||
this._isDataAvailable = true;
|
||||
}
|
||||
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private filterToTopNData(data: IInsightData): IInsightData {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
return {
|
||||
columns: this.getTopNData(data.columns),
|
||||
rows: data.rows.map((row) => {
|
||||
return this.getTopNData(row);
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
columns: data.columns,
|
||||
rows: data.rows.slice(0, this._config.showTopNData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getTopNData(data: any[]): any[] {
|
||||
if (this._config.showTopNData) {
|
||||
if (this._config.dataDirection === 'horizontal' && this._config.labelFirstColumn) {
|
||||
return data.slice(0, this._config.showTopNData + 1);
|
||||
} else {
|
||||
return data.slice(0, this._config.showTopNData);
|
||||
}
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
protected clearMemoize(): void {
|
||||
// unmemoize getters since their result can be changed by a new config
|
||||
unmemoize(this, 'getChartData');
|
||||
unmemoize(this, 'getLabels');
|
||||
unmemoize(this, 'colors');
|
||||
}
|
||||
|
||||
public setConfig(config: IChartConfig) {
|
||||
this.clearMemoize();
|
||||
this._config = mixin(config, this._defaultConfig, false);
|
||||
this.legendPosition = this._config.legendPosition;
|
||||
if (this._isDataAvailable) {
|
||||
this._options = mixin({}, mixin(this._options, { animation: false }));
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/* Typescript does not allow you to access getters/setters for super classes.
|
||||
his is a workaround that allows us to still call base getter */
|
||||
@memoize
|
||||
protected getChartData(): Array<IDataSet> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
if (this._config.labelFirstColumn) {
|
||||
return this._data.rows.map((row) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)).slice(1),
|
||||
label: row[0]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return this._data.rows.map((row, i) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)),
|
||||
label: 'Series' + i
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this._config.columnsAsLabels) {
|
||||
return this._data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i + 1])),
|
||||
label: this._data.columns[i + 1]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return this._data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i + 1])),
|
||||
label: 'Series' + (i + 1)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
return this.getChartData();
|
||||
}
|
||||
|
||||
@memoize
|
||||
public getLabels(): Array<string> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
if (this._config.labelFirstColumn) {
|
||||
return this._data.columns.slice(1);
|
||||
} else {
|
||||
return this._data.columns;
|
||||
}
|
||||
} else {
|
||||
return this._data.rows.map(row => row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
return this.getLabels();
|
||||
}
|
||||
|
||||
|
||||
@memoize
|
||||
private get colors(): { backgroundColor: string[] }[] {
|
||||
if (this._config && this._config.colorMap) {
|
||||
const backgroundColor = this.labels.map((item) => {
|
||||
return this._config.colorMap[item];
|
||||
});
|
||||
const colorsMap = { backgroundColor };
|
||||
return [colorsMap];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set legendPosition(input: LegendPosition) {
|
||||
const options = {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
};
|
||||
if (input === 'none') {
|
||||
options.legend.display = false;
|
||||
} else {
|
||||
options.legend.position = input;
|
||||
}
|
||||
this.options = mixin(this.options, options);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidData(data: IInsightData): boolean {
|
||||
if (types.isUndefinedOrNull(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(data.columns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(data.rows)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Chart.pluginService.register({
|
||||
beforeDraw: function (chart) {
|
||||
if (chart.config.options.viewArea && chart.config.options.viewArea.backgroundColor) {
|
||||
var ctx = chart.chart.ctx;
|
||||
ctx.fillStyle = chart.config.options.viewArea.backgroundColor;
|
||||
ctx.fillRect(0, 0, chart.chart.width, chart.chart.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export const chartInsightSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('chartInsightDescription', 'Displays results of a query as a chart on the dashboard'),
|
||||
properties: {
|
||||
colorMap: {
|
||||
type: 'object',
|
||||
description: nls.localize('colorMapDescription', 'Maps "column name" -> color. for example add "column1": red to ensure this column uses a red color ')
|
||||
},
|
||||
legendPosition: {
|
||||
type: 'string',
|
||||
description: nls.localize('legendDescription', 'Indicates preferred position and visibility of the chart legend. These are the column names from your query, and map to the label of each chart entry'),
|
||||
default: 'none',
|
||||
enum: ['top', 'bottom', 'left', 'right', 'none']
|
||||
},
|
||||
labelFirstColumn: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('labelFirstColumnDescription', 'If dataDirection is horizontal, setting this to true uses the first columns value for the legend.'),
|
||||
default: false
|
||||
},
|
||||
columnsAsLabels: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('columnsAsLabels', 'If dataDirection is vertical, setting this to true will use the columns names for the legend.'),
|
||||
default: false
|
||||
},
|
||||
dataDirection: {
|
||||
type: 'string',
|
||||
description: nls.localize('dataDirectionDescription', 'Defines whether the data is read from a column (vertical) or a row (horizontal). For time series this is ignored as direction must be vertical.'),
|
||||
default: 'vertical',
|
||||
enum: ['vertical', 'horizontal'],
|
||||
enumDescriptions: ['When vertical, the first column is used to define the x-axis labels, with other columns expected to be numerical.', 'When horizontal, the column names are used as the x-axis labels.']
|
||||
},
|
||||
showTopNData: {
|
||||
type: 'number',
|
||||
description: nls.localize('showTopNData', 'If showTopNData is set, showing only top N data in the chart.')
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import * as types from 'vs/base/common/types';
|
||||
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
|
||||
export enum ChartType {
|
||||
Bar = 'bar',
|
||||
Doughnut = 'doughnut',
|
||||
HorizontalBar = 'horizontalBar',
|
||||
Line = 'line',
|
||||
Pie = 'pie',
|
||||
TimeSeries = 'timeSeries',
|
||||
Scatter = 'scatter'
|
||||
}
|
||||
|
||||
export enum DataDirection {
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal'
|
||||
}
|
||||
|
||||
export enum LegendPosition {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export enum DataType {
|
||||
Number = 'number',
|
||||
Point = 'point'
|
||||
}
|
||||
|
||||
export function customMixin(destination: any, source: any, overwrite?: boolean): any {
|
||||
if (types.isObject(source)) {
|
||||
mixin(destination, source, overwrite, customMixin);
|
||||
} else if (types.isArray(source)) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (destination[i]) {
|
||||
mixin(destination[i], source[i], overwrite, customMixin);
|
||||
} else {
|
||||
destination[i] = source[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination = source;
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export interface IDataSet {
|
||||
data: Array<number>;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IPointDataSet {
|
||||
data: Array<{ x: number | string, y: number }>;
|
||||
label?: string;
|
||||
fill: boolean;
|
||||
backgroundColor?: Color;
|
||||
}
|
||||
|
||||
export interface IChartConfig {
|
||||
colorMap?: { [column: string]: string };
|
||||
labelFirstColumn?: boolean;
|
||||
legendPosition?: LegendPosition;
|
||||
dataDirection?: DataDirection;
|
||||
columnsAsLabels?: boolean;
|
||||
showTopNData?: number;
|
||||
}
|
||||
|
||||
export const defaultChartConfig: IChartConfig = {
|
||||
labelFirstColumn: true,
|
||||
columnsAsLabels: true,
|
||||
legendPosition: LegendPosition.Top,
|
||||
dataDirection: DataDirection.Vertical
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartInsight } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { ChartType, IChartConfig, customMixin } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { ChangeDetectorRef, Inject, ElementRef, forwardRef } from '@angular/core';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export interface IBarChartConfig extends IChartConfig {
|
||||
yAxisMin: number;
|
||||
yAxisMax: number;
|
||||
yAxisLabel: string;
|
||||
xAxisMin: number;
|
||||
xAxisMax: number;
|
||||
xAxisLabel: string;
|
||||
}
|
||||
|
||||
export default class BarChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Bar;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
|
||||
public setConfig(config: IBarChartConfig): void {
|
||||
let options = {};
|
||||
if (config.xAxisMax) {
|
||||
const opts = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
max: config.xAxisMax
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.xAxisMin) {
|
||||
const opts = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
min: config.xAxisMin
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.xAxisLabel) {
|
||||
const opts = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: config.xAxisLabel
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.yAxisMax) {
|
||||
const opts = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
max: config.yAxisMax
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.yAxisMin) {
|
||||
const opts = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
min: config.yAxisMin
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
if (config.yAxisLabel) {
|
||||
const opts = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: config.yAxisLabel
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
options = mixin({}, mixin(options, opts, true, customMixin));
|
||||
}
|
||||
|
||||
this.options = mixin({}, mixin(this.options, options, true, customMixin));
|
||||
super.setConfig(config);
|
||||
}
|
||||
|
||||
protected updateTheme(e: IColorTheme): void {
|
||||
super.updateTheme(e);
|
||||
const foregroundColor = e.getColor(colors.editorForeground);
|
||||
const foreground = foregroundColor ? foregroundColor.toString() : null;
|
||||
const gridLinesColor = e.getColor(editorLineNumbers);
|
||||
const gridLines = gridLinesColor ? gridLinesColor.toString() : null;
|
||||
const options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = mixin({}, mixin(this.options, options, true, customMixin));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { clone } from 'sql/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/workbench/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import BarChart from './barChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
properties: {
|
||||
yAxisMin: {
|
||||
type: 'number',
|
||||
description: nls.localize('yAxisMin', "Minimum value of the y axis")
|
||||
},
|
||||
yAxisMax: {
|
||||
type: 'number',
|
||||
description: nls.localize('yAxisMax', "Maximum value of the y axis")
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: 'string',
|
||||
description: nls.localize('barchart.yAxisLabel', "Label for the y axis")
|
||||
},
|
||||
xAxisMin: {
|
||||
type: 'number',
|
||||
description: nls.localize('xAxisMin', "Minimum value of the x axis")
|
||||
},
|
||||
xAxisMax: {
|
||||
type: 'number',
|
||||
description: nls.localize('xAxisMax', "Maximum value of the x axis")
|
||||
},
|
||||
xAxisLabel: {
|
||||
type: 'string',
|
||||
description: nls.localize('barchart.xAxisLabel', "Label for the x axis")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const barChartSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('bar', '', barChartSchema, BarChart);
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import PieChart from './pieChart.component';
|
||||
import { ChartType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
import { ChangeDetectorRef, Inject, forwardRef, ElementRef } from '@angular/core';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export default class DoughnutChart extends PieChart {
|
||||
protected readonly chartType: ChartType = ChartType.Doughnut;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import DoughnutChart from './doughnutChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const doughnutChartSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('doughnut', '', doughnutChartSchema, DoughnutChart);
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import BarChart from './barChart.component';
|
||||
import { ChartType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
import { forwardRef, Inject, ChangeDetectorRef, ElementRef } from '@angular/core';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export default class HorizontalBarChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.HorizontalBar;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import HorizontalBarChart from './horizontalBarChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const horizontalBarSchema = mixin(clone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('horizontalBar', '', horizontalBarSchema, HorizontalBarChart);
|
||||
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
|
||||
import BarChart, { IBarChartConfig } from './barChart.component';
|
||||
import { memoize, unmemoize } from 'sql/base/common/decorators';
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { ChartType, DataType, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
import { ChangeDetectorRef, Inject, forwardRef, ElementRef } from '@angular/core';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export interface ILineConfig extends IBarChartConfig {
|
||||
dataType?: DataType;
|
||||
}
|
||||
|
||||
const defaultLineConfig = mixin(clone(defaultChartConfig), { dataType: 'number' }) as ILineConfig;
|
||||
|
||||
export default class LineChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.Line;
|
||||
protected _config: ILineConfig;
|
||||
protected _defaultConfig = defaultLineConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
|
||||
public init() {
|
||||
if (this._config.dataType === DataType.Point) {
|
||||
this.addAxisLabels();
|
||||
}
|
||||
super.init();
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getChartData();
|
||||
} else {
|
||||
return this.getDataAsPoint();
|
||||
}
|
||||
}
|
||||
|
||||
protected clearMemoize() {
|
||||
super.clearMemoize();
|
||||
unmemoize(this, 'getDataAsPoint');
|
||||
}
|
||||
|
||||
@memoize
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
const dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
const legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: Number(row[1]), y: Number(row[2]) });
|
||||
}
|
||||
});
|
||||
return Object.values(dataSetMap);
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getLabels();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
const xLabel = this._config.xAxisLabel || this._data.columns[1] || 'x';
|
||||
const yLabel = this._config.yAxisLabel || this._data.columns[2] || 'y';
|
||||
const options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: xLabel
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: yLabel,
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
// @SQLTODO
|
||||
this.options = mixin(this.options, options, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { clone } from 'sql/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 { barChartSchema } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import LineChart from './lineChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
properties: {
|
||||
dataType: {
|
||||
type: 'string',
|
||||
description: nls.localize('dataTypeDescription', 'Indicates data property of a data set for a chart.'),
|
||||
default: 'number',
|
||||
enum: ['number', 'point'],
|
||||
enumDescriptions: ['Set "number" if the data values are contained in 1 column.', 'Set "point" if the data is an {x,y} combination requiring 2 columns for each value.']
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const lineSchema = mixin(clone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('line', '', lineSchema, LineChart);
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartInsight } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import { ChartType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
import { ChangeDetectorRef, Inject, forwardRef, ElementRef } from '@angular/core';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export default class PieChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Pie;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { chartInsightSchema } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
|
||||
|
||||
import PieChart from './pieChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const pieSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('pie', '', pieSchema, PieChart);
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import LineChart, { ILineConfig } from './lineChart.component';
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { ChartType, defaultChartConfig } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { ChangeDetectorRef, Inject, forwardRef, ElementRef } from '@angular/core';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
const defaultScatterConfig = mixin(clone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;
|
||||
|
||||
export default class ScatterChart extends LineChart {
|
||||
protected readonly chartType: ChartType = ChartType.Scatter;
|
||||
protected _defaultConfig = defaultScatterConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import ScatterChart from './scatterChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
};
|
||||
|
||||
const scatterSchema = mixin(clone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('scatter', '', scatterSchema, ScatterChart);
|
||||
@@ -0,0 +1,80 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import LineChart, { ILineConfig } from './lineChart.component';
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { ChartType, defaultChartConfig, IPointDataSet } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ChangeDetectorRef, Inject, forwardRef, ElementRef } from '@angular/core';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
const defaultTimeSeriesConfig = mixin(clone(defaultChartConfig), { dataType: 'point', dataDirection: 'horizontal' }) as ILineConfig;
|
||||
|
||||
export default class TimeSeriesChart extends LineChart {
|
||||
protected _defaultConfig = defaultTimeSeriesConfig;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) _el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) themeService: IWorkbenchThemeService,
|
||||
@Inject(ITelemetryService) telemetryService: ITelemetryService
|
||||
) {
|
||||
super(_changeRef, _el, themeService, telemetryService);
|
||||
}
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
const xLabel = this._config.xAxisLabel || this.getLabels()[1] || 'x';
|
||||
const yLabel = this._config.yAxisLabel || this.getLabels()[2] || 'y';
|
||||
|
||||
const options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: xLabel
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: yLabel
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = Object.assign({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
const dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
const legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) });
|
||||
|
||||
if (this.chartType === ChartType.Scatter) {
|
||||
dataSetMap[legend].backgroundColor = Color.cyan;
|
||||
}
|
||||
}
|
||||
});
|
||||
return Object.values(dataSetMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { clone } from 'sql/base/common/objects';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { barChartSchema } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
|
||||
|
||||
import TimeSeriesChart from './timeSeriesChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
};
|
||||
|
||||
const timeSeriesSchema = mixin(clone(barChartSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('timeSeries', '', timeSeriesSchema, TimeSeriesChart);
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef } from '@angular/core';
|
||||
|
||||
import { IInsightsView, IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div style="margin-left: 5px" *ngFor="let label of _labels; let i = index">
|
||||
<span style="font-size: 20px">{{_values[i]}} </span>
|
||||
<span>{{_labels[i]}}</span>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export default class CountInsight implements IInsightsView {
|
||||
protected _labels: Array<string>;
|
||||
protected _values: Array<string>;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef) { }
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
this._labels = [];
|
||||
this._labels = data.columns;
|
||||
this._values = data.rows[0];
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import CountInsight from './countInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const countInsightSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('countInsightDescription', 'For each column in a resultset, displays the value in row 0 as a count followed by the column name. Supports "1 Healthy", "3 Unhealthy" for example, where "Healthy" is the column name and 1 is the value in row 1 cell 1')
|
||||
};
|
||||
|
||||
registerInsight('count', '', countInsightSchema, CountInsight);
|
||||
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, OnInit, ElementRef } from '@angular/core';
|
||||
|
||||
import { IInsightsView, IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
|
||||
interface IConfig {
|
||||
encoding?: string;
|
||||
imageFormat?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
encoding: 'hex',
|
||||
imageFormat: 'jpeg'
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngIf="hasData" #container style="display: block">
|
||||
<img #image src="{{source}}" >
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export default class ImageInsight implements IInsightsView, OnInit {
|
||||
private _rawSource: string;
|
||||
private _config: IConfig = defaultConfig;
|
||||
|
||||
@ViewChild('image') private image: ElementRef;
|
||||
@ViewChild('container') private container: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit() {
|
||||
const size = Math.min(this.container.nativeElement.parentElement.parentElement.offsetHeight, this.container.nativeElement.parentElement.parentElement.offsetWidth);
|
||||
this.image.nativeElement.style.width = size + 'px';
|
||||
this.image.nativeElement.style.height = size + 'px';
|
||||
}
|
||||
|
||||
@Input() set config(config: { [key: string]: any }) {
|
||||
this._config = mixin(config, defaultConfig, false);
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
|
||||
this._rawSource = data.rows[0][0];
|
||||
} else {
|
||||
this._rawSource = '';
|
||||
}
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public get hasData(): boolean {
|
||||
return this._rawSource && this._rawSource !== '';
|
||||
}
|
||||
|
||||
public get source(): string {
|
||||
let img = this._rawSource;
|
||||
if (this._config.encoding === 'hex') {
|
||||
img = ImageInsight._hexToBase64(img);
|
||||
}
|
||||
return `data:image/${this._config.imageFormat};base64,${img}`;
|
||||
}
|
||||
|
||||
private static _hexToBase64(hexVal: string) {
|
||||
|
||||
if (hexVal.startsWith('0x')) {
|
||||
hexVal = hexVal.slice(2);
|
||||
}
|
||||
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
|
||||
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ').map(v => Number(v))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import ImageInsight from './imageInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const imageInsightSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('imageInsightDescription', 'Displays an image, for example one returned by an R query using ggplot2'),
|
||||
properties: {
|
||||
imageFormat: {
|
||||
type: 'string',
|
||||
description: nls.localize('imageFormatDescription', 'What format is expected - is this a JPEG, PNG or other format?'),
|
||||
default: 'jpeg',
|
||||
enum: ['jpeg', 'png']
|
||||
},
|
||||
encoding: {
|
||||
type: 'string',
|
||||
description: nls.localize('encodingDescription', 'Is this encoded as hex, base64 or some other format?'),
|
||||
default: 'hex',
|
||||
enum: ['hex', 'base64']
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
registerInsight('image', '', imageInsightSchema, ImageInsight);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnInit } from '@angular/core';
|
||||
|
||||
import { getContentHeight, getContentWidth, Dimension } from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
import { IInsightsView, IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
|
||||
@Component({
|
||||
template: ''
|
||||
})
|
||||
export default class TableInsight extends Disposable implements IInsightsView, OnInit {
|
||||
private table: Table<any>;
|
||||
private dataView: TableDataView<any>;
|
||||
private columns: Slick.Column<any>[];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _elementRef: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
||||
) {
|
||||
super();
|
||||
this._elementRef.nativeElement.className = 'slickgridContainer';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.createTable();
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
if (!this.dataView) {
|
||||
this.dataView = new TableDataView();
|
||||
if (this.table) {
|
||||
this.table.setData(this.dataView);
|
||||
}
|
||||
}
|
||||
|
||||
this.dataView.clear();
|
||||
this.dataView.push(transformData(data.rows, data.columns));
|
||||
this.columns = transformColumns(data.columns);
|
||||
|
||||
if (this.table) {
|
||||
this.table.columns = this.columns;
|
||||
} else if (this._elementRef && this._elementRef.nativeElement) {
|
||||
this.createTable();
|
||||
}
|
||||
}
|
||||
|
||||
layout() {
|
||||
if (this.table) {
|
||||
this.table.layout(new Dimension(getContentWidth(this._elementRef.nativeElement), getContentHeight(this._elementRef.nativeElement)));
|
||||
}
|
||||
}
|
||||
|
||||
private createTable() {
|
||||
if (!this.table) {
|
||||
this.table = new Table(this._elementRef.nativeElement, { dataProvider: this.dataView, columns: this.columns }, { showRowNumber: true });
|
||||
this.table.setSelectionModel(new CellSelectionModel());
|
||||
this._register(attachTableStyler(this.table, this.themeService));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transformData(rows: string[][], columns: string[]): { [key: string]: string }[] {
|
||||
return rows.map(row => {
|
||||
const object: { [key: string]: string } = {};
|
||||
row.forEach((val, index) => {
|
||||
object[columns[index]] = val;
|
||||
});
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
return columns.map(col => {
|
||||
return <Slick.Column<any>>{
|
||||
name: col,
|
||||
id: col,
|
||||
field: col
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import TableInsight from './tableInsight.component';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const tableInsightSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('tableInsightDescription', 'Displays the results in a simple table')
|
||||
};
|
||||
|
||||
registerInsight('table', '', tableInsightSchema, TableInsight);
|
||||
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProviderProperties } from './propertiesWidget.component';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const azureEditionDisplayName = nls.localize('azureEdition', 'Edition');
|
||||
const azureType = nls.localize('azureType', 'Type');
|
||||
|
||||
export const properties: Array<ProviderProperties> = [
|
||||
{
|
||||
provider: 'MSSQL',
|
||||
flavors: [
|
||||
{
|
||||
flavor: 'on_prem',
|
||||
condition: {
|
||||
field: 'isCloud',
|
||||
operator: '!=',
|
||||
value: true
|
||||
},
|
||||
databaseProperties: [
|
||||
{
|
||||
displayName: nls.localize('recoveryModel', 'Recovery Model'),
|
||||
value: 'recoveryModel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastDatabaseBackup', 'Last Database Backup'),
|
||||
value: 'lastBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastLogBackup', 'Last Log Backup'),
|
||||
value: 'lastLogBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
],
|
||||
serverProperties: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('edition', 'Edition'),
|
||||
value: 'serverEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('computerName', 'Computer Name'),
|
||||
value: 'machineName'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('osVersion', 'OS Version'),
|
||||
value: 'osVersion'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
flavor: 'cloud',
|
||||
condition: {
|
||||
field: 'isCloud',
|
||||
operator: '==',
|
||||
value: true
|
||||
},
|
||||
databaseProperties: [
|
||||
{
|
||||
displayName: azureEditionDisplayName,
|
||||
value: 'azureEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('serviceLevelObjective', 'Pricing Tier'),
|
||||
value: 'serviceLevelObjective'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
],
|
||||
serverProperties: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: azureType,
|
||||
value: 'serverEdition'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div #parent style="position: absolute; height: 100%; width: 100%;">
|
||||
<div [style.margin-right.px]="_clipped ? 30 : 0" [style.width]="_clipped ? 94 + '%' : '100%'" style="overflow: hidden">
|
||||
<span #child style="white-space : nowrap; width: fit-content">
|
||||
<ng-template ngFor let-item [ngForOf]="properties">
|
||||
<span style="margin-left: 10px; display: inline-block;">
|
||||
<div style="font-size: 11px;">{{item.displayName}}</div>
|
||||
<div>{{item.value}}</div>
|
||||
</span>
|
||||
</ng-template>
|
||||
</span>
|
||||
</div>
|
||||
<span *ngIf="_clipped" style="position: absolute; right: 0; top: 0; padding-top: 5px; padding-right: 14px; z-index: 2">
|
||||
...
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { IDashboardRegistry, Extensions as DashboardExtensions } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
|
||||
import { DatabaseInfo, ServerInfo } from 'azdata';
|
||||
|
||||
import { EventType, addDisposableListener } from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
export interface PropertiesConfig {
|
||||
properties: Array<Property>;
|
||||
}
|
||||
|
||||
export interface FlavorProperties {
|
||||
flavor: string;
|
||||
condition?: {
|
||||
field: string;
|
||||
operator: '==' | '<=' | '>=' | '!=';
|
||||
value: string | boolean;
|
||||
};
|
||||
databaseProperties: Array<Property>;
|
||||
serverProperties: Array<Property>;
|
||||
}
|
||||
|
||||
export interface ProviderProperties {
|
||||
provider: string;
|
||||
flavors: Array<FlavorProperties>;
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
displayName: string;
|
||||
value: string;
|
||||
ignore?: Array<string>;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
const dashboardRegistry = Registry.as<IDashboardRegistry>(DashboardExtensions.DashboardContributions);
|
||||
|
||||
export interface DisplayProperty {
|
||||
displayName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'properties-widget',
|
||||
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/widgets/properties/propertiesWidget.component.html'))
|
||||
})
|
||||
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit {
|
||||
private _connection: ConnectionManagementInfo;
|
||||
private _databaseInfo: DatabaseInfo;
|
||||
private _clipped: boolean;
|
||||
private properties: Array<DisplayProperty>;
|
||||
private _hasInit = false;
|
||||
|
||||
@ViewChild('child', { read: ElementRef }) private _child: ElementRef;
|
||||
@ViewChild('parent', { read: ElementRef }) private _parent: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrap: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
consoleError?: ((message?: any, ...optionalParams: any[]) => void)
|
||||
) {
|
||||
super();
|
||||
if (consoleError) {
|
||||
this.consoleError = consoleError;
|
||||
}
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._hasInit = true;
|
||||
this._register(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this._connection = this._bootstrap.connectionManagementService.connectionInfo;
|
||||
this._register(toDisposableSubscription(this._bootstrap.adminService.databaseInfo.subscribe(data => {
|
||||
this._databaseInfo = data;
|
||||
this._changeRef.detectChanges();
|
||||
this.parseProperties();
|
||||
if (this._hasInit) {
|
||||
this.handleClipping();
|
||||
}
|
||||
}, error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.properties.error', "Unable to load dashboard properties");
|
||||
})));
|
||||
}
|
||||
|
||||
private handleClipping(): void {
|
||||
if (this._child.nativeElement.offsetWidth > this._parent.nativeElement.offsetWidth) {
|
||||
this._clipped = true;
|
||||
} else {
|
||||
this._clipped = false;
|
||||
}
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private parseProperties() {
|
||||
const provider = this._config.provider;
|
||||
|
||||
let propertyArray: Array<Property>;
|
||||
|
||||
// if config exists use that, otherwise use default
|
||||
if (this._config.widget['properties-widget'] && this._config.widget['properties-widget'].properties) {
|
||||
const config = <PropertiesConfig>this._config.widget['properties-widget'];
|
||||
propertyArray = config.properties;
|
||||
} else {
|
||||
const providerProperties = dashboardRegistry.getProperties(provider as string);
|
||||
|
||||
if (!providerProperties) {
|
||||
this.consoleError('No property definitions found for provider', provider);
|
||||
return;
|
||||
}
|
||||
|
||||
let flavor: FlavorProperties;
|
||||
|
||||
// find correct flavor
|
||||
if (providerProperties.flavors.length === 1) {
|
||||
flavor = providerProperties.flavors[0];
|
||||
} else if (providerProperties.flavors.length === 0) {
|
||||
this.consoleError('No flavor definitions found for "', provider,
|
||||
'. If there are not multiple flavors of this provider, add one flavor without a condition');
|
||||
return;
|
||||
} else {
|
||||
const flavorArray = providerProperties.flavors.filter((item) => {
|
||||
const condition = this._connection.serverInfo[item.condition.field];
|
||||
switch (item.condition.operator) {
|
||||
case '==':
|
||||
return condition === item.condition.value;
|
||||
case '!=':
|
||||
return condition !== item.condition.value;
|
||||
case '>=':
|
||||
return condition >= item.condition.value;
|
||||
case '<=':
|
||||
return condition <= item.condition.value;
|
||||
default:
|
||||
this.consoleError('Could not parse operator: "', item.condition.operator,
|
||||
'" on item "', item, '"');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flavorArray.length === 0) {
|
||||
this.consoleError('Could not determine flavor');
|
||||
return;
|
||||
} else if (flavorArray.length > 1) {
|
||||
this.consoleError('Multiple flavors matched correctly for this provider', provider);
|
||||
return;
|
||||
}
|
||||
|
||||
flavor = flavorArray[0];
|
||||
}
|
||||
|
||||
// determine what context we should be pulling from
|
||||
if (this._config.context === 'database') {
|
||||
if (!Array.isArray(flavor.databaseProperties)) {
|
||||
this.consoleError('flavor', flavor.flavor, ' does not have a definition for database properties');
|
||||
}
|
||||
|
||||
if (!Array.isArray(flavor.serverProperties)) {
|
||||
this.consoleError('flavor', flavor.flavor, ' does not have a definition for server properties');
|
||||
}
|
||||
|
||||
propertyArray = flavor.databaseProperties;
|
||||
} else {
|
||||
if (!Array.isArray(flavor.serverProperties)) {
|
||||
this.consoleError('flavor', flavor.flavor, ' does not have a definition for server properties');
|
||||
}
|
||||
|
||||
propertyArray = flavor.serverProperties;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let infoObject: ServerInfo | {};
|
||||
if (this._config.context === 'database') {
|
||||
if (this._databaseInfo && this._databaseInfo.options) {
|
||||
infoObject = this._databaseInfo.options;
|
||||
}
|
||||
} else {
|
||||
infoObject = this._connection.serverInfo;
|
||||
}
|
||||
|
||||
// iterate over properties and display them
|
||||
this.properties = [];
|
||||
for (let i = 0; i < propertyArray.length; i++) {
|
||||
const property = propertyArray[i];
|
||||
const assignProperty = {};
|
||||
let propertyObject = this.getValueOrDefault<string>(infoObject, property.value, property.default || '--');
|
||||
|
||||
// make sure the value we got shouldn't be ignored
|
||||
if (property.ignore !== undefined && propertyObject !== '--') {
|
||||
for (let j = 0; j < property.ignore.length; j++) {
|
||||
// set to default value if we should be ignoring it's value
|
||||
if (propertyObject === property.ignore[0]) {
|
||||
propertyObject = property.default || '--';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assignProperty['displayName'] = property.displayName;
|
||||
assignProperty['value'] = propertyObject;
|
||||
this.properties.push(<DisplayProperty>assignProperty);
|
||||
}
|
||||
|
||||
if (this._hasInit) {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private getValueOrDefault<T>(infoObject: ServerInfo | {}, propertyValue: string, defaultVal?: any): T {
|
||||
let val: T = undefined;
|
||||
if (infoObject) {
|
||||
val = infoObject[propertyValue];
|
||||
}
|
||||
if (types.isUndefinedOrNull(val)) {
|
||||
val = defaultVal;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// overwrittable console.error for testing
|
||||
private consoleError(message?: any, ...optionalParams: any[]): void {
|
||||
error(message, optionalParams);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user