2022-04-22 08:33:13 -05:00
|
|
|
import classNames from 'classnames';
|
2021-06-22 07:44:18 -05:00
|
|
|
import React, { PureComponent, CSSProperties } from 'react';
|
2019-01-30 08:28:41 -06:00
|
|
|
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
|
2022-04-22 08:33:13 -05:00
|
|
|
import { connect, ConnectedProps } from 'react-redux';
|
2021-06-22 07:44:18 -05:00
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
2022-04-22 08:33:13 -05:00
|
|
|
import { Subscription } from 'rxjs';
|
|
|
|
|
|
|
|
import { config } from '@grafana/runtime';
|
|
|
|
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
|
|
|
import { cleanAndRemoveMany } from 'app/features/panel/state/actions';
|
|
|
|
import { DashboardPanelsChangedEvent } from 'app/types/events';
|
2019-02-03 05:29:47 -06:00
|
|
|
|
2020-02-10 07:23:54 -06:00
|
|
|
import { AddPanelWidget } from '../components/AddPanelWidget';
|
|
|
|
import { DashboardRow } from '../components/DashboardRow';
|
2019-01-31 01:56:17 -06:00
|
|
|
import { DashboardModel, PanelModel } from '../state';
|
2021-06-22 07:44:18 -05:00
|
|
|
import { GridPos } from '../state/PanelModel';
|
2022-04-22 08:33:13 -05:00
|
|
|
|
|
|
|
import { DashboardPanel } from './DashboardPanel';
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2022-04-12 08:12:03 -05:00
|
|
|
export interface OwnProps {
|
2018-06-19 14:25:57 -05:00
|
|
|
dashboard: DashboardModel;
|
2020-04-10 09:37:26 -05:00
|
|
|
editPanel: PanelModel | null;
|
|
|
|
viewPanel: PanelModel | null;
|
2017-10-09 10:24:10 -05:00
|
|
|
}
|
|
|
|
|
2021-06-15 07:12:32 -05:00
|
|
|
export interface State {
|
|
|
|
isLayoutInitialized: boolean;
|
|
|
|
}
|
2021-06-22 07:44:18 -05:00
|
|
|
|
2022-04-12 08:12:03 -05:00
|
|
|
const mapDispatchToProps = {
|
|
|
|
cleanAndRemoveMany,
|
|
|
|
};
|
|
|
|
|
|
|
|
const connector = connect(null, mapDispatchToProps);
|
|
|
|
|
|
|
|
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
|
|
|
|
|
|
|
export class DashboardGridUnconnected extends PureComponent<Props, State> {
|
2021-09-01 10:03:56 -05:00
|
|
|
private panelMap: { [key: string]: PanelModel } = {};
|
2020-11-26 08:14:57 -06:00
|
|
|
private eventSubs = new Subscription();
|
2021-06-22 07:44:18 -05:00
|
|
|
private windowHeight = 1200;
|
2021-08-17 10:53:25 -05:00
|
|
|
private windowWidth = 1920;
|
2021-06-22 07:44:18 -05:00
|
|
|
private gridWidth = 0;
|
2021-10-28 06:02:01 -05:00
|
|
|
/** Used to keep track of mobile panel layout position */
|
|
|
|
private lastPanelBottom = 0;
|
2017-10-09 10:24:10 -05:00
|
|
|
|
2021-06-15 07:12:32 -05:00
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
isLayoutInitialized: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
componentDidMount() {
|
|
|
|
const { dashboard } = this.props;
|
2020-11-26 08:14:57 -06:00
|
|
|
this.eventSubs.add(dashboard.events.subscribe(DashboardPanelsChangedEvent, this.triggerForceUpdate));
|
2017-10-09 10:24:10 -05:00
|
|
|
}
|
|
|
|
|
2019-02-03 05:29:47 -06:00
|
|
|
componentWillUnmount() {
|
2020-11-26 08:14:57 -06:00
|
|
|
this.eventSubs.unsubscribe();
|
2022-04-12 08:12:03 -05:00
|
|
|
this.props.cleanAndRemoveMany(Object.keys(this.panelMap));
|
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) {
|
2021-09-01 10:03:56 -05:00
|
|
|
if (!panel.key) {
|
|
|
|
panel.key = `panel-${panel.id}-${Date.now()}`;
|
|
|
|
}
|
|
|
|
this.panelMap[panel.key] = panel;
|
2017-10-10 07:20:53 -05:00
|
|
|
|
|
|
|
if (!panel.gridPos) {
|
|
|
|
console.log('panel without gridpos');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-08-26 10:14:40 -05:00
|
|
|
const panelPos: any = {
|
2021-09-01 10:03:56 -05:00
|
|
|
i: panel.key,
|
2017-10-10 07:20:53 -05:00
|
|
|
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) {
|
2022-06-27 06:30:59 -05:00
|
|
|
this.panelMap[newPos.i!].updateGridPos(newPos, this.state.isLayoutInitialized);
|
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();
|
2021-06-22 07:44:18 -05:00
|
|
|
|
|
|
|
// This is called on grid mount as it can correct invalid initial grid positions
|
|
|
|
if (!this.state.isLayoutInitialized) {
|
|
|
|
this.setState({ isLayoutInitialized: true });
|
|
|
|
}
|
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
|
|
|
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
|
2020-02-17 00:25:27 -06:00
|
|
|
this.panelMap[item.i!].updateGridPos(item);
|
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) => {
|
2021-09-20 11:28:46 -05:00
|
|
|
const panel = this.panelMap[newItem.i!];
|
|
|
|
panel.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);
|
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
|
|
|
|
2021-10-28 06:02:01 -05:00
|
|
|
getPanelScreenPos(panel: PanelModel, gridWidth: number): { top: number; bottom: number } {
|
|
|
|
let top = 0;
|
|
|
|
|
|
|
|
// mobile layout
|
|
|
|
if (gridWidth < config.theme2.breakpoints.values.md) {
|
|
|
|
// In mobile layout panels are stacked so we just add the panel vertical margin to the last panel bottom position
|
|
|
|
top = this.lastPanelBottom + GRID_CELL_VMARGIN;
|
|
|
|
} else {
|
|
|
|
// For top position we need to add back the vertical margin removed by translateGridHeightToScreenHeight
|
|
|
|
top = translateGridHeightToScreenHeight(panel.gridPos.y) + GRID_CELL_VMARGIN;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.lastPanelBottom = top + translateGridHeightToScreenHeight(panel.gridPos.h);
|
|
|
|
|
|
|
|
return { top, bottom: this.lastPanelBottom };
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:44:18 -05:00
|
|
|
renderPanels(gridWidth: number) {
|
2017-10-09 10:24:10 -05:00
|
|
|
const panelElements = [];
|
2020-02-10 07:23:54 -06:00
|
|
|
|
2021-10-28 06:02:01 -05:00
|
|
|
// Reset last panel bottom
|
|
|
|
this.lastPanelBottom = 0;
|
|
|
|
|
2021-06-22 07:44:18 -05:00
|
|
|
// This is to avoid layout re-flows, accessing window.innerHeight can trigger re-flow
|
|
|
|
// We assume here that if width change height might have changed as well
|
|
|
|
if (this.gridWidth !== gridWidth) {
|
|
|
|
this.windowHeight = window.innerHeight ?? 1000;
|
2021-08-17 10:53:25 -05:00
|
|
|
this.windowWidth = window.innerWidth;
|
2021-06-22 07:44:18 -05:00
|
|
|
this.gridWidth = gridWidth;
|
|
|
|
}
|
|
|
|
|
2018-09-10 08:42:36 -05:00
|
|
|
for (const panel of this.props.dashboard.panels) {
|
2020-04-10 09:37:26 -05:00
|
|
|
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.isViewing });
|
2021-06-22 07:44:18 -05:00
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
panelElements.push(
|
2021-06-22 07:44:18 -05:00
|
|
|
<GrafanaGridItem
|
2021-09-01 10:03:56 -05:00
|
|
|
key={panel.key}
|
2021-06-22 07:44:18 -05:00
|
|
|
className={panelClasses}
|
2021-09-01 10:03:56 -05:00
|
|
|
data-panelid={panel.id}
|
2021-06-22 07:44:18 -05:00
|
|
|
gridPos={panel.gridPos}
|
|
|
|
gridWidth={gridWidth}
|
|
|
|
windowHeight={this.windowHeight}
|
2021-08-17 10:53:25 -05:00
|
|
|
windowWidth={this.windowWidth}
|
2021-06-22 07:44:18 -05:00
|
|
|
isViewing={panel.isViewing}
|
|
|
|
>
|
|
|
|
{(width: number, height: number) => {
|
2021-10-13 01:53:36 -05:00
|
|
|
return this.renderPanel(panel, width, height);
|
2021-06-22 07:44:18 -05:00
|
|
|
}}
|
|
|
|
</GrafanaGridItem>
|
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;
|
|
|
|
}
|
|
|
|
|
2021-10-13 01:53:36 -05:00
|
|
|
renderPanel(panel: PanelModel, width: any, height: any) {
|
2020-02-10 07:23:54 -06:00
|
|
|
if (panel.type === 'row') {
|
2021-10-13 01:53:36 -05:00
|
|
|
return <DashboardRow key={panel.key} panel={panel} dashboard={this.props.dashboard} />;
|
2020-02-10 07:23:54 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === 'add-panel') {
|
2021-10-13 01:53:36 -05:00
|
|
|
return <AddPanelWidget key={panel.key} panel={panel} dashboard={this.props.dashboard} />;
|
2020-02-10 07:23:54 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<DashboardPanel
|
2021-10-13 01:53:36 -05:00
|
|
|
key={panel.key}
|
|
|
|
stateKey={panel.key}
|
2020-02-10 07:23:54 -06:00
|
|
|
panel={panel}
|
|
|
|
dashboard={this.props.dashboard}
|
|
|
|
isEditing={panel.isEditing}
|
2020-04-10 09:37:26 -05:00
|
|
|
isViewing={panel.isViewing}
|
2021-06-22 07:44:18 -05:00
|
|
|
width={width}
|
|
|
|
height={height}
|
2020-02-10 07:23:54 -06:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-10-09 10:24:10 -05:00
|
|
|
render() {
|
2021-06-22 07:44:18 -05:00
|
|
|
const { dashboard } = this.props;
|
2021-12-13 06:42:33 -06:00
|
|
|
|
|
|
|
/**
|
|
|
|
* We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer
|
|
|
|
* properly working. For more information go here:
|
|
|
|
* https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container
|
|
|
|
*/
|
2017-10-09 10:24:10 -05:00
|
|
|
return (
|
2021-12-13 06:42:33 -06:00
|
|
|
<div style={{ flex: '1 1 auto', display: this.props.editPanel ? 'none' : undefined }}>
|
2021-10-20 09:29:07 -05:00
|
|
|
<AutoSizer disableHeight>
|
|
|
|
{({ width }) => {
|
|
|
|
if (width === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const draggable = width <= 769 ? false : dashboard.meta.canEdit;
|
|
|
|
|
|
|
|
/*
|
2021-06-22 07:44:18 -05:00
|
|
|
Disable draggable if mobile device, solving an issue with unintentionally
|
|
|
|
moving panels. https://github.com/grafana/grafana/issues/18497
|
2021-06-30 02:06:17 -05:00
|
|
|
theme.breakpoints.md = 769
|
2021-06-22 07:44:18 -05:00
|
|
|
*/
|
|
|
|
|
2021-10-20 09:29:07 -05:00
|
|
|
return (
|
|
|
|
/**
|
|
|
|
* The children is using a width of 100% so we need to guarantee that it is wrapped
|
|
|
|
* in an element that has the calculated size given by the AutoSizer. The AutoSizer
|
|
|
|
* has a width of 0 and will let its content overflow its div.
|
|
|
|
*/
|
|
|
|
<div style={{ width: `${width}px`, height: '100%' }}>
|
|
|
|
<ReactGridLayout
|
|
|
|
width={width}
|
|
|
|
isDraggable={draggable}
|
|
|
|
isResizable={dashboard.meta.canEdit}
|
|
|
|
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={this.buildLayout()}
|
|
|
|
onDragStop={this.onDragStop}
|
|
|
|
onResize={this.onResize}
|
|
|
|
onResizeStop={this.onResizeStop}
|
|
|
|
onLayoutChange={this.onLayoutChange}
|
|
|
|
>
|
|
|
|
{this.renderPanels(width)}
|
|
|
|
</ReactGridLayout>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
</AutoSizer>
|
|
|
|
</div>
|
2017-10-09 10:24:10 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2021-06-22 07:44:18 -05:00
|
|
|
|
|
|
|
interface GrafanaGridItemProps extends Record<string, any> {
|
|
|
|
gridWidth?: number;
|
|
|
|
gridPos?: GridPos;
|
|
|
|
isViewing: string;
|
|
|
|
windowHeight: number;
|
2021-08-17 10:53:25 -05:00
|
|
|
windowWidth: number;
|
2021-06-22 07:44:18 -05:00
|
|
|
children: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A hacky way to intercept the react-layout-grid item dimensions and pass them to DashboardPanel
|
|
|
|
*/
|
|
|
|
const GrafanaGridItem = React.forwardRef<HTMLDivElement, GrafanaGridItemProps>((props, ref) => {
|
|
|
|
const theme = config.theme2;
|
|
|
|
let width = 100;
|
|
|
|
let height = 100;
|
|
|
|
|
2021-08-17 10:53:25 -05:00
|
|
|
const { gridWidth, gridPos, isViewing, windowHeight, windowWidth, ...divProps } = props;
|
2021-06-22 07:44:18 -05:00
|
|
|
const style: CSSProperties = props.style ?? {};
|
|
|
|
|
|
|
|
if (isViewing) {
|
2021-10-28 06:02:01 -05:00
|
|
|
// In fullscreen view mode a single panel take up full width & 85% height
|
2021-08-17 10:53:25 -05:00
|
|
|
width = gridWidth!;
|
2021-06-22 07:44:18 -05:00
|
|
|
height = windowHeight * 0.85;
|
|
|
|
style.height = height;
|
|
|
|
style.width = '100%';
|
2021-08-17 10:53:25 -05:00
|
|
|
} else if (windowWidth < theme.breakpoints.values.md) {
|
2021-10-28 06:02:01 -05:00
|
|
|
// Mobile layout is a bit different, every panel take up full width
|
2021-06-22 07:44:18 -05:00
|
|
|
width = props.gridWidth!;
|
2021-10-28 06:02:01 -05:00
|
|
|
height = translateGridHeightToScreenHeight(gridPos!.h);
|
2021-06-22 07:44:18 -05:00
|
|
|
style.height = height;
|
|
|
|
style.width = '100%';
|
|
|
|
} else {
|
2021-10-28 06:02:01 -05:00
|
|
|
// Normal grid layout. The grid framework passes width and height directly to children as style props.
|
2021-06-22 07:44:18 -05:00
|
|
|
width = parseFloat(props.style.width);
|
|
|
|
height = parseFloat(props.style.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
// props.children[0] is our main children. RGL adds the drag handle at props.children[1]
|
|
|
|
return (
|
|
|
|
<div {...divProps} ref={ref}>
|
|
|
|
{/* Pass width and height to children as render props */}
|
|
|
|
{[props.children[0](width, height), props.children.slice(1)]}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2021-10-28 06:02:01 -05:00
|
|
|
/**
|
|
|
|
* This translates grid height dimensions to real pixels
|
|
|
|
*/
|
|
|
|
function translateGridHeightToScreenHeight(gridHeight: number): number {
|
|
|
|
return gridHeight * (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN) - GRID_CELL_VMARGIN;
|
|
|
|
}
|
|
|
|
|
2021-06-22 07:44:18 -05:00
|
|
|
GrafanaGridItem.displayName = 'GridItemWithDimensions';
|
2022-04-12 08:12:03 -05:00
|
|
|
|
|
|
|
export const DashboardGrid = connector(DashboardGridUnconnected);
|