From 48fe1ec6346ea6fc23f1eff741083d45d6102fcf Mon Sep 17 00:00:00 2001 From: idastambuk Date: Mon, 5 Jan 2026 10:45:47 +0100 Subject: [PATCH] Add clickable add button --- .../src/selectors/pages.ts | 3 + .../src/components/Sidebar/SidebarButton.tsx | 22 ++++--- .../edit-pane/DashboardEditPane.tsx | 2 +- .../edit-pane/DashboardEditPaneRenderer.tsx | 49 +++++++++++++-- .../edit-pane/DashboardSidePaneNew.tsx | 56 ++++++++++++++++++ public/img/dashboards/add-panel.png | Bin 0 -> 4334 bytes public/locales/en-US/grafana.json | 11 ++++ 7 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/DashboardSidePaneNew.tsx create mode 100644 public/img/dashboards/add-panel.png diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index b88dac9099c..154e27b4251 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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: { diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx index 9f5e0f20d71..e059a5f5acc 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx @@ -16,10 +16,11 @@ export interface Props extends ButtonHTMLAttributes { active?: boolean; tooltip?: string; title: string; + isAddButton?: boolean; } export const SidebarButton = React.forwardRef( - ({ 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( 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( onClick={onClick} {...restProps} > -
{renderIcon(icon, context.compact)}
+
{renderIcon(icon, isAddButton)}
{!context.compact &&
{title}
} @@ -54,13 +56,13 @@ export const SidebarButton = React.forwardRef( 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 ; + return ; } 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, diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 64f0c774ea8..f49dd81ed4b 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -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 { public constructor() { diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx index 950785c2ffd..34909cb49f9 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx @@ -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 | 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 /> )} + {openPane === 'add' && ( + + + + )} {openPane === 'outline' && ( @@ -68,6 +97,18 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro )} + { + 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'} + /> editPane.selectObject(dashboard, dashboard.state.key!)} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardSidePaneNew.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardSidePaneNew.tsx new file mode 100644 index 00000000000..7e78ae6f584 --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardSidePaneNew.tsx @@ -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 ( +
+ +
+ {t('dashboard.add.new-panel.title', 'Panel')} + + {t('dashboard.add.new-panel.description', 'Drag or click to add a panel')} + + +
+ +
+
+
+ ); +} + +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', + }), + }; +} diff --git a/public/img/dashboards/add-panel.png b/public/img/dashboards/add-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..c64574cd392446c3c7e8c24423e5847ccdbeb757 GIT binary patch literal 4334 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 literal 0 HcmV?d00001 diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 10967a7331c..fad346d0066 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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"