mirror of https://github.com/grafana/grafana
Dashboard: Empty/No Panels dashboard with a new design (#65161)
* 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 * start page for empty dashboard * add feature flag for empty dashboard redesign * only show empty dashboard redesign if FFpull/65463/head
parent
a89202eab2
commit
221c5efedc
|
@ -0,0 +1,100 @@ |
||||
import { css, cx, keyframes } from '@emotion/css'; |
||||
import React from 'react'; |
||||
import tinycolor from 'tinycolor2'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { LibraryPanel } from '@grafana/schema'; |
||||
import { IconButton, useStyles2 } from '@grafana/ui'; |
||||
|
||||
import { |
||||
LibraryPanelsSearch, |
||||
LibraryPanelsSearchVariant, |
||||
} from '../../../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; |
||||
import { DashboardModel, PanelModel } from '../../state'; |
||||
|
||||
interface Props { |
||||
panel: PanelModel; |
||||
dashboard: DashboardModel; |
||||
} |
||||
|
||||
export const AddLibraryPanelWidget = ({ panel, dashboard }: Props) => { |
||||
const onCancelAddPanel = (evt: React.MouseEvent<HTMLButtonElement>) => { |
||||
evt.preventDefault(); |
||||
dashboard.removePanel(panel); |
||||
}; |
||||
|
||||
const onAddLibraryPanel = (panelInfo: LibraryPanel) => { |
||||
const { gridPos } = panel; |
||||
|
||||
const newPanel = { |
||||
...panelInfo.model, |
||||
gridPos, |
||||
libraryPanel: panelInfo, |
||||
}; |
||||
|
||||
dashboard.addPanel(newPanel); |
||||
dashboard.removePanel(panel); |
||||
}; |
||||
|
||||
const styles = useStyles2(getStyles); |
||||
|
||||
return ( |
||||
<div className={styles.wrapper}> |
||||
<div className={cx('panel-container', styles.callToAction)}> |
||||
<div className={cx(styles.headerRow, 'grid-drag-handle')}> |
||||
<span>Add panel from panel library</span> |
||||
<div className="flex-grow-1" /> |
||||
<IconButton aria-label="Close 'Add Panel' widget" name="times" onClick={onCancelAddPanel} /> |
||||
</div> |
||||
<LibraryPanelsSearch onClick={onAddLibraryPanel} variant={LibraryPanelsSearchVariant.Tight} showPanelFilter /> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => { |
||||
const pulsate = keyframes({ |
||||
'0%': { |
||||
boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`, |
||||
}, |
||||
'50%': { |
||||
boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${tinycolor(theme.colors.primary.main) |
||||
.darken(20) |
||||
.toHexString()}`,
|
||||
}, |
||||
'100%': { |
||||
boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${theme.colors.primary.main}`, |
||||
}, |
||||
}); |
||||
|
||||
return { |
||||
// wrapper is used to make sure box-shadow animation isn't cut off in dashboard page
|
||||
wrapper: css({ |
||||
height: '100%', |
||||
paddingTop: `${theme.spacing(0.5)}`, |
||||
}), |
||||
headerRow: css({ |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
height: '38px', |
||||
flexShrink: 0, |
||||
width: '100%', |
||||
fontSize: theme.typography.fontSize, |
||||
fontWeight: theme.typography.fontWeightMedium, |
||||
paddingLeft: `${theme.spacing(1)}`, |
||||
transition: 'background-color 0.1s ease-in-out', |
||||
cursor: 'move', |
||||
|
||||
'&:hover': { |
||||
background: `${theme.colors.background.secondary}`, |
||||
}, |
||||
}), |
||||
callToAction: css({ |
||||
overflow: 'hidden', |
||||
outline: '2px dotted transparent', |
||||
outlineOffset: '2px', |
||||
boxShadow: '0 0 0 2px black, 0 0 0px 4px #1f60c4', |
||||
animation: `${pulsate} 2s ease infinite`, |
||||
}), |
||||
}; |
||||
}; |
@ -0,0 +1 @@ |
||||
export { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; |
@ -0,0 +1,185 @@ |
||||
import { css, cx } from '@emotion/css'; |
||||
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'; |
||||
|
||||
export interface Props { |
||||
dashboard: DashboardModel; |
||||
canCreate: boolean; |
||||
} |
||||
|
||||
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 ( |
||||
<div className={styles.centeredContent}> |
||||
<div className={cx(styles.centeredContent, styles.wrapper)}> |
||||
<div className={cx(styles.containerBox, styles.centeredContent, styles.visualizationContainer)}> |
||||
<h1 className={cx(styles.headerSection, styles.headerBig)}> |
||||
Start your new dashboard by adding a visualization |
||||
</h1> |
||||
<h4 className={cx(styles.bodySection, styles.bodyBig)}> |
||||
Select a data source and then query and visualize your data with charts, stats and tables or create lists, |
||||
markdowns and other widgets. |
||||
</h4> |
||||
<Button |
||||
size="lg" |
||||
icon="plus" |
||||
aria-label="Add new panel" |
||||
onClick={() => { |
||||
reportInteraction('Create new panel'); |
||||
onCreateNewPanel(); |
||||
}} |
||||
disabled={!canCreate} |
||||
> |
||||
Add visualization |
||||
</Button> |
||||
</div> |
||||
<div className={cx(styles.centeredContent, styles.others)}> |
||||
<div className={cx(styles.containerBox, styles.centeredContent, styles.rowContainer)}> |
||||
<h2 className={cx(styles.headerSection, styles.headerSmall)}>Add a row</h2> |
||||
<h5 className={cx(styles.bodySection, styles.bodySmall)}> |
||||
Group your visualizations into expandable sections. |
||||
</h5> |
||||
<Button |
||||
icon="plus" |
||||
fill="outline" |
||||
aria-label="Add new row" |
||||
onClick={() => { |
||||
reportInteraction('Create new row'); |
||||
onCreateNewRow(); |
||||
}} |
||||
disabled={!canCreate} |
||||
> |
||||
Add row |
||||
</Button> |
||||
</div> |
||||
<div className={cx(styles.containerBox, styles.centeredContent, styles.libraryContainer)}> |
||||
<h2 className={cx(styles.headerSection, styles.headerSmall)}>Import panel</h2> |
||||
<h5 className={cx(styles.bodySection, styles.bodySmall)}> |
||||
Import visualizations that are shared with other dashboards. |
||||
</h5> |
||||
<Button |
||||
icon="plus" |
||||
fill="outline" |
||||
aria-label="Add new panel from panel library" |
||||
onClick={() => { |
||||
reportInteraction('Add a panel from the panel library'); |
||||
onAddLibraryPanel(); |
||||
}} |
||||
disabled={!canCreate} |
||||
> |
||||
Import library panel |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => { |
||||
return { |
||||
wrapper: css({ |
||||
label: 'dashboard-empty-wrapper', |
||||
flexDirection: 'column', |
||||
maxWidth: '890px', |
||||
gap: theme.spacing.gridSize * 4, |
||||
}), |
||||
containerBox: css({ |
||||
label: 'container-box', |
||||
flexDirection: 'column', |
||||
boxSizing: 'border-box', |
||||
border: '1px dashed rgba(110, 159, 255, 0.5)', |
||||
}), |
||||
centeredContent: css({ |
||||
label: 'centered', |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
}), |
||||
visualizationContainer: css({ |
||||
label: 'visualization-container', |
||||
padding: theme.spacing.gridSize * 4, |
||||
}), |
||||
others: css({ |
||||
label: 'others-wrapper', |
||||
alignItems: 'stretch', |
||||
flexDirection: 'row', |
||||
gap: theme.spacing.gridSize * 4, |
||||
|
||||
[theme.breakpoints.down('sm')]: { |
||||
flexDirection: 'column', |
||||
}, |
||||
}), |
||||
rowContainer: css({ |
||||
label: 'row-container', |
||||
padding: theme.spacing.gridSize * 3, |
||||
}), |
||||
libraryContainer: css({ |
||||
label: 'library-container', |
||||
padding: theme.spacing.gridSize * 3, |
||||
}), |
||||
visualizationContent: css({ |
||||
gap: theme.spacing.gridSize * 2, |
||||
}), |
||||
headerSection: css({ |
||||
label: 'header-section', |
||||
fontWeight: 600, |
||||
textAlign: 'center', |
||||
}), |
||||
headerBig: css({ |
||||
marginBottom: theme.spacing.gridSize * 2, |
||||
}), |
||||
headerSmall: css({ |
||||
marginBottom: theme.spacing.gridSize, |
||||
}), |
||||
bodySection: css({ |
||||
label: 'body-section', |
||||
fontWeight: theme.typography.fontWeightRegular, |
||||
color: theme.colors.text.secondary, |
||||
textAlign: 'center', |
||||
}), |
||||
bodyBig: css({ |
||||
maxWidth: '75%', |
||||
marginBottom: theme.spacing.gridSize * 4, |
||||
}), |
||||
bodySmall: css({ |
||||
marginBottom: theme.spacing.gridSize * 3, |
||||
}), |
||||
}; |
||||
}; |
Loading…
Reference in new issue