Commit a87a763d by Ryan McKinley Committed by Torkel Ödegaard

DataSourcePlugin: support custom tabs (#16859)

* use ConfigEditor

* add tabs

* add tabs

* set the nav in state

* remove actions

* reorder imports

* catch plugin loading errors

* better text

* keep props

* fix typo

* update snapshot

* rename tab to page

* add missing pages
parent 1001cd7a
......@@ -91,17 +91,17 @@ export interface PluginMetaInfo {
version: string;
}
export interface PluginConfigTabProps<T extends PluginMeta> {
meta: T;
export interface PluginConfigPageProps<T extends GrafanaPlugin> {
plugin: T;
query: { [s: string]: any }; // The URL query parameters
}
export interface PluginConfigTab<T extends PluginMeta> {
export interface PluginConfigPage<T extends GrafanaPlugin> {
title: string; // Display
icon?: string;
id: string; // Unique, in URL
body: ComponentClass<PluginConfigTabProps<T>>;
body: ComponentClass<PluginConfigPageProps<T>>;
}
export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
......@@ -112,14 +112,14 @@ export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
angularConfigCtrl?: any;
// Show configuration tabs on the plugin page
configTabs?: Array<PluginConfigTab<T>>;
configPages?: Array<PluginConfigPage<GrafanaPlugin>>;
// Tabs on the plugin page
addConfigTab(tab: PluginConfigTab<T>) {
if (!this.configTabs) {
this.configTabs = [];
addConfigPage(tab: PluginConfigPage<GrafanaPlugin>) {
if (!this.configPages) {
this.configPages = [];
}
this.configTabs.push(tab);
this.configPages.push(tab);
return this;
}
}
......@@ -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();
......
......@@ -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<Props, State> {
......@@ -73,9 +77,17 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
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<Props, State> {
return this.state.dataSource.id > 0;
}
render() {
const { dataSourceMeta, navModel, setDataSourceName, setIsDefault } = this.props;
const { testingMessage, testingStatus, plugin, dataSource } = this.state;
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 (
<Page navModel={navModel}>
<Page.Contents isLoading={!this.hasDataSource}>
{this.hasDataSource && (
<div>
<form onSubmit={this.onSubmit}>
{this.isReadOnly() && this.renderIsReadOnlyMessage()}
{dataSourceMeta.state && (
<div className="gf-form">
<label className="gf-form-label width-10">Plugin state</label>
<label className="gf-form-label gf-form-label--transparent">
<PluginStateinfo state={dataSourceMeta.state} />
</label>
</div>
)}
<Page navModel={nav}>
<Page.Contents>
<div>
<div className="gf-form-button-row">
{showDelete && (
<button type="submit" className="btn btn-danger" onClick={this.onDelete}>
Delete
</button>
)}
<a className="btn btn-inverse" href="datasources">
Back
</a>
</div>
</div>
</Page.Contents>
</Page>
);
}
<BasicSettings
dataSourceName={dataSource.name}
isDefault={dataSource.isDefault}
onDefaultChange={state => setIsDefault(state)}
onNameChange={name => setDataSourceName(name)}
/>
{dataSourceMeta.module && plugin && (
<PluginSettings
plugin={plugin}
dataSource={this.state.dataSource}
dataSourceMeta={dataSourceMeta}
onModelChange={this.onModelChange}
/>
)}
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 <p.body plugin={plugin} query={this.props.query} />;
}
}
return <div>Page Not Found: {page}</div>;
}
renderSettings() {
const { dataSourceMeta, setDataSourceName, setIsDefault } = this.props;
const { testingMessage, testingStatus, dataSource, plugin } = this.state;
<div className="gf-form-group">
{testingMessage && (
<div className={`alert-${testingStatus} alert`} aria-label="Datasource settings page Alert">
<div className="alert-icon">
{testingStatus === 'error' ? (
<i className="fa fa-exclamation-triangle" />
) : (
<i className="fa fa-check" />
)}
</div>
<div className="alert-body">
<div className="alert-title" aria-label="Datasource settings page Alert message">
{testingMessage}
</div>
</div>
</div>
)}
</div>
<ButtonRow
onSubmit={event => this.onSubmit(event)}
isReadOnly={this.isReadOnly()}
onDelete={this.onDelete}
onTest={event => this.onTest(event)}
/>
</form>
return (
<form onSubmit={this.onSubmit}>
{this.isReadOnly() && this.renderIsReadOnlyMessage()}
{dataSourceMeta.state && (
<div className="gf-form">
<label className="gf-form-label width-10">Plugin state</label>
<label className="gf-form-label gf-form-label--transparent">
<PluginStateinfo state={dataSourceMeta.state} />
</label>
</div>
)}
<BasicSettings
dataSourceName={dataSource.name}
isDefault={dataSource.isDefault}
onDefaultChange={state => setIsDefault(state)}
onNameChange={name => setDataSourceName(name)}
/>
{plugin && (
<PluginSettings
plugin={plugin}
dataSource={this.state.dataSource}
dataSourceMeta={dataSourceMeta}
onModelChange={this.onModelChange}
/>
)}
<div className="gf-form-group">
{testingMessage && (
<div className={`alert-${testingStatus} alert`}>
<div className="alert-icon">
{testingStatus === 'error' ? (
<i className="fa fa-exclamation-triangle" />
) : (
<i className="fa fa-check" />
)}
</div>
<div className="alert-body">
<div className="alert-title">{testingMessage}</div>
</div>
</div>
)}
</div>
<ButtonRow
onSubmit={event => this.onSubmit(event)}
isReadOnly={this.isReadOnly()}
onDelete={this.onDelete}
onTest={event => this.onTest(event)}
/>
</form>
);
}
render() {
const { navModel, page } = this.props;
const { loadError } = this.state;
if (loadError) {
return this.renderLoadError(loadError);
}
return (
<Page navModel={navModel}>
<Page.Contents isLoading={!this.hasDataSource}>
{this.hasDataSource && <div>{page ? this.renderConfigPageBody(page) : this.renderSettings()}</div>}
</Page.Contents>
</Page>
);
......@@ -247,11 +322,19 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
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,
};
}
......
......@@ -54,7 +54,7 @@ export class PluginSettings extends PureComponent<Props> {
}
}
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);
......
......@@ -153,78 +153,6 @@ exports[`Render should render beta info text 1`] = `
onDefaultChange={[Function]}
onNameChange={[Function]}
/>
<PluginSettings
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
"withCredentials": false,
}
}
dataSourceMeta={
Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "1",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
Object {
"name": "project",
"url": "one link",
},
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": Array [
Object {
"path": "screenshot",
},
],
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1",
"module": "path/to/module",
"name": "pretty cool plugin 1",
"pinned": false,
"state": "beta",
"type": "panel",
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
}
}
/>
<div
className="gf-form-group"
/>
......@@ -257,77 +185,6 @@ exports[`Render should render component 1`] = `
onDefaultChange={[Function]}
onNameChange={[Function]}
/>
<PluginSettings
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
"withCredentials": false,
}
}
dataSourceMeta={
Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "1",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
Object {
"name": "project",
"url": "one link",
},
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": Array [
Object {
"path": "screenshot",
},
],
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1",
"module": "path/to/module",
"name": "pretty cool plugin 1",
"pinned": false,
"type": "panel",
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
}
}
/>
<div
className="gf-form-group"
/>
......
......@@ -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<DataSourceSettings>('LOAD_DATA_SOURCE').create();
......@@ -52,9 +53,11 @@ export function loadDataSource(id: number): ThunkResult<void> {
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)));
};
}
......
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: '',
},
description: '',
links: [{ name: '', url: '' }],
logos: {
large: '',
small: '',
meta: {
id: '1',
type: PluginType.datasource,
name: '',
info: {
author: {
name: '',
url: '',
},
description: '',
links: [{ name: '', url: '' }],
logos: {
large: '',
small: '',
},
screenshots: [],
updated: '',
version: '',
},
screenshots: [],
updated: '',
version: '',
includes: [],
module: '',
baseUrl: '',
},
includes: [],
module: '',
baseUrl: '',
}
} as GenericDataSourcePlugin
);
let node: NavModelItem;
......
......@@ -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<Props, State> {
constructor(props: Props) {
......@@ -87,7 +87,7 @@ class PluginPage extends PureComponent<Props, State> {
this.state = {
loading: true,
nav: getLoadingNav(),
defaultTab: TAB_ID_README,
defaultPage: PAGE_ID_README,
};
}
......@@ -103,14 +103,14 @@ class PluginPage extends PureComponent<Props, State> {
}
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<Props, State> {
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<Props, State> {
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<Props, State> {
});
}
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<Props, State> {
}
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<Props, State> {
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 <tab.body meta={plugin.meta} query={query} />;
return <tab.body plugin={plugin} query={query} />;
}
}
}
// Apps have some special behavior
if (plugin.meta.type === PluginType.app) {
if (active.id === TAB_ID_DASHBOARDS) {
if (active.id === PAGE_ID_DASHBOARDS) {
return <PluginDashboards plugin={plugin.meta} />;
}
if (active.id === TAB_ID_CONFIG_CTRL && plugin.angularConfigCtrl) {
if (active.id === PAGE_ID_CONFIG_CTRL && plugin.angularConfigCtrl) {
return <AppConfigCtrlWrapper app={plugin as AppPlugin} />;
}
}
......
......@@ -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<AppPluginMeta> {}
interface Props extends PluginConfigPageProps<AppPlugin> {}
export class ExampleTab1 extends PureComponent<Props> {
export class ExamplePage1 extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
......
......@@ -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<AppPluginMeta> {}
interface Props extends PluginConfigPageProps<AppPlugin> {}
export class ExampleTab2 extends PureComponent<Props> {
export class ExamplePage2 extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
......
......@@ -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',
});
// Libraries
import React, { PureComponent } from 'react';
// Types
import { PluginConfigPageProps, DataSourcePlugin } from '@grafana/ui';
import { TestDataDatasource } from './datasource';
interface Props extends PluginConfigPageProps<DataSourcePlugin<TestDataDatasource>> {}
export class TestInfoTab extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
render() {
return (
<div>
See github for more information about setting up a reproducable test environment.
<br />
<br />
<a className="btn btn-inverse" href="https://github.com/grafana/grafana/tree/master/devenv" target="_blank">
Github
</a>
<br />
</div>
);
}
}
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',
});
......@@ -110,6 +110,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
})
.when('/datasources/edit/:id/', {
template: '<react-container />',
reloadOnSearch: false, // for tabs
resolve: {
component: () => DataSourceSettingsPage,
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment