Explore: Use Datasource Onboarding page when visiting without any datasource set up (#60399)

* allow DatasourceOnboarding title and cta text customization

* Explore: use Datasource Onboarding page when visiting without any datasource set up

* move & rename DatasourceOnboarding

* Rename component
This commit is contained in:
Giordano Ricci
2022-12-29 15:38:40 +00:00
committed by GitHub
parent 15d32546ea
commit 9ff3bf4849
8 changed files with 71 additions and 31 deletions

View File

@@ -3771,9 +3771,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"] [0, 0, 0, "Do not use any type assertions.", "8"]
], ],
"public/app/features/explore/Wrapper.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/explore/spec/helper/setup.tsx:5381": [ "public/app/features/explore/spec/helper/setup.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@@ -2,12 +2,13 @@ import React, { useState } from 'react';
import { useEffectOnce } from 'react-use'; import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { t } from 'app/core/internationalization';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { EmptyStateNoDatasource } from 'app/features/datasources/components/EmptyStateNoDatasource';
import { loadDataSources } from 'app/features/datasources/state'; import { loadDataSources } from 'app/features/datasources/state';
import { useDispatch, useSelector } from 'app/types'; import { useDispatch, useSelector } from 'app/types';
import DashboardPage from './DashboardPage'; import DashboardPage from './DashboardPage';
import { DatasourceOnboarding } from './DatasourceOnboarding';
export default function NewDashboardPage(props: GrafanaRouteComponentProps) { export default function NewDashboardPage(props: GrafanaRouteComponentProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -25,6 +26,13 @@ export default function NewDashboardPage(props: GrafanaRouteComponentProps) {
return showDashboardPage ? ( return showDashboardPage ? (
<DashboardPage {...props} /> <DashboardPage {...props} />
) : ( ) : (
<DatasourceOnboarding onNewDashboard={() => setCreateDashboard(true)} loading={loading} /> <EmptyStateNoDatasource
onCTAClick={() => setCreateDashboard(true)}
loading={loading}
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('dashboard', 'New dashboard'), url: '/dashboard/new' }}
/>
); );
} }

View File

