import { Component } from 'react'; import { PanelProps } from '@grafana/data'; import { PanelOptions } from './models.gen'; import { Subscription } from 'rxjs'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; import { CanvasGroupOptions } from 'app/features/canvas'; import { Scene } from 'app/features/canvas/runtime/scene'; import { PanelContext, PanelContextRoot } from '@grafana/ui'; import { ElementState } from 'app/features/canvas/runtime/element'; import { GroupState } from 'app/features/canvas/runtime/group'; interface Props extends PanelProps {} interface State { refresh: number; } export interface InstanceState { scene: Scene; selected: ElementState[]; layer: GroupState; } export class CanvasPanel extends Component { static contextType = PanelContextRoot; panelContext: PanelContext = {} as PanelContext; readonly scene: Scene; private subs = new Subscription(); needsReload = false; constructor(props: Props) { super(props); this.state = { refresh: 0, }; // Only the initial options are ever used. // later changes are all controlled by the scene this.scene = new Scene(this.props.options.root, this.props.options.inlineEditing, this.onUpdateScene); this.scene.updateSize(props.width, props.height); this.scene.updateData(props.data); this.subs.add( this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt) => { // Remove current selection when entering edit mode for any panel in dashboard let event: MouseEvent = new MouseEvent('click'); this.scene?.selecto?.clickTarget(event, this.scene?.div); }) ); this.subs.add( this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => { if (this.props.id === evt.payload) { this.needsReload = true; } }) ); } componentDidMount() { this.panelContext = this.context as PanelContext; if (this.panelContext.onInstanceStateChange) { this.panelContext.onInstanceStateChange({ scene: this.scene, layer: this.scene.root, }); this.subs.add( this.scene.selection.subscribe({ next: (v) => { this.panelContext.onInstanceStateChange!({ scene: this.scene, selected: v, layer: this.scene.root, }); }, }) ); } } componentWillUnmount() { this.subs.unsubscribe(); } // NOTE, all changes to the scene flow through this function // even the editor gets current state from the same scene instance! onUpdateScene = (root: CanvasGroupOptions) => { const { onOptionsChange, options } = this.props; onOptionsChange({ ...options, root, }); this.setState({ refresh: this.state.refresh + 1 }); // console.log('send changes', root); }; shouldComponentUpdate(nextProps: Props, nextState: State) { const { width, height, data } = this.props; let changed = false; if (width !== nextProps.width || height !== nextProps.height) { this.scene.updateSize(nextProps.width, nextProps.height); changed = true; } if (data !== nextProps.data) { this.scene.updateData(nextProps.data); changed = true; } if (this.state.refresh !== nextState.refresh) { changed = true; } // After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed const shouldUpdateSceneAndPanel = (this.needsReload && this.props.options !== nextProps.options) || this.props.options.inlineEditing !== nextProps.options.inlineEditing; if (shouldUpdateSceneAndPanel) { this.needsReload = false; this.scene.load(nextProps.options.root, nextProps.options.inlineEditing); this.scene.updateSize(nextProps.width, nextProps.height); this.scene.updateData(nextProps.data); changed = true; if (this.props.options.inlineEditing) { this.scene.selecto?.destroy(); } } return changed; } render() { return this.scene.render(); } }