DashboardGrid.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. // Libaries
  2. import React, { PureComponent } from 'react';
  3. import { hot } from 'react-hot-loader';
  4. import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
  5. import classNames from 'classnames';
  6. import sizeMe from 'react-sizeme';
  7. // Types
  8. import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
  9. import { DashboardPanel } from './DashboardPanel';
  10. import { DashboardModel, PanelModel } from '../state';
  11. let lastGridWidth = 1200;
  12. let ignoreNextWidthChange = false;
  13. interface GridWrapperProps {
  14. size: { width: number };
  15. layout: ReactGridLayout.Layout[];
  16. onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
  17. children: JSX.Element | JSX.Element[];
  18. onDragStop: ItemCallback;
  19. onResize: ItemCallback;
  20. onResizeStop: ItemCallback;
  21. onWidthChange: () => void;
  22. className: string;
  23. isResizable?: boolean;
  24. isDraggable?: boolean;
  25. isFullscreen?: boolean;
  26. }
  27. function GridWrapper({
  28. size,
  29. layout,
  30. onLayoutChange,
  31. children,
  32. onDragStop,
  33. onResize,
  34. onResizeStop,
  35. onWidthChange,
  36. className,
  37. isResizable,
  38. isDraggable,
  39. isFullscreen,
  40. }: GridWrapperProps) {
  41. const width = size.width > 0 ? size.width : lastGridWidth;
  42. // logic to ignore width changes (optimization)
  43. if (width !== lastGridWidth) {
  44. if (ignoreNextWidthChange) {
  45. ignoreNextWidthChange = false;
  46. } else if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
  47. onWidthChange();
  48. lastGridWidth = width;
  49. }
  50. }
  51. return (
  52. <ReactGridLayout
  53. width={lastGridWidth}
  54. className={className}
  55. isDraggable={isDraggable}
  56. isResizable={isResizable}
  57. containerPadding={[0, 0]}
  58. useCSSTransforms={false}
  59. margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
  60. cols={GRID_COLUMN_COUNT}
  61. rowHeight={GRID_CELL_HEIGHT}
  62. draggableHandle=".grid-drag-handle"
  63. layout={layout}
  64. onResize={onResize}
  65. onResizeStop={onResizeStop}
  66. onDragStop={onDragStop}
  67. onLayoutChange={onLayoutChange}
  68. >
  69. {children}
  70. </ReactGridLayout>
  71. );
  72. }
  73. const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
  74. export interface Props {
  75. dashboard: DashboardModel;
  76. isEditing: boolean;
  77. isFullscreen: boolean;
  78. scrollTop: number;
  79. }
  80. export class DashboardGrid extends PureComponent<Props> {
  81. panelMap: { [id: string]: PanelModel };
  82. panelRef: { [id: string]: HTMLElement } = {};
  83. componentDidMount() {
  84. const { dashboard } = this.props;
  85. dashboard.on('panel-added', this.triggerForceUpdate);
  86. dashboard.on('panel-removed', this.triggerForceUpdate);
  87. dashboard.on('repeats-processed', this.triggerForceUpdate);
  88. dashboard.on('view-mode-changed', this.onViewModeChanged);
  89. dashboard.on('row-collapsed', this.triggerForceUpdate);
  90. dashboard.on('row-expanded', this.triggerForceUpdate);
  91. }
  92. componentWillUnmount() {
  93. const { dashboard } = this.props;
  94. dashboard.off('panel-added', this.triggerForceUpdate);
  95. dashboard.off('panel-removed', this.triggerForceUpdate);
  96. dashboard.off('repeats-processed', this.triggerForceUpdate);
  97. dashboard.off('view-mode-changed', this.onViewModeChanged);
  98. dashboard.off('row-collapsed', this.triggerForceUpdate);
  99. dashboard.off('row-expanded', this.triggerForceUpdate);
  100. }
  101. buildLayout() {
  102. const layout = [];
  103. this.panelMap = {};
  104. for (const panel of this.props.dashboard.panels) {
  105. const stringId = panel.id.toString();
  106. this.panelMap[stringId] = panel;
  107. if (!panel.gridPos) {
  108. console.log('panel without gridpos');
  109. continue;
  110. }
  111. const panelPos: any = {
  112. i: stringId,
  113. x: panel.gridPos.x,
  114. y: panel.gridPos.y,
  115. w: panel.gridPos.w,
  116. h: panel.gridPos.h,
  117. };
  118. if (panel.type === 'row') {
  119. panelPos.w = GRID_COLUMN_COUNT;
  120. panelPos.h = 1;
  121. panelPos.isResizable = false;
  122. panelPos.isDraggable = panel.collapsed;
  123. }
  124. layout.push(panelPos);
  125. }
  126. return layout;
  127. }
  128. onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
  129. for (const newPos of newLayout) {
  130. this.panelMap[newPos.i].updateGridPos(newPos);
  131. }
  132. this.props.dashboard.sortPanelsByGridPos();
  133. // Call render() after any changes. This is called when the layour loads
  134. this.forceUpdate();
  135. };
  136. triggerForceUpdate = () => {
  137. this.forceUpdate();
  138. };
  139. onWidthChange = () => {
  140. for (const panel of this.props.dashboard.panels) {
  141. panel.resizeDone();
  142. }
  143. };
  144. onViewModeChanged = () => {
  145. ignoreNextWidthChange = true;
  146. };
  147. updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
  148. this.panelMap[item.i].updateGridPos(item);
  149. // react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
  150. // So it's required to call it explicitly when panel resized or moved to save layout changes.
  151. this.onLayoutChange(layout);
  152. };
  153. onResize: ItemCallback = (layout, oldItem, newItem) => {
  154. this.panelMap[newItem.i].updateGridPos(newItem);
  155. };
  156. onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
  157. this.updateGridPos(newItem, layout);
  158. this.panelMap[newItem.i].resizeDone();
  159. };
  160. onDragStop: ItemCallback = (layout, oldItem, newItem) => {
  161. this.updateGridPos(newItem, layout);
  162. };
  163. isInView = (panel: PanelModel): boolean => {
  164. if (panel.fullscreen || panel.isEditing) {
  165. return true;
  166. }
  167. // elem is set *after* the first render
  168. const elem = this.panelRef[panel.id.toString()];
  169. if (!elem) {
  170. // NOTE the gridPos is also not valid until after the first render
  171. // since it is passed to the layout engine and made to be valid
  172. // for example, you can have Y=0 for everything and it will stack them
  173. // down vertically in the second call
  174. return false;
  175. }
  176. const top = elem.offsetTop;
  177. const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
  178. const bottom = top + height;
  179. // Show things that are almost in the view
  180. const buffer = 250;
  181. const viewTop = this.props.scrollTop;
  182. if (viewTop > bottom + buffer) {
  183. return false; // The panel is above the viewport
  184. }
  185. // Use the whole browser height (larger than real value)
  186. // TODO? is there a better way
  187. const viewHeight = isNaN(window.innerHeight) ? (window as any).clientHeight : window.innerHeight;
  188. const viewBot = viewTop + viewHeight;
  189. if (top > viewBot + buffer) {
  190. return false;
  191. }
  192. return !this.props.dashboard.otherPanelInFullscreen(panel);
  193. };
  194. renderPanels() {
  195. const panelElements = [];
  196. for (const panel of this.props.dashboard.panels) {
  197. const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
  198. const id = panel.id.toString();
  199. panel.isInView = this.isInView(panel);
  200. panelElements.push(
  201. <div
  202. key={id}
  203. className={panelClasses}
  204. id={'panel-' + id}
  205. ref={elem => {
  206. this.panelRef[id] = elem;
  207. }}
  208. >
  209. <DashboardPanel
  210. panel={panel}
  211. dashboard={this.props.dashboard}
  212. isEditing={panel.isEditing}
  213. isFullscreen={panel.fullscreen}
  214. isInView={panel.isInView}
  215. />
  216. </div>
  217. );
  218. }
  219. return panelElements;
  220. }
  221. render() {
  222. const { dashboard, isFullscreen } = this.props;
  223. return (
  224. <SizedReactLayoutGrid
  225. className={classNames({ layout: true })}
  226. layout={this.buildLayout()}
  227. isResizable={dashboard.meta.canEdit}
  228. isDraggable={dashboard.meta.canEdit}
  229. onLayoutChange={this.onLayoutChange}
  230. onWidthChange={this.onWidthChange}
  231. onDragStop={this.onDragStop}
  232. onResize={this.onResize}
  233. onResizeStop={this.onResizeStop}
  234. isFullscreen={isFullscreen}
  235. >
  236. {this.renderPanels()}
  237. </SizedReactLayoutGrid>
  238. );
  239. }
  240. }
  241. export default hot(module)(DashboardGrid);