2018-10-14 08:39:34 -05:00
|
|
|
// Libraries
|
2018-11-20 10:01:58 -06:00
|
|
|
import React, { PureComponent } from 'react';
|
2019-05-06 08:26:09 -05:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import { Unsubscribable } from 'rxjs';
|
2018-10-14 08:39:34 -05:00
|
|
|
|
|
|
|
// Components
|
2018-10-25 05:47:09 -05:00
|
|
|
import { PanelHeader } from './PanelHeader/PanelHeader';
|
2019-09-04 06:59:30 -05:00
|
|
|
import { ErrorBoundary } from '@grafana/ui';
|
2018-10-14 08:39:34 -05:00
|
|
|
|
2019-05-06 08:26:09 -05:00
|
|
|
// Utils & Services
|
|
|
|
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
|
|
|
import { applyPanelTimeOverrides, calculateInnerPanelHeight } from 'app/features/dashboard/utils/panel';
|
2019-02-01 01:12:58 -06:00
|
|
|
import { profiler } from 'app/core/profiler';
|
2019-09-12 10:28:46 -05:00
|
|
|
import { getProcessedDataFrames } from '../state/runRequest';
|
2019-05-06 08:26:09 -05:00
|
|
|
import templateSrv from 'app/features/templating/template_srv';
|
2019-03-12 08:24:48 -05:00
|
|
|
import config from 'app/core/config';
|
2018-11-08 07:44:12 -06:00
|
|
|
|
2018-10-14 08:39:34 -05:00
|
|
|
// Types
|
2019-01-31 07:16:06 -06:00
|
|
|
import { DashboardModel, PanelModel } from '../state';
|
2019-09-10 14:06:01 -05:00
|
|
|
import { PanelData, PanelPlugin } from '@grafana/ui';
|
|
|
|
import { LoadingState, ScopedVars } from '@grafana/data';
|
2018-06-19 07:51:57 -05:00
|
|
|
|
2019-02-12 09:06:02 -06:00
|
|
|
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
2019-01-14 07:47:41 -06:00
|
|
|
|
2018-11-06 09:37:51 -06:00
|
|
|
export interface Props {
|
2018-06-19 07:51:57 -05:00
|
|
|
panel: PanelModel;
|
|
|
|
dashboard: DashboardModel;
|
2019-05-01 00:36:46 -05:00
|
|
|
plugin: PanelPlugin;
|
2019-02-17 01:11:57 -06:00
|
|
|
isFullscreen: boolean;
|
2019-05-03 08:35:37 -05:00
|
|
|
isInView: boolean;
|
2019-04-17 12:51:50 -05:00
|
|
|
width: number;
|
|
|
|
height: number;
|
2018-06-19 07:51:57 -05:00
|
|
|
}
|
|
|
|
|
2018-11-06 09:37:51 -06:00
|
|
|
export interface State {
|
2019-04-17 12:51:50 -05:00
|
|
|
isFirstLoad: boolean;
|
2018-11-05 10:46:09 -06:00
|
|
|
renderCounter: number;
|
2019-02-14 08:18:55 -06:00
|
|
|
errorMessage: string | null;
|
2019-05-03 08:35:37 -05:00
|
|
|
refreshWhenInView: boolean;
|
2019-04-17 12:51:50 -05:00
|
|
|
|
|
|
|
// Current state of all events
|
|
|
|
data: PanelData;
|
2018-10-14 08:39:34 -05:00
|
|
|
}
|
2018-07-05 15:10:39 -05:00
|
|
|
|
2018-11-06 09:37:51 -06:00
|
|
|
export class PanelChrome extends PureComponent<Props, State> {
|
2018-11-08 07:44:12 -06:00
|
|
|
timeSrv: TimeSrv = getTimeSrv();
|
2019-04-17 12:51:50 -05:00
|
|
|
querySubscription: Unsubscribable;
|
2018-11-08 07:44:12 -06:00
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
constructor(props: Props) {
|
2018-06-19 07:51:57 -05:00
|
|
|
super(props);
|
2018-10-14 08:39:34 -05:00
|
|
|
this.state = {
|
2019-04-17 12:51:50 -05:00
|
|
|
isFirstLoad: true,
|
2018-11-05 10:46:09 -06:00
|
|
|
renderCounter: 0,
|
2019-02-14 08:18:55 -06:00
|
|
|
errorMessage: null,
|
2019-05-03 08:35:37 -05:00
|
|
|
refreshWhenInView: false,
|
2019-04-17 12:51:50 -05:00
|
|
|
data: {
|
|
|
|
state: LoadingState.NotStarted,
|
|
|
|
series: [],
|
|
|
|
},
|
2018-10-14 08:39:34 -05:00
|
|
|
};
|
2018-07-05 15:10:39 -05:00
|
|
|
}
|
|
|
|
|
2018-11-06 09:37:51 -06:00
|
|
|
componentDidMount() {
|
2019-04-17 12:51:50 -05:00
|
|
|
const { panel, dashboard } = this.props;
|
|
|
|
panel.events.on('refresh', this.onRefresh);
|
|
|
|
panel.events.on('render', this.onRender);
|
|
|
|
dashboard.panelInitialized(this.props.panel);
|
|
|
|
|
|
|
|
// Move snapshot data into the query response
|
|
|
|
if (this.hasPanelSnapshot) {
|
|
|
|
this.setState({
|
|
|
|
data: {
|
|
|
|
state: LoadingState.Done,
|
2019-08-02 01:16:29 -05:00
|
|
|
series: getProcessedDataFrames(panel.snapshotData),
|
2019-04-17 12:51:50 -05:00
|
|
|
},
|
|
|
|
isFirstLoad: false,
|
|
|
|
});
|
|
|
|
} else if (!this.wantsQueryExecution) {
|
|
|
|
this.setState({ isFirstLoad: false });
|
|
|
|
}
|
2018-10-14 08:39:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
this.props.panel.events.off('refresh', this.onRefresh);
|
2019-09-12 10:28:46 -05:00
|
|
|
|
2019-04-18 01:22:14 -05:00
|
|
|
if (this.querySubscription) {
|
|
|
|
this.querySubscription.unsubscribe();
|
|
|
|
this.querySubscription = null;
|
|
|
|
}
|
2018-10-14 08:39:34 -05:00
|
|
|
}
|
|
|
|
|
2019-05-03 08:35:37 -05:00
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
|
|
const { isInView } = this.props;
|
|
|
|
|
|
|
|
// View state has changed
|
|
|
|
if (isInView !== prevProps.isInView) {
|
|
|
|
if (isInView) {
|
|
|
|
// Check if we need a delayed refresh
|
|
|
|
if (this.state.refreshWhenInView) {
|
|
|
|
this.onRefresh();
|
|
|
|
}
|
|
|
|
} else if (this.querySubscription) {
|
|
|
|
this.querySubscription.unsubscribe();
|
|
|
|
this.querySubscription = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
// Updates the response with information from the stream
|
2019-04-18 01:22:14 -05:00
|
|
|
// The next is outside a react synthetic event so setState is not batched
|
|
|
|
// So in this context we can only do a single call to setState
|
2019-04-17 12:51:50 -05:00
|
|
|
panelDataObserver = {
|
|
|
|
next: (data: PanelData) => {
|
2019-05-03 08:35:37 -05:00
|
|
|
if (!this.props.isInView) {
|
|
|
|
// Ignore events when not visible.
|
|
|
|
// The call will be repeated when the panel comes into view
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-04-18 01:22:14 -05:00
|
|
|
let { errorMessage, isFirstLoad } = this.state;
|
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
if (data.state === LoadingState.Error) {
|
|
|
|
const { error } = data;
|
|
|
|
if (error) {
|
2019-05-03 08:35:37 -05:00
|
|
|
if (errorMessage !== error.message) {
|
2019-04-18 01:22:14 -05:00
|
|
|
errorMessage = error.message;
|
2019-04-17 12:51:50 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2019-04-18 01:22:14 -05:00
|
|
|
errorMessage = null;
|
2019-04-17 12:51:50 -05:00
|
|
|
}
|
|
|
|
|
2019-04-18 01:22:14 -05:00
|
|
|
if (data.state === LoadingState.Done) {
|
|
|
|
// If we are doing a snapshot save data in panel model
|
|
|
|
if (this.props.dashboard.snapshot) {
|
|
|
|
this.props.panel.snapshotData = data.series;
|
|
|
|
}
|
2019-05-03 08:35:37 -05:00
|
|
|
if (isFirstLoad) {
|
2019-04-18 01:22:14 -05:00
|
|
|
isFirstLoad = false;
|
2019-04-17 12:51:50 -05:00
|
|
|
}
|
|
|
|
}
|
2019-04-18 01:22:14 -05:00
|
|
|
|
2019-05-03 08:35:37 -05:00
|
|
|
this.setState({ isFirstLoad, errorMessage, data });
|
2019-04-17 12:51:50 -05:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2018-10-14 08:39:34 -05:00
|
|
|
onRefresh = () => {
|
2019-05-03 08:35:37 -05:00
|
|
|
const { panel, isInView, width } = this.props;
|
|
|
|
|
|
|
|
if (!isInView) {
|
|
|
|
console.log('Refresh when panel is visible', panel.id);
|
|
|
|
this.setState({ refreshWhenInView: true });
|
2018-11-08 07:44:12 -06:00
|
|
|
return;
|
|
|
|
}
|
2018-11-12 04:23:41 -06:00
|
|
|
|
2018-11-13 08:05:07 -06:00
|
|
|
const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
|
2018-07-09 15:24:15 -05:00
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
// Issue Query
|
2019-04-18 01:22:14 -05:00
|
|
|
if (this.wantsQueryExecution) {
|
2019-04-17 12:51:50 -05:00
|
|
|
if (width < 0) {
|
2019-04-18 16:10:18 -05:00
|
|
|
console.log('Refresh skippted, no width yet... wait till we know');
|
2019-04-17 12:51:50 -05:00
|
|
|
return;
|
|
|
|
}
|
2019-04-18 16:10:18 -05:00
|
|
|
|
|
|
|
const queryRunner = panel.getQueryRunner();
|
|
|
|
|
2019-04-18 01:22:14 -05:00
|
|
|
if (!this.querySubscription) {
|
2019-09-12 10:28:46 -05:00
|
|
|
this.querySubscription = queryRunner.getData().subscribe(this.panelDataObserver);
|
2019-04-18 01:22:14 -05:00
|
|
|
}
|
2019-09-12 10:28:46 -05:00
|
|
|
|
2019-04-18 16:10:18 -05:00
|
|
|
queryRunner.run({
|
2019-04-17 12:51:50 -05:00
|
|
|
datasource: panel.datasource,
|
|
|
|
queries: panel.targets,
|
|
|
|
panelId: panel.id,
|
|
|
|
dashboardId: this.props.dashboard.id,
|
2019-08-13 00:32:43 -05:00
|
|
|
timezone: this.props.dashboard.getTimezone(),
|
2019-04-17 12:51:50 -05:00
|
|
|
timeRange: timeData.timeRange,
|
2019-04-18 02:00:46 -05:00
|
|
|
timeInfo: timeData.timeInfo,
|
2019-04-17 12:51:50 -05:00
|
|
|
widthPixels: width,
|
|
|
|
maxDataPoints: panel.maxDataPoints,
|
2019-04-18 16:10:18 -05:00
|
|
|
minInterval: panel.interval,
|
2019-04-17 12:51:50 -05:00
|
|
|
scopedVars: panel.scopedVars,
|
|
|
|
cacheTimeout: panel.cacheTimeout,
|
2019-09-09 01:58:57 -05:00
|
|
|
transformations: panel.transformations,
|
2019-04-17 12:51:50 -05:00
|
|
|
});
|
|
|
|
}
|
2018-10-14 08:39:34 -05:00
|
|
|
};
|
2018-07-09 15:24:15 -05:00
|
|
|
|
2018-11-05 10:46:09 -06:00
|
|
|
onRender = () => {
|
2019-04-25 13:01:02 -05:00
|
|
|
const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
|
|
|
|
|
|
|
|
this.setState(stateUpdate);
|
2018-11-03 17:36:40 -05:00
|
|
|
};
|
|
|
|
|
2019-04-24 03:14:18 -05:00
|
|
|
onOptionsChange = (options: any) => {
|
|
|
|
this.props.panel.updateOptions(options);
|
|
|
|
};
|
|
|
|
|
2019-03-05 23:56:52 -06:00
|
|
|
replaceVariables = (value: string, extraVars?: ScopedVars, format?: string) => {
|
|
|
|
let vars = this.props.panel.scopedVars;
|
|
|
|
if (extraVars) {
|
|
|
|
vars = vars ? { ...vars, ...extraVars } : extraVars;
|
|
|
|
}
|
|
|
|
return templateSrv.replace(value, vars, format);
|
2019-01-15 10:15:46 -06:00
|
|
|
};
|
|
|
|
|
2019-02-14 08:18:55 -06:00
|
|
|
onPanelError = (message: string) => {
|
|
|
|
if (this.state.errorMessage !== message) {
|
|
|
|
this.setState({ errorMessage: message });
|
|
|
|
}
|
2019-01-30 03:39:42 -06:00
|
|
|
};
|
|
|
|
|
2019-02-12 05:25:10 -06:00
|
|
|
get hasPanelSnapshot() {
|
|
|
|
const { panel } = this.props;
|
|
|
|
return panel.snapshotData && panel.snapshotData.length;
|
|
|
|
}
|
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
get wantsQueryExecution() {
|
2019-05-10 00:46:14 -05:00
|
|
|
return !(this.props.plugin.meta.skipDataQuery || this.hasPanelSnapshot);
|
2019-02-12 05:25:10 -06:00
|
|
|
}
|
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
renderPanel(width: number, height: number): JSX.Element {
|
2019-01-30 03:39:42 -06:00
|
|
|
const { panel, plugin } = this.props;
|
2019-04-18 02:00:46 -05:00
|
|
|
const { renderCounter, data, isFirstLoad } = this.state;
|
2019-05-06 08:26:09 -05:00
|
|
|
const { theme } = config;
|
2019-01-30 03:39:42 -06:00
|
|
|
|
2019-02-01 01:12:58 -06:00
|
|
|
// This is only done to increase a counter that is used by backend
|
|
|
|
// image rendering (phantomjs/headless chrome) to know when to capture image
|
2019-04-17 12:51:50 -05:00
|
|
|
const loading = data.state;
|
|
|
|
if (loading === LoadingState.Done) {
|
2019-05-13 02:38:19 -05:00
|
|
|
profiler.renderingCompleted();
|
2019-02-01 01:12:58 -06:00
|
|
|
}
|
|
|
|
|
2019-04-17 12:51:50 -05:00
|
|
|
// do not render component until we have first data
|
|
|
|
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
|
|
|
return this.renderLoadingState();
|
|
|
|
}
|
2019-01-30 03:39:42 -06:00
|
|
|
|
2019-05-06 08:26:09 -05:00
|
|
|
const PanelComponent = plugin.panel;
|
|
|
|
const innerPanelHeight = calculateInnerPanelHeight(panel, height);
|
|
|
|
|
2019-02-07 14:34:50 -06:00
|
|
|
return (
|
|
|
|
<>
|
2019-04-17 12:51:50 -05:00
|
|
|
{loading === LoadingState.Loading && this.renderLoadingState()}
|
|
|
|
<div className="panel-content">
|
|
|
|
<PanelComponent
|
2019-05-10 00:48:31 -05:00
|
|
|
id={panel.id}
|
2019-04-17 12:51:50 -05:00
|
|
|
data={data}
|
2019-04-18 02:00:46 -05:00
|
|
|
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
|
2019-08-13 00:32:43 -05:00
|
|
|
timeZone={this.props.dashboard.getTimezone()}
|
2019-05-21 06:19:19 -05:00
|
|
|
options={panel.getOptions()}
|
2019-05-23 04:58:51 -05:00
|
|
|
transparent={panel.transparent}
|
2019-05-06 08:26:09 -05:00
|
|
|
width={width - theme.panelPadding * 2}
|
|
|
|
height={innerPanelHeight}
|
2019-04-17 12:51:50 -05:00
|
|
|
renderCounter={renderCounter}
|
|
|
|
replaceVariables={this.replaceVariables}
|
2019-04-24 03:14:18 -05:00
|
|
|
onOptionsChange={this.onOptionsChange}
|
2019-04-17 12:51:50 -05:00
|
|
|
/>
|
|
|
|
</div>
|
2019-02-07 14:34:50 -06:00
|
|
|
</>
|
|
|
|
);
|
2019-04-17 12:51:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private renderLoadingState(): JSX.Element {
|
|
|
|
return (
|
|
|
|
<div className="panel-loading">
|
|
|
|
<i className="fa fa-spinner fa-spin" />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2019-02-07 14:34:50 -06:00
|
|
|
|
2018-10-14 08:39:34 -05:00
|
|
|
render() {
|
2019-04-17 12:51:50 -05:00
|
|
|
const { dashboard, panel, isFullscreen, width, height } = this.props;
|
2019-04-18 02:00:46 -05:00
|
|
|
const { errorMessage, data } = this.state;
|
2019-02-07 14:34:50 -06:00
|
|
|
const { transparent } = panel;
|
2018-11-06 08:03:56 -06:00
|
|
|
|
2019-05-06 08:26:09 -05:00
|
|
|
const containerClassNames = classNames({
|
|
|
|
'panel-container': true,
|
|
|
|
'panel-container--absolute': true,
|
|
|
|
'panel-container--no-title': !panel.hasTitle(),
|
|
|
|
'panel-transparent': transparent,
|
|
|
|
});
|
|
|
|
|
2018-06-19 07:51:57 -05:00
|
|
|
return (
|
2019-04-17 12:51:50 -05:00
|
|
|
<div className={containerClassNames}>
|
|
|
|
<PanelHeader
|
|
|
|
panel={panel}
|
|
|
|
dashboard={dashboard}
|
2019-04-18 02:00:46 -05:00
|
|
|
timeInfo={data.request ? data.request.timeInfo : null}
|
2019-04-17 12:51:50 -05:00
|
|
|
title={panel.title}
|
|
|
|
description={panel.description}
|
|
|
|
scopedVars={panel.scopedVars}
|
|
|
|
links={panel.links}
|
|
|
|
error={errorMessage}
|
|
|
|
isFullscreen={isFullscreen}
|
|
|
|
/>
|
|
|
|
<ErrorBoundary>
|
|
|
|
{({ error, errorInfo }) => {
|
|
|
|
if (errorInfo) {
|
|
|
|
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return this.renderPanel(width, height);
|
|
|
|
}}
|
|
|
|
</ErrorBoundary>
|
|
|
|
</div>
|
2018-06-19 07:51:57 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|