diff --git a/public/app/features/connections/hooks/useDataSourceSettingsNav.ts b/public/app/features/connections/hooks/useDataSourceSettingsNav.ts
index 9b9383c0205..6d165be812d 100644
--- a/public/app/features/connections/hooks/useDataSourceSettingsNav.ts
+++ b/public/app/features/connections/hooks/useDataSourceSettingsNav.ts
@@ -1,26 +1,70 @@
import { useLocation, useParams } from 'react-router-dom';
-import {
- useDataSource,
- useDataSourceSettingsNav as useDataSourceSettingsNavOriginal,
-} from 'app/features/datasources/state/hooks';
+import { NavModel, NavModelItem } from '@grafana/data';
+import { getDataSourceSrv } from '@grafana/runtime';
+import { getNavModel } from 'app/core/selectors/navModel';
+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 { useSelector } from 'app/types';
-// We are extending the original useDataSourceSettingsNav in the following ways:
-// - changing the URL of the nav items to point to Connections
-// - setting the parent nav item
-export function useDataSourceSettingsNav(pageId?: string) {
+export function useDataSourceSettingsNav(pageIdParam?: string) {
const { uid } = useParams<{ uid: string }>();
const location = useLocation();
const datasource = useDataSource(uid);
const datasourcePlugin = useGetSingle(datasource.type);
const params = new URLSearchParams(location.search);
- const nav = useDataSourceSettingsNavOriginal(uid, pageId || params.get('page'));
- const pageNav = {
- ...nav.main,
+ const pageId = pageIdParam || params.get('page');
+
+ 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,
subTitle: `Type: ${datasourcePlugin?.name}`,
- children: (nav.main.children || []).map((navModelItem) => ({
+ children: (pageNav.main.children || []).map((navModelItem) => ({
...navModelItem,
url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'),
})),
@@ -28,7 +72,9 @@ export function useDataSourceSettingsNav(pageId?: string) {
return {
navId: 'connections-datasources',
- pageNav,
- dataSourceHeader: nav.dataSourceHeader,
+ pageNav: connectionsPageNav,
+ dataSourceHeader: {
+ alertingSupported,
+ },
};
}
diff --git a/public/app/features/datasources/pages/DataSourceDashboardsPage.test.tsx b/public/app/features/datasources/pages/DataSourceDashboardsPage.test.tsx
deleted file mode 100644
index 3aa51ab03b5..00000000000
--- a/public/app/features/datasources/pages/DataSourceDashboardsPage.test.tsx
+++ /dev/null
@@ -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(
-
-
-
- );
-
-describe('', () => {
- 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();
- });
-});
diff --git a/public/app/features/datasources/pages/DataSourceDashboardsPage.tsx b/public/app/features/datasources/pages/DataSourceDashboardsPage.tsx
deleted file mode 100644
index 9f8fc7d00ab..00000000000
--- a/public/app/features/datasources/pages/DataSourceDashboardsPage.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- );
-}
-
-export default DataSourceDashboardsPage;
diff --git a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx
deleted file mode 100644
index de0a4b9670b..00000000000
--- a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx
+++ /dev/null
@@ -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(
-
-
-
- );
-};
-
-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');
- });
- });
-});
diff --git a/public/app/features/datasources/pages/DataSourcesListPage.tsx b/public/app/features/datasources/pages/DataSourcesListPage.tsx
deleted file mode 100644
index ce4d8769450..00000000000
--- a/public/app/features/datasources/pages/DataSourcesListPage.tsx
+++ /dev/null
@@ -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 ? : undefined;
- return (
-
-
-
-
-
-
- );
-}
-
-export default DataSourcesListPage;
diff --git a/public/app/features/datasources/pages/EditDataSourcePage.test.tsx b/public/app/features/datasources/pages/EditDataSourcePage.test.tsx
deleted file mode 100644
index 4ad10721056..00000000000
--- a/public/app/features/datasources/pages/EditDataSourcePage.test.tsx
+++ /dev/null
@@ -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(
-
-
-
- );
-
-describe('', () => {
- 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);
- });
- });
-});
diff --git a/public/app/features/datasources/pages/EditDataSourcePage.tsx b/public/app/features/datasources/pages/EditDataSourcePage.tsx
deleted file mode 100644
index cd8d52ba85d..00000000000
--- a/public/app/features/datasources/pages/EditDataSourcePage.tsx
+++ /dev/null
@@ -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 ;
- }
-
- return (
- }>
-
-
-
-
- );
-}
-
-export default EditDataSourcePage;
diff --git a/public/app/features/datasources/pages/NewDataSourcePage.tsx b/public/app/features/datasources/pages/NewDataSourcePage.tsx
deleted file mode 100644
index 498cae88f10..00000000000
--- a/public/app/features/datasources/pages/NewDataSourcePage.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- );
-}
-
-export default NewDataSourcePage;
diff --git a/public/app/features/datasources/state/hooks.ts b/public/app/features/datasources/state/hooks.ts
index 92222cd5c85..11587a60d42 100644
--- a/public/app/features/datasources/state/hooks.ts
+++ b/public/app/features/datasources/state/hooks.ts
@@ -1,12 +1,9 @@
import { useContext, useEffect } from 'react';
-import { DataSourcePluginMeta, DataSourceSettings, NavModel, NavModelItem } from '@grafana/data';
-import { getDataSourceSrv } from '@grafana/runtime';
+import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
import { cleanUpAction } from 'app/core/actions/cleanUp';
import appEvents from 'app/core/app_events';
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 { ShowConfirmModalEvent } from 'app/types/events';
@@ -24,7 +21,6 @@ import {
deleteLoadedDataSource,
} from './actions';
import { DataSourcesRoutesContext } from './contexts';
-import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from './navModel';
import { initialDataSourceSettingsState } from './reducers';
import { getDataSource, getDataSourceMeta } from './selectors';
@@ -128,65 +124,6 @@ export const useDataSourceSettings = () => {
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 => {
const dataSource = useDataSource(uid);
const readOnly = dataSource.readOnly === true;