CanvasPanel.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { css } from '@emotion/css';
  2. import React, { Component } from 'react';
  3. import { ReplaySubject, Subscription } from 'rxjs';
  4. import { GrafanaTheme, PanelProps } from '@grafana/data';
  5. import { config, locationService } from '@grafana/runtime/src';
  6. import { Button, PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui';
  7. import { CanvasFrameOptions } from 'app/features/canvas';
  8. import { ElementState } from 'app/features/canvas/runtime/element';
  9. import { Scene } from 'app/features/canvas/runtime/scene';
  10. import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
  11. import { InlineEdit } from './InlineEdit';
  12. import { PanelOptions } from './models.gen';
  13. interface Props extends PanelProps<PanelOptions> {}
  14. interface State {
  15. refresh: number;
  16. openInlineEdit: boolean;
  17. }
  18. export interface InstanceState {
  19. scene: Scene;
  20. selected: ElementState[];
  21. }
  22. export interface SelectionAction {
  23. panel: CanvasPanel;
  24. }
  25. let canvasInstances: CanvasPanel[] = [];
  26. let activeCanvasPanel: CanvasPanel | undefined = undefined;
  27. let isInlineEditOpen = false;
  28. export const activePanelSubject = new ReplaySubject<SelectionAction>(1);
  29. export class CanvasPanel extends Component<Props, State> {
  30. static contextType = PanelContextRoot;
  31. panelContext: PanelContext = {} as PanelContext;
  32. readonly scene: Scene;
  33. private subs = new Subscription();
  34. needsReload = false;
  35. styles = getStyles(config.theme);
  36. isEditing = locationService.getSearchObject().editPanel !== undefined;
  37. constructor(props: Props) {
  38. super(props);
  39. this.state = {
  40. refresh: 0,
  41. openInlineEdit: false,
  42. };
  43. // Only the initial options are ever used.
  44. // later changes are all controlled by the scene
  45. this.scene = new Scene(this.props.options.root, this.props.options.inlineEditing, this.onUpdateScene);
  46. this.scene.updateSize(props.width, props.height);
  47. this.scene.updateData(props.data);
  48. this.subs.add(
  49. this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt) => {
  50. // Remove current selection when entering edit mode for any panel in dashboard
  51. this.scene.clearCurrentSelection();
  52. this.inlineEditButtonClose();
  53. })
  54. );
  55. this.subs.add(
  56. this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
  57. if (this.props.id === evt.payload) {
  58. this.needsReload = true;
  59. }
  60. })
  61. );
  62. }
  63. componentDidMount() {
  64. activeCanvasPanel = this;
  65. activePanelSubject.next({ panel: this });
  66. this.panelContext = this.context as PanelContext;
  67. if (this.panelContext.onInstanceStateChange) {
  68. this.panelContext.onInstanceStateChange({
  69. scene: this.scene,
  70. layer: this.scene.root,
  71. });
  72. this.subs.add(
  73. this.scene.selection.subscribe({
  74. next: (v) => {
  75. this.panelContext.onInstanceStateChange!({
  76. scene: this.scene,
  77. selected: v,
  78. layer: this.scene.root,
  79. });
  80. activeCanvasPanel = this;
  81. activePanelSubject.next({ panel: this });
  82. canvasInstances.forEach((canvasInstance) => {
  83. if (canvasInstance !== activeCanvasPanel) {
  84. canvasInstance.scene.clearCurrentSelection(true);
  85. }
  86. });
  87. },
  88. })
  89. );
  90. }
  91. canvasInstances.push(this);
  92. }
  93. componentWillUnmount() {
  94. this.subs.unsubscribe();
  95. isInlineEditOpen = false;
  96. canvasInstances = canvasInstances.filter((ci) => ci.props.id !== activeCanvasPanel?.props.id);
  97. }
  98. // NOTE, all changes to the scene flow through this function
  99. // even the editor gets current state from the same scene instance!
  100. onUpdateScene = (root: CanvasFrameOptions) => {
  101. const { onOptionsChange, options } = this.props;
  102. onOptionsChange({
  103. ...options,
  104. root,
  105. });
  106. this.setState({ refresh: this.state.refresh + 1 });
  107. // console.log('send changes', root);
  108. };
  109. shouldComponentUpdate(nextProps: Props, nextState: State) {
  110. const { width, height, data } = this.props;
  111. let changed = false;
  112. if (width !== nextProps.width || height !== nextProps.height) {
  113. this.scene.updateSize(nextProps.width, nextProps.height);
  114. changed = true;
  115. }
  116. if (data !== nextProps.data) {
  117. this.scene.updateData(nextProps.data);
  118. changed = true;
  119. }
  120. if (this.state.refresh !== nextState.refresh) {
  121. changed = true;
  122. }
  123. if (this.state.openInlineEdit !== nextState.openInlineEdit) {
  124. changed = true;
  125. }
  126. // After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed
  127. const shouldUpdateSceneAndPanel = this.needsReload && this.props.options !== nextProps.options;
  128. const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
  129. if (shouldUpdateSceneAndPanel || inlineEditingSwitched) {
  130. this.needsReload = false;
  131. this.scene.load(nextProps.options.root, nextProps.options.inlineEditing);
  132. this.scene.updateSize(nextProps.width, nextProps.height);
  133. this.scene.updateData(nextProps.data);
  134. changed = true;
  135. if (inlineEditingSwitched && this.props.options.inlineEditing) {
  136. this.scene.selecto?.destroy();
  137. }
  138. }
  139. return changed;
  140. }
  141. inlineEditButtonClick = () => {
  142. if (isInlineEditOpen) {
  143. this.forceUpdate();
  144. this.setActivePanel();
  145. return;
  146. }
  147. this.setActivePanel();
  148. this.setState({ openInlineEdit: true });
  149. isInlineEditOpen = true;
  150. };
  151. inlineEditButtonClose = () => {
  152. this.setState({ openInlineEdit: false });
  153. isInlineEditOpen = false;
  154. };
  155. setActivePanel = () => {
  156. activeCanvasPanel = this;
  157. activePanelSubject.next({ panel: this });
  158. };
  159. renderInlineEdit = () => {
  160. return <InlineEdit onClose={() => this.inlineEditButtonClose()} />;
  161. };
  162. render() {
  163. return (
  164. <>
  165. {this.scene.render()}
  166. {this.props.options.inlineEditing && !this.isEditing && (
  167. <div>
  168. <div className={this.styles.inlineEditButton}>
  169. <Button
  170. size="lg"
  171. variant="secondary"
  172. icon="edit"
  173. data-btninlineedit={this.props.id}
  174. onClick={this.inlineEditButtonClick}
  175. />
  176. </div>
  177. {this.state.openInlineEdit && this.renderInlineEdit()}
  178. </div>
  179. )}
  180. </>
  181. );
  182. }
  183. }
  184. const getStyles = stylesFactory((theme: GrafanaTheme) => ({
  185. inlineEditButton: css`
  186. position: absolute;
  187. bottom: 8px;
  188. left: 8px;
  189. z-index: 999;
  190. `,
  191. }));