mirror of https://github.com/grafana/grafana
Dashboard: Add new visualization/row/library panel/pasted panel is now a dropdown menu (#65361)
* Empty Dashboard state has its own CTA items and its own separate box to choose a library panel to create * show empty dashboard screen if no panels * add feature flag for empty dashboard redesign * only show empty dashboard redesign if FF * add-new-panel button is a dropdown with options: visualization, row, import, paste * fix onPasteCopiedPanel types * do not create new panel to new dashboard if emptyDashboardPage FF true * ToolbarButton does not allow rendering of Dropdown's overlay - switch to Button * remove panel-add icon's default size of xl * separate components for add new panel from dash navigation barpull/65674/head
parent
1c7921770c
commit
b9fb23502c
@ -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 ( |
||||
<Dropdown |
||||
overlay={() => <AddPanelMenu dashboard={dashboard} />} |
||||
placement="bottom" |
||||
offset={[0, 6]} |
||||
onVisibleChange={setIsMenuOpen} |
||||
> |
||||
<Button |
||||
tooltip={t('dashboard.toolbar.add-panel', 'Add panel')} |
||||
icon="panel-add" |
||||
size="lg" |
||||
fill="outline" |
||||
className={cx(styles.button, styles.buttonIcon, styles.buttonText)} |
||||
> |
||||
Add |
||||
<Icon name={isMenuOpen ? 'angle-up' : 'angle-down'} size="lg" /> |
||||
</Button> |
||||
</Dropdown> |
||||
); |
||||
}; |
||||
|
||||
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), |
||||
}, |
||||
}), |
||||
}; |
||||
} |
@ -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 ( |
||||
<Menu> |
||||
<Menu.Item |
||||
key="add-visualisation" |
||||
label="Visualization" |
||||
ariaLabel="Add new panel" |
||||
onClick={() => { |
||||
reportInteraction('Create new panel'); |
||||
const id = onCreateNewPanel(dashboard); |
||||
locationService.partial({ editPanel: id }); |
||||
}} |
||||
/> |
||||
<Menu.Item |
||||
key="add-row" |
||||
label="Row" |
||||
ariaLabel="Add new row" |
||||
onClick={() => { |
||||
reportInteraction('Create new row'); |
||||
onCreateNewRow(dashboard); |
||||
}} |
||||
/> |
||||
<Menu.Item |
||||
key="add-panel-lib" |
||||
label="Import from library" |
||||
ariaLabel="Add new panel from panel library" |
||||
onClick={() => { |
||||
reportInteraction('Add a panel from the panel library'); |
||||
onAddLibraryPanel(dashboard); |
||||
}} |
||||
/> |
||||
<Menu.Item |
||||
key="add-panel-clipboard" |
||||
label="Paste panel" |
||||
ariaLabel="Add new panel from clipboard" |
||||
onClick={() => { |
||||
reportInteraction('Paste panel from clipboard'); |
||||
onPasteCopiedPanel(dashboard, copiedPanelPlugin); |
||||
}} |
||||
disabled={!copiedPanelPlugin} |
||||
/> |
||||
</Menu> |
||||
); |
||||
}; |
@ -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<PanelModel> = { |
||||
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; |
||||
} |
Loading…
Reference in new issue