diff --git a/.betterer.results b/.betterer.results index 5b994c6eb51..90befadff48 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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.", "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": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/public/app/features/dashboard/containers/NewDashboardPage.tsx b/public/app/features/dashboard/containers/NewDashboardPage.tsx index 2e1575d8e3b..934728a73e9 100644 --- a/public/app/features/dashboard/containers/NewDashboardPage.tsx +++ b/public/app/features/dashboard/containers/NewDashboardPage.tsx @@ -2,12 +2,13 @@ import React, { useState } from 'react'; import { useEffectOnce } from 'react-use'; 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 { loadDataSources } from 'app/features/datasources/state'; import { useDispatch, useSelector } from 'app/types'; import DashboardPage from './DashboardPage'; -import { DatasourceOnboarding } from './DatasourceOnboarding'; export default function NewDashboardPage(props: GrafanaRouteComponentProps) { const dispatch = useDispatch(); @@ -25,6 +26,13 @@ export default function NewDashboardPage(props: GrafanaRouteComponentProps) { return showDashboardPage ? ( ) : ( - setCreateDashboard(true)} loading={loading} /> + 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' }} + /> ); } diff --git a/public/app/features/dashboard/containers/DatasourceOnboarding.tsx b/public/app/features/datasources/components/EmptyStateNoDatasource.tsx similarity index 90% rename from public/app/features/dashboard/containers/DatasourceOnboarding.tsx rename to public/app/features/datasources/components/EmptyStateNoDatasource.tsx index 36abb698e6c..b0a53fdcab4 100644 --- a/public/app/features/dashboard/containers/DatasourceOnboarding.tsx +++ b/public/app/features/datasources/components/EmptyStateNoDatasource.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { useAsync } from 'react-use'; import { DataSourcePluginMeta, GrafanaTheme2, PageLayoutType } from '@grafana/data'; @@ -21,13 +21,14 @@ const topDatasources = [ 'grafana-azure-monitor-datasource', ]; -export function DatasourceOnboarding({ - onNewDashboard, - loading = false, -}: { - onNewDashboard?: () => void; +interface Props extends Pick, '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 { value: datasources, loading: loadingDatasources } = useAsync(async () => { const datasourceMeta: DataSourcePluginMeta[] = await getBackendSrv().get('/api/plugins', { @@ -50,13 +51,9 @@ export function DatasourceOnboarding({ } return ( - +
-

{t('datasource-onboarding.welcome', 'Welcome to Grafana dashboards!')}

+

{title}

{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.')}

)} -
@@ -193,7 +190,7 @@ function getStyles(theme: GrafanaTheme2) { textDecoration: 'underline', textUnderlinePosition: 'under', }), - createNew: css({ + ctaButton: css({ display: 'flex', alignItems: 'center', gap: theme.spacing(1), diff --git a/public/app/features/explore/EmptyStateWrapper.tsx b/public/app/features/explore/EmptyStateWrapper.tsx new file mode 100644 index 00000000000..e0c10d08e65 --- /dev/null +++ b/public/app/features/explore/EmptyStateWrapper.tsx @@ -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 ? ( + + ) : ( + setShowOnboarding(false)} + loading={loading} + title="Welcome to Grafana Explore!" + CTAText="Or explore sample data" + navId="explore" + /> + ); +} diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/ExplorePage.test.tsx similarity index 98% rename from public/app/features/explore/Wrapper.test.tsx rename to public/app/features/explore/ExplorePage.test.tsx index 9a5f8be2512..10e1875e630 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/ExplorePage.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; 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 { locationService, config } from '@grafana/runtime'; @@ -27,13 +28,13 @@ jest.mock('app/core/core', () => { jest.mock('react-virtualized-auto-sizer', () => { return { __esModule: true, - default(props: any) { - return
{props.children({ width: 1000 })}
; + default(props: ComponentProps) { + return
{props.children({ width: 1000, height: 1000 })}
; }, }; }); -describe('Wrapper', () => { +describe('ExplorePage', () => { afterEach(() => { tearDown(); }); diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/ExplorePage.tsx similarity index 98% rename from public/app/features/explore/Wrapper.tsx rename to public/app/features/explore/ExplorePage.tsx index 211f0805c75..c5b31737eb0 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -31,7 +31,7 @@ const styles = { `, }; -function Wrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { +export function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { useExplorePageTitle(); const dispatch = useDispatch(); const queryParams = props.queryParams; @@ -173,5 +173,3 @@ const useExplorePageTitle = () => { document.title = `${navModel.main.text} - ${datasources.join(' | ')} - ${Branding.AppTitle}`; }; - -export default Wrapper; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 3ff442a8f7e..1c163dcbeda 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -29,7 +29,7 @@ import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource'; import { LokiQuery } from '../../../../plugins/datasource/loki/types'; import { ExploreId } from '../../../../types'; import { initialUserState } from '../../../profile/state/reducers'; -import Wrapper from '../../Wrapper'; +import { ExplorePage } from '../../ExplorePage'; type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; @@ -120,7 +120,7 @@ export function setupExplore(options?: SetupOptions): { locationService.partial(urlParams); } - const route = { component: Wrapper }; + const route = { component: ExplorePage }; const { unmount, container } = render( diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 69a35ab937e..cd224d9bb75 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -216,7 +216,7 @@ export function getAppRoutes(): RouteDescriptor[] { ), component: SafeDynamicImport(() => 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') ), },