diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index c083fd95be3..72e9d2453fd 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -91,17 +91,17 @@ export interface PluginMetaInfo { version: string; } -export interface PluginConfigTabProps { - meta: T; +export interface PluginConfigPageProps { + plugin: T; query: { [s: string]: any }; // The URL query parameters } -export interface PluginConfigTab { +export interface PluginConfigPage { title: string; // Display icon?: string; id: string; // Unique, in URL - body: ComponentClass>; + body: ComponentClass>; } export class GrafanaPlugin { @@ -112,14 +112,14 @@ export class GrafanaPlugin { angularConfigCtrl?: any; // Show configuration tabs on the plugin page - configTabs?: Array>; + configPages?: Array>; // Tabs on the plugin page - addConfigTab(tab: PluginConfigTab) { - if (!this.configTabs) { - this.configTabs = []; + addConfigPage(tab: PluginConfigPage) { + if (!this.configPages) { + this.configPages = []; } - this.configTabs.push(tab); + this.configPages.push(tab); return this; } } diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx index 575311c2d17..6f9939e882b 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx @@ -19,7 +19,7 @@ const setup = (propOverrides?: object) => { setDataSourceName, updateDataSource: jest.fn(), setIsDefault, - plugin: pluginMock, + query: {}, ...propOverrides, }; @@ -45,7 +45,6 @@ describe('Render', () => { it('should render beta info text', () => { const wrapper = setup({ dataSourceMeta: { ...getMockPlugin(), state: 'beta' }, - plugin: pluginMock, }); expect(wrapper).toMatchSnapshot(); diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index e02684d57fc..5c31b946149 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; +import isString from 'lodash/isString'; // Components import Page from 'app/core/components/Page/Page'; @@ -21,7 +22,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId } from 'app/core/selectors/location'; // Types -import { StoreState } from 'app/types/'; +import { StoreState, UrlQueryMap } from 'app/types/'; import { NavModel, DataSourceSettings, DataSourcePluginMeta } from '@grafana/ui'; import { getDataSourceLoadingNav } from '../state/navModel'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; @@ -38,14 +39,17 @@ export interface Props { updateDataSource: typeof updateDataSource; setIsDefault: typeof setIsDefault; plugin?: GenericDataSourcePlugin; + query: UrlQueryMap; + page?: string; } interface State { dataSource: DataSourceSettings; - plugin: GenericDataSourcePlugin; + plugin?: GenericDataSourcePlugin; isTesting?: boolean; testingMessage?: string; testingStatus?: string; + loadError?: any; } export class DataSourceSettingsPage extends PureComponent { @@ -73,9 +77,17 @@ export class DataSourceSettingsPage extends PureComponent { async componentDidMount() { const { loadDataSource, pageId } = this.props; - await loadDataSource(pageId); - if (!this.state.plugin) { - await this.loadPlugin(); + if (isNaN(pageId)) { + this.setState({ loadError: 'Invalid ID' }); + return; + } + try { + await loadDataSource(pageId); + if (!this.state.plugin) { + await this.loadPlugin(); + } + } catch (err) { + this.setState({ loadError: err }); } } @@ -174,70 +186,133 @@ export class DataSourceSettingsPage extends PureComponent { return this.state.dataSource.id > 0; } + renderLoadError(loadError: any) { + let showDelete = false; + let msg = loadError.toString(); + if (loadError.data) { + if (loadError.data.message) { + msg = loadError.data.message; + } + } else if (isString(loadError)) { + showDelete = true; + } + + const node = { + text: msg, + subTitle: 'Data Source Error', + icon: 'fa fa-fw fa-warning', + }; + const nav = { + node: node, + main: node, + }; + + return ( + + +
+
+ {showDelete && ( + + )} + + Back + +
+
+
+
+ ); + } + + renderConfigPageBody(page: string) { + const { plugin } = this.state; + if (!plugin || !plugin.configPages) { + return null; // still loading + } + + for (const p of plugin.configPages) { + if (p.id === page) { + return ; + } + } + + return
Page Not Found: {page}
; + } + + renderSettings() { + const { dataSourceMeta, setDataSourceName, setIsDefault } = this.props; + const { testingMessage, testingStatus, dataSource, plugin } = this.state; + + return ( +
+ {this.isReadOnly() && this.renderIsReadOnlyMessage()} + {dataSourceMeta.state && ( +
+ + +
+ )} + + setIsDefault(state)} + onNameChange={name => setDataSourceName(name)} + /> + + {plugin && ( + + )} + +
+ {testingMessage && ( +
+
+ {testingStatus === 'error' ? ( + + ) : ( + + )} +
+
+
{testingMessage}
+
+
+ )} +
+ + this.onSubmit(event)} + isReadOnly={this.isReadOnly()} + onDelete={this.onDelete} + onTest={event => this.onTest(event)} + /> + + ); + } + render() { - const { dataSourceMeta, navModel, setDataSourceName, setIsDefault } = this.props; - const { testingMessage, testingStatus, plugin, dataSource } = this.state; + const { navModel, page } = this.props; + const { loadError } = this.state; + + if (loadError) { + return this.renderLoadError(loadError); + } return ( - {this.hasDataSource && ( -
-
- {this.isReadOnly() && this.renderIsReadOnlyMessage()} - {dataSourceMeta.state && ( -
- - -
- )} - - setIsDefault(state)} - onNameChange={name => setDataSourceName(name)} - /> - - {dataSourceMeta.module && plugin && ( - - )} - -
- {testingMessage && ( -
-
- {testingStatus === 'error' ? ( - - ) : ( - - )} -
-
-
- {testingMessage} -
-
-
- )} -
- - this.onSubmit(event)} - isReadOnly={this.isReadOnly()} - onDelete={this.onDelete} - onTest={event => this.onTest(event)} - /> - -
- )} + {this.hasDataSource &&
{page ? this.renderConfigPageBody(page) : this.renderSettings()}
}
); @@ -247,11 +322,19 @@ export class DataSourceSettingsPage extends PureComponent { function mapStateToProps(state: StoreState) { const pageId = getRouteParamsId(state.location); const dataSource = getDataSource(state.dataSources, pageId); + const page = state.location.query.page as string; + return { - navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')), + navModel: getNavModel( + state.navIndex, + page ? `datasource-page-${page}` : `datasource-settings-${pageId}`, + getDataSourceLoadingNav('settings') + ), dataSource: getDataSource(state.dataSources, pageId), dataSourceMeta: getDataSourceMeta(state.dataSources, dataSource.type), pageId: pageId, + query: state.location.query, + page, }; } diff --git a/public/app/features/datasources/settings/PluginSettings.tsx b/public/app/features/datasources/settings/PluginSettings.tsx index 94c054d56ee..a7462cbb45c 100644 --- a/public/app/features/datasources/settings/PluginSettings.tsx +++ b/public/app/features/datasources/settings/PluginSettings.tsx @@ -54,7 +54,7 @@ export class PluginSettings extends PureComponent { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { const { plugin } = this.props; if (!plugin.components.ConfigEditor && this.props.dataSource !== prevProps.dataSource) { this.scopeProps.ctrl.current = _.cloneDeep(this.props.dataSource); diff --git a/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap index 2ca6a2ee28f..063ebf8418d 100644 --- a/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap @@ -153,78 +153,6 @@ exports[`Render should render beta info text 1`] = ` onDefaultChange={[Function]} onNameChange={[Function]} /> -
@@ -257,77 +185,6 @@ exports[`Render should render component 1`] = ` onDefaultChange={[Function]} onNameChange={[Function]} /> -
diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index a74943aa9ec..a0928950069 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -10,6 +10,7 @@ import { StoreState, LocationUpdate } from 'app/types'; import { actionCreatorFactory } from 'app/core/redux'; import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory'; import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache'; +import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; export const dataSourceLoaded = actionCreatorFactory('LOAD_DATA_SOURCE').create(); @@ -52,9 +53,11 @@ export function loadDataSource(id: number): ThunkResult { return async dispatch => { const dataSource = await getBackendSrv().get(`/api/datasources/${id}`); const pluginInfo = (await getPluginSettings(dataSource.type)) as DataSourcePluginMeta; + const plugin = await importDataSourcePlugin(pluginInfo); + dispatch(dataSourceLoaded(dataSource)); dispatch(dataSourceMetaLoaded(pluginInfo)); - dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo))); + dispatch(updateNavIndex(buildNavModel(dataSource, plugin))); }; } diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 6fdc6f5762f..6b5f679451d 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -1,7 +1,10 @@ -import { PluginMeta, DataSourceSettings, PluginType, NavModel, NavModelItem, PluginInclude } from '@grafana/ui'; +import { DataSourceSettings, PluginType, NavModel, NavModelItem, PluginInclude } from '@grafana/ui'; import config from 'app/core/config'; +import { GenericDataSourcePlugin } from '../settings/PluginSettings'; + +export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDataSourcePlugin): NavModelItem { + const pluginMeta = plugin.meta; -export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: PluginMeta): NavModelItem { const navModel = { img: pluginMeta.info.logos.large, id: 'datasource-' + dataSource.id, @@ -20,6 +23,18 @@ export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: Plugin ], }; + if (plugin.configPages) { + for (const page of plugin.configPages) { + navModel.children.push({ + active: false, + text: page.title, + icon: page.icon, + url: `datasources/edit/${dataSource.id}/?page=${page.id}`, + id: `datasource-page-${page.id}`, + }); + } + } + if (pluginMeta.includes && hasDashboards(pluginMeta.includes)) { navModel.children.push({ active: false, @@ -65,28 +80,30 @@ export function getDataSourceLoadingNav(pageName: string): NavModel { user: '', }, { - id: '1', - type: PluginType.datasource, - name: '', - info: { - author: { - name: '', - url: '', + meta: { + id: '1', + type: PluginType.datasource, + name: '', + info: { + author: { + name: '', + url: '', + }, + description: '', + links: [{ name: '', url: '' }], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '', + version: '', }, - description: '', - links: [{ name: '', url: '' }], - logos: { - large: '', - small: '', - }, - screenshots: [], - updated: '', - version: '', + includes: [], + module: '', + baseUrl: '', }, - includes: [], - module: '', - baseUrl: '', - } + } as GenericDataSourcePlugin ); let node: NavModelItem; diff --git a/public/app/features/plugins/PluginPage.tsx b/public/app/features/plugins/PluginPage.tsx index 176c03c6b6c..9da5b097f2a 100644 --- a/public/app/features/plugins/PluginPage.tsx +++ b/public/app/features/plugins/PluginPage.tsx @@ -74,12 +74,12 @@ interface State { loading: boolean; plugin?: GrafanaPlugin; nav: NavModel; - defaultTab: string; // The first configured one or readme + defaultPage: string; // The first configured one or readme } -const TAB_ID_README = 'readme'; -const TAB_ID_DASHBOARDS = 'dashboards'; -const TAB_ID_CONFIG_CTRL = 'config'; +const PAGE_ID_README = 'readme'; +const PAGE_ID_DASHBOARDS = 'dashboards'; +const PAGE_ID_CONFIG_CTRL = 'config'; class PluginPage extends PureComponent { constructor(props: Props) { @@ -87,7 +87,7 @@ class PluginPage extends PureComponent { this.state = { loading: true, nav: getLoadingNav(), - defaultTab: TAB_ID_README, + defaultPage: PAGE_ID_README, }; } @@ -103,14 +103,14 @@ class PluginPage extends PureComponent { } const { meta } = plugin; - let defaultTab: string; - const tabs: NavModelItem[] = []; + let defaultPage: string; + const pages: NavModelItem[] = []; if (true) { - tabs.push({ + pages.push({ text: 'Readme', icon: 'fa fa-fw fa-file-text-o', - url: path + '?tab=' + TAB_ID_README, - id: TAB_ID_README, + url: path + '?page=' + PAGE_ID_README, + id: PAGE_ID_README, }); } @@ -118,42 +118,42 @@ class PluginPage extends PureComponent { if (meta.type === PluginType.app) { // Legacy App Config if (plugin.angularConfigCtrl) { - tabs.push({ + pages.push({ text: 'Config', icon: 'gicon gicon-cog', - url: path + '?tab=' + TAB_ID_CONFIG_CTRL, - id: TAB_ID_CONFIG_CTRL, + url: path + '?page=' + PAGE_ID_CONFIG_CTRL, + id: PAGE_ID_CONFIG_CTRL, }); - defaultTab = TAB_ID_CONFIG_CTRL; + defaultPage = PAGE_ID_CONFIG_CTRL; } - if (plugin.configTabs) { - for (const tab of plugin.configTabs) { - tabs.push({ - text: tab.title, - icon: tab.icon, - url: path + '?tab=' + tab.id, - id: tab.id, + if (plugin.configPages) { + for (const page of plugin.configPages) { + pages.push({ + text: page.title, + icon: page.icon, + url: path + '?page=' + page.id, + id: page.id, }); - if (!defaultTab) { - defaultTab = tab.id; + if (!defaultPage) { + defaultPage = page.id; } } } - // Check for the dashboard tabs + // Check for the dashboard pages if (find(meta.includes, { type: 'dashboard' })) { - tabs.push({ + pages.push({ text: 'Dashboards', icon: 'gicon gicon-dashboard', - url: path + '?tab=' + TAB_ID_DASHBOARDS, - id: TAB_ID_DASHBOARDS, + url: path + '?page=' + PAGE_ID_DASHBOARDS, + id: PAGE_ID_DASHBOARDS, }); } } - if (!defaultTab) { - defaultTab = tabs[0].id; // the first tab + if (!defaultPage) { + defaultPage = pages[0].id; // the first tab } const node = { @@ -162,13 +162,13 @@ class PluginPage extends PureComponent { subTitle: meta.info.author.name, breadcrumbs: [{ title: 'Plugins', url: '/plugins' }], url: path, - children: this.setActiveTab(query.tab as string, tabs, defaultTab), + children: this.setActivePage(query.page as string, pages, defaultPage), }; this.setState({ loading: false, plugin, - defaultTab, + defaultPage, nav: { node: node, main: node, @@ -176,15 +176,15 @@ class PluginPage extends PureComponent { }); } - setActiveTab(tabId: string, tabs: NavModelItem[], defaultTabId: string): NavModelItem[] { + setActivePage(pageId: string, pages: NavModelItem[], defaultPageId: string): NavModelItem[] { let found = false; - const selected = tabId || defaultTabId; - const changed = tabs.map(tab => { - const active = !found && selected === tab.id; + const selected = pageId || defaultPageId; + const changed = pages.map(p => { + const active = !found && selected === p.id; if (active) { found = true; } - return { ...tab, active }; + return { ...p, active }; }); if (!found) { changed[0].active = true; @@ -193,13 +193,13 @@ class PluginPage extends PureComponent { } componentDidUpdate(prevProps: Props) { - const prevTab = prevProps.query.tab as string; - const tab = this.props.query.tab as string; - if (prevTab !== tab) { - const { nav, defaultTab } = this.state; + const prevPage = prevProps.query.page as string; + const page = this.props.query.page as string; + if (prevPage !== page) { + const { nav, defaultPage } = this.state; const node = { ...nav.node, - children: this.setActiveTab(tab, nav.node.children, defaultTab), + children: this.setActivePage(page, nav.node.children, defaultPage), }; this.setState({ nav: { @@ -221,21 +221,21 @@ class PluginPage extends PureComponent { const active = nav.main.children.find(tab => tab.active); if (active) { // Find the current config tab - if (plugin.configTabs) { - for (const tab of plugin.configTabs) { + if (plugin.configPages) { + for (const tab of plugin.configPages) { if (tab.id === active.id) { - return ; + return ; } } } // Apps have some special behavior if (plugin.meta.type === PluginType.app) { - if (active.id === TAB_ID_DASHBOARDS) { + if (active.id === PAGE_ID_DASHBOARDS) { return ; } - if (active.id === TAB_ID_CONFIG_CTRL && plugin.angularConfigCtrl) { + if (active.id === PAGE_ID_CONFIG_CTRL && plugin.angularConfigCtrl) { return ; } } diff --git a/public/app/plugins/app/example-app/config/ExampleTab1.tsx b/public/app/plugins/app/example-app/config/ExamplePage1.tsx similarity index 65% rename from public/app/plugins/app/example-app/config/ExampleTab1.tsx rename to public/app/plugins/app/example-app/config/ExamplePage1.tsx index cf79a880f43..c25eee35e53 100644 --- a/public/app/plugins/app/example-app/config/ExampleTab1.tsx +++ b/public/app/plugins/app/example-app/config/ExamplePage1.tsx @@ -2,11 +2,11 @@ import React, { PureComponent } from 'react'; // Types -import { PluginConfigTabProps, AppPluginMeta } from '@grafana/ui'; +import { PluginConfigPageProps, AppPlugin } from '@grafana/ui'; -interface Props extends PluginConfigTabProps {} +interface Props extends PluginConfigPageProps {} -export class ExampleTab1 extends PureComponent { +export class ExamplePage1 extends PureComponent { constructor(props: Props) { super(props); } diff --git a/public/app/plugins/app/example-app/config/ExampleTab2.tsx b/public/app/plugins/app/example-app/config/ExamplePage2.tsx similarity index 65% rename from public/app/plugins/app/example-app/config/ExampleTab2.tsx rename to public/app/plugins/app/example-app/config/ExamplePage2.tsx index bf2ae181405..596bb88e9ba 100644 --- a/public/app/plugins/app/example-app/config/ExampleTab2.tsx +++ b/public/app/plugins/app/example-app/config/ExamplePage2.tsx @@ -2,11 +2,11 @@ import React, { PureComponent } from 'react'; // Types -import { PluginConfigTabProps, AppPluginMeta } from '@grafana/ui'; +import { PluginConfigPageProps, AppPlugin } from '@grafana/ui'; -interface Props extends PluginConfigTabProps {} +interface Props extends PluginConfigPageProps {} -export class ExampleTab2 extends PureComponent { +export class ExamplePage2 extends PureComponent { constructor(props: Props) { super(props); } diff --git a/public/app/plugins/app/example-app/module.ts b/public/app/plugins/app/example-app/module.ts index 0b4a2ae646a..f82f7faec08 100644 --- a/public/app/plugins/app/example-app/module.ts +++ b/public/app/plugins/app/example-app/module.ts @@ -2,8 +2,8 @@ import { ExampleConfigCtrl } from './legacy/config'; import { AngularExamplePageCtrl } from './legacy/angular_example_page'; import { AppPlugin } from '@grafana/ui'; -import { ExampleTab1 } from './config/ExampleTab1'; -import { ExampleTab2 } from './config/ExampleTab2'; +import { ExamplePage1 } from './config/ExamplePage1'; +import { ExamplePage2 } from './config/ExamplePage2'; import { ExampleRootPage } from './ExampleRootPage'; // Legacy exports just for testing @@ -14,15 +14,15 @@ export { export const plugin = new AppPlugin() .setRootPage(ExampleRootPage) - .addConfigTab({ - title: 'Tab 1', + .addConfigPage({ + title: 'Page 1', icon: 'fa fa-info', - body: ExampleTab1, - id: 'tab1', + body: ExamplePage1, + id: 'page1', }) - .addConfigTab({ - title: 'Tab 2', + .addConfigPage({ + title: 'Page 2', icon: 'fa fa-user', - body: ExampleTab2, - id: 'tab2', + body: ExamplePage2, + id: 'page2', }); diff --git a/public/app/plugins/datasource/testdata/TestInfoTab.tsx b/public/app/plugins/datasource/testdata/TestInfoTab.tsx new file mode 100644 index 00000000000..686d8b94e4a --- /dev/null +++ b/public/app/plugins/datasource/testdata/TestInfoTab.tsx @@ -0,0 +1,28 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Types +import { PluginConfigPageProps, DataSourcePlugin } from '@grafana/ui'; +import { TestDataDatasource } from './datasource'; + +interface Props extends PluginConfigPageProps> {} + +export class TestInfoTab extends PureComponent { + constructor(props: Props) { + super(props); + } + + render() { + return ( +
+ See github for more information about setting up a reproducable test environment. +
+
+ + Github + +
+
+ ); + } +} diff --git a/public/app/plugins/datasource/testdata/module.tsx b/public/app/plugins/datasource/testdata/module.tsx index 738149451b9..800f61db737 100644 --- a/public/app/plugins/datasource/testdata/module.tsx +++ b/public/app/plugins/datasource/testdata/module.tsx @@ -1,6 +1,7 @@ import { DataSourcePlugin } from '@grafana/ui'; import { TestDataDatasource } from './datasource'; import { TestDataQueryCtrl } from './query_ctrl'; +import { TestInfoTab } from './TestInfoTab'; import { ConfigEditor } from './ConfigEditor'; class TestDataAnnotationsQueryCtrl { @@ -12,4 +13,10 @@ class TestDataAnnotationsQueryCtrl { export const plugin = new DataSourcePlugin(TestDataDatasource) .setConfigEditor(ConfigEditor) .setQueryCtrl(TestDataQueryCtrl) - .setAnnotationQueryCtrl(TestDataAnnotationsQueryCtrl); + .setAnnotationQueryCtrl(TestDataAnnotationsQueryCtrl) + .addConfigPage({ + title: 'Setup', + icon: 'fa fa-list-alt', + body: TestInfoTab, + id: 'setup', + }); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 9a7c9b5d2d5..a863797dea0 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -110,6 +110,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }) .when('/datasources/edit/:id/', { template: '', + reloadOnSearch: false, // for tabs resolve: { component: () => DataSourceSettingsPage, },