Dashbord: Adding elements via buttons in canvas v2 (#102806)

pull/103015/head
Torkel Ödegaard 4 months ago committed by GitHub
parent e921c133c5
commit 4d61022c8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts
  2. 2
      public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
  3. 88
      public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutRenderer.tsx
  4. 24
      public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
  5. 93
      public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx
  6. 32
      public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx
  7. 13
      public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx
  8. 24
      public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx
  9. 90
      public/app/features/dashboard-scene/scene/layout-tabs/TabItemMenu.tsx
  10. 21
      public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx
  11. 24
      public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts
  12. 29
      public/locales/en-US/grafana.json

@ -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',
},
},

@ -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);

@ -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<AutoGridLayout>) {
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 (
<div className={styles.container} ref={(ref) => model.setRef(ref)}>
<div
className={cx(styles.container, fillScreen && styles.containerFillScreen, isEditing && styles.containerEditing)}
>
{children.map((item) =>
isLazy ? (
<LazyLoader key={item.state.key!} className={styles.container}>
@ -28,6 +35,47 @@ 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>
)}
</div>
);
}
@ -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,

@ -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 });

@ -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 (
<ToolbarButtonRow className={cx(styles.container, 'dashboard-canvas-add-button')}>
<Dropdown
placement="bottom-end"
overlay={() => (
<Menu>
<Menu.Item
ariaLabel={t('dashboard.rows-layout.row.menu.add-panel', 'Panel')}
label={t('dashboard.rows-layout.row.menu.add-panel', 'Panel')}
onClick={() => model.onAddPanel()}
/>
<Menu.Divider />
<Menu.Item
ariaLabel={t('dashboard.rows-layout.row.menu.add-row-above', 'Row above')}
label={t('dashboard.rows-layout.row.menu.add-row-above', 'Row above')}
onClick={() => model.onAddRowAbove()}
/>
<Menu.Item
ariaLabel={t('dashboard.rows-layout.row.menu.add-row-below', 'Row below')}
label={t('dashboard.rows-layout.row.menu.add-row-below', 'Row below')}
onClick={() => model.onAddRowBelow()}
/>
</Menu>
)}
>
<Button
aria-label={t('dashboard.rows-layout.row.menu.add', 'Add row')}
title={t('dashboard.rows-layout.row.menu.add', 'Add row')}
tooltip={t('dashboard.rows-layout.row.menu.add', 'Add row')}
icon="plus"
size="sm"
variant="secondary"
>
<Trans i18nKey="grafana-ui.tags-input.add">Add</Trans>
</Button>
</Dropdown>
<Dropdown
placement="bottom-end"
overlay={() => (
<Menu>
<Menu.Item
aria-label={t('dashboard.rows-layout.row.menu.move-up', 'Move row up')}
label={t('dashboard.rows-layout.row.menu.move-up', 'Move row up')}
onClick={() => model.onMoveUp()}
disabled={model.isFirstRow()}
/>
<Menu.Divider />
<Menu.Item
aria-label={t('dashboard.rows-layout.row.menu.move-down', 'Move row down')}
label={t('dashboard.rows-layout.row.menu.move-down', 'Move row down')}
onClick={() => model.onMoveDown()}
disabled={model.isLastRow()}
/>
</Menu>
)}
>
<Button
aria-label={t('dashboard.rows-layout.row.menu.move-row', 'Move row')}
title={t('dashboard.rows-layout.row.menu.move-row', 'Move row')}
tooltip={t('dashboard.rows-layout.row.menu.move-row', 'Move row')}
icon="arrows-v"
size="sm"
variant="secondary"
/>
</Dropdown>
</ToolbarButtonRow>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
display: 'flex',
alignItems: 'center',
flex: 1,
justifyContent: 'flex-end',
gap: theme.spacing(1),
}),
});

@ -14,9 +14,9 @@ import {
useInterpolatedTitle,
useIsConditionallyHidden,
} from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { RowItem } from './RowItem';
import { RowItemMenu } from './RowItemMenu';
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget } = model.useState();
@ -27,6 +27,7 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
const title = useInterpolatedTitle(model);
const styles = useStyles2(getStyles);
const clearStyles = useStyles2(clearButtonStyles);
const isTopLevel = model.parent?.parent instanceof DashboardScene;
const shouldGrow = !isCollapsed && fillScreen;
const isHidden = isConditionallyHidden && !isEditing;
@ -83,7 +84,14 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
data-testid={selectors.components.DashboardRow.title(title!)}
>
<Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />
<span className={cx(styles.rowTitle, isHeaderHidden && styles.rowTitleHidden)} role="heading">
<span
className={cx(
styles.rowTitle,
isHeaderHidden && styles.rowTitleHidden,
!isTopLevel && styles.rowTitleNested
)}
role="heading"
>
{title}
{isHeaderHidden && (
<Tooltip
@ -94,7 +102,6 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
)}
</span>
</button>
{!isClone && isEditing && <RowItemMenu model={model} />}
</div>
)}
{!isCollapsed && <layout.Component model={layout} />}
@ -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),

