123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- import classNames from 'classnames';
- import React, { PureComponent } from 'react';
- import { Subscription } from 'rxjs';
- import {
- AbsoluteTimeRange,
- AnnotationChangeEvent,
- AnnotationEventUIModel,
- CoreApp,
- DashboardCursorSync,
- EventFilterOptions,
- FieldConfigSource,
- getDefaultTimeRange,
- LoadingState,
- PanelData,
- PanelPlugin,
- PanelPluginMeta,
- TimeRange,
- toDataFrameDTO,
- toUtc,
- } from '@grafana/data';
- import { selectors } from '@grafana/e2e-selectors';
- import { locationService, RefreshEvent } from '@grafana/runtime';
- import { VizLegendOptions } from '@grafana/schema';
- import { ErrorBoundary, PanelContext, PanelContextProvider, SeriesVisibilityChangeMode } from '@grafana/ui';
- import config from 'app/core/config';
- import { PANEL_BORDER } from 'app/core/constants';
- import { profiler } from 'app/core/profiler';
- import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
- import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory';
- import { RenderEvent } from 'app/types/events';
- import { contextSrv } from '../../../core/services/context_srv';
- import { isSoloRoute } from '../../../routes/utils';
- import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annotations/api';
- import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
- import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
- import { DashboardModel, PanelModel } from '../state';
- import { loadSnapshotData } from '../utils/loadSnapshotData';
- import { PanelHeader } from './PanelHeader/PanelHeader';
- import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
- import { liveTimer } from './liveTimer';
- const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
- export interface Props {
- panel: PanelModel;
- dashboard: DashboardModel;
- plugin: PanelPlugin;
- isViewing: boolean;
- isEditing: boolean;
- isInView: boolean;
- width: number;
- height: number;
- onInstanceStateChange: (value: any) => void;
- }
- export interface State {
- isFirstLoad: boolean;
- renderCounter: number;
- errorMessage?: string;
- refreshWhenInView: boolean;
- context: PanelContext;
- data: PanelData;
- liveTime?: TimeRange;
- }
- export class PanelChrome extends PureComponent<Props, State> {
- private readonly timeSrv: TimeSrv = getTimeSrv();
- private subs = new Subscription();
- private eventFilter: EventFilterOptions = { onlyLocal: true };
- constructor(props: Props) {
- super(props);
- // Can this eventBus be on PanelModel? when we have more complex event filtering, that may be a better option
- const eventBus = props.dashboard.events.newScopedBus(`panel:${props.panel.id}`, this.eventFilter);
- this.state = {
- isFirstLoad: true,
- renderCounter: 0,
- refreshWhenInView: false,
- context: {
- eventBus,
- app: this.getPanelContextApp(),
- sync: this.getSync,
- onSeriesColorChange: this.onSeriesColorChange,
- onToggleSeriesVisibility: this.onSeriesVisibilityChange,
- onAnnotationCreate: this.onAnnotationCreate,
- onAnnotationUpdate: this.onAnnotationUpdate,
- onAnnotationDelete: this.onAnnotationDelete,
- canAddAnnotations: this.canAddAnnotation,
- onInstanceStateChange: this.onInstanceStateChange,
- onToggleLegendSort: this.onToggleLegendSort,
- canEditAnnotations: this.canEditAnnotation,
- canDeleteAnnotations: this.canDeleteAnnotation,
- },
- data: this.getInitialPanelDataState(),
- };
- }
- canEditDashboard = () => Boolean(this.props.dashboard.meta.canEdit || this.props.dashboard.meta.canMakeEditable);
- canAddAnnotation = () => {
- let canAdd = true;
- if (contextSrv.accessControlEnabled()) {
- canAdd = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canAdd;
- }
- return canAdd && this.canEditDashboard();
- };
- canEditAnnotation = (dashboardId: number) => {
- let canEdit = true;
- if (contextSrv.accessControlEnabled()) {
- if (dashboardId !== 0) {
- canEdit = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canEdit;
- } else {
- canEdit = !!this.props.dashboard.meta.annotationsPermissions?.organization.canEdit;
- }
- }
- return canEdit && this.canEditDashboard();
- };
- canDeleteAnnotation = (dashboardId: number) => {
- let canDelete = true;
- if (contextSrv.accessControlEnabled()) {
- if (dashboardId !== 0) {
- canDelete = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canDelete;
- } else {
- canDelete = !!this.props.dashboard.meta.annotationsPermissions?.organization.canDelete;
- }
- }
- return canDelete && this.canEditDashboard();
- };
- // Due to a mutable panel model we get the sync settings via function that proactively reads from the model
- getSync = () => (this.props.isEditing ? DashboardCursorSync.Off : this.props.dashboard.graphTooltip);
- onInstanceStateChange = (value: any) => {
- this.props.onInstanceStateChange(value);
- this.setState({
- context: {
- ...this.state.context,
- instanceState: value,
- },
- });
- };
- getPanelContextApp() {
- if (this.props.isEditing) {
- return CoreApp.PanelEditor;
- }
- if (this.props.isViewing) {
- return CoreApp.PanelViewer;
- }
- return CoreApp.Dashboard;
- }
- onSeriesColorChange = (label: string, color: string) => {
- this.onFieldConfigChange(changeSeriesColorConfigFactory(label, color, this.props.panel.fieldConfig));
- };
- onSeriesVisibilityChange = (label: string, mode: SeriesVisibilityChangeMode) => {
- this.onFieldConfigChange(
- seriesVisibilityConfigFactory(label, mode, this.props.panel.fieldConfig, this.state.data.series)
- );
- };
- onToggleLegendSort = (sortKey: string) => {
- const legendOptions: VizLegendOptions = this.props.panel.options.legend;
- // We don't want to do anything when legend options are not available
- if (!legendOptions) {
- return;
- }
- let sortDesc = legendOptions.sortDesc;
- let sortBy = legendOptions.sortBy;
- if (sortKey !== sortBy) {
- sortDesc = undefined;
- }
- // if already sort ascending, disable sorting
- if (sortDesc === false) {
- sortBy = undefined;
- sortDesc = undefined;
- } else {
- sortDesc = !sortDesc;
- sortBy = sortKey;
- }
- this.onOptionsChange({
- ...this.props.panel.options,
- legend: { ...legendOptions, sortBy, sortDesc },
- });
- };
- getInitialPanelDataState(): PanelData {
- return {
- state: LoadingState.NotStarted,
- series: [],
- timeRange: getDefaultTimeRange(),
- };
- }
- componentDidMount() {
- const { panel, dashboard } = this.props;
- // Subscribe to panel events
- this.subs.add(panel.events.subscribe(RefreshEvent, this.onRefresh));
- this.subs.add(panel.events.subscribe(RenderEvent, this.onRender));
- dashboard.panelInitialized(this.props.panel);
- // Move snapshot data into the query response
- if (this.hasPanelSnapshot) {
- this.setState({
- data: loadSnapshotData(panel, dashboard),
- isFirstLoad: false,
- });
- return;
- }
- if (!this.wantsQueryExecution) {
- this.setState({ isFirstLoad: false });
- }
- this.subs.add(
- panel
- .getQueryRunner()
- .getData({ withTransforms: true, withFieldConfig: true })
- .subscribe({
- next: (data) => this.onDataUpdate(data),
- })
- );
- // Listen for live timer events
- liveTimer.listen(this);
- }
- componentWillUnmount() {
- this.subs.unsubscribe();
- liveTimer.remove(this);
- }
- liveTimeChanged(liveTime: TimeRange) {
- const { data } = this.state;
- if (data.timeRange) {
- const delta = liveTime.to.valueOf() - data.timeRange.to.valueOf();
- if (delta < 100) {
- // 10hz
- console.log('Skip tick render', this.props.panel.title, delta);
- return;
- }
- }
- this.setState({ liveTime });
- }
- componentDidUpdate(prevProps: Props) {
- const { isInView, width } = this.props;
- const { context } = this.state;
- const app = this.getPanelContextApp();
- if (context.app !== app) {
- this.setState({
- context: {
- ...context,
- app,
- },
- });
- }
- // View state has changed
- if (isInView !== prevProps.isInView) {
- if (isInView) {
- // Check if we need a delayed refresh
- if (this.state.refreshWhenInView) {
- this.onRefresh();
- }
- }
- }
- // The timer depends on panel width
- if (width !== prevProps.width) {
- liveTimer.updateInterval(this);
- }
- }
- // 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) {
- const { dashboard, panel, plugin } = this.props;
- // Ignore this data update if we are now a non data panel
- if (plugin.meta.skipDataQuery) {
- this.setState({ data: this.getInitialPanelDataState() });
- 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 (dashboard.snapshot) {
- panel.snapshotData = data.series.map((frame) => toDataFrameDTO(frame));
- }
- if (isFirstLoad) {
- isFirstLoad = false;
- }
- break;
- }
- this.setState({ isFirstLoad, errorMessage, data, liveTime: undefined });
- }
- onRefresh = () => {
- const { panel, isInView, width } = this.props;
- if (!isInView) {
- this.setState({ refreshWhenInView: true });
- return;
- }
- const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
- // Issue Query
- if (this.wantsQueryExecution) {
- if (width < 0) {
- return;
- }
- if (this.state.refreshWhenInView) {
- this.setState({ refreshWhenInView: false });
- }
- panel.runAllPanelQueries(this.props.dashboard.id, this.props.dashboard.getTimezone(), timeData, width);
- } else {
- // The panel should render on refresh as well if it doesn't have a query, like clock panel
- this.setState({
- data: { ...this.state.data, timeRange: this.timeSrv.timeRange() },
- renderCounter: this.state.renderCounter + 1,
- liveTime: undefined,
- });
- }
- };
- onRender = () => {
- const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
- this.setState(stateUpdate);
- };
- onOptionsChange = (options: any) => {
- this.props.panel.updateOptions(options);
- };
- onFieldConfigChange = (config: FieldConfigSource) => {
- this.props.panel.updateFieldConfig(config);
- };
- onPanelError = (error: Error) => {
- const errorMessage = error.message || DEFAULT_PLUGIN_ERROR;
- if (this.state.errorMessage !== errorMessage) {
- this.setState({ errorMessage });
- }
- };
- onPanelErrorRecover = () => {
- this.setState({ errorMessage: undefined });
- };
- onAnnotationCreate = async (event: AnnotationEventUIModel) => {
- const isRegion = event.from !== event.to;
- const anno = {
- dashboardId: this.props.dashboard.id,
- panelId: this.props.panel.id,
- isRegion,
- time: event.from,
- timeEnd: isRegion ? event.to : 0,
- tags: event.tags,
- text: event.description,
- };
- await saveAnnotation(anno);
- getDashboardQueryRunner().run({ dashboard: this.props.dashboard, range: this.timeSrv.timeRange() });
- this.state.context.eventBus.publish(new AnnotationChangeEvent(anno));
- };
- onAnnotationDelete = async (id: string) => {
- await deleteAnnotation({ id });
- getDashboardQueryRunner().run({ dashboard: this.props.dashboard, range: this.timeSrv.timeRange() });
- this.state.context.eventBus.publish(new AnnotationChangeEvent({ id }));
- };
- onAnnotationUpdate = async (event: AnnotationEventUIModel) => {
- const isRegion = event.from !== event.to;
- const anno = {
- id: event.id,
- dashboardId: this.props.dashboard.id,
- panelId: this.props.panel.id,
- isRegion,
- time: event.from,
- timeEnd: isRegion ? event.to : 0,
- tags: event.tags,
- text: event.description,
- };
- await updateAnnotation(anno);
- getDashboardQueryRunner().run({ dashboard: this.props.dashboard, range: this.timeSrv.timeRange() });
- this.state.context.eventBus.publish(new AnnotationChangeEvent(anno));
- };
- get hasPanelSnapshot() {
- const { panel } = this.props;
- return panel.snapshotData && panel.snapshotData.length;
- }
- get wantsQueryExecution() {
- return !(this.props.plugin.meta.skipDataQuery || this.hasPanelSnapshot);
- }
- onChangeTimeRange = (timeRange: AbsoluteTimeRange) => {
- this.timeSrv.setTime({
- from: toUtc(timeRange.from),
- to: toUtc(timeRange.to),
- });
- };
- shouldSignalRenderingCompleted(loadingState: LoadingState, pluginMeta: PanelPluginMeta) {
- return loadingState === LoadingState.Done || pluginMeta.skipDataQuery;
- }
- skipFirstRender(loadingState: LoadingState) {
- const { isFirstLoad } = this.state;
- return (
- this.wantsQueryExecution &&
- isFirstLoad &&
- (loadingState === LoadingState.Loading || loadingState === LoadingState.NotStarted)
- );
- }
- renderPanel(width: number, height: number) {
- const { panel, plugin, dashboard } = this.props;
- const { renderCounter, data } = this.state;
- const { theme } = config;
- const { state: loadingState } = data;
- // do not render component until we have first data
- if (this.skipFirstRender(loadingState)) {
- return null;
- }
- // This is only done to increase a counter that is used by backend
- // image rendering to know when to capture image
- if (this.shouldSignalRenderingCompleted(loadingState, plugin.meta)) {
- profiler.renderingCompleted();
- }
- const PanelComponent = plugin.panel!;
- const timeRange = this.state.liveTime ?? 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,
- });
- const panelOptions = panel.getOptions();
- // Update the event filter (dashboard settings may have changed)
- // Yes this is called ever render for a function that is triggered on every mouse move
- this.eventFilter.onlyLocal = dashboard.graphTooltip === 0;
- return (
- <>
- <div className={panelContentClassNames}>
- <PanelContextProvider value={this.state.context}>
- <PanelComponent
- id={panel.id}
- data={data}
- title={panel.title}
- timeRange={timeRange}
- timeZone={this.props.dashboard.getTimezone()}
- options={panelOptions}
- fieldConfig={panel.fieldConfig}
- transparent={panel.transparent}
- width={panelWidth}
- height={innerPanelHeight}
- renderCounter={renderCounter}
- replaceVariables={panel.replaceVariables}
- onOptionsChange={this.onOptionsChange}
- onFieldConfigChange={this.onFieldConfigChange}
- onChangeTimeRange={this.onChangeTimeRange}
- eventBus={dashboard.events}
- />
- </PanelContextProvider>
- </div>
- </>
- );
- }
- hasOverlayHeader() {
- const { panel } = this.props;
- const { data } = this.state;
- // always show normal header if we have time override
- if (data.request && data.request.timeInfo) {
- return false;
- }
- return !panel.hasTitle();
- }
- render() {
- const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
- const { errorMessage, data } = this.state;
- const { transparent } = panel;
- const alertState = data.alertState?.state;
- const containerClassNames = classNames({
- 'panel-container': true,
- 'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname),
- 'panel-container--transparent': transparent,
- 'panel-container--no-title': this.hasOverlayHeader(),
- [`panel-alert-state--${alertState}`]: alertState !== undefined,
- });
- return (
- <section
- className={containerClassNames}
- aria-label={selectors.components.Panels.Panel.containerByTitle(panel.title)}
- >
- <PanelHeader
- panel={panel}
- dashboard={dashboard}
- title={panel.title}
- description={panel.description}
- links={panel.links}
- error={errorMessage}
- isEditing={isEditing}
- isViewing={isViewing}
- alertState={alertState}
- data={data}
- />
- <ErrorBoundary
- dependencies={[data, plugin, panel.getOptions()]}
- onError={this.onPanelError}
- onRecover={this.onPanelErrorRecover}
- >
- {({ error }) => {
- if (error) {
- return null;
- }
- return this.renderPanel(width, height);
- }}
- </ErrorBoundary>
- </section>
- );
- }
- }
|