Datasource Onboarding: Prevent flickering of onboarding page after first load (#63360)

* Datasource Onboarding: Prevent flickering of onboarding page after first load

* add loading state to loadDatasources & refactor

* fix test

* avoid loading state when loading datasources on add

* fix test

* add explainer on why fetching datasources is needed
This commit is contained in:
Giordano Ricci
2023-02-24 11:48:30 +00:00
committed by GitHub
parent 200d2ad249
commit c136ad1f16
10 changed files with 45 additions and 40 deletions
@@ -1,24 +1,19 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { t } from 'app/core/internationalization'; 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 { EmptyStateNoDatasource } from 'app/features/datasources/components/EmptyStateNoDatasource';
import { loadDataSources } from 'app/features/datasources/state'; import { useLoadDataSources } from 'app/features/datasources/state';
import { useDispatch, useSelector } from 'app/types'; import { useSelector } from 'app/types';
import DashboardPage from './DashboardPage'; import DashboardPage from './DashboardPage';
export default function NewDashboardPage(props: GrafanaRouteComponentProps) { export default function NewDashboardPage(props: GrafanaRouteComponentProps) {
const dispatch = useDispatch(); const { isLoading } = useLoadDataSources();
useEffectOnce(() => {
dispatch(loadDataSources());
});
const { hasDatasource, loading } = useSelector((state) => ({ const { hasDatasource } = useSelector((state) => ({
hasDatasource: state.dataSources.dataSourcesCount > 0, hasDatasource: state.dataSources.dataSourcesCount > 0,
loading: !state.dataSources.hasFetched,
})); }));
const [createDashboard, setCreateDashboard] = useState(false); const [createDashboard, setCreateDashboard] = useState(false);
const showDashboardPage = hasDatasource || createDashboard || !config.featureToggles.datasourceOnboarding; const showDashboardPage = hasDatasource || createDashboard || !config.featureToggles.datasourceOnboarding;
@@ -28,7 +23,7 @@ export default function NewDashboardPage(props: GrafanaRouteComponentProps) {
) : ( ) : (
<EmptyStateNoDatasource <EmptyStateNoDatasource
onCTAClick={() => setCreateDashboard(true)} onCTAClick={() => setCreateDashboard(true)}
loading={loading} loading={isLoading}
title={t('datasource-onboarding.welcome', 'Welcome to Grafana dashboards!')} title={t('datasource-onboarding.welcome', 'Welcome to Grafana dashboards!')}
CTAText={t('datasource-onboarding.sampleData', 'Or set up a new dashboard with sample data')} CTAText={t('datasource-onboarding.sampleData', 'Or set up a new dashboard with sample data')}
navId="dashboards/browse" navId="dashboards/browse"
@@ -17,11 +17,10 @@ import { constructDataSourceExploreUrl } from '../utils';
import { DataSourcesListHeader } from './DataSourcesListHeader'; import { DataSourcesListHeader } from './DataSourcesListHeader';
export function DataSourcesList() { export function DataSourcesList() {
useLoadDataSources(); const { isLoading } = useLoadDataSources();
const dataSources = useSelector((state) => getDataSources(state.dataSources)); const dataSources = useSelector((state) => getDataSources(state.dataSources));
const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources)); const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources));
const hasFetched = useSelector(({ dataSources }: StoreState) => dataSources.hasFetched);
const hasCreateRights = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); const hasCreateRights = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
const hasWriteRights = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite); const hasWriteRights = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite);
const hasExploreRights = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore); const hasExploreRights = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
@@ -30,7 +29,7 @@ export function DataSourcesList() {
<DataSourcesListView <DataSourcesListView
dataSources={dataSources} dataSources={dataSources}
dataSourcesCount={dataSourcesCount} dataSourcesCount={dataSourcesCount}
isLoading={!hasFetched} isLoading={isLoading}
hasCreateRights={hasCreateRights} hasCreateRights={hasCreateRights}
hasWriteRights={hasWriteRights} hasWriteRights={hasWriteRights}
hasExploreRights={hasExploreRights} hasExploreRights={hasExploreRights}
@@ -23,7 +23,7 @@ export function NewDataSource() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const filteredDataSources = useSelector((s: StoreState) => getFilteredDataSourcePlugins(s.dataSources)); const filteredDataSources = useSelector((s: StoreState) => getFilteredDataSourcePlugins(s.dataSources));
const searchQuery = useSelector((s: StoreState) => s.dataSources.dataSourceTypeSearchQuery); const searchQuery = useSelector((s: StoreState) => s.dataSources.dataSourceTypeSearchQuery);
const isLoading = useSelector((s: StoreState) => s.dataSources.isLoadingDataSources); const isLoadingDatasourcePlugins = useSelector((s: StoreState) => s.dataSources.isLoadingDataSourcePlugins);
const dataSourceCategories = useSelector((s: StoreState) => s.dataSources.categories); const dataSourceCategories = useSelector((s: StoreState) => s.dataSources.categories);
const onAddDataSource = useAddDatasource(); const onAddDataSource = useAddDatasource();
const onSetSearchQuery = (q: string) => dispatch(setDataSourceTypeSearchQuery(q)); const onSetSearchQuery = (q: string) => dispatch(setDataSourceTypeSearchQuery(q));
@@ -33,7 +33,7 @@ export function NewDataSource() {
dataSources={filteredDataSources} dataSources={filteredDataSources}
dataSourceCategories={dataSourceCategories} dataSourceCategories={dataSourceCategories}
searchQuery={searchQuery} searchQuery={searchQuery}
isLoading={isLoading} isLoading={isLoadingDatasourcePlugins}
onAddDataSource={onAddDataSource} onAddDataSource={onAddDataSource}
onSetSearchQuery={onSetSearchQuery} onSetSearchQuery={onSetSearchQuery}
/> />
@@ -80,7 +80,7 @@ describe('<EditDataSourcePage>', () => {
dataSource: dataSource, dataSource: dataSource,
dataSourceMeta: dataSourceMeta, dataSourceMeta: dataSourceMeta,
layoutMode: LayoutModes.Grid, layoutMode: LayoutModes.Grid,
hasFetched: true, isLoadingDataSources: false,
}, },
navIndex: { navIndex: {
...navIndex, ...navIndex,
@@ -28,6 +28,7 @@ import {
dataSourceMetaLoaded, dataSourceMetaLoaded,
dataSourcePluginsLoad, dataSourcePluginsLoad,
dataSourcePluginsLoaded, dataSourcePluginsLoaded,
dataSourcesLoad,
dataSourcesLoaded, dataSourcesLoaded,
initDataSourceSettingsFailed, initDataSourceSettingsFailed,
initDataSourceSettingsSucceeded, initDataSourceSettingsSucceeded,
@@ -144,8 +145,9 @@ export const testDataSource = (
}; };
}; };
export function loadDataSources(): ThunkResult<void> { export function loadDataSources(): ThunkResult<Promise<void>> {
return async (dispatch) => { return async (dispatch) => {
dispatch(dataSourcesLoad());
const response = await api.getDataSources(); const response = await api.getDataSources();
dispatch(dataSourcesLoaded(response)); dispatch(dataSourcesLoaded(response));
}; };
@@ -193,9 +195,16 @@ export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult<
}; };
} }
export function addDataSource(plugin: DataSourcePluginMeta, editRoute = DATASOURCES_ROUTES.Edit): ThunkResult<void> { export function addDataSource(
plugin: DataSourcePluginMeta,
editRoute = DATASOURCES_ROUTES.Edit
): ThunkResult<Promise<void>> {
return async (dispatch, getStore) => { return async (dispatch, getStore) => {
await dispatch(loadDataSources()); // update the list of datasources first.
// We later use this list to check whether the name of the datasource
// being created is unuque or not and assign a new name to it if needed.
const response = await api.getDataSources();
dispatch(dataSourcesLoaded(response));
const dataSources = getStore().dataSources.dataSources; const dataSources = getStore().dataSources.dataSources;
const isFirstDataSource = dataSources.length === 0; const isFirstDataSource = dataSources.length === 0;
@@ -51,10 +51,14 @@ export const useTestDataSource = (uid: string) => {
export const useLoadDataSources = () => { export const useLoadDataSources = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isLoading = useSelector((state) => state.dataSources.isLoadingDataSources);
const dataSources = useSelector((state) => state.dataSources.dataSources);
useEffect(() => { useEffect(() => {
dispatch(loadDataSources()); dispatch(loadDataSources());
}, [dispatch]); }, [dispatch]);
return { isLoading, dataSources };
}; };
export const useLoadDataSource = (uid: string) => { export const useLoadDataSource = (uid: string) => {
@@ -47,7 +47,7 @@ describe('dataSourcesReducer', () => {
reducerTester<DataSourcesState>() reducerTester<DataSourcesState>()
.givenReducer(dataSourcesReducer, initialState) .givenReducer(dataSourcesReducer, initialState)
.whenActionIsDispatched(dataSourcesLoaded(dataSources)) .whenActionIsDispatched(dataSourcesLoaded(dataSources))
.thenStateShouldEqual({ ...initialState, hasFetched: true, dataSources, dataSourcesCount: 1 }); .thenStateShouldEqual({ ...initialState, isLoadingDataSources: false, dataSources, dataSourcesCount: 1 });
}); });
}); });
@@ -89,19 +89,19 @@ describe('dataSourcesReducer', () => {
reducerTester<DataSourcesState>() reducerTester<DataSourcesState>()
.givenReducer(dataSourcesReducer, state) .givenReducer(dataSourcesReducer, state)
.whenActionIsDispatched(dataSourcePluginsLoad()) .whenActionIsDispatched(dataSourcePluginsLoad())
.thenStateShouldEqual({ ...initialState, isLoadingDataSources: true }); .thenStateShouldEqual({ ...initialState, isLoadingDataSourcePlugins: true });
}); });
}); });
describe('when dataSourcePluginsLoaded is dispatched', () => { describe('when dataSourcePluginsLoaded is dispatched', () => {
it('then state should be correct', () => { it('then state should be correct', () => {
const dataSourceTypes = [mockPlugin()]; const dataSourceTypes = [mockPlugin()];
const state: DataSourcesState = { ...initialState, isLoadingDataSources: true }; const state: DataSourcesState = { ...initialState, isLoadingDataSourcePlugins: true };
reducerTester<DataSourcesState>() reducerTester<DataSourcesState>()
.givenReducer(dataSourcesReducer, state) .givenReducer(dataSourcesReducer, state)
.whenActionIsDispatched(dataSourcePluginsLoaded({ plugins: dataSourceTypes, categories: [] })) .whenActionIsDispatched(dataSourcePluginsLoaded({ plugins: dataSourceTypes, categories: [] }))
.thenStateShouldEqual({ ...initialState, plugins: dataSourceTypes, isLoadingDataSources: false }); .thenStateShouldEqual({ ...initialState, plugins: dataSourceTypes, isLoadingDataSourcePlugins: false });
}); });
}); });
@@ -17,13 +17,14 @@ export const initialState: DataSourcesState = {
searchQuery: '', searchQuery: '',
dataSourcesCount: 0, dataSourcesCount: 0,
dataSourceTypeSearchQuery: '', dataSourceTypeSearchQuery: '',
hasFetched: false,
isLoadingDataSources: false, isLoadingDataSources: false,
isLoadingDataSourcePlugins: false,
dataSourceMeta: {} as DataSourcePluginMeta, dataSourceMeta: {} as DataSourcePluginMeta,
isSortAscending: true, isSortAscending: true,
}; };
export const dataSourceLoaded = createAction<DataSourceSettings>('dataSources/dataSourceLoaded'); export const dataSourceLoaded = createAction<DataSourceSettings>('dataSources/dataSourceLoaded');
export const dataSourcesLoad = createAction<void>('dataSources/dataSourcesLoad');
export const dataSourcesLoaded = createAction<DataSourceSettings[]>('dataSources/dataSourcesLoaded'); export const dataSourcesLoaded = createAction<DataSourceSettings[]>('dataSources/dataSourcesLoaded');
export const dataSourceMetaLoaded = createAction<DataSourcePluginMeta>('dataSources/dataSourceMetaLoaded'); export const dataSourceMetaLoaded = createAction<DataSourcePluginMeta>('dataSources/dataSourceMetaLoaded');
export const dataSourcePluginsLoad = createAction('dataSources/dataSourcePluginsLoad'); export const dataSourcePluginsLoad = createAction('dataSources/dataSourcePluginsLoad');
@@ -43,10 +44,14 @@ export const setIsSortAscending = createAction<boolean>('dataSources/setIsSortAs
// the frozen state. // the frozen state.
// https://github.com/reduxjs/redux-toolkit/issues/242 // https://github.com/reduxjs/redux-toolkit/issues/242
export const dataSourcesReducer = (state: DataSourcesState = initialState, action: AnyAction): DataSourcesState => { export const dataSourcesReducer = (state: DataSourcesState = initialState, action: AnyAction): DataSourcesState => {
if (dataSourcesLoad.match(action)) {
return { ...state, isLoadingDataSources: true };
}
if (dataSourcesLoaded.match(action)) { if (dataSourcesLoaded.match(action)) {
return { return {
...state, ...state,
hasFetched: true, isLoadingDataSources: false,
dataSources: action.payload, dataSources: action.payload,
dataSourcesCount: action.payload.length, dataSourcesCount: action.payload.length,
}; };
@@ -65,7 +70,7 @@ export const dataSourcesReducer = (state: DataSourcesState = initialState, actio
} }
if (dataSourcePluginsLoad.match(action)) { if (dataSourcePluginsLoad.match(action)) {
return { ...state, plugins: [], isLoadingDataSources: true }; return { ...state, plugins: [], isLoadingDataSourcePlugins: true };
} }
if (dataSourcePluginsLoaded.match(action)) { if (dataSourcePluginsLoaded.match(action)) {
@@ -73,7 +78,7 @@ export const dataSourcesReducer = (state: DataSourcesState = initialState, actio
...state, ...state,
plugins: action.payload.plugins, plugins: action.payload.plugins,
categories: action.payload.categories, categories: action.payload.categories,
isLoadingDataSources: false, isLoadingDataSourcePlugins: false,
}; };
} }
@@ -1,26 +1,19 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { EmptyStateNoDatasource } from 'app/features/datasources/components/EmptyStateNoDatasource'; import { EmptyStateNoDatasource } from 'app/features/datasources/components/EmptyStateNoDatasource';
import { ExploreQueryParams, useDispatch, useSelector } from 'app/types'; import { ExploreQueryParams, useSelector } from 'app/types';
import { loadDataSources } from '../datasources/state'; import { useLoadDataSources } from '../datasources/state';
import { ExplorePage } from './ExplorePage'; import { ExplorePage } from './ExplorePage';
export default function EmptyStateWrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { export default function EmptyStateWrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
const dispatch = useDispatch(); const { isLoading } = useLoadDataSources();
useEffectOnce(() => {
if (config.featureToggles.datasourceOnboarding) {
dispatch(loadDataSources());
}
});
const { hasDatasource, loading } = useSelector((state) => ({ const { hasDatasource } = useSelector((state) => ({
hasDatasource: state.dataSources.dataSourcesCount > 0, hasDatasource: state.dataSources.dataSourcesCount > 0,
loading: !state.dataSources.hasFetched,
})); }));
const [showOnboarding, setShowOnboarding] = useState(config.featureToggles.datasourceOnboarding); const [showOnboarding, setShowOnboarding] = useState(config.featureToggles.datasourceOnboarding);
const showExplorePage = hasDatasource || !showOnboarding; const showExplorePage = hasDatasource || !showOnboarding;
@@ -30,7 +23,7 @@ export default function EmptyStateWrapper(props: GrafanaRouteComponentProps<{},
) : ( ) : (
<EmptyStateNoDatasource <EmptyStateNoDatasource
onCTAClick={() => setShowOnboarding(false)} onCTAClick={() => setShowOnboarding(false)}
loading={loading} loading={isLoading}
title="Welcome to Grafana Explore!" title="Welcome to Grafana Explore!"
CTAText="Or explore sample data" CTAText="Or explore sample data"
navId="explore" navId="explore"
+1 -1
View File
@@ -10,8 +10,8 @@ export interface DataSourcesState {
dataSourcesCount: number; dataSourcesCount: number;
dataSource: DataSourceSettings; dataSource: DataSourceSettings;
dataSourceMeta: DataSourcePluginMeta; dataSourceMeta: DataSourcePluginMeta;
hasFetched: boolean;
isLoadingDataSources: boolean; isLoadingDataSources: boolean;
isLoadingDataSourcePlugins: boolean;
plugins: DataSourcePluginMeta[]; plugins: DataSourcePluginMeta[];
categories: DataSourcePluginCategory[]; categories: DataSourcePluginCategory[];
isSortAscending: boolean; isSortAscending: boolean;