mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 FF
This commit is contained in:
parent
a89202eab2
commit
221c5efedc
@ -82,6 +82,7 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
||||
| `elasticsearchBackendMigration` | Use Elasticsearch as backend data source |
|
||||
| `datasourceOnboarding` | Enable data source onboarding page |
|
||||
| `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels |
|
||||
| `secureSocksDatasourceProxy` | Enable secure socks tunneling for supported core datasources |
|
||||
| `authnService` | Use new auth service to perform authentication |
|
||||
| `alertingBacktesting` | Rule backtesting API for alerting |
|
||||
|
@ -66,6 +66,7 @@ export interface FeatureToggles {
|
||||
accessTokenExpirationCheck?: boolean;
|
||||
elasticsearchBackendMigration?: boolean;
|
||||
datasourceOnboarding?: boolean;
|
||||
emptyDashboardPage?: boolean;
|
||||
secureSocksDatasourceProxy?: boolean;
|
||||
authnService?: boolean;
|
||||
disablePrometheusExemplarSampling?: boolean;
|
||||
|
@ -320,6 +320,13 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
},
|
||||
{
|
||||
Name: "emptyDashboardPage",
|
||||
Description: "Enable the redesigned user interface of a dashboard page that includes no panels",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
},
|
||||
{
|
||||
Name: "secureSocksDatasourceProxy",
|
||||
Description: "Enable secure socks tunneling for supported core datasources",
|
||||
|
@ -47,6 +47,7 @@ nestedFolders,alpha,@grafana/backend-platform,true,false,false,false
|
||||
accessTokenExpirationCheck,stable,@grafana/grafana-authnz-team,false,false,false,false
|
||||
elasticsearchBackendMigration,alpha,@grafana/observability-logs,false,false,false,false
|
||||
datasourceOnboarding,alpha,@grafana/dashboards-squad,false,false,false,false
|
||||
emptyDashboardPage,alpha,@grafana/dashboards-squad,false,false,false,true
|
||||
secureSocksDatasourceProxy,alpha,@grafana/hosted-grafana-team,false,false,false,false
|
||||
authnService,alpha,@grafana/grafana-authnz-team,false,false,false,false
|
||||
disablePrometheusExemplarSampling,stable,@grafana/observability-metrics,false,false,false,false
|
||||
|
|
@ -199,6 +199,10 @@ const (
|
||||
// Enable data source onboarding page
|
||||
FlagDatasourceOnboarding = "datasourceOnboarding"
|
||||
|
||||
// FlagEmptyDashboardPage
|
||||
// Enable the redesigned user interface of a dashboard page that includes no panels
|
||||
FlagEmptyDashboardPage = "emptyDashboardPage"
|
||||
|
||||
// FlagSecureSocksDatasourceProxy
|
||||
// Enable secure socks tunneling for supported core datasources
|
||||
FlagSecureSocksDatasourceProxy = "secureSocksDatasourceProxy"
|
||||
|
@ -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';
|
@ -48,6 +48,7 @@ import { liveTimer } from '../dashgrid/liveTimer';
|
||||
import { getTimeSrv } from '../services/TimeSrv';
|
||||
import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||
import { initDashboard } from '../state/initDashboard';
|
||||
import { calculateNewPanelGridPos } from '../utils/panel';
|
||||
|
||||
export interface DashboardPageRouteParams {
|
||||
uid?: string;
|
||||
@ -361,17 +362,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move all panels down by the height of the "add panel" widget.
|
||||
// This is to work around an issue with react-grid-layout that can mess up the layout
|
||||
// in certain configurations. (See https://github.com/react-grid-layout/react-grid-layout/issues/1787)
|
||||
const addPanelWidgetHeight = 8;
|
||||
for (const panel of dashboard.panelIterator()) {
|
||||
panel.gridPos.y += addPanelWidgetHeight;
|
||||
}
|
||||
|
||||
dashboard.addPanel({
|
||||
type: 'add-panel',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: addPanelWidgetHeight },
|
||||
gridPos: calculateNewPanelGridPos(dashboard),
|
||||
title: 'Panel Title',
|
||||
});
|
||||
|
||||
|
185
public/app/features/dashboard/dashgrid/DashboardEmpty.tsx
Normal file
185
public/app/features/dashboard/dashgrid/DashboardEmpty.tsx
Normal file
@ -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,
|
||||
}),
|
||||
};
|
||||
};
|
@ -8,11 +8,13 @@ import { config } from '@grafana/runtime';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { DashboardPanelsChangedEvent } from 'app/types/events';
|
||||
|
||||
import { AddLibraryPanelWidget } from '../components/AddLibraryPanelWidget';
|
||||
import { AddPanelWidget } from '../components/AddPanelWidget';
|
||||
import { DashboardRow } from '../components/DashboardRow';
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { GridPos } from '../state/PanelModel';
|
||||
|
||||
import { DashboardEmpty } from './DashboardEmpty';
|
||||
import { DashboardPanel } from './DashboardPanel';
|
||||
|
||||
export interface Props {
|
||||
@ -187,6 +189,10 @@ export class DashboardGrid extends PureComponent<Props, State> {
|
||||
return <AddPanelWidget key={panel.key} panel={panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
if (panel.type === 'add-library-panel') {
|
||||
return <AddLibraryPanelWidget key={panel.key} panel={panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPanel
|
||||
key={panel.key}
|
||||
@ -203,7 +209,8 @@ export class DashboardGrid extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isEditable } = this.props;
|
||||
const { dashboard, isEditable } = this.props;
|
||||
const hasPanels = dashboard.panels && dashboard.panels.length > 0;
|
||||
|
||||
/**
|
||||
* We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer
|
||||
@ -225,7 +232,40 @@ export class DashboardGrid extends PureComponent<Props, State> {
|
||||
moving panels. https://github.com/grafana/grafana/issues/18497
|
||||
theme.breakpoints.md = 769
|
||||
*/
|
||||
return (
|
||||
return config.featureToggles.emptyDashboardPage ? (
|
||||
hasPanels ? (
|
||||
/**
|
||||
* The children is using a width of 100% so we need to guarantee that it is wrapped
|
||||
* in an element that has the calculated size given by the AutoSizer. The AutoSizer
|
||||
* has a width of 0 and will let its content overflow its div.
|
||||
*/
|
||||
<div style={{ width: `${width}px`, height: '100%' }}>
|
||||
<ReactGridLayout
|
||||
width={width}
|
||||
isDraggable={draggable}
|
||||
isResizable={isEditable}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
|
||||
cols={GRID_COLUMN_COUNT}
|
||||
rowHeight={GRID_CELL_HEIGHT}
|
||||
draggableHandle=".grid-drag-handle"
|
||||
draggableCancel=".grid-drag-cancel"
|
||||
layout={this.buildLayout()}
|
||||
onDragStop={this.onDragStop}
|
||||
onResize={this.onResize}
|
||||
onResizeStop={this.onResizeStop}
|
||||
onLayoutChange={this.onLayoutChange}
|
||||
>
|
||||
{this.renderPanels(width)}
|
||||
</ReactGridLayout>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: `${width}px`, height: '100%', padding: `${draggable ? '100px 0' : '0'}` }}>
|
||||
<DashboardEmpty dashboard={dashboard} canCreate={isEditable} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
/**
|
||||
* The children is using a width of 100% so we need to guarantee that it is wrapped
|
||||
* in an element that has the calculated size given by the AutoSizer. The AutoSizer
|
||||
|
@ -181,3 +181,15 @@ export function calculateInnerPanelHeight(panel: PanelModel, containerHeight: nu
|
||||
const headerHeight = panel.hasTitle() ? config.theme.panelHeaderHeight : 0;
|
||||
return containerHeight - headerHeight - chromePadding - PANEL_BORDER;
|
||||
}
|
||||
|
||||
export function calculateNewPanelGridPos(dashboard: DashboardModel): PanelModel['gridPos'] {
|
||||
// Move all panels down by the height of the "add panel" widget.
|
||||
// This is to work around an issue with react-grid-layout that can mess up the layout
|
||||
// in certain configurations. (See https://github.com/react-grid-layout/react-grid-layout/issues/1787)
|
||||
const addPanelWidgetHeight = 8;
|
||||
for (const panel of dashboard.panelIterator()) {
|
||||
panel.gridPos.y += addPanelWidgetHeight;
|
||||
}
|
||||
|
||||
return { x: 0, y: 0, w: 12, h: addPanelWidgetHeight };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user