@ -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<RowsLayoutManager>) {
const { rows } = model.useState();
const { isEditing } = useDashboardState(model);
const styles = useStyles2(getStyles);
return (
@ -15,6 +19,13 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps<RowsLayo
{rows.map((row) => (
<row.Component model={row} key={row.state.key!} />
))}
{isEditing && (
<div className="dashboard-canvas-add-button">
<Button icon="plus" variant="primary" fill="text" onClick={() => model.addNewRow()}>
<Trans i18nKey="dashboard.canvas-actions.new-row">New row</Trans>
</Button>
</div>
)}
</div>
);
}

@ -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) {

@ -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 (
<ToolbarButtonRow className={styles.container}>
<Dropdown
placement="bottom-end"
overlay={() => (
<Menu>
<Menu.Item
ariaLabel={t('dashboard.tabs-layout.tab.menu.add-panel', 'Panel')}
label={t('dashboard.tabs-layout.tab.menu.add-panel', 'Panel')}
onClick={() => model.onAddPanel()}
/>
<Menu.Divider />
<Menu.Item
ariaLabel={t('dashboard.tabs-layout.tab.menu.add-tab-before', 'Tab before')}
label={t('dashboard.tabs-layout.tab.menu.add-tab-above', 'Tab before')}
onClick={() => model.onAddTabBefore()}
/>
<Menu.Item
ariaLabel={t('dashboard.tabs-layout.tab.menu.add-tab-after', 'Tab after')}
label={t('dashboard.tabs-layout.tab.menu.add-tab-after', 'Tab after')}
onClick={() => model.onAddTabAfter()}
/>
</Menu>
)}
>
<Button
aria-label={t('dashboard.tabs-layout.tab.menu.add', 'Add tab')}
title={t('dashboard.tabs-layout.tab.menu.add', 'Add tab')}
tooltip={t('dashboard.tabs-layout.tab.menu.add', 'Add tab')}
icon="plus"
variant="secondary"
size="sm"
>
<Trans i18nKey="grafana-ui.tags-input.add">Add</Trans>
</Button>
</Dropdown>
<Dropdown
placement="bottom-end"
overlay={() => (
<Menu>
<Menu.Item
aria-label={t('dashboard.tabs-layout.tab.menu.move-left', 'Move tab left')}
label={t('dashboard.tabs-layout.tab.menu.move-left', 'Move tab left')}
onClick={() => model.onMoveLeft()}
/>
<Menu.Divider />
<Menu.Item
aria-label={t('dashboard.tabs-layout.tab.menu.move-right', 'Move tab right')}
label={t('dashboard.tabs-layout.tab.menu.move-right', 'Move tab right')}
onClick={() => model.onMoveRight()}
/>
</Menu>
)}
>
<Button
aria-label={t('dashboard.tabs-layout.menu.move-tab', 'Move tab')}
title={t('dashboard.tabs-layout.menu.move-tab', 'Move tab')}
tooltip={t('dashboard.tabs-layout.menu.move-tab', 'Move tab')}
icon="arrows-h"
variant="secondary"
size="sm"
/>
</Dropdown>
</ToolbarButtonRow>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
gap: theme.spacing(1),
flexShrink: 0,
}),
};
}

@ -3,11 +3,11 @@ import { Fragment } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { TabContent, TabsBar, useStyles2 } from '@grafana/ui';
import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { getDashboardSceneFor } from '../../utils/utils';
import { TabItemMenu } from './TabItemMenu';
import { TabsLayoutManager } from './TabsLayoutManager';
export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLayoutManager>) {
@ -29,7 +29,13 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
</Fragment>
))}
</div>
{isEditing && <TabItemMenu model={currentTab} />}
{isEditing && (
<div className="dashboard-canvas-add-button">
<Button icon="plus" variant="primary" fill="text" onClick={() => model.addNewTab()}>
<Trans i18nKey="dashboard.canvas-actions.new-tab">New tab</Trans>
</Button>
</div>
)}
</div>
</TabsBar>
<TabContent className={styles.tabContentContainer}>
@ -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),

@ -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

@ -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": {

Loading…
Cancel
Save