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({
    - + Cancel
    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') ),