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 00000000000..c64574cd392 Binary files /dev/null and b/public/img/dashboards/add-panel.png differ 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"