grafana/public/app/features/dashboard/dashgrid/DashboardGrid.tsx

308 lines
8.6 KiB
TypeScript
Raw Normal View History

// Libaries
import React, { PureComponent } from 'react';
2018-11-23 08:01:36 -06:00
import { hot } from 'react-hot-loader';
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
import classNames from 'classnames';
// @ts-ignore
import sizeMe from 'react-sizeme';
// Components
import { AddPanelWidget } from '../components/AddPanelWidget';
import { DashboardRow } from '../components/DashboardRow';
// Types
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
import { DashboardPanel } from './DashboardPanel';
import { DashboardModel, PanelModel } from '../state';
import { CoreEvents } from 'app/types';
import { PanelEvents } from '@grafana/data';
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
interface GridWrapperProps {
size: { width: number };
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;
}
function GridWrapper({
size,
layout,
onLayoutChange,
children,
onDragStop,
onResize,
onResizeStop,
onWidthChange,
className,
isResizable,
isDraggable,
isFullscreen,
}: GridWrapperProps) {
const width = size.width > 0 ? size.width : lastGridWidth;
2018-11-10 09:34:09 -06:00
// logic to ignore width changes (optimization)
if (width !== lastGridWidth) {
2018-11-10 09:34:09 -06:00
if (ignoreNextWidthChange) {
ignoreNextWidthChange = false;
} else if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
onWidthChange();
lastGridWidth = width;
}
2017-10-11 14:36:03 -05:00
}
2017-10-09 10:24:10 -05: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;
return (
<ReactGridLayout
width={lastGridWidth}
2017-12-08 05:17:09 -06:00
className={className}
isDraggable={draggable}
isResizable={isResizable}
containerPadding={[0, 0]}
useCSSTransforms={false}
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}
onDragStop={onDragStop}
onLayoutChange={onLayoutChange}
>
{children}
</ReactGridLayout>
);
2017-10-09 10:24:10 -05:00
}
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
2017-10-09 10:24:10 -05:00
export interface Props {
2018-06-19 14:25:57 -05:00
dashboard: DashboardModel;
isEditing: boolean;
isFullscreen: boolean;
scrollTop: number;
isNewEditorOpen?: boolean;
2017-10-09 10:24:10 -05:00
}
export class DashboardGrid extends PureComponent<Props> {
panelMap: { [id: string]: PanelModel };
panelRef: { [id: string]: HTMLElement } = {};
2017-10-09 10:24:10 -05:00
componentDidMount() {
const { dashboard } = this.props;
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
}
componentWillUnmount() {
const { dashboard } = this.props;
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);
}
2017-10-09 10:24:10 -05:00
buildLayout() {
const layout = [];
this.panelMap = {};
2017-10-10 02:34:14 -05:00
for (const panel of this.props.dashboard.panels) {
const stringId = panel.id.toString();
this.panelMap[stringId] = panel;
if (!panel.gridPos) {
console.log('panel without gridpos');
continue;
}
const panelPos: any = {
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;
}
onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
for (const newPos of newLayout) {
this.panelMap[newPos.i!].updateGridPos(newPos);
}
2018-06-19 14:25:57 -05:00
this.props.dashboard.sortPanelsByGridPos();
// Call render() after any changes. This is called when the layour loads
this.forceUpdate();
};
2017-10-09 10:24:10 -05:00
triggerForceUpdate = () => {
2017-10-10 10:57:53 -05:00
this.forceUpdate();
};
2017-10-10 10:57:53 -05: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();
}
};
2017-10-11 14:36:03 -05:00
onViewModeChanged = () => {
2018-11-10 09:34:09 -06:00
ignoreNextWidthChange = true;
};
2018-06-19 09:57:55 -05:00
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
this.panelMap[item.i!].updateGridPos(item);
// 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);
};
onResize: ItemCallback = (layout, oldItem, newItem) => {
this.panelMap[newItem.i!].updateGridPos(newItem);
};
2017-10-10 10:57:53 -05:00
onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
this.updateGridPos(newItem, layout);
this.panelMap[newItem.i!].resizeDone();
};
2017-10-11 14:36:03 -05:00
onDragStop: ItemCallback = (layout, oldItem, newItem) => {
this.updateGridPos(newItem, layout);
};
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;
}
const top = elem.offsetTop;
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 = [];
for (const panel of this.props.dashboard.panels) {
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
const id = panel.id.toString();
panel.isInView = this.isInView(panel);
2017-10-09 10:24:10 -05:00
panelElements.push(
<div key={id} className={panelClasses} id={'panel-' + id} ref={elem => elem && (this.panelRef[id] = elem)}>
{this.renderPanel(panel)}
</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;
}
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() {
const { dashboard, isFullscreen } = this.props;
2017-10-09 10:24:10 -05:00
return (
2017-10-11 14:36:03 -05:00
<SizedReactLayoutGrid
className={classNames({ layout: true })}
2017-10-11 14:36:03 -05:00
layout={this.buildLayout()}
isResizable={dashboard.meta.canEdit}
isDraggable={dashboard.meta.canEdit}
2017-10-11 14:36:03 -05:00
onLayoutChange={this.onLayoutChange}
onWidthChange={this.onWidthChange}
onDragStop={this.onDragStop}
2017-10-11 14:36:03 -05:00
onResize={this.onResize}
onResizeStop={this.onResizeStop}
isFullscreen={isFullscreen}
>
2017-10-09 10:24:10 -05:00
{this.renderPanels()}
</SizedReactLayoutGrid>
);
}
}
2018-11-23 08:01:36 -06:00
export default hot(module)(DashboardGrid);