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 bar
This commit is contained in:
Polina Boneva 2023-03-30 13:50:35 +03:00 committed by GitHub
parent 1c7921770c
commit b9fb23502c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 61 deletions

View File

@ -45,10 +45,6 @@ export const Icon = React.forwardRef<HTMLDivElement, IconProps>(
return <i className={getFontAwesomeIconStyles(name, className)} {...divElementProps} style={style} />;
}
if (name === 'panel-add') {
size = 'xl';
}
if (!cacheInitialized) {
initIconCache();
}

View File

@ -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),
},
}),
};
}

View File

@ -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>
);
};

View File

@ -217,7 +217,7 @@ const AddPanelWidgetHandle = ({ children, onBack, onCancel, styles }: AddPanelWi
)}
{!onBack && (
<div className={styles.backButton}>
<Icon name="panel-add" size="md" />
<Icon name="panel-add" size="xl" />
</div>
)}
{children && <span>{children}</span>}

View File

@ -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>((props) => {
}
if (canEdit && !isFullscreen) {
buttons.push(
<ToolbarButton
tooltip={t('dashboard.toolbar.add-panel', 'Add panel')}
icon="panel-add"
onClick={onAddPanel}
key="button-panel-add"
/>
);
if (config.featureToggles.emptyDashboardPage) {
buttons.push(<AddPanelButton dashboard={dashboard} key="panel-add-dropdown" />);
} else {
buttons.push(
<ToolbarButton
tooltip={t('dashboard.toolbar.add-panel', 'Add panel')}
icon="panel-add"
iconSize="xl"
onClick={onAddPanel}
key="button-panel-add"
/>
);
}
}
if (canSave && !isFullscreen) {

View File

@ -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<PanelModel> = {
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,
}),
};
};
}

View File

@ -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,
},
};

View File

@ -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;
}