grafana/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx
Torkel Ödegaard d62ca1283c
PanelState: Introduce a new separate redux panel state not keyed by panel.id (#40302)
* Initial pass to move panel state to it's own, and make it by key not panel.id

* Progress

* Not making much progress, having panel.key be mutable is causing a lot of issues

* Think this is starting to work

* Began fixing tests

* Add selector

* Bug fixes and changes to cleanup, and fixing all flicking when switching library panels

* Removed console.log

* fixes after merge

* fixing tests

* fixing tests

* Added new test for changePlugin thunk
2021-10-13 08:53:36 +02:00

225 lines
6.5 KiB
TypeScript

import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { Subscription } from 'rxjs';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { AngularComponent, getAngularLoader, locationService } from '@grafana/runtime';
import { getDefaultTimeRange, LoadingState, PanelData, PanelPlugin } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { PanelHeader } from './PanelHeader/PanelHeader';
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
import { setPanelAngularComponent } from 'app/features/panel/state/reducers';
import config from 'app/core/config';
import { DashboardModel, PanelModel } from '../state';
import { StoreState } from 'app/types';
import { PANEL_BORDER } from 'app/core/constants';
import { isSoloRoute } from '../../../routes/utils';
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
interface OwnProps {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
isViewing: boolean;
isEditing: boolean;
isInView: boolean;
width: number;
height: number;
}
interface ConnectedProps {
angularComponent?: AngularComponent;
}
interface DispatchProps {
setPanelAngularComponent: typeof setPanelAngularComponent;
}
export type Props = OwnProps & ConnectedProps & DispatchProps;
export interface State {
data: PanelData;
errorMessage?: string;
}
interface AngularScopeProps {
panel: PanelModel;
dashboard: DashboardModel;
size: {
height: number;
width: number;
};
}
export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
element: HTMLElement | null = null;
timeSrv: TimeSrv = getTimeSrv();
scopeProps?: AngularScopeProps;
subs = new Subscription();
constructor(props: Props) {
super(props);
this.state = {
data: {
state: LoadingState.NotStarted,
series: [],
timeRange: getDefaultTimeRange(),
},
};
}
componentDidMount() {
const { panel } = this.props;
this.loadAngularPanel();
// subscribe to data events
const queryRunner = panel.getQueryRunner();
// we are not displaying any of this data so no need for transforms or field config
this.subs.add(
queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
next: (data: PanelData) => this.onPanelDataUpdate(data),
})
);
}
onPanelDataUpdate(data: PanelData) {
let errorMessage: string | undefined;
if (data.state === LoadingState.Error) {
const { error } = data;
if (error) {
if (errorMessage !== error.message) {
errorMessage = error.message;
}
}
}
this.setState({ data, errorMessage });
}
componentWillUnmount() {
this.subs.unsubscribe();
}
componentDidUpdate(prevProps: Props, prevState: State) {
const { plugin, height, width, panel } = this.props;
if (prevProps.plugin !== plugin) {
this.loadAngularPanel();
}
if (prevProps.width !== width || prevProps.height !== height) {
if (this.scopeProps) {
this.scopeProps.size.height = this.getInnerPanelHeight();
this.scopeProps.size.width = this.getInnerPanelWidth();
panel.render();
}
}
}
getInnerPanelHeight() {
const { plugin, height } = this.props;
const { theme } = config;
const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight;
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
return height - headerHeight - chromePadding * 2 - PANEL_BORDER;
}
getInnerPanelWidth() {
const { plugin, width } = this.props;
const { theme } = config;
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
return width - chromePadding * 2 - PANEL_BORDER;
}
loadAngularPanel() {
const { panel, dashboard, setPanelAngularComponent } = this.props;
// if we have no element or already have loaded the panel return
if (!this.element) {
return;
}
const loader = getAngularLoader();
const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
this.scopeProps = {
panel: panel,
dashboard: dashboard,
size: { width: this.getInnerPanelWidth(), height: this.getInnerPanelHeight() },
};
setPanelAngularComponent({
key: panel.key,
angularComponent: loader.load(this.element, this.scopeProps, template),
});
}
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, 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-has-alert': panel.alert !== undefined,
[`panel-alert-state--${alertState}`]: alertState !== undefined,
});
const panelContentClassNames = classNames({
'panel-content': true,
'panel-content--no-padding': plugin.noPadding,
});
return (
<div 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}
isViewing={isViewing}
isEditing={isEditing}
data={data}
alertState={alertState}
/>
<div className={panelContentClassNames}>
<div ref={(element) => (this.element = element)} className="panel-height-helper" />
</div>
</div>
);
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
return {
angularComponent: getPanelStateForModel(state, props.panel)?.angularComponent,
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { setPanelAngularComponent };
export const PanelChromeAngular = connect(mapStateToProps, mapDispatchToProps)(PanelChromeAngularUnconnected);