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';