diff --git a/src/sql/parts/dashboard/common/dashboardPage.component.html b/src/sql/parts/dashboard/common/dashboardPage.component.html
index dac1282f03..ed17c62847 100644
--- a/src/sql/parts/dashboard/common/dashboardPage.component.html
+++ b/src/sql/parts/dashboard/common/dashboardPage.component.html
@@ -16,6 +16,8 @@
+
+
diff --git a/src/sql/parts/dashboard/common/dashboardPage.component.ts b/src/sql/parts/dashboard/common/dashboardPage.component.ts
index f37849b30f..9c8633e38a 100644
--- a/src/sql/parts/dashboard/common/dashboardPage.component.ts
+++ b/src/sql/parts/dashboard/common/dashboardPage.component.ts
@@ -10,22 +10,20 @@ import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, Que
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { WidgetConfig, TabConfig, PinConfig } from 'sql/parts/dashboard/common/dashboardWidget';
-import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
import { PanelComponent } from 'sql/base/browser/ui/panel/panel.component';
-import { subscriptionToDisposable } from 'sql/base/common/lifecycle';
import { IDashboardRegistry, Extensions as DashboardExtensions, IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
import { PinUnpinTabAction, AddFeatureTabAction } from './actions';
import { TabComponent } from 'sql/base/browser/ui/panel/tab.component';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
-import { AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
+import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
import { error } from 'sql/base/common/log';
-import { WIDGETS_TABS } from 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
-import { GRID_TABS } from 'sql/parts/dashboard/tabs/dashboardGridTab.contribution';
-import { WEBVIEW_TABS } from 'sql/parts/dashboard/tabs/dashboardWebviewTab.contribution';
+import * as widgetHelper from 'sql/parts/dashboard/common/dashboardWidgetHelper';
+import { WIDGETS_TAB } from 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
+import { GRID_TAB } from 'sql/parts/dashboard/tabs/dashboardGridTab.contribution';
import { Registry } from 'vs/platform/registry/common/platform';
import * as types from 'vs/base/common/types';
@@ -38,7 +36,6 @@ import { addDisposableListener, getContentHeight, EventType } from 'vs/base/brow
import { IColorTheme } 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 { generateUuid } from 'vs/base/common/uuid';
import * as objects from 'vs/base/common/objects';
import Event, { Emitter } from 'vs/base/common/event';
import { Action } from 'vs/base/common/actions';
@@ -46,12 +43,6 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat
const dashboardRegistry = Registry.as(DashboardExtensions.DashboardContributions);
-/**
- * @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
- */
-function isNumberArray(value: any): value is number[] {
- return types.isArray(value) && (value).every(elem => types.isNumber(elem));
-}
@Component({
selector: 'dashboard-page',
@@ -87,17 +78,17 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
private readonly homeTabTitle: string = nls.localize('home', 'Home');
// a set of config modifiers
- private readonly _configModifiers: Array<(item: Array) => Array> = [
- this.removeEmpty,
- this.initExtensionConfigs,
- this.addProvider,
- this.addEdition,
- this.addContext,
- this.filterConfigs
+ private readonly _configModifiers: Array<(item: Array, dashboardServer: DashboardServiceInterface, context: string) => Array> = [
+ widgetHelper.removeEmpty,
+ widgetHelper.initExtensionConfigs,
+ widgetHelper.addProvider,
+ widgetHelper.addEdition,
+ widgetHelper.addContext,
+ widgetHelper.filterConfigs
];
- private readonly _gridModifiers: Array<(item: Array) => Array> = [
- this.validateGridConfig
+ private readonly _gridModifiers: Array<(item: Array, originalConfig: Array) => Array> = [
+ widgetHelper.validateGridConfig
];
constructor(
@@ -118,11 +109,11 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
this._originalConfig = objects.deepClone(tempWidgets);
let properties = this.getProperties();
this._configModifiers.forEach((cb) => {
- tempWidgets = cb.apply(this, [tempWidgets]);
- properties = properties ? cb.apply(this, [properties]) : undefined;
+ tempWidgets = cb.apply(this, [tempWidgets, this.dashboardService, this.context]);
+ properties = properties ? cb.apply(this, [properties, this.dashboardService, this.context]) : undefined;
});
this._gridModifiers.forEach(cb => {
- tempWidgets = cb.apply(this, [tempWidgets]);
+ tempWidgets = cb.apply(this, [tempWidgets, this._originalConfig]);
});
this.propertiesWidget = properties ? properties[0] : undefined;
@@ -198,7 +189,7 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
this.addNewTab(homeTab);
this._panel.selectTab(homeTab.id);
- let allTabs = this.filterConfigs(dashboardRegistry.tabs);
+ let allTabs = widgetHelper.filterConfigs(dashboardRegistry.tabs, this.dashboardService);
// Load always show tabs
let alwaysShowTabs = allTabs.filter(tab => tab.alwaysShow);
@@ -252,19 +243,19 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
let selectedTabs = dashboardTabs.map(v => {
if (Object.keys(v.content).length !== 1) {
- error('Exactly 1 widget must be defined per space');
+ error('Exactly 1 content must be defined per space');
}
let key = Object.keys(v.content)[0];
- if (key === WIDGETS_TABS || key === GRID_TABS) {
+ if (key === WIDGETS_TAB || key === GRID_TAB) {
let configs = Object.values(v.content)[0];
this._configModifiers.forEach(cb => {
- configs = cb.apply(this, [configs]);
+ configs = cb.apply(this, [configs, this.dashboardService, this.context]);
});
this._gridModifiers.forEach(cb => {
- configs = cb.apply(this, [configs]);
+ configs = cb.apply(this, [configs, this._originalConfig]);
});
- if (key === WIDGETS_TABS) {
+ if (key === WIDGETS_TAB) {
return { id: v.id, title: v.title, content: { 'widgets-tab': configs }, alwaysShow: v.alwaysShow };
} else {
@@ -339,158 +330,6 @@ export abstract class DashboardPage extends Disposable implements OnDestroy {
protected abstract propertiesWidget: WidgetConfig;
protected abstract get context(): string;
- /**
- * Returns a filtered version of the widgets passed based on edition and provider
- * @param config widgets to filter
- */
- private filterConfigs(config: T[]): Array {
- let connectionInfo: ConnectionManagementInfo = this.dashboardService.connectionManagementService.connectionInfo;
- let edition = connectionInfo.serverInfo.engineEditionId;
- let provider = connectionInfo.providerId;
-
- // filter by provider
- return config.filter((item) => {
- if (item.provider) {
- return this.stringCompare(item.provider, provider);
- } else {
- return true;
- }
- }).filter((item) => {
- if (item.edition) {
- if (edition) {
- return this.stringCompare(isNumberArray(item.edition) ? item.edition.map(item => item.toString()) : item.edition.toString(), edition.toString());
- } else {
- this.dashboardService.messageService.show(Severity.Warning, nls.localize('providerMissingEdition', 'Widget filters based on edition, but the provider does not have an edition'));
- return true;
- }
- } else {
- return true;
- }
- });
- }
-
- /**
- * Does a compare against the val passed in and the compare string
- * @param val string or array of strings to compare the compare value to; if array, it will compare each val in the array
- * @param compare value to compare to
- */
- private stringCompare(val: string | Array, compare: string): boolean {
- if (types.isUndefinedOrNull(val)) {
- return true;
- } else if (types.isString(val)) {
- return val === compare;
- } else if (types.isStringArray(val)) {
- return val.some(item => item === compare);
- } else {
- return false;
- }
- }
-
- /**
- * Add provider to the passed widgets and returns the new widgets
- * @param widgets Array of widgets to add provider onto
- */
- protected addProvider(config: WidgetConfig[]): Array {
- let provider = this.dashboardService.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
- */
- protected addEdition(config: WidgetConfig[]): Array {
- let connectionInfo: ConnectionManagementInfo = this.dashboardService.connectionManagementService.connectionInfo;
- let edition = connectionInfo.serverInfo.engineEditionId;
- return config.map((item) => {
- if (item.edition === undefined) {
- item.edition = edition;
- }
- return item;
- });
- }
-
- /**
- * Adds the context to the passed widgets and returns the new widgets
- * @param widgets Array of widgets to add context to
- */
- protected addContext(config: WidgetConfig[]): Array {
- let context = this.context;
- return config.map((item) => {
- if (item.context === undefined) {
- item.context = context;
- }
- return item;
- });
- }
-
- /**
- * Validates configs to make sure nothing will error out and returns the modified widgets
- * @param config Array of widgets to validate
- */
- protected removeEmpty(config: WidgetConfig[]): Array {
- 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
- */
- protected validateGridConfig(config: WidgetConfig[]): Array {
- return config.map((widget, index) => {
- if (widget.gridItemConfig === undefined) {
- widget.gridItemConfig = {};
- }
- const id = generateUuid();
- widget.gridItemConfig.payload = { id };
- widget.id = id;
- this._originalConfig[index].id = id;
- return widget;
- });
- }
-
- protected initExtensionConfigs(configurations: WidgetConfig[]): Array {
- let widgetRegistry = Registry.as(Extensions.InsightContribution);
- return configurations.map((config) => {
- if (config.widget && Object.keys(config.widget).length === 1) {
- let key = Object.keys(config.widget)[0];
- let insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
- if (insightConfig !== undefined) {
- // Setup the default properties for this extension if needed
- if (!config.provider && insightConfig.provider) {
- config.provider = insightConfig.provider;
- }
- if (!config.name && insightConfig.name) {
- config.name = insightConfig.name;
- }
- if (!config.edition && insightConfig.edition) {
- config.edition = insightConfig.edition;
- }
- 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;
- });
- }
-
private getProperties(): Array {
let properties = this.dashboardService.getSettings([this.context, 'properties'].join('.'));
this._propertiesConfigLocation = 'default';
diff --git a/src/sql/parts/dashboard/common/dashboardPanel.css b/src/sql/parts/dashboard/common/dashboardPanel.css
index de814cc76f..5b3f82987e 100644
--- a/src/sql/parts/dashboard/common/dashboardPanel.css
+++ b/src/sql/parts/dashboard/common/dashboardPanel.css
@@ -13,6 +13,7 @@ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header .tab > .tab
border-bottom: 0px solid;
}
+panel.dashboard-panel > .tabbedPanel > .title > .title-actions,
panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header {
box-sizing: border-box;
border: 1px solid transparent;
diff --git a/src/sql/parts/dashboard/common/dashboardPanelStyles.ts b/src/sql/parts/dashboard/common/dashboardPanelStyles.ts
index 012ef32e90..b77c1265cd 100644
--- a/src/sql/parts/dashboard/common/dashboardPanelStyles.ts
+++ b/src/sql/parts/dashboard/common/dashboardPanelStyles.ts
@@ -24,6 +24,14 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header.active {
background-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;
+ }
`);
}
@@ -65,13 +73,18 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const tabBoarder = theme.getColor(TAB_BORDER);
if (tabBoarder) {
collector.addRule(`
- panel.dashboard-panel > .tabbedPanel.horizontal > .title > .tabList .tab-header {
+ panel.dashboard-panel > .tabbedPanel > .title > .tabList .tab-header {
border-right-color: ${tabBoarder};
+ border-bottom-color: ${tabBoarder};
}
- panel.dashboard-panel > .tabbedPanel.vertical > .title > .tabList .tab-header {
+ panel.dashboard-panel > .tabbedPanel.horizontal > .title > .title-actions {
border-bottom-color: ${tabBoarder};
}
+
+ panel.dashboard-panel > .tabbedPanel.vertical > .title > .title-actions {
+ border-right-color: ${tabBoarder};
+ }
`);
}
diff --git a/src/sql/parts/dashboard/common/dashboardWidgetHelper.ts b/src/sql/parts/dashboard/common/dashboardWidgetHelper.ts
new file mode 100644
index 0000000000..5314d30c2f
--- /dev/null
+++ b/src/sql/parts/dashboard/common/dashboardWidgetHelper.ts
@@ -0,0 +1,175 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { Severity } from 'vs/platform/message/common/message';
+import * as nls from 'vs/nls';
+
+import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
+import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
+import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
+import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
+
+
+/**
+ * @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
+ */
+function isNumberArray(value: any): value is number[] {
+ return types.isArray(value) && (value).every(elem => types.isNumber(elem));
+}
+
+/**
+ * Does a compare against the val passed in and the compare string
+ * @param val string or array of strings to compare the compare value to; if array, it will compare each val in the array
+ * @param compare value to compare to
+ */
+function stringOrStringArrayCompare(val: string | Array, compare: string): boolean {
+ if (types.isUndefinedOrNull(val)) {
+ return true;
+ } else if (types.isString(val)) {
+ return val === compare;
+ } else if (types.isStringArray(val)) {
+ return val.some(item => item === compare);
+ } else {
+ return false;
+ }
+}
+
+/**
+ * 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 {
+ 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 {
+ 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 {
+ let widgetRegistry = Registry.as(Extensions.InsightContribution);
+ return configurations.map((config) => {
+ if (config.widget && Object.keys(config.widget).length === 1) {
+ let key = Object.keys(config.widget)[0];
+ let insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
+ if (insightConfig !== undefined) {
+ // Setup the default properties for this extension if needed
+ if (!config.provider && insightConfig.provider) {
+ config.provider = insightConfig.provider;
+ }
+ if (!config.name && insightConfig.name) {
+ config.name = insightConfig.name;
+ }
+ if (!config.edition && insightConfig.edition) {
+ config.edition = insightConfig.edition;
+ }
+ 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(config: WidgetConfig[], dashboardService: DashboardServiceInterface): Array {
+ let provider = dashboardService.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(config: WidgetConfig[], dashboardService: DashboardServiceInterface): Array {
+ let connectionInfo: ConnectionManagementInfo = dashboardService.connectionManagementService.connectionInfo;
+ let edition = connectionInfo.serverInfo.engineEditionId;
+ return config.map((item) => {
+ if (item.edition === undefined) {
+ item.edition = edition;
+ }
+ return item;
+ });
+}
+
+/**
+ * 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[], dashboardServer: DashboardServiceInterface, context: string): Array {
+ 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(config: T[], dashboardService: DashboardServiceInterface): Array {
+ let connectionInfo: ConnectionManagementInfo = dashboardService.connectionManagementService.connectionInfo;
+ let edition = connectionInfo.serverInfo.engineEditionId;
+ let provider = connectionInfo.providerId;
+
+ // filter by provider
+ return config.filter((item) => {
+ if (item.provider) {
+ return stringOrStringArrayCompare(item.provider, provider);
+ } else {
+ return true;
+ }
+ }).filter((item) => {
+ if (item.edition) {
+ if (edition) {
+ return stringOrStringArrayCompare(isNumberArray(item.edition) ? item.edition.map(item => item.toString()) : item.edition.toString(), edition.toString());
+ } else {
+ dashboardService.messageService.show(Severity.Warning, nls.localize('providerMissingEdition', 'Widget filters based on edition, but the provider does not have an edition'));
+ return true;
+ }
+ } else {
+ return true;
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/dashboard.module.ts b/src/sql/parts/dashboard/dashboard.module.ts
index ad0500c1b6..388a1e342b 100644
--- a/src/sql/parts/dashboard/dashboard.module.ts
+++ b/src/sql/parts/dashboard/dashboard.module.ts
@@ -30,11 +30,12 @@ import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidg
import { DashboardWidgetTab } from 'sql/parts/dashboard/tabs/dashboardWidgetTab.component';
import { DashboardGridTab } from 'sql/parts/dashboard/tabs/dashboardGridTab.component';
import { DashboardWebviewTab } from 'sql/parts/dashboard/tabs/dashboardWebviewTab.component';
+import { DashboardLeftNavBar } from 'sql/parts/dashboard/tabs/dashboardLeftNavBar.component';
import { WidgetContent } from 'sql/parts/dashboard/contents/widgetContent.component';
import { WebviewContent } from 'sql/parts/dashboard/contents/webviewContent.component';
import { BreadcrumbComponent } from 'sql/base/browser/ui/breadcrumb/breadcrumb.component';
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
-let baseComponents = [DashboardComponent, DashboardWidgetWrapper, DashboardWebviewTab, DashboardWidgetTab, DashboardGridTab, WidgetContent, WebviewContent, ComponentHostDirective, BreadcrumbComponent];
+let baseComponents = [DashboardComponent, DashboardWidgetWrapper, DashboardWebviewTab, DashboardWidgetTab, DashboardGridTab, DashboardLeftNavBar, WidgetContent, WebviewContent, ComponentHostDirective, BreadcrumbComponent];
/* Panel */
import { PanelModule } from 'sql/base/browser/ui/panel/panel.module';
diff --git a/src/sql/parts/dashboard/tabs/dashboardGridTab.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardGridTab.contribution.ts
index 3f5cd4233c..c2715468e4 100644
--- a/src/sql/parts/dashboard/tabs/dashboardGridTab.contribution.ts
+++ b/src/sql/parts/dashboard/tabs/dashboardGridTab.contribution.ts
@@ -8,7 +8,7 @@ import * as nls from 'vs/nls';
import { generateDashboardGridLayoutSchema } from 'sql/parts/dashboard/pages/dashboardPageContribution';
import { registerTabContent } from 'sql/platform/dashboard/common/dashboardRegistry';
-export const GRID_TABS = 'grid-tab';
+export const GRID_TAB = 'grid-tab';
let gridContentsSchema: IJSONSchema = {
type: 'array',
@@ -16,4 +16,4 @@ let gridContentsSchema: IJSONSchema = {
items: generateDashboardGridLayoutSchema(undefined, true)
};
-registerTabContent(GRID_TABS, gridContentsSchema);
+registerTabContent(GRID_TAB, gridContentsSchema);
diff --git a/src/sql/parts/dashboard/tabs/dashboardInnerTab.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardInnerTab.contribution.ts
new file mode 100644
index 0000000000..7ec962442a
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardInnerTab.contribution.ts
@@ -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/platform/extensions/common/extensionsRegistry';
+import { IJSONSchema } from 'vs/base/common/jsonSchema';
+import { localize } from 'vs/nls';
+import { join } from 'path';
+import { createCSSRule } from 'vs/base/browser/dom';
+import URI from 'vs/base/common/uri';
+
+import { registerInnerTab, generateInnerTabContentSchemaProperties } from 'sql/platform/dashboard/common/innerTabRegistry';
+
+export type IUserFriendlyIcon = string | { light: string; dark: string; };
+
+export interface IDashboardInnerTabContrib {
+ id: string;
+ title: string;
+ icon?: IUserFriendlyIcon;
+ content: object;
+}
+
+const innerTabSchema: IJSONSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ description: localize('sqlops.extension.contributes.dashboard.innertab.id', "Unique identifier for this inner tab. Will be passed to the extension for any requests.")
+ },
+ icon: {
+ description: localize('sqlops.extension.contributes.dashboard.innertab.icon', '(Optional) Icon which is used to represent this inner tab in the UI. Either a file path or a themable configuration'),
+ anyOf: [{
+ type: 'string'
+ },
+ {
+ type: 'object',
+ properties: {
+ light: {
+ description: localize('carbon.extension.contributes.account.icon.light', 'Icon path when a light theme is used'),
+ type: 'string'
+ },
+ dark: {
+ description: localize('carbon.extension.contributes.account.icon.dark', 'Icon path when a dark theme is used'),
+ type: 'string'
+ }
+ }
+ }]
+ },
+ title: {
+ type: 'string',
+ description: localize('sqlops.extension.contributes.dashboard.innertab.title', "Title of the inner tab to show the user.")
+ },
+ content: {
+ description: localize('sqlops.extension.contributes.dashboard.innertab.content', "The content that will be displayed in this inner tab."),
+ type: 'object',
+ properties: generateInnerTabContentSchemaProperties()
+ }
+ }
+};
+
+const innerTabContributionSchema: IJSONSchema = {
+ description: localize('sqlops.extension.contributes.innertabs', "Contributes a single or multiple inner tabs for users to add to their dashboard."),
+ oneOf: [
+ innerTabSchema,
+ {
+ type: 'array',
+ items: innerTabSchema
+ }
+ ]
+};
+
+ExtensionsRegistry.registerExtensionPoint('dashboard.innertabs', [], innerTabContributionSchema).setHandler(extensions => {
+
+ function handleCommand(innerTab: IDashboardInnerTabContrib, extension: IExtensionPointUser) {
+ let { title, id, content, icon } = innerTab;
+ if (!title) {
+ extension.collector.error('No title specified for extension.');
+ return;
+ }
+ if (!content) {
+ extension.collector.warn('No content specified to show.');
+ }
+
+ let iconClass: string;
+ if (icon) {
+ iconClass = id;
+ if (typeof icon === 'string') {
+ const path = join(extension.description.extensionFolderPath, icon);
+ createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(path).toString()}")`);
+ } else {
+ const light = join(extension.description.extensionFolderPath, icon.light);
+ const dark = join(extension.description.extensionFolderPath, 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()}")`);
+ }
+ }
+
+ registerInnerTab({ title, id, content, hasIcon: !!icon });
+ }
+
+ for (let extension of extensions) {
+ const { value } = extension;
+ if (Array.isArray(value)) {
+ for (let command of value) {
+ handleCommand(command, extension);
+ }
+ } else {
+ handleCommand(value, extension);
+ }
+ }
+});
diff --git a/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.component.html b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.component.html
new file mode 100644
index 0000000000..b1214c0585
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.component.ts b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.component.ts
new file mode 100644
index 0000000000..1b45c37b09
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.component.ts
@@ -0,0 +1,176 @@
+/*---------------------------------------------------------------------------------------------
+ * 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!./dashboardLeftNavBar';
+
+import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, EventEmitter, OnChanges, AfterContentInit } from '@angular/core';
+
+import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
+import { WidgetConfig, TabConfig } from 'sql/parts/dashboard/common/dashboardWidget';
+import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
+import { IDashboardInnerTabRegistry, Extensions as InnerTabExtensions, IDashboardInnerTab } from 'sql/platform/dashboard/common/innerTabRegistry';
+import { TabComponent } from 'sql/base/browser/ui/panel/tab.component';
+import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
+import { error } from 'sql/base/common/log';
+import { WIDGETS_TAB } from 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
+import * as widgetHelper from 'sql/parts/dashboard/common/dashboardWidgetHelper';
+
+import { Registry } from 'vs/platform/registry/common/platform';
+import Event, { Emitter } from 'vs/base/common/event';
+
+const innerTabRegistry = Registry.as(InnerTabExtensions.InnerTabContributions);
+
+@Component({
+ selector: 'dashboard-left-nav-bar',
+ providers: [{ provide: DashboardTab, useExisting: forwardRef(() => DashboardLeftNavBar) }],
+ templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/tabs/dashboardLeftNavBar.component.html'))
+})
+export class DashboardLeftNavBar extends DashboardTab implements OnDestroy, OnChanges, AfterContentInit {
+ @Input() private tab: TabConfig;
+ protected tabs: Array = [];
+ private _onResize = new Emitter();
+ public readonly onResize: Event = 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, dashboardServer: DashboardServiceInterface, context: string) => Array> = [
+ widgetHelper.removeEmpty,
+ widgetHelper.initExtensionConfigs,
+ widgetHelper.addProvider,
+ widgetHelper.addEdition,
+ widgetHelper.addContext,
+ widgetHelper.filterConfigs
+ ];
+
+ private readonly _gridModifiers: Array<(item: Array, originalConfig: Array) => Array> = [
+ widgetHelper.validateGridConfig
+ ];
+
+ @ViewChildren(DashboardTab) private _tabs: QueryList;
+ @ViewChild(PanelComponent) private _panel: PanelComponent;
+ constructor(
+ @Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface,
+ @Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
+ ) {
+ super();
+ }
+
+ ngOnChanges() {
+ this.tabs = [];
+ let innerTabIds = [];
+ let allPosibleInnerTab = innerTabRegistry.innerTabs;
+ let filteredTabs: IDashboardInnerTab[] = [];
+ if (this.tab.content) {
+ innerTabIds = Object.values(this.tab.content)[0];
+ if (innerTabIds && innerTabIds.length > 0) {
+ innerTabIds.forEach(tabId => {
+ let tab = allPosibleInnerTab.find(i => i.id === tabId);
+ filteredTabs.push(tab);
+ });
+ this.loadNewTabs(filteredTabs);
+ }
+ this._cd.detectChanges();
+ }
+ }
+
+ ngAfterContentInit(): void {
+ if (this._tabs) {
+ this._tabs.forEach(tabContent => {
+ this._register(tabContent.onResize(() => {
+ this._onResize.fire();
+ }));
+ });
+ }
+ }
+
+ ngOnDestroy() {
+ this.dispose();
+ }
+
+ private loadNewTabs(dashboardTabs: IDashboardInnerTab[]) {
+ if (dashboardTabs && dashboardTabs.length > 0) {
+ let selectedTabs = dashboardTabs.map(v => {
+
+ if (Object.keys(v.content).length !== 1) {
+ error('Exactly 1 content must be defined per space');
+ }
+
+ let key = Object.keys(v.content)[0];
+ if (key === WIDGETS_TAB) {
+ let configs = Object.values(v.content)[0];
+ this._configModifiers.forEach(cb => {
+ configs = cb.apply(this, [configs, this.dashboardService, this.tab.context]);
+ });
+ this._gridModifiers.forEach(cb => {
+ configs = cb.apply(this, [configs]);
+ });
+ return { id: v.id, title: v.title, content: { 'widgets-tab': configs } };
+ }
+ return { id: v.id, title: v.title, content: v.content };
+ }).map(v => {
+ let config = v as TabConfig;
+ config.context = this.tab.context;
+ config.editable = false;
+ config.canClose = false;
+ this.addNewTab(config);
+ return config;
+ });
+
+ // put this immediately on the stack so that is ran *after* the tab is rendered
+ setTimeout(() => {
+ this._panel.selectTab(selectedTabs.pop().id);
+ });
+ }
+ }
+
+ private addNewTab(tab: TabConfig): void {
+ let existedTab = this.tabs.find(i => i.id === tab.id);
+ if (!existedTab) {
+ this.tabs.push(tab);
+ this._cd.detectChanges();
+ }
+ }
+
+ private getContentType(tab: TabConfig): string {
+ return tab.content ? Object.keys(tab.content)[0] : '';
+ }
+
+ public get id(): string {
+ return this.tab.id;
+ }
+
+ public get editable(): boolean {
+ return this.tab.editable;
+ }
+
+ public 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 handleTabChange(tab: TabComponent): void {
+ let localtab = this._tabs.find(i => i.id === tab.identifier);
+ localtab.layout();
+ }
+}
diff --git a/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.contribution.ts
new file mode 100644
index 0000000000..13c5d2b315
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.contribution.ts
@@ -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 { registerTabContent } from 'sql/platform/dashboard/common/dashboardRegistry';
+
+export const LEFT_NAV_TAB = 'left-nav-bar';
+
+let leftNavSchema: IJSONSchema = {
+ type: 'array',
+ description: nls.localize('dashboard.tab.content.left-nav-bar', "The list of inner tabs IDs that will be displayed in this vertical navigation bar."),
+ items: {
+ type: 'string'
+ }
+};
+
+registerTabContent(LEFT_NAV_TAB, leftNavSchema);
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.css b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.css
new file mode 100644
index 0000000000..35dda77722
--- /dev/null
+++ b/src/sql/parts/dashboard/tabs/dashboardLeftNavBar.css
@@ -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-left-nav-bar {
+ height: 100%;
+ width: 100%;
+ display: block;
+}
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts
index 8b57d3e784..6ef9e69eca 100644
--- a/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts
+++ b/src/sql/parts/dashboard/tabs/dashboardWebviewTab.contribution.ts
@@ -6,8 +6,9 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerTabContent } from 'sql/platform/dashboard/common/dashboardRegistry';
+import { registerInnerTabContent } from 'sql/platform/dashboard/common/innerTabRegistry';
-export const WEBVIEW_TABS = 'webview-tab';
+export const WEBVIEW_TAB = 'webview-tab';
let webviewSchema: IJSONSchema = {
type: 'null',
@@ -15,4 +16,5 @@ let webviewSchema: IJSONSchema = {
default: null
};
-registerTabContent(WEBVIEW_TABS, webviewSchema);
\ No newline at end of file
+registerTabContent(WEBVIEW_TAB, webviewSchema);
+registerInnerTabContent(WEBVIEW_TAB, webviewSchema);
\ No newline at end of file
diff --git a/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts
index 3a34a04964..d0210f6e78 100644
--- a/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts
+++ b/src/sql/parts/dashboard/tabs/dashboardWidgetTab.contribution.ts
@@ -7,8 +7,9 @@ import * as nls from 'vs/nls';
import { generateDashboardWidgetSchema } from 'sql/parts/dashboard/pages/dashboardPageContribution';
import { registerTabContent } from 'sql/platform/dashboard/common/dashboardRegistry';
+import { registerInnerTabContent } from 'sql/platform/dashboard/common/innerTabRegistry';
-export const WIDGETS_TABS = 'widgets-tab';
+export const WIDGETS_TAB = 'widgets-tab';
let widgetsSchema: IJSONSchema = {
type: 'array',
@@ -16,4 +17,5 @@ let widgetsSchema: IJSONSchema = {
items: generateDashboardWidgetSchema(undefined, true)
};
-registerTabContent(WIDGETS_TABS, widgetsSchema);
+registerTabContent(WIDGETS_TAB, widgetsSchema);
+registerInnerTabContent(WIDGETS_TAB, widgetsSchema);
diff --git a/src/sql/platform/dashboard/common/dashboardRegistry.ts b/src/sql/platform/dashboard/common/dashboardRegistry.ts
index b335a42ad0..b4ca4c7ec7 100644
--- a/src/sql/platform/dashboard/common/dashboardRegistry.ts
+++ b/src/sql/platform/dashboard/common/dashboardRegistry.ts
@@ -34,6 +34,7 @@ export interface IDashboardRegistry {
registerDashboardProvider(id: string, properties: ProviderProperties): void;
getProperties(id: string): ProviderProperties;
registerTab(tab: IDashboardTab): void;
+ registerTabContent(id: string, schema: IJSONSchema): void;
tabs: Array;
tabContentSchemaProperties: IJSONSchemaMap;
}
diff --git a/src/sql/platform/dashboard/common/innerTabRegistry.ts b/src/sql/platform/dashboard/common/innerTabRegistry.ts
new file mode 100644
index 0000000000..2e409963ad
--- /dev/null
+++ b/src/sql/platform/dashboard/common/innerTabRegistry.ts
@@ -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 { Registry } from 'vs/platform/registry/common/platform';
+import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
+import { Extensions as ConfigurationExtension } from 'vs/platform/configuration/common/configurationRegistry';
+import { deepClone } from 'vs/base/common/objects';
+
+import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
+
+export const Extensions = {
+ InnerTabContributions: 'dashboard.contributions.innerTabs'
+};
+
+export interface IDashboardInnerTab {
+ id: string;
+ title: string;
+ hasIcon: boolean;
+ content?: object;
+}
+
+export interface IDashboardInnerTabRegistry {
+ registerInnerTab(tab: IDashboardInnerTab): void;
+ registerInnerTabContent(id: string, schema: IJSONSchema): void;
+ innerTabs: Array;
+ innerTabContentSchemaProperties: IJSONSchemaMap;
+}
+
+class DashboardInnerTabRegistry implements IDashboardInnerTabRegistry {
+ private _innertabs = new Array();
+ private _dashboardInnerTabContentSchemaProperties: IJSONSchemaMap = {};
+
+ public registerInnerTab(tab: IDashboardInnerTab): void {
+ this._innertabs.push(tab);
+ }
+
+ public get innerTabs(): Array {
+ return this._innertabs;
+ }
+
+ /**
+ * Register a dashboard widget
+ * @param id id of the widget
+ * @param schema config schema of the widget
+ */
+ public registerInnerTabContent(id: string, schema: IJSONSchema): void {
+ this._dashboardInnerTabContentSchemaProperties[id] = schema;
+ }
+
+ public get innerTabContentSchemaProperties(): IJSONSchemaMap {
+ return deepClone(this._dashboardInnerTabContentSchemaProperties);
+ }
+}
+
+const dashboardInnerTabRegistry = new DashboardInnerTabRegistry();
+Registry.add(Extensions.InnerTabContributions, dashboardInnerTabRegistry);
+
+export function registerInnerTab(innerTab: IDashboardInnerTab): void {
+ dashboardInnerTabRegistry.registerInnerTab(innerTab);
+}
+
+export function registerInnerTabContent(id: string, schema: IJSONSchema): void {
+ dashboardInnerTabRegistry.registerInnerTabContent(id, schema);
+}
+
+export function generateInnerTabContentSchemaProperties(): IJSONSchemaMap {
+ return dashboardInnerTabRegistry.innerTabContentSchemaProperties;
+}
\ No newline at end of file
diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts
index 22f4a16b22..01720aebde 100644
--- a/src/vs/workbench/workbench.main.ts
+++ b/src/vs/workbench/workbench.main.ts
@@ -160,9 +160,11 @@ import 'sql/parts/dashboard/widgets/tasks/tasksWidget.contribution';
import 'sql/parts/dashboard/widgets/webview/webviewWidget.contribution';
import 'sql/parts/dashboard/dashboardConfig.contribution';
/* Tabs */
+import 'sql/parts/dashboard/tabs/dashboardLeftNavBar.contribution';
import 'sql/parts/dashboard/tabs/dashboardWebviewTab.contribution';
-import 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
import 'sql/parts/dashboard/tabs/dashboardGridTab.contribution';
+import 'sql/parts/dashboard/tabs/dashboardWidgetTab.contribution';
+import 'sql/parts/dashboard/tabs/dashboardInnerTab.contribution';
import 'sql/parts/dashboard/common/dashboardTab.contribution';
/* Tasks */
import 'sql/workbench/common/actions.contribution';