Datasources: Refactor the list page (#51438)

* refactor(Data Sources): rename file to follow naming convention

* refactor: use react-redux hooks for interacting with the store

* tests: update data-sources list related test files

* refactor: extract datasource list page contents

* refactor: pass dataSources to the DataSourcesList as a prop

* refactor: use proper typing for navIndex mocks
This commit is contained in:
Levente Balogh 2022-07-07 11:15:34 +02:00 committed by GitHub
parent 689639cdb0
commit 99de3313f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 15158 additions and 10415 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import { locationService } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore';
import DataConnectionsPage from './DataConnectionsPage';
import navIndex from './__mocks__/store.navIndex.mock';
import { navIndex } from './__mocks__/store.navIndex.mock';
import { ROUTE_BASE_ID, ROUTES } from './constants';
const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {

View File

@ -1,8 +1,10 @@
export default {
import { NavIndex, NavSection } from '@grafana/data';
export const navIndex: NavIndex = {
dashboards: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -74,7 +76,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -147,7 +149,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -220,7 +222,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -293,7 +295,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -366,7 +368,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -436,7 +438,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -512,7 +514,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -588,7 +590,7 @@ export default {
parentItem: {
id: 'dashboards',
text: 'Dashboards',
section: 'core',
section: NavSection.Core,
subTitle: 'Manage dashboards and folders',
icon: 'apps',
url: '/dashboards',
@ -661,7 +663,7 @@ export default {
explore: {
id: 'explore',
text: 'Explore',
section: 'core',
section: NavSection.Core,
subTitle: 'Explore your data',
icon: 'compass',
url: '/explore',
@ -670,7 +672,7 @@ export default {
alerting: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -737,7 +739,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -805,7 +807,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -873,7 +875,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -941,7 +943,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -1009,7 +1011,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -1077,7 +1079,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -1148,7 +1150,7 @@ export default {
parentItem: {
id: 'alerting',
text: 'Alerting',
section: 'core',
section: NavSection.Core,
subTitle: 'Alert rules and notifications',
icon: 'bell',
url: '/alerting/list',
@ -1211,7 +1213,7 @@ export default {
'data-connections': {
id: 'data-connections',
text: 'Data Connections',
section: 'core',
section: NavSection.Core,
icon: 'link',
url: '/data-connections',
sortWeight: -1500,
@ -1259,7 +1261,7 @@ export default {
parentItem: {
id: 'data-connections',
text: 'Data Connections',
section: 'core',
section: NavSection.Core,
icon: 'link',
url: '/data-connections',
sortWeight: -1500,
@ -1304,7 +1306,7 @@ export default {
parentItem: {
id: 'data-connections',
text: 'Data Connections',
section: 'core',
section: NavSection.Core,
icon: 'link',
url: '/data-connections',
sortWeight: -1500,
@ -1349,7 +1351,7 @@ export default {
parentItem: {
id: 'data-connections',
text: 'Data Connections',
section: 'core',
section: NavSection.Core,
icon: 'link',
url: '/data-connections',
sortWeight: -1500,
@ -1394,7 +1396,7 @@ export default {
parentItem: {
id: 'data-connections',
text: 'Data Connections',
section: 'core',
section: NavSection.Core,
icon: 'link',
url: '/data-connections',
sortWeight: -1500,
@ -1433,7 +1435,7 @@ export default {
'plugin-page-basic-app': {
id: 'plugin-page-basic-app',
text: 'Basic App',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/basic-app/img/logo.svg',
url: '/a/basic-app/one',
sortWeight: -1400,
@ -1467,7 +1469,7 @@ export default {
parentItem: {
id: 'plugin-page-grafana-synthetic-monitoring-app',
text: 'Synthetic Monitoring',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg',
url: '/a/grafana-synthetic-monitoring-app/home',
sortWeight: -1400,
@ -1502,7 +1504,7 @@ export default {
'plugin-page-cloudflare-app': {
id: 'plugin-page-cloudflare-app',
text: 'Cloudflare Grafana App',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/cloudflare-app/img/cf_icon.png',
sortWeight: -1400,
children: [
@ -1519,7 +1521,7 @@ export default {
'plugin-page-grafana-easystart-app': {
id: 'plugin-page-grafana-easystart-app',
text: 'Integrations and Connections',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/grafana-easystart-app/img/logo.svg',
url: '/a/grafana-easystart-app',
sortWeight: -1400,
@ -1527,7 +1529,7 @@ export default {
'plugin-page-redis-explorer-app': {
id: 'plugin-page-redis-explorer-app',
text: 'Redis Explorer',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/redis-explorer-app/img/logo.svg',
url: '/a/redis-explorer-app/',
sortWeight: -1400,
@ -1567,7 +1569,7 @@ export default {
'plugin-page-grafana-synthetic-monitoring-app': {
id: 'plugin-page-grafana-synthetic-monitoring-app',
text: 'Synthetic Monitoring',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg',
url: '/a/grafana-synthetic-monitoring-app/home',
sortWeight: -1400,
@ -1601,7 +1603,7 @@ export default {
'plugin-page-grafana-k6-app': {
id: 'plugin-page-grafana-k6-app',
text: 'k6 Cloud App',
section: 'plugin',
section: NavSection.Plugin,
img: 'public/plugins/grafana-k6-app/img/logo.svg',
url: '/a/grafana-k6-app',
sortWeight: -1400,
@ -1609,7 +1611,7 @@ export default {
cfg: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -1668,7 +1670,7 @@ export default {
parentItem: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -1728,7 +1730,7 @@ export default {
parentItem: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -1788,7 +1790,7 @@ export default {
parentItem: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -1848,7 +1850,7 @@ export default {
parentItem: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -1908,7 +1910,7 @@ export default {
parentItem: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -1968,7 +1970,7 @@ export default {
parentItem: {
id: 'cfg',
text: 'Configuration',
section: 'config',
section: NavSection.Config,
subTitle: 'Organization: Main Org.',
icon: 'cog',
url: '/datasources',
@ -2022,7 +2024,7 @@ export default {
admin: {
id: 'admin',
text: 'Server Admin',
section: 'config',
section: NavSection.Config,
subTitle: 'Manage all users and orgs',
icon: 'shield',
url: '/admin/users',
@ -2069,7 +2071,7 @@ export default {
parentItem: {
id: 'admin',
text: 'Server Admin',
section: 'config',
section: NavSection.Config,
subTitle: 'Manage all users and orgs',
icon: 'shield',
url: '/admin/users',
@ -2117,7 +2119,7 @@ export default {
parentItem: {
id: 'admin',
text: 'Server Admin',
section: 'config',
section: NavSection.Config,
subTitle: 'Manage all users and orgs',
icon: 'shield',
url: '/admin/users',
@ -2165,7 +2167,7 @@ export default {
parentItem: {
id: 'admin',
text: 'Server Admin',
section: 'config',
section: NavSection.Config,
subTitle: 'Manage all users and orgs',
icon: 'shield',
url: '/admin/users',
@ -2213,7 +2215,7 @@ export default {
parentItem: {
id: 'admin',
text: 'Server Admin',
section: 'config',
section: NavSection.Config,
subTitle: 'Manage all users and orgs',
icon: 'shield',
url: '/admin/users',
@ -2261,7 +2263,7 @@ export default {
parentItem: {
id: 'admin',
text: 'Server Admin',
section: 'config',
section: NavSection.Config,
subTitle: 'Manage all users and orgs',
icon: 'shield',
url: '/admin/users',
@ -2304,7 +2306,7 @@ export default {
profile: {
id: 'profile',
text: 'admin',
section: 'config',
section: NavSection.Config,
img: '/avatar/46d229b033af06a191ff2267bca9ae56',
url: '/profile',
sortWeight: -1100,
@ -2345,7 +2347,7 @@ export default {
parentItem: {
id: 'profile',
text: 'admin',
section: 'config',
section: NavSection.Config,
img: '/avatar/46d229b033af06a191ff2267bca9ae56',
url: '/profile',
sortWeight: -1100,
@ -2387,7 +2389,7 @@ export default {
parentItem: {
id: 'profile',
text: 'admin',
section: 'config',
section: NavSection.Config,
img: '/avatar/46d229b033af06a191ff2267bca9ae56',
url: '/profile',
sortWeight: -1100,
@ -2429,7 +2431,7 @@ export default {
parentItem: {
id: 'profile',
text: 'admin',
section: 'config',
section: NavSection.Config,
img: '/avatar/46d229b033af06a191ff2267bca9ae56',
url: '/profile',
sortWeight: -1100,
@ -2473,7 +2475,7 @@ export default {
parentItem: {
id: 'profile',
text: 'admin',
section: 'config',
section: NavSection.Config,
img: '/avatar/46d229b033af06a191ff2267bca9ae56',
url: '/profile',
sortWeight: -1100,
@ -2510,7 +2512,7 @@ export default {
help: {
id: 'help',
text: 'Help',
section: 'config',
section: NavSection.Config,
subTitle: 'Grafana v9.0.0-pre (abb5c6109a)',
icon: 'question-circle',
url: '#',

View File

@ -1,18 +1,30 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { LayoutModes } from '@grafana/data';
import { configureStore } from 'app/store/configureStore';
import { DataSourcesState } from 'app/types';
import DataSourcesList from './DataSourcesList';
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
import { initialState } from './state/reducers';
const setup = () => {
const props = {
dataSources: getMockDataSources(3),
layoutMode: LayoutModes.Grid,
};
const setup = (stateOverride?: Partial<DataSourcesState>) => {
const store = configureStore({
dataSources: {
...initialState,
dataSources: getMockDataSources(3),
layoutMode: LayoutModes.Grid,
...stateOverride,
},
});
return render(<DataSourcesList {...props} />);
return render(
<Provider store={store}>
<DataSourcesList dataSources={getMockDataSources(3)} />
</Provider>
);
};
describe('DataSourcesList', () => {

View File

@ -1,17 +1,16 @@
// Libraries
import { css } from '@emotion/css';
import React, { FC } from 'react';
import React from 'react';
// Types
import { DataSourceSettings, LayoutMode } from '@grafana/data';
import { DataSourceSettings } from '@grafana/data';
import { Card, Tag, useStyles } from '@grafana/ui';
export interface Props {
export type Props = {
dataSources: DataSourceSettings[];
layoutMode: LayoutMode;
}
};
export const DataSourcesList: FC<Props> = ({ dataSources, layoutMode }) => {
export const DataSourcesList = ({ dataSources }: Props) => {
const styles = useStyles(getStyles);
return (

View File

@ -0,0 +1,26 @@
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { contextSrv } from 'app/core/core';
import { AccessControlAction, StoreState } from 'app/types';
import { setDataSourcesSearchQuery } from './state/reducers';
import { getDataSourcesSearchQuery } from './state/selectors';
export const DataSourcesListHeader = () => {
const dispatch = useDispatch();
const setSearchQuery = useCallback((q: string) => dispatch(setDataSourcesSearchQuery(q)), [dispatch]);
const searchQuery = useSelector(({ dataSources }: StoreState) => getDataSourcesSearchQuery(dataSources));
const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
const linkButton = {
href: 'datasources/new',
title: 'Add data source',
disabled: !canCreateDataSource,
};
return (
<PageActionBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} linkButton={linkButton} key="action-bar" />
);
};

View File

@ -1,11 +1,15 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { DataSourceSettings, NavModel, LayoutModes } from '@grafana/data';
import { DataSourceSettings, LayoutModes } from '@grafana/data';
import { configureStore } from 'app/store/configureStore';
import { DataSourcesState } from 'app/types';
import { DataSourcesListPage, Props } from './DataSourcesListPage';
import { DataSourcesListPage } from './DataSourcesListPage';
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/reducers';
import navIndex from './__mocks__/store.navIndex.mock';
import { initialState } from './state/reducers';
jest.mock('app/core/core', () => {
return {
@ -15,29 +19,30 @@ jest.mock('app/core/core', () => {
};
});
const setup = (propOverrides?: object) => {
const props: Props = {
dataSources: [] as DataSourceSettings[],
layoutMode: LayoutModes.Grid,
loadDataSources: jest.fn(),
navModel: {
main: {
text: 'Configuration',
},
node: {
text: 'Data Sources',
},
} as NavModel,
dataSourcesCount: 0,
searchQuery: '',
setDataSourcesSearchQuery,
setDataSourcesLayoutMode,
hasFetched: false,
};
const getMock = jest.fn().mockResolvedValue([]);
Object.assign(props, propOverrides);
jest.mock('app/core/services/backend_srv', () => ({
...jest.requireActual('app/core/services/backend_srv'),
getBackendSrv: () => ({ get: getMock }),
}));
return render(<DataSourcesListPage {...props} />);
const setup = (stateOverride?: Partial<DataSourcesState>) => {
const store = configureStore({
dataSources: {
...initialState,
dataSources: [] as DataSourceSettings[],
layoutMode: LayoutModes.Grid,
hasFetched: false,
...stateOverride,
},
navIndex,
});
return render(
<Provider store={store}>
<DataSourcesListPage />
</Provider>
);
};
describe('Render', () => {

View File

@ -1,98 +1,22 @@
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import React from 'react';
import { useSelector } from 'react-redux';
import { IconName } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState, AccessControlAction } from 'app/types';
import { StoreState } from 'app/types';
import DataSourcesList from './DataSourcesList';
import { loadDataSources } from './state/actions';
import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/reducers';
import {
getDataSources,
getDataSourcesCount,
getDataSourcesLayoutMode,
getDataSourcesSearchQuery,
} from './state/selectors';
import { DataSourcesListPageContent } from './DataSourcesListPageContent';
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'datasources'),
dataSources: getDataSources(state.dataSources),
layoutMode: getDataSourcesLayoutMode(state.dataSources),
dataSourcesCount: getDataSourcesCount(state.dataSources),
searchQuery: getDataSourcesSearchQuery(state.dataSources),
hasFetched: state.dataSources.hasFetched,
};
}
export const DataSourcesListPage = () => {
const navModel = useSelector(({ navIndex }: StoreState) => getNavModel(navIndex, 'datasources'));
const mapDispatchToProps = {
loadDataSources,
setDataSourcesSearchQuery,
setDataSourcesLayoutMode,
return (
<Page navModel={navModel}>
<Page.Contents>
<DataSourcesListPageContent />
</Page.Contents>
</Page>
);
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export type Props = ConnectedProps<typeof connector>;
const emptyListModel = {
title: 'No data sources defined',
buttonIcon: 'database' as IconName,
buttonLink: 'datasources/new',
buttonTitle: 'Add data source',
proTip: 'You can also define data sources through configuration files.',
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
proTipLinkTitle: 'Learn more',
proTipTarget: '_blank',
};
export class DataSourcesListPage extends PureComponent<Props> {
componentDidMount() {
this.props.loadDataSources();
}
render() {
const { dataSources, dataSourcesCount, navModel, layoutMode, searchQuery, setDataSourcesSearchQuery, hasFetched } =
this.props;
const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
const linkButton = {
href: 'datasources/new',
title: 'Add data source',
disabled: !canCreateDataSource,
};
const emptyList = {
...emptyListModel,
buttonDisabled: !canCreateDataSource,
};
return (
<Page navModel={navModel}>
<Page.Contents isLoading={!hasFetched}>
<>
{hasFetched && dataSourcesCount === 0 && <EmptyListCTA {...emptyList} />}
{hasFetched &&
dataSourcesCount > 0 && [
<PageActionBar
searchQuery={searchQuery}
setSearchQuery={(query) => setDataSourcesSearchQuery(query)}
linkButton={linkButton}
key="action-bar"
/>,
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
]}
</>
</Page.Contents>
</Page>
);
}
}
export default connector(DataSourcesListPage);
export default DataSourcesListPage;

View File

@ -0,0 +1,58 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IconName } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { contextSrv } from 'app/core/core';
import { StoreState, AccessControlAction } from 'app/types';
import DataSourcesList from './DataSourcesList';
import { DataSourcesListHeader } from './DataSourcesListHeader';
import { loadDataSources } from './state/actions';
import { getDataSourcesCount, getDataSources } from './state/selectors';
const buttonIcon: IconName = 'database';
const emptyListModel = {
title: 'No data sources defined',
buttonIcon,
buttonLink: 'datasources/new',
buttonTitle: 'Add data source',
proTip: 'You can also define data sources through configuration files.',
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
proTipLinkTitle: 'Learn more',
proTipTarget: '_blank',
};
export const DataSourcesListPageContent = () => {
const dispatch = useDispatch();
const dataSources = useSelector((state: StoreState) => getDataSources(state.dataSources));
const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources));
const hasFetched = useSelector(({ dataSources }: StoreState) => dataSources.hasFetched);
const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
const emptyList = {
...emptyListModel,
buttonDisabled: !canCreateDataSource,
};
useEffect(() => {
if (!hasFetched) {
dispatch(loadDataSources());
}
}, [dispatch, hasFetched]);
if (!hasFetched) {
return <PageLoader />;
}
if (dataSourcesCount === 0) {
return <EmptyListCTA {...emptyList} />;
}
return (
<>
<DataSourcesListHeader />
<DataSourcesList dataSources={dataSources} />
</>
);
};

File diff suppressed because it is too large Load Diff