diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index a411c06a6a1..2aad1b286d2 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -45,10 +45,6 @@ export const Icon = React.forwardRef( return ; } - if (name === 'panel-add') { - size = 'xl'; - } - if (!cacheInitialized) { initIconCache(); } diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx new file mode 100644 index 00000000000..74e035426a1 --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx @@ -0,0 +1,61 @@ +import { css, cx } from '@emotion/css'; +import React, { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Dropdown, Button, useTheme2, Icon } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; +import { DashboardModel } from 'app/features/dashboard/state'; + +import { AddPanelMenu } from './AddPanelMenu'; + +interface Props { + dashboard: DashboardModel; +} + +export const AddPanelButton = ({ dashboard }: Props) => { + const styles = getStyles(useTheme2()); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + return ( + } + placement="bottom" + offset={[0, 6]} + onVisibleChange={setIsMenuOpen} + > + + + ); +}; + +function getStyles(theme: GrafanaTheme2) { + return { + button: css({ + label: 'add-panel-button', + padding: theme.spacing(0.5, 0.5, 0.5, 0.75), + height: theme.spacing((theme.components.height.sm + theme.components.height.md) / 2), + borderRadius: theme.shape.radius.default, + }), + buttonIcon: css({ + svg: { + margin: 0, + }, + }), + buttonText: css({ + label: 'add-panel-button-text', + fontSize: theme.typography.body.fontSize, + span: { + marginLeft: theme.spacing(0.67), + }, + }), + }; +} diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx new file mode 100644 index 00000000000..bdf8e9a5cbc --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx @@ -0,0 +1,63 @@ +import React, { useMemo } from 'react'; + +import { locationService, reportInteraction } from '@grafana/runtime'; +import { Menu } from '@grafana/ui'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { + getCopiedPanelPlugin, + onAddLibraryPanel, + onCreateNewPanel, + onCreateNewRow, + onPasteCopiedPanel, +} from 'app/features/dashboard/utils/dashboard'; + +interface Props { + dashboard: DashboardModel; +} + +export const AddPanelMenu = ({ dashboard }: Props) => { + const copiedPanelPlugin = useMemo(() => getCopiedPanelPlugin(), []); + + return ( + + { + reportInteraction('Create new panel'); + const id = onCreateNewPanel(dashboard); + locationService.partial({ editPanel: id }); + }} + /> + { + reportInteraction('Create new row'); + onCreateNewRow(dashboard); + }} + /> + { + reportInteraction('Add a panel from the panel library'); + onAddLibraryPanel(dashboard); + }} + /> + { + reportInteraction('Paste panel from clipboard'); + onPasteCopiedPanel(dashboard, copiedPanelPlugin); + }} + disabled={!copiedPanelPlugin} + /> + + ); +}; diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 742f2a78504..becd09aa7d7 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -217,7 +217,7 @@ const AddPanelWidgetHandle = ({ children, onBack, onCancel, styles }: AddPanelWi )} {!onBack && (
- +
)} {children && {children}} diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 0eec0b2180c..d4bc3c21672 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -25,17 +25,17 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { appEvents } from 'app/core/core'; import { useBusEvent } from 'app/core/hooks/useBusEvent'; import { t, Trans } from 'app/core/internationalization'; +import { setStarred } from 'app/core/reducers/navBarTree'; +import { AddPanelButton } from 'app/features/dashboard/components/AddPanelButton/AddPanelButton'; import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer'; import { ShareModal } from 'app/features/dashboard/components/ShareModal'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { DashboardModel } from 'app/features/dashboard/state'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; import { KioskMode } from 'app/types'; import { DashboardMetaChangedEvent, ShowModalReactEvent } from 'app/types/events'; -import { setStarred } from '../../../../core/reducers/navBarTree'; -import { getDashboardSrv } from '../../services/DashboardSrv'; -import { DashboardModel } from '../../state'; - import { DashNavButton } from './DashNavButton'; import { DashNavTimeControls } from './DashNavTimeControls'; @@ -305,14 +305,19 @@ export const DashNav = React.memo((props) => { } if (canEdit && !isFullscreen) { - buttons.push( - - ); + if (config.featureToggles.emptyDashboardPage) { + buttons.push(); + } else { + buttons.push( + + ); + } } if (canSave && !isFullscreen) { diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index e72c0b3d6e8..e9a7f988c9b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; -import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { calculateNewPanelGridPos } from 'app/features/dashboard/utils/panel'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { onAddLibraryPanel, onCreateNewPanel, onCreateNewRow } from 'app/features/dashboard/utils/dashboard'; export interface Props { dashboard: DashboardModel; @@ -13,36 +13,6 @@ export interface Props { } export const DashboardEmpty = ({ dashboard, canCreate }: Props) => { - const onCreateNewPanel = () => { - const newPanel: Partial = { - type: 'timeseries', - title: 'Panel Title', - gridPos: calculateNewPanelGridPos(dashboard), - }; - - dashboard.addPanel(newPanel); - locationService.partial({ editPanel: newPanel.id }); - }; - - const onCreateNewRow = () => { - const newRow = { - type: 'row', - title: 'Row title', - gridPos: { x: 0, y: 0 }, - }; - - dashboard.addPanel(newRow); - }; - - const onAddLibraryPanel = () => { - const newPanel = { - type: 'add-library-panel', - gridPos: calculateNewPanelGridPos(dashboard), - }; - - dashboard.addPanel(newPanel); - }; - const styles = useStyles2(getStyles); return ( @@ -62,7 +32,8 @@ export const DashboardEmpty = ({ dashboard, canCreate }: Props) => { aria-label="Add new panel" onClick={() => { reportInteraction('Create new panel'); - onCreateNewPanel(); + const id = onCreateNewPanel(dashboard); + locationService.partial({ editPanel: id }); }} disabled={!canCreate} > @@ -81,7 +52,7 @@ export const DashboardEmpty = ({ dashboard, canCreate }: Props) => { aria-label="Add new row" onClick={() => { reportInteraction('Create new row'); - onCreateNewRow(); + onCreateNewRow(dashboard); }} disabled={!canCreate} > @@ -99,7 +70,7 @@ export const DashboardEmpty = ({ dashboard, canCreate }: Props) => { aria-label="Add new panel from panel library" onClick={() => { reportInteraction('Add a panel from the panel library'); - onAddLibraryPanel(); + onAddLibraryPanel(dashboard); }} disabled={!canCreate} > @@ -112,7 +83,7 @@ export const DashboardEmpty = ({ dashboard, canCreate }: Props) => { ); }; -const getStyles = (theme: GrafanaTheme2) => { +function getStyles(theme: GrafanaTheme2) { return { wrapper: css({ label: 'dashboard-empty-wrapper', @@ -184,4 +155,4 @@ const getStyles = (theme: GrafanaTheme2) => { marginBottom: theme.spacing.gridSize * 3, }), }; -}; +} diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 705dd42141c..e3baea1250d 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -267,6 +267,16 @@ export function getNewDashboardModelData( urlFolderUid?: string, panelType?: string ): { dashboard: any; meta: DashboardMeta } { + const panels = config.featureToggles.emptyDashboardPage + ? [] + : [ + { + type: panelType ?? 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 9 }, + title: 'Panel Title', + }, + ]; + const data = { meta: { canStar: false, @@ -277,13 +287,7 @@ export function getNewDashboardModelData( }, dashboard: { title: 'New dashboard', - panels: [ - { - type: panelType ?? 'add-panel', - gridPos: { x: 0, y: 0, w: 12, h: 9 }, - title: 'Panel Title', - }, - ], + panels, }, }; diff --git a/public/app/features/dashboard/utils/dashboard.ts b/public/app/features/dashboard/utils/dashboard.ts new file mode 100644 index 00000000000..13fe36eecaa --- /dev/null +++ b/public/app/features/dashboard/utils/dashboard.ts @@ -0,0 +1,91 @@ +import { chain, cloneDeep, defaults, find } from 'lodash'; + +import { PanelPluginMeta } from '@grafana/data'; +import config from 'app/core/config'; +import { LS_PANEL_COPY_KEY } from 'app/core/constants'; +import store from 'app/core/store'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; +import { calculateNewPanelGridPos } from 'app/features/dashboard/utils/panel'; + +export function onCreateNewPanel(dashboard: DashboardModel): number | undefined { + const newPanel: Partial = { + type: 'timeseries', + title: 'Panel Title', + gridPos: calculateNewPanelGridPos(dashboard), + }; + + dashboard.addPanel(newPanel); + return newPanel.id; +} + +export function onCreateNewRow(dashboard: DashboardModel) { + const newRow = { + type: 'row', + title: 'Row title', + gridPos: { x: 0, y: 0 }, + }; + + dashboard.addPanel(newRow); +} + +export function onAddLibraryPanel(dashboard: DashboardModel) { + const newPanel = { + type: 'add-library-panel', + gridPos: calculateNewPanelGridPos(dashboard), + }; + + dashboard.addPanel(newPanel); +} + +type PanelPluginInfo = { defaults: { gridPos: { w: number; h: number }; title: string } }; + +export function onPasteCopiedPanel(dashboard: DashboardModel, panelPluginInfo?: PanelPluginMeta & PanelPluginInfo) { + if (!panelPluginInfo) { + return; + } + + const gridPos = calculateNewPanelGridPos(dashboard); + + const newPanel = { + type: panelPluginInfo.id, + title: 'Panel Title', + gridPos: { + x: gridPos.x, + y: gridPos.y, + w: panelPluginInfo.defaults.gridPos.w, + h: panelPluginInfo.defaults.gridPos.h, + }, + }; + + // apply panel template / defaults + if (panelPluginInfo.defaults) { + defaults(newPanel, panelPluginInfo.defaults); + newPanel.title = panelPluginInfo.defaults.title; + store.delete(LS_PANEL_COPY_KEY); + } + + dashboard.addPanel(newPanel); +} + +export function getCopiedPanelPlugin(): (PanelPluginMeta & PanelPluginInfo) | undefined { + const panels = chain(config.panels) + .filter({ hideFromList: false }) + .map((item) => item) + .value(); + + const copiedPanelJson = store.get(LS_PANEL_COPY_KEY); + if (copiedPanelJson) { + const copiedPanel = JSON.parse(copiedPanelJson); + + const pluginInfo = find(panels, { id: copiedPanel.type }); + if (pluginInfo) { + const pluginCopy: PanelPluginMeta = cloneDeep(pluginInfo); + pluginCopy.name = copiedPanel.title; + pluginCopy.sort = -1; + + return { ...pluginCopy, defaults: { ...copiedPanel } }; + } + } + + return undefined; +}