Dashboard: Canvas add buttons to custom grid (#103181)
* Custom grid add actions * find empty space * Update * Update * Update * Update
This commit is contained in:
@@ -33,7 +33,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
|
||||
size="lg"
|
||||
onClick={onGoBack}
|
||||
tooltip={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
|
||||
aria-abel={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
|
||||
aria-label={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
|
||||
/>
|
||||
)}
|
||||
<Text>{elementInfo.typeName}</Text>
|
||||
|
||||
+68
-12
@@ -1,3 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
SceneObjectState,
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
SceneGridLayoutDragStartEvent,
|
||||
} from '@grafana/scenes';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
||||
@@ -35,11 +39,13 @@ import {
|
||||
useDashboard,
|
||||
getLayoutOrchestratorFor,
|
||||
} from '../../utils/utils';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
|
||||
import { findSpaceForNewPanel } from './findSpaceForNewPanel';
|
||||
import { RowActions } from './row-actions/RowActions';
|
||||
|
||||
interface DefaultGridLayoutManagerState extends SceneObjectState {
|
||||
@@ -102,18 +108,28 @@ export class DefaultGridLayoutManager
|
||||
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
|
||||
vizPanel.clearParent();
|
||||
|
||||
const newGridItem = new DashboardGridItem({
|
||||
height: NEW_PANEL_HEIGHT,
|
||||
width: NEW_PANEL_WIDTH,
|
||||
x: 0,
|
||||
y: 0,
|
||||
body: vizPanel,
|
||||
key: getGridItemKeyForPanelId(panelId),
|
||||
});
|
||||
// With new edit mode we add panels to the bottom of the grid
|
||||
if (config.featureToggles.dashboardNewLayouts) {
|
||||
const emptySpace = findSpaceForNewPanel(this.state.grid);
|
||||
const newGridItem = new DashboardGridItem({
|
||||
...emptySpace,
|
||||
body: vizPanel,
|
||||
key: getGridItemKeyForPanelId(panelId),
|
||||
});
|
||||
|
||||
this.state.grid.setState({
|
||||
children: [newGridItem, ...this.state.grid.state.children],
|
||||
});
|
||||
this.state.grid.setState({ children: [...this.state.grid.state.children, newGridItem] });
|
||||
} else {
|
||||
const newGridItem = new DashboardGridItem({
|
||||
height: NEW_PANEL_HEIGHT,
|
||||
width: NEW_PANEL_WIDTH,
|
||||
x: 0,
|
||||
y: 0,
|
||||
body: vizPanel,
|
||||
key: getGridItemKeyForPanelId(panelId),
|
||||
});
|
||||
|
||||
this.state.grid.setState({ children: [newGridItem, ...this.state.grid.state.children] });
|
||||
}
|
||||
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(vizPanel), true);
|
||||
}
|
||||
@@ -505,6 +521,8 @@ export class DefaultGridLayoutManager
|
||||
function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<DefaultGridLayoutManager>) {
|
||||
const { children } = useSceneObjectState(model.state.grid, { shouldActivateOrKeepAlive: true });
|
||||
const dashboard = useDashboard(model);
|
||||
const { isEditing } = dashboard.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// If we are top level layout and we have no children, show empty state
|
||||
if (model.parent === dashboard && children.length === 0) {
|
||||
@@ -513,5 +531,43 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<Default
|
||||
);
|
||||
}
|
||||
|
||||
return <model.state.grid.Component model={model.state.grid} />;
|
||||
return (
|
||||
<div className={cx(styles.container, isEditing && styles.containerEditing)}>
|
||||
{model.state.grid.Component && <model.state.grid.Component model={model.state.grid} />}
|
||||
{isEditing && (
|
||||
<div className={styles.actionsWrapper}>
|
||||
<CanvasGridAddActions layoutManager={model} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
containerEditing: css({
|
||||
// In editing the add actions should live at the bottom of the grid so we have to
|
||||
// disable flex grow on the SceneGridLayouts first div
|
||||
'> div:first-child': {
|
||||
flexGrow: `0 !important`,
|
||||
minHeight: '250px',
|
||||
},
|
||||
'&:hover': {
|
||||
'.dashboard-canvas-add-button': {
|
||||
opacity: 1,
|
||||
filter: 'unset',
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsWrapper: css({
|
||||
position: 'relative',
|
||||
paddingBottom: theme.spacing(5),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { SceneGridItemLike, SceneGridLayout, SceneGridRow } from '@grafana/scenes';
|
||||
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
|
||||
export interface GridCell {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const NEW_PANEL_MIN_WIDTH = 8;
|
||||
const NEW_PANEL_MIN_HEIGHT = 6;
|
||||
const NEW_PANEL_WIDTH = 12;
|
||||
const NEW_PANEL_HEIGHT = 8;
|
||||
|
||||
export function findSpaceForNewPanel(grid: SceneGridLayout): GridCell | null {
|
||||
// Build a grid of occupied spaces as a Set of "x,y" strings for fast lookup
|
||||
const occupied = new Set<string>();
|
||||
let maxY = 0;
|
||||
|
||||
function addPanelToOccupied(panel: SceneGridItemLike) {
|
||||
for (let dx = 0; dx < panel.state.width!; dx++) {
|
||||
for (let dy = 0; dy < panel.state.height!; dy++) {
|
||||
const key = `${panel.state.x! + dx},${panel.state.y! + dy}`;
|
||||
occupied.add(key);
|
||||
|
||||
if (panel.state.y! + panel.state.height! > maxY) {
|
||||
maxY = panel.state.y! + panel.state.height!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const panel of grid.state.children) {
|
||||
addPanelToOccupied(panel);
|
||||
|
||||
if (panel instanceof SceneGridRow) {
|
||||
for (const rowChild of panel.state.children) {
|
||||
addPanelToOccupied(rowChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start scanning the grid row by row, top to bottom (infinite height)
|
||||
for (let y = 0; y < Infinity; y++) {
|
||||
for (let x = 0; x <= GRID_COLUMN_COUNT - NEW_PANEL_MIN_WIDTH; x++) {
|
||||
let fits = true;
|
||||
|
||||
for (let dx = 0; dx < NEW_PANEL_MIN_WIDTH; dx++) {
|
||||
for (let dy = 0; dy < NEW_PANEL_MIN_HEIGHT; dy++) {
|
||||
const key = `${x + dx},${y + dy}`;
|
||||
if (occupied.has(key)) {
|
||||
fits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fits) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fits) {
|
||||
const cell = { x, y, width: NEW_PANEL_MIN_WIDTH, height: NEW_PANEL_MIN_HEIGHT };
|
||||
return fillEmptySpace(occupied, cell, maxY);
|
||||
}
|
||||
}
|
||||
|
||||
if (y > maxY + NEW_PANEL_MIN_HEIGHT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Should technically never reach this point
|
||||
return { x: 0, y: maxY, width: NEW_PANEL_WIDTH, height: NEW_PANEL_HEIGHT };
|
||||
}
|
||||
|
||||
/**
|
||||
* This tries to expand the found empty space so that it fills it as much as possible
|
||||
*/
|
||||
function fillEmptySpace(occupied: Set<string>, cell: GridCell, maxY: number) {
|
||||
// If we are at maxY we are on a new row, return default new row panel dimensions
|
||||
if (cell.y >= maxY) {
|
||||
cell.width = NEW_PANEL_WIDTH;
|
||||
cell.height = NEW_PANEL_HEIGHT;
|
||||
return cell;
|
||||
}
|
||||
|
||||
// Expand width
|
||||
for (let x = cell.x + cell.width + 1; x <= GRID_COLUMN_COUNT; x++) {
|
||||
let fits = true;
|
||||
|
||||
for (let y = cell.y; y < cell.y + cell.height; y++) {
|
||||
const key = `${x},${y}`;
|
||||
if (occupied.has(key)) {
|
||||
fits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fits) {
|
||||
cell.width = x - cell.x;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// try to expand y
|
||||
for (let y = cell.y + cell.height + 1; y <= maxY; y++) {
|
||||
let fits = true;
|
||||
|
||||
// Some max panel height
|
||||
if (cell.height >= 20) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (let x = cell.x; x < cell.x + cell.width; x++) {
|
||||
const key = `${x},${y}`;
|
||||
if (occupied.has(key)) {
|
||||
fits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fits) {
|
||||
cell.height = y - cell.y;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
+4
-57
@@ -2,11 +2,10 @@ import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { LazyLoader, SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getDefaultVizPanel, useDashboardState } from '../../utils/utils';
|
||||
import { addNewRowTo, addNewTabTo } from '../layouts-shared/addNew';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
|
||||
import { AutoGridLayout, AutoGridLayoutState } from './ResponsiveGridLayout';
|
||||
import { AutoGridLayoutManager } from './ResponsiveGridLayoutManager';
|
||||
@@ -36,47 +35,7 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps<AutoGridLa
|
||||
<item.Component key={item.state.key} model={item} />
|
||||
)
|
||||
)}
|
||||
{isEditing && (
|
||||
<div className={cx(styles.addAction, 'dashboard-canvas-add-button')}>
|
||||
<Button
|
||||
variant="primary"
|
||||
fill="text"
|
||||
icon="plus"
|
||||
onClick={() => layoutManager.addPanel(getDefaultVizPanel())}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.add-panel">Add panel</Trans>
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
icon="list-ul"
|
||||
label={t('dashboard.canvas-actions.group-into-row', 'Group into row')}
|
||||
onClick={() => {
|
||||
addNewRowTo(layoutManager);
|
||||
}}
|
||||
></Menu.Item>
|
||||
<Menu.Item
|
||||
icon="layers"
|
||||
label={t('dashboard.canvas-actions.group-into-tab', 'Group into tab')}
|
||||
onClick={() => {
|
||||
addNewTabTo(layoutManager);
|
||||
}}
|
||||
></Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
fill="text"
|
||||
icon="layers"
|
||||
onClick={() => layoutManager.addPanel(getDefaultVizPanel())}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.group-panels">Group panels</Trans>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
{isEditing && <CanvasGridAddActions layoutManager={layoutManager} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -125,18 +84,6 @@ const getStyles = (theme: GrafanaTheme2, state: AutoGridLayoutState) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}),
|
||||
addAction: css({
|
||||
position: 'absolute',
|
||||
padding: theme.spacing(1, 0),
|
||||
height: theme.spacing(5),
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
opacity: 0,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
}),
|
||||
dragging: css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
import { addNewRowTo, addNewTabTo } from './addNew';
|
||||
|
||||
export interface Props {
|
||||
layoutManager: DashboardLayoutManager;
|
||||
}
|
||||
|
||||
export function CanvasGridAddActions({ layoutManager }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.addAction, 'dashboard-canvas-add-button')}>
|
||||
<Button variant="primary" fill="text" icon="plus" onClick={() => layoutManager.addPanel(getDefaultVizPanel())}>
|
||||
<Trans i18nKey="dashboard.canvas-actions.add-panel">Add panel</Trans>
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
icon="list-ul"
|
||||
label={t('dashboard.canvas-actions.group-into-row', 'Group into row')}
|
||||
onClick={() => {
|
||||
addNewRowTo(layoutManager);
|
||||
}}
|
||||
></Menu.Item>
|
||||
<Menu.Item
|
||||
icon="layers"
|
||||
label={t('dashboard.canvas-actions.group-into-tab', 'Group into tab')}
|
||||
onClick={() => {
|
||||
addNewTabTo(layoutManager);
|
||||
}}
|
||||
></Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
fill="text"
|
||||
icon="layers"
|
||||
onClick={() => layoutManager.addPanel(getDefaultVizPanel())}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.group-panels">Group panels</Trans>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
addAction: css({
|
||||
position: 'absolute',
|
||||
padding: theme.spacing(1, 0),
|
||||
height: theme.spacing(5),
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
opacity: 0,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user