@@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React, { ComponentProps } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { DataSourcePluginMeta, GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { DataSourcePluginMeta, GrafanaTheme2, PageLayoutType } from '@grafana/data';
@@ -21,13 +21,14 @@ const topDatasources = [
'grafana-azure-monitor-datasource', 'grafana-azure-monitor-datasource',
]; ];
export function DatasourceOnboarding({ interface Props extends Pick<ComponentProps<typeof Page>, 'navId' | 'pageNav'> {
onNewDashboard, title: string;
loading = false, CTAText: string;
}: { onCTAClick?: () => void;
onNewDashboard?: () => void;
loading?: boolean; loading?: boolean;
}) { }
export function EmptyStateNoDatasource({ onCTAClick, loading = false, title, CTAText, navId, pageNav }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { value: datasources, loading: loadingDatasources } = useAsync(async () => { const { value: datasources, loading: loadingDatasources } = useAsync(async () => {
const datasourceMeta: DataSourcePluginMeta[] = await getBackendSrv().get('/api/plugins', { const datasourceMeta: DataSourcePluginMeta[] = await getBackendSrv().get('/api/plugins', {
@@ -50,13 +51,9 @@ export function DatasourceOnboarding({
} }
return ( return (
<Page <Page navId={navId} pageNav={pageNav} layout={PageLayoutType.Canvas}>
navId="dashboards/browse"
pageNav={{ text: t('dashboard', 'New dashboard'), url: '/dashboard/new' }}
layout={PageLayoutType.Canvas}
>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<h1 className={styles.title}>{t('datasource-onboarding.welcome', 'Welcome to Grafana dashboards!')}</h1> <h1 className={styles.title}>{title}</h1>
<div className={styles.description}> <div className={styles.description}>
<h4 className={styles.explanation}> <h4 className={styles.explanation}>
{t('datasource-onboarding.explanation', "To visualize your data, you'll need to connect it first.")} {t('datasource-onboarding.explanation', "To visualize your data, you'll need to connect it first.")}
@@ -100,8 +97,8 @@ export function DatasourceOnboarding({
{t('datasource-onboarding.contact-admin', 'Please contact your administrator to configure data sources.')} {t('datasource-onboarding.contact-admin', 'Please contact your administrator to configure data sources.')}
</h4> </h4>
)} )}
<button onClick={onNewDashboard} className={styles.createNew}> <button onClick={onCTAClick} className={styles.ctaButton}>
<span>{t('datasource-onboarding.sampleData', 'Or set up a new dashboard with sample data')}</span> <span>{CTAText}</span>
<Icon name="arrow-right" size="lg" /> <Icon name="arrow-right" size="lg" />
</button> </button>
</div> </div>
@@ -193,7 +190,7 @@ function getStyles(theme: GrafanaTheme2) {
textDecoration: 'underline', textDecoration: 'underline',
textUnderlinePosition: 'under', textUnderlinePosition: 'under',
}), }),
createNew: css({ ctaButton: css({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing(1), gap: theme.spacing(1),

View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { EmptyStateNoDatasource } from 'app/features/datasources/components/EmptyStateNoDatasource';
import { ExploreQueryParams, useDispatch, useSelector } from 'app/types';
import { loadDataSources } from '../datasources/state';
import { ExplorePage } from './ExplorePage';
export default function EmptyStateWrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
const dispatch = useDispatch();
useEffectOnce(() => {
if (config.featureToggles.datasourceOnboarding) {
dispatch(loadDataSources());
}
});
const { hasDatasource, loading } = useSelector((state) => ({
hasDatasource: state.dataSources.dataSourcesCount > 0,
loading: !state.dataSources.hasFetched,
}));
const [showOnboarding, setShowOnboarding] = useState(config.featureToggles.datasourceOnboarding);
const showExplorePage = hasDatasource || !showOnboarding;
return showExplorePage ? (
<ExplorePage {...props} />
) : (
<EmptyStateNoDatasource
onCTAClick={() => setShowOnboarding(false)}
loading={loading}
title="Welcome to Grafana Explore!"
CTAText="Or explore sample data"
navId="explore"
/>
);
}

View File

@@ -1,6 +1,7 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import React from 'react'; import React, { ComponentProps } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { serializeStateToUrlParam } from '@grafana/data'; import { serializeStateToUrlParam } from '@grafana/data';
import { locationService, config } from '@grafana/runtime'; import { locationService, config } from '@grafana/runtime';
@@ -27,13 +28,13 @@ jest.mock('app/core/core', () => {
jest.mock('react-virtualized-auto-sizer', () => { jest.mock('react-virtualized-auto-sizer', () => {
return { return {
__esModule: true, __esModule: true,
default(props: any) { default(props: ComponentProps<typeof AutoSizer>) {
return <div>{props.children({ width: 1000 })}</div>; return <div>{props.children({ width: 1000, height: 1000 })}</div>;
}, },
}; };
}); });
describe('Wrapper', () => { describe('ExplorePage', () => {
afterEach(() => { afterEach(() => {
tearDown(); tearDown();
}); });

View File

@@ -31,7 +31,7 @@ const styles = {
`, `,
}; };
function Wrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { export function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
useExplorePageTitle(); useExplorePageTitle();
const dispatch = useDispatch(); const dispatch = useDispatch();
const queryParams = props.queryParams; const queryParams = props.queryParams;
@@ -173,5 +173,3 @@ const useExplorePageTitle = () => {
document.title = `${navModel.main.text} - ${datasources.join(' | ')} - ${Branding.AppTitle}`; document.title = `${navModel.main.text} - ${datasources.join(' | ')} - ${Branding.AppTitle}`;
}; };
export default Wrapper;

View File

@@ -29,7 +29,7 @@ import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource';
import { LokiQuery } from '../../../../plugins/datasource/loki/types'; import { LokiQuery } from '../../../../plugins/datasource/loki/types';
import { ExploreId } from '../../../../types'; import { ExploreId } from '../../../../types';
import { initialUserState } from '../../../profile/state/reducers'; import { initialUserState } from '../../../profile/state/reducers';
import Wrapper from '../../Wrapper'; import { ExplorePage } from '../../ExplorePage';
type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi };
@@ -120,7 +120,7 @@ export function setupExplore(options?: SetupOptions): {
locationService.partial(urlParams); locationService.partial(urlParams);
} }
const route = { component: Wrapper }; const route = { component: ExplorePage };
const { unmount, container } = render( const { unmount, container } = render(
<Provider store={storeState}> <Provider store={storeState}>

View File

@@ -216,7 +216,7 @@ export function getAppRoutes(): RouteDescriptor[] {
), ),
component: SafeDynamicImport(() => component: SafeDynamicImport(() =>
config.exploreEnabled config.exploreEnabled
? import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper') ? import(/* webpackChunkName: "explore" */ 'app/features/explore/EmptyStateWrapper')
: import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage') : import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage')
), ),
}, },