2019-02-03 05:29:47 -06:00
|
|
|
// Libaries
|
|
|
|
import React, { PureComponent } from 'react';
|
2018-11-23 08:01:36 -06:00
|
|
|
import { hot } from 'react-hot-loader';
|
2019-01-30 08:28:41 -06:00
|
|
|
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
|
2019-02-03 05:29:47 -06:00
|
|
|
import classNames from 'classnames';
|
2019-08-20 04:14:53 -05:00
|
|
|
// @ts-ignore
|
|
|
|
import sizeMe from 'react-sizeme';
|
2019-02-03 05:29:47 -06:00
|
|
|
|
2020-02-10 07:23:54 -06:00
|
|
|
// Components
|
|
|
|
import { AddPanelWidget } from '../components/AddPanelWidget';
|
|
|
|
import { DashboardRow } from '../components/DashboardRow';
|
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
// Types
|
2017-12-15 09:23:26 -06:00
|
|
|
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
|
|
|
import { DashboardPanel } from './DashboardPanel';
|
2019-01-31 01:56:17 -06:00
|
|
|
import { DashboardModel, PanelModel } from '../state';
|
2019-10-14 03:27:47 -05:00
|
|
|
import { CoreEvents } from 'app/types';
|
2019-10-31 04:48:05 -05:00
|
|
|
import { PanelEvents } from '@grafana/data';
|
2019-10-14 03:27:47 -05:00
|
|
|
import { panelAdded, panelRemoved } from '../state/PanelModel';
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2017-10-11 14:36:03 -05:00
|
|
|
let lastGridWidth = 1200;
|
2018-11-10 09:34:09 -06:00
|
|
|
let ignoreNextWidthChange = false;
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
interface GridWrapperProps {
|
2019-02-13 04:14:53 -06:00
|
|
|
size: { width: number };
|
2019-01-30 08:28:41 -06:00
|
|
|
layout: ReactGridLayout.Layout[];
|
|
|
|
onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
|
|
|
|
children: JSX.Element | JSX.Element[];
|
|
|
|
onDragStop: ItemCallback;
|
|
|
|
onResize: ItemCallback;
|
|
|
|
onResizeStop: ItemCallback;
|
|
|
|
onWidthChange: () => void;
|
|
|
|
className: string;
|
|
|
|
isResizable?: boolean;
|
|
|
|
isDraggable?: boolean;
|
|
|
|
isFullscreen?: boolean;
|
|
|
|
}
|
|
|
|
|
2017-12-15 09:23:26 -06:00
|
|
|
function GridWrapper({
|
|
|
|
size,
|
|
|
|
layout,
|
|
|
|
onLayoutChange,
|
|
|
|
children,
|
|
|
|
onDragStop,
|
|
|
|
onResize,
|
|
|
|
onResizeStop,
|
|
|
|
onWidthChange,
|
|
|
|
className,
|
|
|
|
isResizable,
|
|
|
|
isDraggable,
|
2018-10-27 09:54:04 -05:00
|
|
|
isFullscreen,
|
2019-02-13 04:14:53 -06:00
|
|
|
}: GridWrapperProps) {
|
2017-12-08 04:53:51 -06:00
|
|
|
const width = size.width > 0 ? size.width : lastGridWidth;
|
2018-11-10 09:34:09 -06:00
|
|
|
|
|
|
|
// logic to ignore width changes (optimization)
|
2017-12-08 04:53:51 -06:00
|
|
|
if (width !== lastGridWidth) {
|
2018-11-10 09:34:09 -06:00
|
|
|
if (ignoreNextWidthChange) {
|
|
|
|
ignoreNextWidthChange = false;
|
|
|
|
} else if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
|
2018-10-27 09:54:04 -05:00
|
|
|
onWidthChange();
|
|
|
|
lastGridWidth = width;
|
|
|
|
}
|
2017-10-11 14:36:03 -05:00
|
|
|
}
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2019-11-26 05:58:20 -06:00
|
|
|
/*
|
|
|
|
Disable draggable if mobile device, solving an issue with unintentionally
|
|
|
|
moving panels. https://github.com/grafana/grafana/issues/18497
|
|
|
|
*/
|
|
|
|
const draggable = width <= 420 ? false : isDraggable;
|
|
|
|
|
2017-12-08 04:53:51 -06:00
|
|
|
return (
|
|
|
|
<ReactGridLayout
|
|
|
|
width={lastGridWidth}
|
2017-12-08 05:17:09 -06:00
|
|
|
className={className}
|
2019-11-26 05:58:20 -06:00
|
|
|
isDraggable={draggable}
|
2017-12-15 09:23:26 -06:00
|
|
|
isResizable={isResizable}
|
2017-12-08 04:53:51 -06:00
|
|
|
containerPadding={[0, 0]}
|
2018-12-14 13:53:42 -06:00
|
|
|
useCSSTransforms={false}
|
2017-12-08 04:53:51 -06:00
|
|
|
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
|
|
|
|
cols={GRID_COLUMN_COUNT}
|
|
|
|
rowHeight={GRID_CELL_HEIGHT}
|
|
|
|
draggableHandle=".grid-drag-handle"
|
|
|
|
layout={layout}
|
|
|
|
onResize={onResize}
|
|
|
|
onResizeStop={onResizeStop}
|
2017-12-13 10:53:57 -06:00
|
|
|
onDragStop={onDragStop}
|
2018-01-03 06:33:54 -06:00
|
|
|
onLayoutChange={onLayoutChange}
|
|
|
|
>
|
2017-12-08 04:53:51 -06:00
|
|
|
{children}
|
|
|
|
</ReactGridLayout>
|
|
|
|
);
|
2017-10-09 10:24:10 -05:00
|
|
|
}
|
|
|
|
|
2019-08-20 04:14:53 -05:00
|
|
|
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
export interface Props {
|
2018-06-19 14:25:57 -05:00
|
|
|
dashboard: DashboardModel;
|
2019-02-03 05:29:47 -06:00
|
|
|
isEditing: boolean;
|
|
|
|
isFullscreen: boolean;
|
2019-05-03 08:35:37 -05:00
|
|
|
scrollTop: number;
|
2020-02-12 11:36:32 -06:00
|
|
|
isNewEditorOpen?: boolean;
|
2017-10-09 10:24:10 -05:00
|
|
|
}
|
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
export class DashboardGrid extends PureComponent<Props> {
|
2017-12-15 09:23:26 -06:00
|
|
|
panelMap: { [id: string]: PanelModel };
|
2019-05-03 08:35:37 -05:00
|
|
|
panelRef: { [id: string]: HTMLElement } = {};
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
componentDidMount() {
|
|
|
|
const { dashboard } = this.props;
|
2020-02-10 07:23:54 -06:00
|
|
|
|
2019-10-14 03:27:47 -05:00
|
|
|
dashboard.on(panelAdded, this.triggerForceUpdate);
|
|
|
|
dashboard.on(panelRemoved, this.triggerForceUpdate);
|
|
|
|
dashboard.on(CoreEvents.repeatsProcessed, this.triggerForceUpdate);
|
|
|
|
dashboard.on(PanelEvents.viewModeChanged, this.onViewModeChanged);
|
|
|
|
dashboard.on(CoreEvents.rowCollapsed, this.triggerForceUpdate);
|
|
|
|
dashboard.on(CoreEvents.rowExpanded, this.triggerForceUpdate);
|
2017-10-09 10:24:10 -05:00
|
|
|
}
|
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
componentWillUnmount() {
|
|
|
|
const { dashboard } = this.props;
|
2019-10-14 03:27:47 -05:00
|
|
|
dashboard.off(panelAdded, this.triggerForceUpdate);
|
|
|
|
dashboard.off(panelRemoved, this.triggerForceUpdate);
|
|
|
|
dashboard.off(CoreEvents.repeatsProcessed, this.triggerForceUpdate);
|
|
|
|
dashboard.off(PanelEvents.viewModeChanged, this.onViewModeChanged);
|
|
|
|
dashboard.off(CoreEvents.rowCollapsed, this.triggerForceUpdate);
|
|
|
|
dashboard.off(CoreEvents.rowExpanded, this.triggerForceUpdate);
|
2019-02-03 05:29:47 -06:00
|
|
|
}
|
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
buildLayout() {
|
|
|
|
const layout = [];
|
2017-10-10 07:20:53 -05:00
|
|
|
this.panelMap = {};
|
2017-10-10 02:34:14 -05:00
|
|
|
|
2018-09-10 08:42:36 -05:00
|
|
|
for (const panel of this.props.dashboard.panels) {
|
2018-08-26 10:14:40 -05:00
|
|
|
const stringId = panel.id.toString();
|
2017-10-10 07:20:53 -05:00
|
|
|
this.panelMap[stringId] = panel;
|
|
|
|
|
|
|
|
if (!panel.gridPos) {
|
|
|
|
console.log('panel without gridpos');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-08-26 10:14:40 -05:00
|
|
|
const panelPos: any = {
|
2017-10-10 07:20:53 -05:00
|
|
|
i: stringId,
|
|
|
|
x: panel.gridPos.x,
|
|
|
|
y: panel.gridPos.y,
|
|
|
|
w: panel.gridPos.w,
|
|
|
|
h: panel.gridPos.h,
|
2017-10-16 09:09:23 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
if (panel.type === 'row') {
|
|
|
|
panelPos.w = GRID_COLUMN_COUNT;
|
|
|
|
panelPos.h = 1;
|
|
|
|
panelPos.isResizable = false;
|
2017-10-17 07:53:52 -05:00
|
|
|
panelPos.isDraggable = panel.collapsed;
|
2017-10-16 09:09:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
layout.push(panelPos);
|
2017-10-09 10:24:10 -05:00
|
|
|
}
|
2017-10-10 02:34:14 -05:00
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
return layout;
|
|
|
|
}
|
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
|
2017-10-10 07:20:53 -05:00
|
|
|
for (const newPos of newLayout) {
|
2020-02-17 00:25:27 -06:00
|
|
|
this.panelMap[newPos.i!].updateGridPos(newPos);
|
2017-10-10 07:20:53 -05:00
|
|
|
}
|
2017-10-24 05:33:14 -05:00
|
|
|
|
2018-06-19 14:25:57 -05:00
|
|
|
this.props.dashboard.sortPanelsByGridPos();
|
2019-05-03 08:35:37 -05:00
|
|
|
|
|
|
|
// Call render() after any changes. This is called when the layour loads
|
|
|
|
this.forceUpdate();
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
triggerForceUpdate = () => {
|
2017-10-10 10:57:53 -05:00
|
|
|
this.forceUpdate();
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-10-10 10:57:53 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
onWidthChange = () => {
|
2018-06-19 14:25:57 -05:00
|
|
|
for (const panel of this.props.dashboard.panels) {
|
2017-10-11 14:36:03 -05:00
|
|
|
panel.resizeDone();
|
|
|
|
}
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-10-11 14:36:03 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
onViewModeChanged = () => {
|
2018-11-10 09:34:09 -06:00
|
|
|
ignoreNextWidthChange = true;
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2018-06-19 09:57:55 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
|
2020-02-17 00:25:27 -06:00
|
|
|
this.panelMap[item.i!].updateGridPos(item);
|
2017-12-13 10:53:57 -06:00
|
|
|
|
|
|
|
// react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
|
|
|
|
// So it's required to call it explicitly when panel resized or moved to save layout changes.
|
|
|
|
this.onLayoutChange(layout);
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-12-13 10:53:57 -06:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
onResize: ItemCallback = (layout, oldItem, newItem) => {
|
2020-02-17 00:25:27 -06:00
|
|
|
this.panelMap[newItem.i!].updateGridPos(newItem);
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-10-10 10:57:53 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
|
2017-12-13 10:53:57 -06:00
|
|
|
this.updateGridPos(newItem, layout);
|
2020-02-17 00:25:27 -06:00
|
|
|
this.panelMap[newItem.i!].resizeDone();
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-10-11 14:36:03 -05:00
|
|
|
|
2019-01-30 08:28:41 -06:00
|
|
|
onDragStop: ItemCallback = (layout, oldItem, newItem) => {
|
2017-12-13 10:53:57 -06:00
|
|
|
this.updateGridPos(newItem, layout);
|
2019-02-13 04:14:53 -06:00
|
|
|
};
|
2017-12-13 10:53:57 -06:00
|
|
|
|
2019-05-03 08:35:37 -05:00
|
|
|
isInView = (panel: PanelModel): boolean => {
|
|
|
|
if (panel.fullscreen || panel.isEditing) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// elem is set *after* the first render
|
|
|
|
const elem = this.panelRef[panel.id.toString()];
|
|
|
|
if (!elem) {
|
|
|
|
// NOTE the gridPos is also not valid until after the first render
|
|
|
|
// since it is passed to the layout engine and made to be valid
|
|
|
|
// for example, you can have Y=0 for everything and it will stack them
|
|
|
|
// down vertically in the second call
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-05-14 07:41:24 -05:00
|
|
|
const top = elem.offsetTop;
|
2019-05-03 08:35:37 -05:00
|
|
|
const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
|
|
|
|
const bottom = top + height;
|
|
|
|
|
|
|
|
// Show things that are almost in the view
|
|
|
|
const buffer = 250;
|
|
|
|
|
|
|
|
const viewTop = this.props.scrollTop;
|
|
|
|
if (viewTop > bottom + buffer) {
|
|
|
|
return false; // The panel is above the viewport
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use the whole browser height (larger than real value)
|
|
|
|
// TODO? is there a better way
|
|
|
|
const viewHeight = isNaN(window.innerHeight) ? (window as any).clientHeight : window.innerHeight;
|
|
|
|
const viewBot = viewTop + viewHeight;
|
|
|
|
if (top > viewBot + buffer) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !this.props.dashboard.otherPanelInFullscreen(panel);
|
|
|
|
};
|
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
renderPanels() {
|
|
|
|
const panelElements = [];
|
2020-02-10 07:23:54 -06:00
|
|
|
|
2018-09-10 08:42:36 -05:00
|
|
|
for (const panel of this.props.dashboard.panels) {
|
2018-11-20 09:00:19 -06:00
|
|
|
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
|
2019-05-03 08:35:37 -05:00
|
|
|
const id = panel.id.toString();
|
2019-05-03 14:55:22 -05:00
|
|
|
panel.isInView = this.isInView(panel);
|
2020-02-10 07:23:54 -06:00
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
panelElements.push(
|
2020-02-17 00:25:27 -06:00
|
|
|
<div key={id} className={panelClasses} id={'panel-' + id} ref={elem => elem && (this.panelRef[id] = elem)}>
|
2020-02-10 07:23:54 -06:00
|
|
|
{this.renderPanel(panel)}
|
2018-01-03 06:33:54 -06:00
|
|
|
</div>
|
2017-10-09 10:24:10 -05:00
|
|
|
);
|
|
|
|
}
|
2017-10-10 02:34:14 -05:00
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
return panelElements;
|
|
|
|
}
|
|
|
|
|
2020-02-10 07:23:54 -06:00
|
|
|
renderPanel(panel: PanelModel) {
|
|
|
|
if (panel.type === 'row') {
|
|
|
|
return <DashboardRow panel={panel} dashboard={this.props.dashboard} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === 'add-panel') {
|
|
|
|
return <AddPanelWidget panel={panel} dashboard={this.props.dashboard} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<DashboardPanel
|
|
|
|
panel={panel}
|
|
|
|
dashboard={this.props.dashboard}
|
|
|
|
isEditing={panel.isEditing}
|
|
|
|
isFullscreen={panel.fullscreen}
|
|
|
|
isInView={panel.isInView}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
render() {
|
2019-02-03 05:29:47 -06:00
|
|
|
const { dashboard, isFullscreen } = this.props;
|
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
return (
|
2017-10-11 14:36:03 -05:00
|
|
|
<SizedReactLayoutGrid
|
2018-12-14 13:53:42 -06:00
|
|
|
className={classNames({ layout: true })}
|
2017-10-11 14:36:03 -05:00
|
|
|
layout={this.buildLayout()}
|
2019-02-03 05:29:47 -06:00
|
|
|
isResizable={dashboard.meta.canEdit}
|
|
|
|
isDraggable={dashboard.meta.canEdit}
|
2017-10-11 14:36:03 -05:00
|
|
|
onLayoutChange={this.onLayoutChange}
|
|
|
|
onWidthChange={this.onWidthChange}
|
2017-12-13 10:53:57 -06:00
|
|
|
onDragStop={this.onDragStop}
|
2017-10-11 14:36:03 -05:00
|
|
|
onResize={this.onResize}
|
2018-01-03 06:33:54 -06:00
|
|
|
onResizeStop={this.onResizeStop}
|
2019-02-03 05:29:47 -06:00
|
|
|
isFullscreen={isFullscreen}
|
2018-01-03 06:33:54 -06:00
|
|
|
>
|
2017-10-09 10:24:10 -05:00
|
|
|
{this.renderPanels()}
|
|
|
|
</SizedReactLayoutGrid>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2018-11-23 08:01:36 -06:00
|
|
|
|
|
|
|
export default hot(module)(DashboardGrid);
|