diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx index 01a5263d833..9605d479df9 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx @@ -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')} /> )} {elementInfo.typeName} diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index a31bf88ec23..966c5d15b63 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -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), - }); - - this.state.grid.setState({ - children: [newGridItem, ...this.state.grid.state.children], - }); + // 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: [...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) { 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; + return ( +
+ {model.state.grid.Component && } + {isEditing && ( +
+ +
+ )} +
+ ); +} + +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), + }), + }; } diff --git a/public/app/features/dashboard-scene/scene/layout-default/findSpaceForNewPanel.ts b/public/app/features/dashboard-scene/scene/layout-default/findSpaceForNewPanel.ts new file mode 100644 index 00000000000..41da534908b --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/findSpaceForNewPanel.ts @@ -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(); + 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, 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; +} diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutRenderer.tsx index 0721f1fe704..03323b93a22 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutRenderer.tsx @@ -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 ) )} - {isEditing && ( -
- - - { - addNewRowTo(layoutManager); - }} - > - { - addNewTabTo(layoutManager); - }} - > - - } - > - - -
- )} + {isEditing && } ); } @@ -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, diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx new file mode 100644 index 00000000000..c6abc6a30fd --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layouts-shared/CanvasGridAddActions.tsx @@ -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 ( +
+ + + { + addNewRowTo(layoutManager); + }} + > + { + addNewTabTo(layoutManager); + }} + > + + } + > + + +
+ ); +} + +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'), + }, + }), +});