diff --git a/public/app/features/data-connections/DataConnectionsPage.test.tsx b/public/app/features/data-connections/DataConnectionsPage.test.tsx
index ba0bcddeca8..c23ab82a0aa 100644
--- a/public/app/features/data-connections/DataConnectionsPage.test.tsx
+++ b/public/app/features/data-connections/DataConnectionsPage.test.tsx
@@ -4,12 +4,16 @@ import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { locationService } from '@grafana/runtime';
+import { getMockDataSources } from 'app/features/datasources/__mocks__';
+import * as api from 'app/features/datasources/api';
import { configureStore } from 'app/store/configureStore';
import DataConnectionsPage from './DataConnectionsPage';
import { navIndex } from './__mocks__/store.navIndex.mock';
import { ROUTE_BASE_ID, ROUTES } from './constants';
+jest.mock('app/features/datasources/api');
+
const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
// @ts-ignore
const store = configureStore({ navIndex });
@@ -25,6 +29,12 @@ const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
};
describe('Data Connections Page', () => {
+ const mockDatasources = getMockDataSources(3);
+
+ beforeEach(() => {
+ (api.getDataSources as jest.Mock) = jest.fn().mockResolvedValue(mockDatasources);
+ });
+
test('shows all the four tabs', async () => {
renderPage();
@@ -37,7 +47,8 @@ describe('Data Connections Page', () => {
test('shows the "Data sources" tab by default', async () => {
renderPage();
- expect(await screen.findByText('The list of data sources is under development.')).toBeVisible();
+ expect(await screen.findByRole('link', { name: /add data source/i })).toBeVisible();
+ expect(await screen.findByText(mockDatasources[0].name)).toBeVisible();
});
test('renders the correct tab even if accessing it with a "sub-url"', async () => {
diff --git a/public/app/features/data-connections/DataConnectionsPage.tsx b/public/app/features/data-connections/DataConnectionsPage.tsx
index 766e6591e9c..57e452a5c4b 100644
--- a/public/app/features/data-connections/DataConnectionsPage.tsx
+++ b/public/app/features/data-connections/DataConnectionsPage.tsx
@@ -2,29 +2,44 @@ import * as React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Page } from 'app/core/components/Page/Page';
+import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList';
+import { NewDataSource } from 'app/features/datasources/components/NewDataSource';
+import { DataSourcesRoutesContext } from 'app/features/datasources/state';
import { ROUTES } from './constants';
import { useNavModel } from './hooks/useNavModel';
import { CloudIntegrations } from './tabs/CloudIntegrations';
-import { DataSources } from './tabs/DataSources';
+import { DataSourcesEdit } from './tabs/DataSourcesEdit';
import { Plugins } from './tabs/Plugins';
import { RecordedQueries } from './tabs/RecordedQueries';
-export default function DataConnectionsPage(): React.ReactElement | null {
+export default function DataConnectionsPage() {
const navModel = useNavModel();
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
- {/* Default page */}
-
-
-
-
+ {/* Default page */}
+
+
+
+
+
);
}
diff --git a/public/app/features/data-connections/constants.ts b/public/app/features/data-connections/constants.ts
index c35a30aba1b..ddcf0aebaa2 100644
--- a/public/app/features/data-connections/constants.ts
+++ b/public/app/features/data-connections/constants.ts
@@ -5,7 +5,10 @@ export const CLOUD_ONBOARDING_APP_ID = 'grafana-easystart-app';
export const ROUTE_BASE_ID = 'data-connections';
export enum ROUTES {
- DataSources = '/data-connections/data-sources',
+ DataSources = '/data-connections/datasources',
+ DataSourcesNew = '/data-connections/datasources/new',
+ DataSourcesEdit = '/data-connections/datasources/edit/:uid',
+ DataSourcesDashboards = '/data-connections/datasources/edit/:uid/dashboards',
Plugins = '/data-connections/plugins',
CloudIntegrations = '/data-connections/cloud-integrations',
RecordedQueries = '/data-connections/recorded-queries',
diff --git a/public/app/features/data-connections/hooks/useNavModel.ts b/public/app/features/data-connections/hooks/useNavModel.ts
index 598f7a87e3f..36ad82fcdde 100644
--- a/public/app/features/data-connections/hooks/useNavModel.ts
+++ b/public/app/features/data-connections/hooks/useNavModel.ts
@@ -1,6 +1,7 @@
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
+import { NavModelItem } from '@grafana/data';
import { StoreState } from 'app/types/store';
import { ROUTE_BASE_ID } from '../constants';
@@ -9,14 +10,17 @@ import { ROUTE_BASE_ID } from '../constants';
// (In case we were using `getNavModel()` from app/core/selectors/navModel, then we would need to set
// the child nav-model-item's ID on the call-site.)
export const useNavModel = () => {
- const { pathname } = useLocation();
+ const { pathname: currentPath } = useLocation();
const navIndex = useSelector((state: StoreState) => state.navIndex);
const node = navIndex[ROUTE_BASE_ID];
const main = node;
+ const isDefaultRoute = (item: NavModelItem) =>
+ currentPath === `/${ROUTE_BASE_ID}` && item.id === 'data-connections-datasources';
+ const isItemActive = (item: NavModelItem) => currentPath.startsWith(item.url || '');
main.children = main.children?.map((item) => ({
...item,
- active: pathname.startsWith(item.url || ''),
+ active: isItemActive(item) || isDefaultRoute(item),
}));
return {
diff --git a/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx b/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx
index 368facd9d6d..92d44783bfa 100644
--- a/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx
+++ b/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
-import React, { ReactElement } from 'react';
+import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
@@ -7,7 +7,7 @@ import { AppPluginLoader } from 'app/features/plugins/components/AppPluginLoader
import { CLOUD_ONBOARDING_APP_ID, ROUTES } from '../../constants';
-export function CloudIntegrations(): ReactElement | null {
+export function CloudIntegrations() {
const s = useStyles2(getStyles);
return (
diff --git a/public/app/features/data-connections/tabs/DataSources/DataSources.tsx b/public/app/features/data-connections/tabs/DataSources/DataSources.tsx
deleted file mode 100644
index a809c21511e..00000000000
--- a/public/app/features/data-connections/tabs/DataSources/DataSources.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React, { ReactElement } from 'react';
-
-export function DataSources(): ReactElement | null {
- return
The list of data sources is under development.
;
-}
diff --git a/public/app/features/data-connections/tabs/DataSources/index.tsx b/public/app/features/data-connections/tabs/DataSources/index.tsx
deleted file mode 100644
index 5d681837186..00000000000
--- a/public/app/features/data-connections/tabs/DataSources/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './DataSources';
diff --git a/public/app/features/data-connections/tabs/DataSourcesEdit/DataSourcesEdit.tsx b/public/app/features/data-connections/tabs/DataSourcesEdit/DataSourcesEdit.tsx
new file mode 100644
index 00000000000..44e589c7cdf
--- /dev/null
+++ b/public/app/features/data-connections/tabs/DataSourcesEdit/DataSourcesEdit.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
+import { EditDataSource } from 'app/features/datasources/components/EditDataSource';
+
+export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
+
+export function DataSourcesEdit(props: Props) {
+ const uid = props.match.params.uid;
+ const params = new URLSearchParams(props.location.search);
+ const pageId = params.get('page');
+
+ return ;
+}
diff --git a/public/app/features/data-connections/tabs/DataSourcesEdit/index.tsx b/public/app/features/data-connections/tabs/DataSourcesEdit/index.tsx
new file mode 100644
index 00000000000..081617892e3
--- /dev/null
+++ b/public/app/features/data-connections/tabs/DataSourcesEdit/index.tsx
@@ -0,0 +1 @@
+export * from './DataSourcesEdit';
diff --git a/public/app/features/data-connections/tabs/Plugins/Plugins.tsx b/public/app/features/data-connections/tabs/Plugins/Plugins.tsx
index e1befaab13f..6b1fc3f57d0 100644
--- a/public/app/features/data-connections/tabs/Plugins/Plugins.tsx
+++ b/public/app/features/data-connections/tabs/Plugins/Plugins.tsx
@@ -1,5 +1,5 @@
-import React, { ReactElement } from 'react';
+import React from 'react';
-export function Plugins(): ReactElement | null {
+export function Plugins() {
return The list of plugins is under development
;
}
diff --git a/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx b/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx
index 90ec2c24f76..001c4fb32e7 100644
--- a/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx
+++ b/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx
@@ -1,5 +1,5 @@
-import React, { ReactElement } from 'react';
+import React from 'react';
-export function RecordedQueries(): ReactElement | null {
+export function RecordedQueries() {
return The recorded queries tab is under development.
;
}
diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx
index 4a00393656d..a3abb7e00ad 100644
--- a/public/app/features/datasources/components/DataSourcesList.tsx
+++ b/public/app/features/datasources/components/DataSourcesList.tsx
@@ -9,7 +9,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { contextSrv } from 'app/core/core';
import { StoreState, AccessControlAction } from 'app/types';
-import { getDataSources, getDataSourcesCount, useLoadDataSources } from '../state';
+import { getDataSources, getDataSourcesCount, useDataSourcesRoutes, useLoadDataSources } from '../state';
import { DataSourcesListHeader } from './DataSourcesListHeader';
@@ -40,6 +40,7 @@ export type ViewProps = {
export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, hasCreateRights }: ViewProps) {
const styles = useStyles(getStyles);
+ const dataSourcesRoutes = useDataSourcesRoutes();
if (isLoading) {
return ;
@@ -51,7 +52,7 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading,
buttonDisabled={!hasCreateRights}
title="No data sources defined"
buttonIcon="database"
- buttonLink="datasources/new"
+ buttonLink={dataSourcesRoutes.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"
@@ -71,7 +72,7 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading,
{dataSources.map((dataSource) => {
return (
-
+
{dataSource.name}
diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx
index d006f521cf5..8ce4a972e6c 100644
--- a/public/app/features/datasources/components/DataSourcesListHeader.tsx
+++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx
@@ -6,7 +6,7 @@ import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { contextSrv } from 'app/core/core';
import { AccessControlAction, StoreState } from 'app/types';
-import { getDataSourcesSearchQuery, setDataSourcesSearchQuery } from '../state';
+import { getDataSourcesSearchQuery, setDataSourcesSearchQuery, useDataSourcesRoutes } from '../state';
export function DataSourcesListHeader() {
const dispatch = useDispatch();
@@ -30,8 +30,9 @@ export type ViewProps = {
};
export function DataSourcesListHeaderView({ searchQuery, setSearchQuery, canCreateDataSource }: ViewProps) {
+ const dataSourcesRoutes = useDataSourcesRoutes();
const linkButton = {
- href: 'datasources/new',
+ href: dataSourcesRoutes.New,
title: 'Add data source',
disabled: !canCreateDataSource,
};
diff --git a/public/app/features/datasources/components/NewDataSource.tsx b/public/app/features/datasources/components/NewDataSource.tsx
index 658a02a4c68..af47fe1895b 100644
--- a/public/app/features/datasources/components/NewDataSource.tsx
+++ b/public/app/features/datasources/components/NewDataSource.tsx
@@ -15,6 +15,7 @@ import {
useLoadDataSourcePlugins,
getFilteredDataSourcePlugins,
setDataSourceTypeSearchQuery,
+ useDataSourcesRoutes,
} from '../state';
export function NewDataSource() {
@@ -57,6 +58,8 @@ export function NewDataSourceView({
onAddDataSource,
onSetSearchQuery,
}: ViewProps) {
+ const dataSourcesRoutes = useDataSourcesRoutes();
+
if (isLoading) {
return ;
}
@@ -67,7 +70,7 @@ export function NewDataSourceView({
diff --git a/public/app/features/datasources/constants.ts b/public/app/features/datasources/constants.ts
new file mode 100644
index 00000000000..1c7d5871fd4
--- /dev/null
+++ b/public/app/features/datasources/constants.ts
@@ -0,0 +1,12 @@
+import { DataSourcesRoutes } from './types';
+
+/**
+ * Default routes for data sources pages.
+ * (Links to the pages can be overriden for this feature by using `DataSourcesRoutesContext`)
+ */
+export const DATASOURCES_ROUTES: DataSourcesRoutes = {
+ List: '/datasources',
+ Edit: '/datasources/edit/:uid',
+ Dashboards: '/datasources/edit/:uid/dashboards',
+ New: '/datasources/new',
+} as const;
diff --git a/public/app/features/datasources/pages/NewDataSourcePage.tsx b/public/app/features/datasources/pages/NewDataSourcePage.tsx
index f362cd5c8b6..8dd9b57cce6 100644
--- a/public/app/features/datasources/pages/NewDataSourcePage.tsx
+++ b/public/app/features/datasources/pages/NewDataSourcePage.tsx
@@ -4,6 +4,7 @@ import { NavModel } from '@grafana/data';
import { Page } from 'app/core/components/Page/Page';
import { NewDataSource } from '../components/NewDataSource';
+import { DATASOURCES_ROUTES } from '../constants';
const navModel = getNavModel();
@@ -22,7 +23,7 @@ export function getNavModel(): NavModel {
icon: 'database',
id: 'datasource-new',
text: 'Add data source',
- href: 'datasources/new',
+ href: DATASOURCES_ROUTES.New,
subTitle: 'Choose a data source type',
};
diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts
index 715d4cba37e..f8379fd7ae3 100644
--- a/public/app/features/datasources/state/actions.ts
+++ b/public/app/features/datasources/state/actions.ts
@@ -16,6 +16,7 @@ import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
import { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types';
import * as api from '../api';
+import { DATASOURCES_ROUTES } from '../constants';
import { nameExits, findNewName } from '../utils';
import { buildCategories } from './buildCategories';
@@ -174,7 +175,7 @@ export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult<
};
}
-export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult {
+export function addDataSource(plugin: DataSourcePluginMeta, editLink = DATASOURCES_ROUTES.Edit): ThunkResult {
return async (dispatch, getStore) => {
await dispatch(loadDataSources());
@@ -196,7 +197,7 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult {
await getDatasourceSrv().reload();
await contextSrv.fetchUserPermissions();
- locationService.push(`/datasources/edit/${result.datasource.uid}`);
+ locationService.push(editLink.replace(/:uid/gi, result.datasource.uid));
};
}
diff --git a/public/app/features/datasources/state/contexts.ts b/public/app/features/datasources/state/contexts.ts
new file mode 100644
index 00000000000..8153c3f4610
--- /dev/null
+++ b/public/app/features/datasources/state/contexts.ts
@@ -0,0 +1,8 @@
+import { createContext } from 'react';
+
+import { DATASOURCES_ROUTES } from '../constants';
+import { DataSourcesRoutes } from '../types';
+
+// The purpose of this context is to be able to override the data-sources routes (used for links for example) used under
+// the app/features/datasources modules, so we can reuse them more easily in different parts of the application (e.g. under Data Connections)
+export const DataSourcesRoutesContext = createContext(DATASOURCES_ROUTES);
diff --git a/public/app/features/datasources/state/hooks.ts b/public/app/features/datasources/state/hooks.ts
index c9168dfca56..4b55e9377d4 100644
--- a/public/app/features/datasources/state/hooks.ts
+++ b/public/app/features/datasources/state/hooks.ts
@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DataSourcePluginMeta, DataSourceSettings, urlUtil } from '@grafana/data';
@@ -21,6 +21,7 @@ import {
updateDataSource,
deleteLoadedDataSource,
} from './actions';
+import { DataSourcesRoutesContext } from './contexts';
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from './navModel';
import { getDataSource, getDataSourceMeta } from './selectors';
@@ -72,9 +73,10 @@ export const useLoadDataSourcePlugins = () => {
export const useAddDatasource = () => {
const dispatch = useDispatch();
+ const dataSourcesRoutes = useDataSourcesRoutes();
return (plugin: DataSourcePluginMeta) => {
- dispatch(addDataSource(plugin));
+ dispatch(addDataSource(plugin, dataSourcesRoutes.Edit));
};
};
@@ -159,3 +161,7 @@ export const useDataSourceRights = (uid: string): DataSourceRights => {
hasDeleteRights,
};
};
+
+export const useDataSourcesRoutes = () => {
+ return useContext(DataSourcesRoutesContext);
+};
diff --git a/public/app/features/datasources/state/index.ts b/public/app/features/datasources/state/index.ts
index 42fae7a84a7..37a2d6f5cf0 100644
--- a/public/app/features/datasources/state/index.ts
+++ b/public/app/features/datasources/state/index.ts
@@ -1,5 +1,6 @@
export * from './actions';
export * from './buildCategories';
+export * from './contexts';
export * from './hooks';
export * from './navModel';
export * from './reducers';
diff --git a/public/app/features/datasources/types.ts b/public/app/features/datasources/types.ts
index 59d8f4e1fb6..f0833101538 100644
--- a/public/app/features/datasources/types.ts
+++ b/public/app/features/datasources/types.ts
@@ -7,3 +7,10 @@ export type DataSourceRights = {
hasWriteRights: boolean;
hasDeleteRights: boolean;
};
+
+export type DataSourcesRoutes = {
+ New: string;
+ Edit: string;
+ List: string;
+ Dashboards: string;
+};
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index bb9eece76b7..16933572eb2 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -9,6 +9,7 @@ import UserAdminPage from 'app/features/admin/UserAdminPage';
import LdapPage from 'app/features/admin/ldap/LdapPage';
import { getAlertingRoutes } from 'app/features/alerting/routes';
import { getRoutes as getDataConnectionsRoutes } from 'app/features/data-connections/routes';
+import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
import { getLiveRoutes } from 'app/features/live/pages/routes';
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
import { getProfileRoutes } from 'app/features/profile/routes';
@@ -90,19 +91,19 @@ export function getAppRoutes(): RouteDescriptor[] {
),
},
{
- path: '/datasources',
+ path: DATASOURCES_ROUTES.List,
component: SafeDynamicImport(
() => import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/pages/DataSourcesListPage')
),
},
{
- path: '/datasources/edit/:uid/',
+ path: DATASOURCES_ROUTES.Edit,
component: SafeDynamicImport(
() => import(/* webpackChunkName: "EditDataSourcePage"*/ '../features/datasources/pages/EditDataSourcePage')
),
},
{
- path: '/datasources/edit/:uid/dashboards',
+ path: DATASOURCES_ROUTES.Dashboards,
component: SafeDynamicImport(
() =>
import(
@@ -111,7 +112,7 @@ export function getAppRoutes(): RouteDescriptor[] {
),
},
{
- path: '/datasources/new',
+ path: DATASOURCES_ROUTES.New,
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NewDataSourcePage"*/ '../features/datasources/pages/NewDataSourcePage')
),