mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Prevents creating alerts from unsupported queries (#19250)
* Refactor: Makes PanelEditor use state and shows validation message on AlerTab * Refactor: Makes validation message nicer looking * Refactor: Changes imports * Refactor: Removes conditional props * Refactor: Changes after feedback from PR review * Refactor: Removes unused action
This commit is contained in:
parent
68d6da77da
commit
9bd6ed887c
@ -1,34 +1,45 @@
|
|||||||
// Libraries
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { Alert, Button } from '@grafana/ui';
|
||||||
|
|
||||||
// Services & Utils
|
import { AngularComponent, getAngularLoader, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { getAlertingValidationMessage } from './getAlertingValidationMessage';
|
||||||
|
|
||||||
// Components
|
|
||||||
import { EditorTabBody, EditorToolbarView } from '../dashboard/panel_editor/EditorTabBody';
|
import { EditorTabBody, EditorToolbarView } from '../dashboard/panel_editor/EditorTabBody';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import StateHistory from './StateHistory';
|
import StateHistory from './StateHistory';
|
||||||
import 'app/features/alerting/AlertTabCtrl';
|
import 'app/features/alerting/AlertTabCtrl';
|
||||||
import { Alert } from '@grafana/ui';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
||||||
import { PanelModel } from '../dashboard/state/PanelModel';
|
import { PanelModel } from '../dashboard/state/PanelModel';
|
||||||
import { TestRuleResult } from './TestRuleResult';
|
import { TestRuleResult } from './TestRuleResult';
|
||||||
import { AppNotificationSeverity } from 'app/types';
|
import { AppNotificationSeverity, StoreState } from 'app/types';
|
||||||
|
import { PanelEditorTabIds, getPanelEditorTab } from '../dashboard/panel_editor/state/reducers';
|
||||||
|
import { changePanelEditorTab } from '../dashboard/panel_editor/state/actions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
angularPanel?: AngularComponent;
|
angularPanel?: AngularComponent;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
|
changePanelEditorTab: typeof changePanelEditorTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AlertTab extends PureComponent<Props> {
|
interface State {
|
||||||
|
validatonMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||||
element: any;
|
element: any;
|
||||||
component: AngularComponent;
|
component: AngularComponent;
|
||||||
panelCtrl: any;
|
panelCtrl: any;
|
||||||
|
|
||||||
|
state: State = {
|
||||||
|
validatonMessage: '',
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.shouldLoadAlertTab()) {
|
if (this.shouldLoadAlertTab()) {
|
||||||
this.loadAlertTab();
|
this.loadAlertTab();
|
||||||
@ -51,8 +62,8 @@ export class AlertTab extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAlertTab() {
|
async loadAlertTab() {
|
||||||
const { angularPanel } = this.props;
|
const { angularPanel, panel } = this.props;
|
||||||
|
|
||||||
const scope = angularPanel.getScope();
|
const scope = angularPanel.getScope();
|
||||||
|
|
||||||
@ -71,6 +82,17 @@ export class AlertTab extends PureComponent<Props> {
|
|||||||
const scopeProps = { ctrl: this.panelCtrl };
|
const scopeProps = { ctrl: this.panelCtrl };
|
||||||
|
|
||||||
this.component = loader.load(this.element, scopeProps, template);
|
this.component = loader.load(this.element, scopeProps, template);
|
||||||
|
|
||||||
|
const validatonMessage = await getAlertingValidationMessage(
|
||||||
|
panel.transformations,
|
||||||
|
panel.targets,
|
||||||
|
getDataSourceSrv(),
|
||||||
|
panel.datasource
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validatonMessage) {
|
||||||
|
this.setState({ validatonMessage });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stateHistory = (): EditorToolbarView => {
|
stateHistory = (): EditorToolbarView => {
|
||||||
@ -128,19 +150,39 @@ export class AlertTab extends PureComponent<Props> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
switchToQueryTab = () => {
|
||||||
|
const { changePanelEditorTab } = this.props;
|
||||||
|
changePanelEditorTab(getPanelEditorTab(PanelEditorTabIds.Queries));
|
||||||
|
};
|
||||||
|
|
||||||
|
renderValidationMessage = () => {
|
||||||
|
const { validatonMessage } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
width: 508px;
|
||||||
|
margin: 128px auto;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<h2>{validatonMessage}</h2>
|
||||||
|
<br />
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<Button size={'md'} variant={'secondary'} icon="fa fa-arrow-left" onClick={this.switchToQueryTab}>
|
||||||
|
Go back to Queries
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { alert, transformations } = this.props.panel;
|
const { alert, transformations } = this.props.panel;
|
||||||
const hasTransformations = transformations && transformations.length;
|
const { validatonMessage } = this.state;
|
||||||
|
const hasTransformations = transformations && transformations.length > 0;
|
||||||
|
|
||||||
if (!alert && hasTransformations) {
|
if (!alert && validatonMessage) {
|
||||||
return (
|
return this.renderValidationMessage();
|
||||||
<EditorTabBody heading="Alert">
|
|
||||||
<Alert
|
|
||||||
severity={AppNotificationSeverity.Warning}
|
|
||||||
title="Transformations are not supported in alert queries"
|
|
||||||
/>
|
|
||||||
</EditorTabBody>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbarItems = alert ? [this.stateHistory(), this.testRule(), this.deleteAlert()] : [];
|
const toolbarItems = alert ? [this.stateHistory(), this.testRule(), this.deleteAlert()] : [];
|
||||||
@ -163,9 +205,20 @@ export class AlertTab extends PureComponent<Props> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={element => (this.element = element)} />
|
<div ref={element => (this.element = element)} />
|
||||||
{!alert && <EmptyListCTA {...model} />}
|
{!alert && !validatonMessage && <EmptyListCTA {...model} />}
|
||||||
</>
|
</>
|
||||||
</EditorTabBody>
|
</EditorTabBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mapStateToProps = (state: StoreState) => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = { changePanelEditorTab };
|
||||||
|
|
||||||
|
export const AlertTab = hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(UnConnectedAlertTab)
|
||||||
|
);
|
||||||
|
@ -10,6 +10,7 @@ import { DashboardSrv } from '../dashboard/services/DashboardSrv';
|
|||||||
import DatasourceSrv from '../plugins/datasource_srv';
|
import DatasourceSrv from '../plugins/datasource_srv';
|
||||||
import { DataQuery } from '@grafana/ui/src/types/datasource';
|
import { DataQuery } from '@grafana/ui/src/types/datasource';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
|
import { getDefaultCondition } from './getAlertingValidationMessage';
|
||||||
|
|
||||||
export class AlertTabCtrl {
|
export class AlertTabCtrl {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -179,7 +180,7 @@ export class AlertTabCtrl {
|
|||||||
|
|
||||||
alert.conditions = alert.conditions || [];
|
alert.conditions = alert.conditions || [];
|
||||||
if (alert.conditions.length === 0) {
|
if (alert.conditions.length === 0) {
|
||||||
alert.conditions.push(this.buildDefaultCondition());
|
alert.conditions.push(getDefaultCondition());
|
||||||
}
|
}
|
||||||
|
|
||||||
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
|
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
|
||||||
@ -241,16 +242,6 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDefaultCondition() {
|
|
||||||
return {
|
|
||||||
type: 'query',
|
|
||||||
query: { params: ['A', '5m', 'now'] },
|
|
||||||
reducer: { type: 'avg', params: [] as any[] },
|
|
||||||
evaluator: { type: 'gt', params: [null] as any[] },
|
|
||||||
operator: { type: 'and' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
validateModel() {
|
validateModel() {
|
||||||
if (!this.alert) {
|
if (!this.alert) {
|
||||||
return;
|
return;
|
||||||
@ -348,7 +339,7 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addCondition(type: string) {
|
addCondition(type: string) {
|
||||||
const condition = this.buildDefaultCondition();
|
const condition = getDefaultCondition();
|
||||||
// add to persited model
|
// add to persited model
|
||||||
this.alert.conditions.push(condition);
|
this.alert.conditions.push(condition);
|
||||||
// add to view model
|
// add to view model
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import { DataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { DataSourceApi, PluginMeta } from '@grafana/ui';
|
||||||
|
import { DataTransformerConfig } from '@grafana/data';
|
||||||
|
|
||||||
|
import { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types';
|
||||||
|
import { getAlertingValidationMessage } from './getAlertingValidationMessage';
|
||||||
|
|
||||||
|
describe('getAlertingValidationMessage', () => {
|
||||||
|
describe('when called with some targets containing template variables', () => {
|
||||||
|
it('then it should return false', async () => {
|
||||||
|
let call = 0;
|
||||||
|
const datasource: DataSourceApi = ({
|
||||||
|
meta: ({ alerting: true } as any) as PluginMeta,
|
||||||
|
targetContainsTemplate: () => {
|
||||||
|
if (call === 0) {
|
||||||
|
call++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
name: 'some name',
|
||||||
|
} as any) as DataSourceApi;
|
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
|
const datasourceSrv: DataSourceSrv = {
|
||||||
|
get: getMock,
|
||||||
|
};
|
||||||
|
const targets: ElasticsearchQuery[] = [
|
||||||
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||||
|
{ refId: 'B', query: '@instance:instance', isLogsQuery: false },
|
||||||
|
];
|
||||||
|
const transformations: DataTransformerConfig[] = [];
|
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||||
|
|
||||||
|
expect(result).toBe('');
|
||||||
|
expect(getMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getMock).toHaveBeenCalledWith(datasource.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with some targets using a datasource that does not support alerting', () => {
|
||||||
|
it('then it should return false', async () => {
|
||||||
|
const alertingDatasource: DataSourceApi = ({
|
||||||
|
meta: ({ alerting: true } as any) as PluginMeta,
|
||||||
|
targetContainsTemplate: () => false,
|
||||||
|
name: 'alertingDatasource',
|
||||||
|
} as any) as DataSourceApi;
|
||||||
|
const datasource: DataSourceApi = ({
|
||||||
|
meta: ({ alerting: false } as any) as PluginMeta,
|
||||||
|
targetContainsTemplate: () => false,
|
||||||
|
name: 'datasource',
|
||||||
|
} as any) as DataSourceApi;
|
||||||
|
|
||||||
|
const datasourceSrv: DataSourceSrv = {
|
||||||
|
get: (name: string) => {
|
||||||
|
if (name === datasource.name) {
|
||||||
|
return Promise.resolve(datasource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(alertingDatasource);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const targets: any[] = [
|
||||||
|
{ refId: 'A', query: 'some query', datasource: 'alertingDatasource' },
|
||||||
|
{ refId: 'B', query: 'some query', datasource: 'datasource' },
|
||||||
|
];
|
||||||
|
const transformations: DataTransformerConfig[] = [];
|
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||||
|
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with all targets containing template variables', () => {
|
||||||
|
it('then it should return false', async () => {
|
||||||
|
const datasource: DataSourceApi = ({
|
||||||
|
meta: ({ alerting: true } as any) as PluginMeta,
|
||||||
|
targetContainsTemplate: () => true,
|
||||||
|
name: 'some name',
|
||||||
|
} as any) as DataSourceApi;
|
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
|
const datasourceSrv: DataSourceSrv = {
|
||||||
|
get: getMock,
|
||||||
|
};
|
||||||
|
const targets: ElasticsearchQuery[] = [
|
||||||
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||||
|
{ refId: 'B', query: '@instance:$instance', isLogsQuery: false },
|
||||||
|
];
|
||||||
|
const transformations: DataTransformerConfig[] = [];
|
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||||
|
|
||||||
|
expect(result).toBe('Template variables are not supported in alert queries');
|
||||||
|
expect(getMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getMock).toHaveBeenCalledWith(datasource.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with all targets using a datasource that does not support alerting', () => {
|
||||||
|
it('then it should return false', async () => {
|
||||||
|
const datasource: DataSourceApi = ({
|
||||||
|
meta: ({ alerting: false } as any) as PluginMeta,
|
||||||
|
targetContainsTemplate: () => false,
|
||||||
|
name: 'some name',
|
||||||
|
} as any) as DataSourceApi;
|
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
|
const datasourceSrv: DataSourceSrv = {
|
||||||
|
get: getMock,
|
||||||
|
};
|
||||||
|
const targets: ElasticsearchQuery[] = [
|
||||||
|
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
||||||
|
{ refId: 'B', query: '@instance:instance', isLogsQuery: false },
|
||||||
|
];
|
||||||
|
const transformations: DataTransformerConfig[] = [];
|
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||||
|
|
||||||
|
expect(result).toBe('The datasource does not support alerting queries');
|
||||||
|
expect(getMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getMock).toHaveBeenCalledWith(datasource.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with transformations', () => {
|
||||||
|
it('then it should return false', async () => {
|
||||||
|
const datasource: DataSourceApi = ({
|
||||||
|
meta: ({ alerting: true } as any) as PluginMeta,
|
||||||
|
targetContainsTemplate: () => false,
|
||||||
|
name: 'some name',
|
||||||
|
} as any) as DataSourceApi;
|
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
|
const datasourceSrv: DataSourceSrv = {
|
||||||
|
get: getMock,
|
||||||
|
};
|
||||||
|
const targets: ElasticsearchQuery[] = [
|
||||||
|
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false },
|
||||||
|
{ refId: 'B', query: '@instance:instance', isLogsQuery: false },
|
||||||
|
];
|
||||||
|
const transformations: DataTransformerConfig[] = [{ id: 'A', options: null }];
|
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||||
|
|
||||||
|
expect(result).toBe('Transformations are not supported in alert queries');
|
||||||
|
expect(getMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
49
public/app/features/alerting/getAlertingValidationMessage.ts
Normal file
49
public/app/features/alerting/getAlertingValidationMessage.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { DataQuery } from '@grafana/ui';
|
||||||
|
import { DataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { DataTransformerConfig } from '@grafana/data';
|
||||||
|
|
||||||
|
export const getDefaultCondition = () => ({
|
||||||
|
type: 'query',
|
||||||
|
query: { params: ['A', '5m', 'now'] },
|
||||||
|
reducer: { type: 'avg', params: [] as any[] },
|
||||||
|
evaluator: { type: 'gt', params: [null] as any[] },
|
||||||
|
operator: { type: 'and' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getAlertingValidationMessage = async (
|
||||||
|
transformations: DataTransformerConfig[],
|
||||||
|
targets: DataQuery[],
|
||||||
|
datasourceSrv: DataSourceSrv,
|
||||||
|
datasourceName: string
|
||||||
|
): Promise<string> => {
|
||||||
|
if (targets.length === 0) {
|
||||||
|
return 'Could not find any metric queries';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformations && transformations.length) {
|
||||||
|
return 'Transformations are not supported in alert queries';
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertingNotSupported = 0;
|
||||||
|
let templateVariablesNotSupported = 0;
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const dsName = target.datasource || datasourceName;
|
||||||
|
const ds = await datasourceSrv.get(dsName);
|
||||||
|
if (!ds.meta.alerting) {
|
||||||
|
alertingNotSupported++;
|
||||||
|
} else if (ds.targetContainsTemplate && ds.targetContainsTemplate(target)) {
|
||||||
|
templateVariablesNotSupported++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertingNotSupported === targets.length) {
|
||||||
|
return 'The datasource does not support alerting queries';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateVariablesNotSupported === targets.length) {
|
||||||
|
return 'Template variables are not supported in alert queries';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
@ -1,19 +1,19 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Tooltip, PanelPlugin, PanelPluginMeta } from '@grafana/ui';
|
||||||
|
import { AngularComponent, config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { QueriesTab } from './QueriesTab';
|
import { QueriesTab } from './QueriesTab';
|
||||||
import VisualizationTab from './VisualizationTab';
|
import VisualizationTab from './VisualizationTab';
|
||||||
import { GeneralTab } from './GeneralTab';
|
import { GeneralTab } from './GeneralTab';
|
||||||
import { AlertTab } from '../../alerting/AlertTab';
|
import { AlertTab } from '../../alerting/AlertTab';
|
||||||
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { store } from 'app/store/store';
|
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import { AngularComponent } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { PanelModel } from '../state/PanelModel';
|
import { PanelModel } from '../state/PanelModel';
|
||||||
import { DashboardModel } from '../state/DashboardModel';
|
import { DashboardModel } from '../state/DashboardModel';
|
||||||
import { Tooltip, PanelPlugin, PanelPluginMeta } from '@grafana/ui';
|
import { StoreState } from '../../../types';
|
||||||
|
import { PanelEditorTabIds, PanelEditorTab } from './state/reducers';
|
||||||
|
import { refreshPanelEditor, changePanelEditorTab, panelEditorCleanUp } from './state/actions';
|
||||||
|
|
||||||
interface PanelEditorProps {
|
interface PanelEditorProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -21,56 +21,54 @@ interface PanelEditorProps {
|
|||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
angularPanel?: AngularComponent;
|
angularPanel?: AngularComponent;
|
||||||
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
||||||
|
activeTab: PanelEditorTabIds;
|
||||||
|
tabs: PanelEditorTab[];
|
||||||
|
refreshPanelEditor: typeof refreshPanelEditor;
|
||||||
|
panelEditorCleanUp: typeof panelEditorCleanUp;
|
||||||
|
changePanelEditorTab: typeof changePanelEditorTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PanelEditorTab {
|
class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PanelEditorTabIds {
|
|
||||||
Queries = 'queries',
|
|
||||||
Visualization = 'visualization',
|
|
||||||
Advanced = 'advanced',
|
|
||||||
Alert = 'alert',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PanelEditorTab {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelEditorTabTexts = {
|
|
||||||
[PanelEditorTabIds.Queries]: 'Queries',
|
|
||||||
[PanelEditorTabIds.Visualization]: 'Visualization',
|
|
||||||
[PanelEditorTabIds.Advanced]: 'General',
|
|
||||||
[PanelEditorTabIds.Alert]: 'Alert',
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
|
|
||||||
return {
|
|
||||||
id: tabId,
|
|
||||||
text: panelEditorTabTexts[tabId],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PanelEditor extends PureComponent<PanelEditorProps> {
|
|
||||||
constructor(props: PanelEditorProps) {
|
constructor(props: PanelEditorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.refreshFromState();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
const { panelEditorCleanUp } = this.props;
|
||||||
|
panelEditorCleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshFromState = (meta?: PanelPluginMeta) => {
|
||||||
|
const { refreshPanelEditor, plugin } = this.props;
|
||||||
|
meta = meta || plugin.meta;
|
||||||
|
|
||||||
|
refreshPanelEditor({
|
||||||
|
hasQueriesTab: !meta.skipDataQuery,
|
||||||
|
usesGraphPlugin: meta.id === 'graph',
|
||||||
|
alertingEnabled: config.alertingEnabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onChangeTab = (tab: PanelEditorTab) => {
|
onChangeTab = (tab: PanelEditorTab) => {
|
||||||
store.dispatch(
|
const { changePanelEditorTab } = this.props;
|
||||||
updateLocation({
|
// Angular Query Components can potentially refresh the PanelModel
|
||||||
query: { tab: tab.id, openVizPicker: null },
|
// onBlur so this makes sure we change tab after that
|
||||||
partial: true,
|
setTimeout(() => changePanelEditorTab(tab), 10);
|
||||||
})
|
};
|
||||||
);
|
|
||||||
this.forceUpdate();
|
onPluginTypeChange = (newType: PanelPluginMeta) => {
|
||||||
|
const { onPluginTypeChange } = this.props;
|
||||||
|
onPluginTypeChange(newType);
|
||||||
|
|
||||||
|
this.refreshFromState(newType);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderCurrentTab(activeTab: string) {
|
renderCurrentTab(activeTab: string) {
|
||||||
const { panel, dashboard, onPluginTypeChange, plugin, angularPanel } = this.props;
|
const { panel, dashboard, plugin, angularPanel } = this.props;
|
||||||
|
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'advanced':
|
case 'advanced':
|
||||||
@ -85,7 +83,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
|||||||
panel={panel}
|
panel={panel}
|
||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
onPluginTypeChange={onPluginTypeChange}
|
onPluginTypeChange={this.onPluginTypeChange}
|
||||||
angularPanel={angularPanel}
|
angularPanel={angularPanel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -95,28 +93,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { plugin } = this.props;
|
const { activeTab, tabs } = this.props;
|
||||||
let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries;
|
|
||||||
|
|
||||||
const tabs: PanelEditorTab[] = [
|
|
||||||
getPanelEditorTab(PanelEditorTabIds.Queries),
|
|
||||||
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
|
||||||
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
|
||||||
];
|
|
||||||
|
|
||||||
// handle panels that do not have queries tab
|
|
||||||
if (plugin.meta.skipDataQuery) {
|
|
||||||
// remove queries tab
|
|
||||||
tabs.shift();
|
|
||||||
// switch tab
|
|
||||||
if (activeTab === PanelEditorTabIds.Queries) {
|
|
||||||
activeTab = PanelEditorTabIds.Visualization;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.alertingEnabled && plugin.meta.id === 'graph') {
|
|
||||||
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-editor-container__editor">
|
<div className="panel-editor-container__editor">
|
||||||
@ -131,6 +108,20 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mapStateToProps = (state: StoreState) => ({
|
||||||
|
activeTab: state.location.query.tab || PanelEditorTabIds.Queries,
|
||||||
|
tabs: state.panelEditor.tabs,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = { refreshPanelEditor, panelEditorCleanUp, changePanelEditorTab };
|
||||||
|
|
||||||
|
export const PanelEditor = hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(UnConnectedPanelEditor)
|
||||||
|
);
|
||||||
|
|
||||||
interface TabItemParams {
|
interface TabItemParams {
|
||||||
tab: PanelEditorTab;
|
tab: PanelEditorTab;
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
|
127
public/app/features/dashboard/panel_editor/state/actions.test.ts
Normal file
127
public/app/features/dashboard/panel_editor/state/actions.test.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { thunkTester } from '../../../../../test/core/thunk/thunkTester';
|
||||||
|
import { initialState, getPanelEditorTab, PanelEditorTabIds } from './reducers';
|
||||||
|
import { refreshPanelEditor, panelEditorInitCompleted, changePanelEditorTab } from './actions';
|
||||||
|
import { updateLocation } from '../../../../core/actions';
|
||||||
|
|
||||||
|
describe('refreshPanelEditor', () => {
|
||||||
|
describe('when called and there is no activeTab in state', () => {
|
||||||
|
it('then the dispatched action should default the activeTab to PanelEditorTabIds.Queries', async () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Queries;
|
||||||
|
const tabs = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert),
|
||||||
|
];
|
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState, activeTab: null } })
|
||||||
|
.givenThunk(refreshPanelEditor)
|
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true });
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called and there is already an activeTab in state', () => {
|
||||||
|
it('then the dispatched action should include activeTab from state', async () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Visualization;
|
||||||
|
const tabs = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert),
|
||||||
|
];
|
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState, activeTab } })
|
||||||
|
.givenThunk(refreshPanelEditor)
|
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true });
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called and plugin has no queries tab', () => {
|
||||||
|
it('then the dispatched action should not include Queries tab and default the activeTab to PanelEditorTabIds.Visualization', async () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Visualization;
|
||||||
|
const tabs = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert),
|
||||||
|
];
|
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
|
||||||
|
.givenThunk(refreshPanelEditor)
|
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: false, alertingEnabled: true, usesGraphPlugin: true });
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called and alerting is enabled and the visualization is the graph plugin', () => {
|
||||||
|
it('then the dispatched action should include the alert tab', async () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Queries;
|
||||||
|
const tabs = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert),
|
||||||
|
];
|
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
|
||||||
|
.givenThunk(refreshPanelEditor)
|
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true });
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called and alerting is not enabled', () => {
|
||||||
|
it('then the dispatched action should not include the alert tab', async () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Queries;
|
||||||
|
const tabs = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
];
|
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
|
||||||
|
.givenThunk(refreshPanelEditor)
|
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: false, usesGraphPlugin: true });
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called and the visualization is not the graph plugin', () => {
|
||||||
|
it('then the dispatched action should not include the alert tab', async () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Queries;
|
||||||
|
const tabs = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
];
|
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
|
||||||
|
.givenThunk(refreshPanelEditor)
|
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: false });
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changePanelEditorTab', () => {
|
||||||
|
describe('when called', () => {
|
||||||
|
it('then it should dispatch correct actions', async () => {
|
||||||
|
const activeTab = getPanelEditorTab(PanelEditorTabIds.Visualization);
|
||||||
|
const dispatchedActions = await thunkTester({})
|
||||||
|
.givenThunk(changePanelEditorTab)
|
||||||
|
.whenThunkIsDispatched(activeTab);
|
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1);
|
||||||
|
expect(dispatchedActions).toEqual([
|
||||||
|
updateLocation({ query: { tab: activeTab.id, openVizPicker: null }, partial: true }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
54
public/app/features/dashboard/panel_editor/state/actions.ts
Normal file
54
public/app/features/dashboard/panel_editor/state/actions.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { actionCreatorFactory, noPayloadActionCreatorFactory } from '../../../../core/redux';
|
||||||
|
import { PanelEditorTabIds, PanelEditorTab, getPanelEditorTab } from './reducers';
|
||||||
|
import { ThunkResult } from '../../../../types';
|
||||||
|
import { updateLocation } from '../../../../core/actions';
|
||||||
|
|
||||||
|
export interface PanelEditorInitCompleted {
|
||||||
|
activeTab: PanelEditorTabIds;
|
||||||
|
tabs: PanelEditorTab[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const panelEditorInitCompleted = actionCreatorFactory<PanelEditorInitCompleted>(
|
||||||
|
'PANEL_EDITOR_INIT_COMPLETED'
|
||||||
|
).create();
|
||||||
|
|
||||||
|
export const panelEditorCleanUp = noPayloadActionCreatorFactory('PANEL_EDITOR_CLEAN_UP').create();
|
||||||
|
|
||||||
|
export const refreshPanelEditor = (props: {
|
||||||
|
hasQueriesTab?: boolean;
|
||||||
|
usesGraphPlugin?: boolean;
|
||||||
|
alertingEnabled?: boolean;
|
||||||
|
}): ThunkResult<void> => {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
let activeTab = getState().panelEditor.activeTab || PanelEditorTabIds.Queries;
|
||||||
|
const { hasQueriesTab, usesGraphPlugin, alertingEnabled } = props;
|
||||||
|
|
||||||
|
const tabs: PanelEditorTab[] = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
];
|
||||||
|
|
||||||
|
// handle panels that do not have queries tab
|
||||||
|
if (!hasQueriesTab) {
|
||||||
|
// remove queries tab
|
||||||
|
tabs.shift();
|
||||||
|
// switch tab
|
||||||
|
if (activeTab === PanelEditorTabIds.Queries) {
|
||||||
|
activeTab = PanelEditorTabIds.Visualization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertingEnabled && usesGraphPlugin) {
|
||||||
|
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(panelEditorInitCompleted({ activeTab, tabs }));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changePanelEditorTab = (activeTab: PanelEditorTab): ThunkResult<void> => {
|
||||||
|
return async dispatch => {
|
||||||
|
dispatch(updateLocation({ query: { tab: activeTab.id, openVizPicker: null }, partial: true }));
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { reducerTester } from '../../../../../test/core/redux/reducerTester';
|
||||||
|
import { initialState, panelEditorReducer, PanelEditorTabIds, PanelEditorTab, getPanelEditorTab } from './reducers';
|
||||||
|
import { panelEditorInitCompleted, panelEditorCleanUp } from './actions';
|
||||||
|
|
||||||
|
describe('panelEditorReducer', () => {
|
||||||
|
describe('when panelEditorInitCompleted is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Alert;
|
||||||
|
const tabs: PanelEditorTab[] = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
];
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(panelEditorReducer, initialState)
|
||||||
|
.whenActionIsDispatched(panelEditorInitCompleted({ activeTab, tabs }))
|
||||||
|
.thenStateShouldEqual({ activeTab, tabs });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when panelEditorCleanUp is dispatched', () => {
|
||||||
|
it('then state should be intialState', () => {
|
||||||
|
const activeTab = PanelEditorTabIds.Alert;
|
||||||
|
const tabs: PanelEditorTab[] = [
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||||
|
];
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(panelEditorReducer, { activeTab, tabs })
|
||||||
|
.whenActionIsDispatched(panelEditorCleanUp())
|
||||||
|
.thenStateShouldEqual(initialState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
56
public/app/features/dashboard/panel_editor/state/reducers.ts
Normal file
56
public/app/features/dashboard/panel_editor/state/reducers.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { reducerFactory } from '../../../../core/redux';
|
||||||
|
import { panelEditorCleanUp, panelEditorInitCompleted } from './actions';
|
||||||
|
|
||||||
|
export interface PanelEditorTab {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PanelEditorTabIds {
|
||||||
|
Queries = 'queries',
|
||||||
|
Visualization = 'visualization',
|
||||||
|
Advanced = 'advanced',
|
||||||
|
Alert = 'alert',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const panelEditorTabTexts = {
|
||||||
|
[PanelEditorTabIds.Queries]: 'Queries',
|
||||||
|
[PanelEditorTabIds.Visualization]: 'Visualization',
|
||||||
|
[PanelEditorTabIds.Advanced]: 'General',
|
||||||
|
[PanelEditorTabIds.Alert]: 'Alert',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
|
||||||
|
return {
|
||||||
|
id: tabId,
|
||||||
|
text: panelEditorTabTexts[tabId],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PanelEditorState {
|
||||||
|
activeTab: PanelEditorTabIds;
|
||||||
|
tabs: PanelEditorTab[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: PanelEditorState = {
|
||||||
|
activeTab: null,
|
||||||
|
tabs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const panelEditorReducer = reducerFactory<PanelEditorState>(initialState)
|
||||||
|
.addMapper({
|
||||||
|
filter: panelEditorInitCompleted,
|
||||||
|
mapper: (state, action): PanelEditorState => {
|
||||||
|
const { activeTab, tabs } = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeTab,
|
||||||
|
tabs,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: panelEditorCleanUp,
|
||||||
|
mapper: (): PanelEditorState => initialState,
|
||||||
|
})
|
||||||
|
.create();
|
@ -1,10 +1,8 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataQuery, DataQueryResponseData, PanelPlugin } from '@grafana/ui';
|
import { DataQuery, DataQueryResponseData, PanelPlugin } from '@grafana/ui';
|
||||||
import { DataLink, DataTransformerConfig, ScopedVars } from '@grafana/data';
|
import { DataLink, DataTransformerConfig, ScopedVars } from '@grafana/data';
|
||||||
|
@ -2,11 +2,9 @@
|
|||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { actionCreatorFactory, noPayloadActionCreatorFactory } from 'app/core/redux';
|
import { actionCreatorFactory, noPayloadActionCreatorFactory } from 'app/core/redux';
|
||||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
ThunkResult,
|
ThunkResult,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import { reducerFactory } from 'app/core/redux';
|
import { reducerFactory } from 'app/core/redux';
|
||||||
import { processAclItems } from 'app/core/utils/acl';
|
import { processAclItems } from 'app/core/utils/acl';
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
|
import { panelEditorReducer } from '../panel_editor/state/reducers';
|
||||||
|
|
||||||
export const initialState: DashboardState = {
|
export const initialState: DashboardState = {
|
||||||
initPhase: DashboardInitPhase.NotStarted,
|
initPhase: DashboardInitPhase.NotStarted,
|
||||||
@ -87,4 +88,5 @@ export const dashboardReducer = reducerFactory(initialState)
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
dashboard: dashboardReducer,
|
dashboard: dashboardReducer,
|
||||||
|
panelEditor: panelEditorReducer,
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,7 @@ import { PluginsState } from './plugins';
|
|||||||
import { NavIndex } from '@grafana/data';
|
import { NavIndex } from '@grafana/data';
|
||||||
import { ApplicationState } from './application';
|
import { ApplicationState } from './application';
|
||||||
import { LdapState, LdapUserState } from './ldap';
|
import { LdapState, LdapUserState } from './ldap';
|
||||||
|
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
navIndex: NavIndex;
|
navIndex: NavIndex;
|
||||||
@ -24,6 +25,7 @@ export interface StoreState {
|
|||||||
team: TeamState;
|
team: TeamState;
|
||||||
folder: FolderState;
|
folder: FolderState;
|
||||||
dashboard: DashboardState;
|
dashboard: DashboardState;
|
||||||
|
panelEditor: PanelEditorState;
|
||||||
dataSources: DataSourcesState;
|
dataSources: DataSourcesState;
|
||||||
explore: ExploreState;
|
explore: ExploreState;
|
||||||
users: UsersState;
|
users: UsersState;
|
||||||
|
@ -29,7 +29,7 @@ export const thunkTester = (initialState: any, debug?: boolean): ThunkGiven => {
|
|||||||
|
|
||||||
dispatchedActions = store.getActions();
|
dispatchedActions = store.getActions();
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log('resultingActions:', dispatchedActions);
|
console.log('resultingActions:', JSON.stringify(dispatchedActions, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatchedActions;
|
return dispatchedActions;
|
||||||
|
Loading…
Reference in New Issue
Block a user