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;