Add clickable add button

This commit is contained in:
idastambuk
2026-01-05 10:45:47 +01:00
parent 318a0ebb36
commit 48fe1ec634
7 changed files with 131 additions and 12 deletions
@@ -190,6 +190,9 @@ export const versionedPages = {
outlineButton: {
'12.4.0': 'data-testid Dashboard Sidebar outline button',
},
addButton: {
'12.4.0': 'data-testid Dashboard Sidebar new button',
}
},
DashNav: {
nav: {
@@ -16,10 +16,11 @@ export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
active?: boolean;
tooltip?: string;
title: string;
isAddButton?: boolean;
}
export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
({ icon, active, onClick, title, tooltip, ...restProps }, ref) => {
({ icon, active, onClick, title, tooltip, isAddButton, ...restProps }, ref) => {
const styles = useStyles2(getStyles);
const context = useContext(SidebarContext);
@@ -31,7 +32,8 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
styles.button,
context.compact && styles.compact,
active && styles.active,
context.position === 'left' && styles.leftButton
context.position === 'left' && styles.leftButton,
isAddButton && 'primary'
);
return (
@@ -44,7 +46,7 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
onClick={onClick}
{...restProps}
>
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
<div>{renderIcon(icon, isAddButton)}</div>
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
</button>
</Tooltip>
@@ -54,13 +56,13 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
SidebarButton.displayName = 'SidebarButton';
function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) {
function renderIcon(icon: IconName | React.ReactNode, isAddButton?: boolean) {
if (!icon) {
return null;
}
if (isIconName(icon)) {
return <Icon name={icon} size={compact ? `lg` : `lg`} />;
return <Icon name={icon} size={isAddButton ? 'xl' : 'lg'} />;
}
return icon;
@@ -83,7 +85,14 @@ const getStyles = (theme: GrafanaTheme2) => {
color: theme.colors.text.secondary,
background: 'transparent',
border: `none`,
'&.primary': css({
svg: {
backgroundColor: theme.colors.primary.main,
color: theme.colors.getContrastText(theme.colors.primary.main),
borderRadius: theme.shape.radius.sm,
padding: 2,
},
}),
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: theme.transitions.create(['background-color', 'border-color', 'color'], {
duration: theme.transitions.duration.short,
@@ -145,7 +154,6 @@ const getStyles = (theme: GrafanaTheme2) => {
width: '100%',
whiteSpace: 'nowrap',
}),
iconWrapper: css({}),
title: css({
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
@@ -30,7 +30,7 @@ export interface DashboardEditPaneState extends SceneObjectState {
isDocked?: boolean;
}
export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters';
export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters' | 'add';
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
public constructor() {
@@ -1,38 +1,42 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { useSceneObjectState } from '@grafana/scenes';
import { SceneObject, SceneObjectState, useSceneObjectState, VizPanel } from '@grafana/scenes';
import { Sidebar } from '@grafana/ui';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardScene } from '../scene/DashboardScene';
import { onOpenSnapshotOriginalDashboard } from '../scene/GoToSnapshotOriginButton';
import { ManagedDashboardNavBarBadge } from '../scene/ManagedDashboardNavBarBadge';
import { RowItem } from '../scene/layout-rows/RowItem';
import { TabItem } from '../scene/layout-tabs/TabItem';
import { ToolbarActionProps } from '../scene/new-toolbar/types';
import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
import { getDefaultVizPanel } from '../utils/utils';
import { DashboardEditPane } from './DashboardEditPane';
import { ShareExportDashboardButton } from './DashboardExportButton';
import { DashboardOutline } from './DashboardOutline';
import { DashboardSidePaneNew } from './DashboardSidePaneNew';
import { ElementEditPane } from './ElementEditPane';
export interface Props {
editPane: DashboardEditPane;
dashboard: DashboardScene;
isDocked?: boolean;
}
/**
* Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props
*/
export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Props) {
export function DashboardEditPaneRenderer({ editPane, dashboard }: Props) {
const { selection, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const { isEditing, meta, uid } = dashboard.useState();
const hasUid = Boolean(uid);
const selectedObject = selection?.getFirstObject();
const isNewElement = selection?.isNewElement() ?? false;
const [selectedLayoutElement, setSelectedLayoutElement] = useState(selectedObject);
const editableElement = useMemo(() => {
if (selection) {
@@ -42,6 +46,26 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
return undefined;
}, [selection]);
// saves the row or tab selected when opening the 'add' pane, so new panels can later be added to it
const onSetLayoutElement = (obj: SceneObject<SceneObjectState> | undefined) => {
if (obj instanceof RowItem || obj instanceof TabItem) {
setSelectedLayoutElement(obj);
} else if (!(obj instanceof VizPanel)) {
setSelectedLayoutElement(dashboard);
}
};
const onAddNewPanel = () => {
if (selectedLayoutElement) {
const panel = getDefaultVizPanel();
if (selectedLayoutElement instanceof DashboardScene) {
dashboard.addPanel(panel);
} else if (selectedLayoutElement instanceof RowItem || selectedLayoutElement instanceof TabItem) {
selectedLayoutElement.getLayout().addPanel(panel);
}
}
};
return (
<>
{editableElement && (
@@ -54,6 +78,11 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
/>
</Sidebar.OpenPane>
)}
{openPane === 'add' && (
<Sidebar.OpenPane>
<DashboardSidePaneNew onAddPanel={onAddNewPanel} />
</Sidebar.OpenPane>
)}
{openPane === 'outline' && (
<Sidebar.OpenPane>
<DashboardOutline editPane={editPane} isEditing={isEditing} />
@@ -68,6 +97,18 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
<RedoButton dashboard={dashboard} />
</>
)}
<Sidebar.Button
icon="plus"
isAddButton
onClick={() => {
onSetLayoutElement(selectedObject);
editPane.openPane('add');
}}
title={t('dashboard.sidebar.add.title', 'Add')}
tooltip={t('dashboard.sidebar.add.tooltip', 'Add new element')}
data-testid={selectors.pages.Dashboard.Sidebar.addButton}
active={selectedObject === null || openPane === 'add'}
/>
<Sidebar.Button
icon="cog"
onClick={() => editPane.selectObject(dashboard, dashboard.state.key!)}
@@ -0,0 +1,56 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { Sidebar, Text, useStyles2 } from '@grafana/ui';
import addPanelImg from 'img/dashboards/add-panel.png';
export function DashboardSidePaneNew({ onAddPanel }: {onAddPanel: () => void}) {
const styles = useStyles2(getStyles);
return (
<div className={styles.sidePanel}>
<Sidebar.PaneHeader title={t('dashboard.add.pane-header', 'Add')} />
<div className={styles.sidePanelContainer}>
<Text weight="medium">{t('dashboard.add.new-panel.title', 'Panel')}</Text>
<Text variant="bodySmall">
{t('dashboard.add.new-panel.description', 'Drag or click to add a panel')}
</Text>
<div className={styles.imageContainer}>
<button className={styles.noStyles} onClick={onAddPanel} aria-label={t('dashboard.add.new-panel.title', 'Panel')}>
<img alt="Add panel click area" src={addPanelImg} />
</button>
</div>
</div>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
sidePanel: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
sidePanelContainer: css({
padding: theme.spacing(2),
span: {
display: 'block',
},
}),
imageContainer: css({
opacity: 0.8,
padding: theme.spacing(2, 0),
borderRadius: theme.shape.radius.sm,
cursor: 'pointer',
width: '100%',
'&:hover': {
opacity: 1,
}
}),
noStyles: css({
all: 'unset',
}),
};
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+11
View File
@@ -5216,6 +5216,13 @@
"title-matched_one": "Matched {{count}}/{{totalCount}} options",
"title-matched_other": "Matched {{count}}/{{totalCount}} options"
},
"add": {
"pane-header": "Add",
"new-panel": {
"title": "Panel",
"description": "Drag or click to add a panel"
}
},
"outline": {
"pane-header": "Content outline",
"repeated-item": "Repeat",
@@ -5433,6 +5440,10 @@
"title": "Options",
"tooltip": "Dashboard options"
},
"add": {
"title": "Add",
"tooltip": "Add a new element"
},
"edit-schema": {
"title": "Code",
"tooltip": "Edit as code"