diff --git a/src/sql/parts/dashboard/common/dashboardHelper.ts b/src/sql/parts/dashboard/common/dashboardHelper.ts index 0bd824b672..f6bb49f585 100644 --- a/src/sql/parts/dashboard/common/dashboardHelper.ts +++ b/src/sql/parts/dashboard/common/dashboardHelper.ts @@ -172,20 +172,17 @@ export function filterConfigs(config: T[], dashboar * Get registered container if it is specified as the key * @param container dashboard container */ -export function getDashboardContainer(container: object): object { - if (Object.keys(container).length !== 1) { - error(nls.localize('moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space')); - } - +export function getDashboardContainer(container: object): { result: boolean, message: string, container: object } { let key = Object.keys(container)[0]; let containerTypeFound = containerTypes.find(c => (c === key)); if (!containerTypeFound) { let dashboardContainer = dashboardcontainerRegistry.getRegisteredContainer(key); if (!dashboardContainer) { - error(nls.localize('unknownDashboardContainerError', 'The specified dashboard container is unknown.')); + let error = nls.localize('unknownDashboardContainerError', '{0} is an unknown container.', key); + return { result: false, message: error, container: null }; } else { container = dashboardContainer.container; } } - return container; + return { result: true, message: null, container: container }; } \ No newline at end of file diff --git a/src/sql/parts/dashboard/common/dashboardPage.component.ts b/src/sql/parts/dashboard/common/dashboardPage.component.ts index 93a7f98902..4838824ab5 100644 --- a/src/sql/parts/dashboard/common/dashboardPage.component.ts +++ b/src/sql/parts/dashboard/common/dashboardPage.component.ts @@ -189,11 +189,16 @@ export abstract class DashboardPage extends Disposable implements OnDestroy { private loadNewTabs(dashboardTabs: IDashboardTab[], openLastTab: boolean = false) { if (dashboardTabs && dashboardTabs.length > 0) { let selectedTabs = dashboardTabs.map(v => { - let container = dashboardHelper.getDashboardContainer(v.container); - let key = Object.keys(container)[0]; + let containerResult = dashboardHelper.getDashboardContainer(v.container); + if (!containerResult.result) { + let errorTitle = nls.localize('dashboardPage_loadTabError', 'Cannot open {0}. ', v.title); + this.dashboardService.messageService.show(Severity.Error, errorTitle + containerResult.message); + return null; + } + let key = Object.keys(containerResult.container)[0]; if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) { - let configs = Object.values(container)[0]; + let configs = Object.values(containerResult.container)[0]; this._configModifiers.forEach(cb => { configs = cb.apply(this, [configs, this.dashboardService, this.context]); }); @@ -207,27 +212,33 @@ export abstract class DashboardPage extends Disposable implements OnDestroy { return { id: v.id, title: v.title, container: { 'grid-container': configs }, alwaysShow: v.alwaysShow }; } } - return { id: v.id, title: v.title, container: container, alwaysShow: v.alwaysShow }; + return { id: v.id, title: v.title, container: containerResult.container, alwaysShow: v.alwaysShow }; }).map(v => { - let actions = []; - if (!v.alwaysShow) { - let pinnedTab = this._pinnedTabs.find(i => i.tabId === v.id); - actions.push(this.dashboardService.instantiationService.createInstance(PinUnpinTabAction, v.id, this.dashboardService.getUnderlyingUri(), !!pinnedTab)); - } + if (v) { + let actions = []; + if (!v.alwaysShow) { + let pinnedTab = this._pinnedTabs.find(i => i.tabId === v.id); + actions.push(this.dashboardService.instantiationService.createInstance(PinUnpinTabAction, v.id, this.dashboardService.getUnderlyingUri(), !!pinnedTab)); + } - let config = v as TabConfig; - config.context = this.context; - config.editable = false; - config.canClose = true; - config.actions = actions; - this.addNewTab(config); - return config; + let config = v as TabConfig; + config.context = this.context; + config.editable = false; + config.canClose = true; + config.actions = actions; + this.addNewTab(config); + return config; + } + return null; }); if (openLastTab) { // put this immediately on the stack so that is ran *after* the tab is rendered setTimeout(() => { - this._panel.selectTab(selectedTabs.pop().id); + let selectedLastTab = selectedTabs.pop(); + if (selectedLastTab) { + this._panel.selectTab(selectedLastTab.id); + } }); } } diff --git a/src/sql/parts/dashboard/common/dashboardTab.contribution.ts b/src/sql/parts/dashboard/common/dashboardTab.contribution.ts index 88094cd4fc..506cd9e7db 100644 --- a/src/sql/parts/dashboard/common/dashboardTab.contribution.ts +++ b/src/sql/parts/dashboard/common/dashboardTab.contribution.ts @@ -8,6 +8,9 @@ import { localize } from 'vs/nls'; import { registerTab } from 'sql/platform/dashboard/common/dashboardRegistry'; import { generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry'; +import { NAV_SECTION, validateNavSectionContribution } from 'sql/parts/dashboard/containers/dashboardNavSection.contribution'; +import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution'; +import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/parts/dashboard/containers/dashboardGridContainer.contribution'; export interface IDashboardTabContrib { id: string; @@ -92,16 +95,43 @@ ExtensionsRegistry.registerExtensionPoint) { let { id, container } = dashboardContainer; - if (!container) { - extension.collector.warn('No container specified to show.'); + 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('dashboardTab.contribution.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.')); + return; + } + + let result = true; + let containerkey = Object.keys(container)[0]; + let containerValue = Object.values(container)[0]; + + let 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 = validateNavSectionContribution(extension, containerValue); + break; + } + + if (result) { + registerContainer({ id, container }); } - registerContainer({ id, container }); } for (let extension of extensions) { diff --git a/src/sql/parts/dashboard/containers/dashboardGridContainer.contribution.ts b/src/sql/parts/dashboard/containers/dashboardGridContainer.contribution.ts index 531e0ad9d0..77ddf17d42 100644 --- a/src/sql/parts/dashboard/containers/dashboardGridContainer.contribution.ts +++ b/src/sql/parts/dashboard/containers/dashboardGridContainer.contribution.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IExtensionPointUser } from 'vs/platform/extensions/common/extensionsRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; @@ -18,3 +19,17 @@ let gridContainersSchema: IJSONSchema = { registerContainerType(GRID_CONTAINER, gridContainersSchema); registerNavSectionContainerType(GRID_CONTAINER, gridContainersSchema); + +export function validateGridContainerContribution(extension: IExtensionPointUser, gridConfigs: object[]): boolean { + let result = true; + gridConfigs.forEach(widgetConfig => { + let allKeys = Object.keys(widgetConfig); + let 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; +} diff --git a/src/sql/parts/dashboard/containers/dashboardNavSection.component.ts b/src/sql/parts/dashboard/containers/dashboardNavSection.component.ts index 8c0c7e26a2..e5534364ab 100644 --- a/src/sql/parts/dashboard/containers/dashboardNavSection.component.ts +++ b/src/sql/parts/dashboard/containers/dashboardNavSection.component.ts @@ -19,6 +19,8 @@ import * as dashboardHelper from 'sql/parts/dashboard/common/dashboardHelper'; import { Registry } from 'vs/platform/registry/common/platform'; import Event, { Emitter } from 'vs/base/common/event'; +import Severity from 'vs/base/common/severity'; +import * as nls from 'vs/nls'; @Component({ selector: 'dashboard-nav-section', @@ -86,11 +88,16 @@ export class DashboardNavSection extends DashboardTab implements OnDestroy, OnCh if (dashboardTabs && dashboardTabs.length > 0) { let selectedTabs = dashboardTabs.map(v => { - let container = dashboardHelper.getDashboardContainer(v.container); - let key = Object.keys(container)[0]; + let containerResult = dashboardHelper.getDashboardContainer(v.container); + if (!containerResult.result) { + let errorTitle = nls.localize('dashboardNavSection_loadTabError', 'There is an error while loading {0} section. ', v.title); + this.dashboardService.messageService.show(Severity.Error, errorTitle + containerResult.message); + return null; + } + let key = Object.keys(containerResult.container)[0]; if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) { - let configs = Object.values(container)[0]; + let configs = Object.values(containerResult.container)[0]; this._configModifiers.forEach(cb => { configs = cb.apply(this, [configs, this.dashboardService, this.tab.context]); }); @@ -104,20 +111,25 @@ export class DashboardNavSection extends DashboardTab implements OnDestroy, OnCh return { id: v.id, title: v.title, container: { 'grid-container': configs } }; } } - return { id: v.id, title: v.title, container: container }; + return { id: v.id, title: v.title, container: containerResult.container }; }).map(v => { - let config = v as TabConfig; - config.context = this.tab.context; - config.editable = false; - config.canClose = false; - this.addNewTab(config); - return config; + if (v) { + let config = v as TabConfig; + config.context = this.tab.context; + config.editable = false; + config.canClose = false; + this.addNewTab(config); + return config; + } + return null; }); - // put this immediately on the stack so that is ran *after* the tab is rendered - setTimeout(() => { - this._panel.selectTab(selectedTabs[0].id); - }); + if (selectedTabs && selectedTabs[0]) { + // put this immediately on the stack so that is ran *after* the tab is rendered + setTimeout(() => { + this._panel.selectTab(selectedTabs[0].id); + }); + } } } diff --git a/src/sql/parts/dashboard/containers/dashboardNavSection.contribution.ts b/src/sql/parts/dashboard/containers/dashboardNavSection.contribution.ts index 724823e88d..736b8645bc 100644 --- a/src/sql/parts/dashboard/containers/dashboardNavSection.contribution.ts +++ b/src/sql/parts/dashboard/containers/dashboardNavSection.contribution.ts @@ -2,10 +2,15 @@ * 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/platform/extensions/common/extensionsRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; +import { NavSectionConfig } from 'sql/parts/dashboard/common/dashboardWidget'; import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry'; +import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution'; +import { WEBVIEW_CONTAINER } from 'sql/parts/dashboard/containers/dashboardWebviewContainer.contribution'; +import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/parts/dashboard/containers/dashboardGridContainer.contribution'; export const NAV_SECTION = 'nav-section'; @@ -53,4 +58,41 @@ let NavSectionSchema: IJSONSchema = { items: navSectionContainerSchema }; -registerContainerType(NAV_SECTION, NavSectionSchema); \ No newline at end of file +registerContainerType(NAV_SECTION, NavSectionSchema); + +export function validateNavSectionContribution(extension: IExtensionPointUser, 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.')); + } + + let containerKey = Object.keys(section.container)[0]; + let 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; +} \ No newline at end of file diff --git a/src/sql/parts/dashboard/containers/dashboardWidgetContainer.contribution.ts b/src/sql/parts/dashboard/containers/dashboardWidgetContainer.contribution.ts index 24ef0819e3..fec6943df3 100644 --- a/src/sql/parts/dashboard/containers/dashboardWidgetContainer.contribution.ts +++ b/src/sql/parts/dashboard/containers/dashboardWidgetContainer.contribution.ts @@ -2,11 +2,13 @@ * 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/platform/extensions/common/extensionsRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { generateDashboardWidgetSchema } from 'sql/parts/dashboard/pages/dashboardPageContribution'; import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry'; +import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget'; export const WIDGETS_CONTAINER = 'widgets-container'; @@ -18,3 +20,16 @@ let widgetsSchema: IJSONSchema = { registerContainerType(WIDGETS_CONTAINER, widgetsSchema); registerNavSectionContainerType(WIDGETS_CONTAINER, widgetsSchema); + +export function validateWidgetContainerContribution(extension: IExtensionPointUser, WidgetConfigs: object[]): boolean { + let result = true; + WidgetConfigs.forEach(widgetConfig => { + let allKeys = Object.keys(widgetConfig); + let 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; +} \ No newline at end of file