From 4d61022c8c5b429bc6d6bab5c77a378d01797a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Mar 2025 18:20:42 +0100 Subject: [PATCH] Dashbord: Adding elements via buttons in canvas v2 (#102806) --- .../src/themes/GlobalStyles/dashboardGrid.ts | 5 +- .../ResponsiveGridLayoutManager.tsx | 2 +- .../ResponsiveGridLayoutRenderer.tsx | 88 ++++++++++++++++-- .../scene/layout-rows/RowItem.tsx | 24 ----- .../scene/layout-rows/RowItemMenu.tsx | 93 ------------------- .../scene/layout-rows/RowItemRenderer.tsx | 32 ++++++- .../layout-rows/RowsLayoutManagerRenderer.tsx | 13 ++- .../scene/layout-tabs/TabItem.tsx | 24 +---- .../scene/layout-tabs/TabItemMenu.tsx | 90 ------------------ .../layout-tabs/TabsLayoutManagerRenderer.tsx | 21 ++++- .../scene/layouts-shared/addNew.ts | 24 ++--- public/locales/en-US/grafana.json | 29 ++---- 12 files changed, 158 insertions(+), 287 deletions(-) delete mode 100644 public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx delete mode 100644 public/app/features/dashboard-scene/scene/layout-tabs/TabItemMenu.tsx diff --git a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts index dbd5f5726eb..74d196540cc 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts @@ -91,10 +91,13 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) { }, '.dashboard-canvas-add-button': { - opacity: 0, + opacity: 0.5, + transition: theme.transitions.create('opacity'), + filter: `grayscale(100%)`, '&:hover': { opacity: 1, + filter: 'unset', }, }, diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 097aa617f58..0059d334686 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -92,7 +92,7 @@ export class AutoGridLayoutManager vizPanel.clearParent(); this.state.layout.setState({ - children: [new AutoGridItem({ body: vizPanel }), ...this.state.layout.state.children], + children: [...this.state.layout.state.children, new AutoGridItem({ body: vizPanel })], }); this.publishEvent(new NewObjectAddedToCanvasEvent(vizPanel), true); 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 e90c3d40272..738ead16493 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 @@ -1,24 +1,31 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { LazyLoader, SceneComponentProps } from '@grafana/scenes'; -import { useStyles2 } from '@grafana/ui'; +import { LazyLoader, SceneComponentProps, sceneGraph } from '@grafana/scenes'; +import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; -import { getDashboardSceneFor } from '../../utils/utils'; +import { getDefaultVizPanel, useDashboardState } from '../../utils/utils'; +import { addNewRowTo, addNewTabTo } from '../layouts-shared/addNew'; import { AutoGridLayout, AutoGridLayoutState } from './ResponsiveGridLayout'; +import { AutoGridLayoutManager } from './ResponsiveGridLayoutManager'; export function AutoGridLayoutRenderer({ model }: SceneComponentProps) { const { children, isHidden, isLazy } = model.useState(); const styles = useStyles2(getStyles, model.state); - const { layoutOrchestrator } = getDashboardSceneFor(model).state; + const { layoutOrchestrator, isEditing } = useDashboardState(model); + const layoutManager = sceneGraph.getAncestor(model, AutoGridLayoutManager); + const { fillScreen } = layoutManager.useState(); if (isHidden || !layoutOrchestrator) { return null; } return ( -
model.setRef(ref)}> +
{children.map((item) => isLazy ? ( @@ -28,6 +35,47 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps ) )} + {isEditing && ( +
+ + + { + addNewRowTo(layoutManager); + }} + > + { + addNewTabTo(layoutManager); + }} + > + + } + > + + +
+ )}
); } @@ -44,8 +92,6 @@ const getStyles = (theme: GrafanaTheme2, state: AutoGridLayoutState) => ({ justifyItems: state.justifyItems || 'unset', alignItems: state.alignItems || 'unset', justifyContent: state.justifyContent || 'unset', - flexGrow: 1, - [theme.breakpoints.down('md')]: state.md ? { gridTemplateRows: state.md.templateRows, @@ -57,6 +103,20 @@ const getStyles = (theme: GrafanaTheme2, state: AutoGridLayoutState) => ({ justifyContent: state.md.justifyContent, } : undefined, + // Show add action when hovering over the grid + '&:hover': { + '.dashboard-canvas-add-button': { + opacity: 1, + filter: 'unset', + }, + }, + }), + containerFillScreen: css({ + flexGrow: 1, + }), + containerEditing: css({ + paddingBottom: theme.spacing(5), + position: 'relative', }), wrapper: css({ display: 'grid', @@ -64,6 +124,18 @@ 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/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index d9bd641127c..de70e459535 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -120,30 +120,6 @@ export class RowItem this.getLayout().addPanel(panel); } - public onAddRowAbove() { - this._getParentLayout().addRowAbove(this); - } - - public onAddRowBelow() { - this._getParentLayout().addRowBelow(this); - } - - public onMoveUp() { - this._getParentLayout().moveRowUp(this); - } - - public onMoveDown() { - this._getParentLayout().moveRowDown(this); - } - - public isFirstRow(): boolean { - return this._getParentLayout().isFirstRow(this); - } - - public isLastRow(): boolean { - return this._getParentLayout().isLastRow(this); - } - public setIsDropTarget(isDropTarget: boolean) { if (!!this.state.isDropTarget !== isDropTarget) { this.setState({ isDropTarget }); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx deleted file mode 100644 index 0782595ba45..00000000000 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { css, cx } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Dropdown, Menu, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; - -import { RowItem } from './RowItem'; - -interface RowItemMenuProps { - model: RowItem; -} - -export function RowItemMenu({ model }: RowItemMenuProps) { - const styles = useStyles2(getStyles); - - return ( - - ( - - model.onAddPanel()} - /> - - model.onAddRowAbove()} - /> - model.onAddRowBelow()} - /> - - )} - > - - - ( - - model.onMoveUp()} - disabled={model.isFirstRow()} - /> - - model.onMoveDown()} - disabled={model.isLastRow()} - /> - - )} - > - - {!isClone && isEditing && }
)} {!isCollapsed && } @@ -108,7 +115,7 @@ function getStyles(theme: GrafanaTheme2) { width: '100%', display: 'flex', gap: theme.spacing(1), - padding: theme.spacing(0.5), + padding: theme.spacing(0.5, 0.5, 0.5, 0), alignItems: 'center', marginBottom: theme.spacing(1), }), @@ -137,11 +144,28 @@ function getStyles(theme: GrafanaTheme2) { rowTitleHidden: css({ textDecoration: 'line-through', }), + rowTitleNested: css({ + fontSize: theme.typography.body.fontSize, + fontWeight: theme.typography.fontWeightRegular, + }), wrapper: css({ display: 'flex', flexDirection: 'column', width: '100%', minHeight: '100px', + '> div:nth-child(2)': { + marginLeft: theme.spacing(3), + position: 'relative', + '&:before': { + content: '""', + position: 'absolute', + top: `-8px`, + bottom: 0, + left: '-16px', + width: '1px', + backgroundColor: theme.colors.border.weak, + }, + }, }), wrapperEditing: css({ padding: theme.spacing(0.5), diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx index 390d7a824a5..488a3ad8dad 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx @@ -2,12 +2,16 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps } from '@grafana/scenes'; -import { useStyles2 } from '@grafana/ui'; +import { Button, useStyles2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +import { useDashboardState } from '../../utils/utils'; import { RowsLayoutManager } from './RowsLayoutManager'; export function RowLayoutManagerRenderer({ model }: SceneComponentProps) { const { rows } = model.useState(); + const { isEditing } = useDashboardState(model); const styles = useStyles2(getStyles); return ( @@ -15,6 +19,13 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps ( ))} + {isEditing && ( +
+ +
+ )} ); } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx index 1e9eda12fdc..0403ecd7a3a 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -106,28 +106,8 @@ export class TabItem this.getLayout().addPanel(panel); } - public onAddTabBefore() { - this._getParentLayout().addTabBefore(this); - } - - public onAddTabAfter() { - this._getParentLayout().addTabAfter(this); - } - - public onMoveLeft() { - this._getParentLayout().moveTabLeft(this); - } - - public onMoveRight() { - this._getParentLayout().moveTabRight(this); - } - - public isFirstTab(): boolean { - return this._getParentLayout().isFirstTab(this); - } - - public isLastTab(): boolean { - return this._getParentLayout().isLastTab(this); + public onAddTab() { + this._getParentLayout().addNewTab(); } public onChangeTitle(title: string) { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemMenu.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemMenu.tsx deleted file mode 100644 index aee83633b9a..00000000000 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemMenu.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Dropdown, Menu, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; - -import { TabItem } from './TabItem'; - -interface Props { - model: TabItem; -} - -export function TabItemMenu({ model }: Props) { - const styles = useStyles2(getStyles); - - return ( - - ( - - model.onAddPanel()} - /> - - model.onAddTabBefore()} - /> - model.onAddTabAfter()} - /> - - )} - > - - - ( - - model.onMoveLeft()} - /> - - model.onMoveRight()} - /> - - )} - > - + + )} @@ -47,11 +53,17 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), tabsBar: css({ overflow: 'hidden', + '&:hover': { + '.dashboard-canvas-add-button': { + filter: 'unset', + opacity: 1, + }, + }, }), tabsRow: css({ - justifyContent: 'space-between', display: 'flex', width: '100%', + alignItems: 'center', }), tabsContainer: css({ display: 'flex', @@ -65,6 +77,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ tabContentContainer: css({ backgroundColor: 'transparent', display: 'flex', + flexDirection: 'column', flex: 1, minHeight: 0, paddingTop: theme.spacing(1), diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts b/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts index d7c139d1716..5ada92b7fa6 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts @@ -1,5 +1,5 @@ import { config } from '@grafana/runtime'; -import { sceneGraph, SceneGridRow } from '@grafana/scenes'; +import { SceneGridRow } from '@grafana/scenes'; import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; @@ -16,12 +16,6 @@ export function addNewTabTo(layout: DashboardLayoutManager): TabItem { throw new Error('Parent layout is not a LayoutParent'); } - // If layout parent is tab item we add new tab after it rather than create a nested tab - if (layoutParent instanceof TabItem) { - const tabsLayout = sceneGraph.getAncestor(layoutParent, TabsLayoutManager); - return tabsLayout.addTabAfter(layoutParent); - } - if (layout instanceof TabsLayoutManager) { return layout.addNewTab(); } @@ -50,17 +44,6 @@ export function addNewRowTo(layout: DashboardLayoutManager): RowItem | SceneGrid } } - const layoutParent = layout.parent!; - if (!isLayoutParent(layoutParent)) { - throw new Error('Parent layout is not a LayoutParent'); - } - - // If adding we are adding a row to a row we add it below the current row - if (layoutParent instanceof RowItem) { - const rowsLayout = sceneGraph.getAncestor(layoutParent, RowsLayoutManager); - return rowsLayout.addRowBelow(layoutParent); - } - if (layout instanceof RowsLayoutManager) { return layout.addNewRow(); } @@ -70,6 +53,11 @@ export function addNewRowTo(layout: DashboardLayoutManager): RowItem | SceneGrid return addNewRowTo(currentTab.state.layout); } + const layoutParent = layout.parent!; + if (!isLayoutParent(layoutParent)) { + throw new Error('Parent layout is not a LayoutParent'); + } + // If we want to add a row and current layout is custom grid or auto we migrate to rows layout // And wrap current layout in a row diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 858e1b19f60..297ee9278db 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1372,6 +1372,14 @@ "min-width-error": "A number between 50 and 2000 is required" } }, + "canvas-actions": { + "add-panel": "Add panel", + "group-into-row": "Group into row", + "group-into-tab": "Group into tab", + "group-panels": "Group panels", + "new-row": "New row", + "new-tab": "New tab" + }, "conditional-rendering": { "data": { "disable": "Disable", @@ -1612,15 +1620,6 @@ "row": { "collapse": "Collapse row", "expand": "Expand row", - "menu": { - "add": "Add row", - "add-panel": "Panel", - "add-row-above": "Row above", - "add-row-below": "Row below", - "move-down": "Move row down", - "move-row": "Move row", - "move-up": "Move row up" - }, "new": "New row", "repeat": { "learn-more": "Learn more", @@ -1645,20 +1644,8 @@ }, "tabs-layout": { "description": "Organize panels into horizontal tabs", - "menu": { - "move-tab": "Move tab" - }, "name": "Tabs", "tab": { - "menu": { - "add": "Add tab", - "add-panel": "Panel", - "add-tab-above": "Tab before", - "add-tab-after": "Tab after", - "add-tab-before": "Tab before", - "move-left": "Move tab left", - "move-right": "Move tab right" - }, "new": "New tab" }, "tab-options": {