Datasources: Cleanup unused code from datasources (#73360)

* remove unused datasource pages

* move useDataSourceSettingsNav from datasources to connections

without any modification in the code yet

The reason for this is that its only usage is in this file, so we are
going to merge it with the other useDataSourceSettingsNav hook in the
next commit.

* merge two useDataSourceSettingsNav's

and rename consts and remove duplicated calls

* simplify useDataSourceSettingsNav
This commit is contained in:
mikkancso
2023-08-17 14:37:39 +02:00
committed by GitHub
parent 7ee13c4c8c
commit 6bc7c70c02
9 changed files with 61 additions and 536 deletions

View File

@@ -1,26 +1,70 @@
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { import { NavModel, NavModelItem } from '@grafana/data';
useDataSource, import { getDataSourceSrv } from '@grafana/runtime';
useDataSourceSettingsNav as useDataSourceSettingsNavOriginal, import { getNavModel } from 'app/core/selectors/navModel';
} from 'app/features/datasources/state/hooks'; import { useDataSource, useDataSourceSettings } from 'app/features/datasources/state/hooks';
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from 'app/features/datasources/state/navModel';
import { useGetSingle } from 'app/features/plugins/admin/state/hooks'; import { useGetSingle } from 'app/features/plugins/admin/state/hooks';
import { useSelector } from 'app/types';
// We are extending the original useDataSourceSettingsNav in the following ways: export function useDataSourceSettingsNav(pageIdParam?: string) {
// - changing the URL of the nav items to point to Connections
// - setting the parent nav item
export function useDataSourceSettingsNav(pageId?: string) {
const { uid } = useParams<{ uid: string }>(); const { uid } = useParams<{ uid: string }>();
const location = useLocation(); const location = useLocation();
const datasource = useDataSource(uid); const datasource = useDataSource(uid);
const datasourcePlugin = useGetSingle(datasource.type); const datasourcePlugin = useGetSingle(datasource.type);
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const nav = useDataSourceSettingsNavOriginal(uid, pageId || params.get('page')); const pageId = pageIdParam || params.get('page');
const pageNav = {
...nav.main, const { plugin, loadError, loading } = useDataSourceSettings();
const dsi = getDataSourceSrv()?.getInstanceSettings(uid);
const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false);
const isAlertManagerDatasource = dsi?.type === 'alertmanager';
const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource;
const navIndex = useSelector((state) => state.navIndex);
const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-settings-${uid}`;
let pageNav: NavModel = {
node: {
text: 'Data Source Nav Node',
},
main: {
text: 'Data Source Nav Node',
},
};
if (loadError) {
const node: NavModelItem = {
text: loadError,
subTitle: 'Data Source Error',
icon: 'exclamation-triangle',
};
pageNav = {
node: node,
main: node,
};
}
if (loading || !plugin) {
pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav('settings'));
}
if (plugin) {
pageNav = getNavModel(
navIndex,
navIndexId,
getDataSourceNav(buildNavModel(datasource, plugin), pageId || 'settings')
);
}
const connectionsPageNav = {
...pageNav.main,
dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '',
active: true,
text: datasource.name, text: datasource.name,
subTitle: `Type: ${datasourcePlugin?.name}`, subTitle: `Type: ${datasourcePlugin?.name}`,
children: (nav.main.children || []).map((navModelItem) => ({ children: (pageNav.main.children || []).map((navModelItem) => ({
...navModelItem, ...navModelItem,
url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'), url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'),
})), })),
@@ -28,7 +72,9 @@ export function useDataSourceSettingsNav(pageId?: string) {
return { return {
navId: 'connections-datasources', navId: 'connections-datasources',
pageNav, pageNav: connectionsPageNav,
dataSourceHeader: nav.dataSourceHeader, dataSourceHeader: {
alertingSupported,
},
}; };
} }

View File

@@ -1,82 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Store } from 'redux';
import { TestProvider } from 'test/helpers/TestProvider';
import { setAngularLoader } from '@grafana/runtime';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { configureStore } from 'app/store/configureStore';
import { navIndex, getMockDataSource } from '../__mocks__';
import * as api from '../api';
import { initialState as dataSourcesInitialState } from '../state';
import DataSourceDashboardsPage from './DataSourceDashboardsPage';
jest.mock('../api');
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
hasPermission: () => true,
hasPermissionInMetadata: () => true,
},
}));
const setup = (uid: string, store: Store) =>
render(
<TestProvider store={store}>
<DataSourceDashboardsPage
{...getRouteComponentProps({
// @ts-ignore
match: {
params: {
uid,
},
},
})}
/>
</TestProvider>
);
describe('<DataSourceDashboardsPage>', () => {
const uid = 'foo';
const dataSourceName = 'My DataSource';
const dataSource = getMockDataSource<{}>({ uid, name: dataSourceName });
let store: Store;
beforeAll(() => {
setAngularLoader({
load: () => ({
destroy: jest.fn(),
digest: jest.fn(),
getScope: () => ({ $watch: () => {} }),
}),
});
});
beforeEach(() => {
// @ts-ignore
api.getDataSourceByIdOrUid = jest.fn().mockResolvedValue(dataSource);
store = configureStore({
dataSources: {
...dataSourcesInitialState,
dataSource: dataSource,
},
navIndex: {
...navIndex,
[`datasource-dashboards-${uid}`]: {
id: `datasource-dashboards-${uid}`,
text: dataSourceName,
icon: 'list-ul',
url: `/datasources/edit/${uid}/dashboards`,
},
},
});
});
it('should render the dashboards page without an issue', async () => {
setup(uid, store);
expect(await screen.findByText(dataSourceName)).toBeVisible();
});
});

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { DataSourceDashboards } from '../components/DataSourceDashboards';
import { useDataSourceSettingsNav } from '../state';
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
export function DataSourceDashboardsPage(props: Props) {
const uid = props.match.params.uid;
const nav = useDataSourceSettingsNav(uid, 'dashboards');
return (
<Page navId="datasources" pageNav={nav.main}>
<Page.Contents>
<DataSourceDashboards uid={uid} />
</Page.Contents>
</Page>
);
}
export default DataSourceDashboardsPage;

View File

@@ -1,143 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { LayoutModes } from '@grafana/data';
import { contextSrv } from 'app/core/services/context_srv';
import { navIndex, getMockDataSources } from '../__mocks__';
import { getDataSources } from '../api';
import { initialState } from '../state';
import { DataSourcesListPage } from './DataSourcesListPage';
jest.mock('app/core/services/context_srv');
jest.mock('../api', () => ({
...jest.requireActual('../api'),
getDataSources: jest.fn().mockResolvedValue([]),
}));
const getDataSourcesMock = getDataSources as jest.Mock;
const setup = (options: { isSortAscending: boolean }) => {
const storeState = {
dataSources: {
...initialState,
layoutMode: LayoutModes.Grid,
isSortAscending: options.isSortAscending,
},
navIndex,
};
return render(
<TestProvider storeState={storeState}>
<DataSourcesListPage />
</TestProvider>
);
};
describe('Render', () => {
beforeEach(() => {
(contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true);
});
it('should render component', async () => {
setup({ isSortAscending: true });
expect(await screen.findByRole('heading', { name: 'Data sources' })).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'Add data source' })).toBeInTheDocument();
// Should not show button in page header when the list is empty
expect(await screen.queryByRole('link', { name: 'Add new data source' })).toBeNull();
});
describe('when user has no permissions', () => {
beforeEach(() => {
(contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(false);
});
it('should disable the "Add data source" button if user has no permissions', async () => {
setup({ isSortAscending: true });
expect(await screen.findByRole('heading', { name: 'Data sources' })).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'Add data source' })).toHaveStyle('pointer-events: none');
});
it('should not show the Explore button', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(3));
setup({ isSortAscending: true });
expect(await screen.findAllByRole('link', { name: /Build a dashboard/i })).toHaveLength(3);
expect(screen.queryAllByRole('link', { name: 'Explore' })).toHaveLength(0);
});
it('should not link cards to edit pages', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(1));
setup({ isSortAscending: true });
expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
expect(await screen.queryByRole('link', { name: 'dataSource-0' })).toBeNull();
});
});
it('should show the Explore button', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(3));
setup({ isSortAscending: true });
expect(await screen.findAllByRole('link', { name: /Build a dashboard/i })).toHaveLength(3);
expect(screen.queryAllByRole('link', { name: 'Explore' })).toHaveLength(3);
});
it('should link cards to edit pages', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(1));
setup({ isSortAscending: true });
expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'dataSource-0' })).toBeInTheDocument();
});
it('should render action bar and datasources', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(5));
setup({ isSortAscending: true });
expect(await screen.findByPlaceholderText('Search by name or type')).toBeInTheDocument();
expect(await screen.findByRole('combobox', { name: 'Sort' })).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: 'dataSource-1' })).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: 'dataSource-2' })).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: 'dataSource-3' })).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: 'dataSource-4' })).toBeInTheDocument();
expect(await screen.findAllByRole('img')).toHaveLength(5);
// Should show button in page header when the list is not empty
expect(await screen.findByRole('link', { name: 'Add new data source' })).toBeInTheDocument();
});
describe('should render elements in sort order', () => {
it('ascending', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(5));
setup({ isSortAscending: true });
expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
const dataSourceItems = await screen.findAllByRole('heading');
expect(dataSourceItems).toHaveLength(6);
expect(dataSourceItems[0]).toHaveTextContent('Data sources');
expect(dataSourceItems[1]).toHaveTextContent('dataSource-0');
expect(dataSourceItems[2]).toHaveTextContent('dataSource-1');
});
it('descending', async () => {
getDataSourcesMock.mockResolvedValue(getMockDataSources(5));
setup({ isSortAscending: false });
expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
const dataSourceItems = await screen.findAllByRole('heading');
expect(dataSourceItems).toHaveLength(6);
expect(dataSourceItems[0]).toHaveTextContent('Data sources');
expect(dataSourceItems[1]).toHaveTextContent('dataSource-4');
expect(dataSourceItems[2]).toHaveTextContent('dataSource-3');
});
});
});

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
import { ConnectionsRedirectNotice } from 'app/features/connections/components/ConnectionsRedirectNotice';
import { StoreState, useSelector } from 'app/types';
import { DataSourceAddButton } from '../components/DataSourceAddButton';
import { DataSourcesList } from '../components/DataSourcesList';
import { getDataSourcesCount } from '../state';
export function DataSourcesListPage() {
const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources));
const actions = dataSourcesCount > 0 ? <DataSourceAddButton /> : undefined;
return (
<Page navId="datasources" actions={actions}>
<Page.Contents>
<ConnectionsRedirectNotice />
<DataSourcesList />
</Page.Contents>
</Page>
);
}
export default DataSourcesListPage;

View File

@@ -1,123 +0,0 @@
import { screen, render, waitFor } from '@testing-library/react';
import React from 'react';
import { Store } from 'redux';
import { TestProvider } from 'test/helpers/TestProvider';
import { LayoutModes } from '@grafana/data';
import { setAngularLoader, setPluginExtensionGetter } from '@grafana/runtime';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { configureStore } from 'app/store/configureStore';
import { navIndex, getMockDataSource, getMockDataSourceMeta, getMockDataSourceSettingsState } from '../__mocks__';
import * as api from '../api';
import { initialState } from '../state';
import { EditDataSourcePage } from './EditDataSourcePage';
jest.mock('../api');
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
hasPermission: () => true,
hasPermissionInMetadata: () => true,
},
}));
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getDataSourceSrv: jest.fn(() => ({
getInstanceSettings: (uid: string) => ({
uid,
meta: getMockDataSourceMeta(),
}),
})),
};
});
const setup = (uid: string, store: Store) =>
render(
<TestProvider store={store}>
<EditDataSourcePage
{...getRouteComponentProps({
// @ts-ignore
match: {
params: {
uid,
},
},
})}
/>
</TestProvider>
);
describe('<EditDataSourcePage>', () => {
const uid = 'foo';
const name = 'My DataSource';
const dataSource = getMockDataSource<{}>({ uid, name });
const dataSourceMeta = getMockDataSourceMeta();
const dataSourceSettings = getMockDataSourceSettingsState();
let store: Store;
beforeAll(() => {
setAngularLoader({
load: () => ({
destroy: jest.fn(),
digest: jest.fn(),
getScope: () => ({ $watch: () => {} }),
}),
});
});
beforeEach(() => {
// @ts-ignore
api.getDataSourceByIdOrUid = jest.fn().mockResolvedValue(dataSource);
setPluginExtensionGetter(jest.fn().mockReturnValue({ extensions: [] }));
store = configureStore({
dataSourceSettings,
dataSources: {
...initialState,
dataSources: [dataSource],
dataSource: dataSource,
dataSourceMeta: dataSourceMeta,
layoutMode: LayoutModes.Grid,
isLoadingDataSources: false,
},
navIndex: {
...navIndex,
[`datasource-settings-${uid}`]: {
id: `datasource-settings-${uid}`,
text: name,
icon: 'list-ul',
url: `/datasources/edit/${uid}`,
},
},
});
});
it('should render the edit page without an issue', async () => {
setup(uid, store);
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
// Title
expect(screen.queryByText(name)).toBeVisible();
// Buttons
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
// wait for the rest of the async processes to finish
expect(await screen.findByText(name)).toBeVisible();
});
it('should show updated action buttons when topnav is on', async () => {
setup(uid, store);
await waitFor(() => {
// Buttons
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
expect(screen.queryByRole('link', { name: /Build a dashboard/i })).toBeVisible();
expect(screen.queryAllByRole('link', { name: /Explore data/i })).toHaveLength(1);
});
});
});

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import DataSourceTabPage from '../components/DataSourceTabPage';
import { EditDataSource } from '../components/EditDataSource';
import { EditDataSourceActions } from '../components/EditDataSourceActions';
import { useDataSourceSettingsNav } from '../state';
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
export function EditDataSourcePage(props: Props) {
const uid = props.match.params.uid;
const params = new URLSearchParams(props.location.search);
const pageId = params.get('page');
const dataSourcePageHeader = config.featureToggles.dataSourcePageHeader;
const nav = useDataSourceSettingsNav(uid, pageId);
if (dataSourcePageHeader) {
return <DataSourceTabPage uid={uid} pageId={pageId} />;
}
return (
<Page navId="datasources" pageNav={nav.main} actions={<EditDataSourceActions uid={uid} />}>
<Page.Contents>
<EditDataSource uid={uid} pageId={pageId} />
</Page.Contents>
</Page>
);
}
export default EditDataSourcePage;

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { NavModelItem } from '@grafana/data';
import { Page } from 'app/core/components/Page/Page';
import { NewDataSource } from '../components/NewDataSource';
import { DATASOURCES_ROUTES } from '../constants';
export function NewDataSourcePage() {
const pageNav: NavModelItem = {
icon: 'database',
id: 'datasource-new',
text: 'Add data source',
url: DATASOURCES_ROUTES.New,
subTitle: 'Choose a data source type',
};
return (
<Page navId="datasources" pageNav={pageNav}>
<Page.Contents>
<NewDataSource />
</Page.Contents>
</Page>
);
}
export default NewDataSourcePage;

View File

@@ -1,12 +1,9 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { DataSourcePluginMeta, DataSourceSettings, NavModel, NavModelItem } from '@grafana/data'; import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { cleanUpAction } from 'app/core/actions/cleanUp'; import { cleanUpAction } from 'app/core/actions/cleanUp';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { useGetSingle } from 'app/features/plugins/admin/state/hooks';
import { AccessControlAction, useDispatch, useSelector } from 'app/types'; import { AccessControlAction, useDispatch, useSelector } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events'; import { ShowConfirmModalEvent } from 'app/types/events';
@@ -24,7 +21,6 @@ import {
deleteLoadedDataSource, deleteLoadedDataSource,
} from './actions'; } from './actions';
import { DataSourcesRoutesContext } from './contexts'; import { DataSourcesRoutesContext } from './contexts';
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from './navModel';
import { initialDataSourceSettingsState } from './reducers'; import { initialDataSourceSettingsState } from './reducers';
import { getDataSource, getDataSourceMeta } from './selectors'; import { getDataSource, getDataSourceMeta } from './selectors';
@@ -128,65 +124,6 @@ export const useDataSourceSettings = () => {
return useSelector((state) => state.dataSourceSettings); return useSelector((state) => state.dataSourceSettings);
}; };
export const useDataSourceSettingsNav = (dataSourceId: string, pageId: string | null) => {
const { plugin, loadError, loading } = useDataSourceSettings();
const dataSource = useDataSource(dataSourceId);
const dsi = getDataSourceSrv()?.getInstanceSettings(dataSourceId);
const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false);
const isAlertManagerDatasource = dsi?.type === 'alertmanager';
const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource;
const datasourcePlugin = useGetSingle(dataSource.type);
const navIndex = useSelector((state) => state.navIndex);
const navIndexId = pageId ? `datasource-${pageId}-${dataSourceId}` : `datasource-settings-${dataSourceId}`;
let pageNav: NavModel = {
node: {
text: 'Data Source Nav Node',
},
main: {
text: 'Data Source Nav Node',
},
};
if (loadError) {
const node: NavModelItem = {
text: loadError,
subTitle: 'Data Source Error',
icon: 'exclamation-triangle',
};
pageNav = {
node: node,
main: node,
};
}
if (loading || !plugin) {
pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav('settings'));
}
if (plugin) {
pageNav = getNavModel(
navIndex,
navIndexId,
getDataSourceNav(buildNavModel(dataSource, plugin), pageId || 'settings')
);
}
return {
node: pageNav.node,
main: {
...pageNav.main,
text: dataSource.name,
dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '',
active: true,
},
dataSourceHeader: {
alertingSupported,
},
};
};
export const useDataSourceRights = (uid: string): DataSourceRights => { export const useDataSourceRights = (uid: string): DataSourceRights => {
const dataSource = useDataSource(uid); const dataSource = useDataSource(uid);
const readOnly = dataSource.readOnly === true; const readOnly = dataSource.readOnly === true;