mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
committed by
Torkel Ödegaard
parent
1001cd7ac3
commit
a87a763d83
@@ -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;
|
||||
}
|
||||
|
||||
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={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>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 { 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 (
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
{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)));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
28
public/app/plugins/datasource/testdata/TestInfoTab.tsx
vendored
Normal file
28
public/app/plugins/datasource/testdata/TestInfoTab.tsx
vendored
Normal file
@@ -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<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -110,6 +110,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
})
|
||||
.when('/datasources/edit/:id/', {
|
||||
template: '<react-container />',
|
||||
reloadOnSearch: false, // for tabs
|
||||
resolve: {
|
||||
component: () => DataSourceSettingsPage,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user