mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Remove no-datasource screen (#68574)
* Dashboards: Remove non-ds configured landing page * Remove `datasourceOnboarding` feature toggle
This commit is contained in:
@@ -88,7 +88,6 @@ Alpha features might be changed or removed without prior notice.
|
|||||||
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
||||||
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
||||||
| `showTraceId` | Show trace ids for requests |
|
| `showTraceId` | Show trace ids for requests |
|
||||||
| `datasourceOnboarding` | Enable data source onboarding page |
|
|
||||||
| `authnService` | Use new auth service to perform authentication |
|
| `authnService` | Use new auth service to perform authentication |
|
||||||
| `alertingBacktesting` | Rule backtesting API for alerting |
|
| `alertingBacktesting` | Rule backtesting API for alerting |
|
||||||
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
|
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
|
||||||
|
@@ -59,7 +59,6 @@ export interface FeatureToggles {
|
|||||||
nestedFolders?: boolean;
|
nestedFolders?: boolean;
|
||||||
accessTokenExpirationCheck?: boolean;
|
accessTokenExpirationCheck?: boolean;
|
||||||
showTraceId?: boolean;
|
showTraceId?: boolean;
|
||||||
datasourceOnboarding?: boolean;
|
|
||||||
emptyDashboardPage?: boolean;
|
emptyDashboardPage?: boolean;
|
||||||
authnService?: boolean;
|
authnService?: boolean;
|
||||||
disablePrometheusExemplarSampling?: boolean;
|
disablePrometheusExemplarSampling?: boolean;
|
||||||
|
@@ -277,12 +277,6 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
Owner: grafanaObservabilityLogsSquad,
|
Owner: grafanaObservabilityLogsSquad,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "datasourceOnboarding",
|
|
||||||
Description: "Enable data source onboarding page",
|
|
||||||
State: FeatureStateAlpha,
|
|
||||||
Owner: grafanaDashboardsSquad,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "emptyDashboardPage",
|
Name: "emptyDashboardPage",
|
||||||
Description: "Enable the redesigned user interface of a dashboard page that includes no panels",
|
Description: "Enable the redesigned user interface of a dashboard page that includes no panels",
|
||||||
|
@@ -40,7 +40,6 @@ accessControlOnCall,beta,@grafana/grafana-authnz-team,false,false,false,false
|
|||||||
nestedFolders,beta,@grafana/backend-platform,false,false,false,false
|
nestedFolders,beta,@grafana/backend-platform,false,false,false,false
|
||||||
accessTokenExpirationCheck,stable,@grafana/grafana-authnz-team,false,false,false,false
|
accessTokenExpirationCheck,stable,@grafana/grafana-authnz-team,false,false,false,false
|
||||||
showTraceId,alpha,@grafana/observability-logs,false,false,false,false
|
showTraceId,alpha,@grafana/observability-logs,false,false,false,false
|
||||||
datasourceOnboarding,alpha,@grafana/dashboards-squad,false,false,false,false
|
|
||||||
emptyDashboardPage,stable,@grafana/dashboards-squad,false,false,false,true
|
emptyDashboardPage,stable,@grafana/dashboards-squad,false,false,false,true
|
||||||
authnService,alpha,@grafana/grafana-authnz-team,false,false,false,false
|
authnService,alpha,@grafana/grafana-authnz-team,false,false,false,false
|
||||||
disablePrometheusExemplarSampling,stable,@grafana/observability-metrics,false,false,false,false
|
disablePrometheusExemplarSampling,stable,@grafana/observability-metrics,false,false,false,false
|
||||||
|
|
@@ -171,10 +171,6 @@ const (
|
|||||||
// Show trace ids for requests
|
// Show trace ids for requests
|
||||||
FlagShowTraceId = "showTraceId"
|
FlagShowTraceId = "showTraceId"
|
||||||
|
|
||||||
// FlagDatasourceOnboarding
|
|
||||||
// Enable data source onboarding page
|
|
||||||
FlagDatasourceOnboarding = "datasourceOnboarding"
|
|
||||||
|
|
||||||
// FlagEmptyDashboardPage
|
// FlagEmptyDashboardPage
|
||||||
// Enable the redesigned user interface of a dashboard page that includes no panels
|
// Enable the redesigned user interface of a dashboard page that includes no panels
|
||||||
FlagEmptyDashboardPage = "emptyDashboardPage"
|
FlagEmptyDashboardPage = "emptyDashboardPage"
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { t } from 'app/core/internationalization';
|
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|
||||||
import { EmptyStateNoDatasource } from 'app/features/datasources/components/EmptyStateNoDatasource';
|
|
||||||
import { useLoadDataSources } from 'app/features/datasources/state';
|
|
||||||
import { useSelector } from 'app/types';
|
|
||||||
|
|
||||||
import DashboardPage from './DashboardPage';
|
|
||||||
|
|
||||||
export default function NewDashboardPage(props: GrafanaRouteComponentProps) {
|
|
||||||
const { isLoading } = useLoadDataSources();
|
|
||||||
|
|
||||||
const { hasDatasource } = useSelector((state) => ({
|
|
||||||
hasDatasource: state.dataSources.dataSourcesCount > 0,
|
|
||||||
}));
|
|
||||||
const [createDashboard, setCreateDashboard] = useState(false);
|
|
||||||
const showDashboardPage = hasDatasource || createDashboard || !config.featureToggles.datasourceOnboarding;
|
|
||||||
|
|
||||||
return showDashboardPage ? (
|
|
||||||
<DashboardPage {...props} />
|
|
||||||
) : (
|
|
||||||
<EmptyStateNoDatasource
|
|
||||||
onCTAClick={() => setCreateDashboard(true)}
|
|
||||||
loading={isLoading}
|
|
||||||
title={t('datasource-onboarding.welcome', 'Welcome to Grafana dashboards!')}
|
|
||||||
CTAText={t('datasource-onboarding.sampleData', 'Or set up a new dashboard with sample data')}
|
|
||||||
navId="dashboards/browse"
|
|
||||||
pageNav={{ text: t('datasource-onboarding.new-dashboard', 'New dashboard'), url: '/dashboard/new' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,78 +0,0 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
||||||
import { rest } from 'msw';
|
|
||||||
import { setupServer } from 'msw/node';
|
|
||||||
import React from 'react';
|
|
||||||
import { DeepPartial } from 'react-hook-form';
|
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
|
||||||
|
|
||||||
import { DataSourcePluginMeta } from '@grafana/data';
|
|
||||||
import * as runtime from '@grafana/runtime';
|
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
|
||||||
import 'whatwg-fetch';
|
|
||||||
|
|
||||||
import { EmptyStateNoDatasource } from './EmptyStateNoDatasource';
|
|
||||||
|
|
||||||
let reportInteractionSpy: jest.SpyInstance;
|
|
||||||
const server = setupServer();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true);
|
|
||||||
reportInteractionSpy = jest.spyOn(runtime, 'reportInteraction');
|
|
||||||
server.resetHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
server.listen({ onUnhandledRequest: 'bypass' });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('EmptyStateNoDatasource', () => {
|
|
||||||
it('correctly tracks user interactions', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get('/api/plugins', (_, res, ctx) => {
|
|
||||||
return res(
|
|
||||||
ctx.json<Array<DeepPartial<DataSourcePluginMeta>>>([
|
|
||||||
{ id: 'prometheus', name: 'Prometheus', info: { logos: { small: 'prometheus.png' } } },
|
|
||||||
{ id: 'mysql', name: 'MySQL', info: { logos: { small: 'mysql.png' } } },
|
|
||||||
{ id: 'elasticsearch', name: 'Elasticsearch', info: { logos: { small: 'elasticsearch.png' } } },
|
|
||||||
{ id: 'influxdb', name: 'InfluxDB', info: { logos: { small: 'influxdb.png' } } },
|
|
||||||
{ id: 'graphite', name: 'Graphite', info: { logos: { small: 'graphite.png' } } },
|
|
||||||
{ id: 'stackdriver', name: 'StackDriver', info: { logos: { small: 'stackdriver.png' } } },
|
|
||||||
{ id: 'cloudwatch', name: 'CloudWatch', info: { logos: { small: 'cloudwatch.png' } } },
|
|
||||||
{
|
|
||||||
id: 'grafana-azure-monitor-datasource',
|
|
||||||
name: 'Azure Monitor',
|
|
||||||
info: { logos: { small: 'grafana-azure-monitor-datasource.png' } },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
render(
|
|
||||||
<TestProvider>
|
|
||||||
<EmptyStateNoDatasource title="A Title" CTAText="CTA" />
|
|
||||||
</TestProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByRole('heading', { name: 'A Title' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: 'Prometheus' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Prometheus' }));
|
|
||||||
expect(reportInteractionSpy).toHaveBeenLastCalledWith('dashboards_connectds_ds_clicked');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('link', { name: 'View all' }));
|
|
||||||
expect(reportInteractionSpy).toHaveBeenCalledWith('dashboards_connectds_viewall_clicked');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'CTA' }));
|
|
||||||
expect(reportInteractionSpy).toHaveBeenLastCalledWith('dashboards_connectds_sampledata_clicked');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,215 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React, { ComponentProps } from 'react';
|
|
||||||
import { useAsync } from 'react-use';
|
|
||||||
|
|
||||||
import { DataSourcePluginMeta, GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
|
||||||
import { Icon, useStyles2 } from '@grafana/ui';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
|
||||||
import { contextSrv } from 'app/core/core';
|
|
||||||
import { t } from 'app/core/internationalization';
|
|
||||||
import { useAddDatasource } from 'app/features/datasources/state';
|
|
||||||
|
|
||||||
const topDatasources = [
|
|
||||||
'prometheus',
|
|
||||||
'mysql',
|
|
||||||
'elasticsearch',
|
|
||||||
'influxdb',
|
|
||||||
'graphite',
|
|
||||||
'stackdriver',
|
|
||||||
'cloudwatch',
|
|
||||||
'grafana-azure-monitor-datasource',
|
|
||||||
];
|
|
||||||
|
|
||||||
interface Props extends Pick<ComponentProps<typeof Page>, 'navId' | 'pageNav'> {
|
|
||||||
title: string;
|
|
||||||
CTAText: string;
|
|
||||||
onCTAClick?: () => void;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmptyStateNoDatasource({ onCTAClick, loading = false, title, CTAText, navId, pageNav }: Props) {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const { backend } = useGrafana();
|
|
||||||
const { value: datasources, loading: loadingDatasources } = useAsync(async () => {
|
|
||||||
const datasourceMeta: DataSourcePluginMeta[] = await backend.get('/api/plugins', {
|
|
||||||
enabled: 1,
|
|
||||||
type: 'datasource',
|
|
||||||
});
|
|
||||||
|
|
||||||
const byId = datasourceMeta.reduce<Record<string, DataSourcePluginMeta>>((prev, cur) => {
|
|
||||||
prev[cur.id] = cur;
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return topDatasources.map((d) => byId[d]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onAddDatasource = useAddDatasource();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page navId={navId} pageNav={pageNav} layout={PageLayoutType.Canvas}>
|
|
||||||
<div className={styles.wrapper}>
|
|
||||||
<h1 className={styles.title}>{title}</h1>
|
|
||||||
<div className={styles.description}>
|
|
||||||
<h4 className={styles.explanation}>
|
|
||||||
{t('datasource-onboarding.explanation', "To visualize your data, you'll need to connect it first.")}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
{contextSrv.hasRole('Admin') ? (
|
|
||||||
<>
|
|
||||||
<h4 className={styles.preferredDataSource}>
|
|
||||||
{t('datasource-onboarding.preferred', 'Connect your preferred data source:')}
|
|
||||||
</h4>
|
|
||||||
{!loadingDatasources && datasources !== undefined && (
|
|
||||||
<ul className={styles.datasources}>
|
|
||||||
{datasources.map((d) => (
|
|
||||||
<li key={d.id}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
reportInteraction('dashboards_connectds_ds_clicked');
|
|
||||||
onAddDatasource(d);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={d.info.logos.small} alt="" height="16" width="16" className={styles.logo} />
|
|
||||||
<span className={styles.datasourceName}>{d.name}</span>
|
|
||||||
<Icon name="arrow-right" size="lg" className={styles.arrowIcon} />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="/datasources/new"
|
|
||||||
className={styles.viewAll}
|
|
||||||
onClick={() => reportInteraction('dashboards_connectds_viewall_clicked')}
|
|
||||||
>
|
|
||||||
<span>{t('datasource-onboarding.viewAll', 'View all')}</span>
|
|
||||||
<Icon name="arrow-right" size="lg" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<h4>
|
|
||||||
{t('datasource-onboarding.contact-admin', 'Please contact your administrator to configure data sources.')}
|
|
||||||
</h4>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
reportInteraction('dashboards_connectds_sampledata_clicked');
|
|
||||||
onCTAClick?.();
|
|
||||||
}}
|
|
||||||
className={styles.ctaButton}
|
|
||||||
>
|
|
||||||
<span>{CTAText}</span>
|
|
||||||
<Icon name="arrow-right" size="lg" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
|
||||||
return {
|
|
||||||
wrapper: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
flexGrow: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}),
|
|
||||||
title: css({
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: theme.typography.pxToRem(32),
|
|
||||||
fontWeight: theme.typography.fontWeightBold,
|
|
||||||
}),
|
|
||||||
description: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
maxWidth: '50vw',
|
|
||||||
marginBottom: theme.spacing(8),
|
|
||||||
color: theme.colors.text.secondary,
|
|
||||||
}),
|
|
||||||
explanation: css({
|
|
||||||
marginBottom: '0px',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: theme.typography.pxToRem(19),
|
|
||||||
}),
|
|
||||||
preferredDataSource: css({
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
fontSize: theme.typography.pxToRem(19),
|
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
|
||||||
}),
|
|
||||||
datasources: css({
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: `repeat(auto-fit, minmax(${theme.spacing(28)}, 1fr))`,
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
listStyle: 'none',
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: theme.spacing(88),
|
|
||||||
|
|
||||||
'> li': {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
'> button': {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
|
||||||
height: theme.spacing(7),
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
margin: '0px',
|
|
||||||
padding: `calc(${theme.spacing(2)} - 1px)`,
|
|
||||||
lineHeight: theme.spacing(3),
|
|
||||||
border: `1px solid ${theme.colors.border.weak}`,
|
|
||||||
borderRadius: theme.shape.borderRadius(1),
|
|
||||||
background: theme.colors.background.primary,
|
|
||||||
fontSize: theme.typography.pxToRem(19),
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
logo: css({
|
|
||||||
width: theme.spacing(2),
|
|
||||||
height: theme.spacing(2),
|
|
||||||
objectFit: 'contain',
|
|
||||||
}),
|
|
||||||
datasourceName: css({
|
|
||||||
marginRight: 'auto',
|
|
||||||
}),
|
|
||||||
arrowIcon: css({
|
|
||||||
color: theme.colors.text.link,
|
|
||||||
}),
|
|
||||||
viewAll: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexGrow: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
lineHeight: theme.spacing(3),
|
|
||||||
fontSize: theme.typography.pxToRem(19),
|
|
||||||
color: theme.colors.text.link,
|
|
||||||
textDecoration: 'underline',
|
|
||||||
textUnderlinePosition: 'under',
|
|
||||||
}),
|
|
||||||
ctaButton: css({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
margin: '0px',
|
|
||||||
marginTop: theme.spacing(8),
|
|
||||||
padding: theme.spacing(0),
|
|
||||||
border: 'none',
|
|
||||||
background: 'inherit',
|
|
||||||
fontSize: theme.typography.h6.fontSize,
|
|
||||||
color: theme.colors.text.secondary,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
@@ -50,7 +50,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
pageClass: 'page-dashboard',
|
pageClass: 'page-dashboard',
|
||||||
routeName: DashboardRoutes.New,
|
routeName: DashboardRoutes.New,
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/NewDashboardPage')
|
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user