mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
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:
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user