The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/dashboard/dashgrid/PanelChrome.tsx

365 lines
10 KiB

// Libraries
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { Unsubscribable } from 'rxjs';
// Components
import { PanelHeader } from './PanelHeader/PanelHeader';
import { ErrorBoundary } from '@grafana/ui';
// Utils & Services
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { profiler } from 'app/core/profiler';
QueryProcessing: Observable query interface and RxJS for query & stream processing (#18899) * I needed to learn some rxjs and understand this more, so just playing around * Updated * Removed all the complete calls * Refactoring * StreamHandler -> observable start * progress * simple singal works * Handle update time range * added error handling * wrap old function * minor changes * handle data format in the subscribe function * Use replay subject to return last value to subscribers * Set loading state after no response in 50ms * added missing file * updated comment * Added cancelation of network requests * runRequest: Added unit test scenario framework * Progress on tests * minor refactor of unit tests * updated test * removed some old code * Shared queries work again, and also became so much simplier * unified query and observe methods * implict any fix * Fixed closed subject issue * removed comment * Use last returned data for loading state * WIP: Explore to runRequest makover step1 * Minor progress * Minor progress on explore and runRequest * minor progress * Things are starting to work in explore * Updated prometheus to use new observable query response, greatly simplified code * Revert refId change * Found better solution for key/refId/requestId problem * use observable with loki * tests compile * fix loki query prep * Explore: correct first response handling * Refactorings * Refactoring * Explore: Fixes LoadingState and GraphResults between runs (#18986) * Refactor: Adds state to DataQueryResponse * Fix: Fixes so we do not empty results before new data arrives Fixes: #17409 * Transformations work * observable test data * remove single() from loki promise * Fixed comment * Explore: Fixes failing Loki and Prometheus unit tests (#18995) * Tests: Makes datasource tests work again * Fix: Fixes loki datasource so highligthing works * Chore: Runs Prettier * Fixed query runner tests * Delay loading state indication to 200ms * Fixed test * fixed unit tests * Clear cached calcs * Fixed bug getProcesedDataFrames * Fix the correct test is a better idea * Fix: Fixes so queries in Explore are only run if Graph/Table is shown (#19000) * Fix: Fixes so queries in Explore are only run if Graph/Table is shown Fixes: #18618 * Refactor: Removes unnecessary condition * PanelData: provide legacy data only when needed (#19018) * no legacy * invert logic... now compiles * merge getQueryResponseData and getDataRaw * update comment about query editor * use single getData() function * only send legacy when it is used in explore * pre process rather than post process * pre process rather than post process * Minor refactoring * Add missing tags to test datasource response * MixedDatasource: Adds query observable pattern to MixedDatasource (#19037) * start mixed datasource * Refactor: Refactors into observable parttern * Tests: Fixes tests * Tests: Removes console.log * Refactor: Adds unique requestId
6 years ago
import { getProcessedDataFrames } from '../state/runRequest';
import templateSrv from 'app/features/templating/template_srv';
import config from 'app/core/config';
// Types
import { DashboardModel, PanelModel } from '../state';
import { PANEL_BORDER } from 'app/core/constants';
import {
LoadingState,
ScopedVars,
AbsoluteTimeRange,
DefaultTimeRange,
toUtc,
toDataFrameDTO,
PanelEvents,
PanelData,
PanelPlugin,
} from '@grafana/data';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
isFullscreen: boolean;
isInView: boolean;
isInEditMode?: boolean;
width: number;
height: number;
}
export interface State {
isFirstLoad: boolean;
renderCounter: number;
errorMessage?: string;
refreshWhenInView: boolean;
// Current state of all events
data: PanelData;
}
export class PanelChrome extends PureComponent<Props, State> {
timeSrv: TimeSrv = getTimeSrv();
querySubscription: Unsubscribable;
constructor(props: Props) {
super(props);
this.state = {
isFirstLoad: true,
renderCounter: 0,
refreshWhenInView: false,
data: {
state: LoadingState.NotStarted,
series: [],
timeRange: DefaultTimeRange,
},
};
}
componentDidMount() {
const { panel, dashboard, isInEditMode } = this.props;
panel.events.on(PanelEvents.refresh, this.onRefresh);
panel.events.on(PanelEvents.render, this.onRender);
dashboard.panelInitialized(this.props.panel);
// Move snapshot data into the query response
if (this.hasPanelSnapshot) {
this.setState({
data: {
...this.state.data,
state: LoadingState.Done,
series: getProcessedDataFrames(panel.snapshotData),
},
isFirstLoad: false,
});
} else {
if (isInEditMode) {
this.querySubscription = panel
.getQueryRunner()
.getData()
.subscribe({
next: data => this.onDataUpdate(data),
});
}
if (!this.wantsQueryExecution) {
this.setState({ isFirstLoad: false });
}
}
}
componentWillUnmount() {
this.props.panel.events.off(PanelEvents.refresh, this.onRefresh);
this.props.panel.events.off(PanelEvents.render, this.onRender);
if (this.querySubscription) {
this.querySubscription.unsubscribe();
}
}
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();
}
}
}
}
// Updates the response with information from the stream
// 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
onDataUpdate(data: PanelData) {
if (!this.props.isInView) {
// Ignore events when not visible.
// The call will be repeated when the panel comes into view
return;
}
let { isFirstLoad } = this.state;
let errorMessage: string | undefined;
switch (data.state) {
case LoadingState.Loading:
// Skip updating state data if it is already in loading state
// This is to avoid rendering partial loading responses
if (this.state.data.state === LoadingState.Loading) {
return;
}
break;
case LoadingState.Error:
const { error } = data;
if (error) {
if (errorMessage !== error.message) {
errorMessage = error.message;
}
}
break;
case LoadingState.Done:
// If we are doing a snapshot save data in panel model
if (this.props.dashboard.snapshot) {
this.props.panel.snapshotData = data.series.map(frame => toDataFrameDTO(frame));
}
if (isFirstLoad) {
isFirstLoad = false;
}
break;
}
this.setState({ isFirstLoad, errorMessage, data });
}
onRefresh = () => {
const { panel, isInView, width } = this.props;
if (!isInView) {
console.log('Refresh when panel is visible', panel.id);
this.setState({ refreshWhenInView: true });
return;
}
const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
// Issue Query
if (this.wantsQueryExecution) {
if (width < 0) {
console.log('Refresh skippted, no width yet... wait till we know');
return;
}
const queryRunner = panel.getQueryRunner();
if (!this.querySubscription) {
this.querySubscription = queryRunner.getData().subscribe({
next: data => this.onDataUpdate(data),
});
}
QueryProcessing: Observable query interface and RxJS for query & stream processing (#18899) * I needed to learn some rxjs and understand this more, so just playing around * Updated * Removed all the complete calls * Refactoring * StreamHandler -> observable start * progress * simple singal works * Handle update time range * added error handling * wrap old function * minor changes * handle data format in the subscribe function * Use replay subject to return last value to subscribers * Set loading state after no response in 50ms * added missing file * updated comment * Added cancelation of network requests * runRequest: Added unit test scenario framework * Progress on tests * minor refactor of unit tests * updated test * removed some old code * Shared queries work again, and also became so much simplier * unified query and observe methods * implict any fix * Fixed closed subject issue * removed comment * Use last returned data for loading state * WIP: Explore to runRequest makover step1 * Minor progress * Minor progress on explore and runRequest * minor progress * Things are starting to work in explore * Updated prometheus to use new observable query response, greatly simplified code * Revert refId change * Found better solution for key/refId/requestId problem * use observable with loki * tests compile * fix loki query prep * Explore: correct first response handling * Refactorings * Refactoring * Explore: Fixes LoadingState and GraphResults between runs (#18986) * Refactor: Adds state to DataQueryResponse * Fix: Fixes so we do not empty results before new data arrives Fixes: #17409 * Transformations work * observable test data * remove single() from loki promise * Fixed comment * Explore: Fixes failing Loki and Prometheus unit tests (#18995) * Tests: Makes datasource tests work again * Fix: Fixes loki datasource so highligthing works * Chore: Runs Prettier * Fixed query runner tests * Delay loading state indication to 200ms * Fixed test * fixed unit tests * Clear cached calcs * Fixed bug getProcesedDataFrames * Fix the correct test is a better idea * Fix: Fixes so queries in Explore are only run if Graph/Table is shown (#19000) * Fix: Fixes so queries in Explore are only run if Graph/Table is shown Fixes: #18618 * Refactor: Removes unnecessary condition * PanelData: provide legacy data only when needed (#19018) * no legacy * invert logic... now compiles * merge getQueryResponseData and getDataRaw * update comment about query editor * use single getData() function * only send legacy when it is used in explore * pre process rather than post process * pre process rather than post process * Minor refactoring * Add missing tags to test datasource response * MixedDatasource: Adds query observable pattern to MixedDatasource (#19037) * start mixed datasource * Refactor: Refactors into observable parttern * Tests: Fixes tests * Tests: Removes console.log * Refactor: Adds unique requestId
6 years ago
queryRunner.run({
datasource: panel.datasource,
queries: panel.targets,
panelId: panel.id,
dashboardId: this.props.dashboard.id,
timezone: this.props.dashboard.getTimezone(),
timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo,
widthPixels: width,
maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval,
scopedVars: panel.scopedVars,
cacheTimeout: panel.cacheTimeout,
transformations: panel.transformations,
});
}
};
onRender = () => {
const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
this.setState(stateUpdate);
};
onOptionsChange = (options: any) => {
this.props.panel.updateOptions(options);
};
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);
6 years ago
};
onPanelError = (message: string) => {
if (this.state.errorMessage !== message) {
this.setState({ errorMessage: message });
}
};
get hasPanelSnapshot() {
const { panel } = this.props;
return panel.snapshotData && panel.snapshotData.length;
}
panelHasLastResult = () => {
return !!this.props.panel.getQueryRunner().getLastResult();
};
get wantsQueryExecution() {
return !(this.props.plugin.meta.skipDataQuery || this.hasPanelSnapshot);
}
onChangeTimeRange = (timeRange: AbsoluteTimeRange) => {
this.timeSrv.setTime({
from: toUtc(timeRange.from),
to: toUtc(timeRange.to),
});
};
renderPanel(width: number, height: number) {
const { panel, plugin } = this.props;
const { renderCounter, data, isFirstLoad } = this.state;
const { theme } = config;
// This is only done to increase a counter that is used by backend
// image rendering (phantomjs/headless chrome) to know when to capture image
const loading = data.state;
if (loading === LoadingState.Done) {
profiler.renderingCompleted();
}
// do not render component until we have first data
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
return null;
}
const PanelComponent = plugin.panel;
const timeRange = data.timeRange || this.timeSrv.timeRange();
const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight;
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
const panelWidth = width - chromePadding * 2 - PANEL_BORDER;
const innerPanelHeight = height - headerHeight - chromePadding * 2 - PANEL_BORDER;
const panelContentClassNames = classNames({
'panel-content': true,
'panel-content--no-padding': plugin.noPadding,
});
return (
<>
<div className={panelContentClassNames}>
<PanelComponent
id={panel.id}
data={data}
timeRange={timeRange}
timeZone={this.props.dashboard.getTimezone()}
options={panel.getOptions()}
transparent={panel.transparent}
width={panelWidth}
height={innerPanelHeight}
renderCounter={renderCounter}
replaceVariables={this.replaceVariables}
onOptionsChange={this.onOptionsChange}
onChangeTimeRange={this.onChangeTimeRange}
/>
</div>
</>
);
}
hasOverlayHeader() {
const { panel } = this.props;
const { errorMessage, data } = this.state;
// always show normal header if we have an error message
if (errorMessage) {
return false;
}
// always show normal header if we have time override
if (data.request && data.request.timeInfo) {
return false;
}
return !panel.hasTitle();
}
render() {
const { dashboard, panel, isFullscreen, width, height } = this.props;
const { errorMessage, data } = this.state;
const { transparent } = panel;
const containerClassNames = classNames({
'panel-container': true,
'panel-container--absolute': true,
'panel-container--transparent': transparent,
'panel-container--no-title': this.hasOverlayHeader(),
});
return (
<div className={containerClassNames}>
<PanelHeader
panel={panel}
dashboard={dashboard}
timeInfo={data.request ? data.request.timeInfo : undefined}
title={panel.title}
description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links}
error={errorMessage}
isFullscreen={isFullscreen}
isLoading={data.state === LoadingState.Loading}
/>
<ErrorBoundary>
{({ error }) => {
if (error) {
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
return null;
}
return this.renderPanel(width, height);
}}
</ErrorBoundary>
</div>
);
}
}