mirror of https://github.com/grafana/grafana
Dashboard: Canvas add buttons to custom grid (#103181)
* Custom grid add actions * find empty space * Update * Update * Update * Updatepull/103270/head
parent
0aeefedb0c
commit
aea7f87732
@ -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; |
||||
} |
@ -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'), |
||||
}, |
||||
}), |
||||
}); |
Loading…
Reference in new issue