diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c9ba8538b5c..b0e2c69f0f4 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index dd8f8ac59ee..b36cca8b73b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -66,6 +66,7 @@ export interface FeatureToggles { accessTokenExpirationCheck?: boolean; elasticsearchBackendMigration?: boolean; datasourceOnboarding?: boolean; + emptyDashboardPage?: boolean; secureSocksDatasourceProxy?: boolean; authnService?: boolean; disablePrometheusExemplarSampling?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 09a9e57d9c6..e7401c126db 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 66650de00ae..349877154ec 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 550069987d1..8c47ffe4e02 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/public/app/features/dashboard/components/AddLibraryPanelWidget/AddLibraryPanelWidget.tsx b/public/app/features/dashboard/components/AddLibraryPanelWidget/AddLibraryPanelWidget.tsx new file mode 100644 index 00000000000..befdbbc235d --- /dev/null +++ b/public/app/features/dashboard/components/AddLibraryPanelWidget/AddLibraryPanelWidget.tsx @@ -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) => { + 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 ( +
+
+
+ Add panel from panel library +
+ +
+ +
+
+ ); +}; + +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`, + }), + }; +}; diff --git a/public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts b/public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts new file mode 100644 index 00000000000..b11f3ac6389 --- /dev/null +++ b/public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts @@ -0,0 +1 @@ +export { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3e5ee3daa8e..9e255becd5f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -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 { 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', }); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx new file mode 100644 index 00000000000..554dbe784c5 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -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 = { + 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 ( +
+
+
+

+ Start your new dashboard by adding a visualization +

+

+ Select a data source and then query and visualize your data with charts, stats and tables or create lists, + markdowns and other widgets. +

+ +
+
+
+

Add a row

+
+ Group your visualizations into expandable sections. +
+ +
+
+

Import panel

+
+ Import visualizations that are shared with other dashboards. +
+ +
+
+
+
+ ); +}; + +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, + }), + }; +}; diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index f75c2d6c27e..f1c7ced4404 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -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 { return ; } + if (panel.type === 'add-library-panel') { + return ; + } + return ( { } 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 { 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. + */ +
+ + {this.renderPanels(width)} + +
+ ) : ( +
+ +
+ ) + ) : ( /** * 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 diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 1a870ee6b03..38156c1c115 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -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 }; +}