From c34e0da56210c9bb32708456b6a2a112c89411bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 10 Nov 2022 11:53:40 +0100 Subject: [PATCH 01/22] navtree.go: update Data sources title and subtitle --- pkg/services/navtree/navtreeimpl/navtree.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 73d601b6ba3d..d27f1b4d8af9 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -552,8 +552,8 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *models.ReqContext) *navtree // Datasources Children: []*navtree.NavLink{{ Id: "connections-your-connections-datasources", - Text: "Datasources", - SubTitle: "Manage your existing datasource connections", + Text: "Data sources", + SubTitle: "View and manage your connected data source connections", Url: baseUrl + "/your-connections/datasources", }}, }) From 84adf117688db2235edcd805be0eb60d416aad8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 10 Nov 2022 11:54:39 +0100 Subject: [PATCH 02/22] DataSourceList: move add button to header --- .../connections/pages/DataSourcesListPage.tsx | 3 +- .../components/DataSourceAddButton.tsx | 18 ++++++++++ .../components/DataSourcesListHeader.tsx | 34 ++----------------- .../datasources/pages/DataSourcesListPage.tsx | 3 +- 4 files changed, 25 insertions(+), 33 deletions(-) create mode 100644 public/app/features/datasources/components/DataSourceAddButton.tsx diff --git a/public/app/features/connections/pages/DataSourcesListPage.tsx b/public/app/features/connections/pages/DataSourcesListPage.tsx index 518cc4c39db7..17612363ba4c 100644 --- a/public/app/features/connections/pages/DataSourcesListPage.tsx +++ b/public/app/features/connections/pages/DataSourcesListPage.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { Page } from 'app/core/components/Page/Page'; +import { DataSourceAddButton } from 'app/features/datasources/components/DataSourceAddButton'; import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList'; export function DataSourcesListPage() { return ( - + diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx new file mode 100644 index 000000000000..d585e6f17afd --- /dev/null +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { LinkButton } from '@grafana/ui'; +import { contextSrv } from 'app/core/core'; +import { AccessControlAction } from 'app/types'; + +import { useDataSourcesRoutes } from '../state'; + +export function DataSourceAddButton() { + const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); + const dataSourcesRoutes = useDataSourcesRoutes(); + + return ( + + Add new data source + + ); +} diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx index 8db867bb2454..a61fccb65fb6 100644 --- a/public/app/features/datasources/components/DataSourcesListHeader.tsx +++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx @@ -1,42 +1,14 @@ import React, { useCallback } from 'react'; -import { AnyAction } from 'redux'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; -import { contextSrv } from 'app/core/core'; -import { AccessControlAction, StoreState, useSelector, useDispatch } from 'app/types'; +import { StoreState, useSelector, useDispatch } from 'app/types'; -import { getDataSourcesSearchQuery, setDataSourcesSearchQuery, useDataSourcesRoutes } from '../state'; +import { getDataSourcesSearchQuery, setDataSourcesSearchQuery } from '../state'; export function DataSourcesListHeader() { const dispatch = useDispatch(); const setSearchQuery = useCallback((q: string) => dispatch(setDataSourcesSearchQuery(q)), [dispatch]); const searchQuery = useSelector(({ dataSources }: StoreState) => getDataSourcesSearchQuery(dataSources)); - const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); - return ( - - ); -} - -export type ViewProps = { - searchQuery: string; - setSearchQuery: (q: string) => AnyAction; - canCreateDataSource: boolean; -}; - -export function DataSourcesListHeaderView({ searchQuery, setSearchQuery, canCreateDataSource }: ViewProps) { - const dataSourcesRoutes = useDataSourcesRoutes(); - const linkButton = { - href: dataSourcesRoutes.New, - title: 'Add data source', - disabled: !canCreateDataSource, - }; - - return ( - - ); + return ; } diff --git a/public/app/features/datasources/pages/DataSourcesListPage.tsx b/public/app/features/datasources/pages/DataSourcesListPage.tsx index 2315b0363674..e8ddc5f45875 100644 --- a/public/app/features/datasources/pages/DataSourcesListPage.tsx +++ b/public/app/features/datasources/pages/DataSourcesListPage.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { Page } from 'app/core/components/Page/Page'; +import { DataSourceAddButton } from '../components/DataSourceAddButton'; import { DataSourcesList } from '../components/DataSourcesList'; export function DataSourcesListPage() { return ( - + From ae7c531f0260cf68dd9e8d4a27a6f0ab88aa1be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 10 Nov 2022 11:56:50 +0100 Subject: [PATCH 03/22] DataSourcesList: add buttons to items The action buttons are added inside `` so that they end up at the right end of the card, as it was designed. The "Build a Dashboard" button's functionality is not defined yet. --- .../components/DataSourcesList.tsx | 22 ++++++++++++++++++- .../app/features/datasources/state/hooks.ts | 8 +++---- public/app/features/datasources/utils.ts | 9 ++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index 06343ce5d697..617e5a02290a 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -2,13 +2,14 @@ import { css } from '@emotion/css'; import React from 'react'; import { DataSourceSettings } from '@grafana/data'; -import { Card, Tag, useStyles2 } from '@grafana/ui'; +import { Button, LinkButton, Card, Tag, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { contextSrv } from 'app/core/core'; import { StoreState, AccessControlAction, useSelector } from 'app/types'; import { getDataSources, getDataSourcesCount, useDataSourcesRoutes, useLoadDataSources } from '../state'; +import { constructDataSourceExploreUrl } from '../utils'; import { DataSourcesListHeader } from './DataSourcesListHeader'; @@ -40,6 +41,7 @@ export type ViewProps = { export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, hasCreateRights }: ViewProps) { const styles = useStyles2(getStyles); const dataSourcesRoutes = useDataSourcesRoutes(); + const canExploreDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore); if (isLoading) { return ; @@ -83,6 +85,21 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, dataSource.isDefault && , ]} + + + + Explore + + ); @@ -102,5 +119,8 @@ const getStyles = () => { logo: css({ objectFit: 'contain', }), + button: css({ + 'margin-left': '16px', + }), }; }; diff --git a/public/app/features/datasources/state/hooks.ts b/public/app/features/datasources/state/hooks.ts index cc6bc00c0545..70bf7acfaa46 100644 --- a/public/app/features/datasources/state/hooks.ts +++ b/public/app/features/datasources/state/hooks.ts @@ -1,6 +1,6 @@ import { useContext, useEffect } from 'react'; -import { DataSourcePluginMeta, DataSourceSettings, NavModelItem, urlUtil } from '@grafana/data'; +import { DataSourcePluginMeta, DataSourceSettings, NavModelItem } from '@grafana/data'; import { cleanUpAction } from 'app/core/actions/cleanUp'; import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; @@ -9,6 +9,7 @@ import { AccessControlAction, useDispatch, useSelector } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; import { DataSourceRights } from '../types'; +import { constructDataSourceExploreUrl } from '../utils'; import { initDataSourceSettings, @@ -108,10 +109,7 @@ export const useDataSource = (uid: string) => { export const useDataSourceExploreUrl = (uid: string) => { const dataSource = useDataSource(uid); - const exploreState = JSON.stringify({ datasource: dataSource.name, context: 'explore' }); - const exploreUrl = urlUtil.renderUrl('/explore', { left: exploreState }); - - return exploreUrl; + return constructDataSourceExploreUrl(dataSource); }; export const useDataSourceMeta = (pluginType: string): DataSourcePluginMeta => { diff --git a/public/app/features/datasources/utils.ts b/public/app/features/datasources/utils.ts index 48dcb500fe23..9137a23485ce 100644 --- a/public/app/features/datasources/utils.ts +++ b/public/app/features/datasources/utils.ts @@ -1,3 +1,5 @@ +import { DataSourceJsonData, DataSourceSettings, urlUtil } from '@grafana/data'; + interface ItemWithName { name: string; } @@ -45,3 +47,10 @@ function incrementLastDigit(digit: number) { function getNewName(name: string) { return name.slice(0, name.length - 1); } + +export const constructDataSourceExploreUrl = (dataSource: DataSourceSettings) => { + const exploreState = JSON.stringify({ datasource: dataSource.name, context: 'explore' }); + const exploreUrl = urlUtil.renderUrl('/explore', { left: exploreState }); + + return exploreUrl; +}; From 52814fff77373e989df287fa292de23c88a28caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 10 Nov 2022 14:05:30 +0100 Subject: [PATCH 04/22] DataSourcesListHeader: add sort picker --- .../src/themes/GlobalStyles/page.ts | 3 ++- .../PageActionBar/PageActionBar.tsx | 24 ++++++++++++++++- .../components/DataSourcesListHeader.tsx | 27 +++++++++++++++++-- .../features/datasources/state/reducers.ts | 9 +++++++ .../features/datasources/state/selectors.ts | 9 ++++++- public/app/types/datasources.ts | 1 + 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/themes/GlobalStyles/page.ts b/packages/grafana-ui/src/themes/GlobalStyles/page.ts index 033d0beac4a8..1c301bc188dc 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/page.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/page.ts @@ -90,7 +90,8 @@ export function getPageStyles(theme: GrafanaTheme2) { align-items: flex-start; > a, - > button { + > button, + > div:nth-child(2) { margin-left: ${theme.spacing(2)}; } } diff --git a/public/app/core/components/PageActionBar/PageActionBar.tsx b/public/app/core/components/PageActionBar/PageActionBar.tsx index eac3fcc8f3cc..72699bbca1b7 100644 --- a/public/app/core/components/PageActionBar/PageActionBar.tsx +++ b/public/app/core/components/PageActionBar/PageActionBar.tsx @@ -1,18 +1,33 @@ import React, { PureComponent } from 'react'; +import { SelectableValue } from '@grafana/data'; import { LinkButton, FilterInput } from '@grafana/ui'; +import { SortPicker } from '../Select/SortPicker'; + export interface Props { searchQuery: string; setSearchQuery: (value: string) => void; linkButton?: { href: string; title: string; disabled?: boolean }; target?: string; placeholder?: string; + sortPicker?: { + onChange: (sortValue: SelectableValue) => void; + value?: string; + getSortOptions?: () => Promise; + }; } export default class PageActionBar extends PureComponent { render() { - const { searchQuery, linkButton, setSearchQuery, target, placeholder = 'Search by name or type' } = this.props; + const { + searchQuery, + linkButton, + setSearchQuery, + target, + placeholder = 'Search by name or type', + sortPicker, + } = this.props; const linkProps: typeof LinkButton.defaultProps = { href: linkButton?.href, disabled: linkButton?.disabled }; if (target) { @@ -24,6 +39,13 @@ export default class PageActionBar extends PureComponent {
+ {sortPicker && ( + + )} {linkButton && {linkButton.title}} ); diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx index a61fccb65fb6..ce8907f704be 100644 --- a/public/app/features/datasources/components/DataSourcesListHeader.tsx +++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx @@ -1,14 +1,37 @@ import React, { useCallback } from 'react'; +import { SelectableValue } from '@grafana/data'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; import { StoreState, useSelector, useDispatch } from 'app/types'; -import { getDataSourcesSearchQuery, setDataSourcesSearchQuery } from '../state'; +import { getDataSourcesSearchQuery, getDataSourcesSort, setDataSourcesSearchQuery, setIsSortAscending } from '../state'; + +const ascendingSortValue = 'alpha-asc'; +const descendingSortValue = 'alpha-desc'; + +const sortOptions = [ + { label: 'Sort by A–Z', value: ascendingSortValue }, + { label: 'Sort by Z–A', value: descendingSortValue }, +]; export function DataSourcesListHeader() { const dispatch = useDispatch(); const setSearchQuery = useCallback((q: string) => dispatch(setDataSourcesSearchQuery(q)), [dispatch]); const searchQuery = useSelector(({ dataSources }: StoreState) => getDataSourcesSearchQuery(dataSources)); - return ; + const setSort = useCallback( + (sort: SelectableValue) => dispatch(setIsSortAscending(sort.value === ascendingSortValue)), + [dispatch] + ); + const isSortAscending = useSelector(({ dataSources }: StoreState) => getDataSourcesSort(dataSources)); + + const sortPicker = { + onChange: setSort, + value: isSortAscending ? ascendingSortValue : descendingSortValue, + getSortOptions: () => Promise.resolve(sortOptions), + }; + + return ( + + ); } diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 1153c40962a6..c0ee343d378e 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -19,6 +19,7 @@ export const initialState: DataSourcesState = { hasFetched: false, isLoadingDataSources: false, dataSourceMeta: {} as DataSourcePluginMeta, + isSortAscending: true, }; export const dataSourceLoaded = createAction('dataSources/dataSourceLoaded'); @@ -33,6 +34,7 @@ export const setDataSourcesLayoutMode = createAction('dataSources/se export const setDataSourceTypeSearchQuery = createAction('dataSources/setDataSourceTypeSearchQuery'); export const setDataSourceName = createAction('dataSources/setDataSourceName'); export const setIsDefault = createAction('dataSources/setIsDefault'); +export const setIsSortAscending = createAction('dataSources/setIsSortAscending'); // Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated. // ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice @@ -93,6 +95,13 @@ export const dataSourcesReducer = (state: DataSourcesState = initialState, actio }; } + if (setIsSortAscending.match(action)) { + return { + ...state, + isSortAscending: action.payload, + }; + } + return state; }; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index 36181f4e7c88..8e45aad79e35 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/public/app/features/datasources/state/selectors.ts @@ -4,9 +4,15 @@ import { DataSourcesState } from 'app/types/datasources'; export const getDataSources = (state: DataSourcesState) => { const regex = new RegExp(state.searchQuery, 'i'); - return state.dataSources.filter((dataSource: DataSourceSettings) => { + const filteredDataSources = state.dataSources.filter((dataSource: DataSourceSettings) => { return regex.test(dataSource.name) || regex.test(dataSource.database) || regex.test(dataSource.type); }); + + if (state.isSortAscending) { + return filteredDataSources.sort((a, b) => a.name.localeCompare(b.name)); + } else { + return filteredDataSources.sort((a, b) => b.name.localeCompare(a.name)); + } }; export const getFilteredDataSourcePlugins = (state: DataSourcesState) => { @@ -35,3 +41,4 @@ export const getDataSourceMeta = (state: DataSourcesState, type: string): DataSo export const getDataSourcesSearchQuery = (state: DataSourcesState) => state.searchQuery; export const getDataSourcesLayoutMode = (state: DataSourcesState) => state.layoutMode; export const getDataSourcesCount = (state: DataSourcesState) => state.dataSourcesCount; +export const getDataSourcesSort = (state: DataSourcesState) => state.isSortAscending; diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 3b05d3a07cb4..207bd18995c6 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -14,6 +14,7 @@ export interface DataSourcesState { isLoadingDataSources: boolean; plugins: DataSourcePluginMeta[]; categories: DataSourcePluginCategory[]; + isSortAscending: boolean; } export interface TestingStatus { From c12acb7ce9887ca7077df384a3fb789420283bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 10 Nov 2022 16:02:18 +0100 Subject: [PATCH 05/22] fix css --- public/app/features/datasources/components/DataSourcesList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index 617e5a02290a..6bf35c7c2515 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -120,7 +120,7 @@ const getStyles = () => { objectFit: 'contain', }), button: css({ - 'margin-left': '16px', + marginLeft: '16px', }), }; }; From f94dec8ffb850c815db6d8a1fd45cf5a1080c6de Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Tue, 22 Nov 2022 10:16:38 +0100 Subject: [PATCH 06/22] tests: look for the updated "Add new data source" text --- public/app/features/connections/Connections.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/connections/Connections.test.tsx b/public/app/features/connections/Connections.test.tsx index d6c8404848c3..d504e7eff136 100644 --- a/public/app/features/connections/Connections.test.tsx +++ b/public/app/features/connections/Connections.test.tsx @@ -43,7 +43,7 @@ describe('Connections', () => { expect(await screen.findByText('Datasources')).toBeVisible(); expect(await screen.findByText('Manage your existing datasource connections')).toBeVisible(); - expect(await screen.findByRole('link', { name: /add data source/i })).toBeVisible(); + expect(await screen.findByRole('link', { name: /add new data source/i })).toBeVisible(); expect(await screen.findByText(mockDatasources[0].name)).toBeVisible(); }); From 82065bb42585e04f25ed158adaa698effdb0086b Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Tue, 22 Nov 2022 10:33:49 +0100 Subject: [PATCH 07/22] tests: use an async test method to verify component updates are wrapped in an act() --- .../features/connections/Connections.test.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/public/app/features/connections/Connections.test.tsx b/public/app/features/connections/Connections.test.tsx index d504e7eff136..b1d60efefe54 100644 --- a/public/app/features/connections/Connections.test.tsx +++ b/public/app/features/connections/Connections.test.tsx @@ -1,4 +1,4 @@ -import { render, RenderResult, screen } from '@testing-library/react'; +import { render, RenderResult, screen, waitForElementToBeRemoved } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; @@ -43,6 +43,7 @@ describe('Connections', () => { expect(await screen.findByText('Datasources')).toBeVisible(); expect(await screen.findByText('Manage your existing datasource connections')).toBeVisible(); + expect(await screen.findByText('Sort by A–Z')).toBeVisible(); expect(await screen.findByRole('link', { name: /add new data source/i })).toBeVisible(); expect(await screen.findByText(mockDatasources[0].name)).toBeVisible(); }); @@ -57,7 +58,15 @@ describe('Connections', () => { expect(screen.queryByText('Manage your existing datasource connections')).not.toBeInTheDocument(); }); - test('renders the "Connect data" page using a plugin in case it is a standalone plugin page', async () => { + test('renders the core "Connect data" page in case there is no standalone plugin page override for it', async () => { + renderPage(ROUTES.ConnectData); + + // We expect to see no results and "Data sources" as a header (we only have data sources in OSS Grafana at this point) + expect(await screen.findByText('Data sources')).toBeVisible(); + expect(await screen.findByText('No results matching your query were found.')).toBeVisible(); + }); + + test('does not render anything for the "Connect data" page in case it is displayed by a standalone plugin page', async () => { // We are overriding the navIndex to have the "Connect data" page registered by a plugin const standalonePluginPage = { id: 'standalone-plugin-page-/connections/connect-data', @@ -83,7 +92,10 @@ describe('Connections', () => { renderPage(ROUTES.ConnectData, store); - // We expect not to see the same text as if it was rendered by core. + // We expect not to see the text that would be rendered by the core "Connect data" page + // (Instead we expect to see the default route "Datasources") + expect(await screen.findByText('Datasources')).toBeVisible(); + expect(await screen.findByText('Manage your existing datasource connections')).toBeVisible(); expect(screen.queryByText('No results matching your query were found.')).not.toBeInTheDocument(); }); }); From 6dcfeff2576227056d99a7989be12a395d6ffeac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 13:18:03 +0100 Subject: [PATCH 08/22] update e2e selector for add data source button --- packages/grafana-e2e-selectors/src/selectors/pages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index a6d0faae018b..8fa94cb8d9c5 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -31,7 +31,7 @@ export const Pages = { url: '/datasources/new', /** @deprecated Use dataSourcePluginsV2 */ dataSourcePlugins: (pluginName: string) => `Data source plugin item ${pluginName}`, - dataSourcePluginsV2: (pluginName: string) => `Add data source ${pluginName}`, + dataSourcePluginsV2: (pluginName: string) => `Add new data source ${pluginName}`, }, ConfirmModal: { delete: 'Confirm Modal Danger Button', From 4617339e64713f05b41581d1d3e3ecfc1d4b5068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 13:18:32 +0100 Subject: [PATCH 09/22] fix DataSourceList{,Page} tests --- .../components/DataSourcesList.test.tsx | 19 +++++-- .../pages/DataSourcesListPage.test.tsx | 55 +++++++++---------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/public/app/features/datasources/components/DataSourcesList.test.tsx b/public/app/features/datasources/components/DataSourcesList.test.tsx index 45991e8bc1cf..b3fbc50ecc4c 100644 --- a/public/app/features/datasources/components/DataSourcesList.test.tsx +++ b/public/app/features/datasources/components/DataSourcesList.test.tsx @@ -24,17 +24,24 @@ const setup = () => { }; describe('', () => { - it('should render list of datasources', () => { + it('should render action bar', async () => { setup(); - expect(screen.getAllByRole('listitem')).toHaveLength(3); - expect(screen.getAllByRole('heading')).toHaveLength(3); + expect(await screen.findByPlaceholderText('Search by name or type')).toBeInTheDocument(); + expect(await screen.findByRole('combobox', { name: 'Sort' })).toBeInTheDocument(); }); - it('should render all elements in the list item', () => { + it('should render list of datasources', async () => { setup(); - expect(screen.getByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'dataSource-0' })).toBeInTheDocument(); + expect(await screen.findAllByRole('listitem')).toHaveLength(3); + expect(await screen.findAllByRole('heading')).toHaveLength(3); + }); + + it('should render all elements in the list item', async () => { + setup(); + + expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'dataSource-0' })).toBeInTheDocument(); }); }); diff --git a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx index 95eaaf78f398..e16978f3e14d 100644 --- a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx @@ -2,28 +2,27 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; -import { DataSourceSettings, LayoutModes } from '@grafana/data'; +import { LayoutModes } from '@grafana/data'; import { configureStore } from 'app/store/configureStore'; -import { DataSourcesState } from 'app/types'; import { navIndex, getMockDataSources } from '../__mocks__'; +import { getDataSources } from '../api'; import { initialState } from '../state'; import { DataSourcesListPage } from './DataSourcesListPage'; -jest.mock('app/core/services/backend_srv', () => ({ - ...jest.requireActual('app/core/services/backend_srv'), - getBackendSrv: () => ({ get: jest.fn().mockResolvedValue([]) }), +jest.mock('../api', () => ({ + ...jest.requireActual('../api'), + getDataSources: jest.fn().mockResolvedValue([]), })); -const setup = (stateOverride?: Partial) => { +const getDataSourcesMock = getDataSources as jest.Mock; + +const setup = () => { const store = configureStore({ dataSources: { ...initialState, - dataSources: [] as DataSourceSettings[], layoutMode: LayoutModes.Grid, - hasFetched: false, - ...stateOverride, }, navIndex, }); @@ -36,28 +35,28 @@ const setup = (stateOverride?: Partial) => { }; describe('Render', () => { - it('should render component', () => { + it('should render component', async () => { setup(); - expect(screen.getByRole('heading', { name: 'Configuration' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Documentation' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Support' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Community' })).toBeInTheDocument(); + expect(await screen.findByRole('heading', { name: 'Configuration' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Documentation' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Support' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Community' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Add new data source' })).toBeInTheDocument(); }); - it('should render action bar and datasources', () => { - setup({ - dataSources: getMockDataSources(5), - dataSourcesCount: 5, - hasFetched: true, - }); - - expect(screen.getByRole('link', { name: 'Add data source' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'dataSource-1' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'dataSource-2' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'dataSource-3' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'dataSource-4' })).toBeInTheDocument(); - expect(screen.getAllByRole('img')).toHaveLength(5); + it('should render action bar and datasources', async () => { + getDataSourcesMock.mockResolvedValue(getMockDataSources(5)); + + setup(); + + 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); }); }); From 6131879a37d1b7e07117a000a48a272ea76fdd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 13:32:03 +0100 Subject: [PATCH 10/22] add comment for en dash character --- .../features/datasources/components/DataSourcesListHeader.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx index ce8907f704be..4961a22e1727 100644 --- a/public/app/features/datasources/components/DataSourcesListHeader.tsx +++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx @@ -10,6 +10,9 @@ const ascendingSortValue = 'alpha-asc'; const descendingSortValue = 'alpha-desc'; const sortOptions = [ + // We use this unicode 'en dash' character (U+2013), because it looks nicer + // than simple dash in this context. This is also used in the response of + // the `sorting` endpoint, which is used in the search dashboard page. { label: 'Sort by A–Z', value: ascendingSortValue }, { label: 'Sort by Z–A', value: descendingSortValue }, ]; From b069ea622614d2d9436f05e31f730b4097abdcf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 13:35:26 +0100 Subject: [PATCH 11/22] simplify sorting --- public/app/features/datasources/state/selectors.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index 8e45aad79e35..3585de24db05 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/public/app/features/datasources/state/selectors.ts @@ -8,11 +8,9 @@ export const getDataSources = (state: DataSourcesState) => { return regex.test(dataSource.name) || regex.test(dataSource.database) || regex.test(dataSource.type); }); - if (state.isSortAscending) { - return filteredDataSources.sort((a, b) => a.name.localeCompare(b.name)); - } else { - return filteredDataSources.sort((a, b) => b.name.localeCompare(a.name)); - } + return filteredDataSources.sort((a, b) => + state.isSortAscending ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name) + ); }; export const getFilteredDataSourcePlugins = (state: DataSourcesState) => { From af33549e04a2eb8e2b314c7e2af81728c0d40282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 13:57:29 +0100 Subject: [PATCH 12/22] add link to Build a Dashboard button --- .../app/features/datasources/components/DataSourcesList.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index 6bf35c7c2515..54d675749f68 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { DataSourceSettings } from '@grafana/data'; -import { Button, LinkButton, Card, Tag, useStyles2 } from '@grafana/ui'; +import { LinkButton, Card, Tag, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { contextSrv } from 'app/core/core'; @@ -86,9 +86,9 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, ]} - + Date: Tue, 22 Nov 2022 13:57:54 +0100 Subject: [PATCH 13/22] fix test --- public/app/features/connections/Connections.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/connections/Connections.test.tsx b/public/app/features/connections/Connections.test.tsx index b1d60efefe54..726ba96de5f9 100644 --- a/public/app/features/connections/Connections.test.tsx +++ b/public/app/features/connections/Connections.test.tsx @@ -1,4 +1,4 @@ -import { render, RenderResult, screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { render, RenderResult, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; From 6d8bb44929e952d9fe9cba095b9f41efbf65c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 14:28:47 +0100 Subject: [PATCH 14/22] test build a dashboard and explore buttons --- .../features/datasources/components/DataSourcesList.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/datasources/components/DataSourcesList.test.tsx b/public/app/features/datasources/components/DataSourcesList.test.tsx index b3fbc50ecc4c..883620f998fd 100644 --- a/public/app/features/datasources/components/DataSourcesList.test.tsx +++ b/public/app/features/datasources/components/DataSourcesList.test.tsx @@ -36,6 +36,8 @@ describe('', () => { expect(await screen.findAllByRole('listitem')).toHaveLength(3); expect(await screen.findAllByRole('heading')).toHaveLength(3); + expect(await screen.findAllByRole('link', { name: 'Build a Dashboard' })).toHaveLength(3); + expect(await screen.findAllByRole('link', { name: 'Explore' })).toHaveLength(3); }); it('should render all elements in the list item', async () => { From eb50cd6977d6a27ffb3fe5fa667d47a3ec9d5d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Tue, 22 Nov 2022 14:54:29 +0100 Subject: [PATCH 15/22] test sorting data source elements --- .../pages/DataSourcesListPage.test.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx index e16978f3e14d..705c180791b6 100644 --- a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx @@ -18,11 +18,12 @@ jest.mock('../api', () => ({ const getDataSourcesMock = getDataSources as jest.Mock; -const setup = () => { +const setup = (isSortAscending = true) => { const store = configureStore({ dataSources: { ...initialState, layoutMode: LayoutModes.Grid, + isSortAscending, }, navIndex, }); @@ -59,4 +60,31 @@ describe('Render', () => { expect(await screen.findByRole('heading', { name: 'dataSource-4' })).toBeInTheDocument(); expect(await screen.findAllByRole('img')).toHaveLength(5); }); + + describe('should render elements in sort order', () => { + it('ascending', async () => { + getDataSourcesMock.mockResolvedValue(getMockDataSources(5)); + setup(true); + + expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); + const dataSourceItems = await screen.findAllByRole('heading'); + + expect(dataSourceItems).toHaveLength(6); + expect(dataSourceItems[0]).toHaveTextContent('Configuration'); + expect(dataSourceItems[1]).toHaveTextContent('dataSource-0'); + expect(dataSourceItems[2]).toHaveTextContent('dataSource-1'); + }); + it('descending', async () => { + getDataSourcesMock.mockResolvedValue(getMockDataSources(5)); + setup(false); + + expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); + const dataSourceItems = await screen.findAllByRole('heading'); + + expect(dataSourceItems).toHaveLength(6); + expect(dataSourceItems[0]).toHaveTextContent('Configuration'); + expect(dataSourceItems[1]).toHaveTextContent('dataSource-4'); + expect(dataSourceItems[2]).toHaveTextContent('dataSource-3'); + }); + }); }); From e55d71319cc0ffe3daef6c6cf07f0f72f4ded892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 11:51:57 +0100 Subject: [PATCH 16/22] DataSourceAddButton: hide button when user has no permission --- .../datasources/components/DataSourceAddButton.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx index d585e6f17afd..ca57d7c4165d 100644 --- a/public/app/features/datasources/components/DataSourceAddButton.tsx +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -11,8 +11,10 @@ export function DataSourceAddButton() { const dataSourcesRoutes = useDataSourcesRoutes(); return ( - - Add new data source - + canCreateDataSource && ( + + Add new data source + + ) ); } From 2bb795e2b028020de39862f5fb697fa34bd3f056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 11:54:05 +0100 Subject: [PATCH 17/22] PageActionBar: remove unneeded '?' --- public/app/core/components/PageActionBar/PageActionBar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/core/components/PageActionBar/PageActionBar.tsx b/public/app/core/components/PageActionBar/PageActionBar.tsx index 72699bbca1b7..f85f0bab4656 100644 --- a/public/app/core/components/PageActionBar/PageActionBar.tsx +++ b/public/app/core/components/PageActionBar/PageActionBar.tsx @@ -41,9 +41,9 @@ export default class PageActionBar extends PureComponent { {sortPicker && ( )} {linkButton && {linkButton.title}} From 670bb060e262927213bc32d0d5abfe5c50cbb762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 11:55:02 +0100 Subject: [PATCH 18/22] DataSourcesList: hide explore button if user has no permission --- .../components/DataSourcesList.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index 54d675749f68..f02e56c2c0e6 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -89,16 +89,17 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, Build a Dashboard - - Explore - + {canExploreDataSources && ( + + Explore + + )} From 83c23f335e782e0155fef3398b2c59565cf18d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 11:57:47 +0100 Subject: [PATCH 19/22] DataSourcesListPage.test: make setup prop explicit --- .../datasources/pages/DataSourcesListPage.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx index 705c180791b6..bb36ee8278c6 100644 --- a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx @@ -18,12 +18,12 @@ jest.mock('../api', () => ({ const getDataSourcesMock = getDataSources as jest.Mock; -const setup = (isSortAscending = true) => { +const setup = (options: { isSortAscending: boolean }) => { const store = configureStore({ dataSources: { ...initialState, layoutMode: LayoutModes.Grid, - isSortAscending, + isSortAscending: options.isSortAscending, }, navIndex, }); @@ -37,7 +37,7 @@ const setup = (isSortAscending = true) => { describe('Render', () => { it('should render component', async () => { - setup(); + setup({ isSortAscending: true }); expect(await screen.findByRole('heading', { name: 'Configuration' })).toBeInTheDocument(); expect(await screen.findByRole('link', { name: 'Documentation' })).toBeInTheDocument(); @@ -49,7 +49,7 @@ describe('Render', () => { it('should render action bar and datasources', async () => { getDataSourcesMock.mockResolvedValue(getMockDataSources(5)); - setup(); + setup({ isSortAscending: true }); expect(await screen.findByPlaceholderText('Search by name or type')).toBeInTheDocument(); expect(await screen.findByRole('combobox', { name: 'Sort' })).toBeInTheDocument(); @@ -64,7 +64,7 @@ describe('Render', () => { describe('should render elements in sort order', () => { it('ascending', async () => { getDataSourcesMock.mockResolvedValue(getMockDataSources(5)); - setup(true); + setup({ isSortAscending: true }); expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); const dataSourceItems = await screen.findAllByRole('heading'); @@ -76,7 +76,7 @@ describe('Render', () => { }); it('descending', async () => { getDataSourcesMock.mockResolvedValue(getMockDataSources(5)); - setup(false); + setup({ isSortAscending: false }); expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); const dataSourceItems = await screen.findAllByRole('heading'); From ad433c3a9f2e2d4b2888a705d2a9078577e1dac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 12:00:43 +0100 Subject: [PATCH 20/22] DataSourcesList: use theme.spacing --- .../app/features/datasources/components/DataSourcesList.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index f02e56c2c0e6..39cc16c1f4d4 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import React from 'react'; -import { DataSourceSettings } from '@grafana/data'; +import { DataSourceSettings, GrafanaTheme2 } from '@grafana/data'; import { LinkButton, Card, Tag, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; @@ -110,7 +110,7 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, ); } -const getStyles = () => { +const getStyles = (theme: GrafanaTheme2) => { return { list: css({ listStyle: 'none', @@ -121,7 +121,7 @@ const getStyles = () => { objectFit: 'contain', }), button: css({ - marginLeft: '16px', + marginLeft: theme.spacing(2), }), }; }; From e1473c670c3ff9f23aebe3c1f5f2ceda9b809d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 12:20:32 +0100 Subject: [PATCH 21/22] datasources: assure explore url includes appSubUrl --- public/app/features/datasources/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/datasources/utils.ts b/public/app/features/datasources/utils.ts index 9137a23485ce..7da4a2d41fc6 100644 --- a/public/app/features/datasources/utils.ts +++ b/public/app/features/datasources/utils.ts @@ -1,4 +1,4 @@ -import { DataSourceJsonData, DataSourceSettings, urlUtil } from '@grafana/data'; +import { DataSourceJsonData, DataSourceSettings, urlUtil, locationUtil } from '@grafana/data'; interface ItemWithName { name: string; @@ -50,7 +50,7 @@ function getNewName(name: string) { export const constructDataSourceExploreUrl = (dataSource: DataSourceSettings) => { const exploreState = JSON.stringify({ datasource: dataSource.name, context: 'explore' }); - const exploreUrl = urlUtil.renderUrl('/explore', { left: exploreState }); + const exploreUrl = urlUtil.renderUrl(locationUtil.assureBaseUrl('/explore'), { left: exploreState }); return exploreUrl; }; From feaed68fff02f60c178c1e6ce8986f002bd8c82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Thu, 24 Nov 2022 13:35:23 +0100 Subject: [PATCH 22/22] fix tests and add test case for missing permissions --- .../features/connections/Connections.test.tsx | 3 +++ .../components/DataSourcesList.test.tsx | 15 +++++++++++++++ .../pages/DataSourcesListPage.test.tsx | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/public/app/features/connections/Connections.test.tsx b/public/app/features/connections/Connections.test.tsx index 726ba96de5f9..043e09321d1f 100644 --- a/public/app/features/connections/Connections.test.tsx +++ b/public/app/features/connections/Connections.test.tsx @@ -4,6 +4,7 @@ import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { locationService } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; import { getMockDataSources } from 'app/features/datasources/__mocks__'; import * as api from 'app/features/datasources/api'; import { configureStore } from 'app/store/configureStore'; @@ -14,6 +15,7 @@ import Connections from './Connections'; import { navIndex } from './__mocks__/store.navIndex.mock'; import { ROUTE_BASE_ID, ROUTES } from './constants'; +jest.mock('app/core/services/context_srv'); jest.mock('app/features/datasources/api'); const renderPage = ( @@ -36,6 +38,7 @@ describe('Connections', () => { beforeEach(() => { (api.getDataSources as jest.Mock) = jest.fn().mockResolvedValue(mockDatasources); + (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true); }); test('shows the "Data sources" page by default', async () => { diff --git a/public/app/features/datasources/components/DataSourcesList.test.tsx b/public/app/features/datasources/components/DataSourcesList.test.tsx index 883620f998fd..bb6cf117858a 100644 --- a/public/app/features/datasources/components/DataSourcesList.test.tsx +++ b/public/app/features/datasources/components/DataSourcesList.test.tsx @@ -2,12 +2,15 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; +import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { getMockDataSources } from '../__mocks__'; import { DataSourcesListView } from './DataSourcesList'; +jest.mock('app/core/services/context_srv'); + const setup = () => { const store = configureStore(); @@ -24,6 +27,10 @@ const setup = () => { }; describe('', () => { + beforeEach(() => { + (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true); + }); + it('should render action bar', async () => { setup(); @@ -46,4 +53,12 @@ describe('', () => { expect(await screen.findByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); expect(await screen.findByRole('link', { name: 'dataSource-0' })).toBeInTheDocument(); }); + + it('should not render Explore button if user has no permissions', async () => { + (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(false); + setup(); + + expect(await screen.findAllByRole('link', { name: 'Build a Dashboard' })).toHaveLength(3); + expect(screen.queryAllByRole('link', { name: 'Explore' })).toHaveLength(0); + }); }); diff --git a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx index bb36ee8278c6..ced9e960885a 100644 --- a/public/app/features/datasources/pages/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/pages/DataSourcesListPage.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { LayoutModes } from '@grafana/data'; +import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { navIndex, getMockDataSources } from '../__mocks__'; @@ -11,6 +12,7 @@ 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([]), @@ -36,6 +38,10 @@ const setup = (options: { isSortAscending: boolean }) => { }; describe('Render', () => { + beforeEach(() => { + (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true); + }); + it('should render component', async () => { setup({ isSortAscending: true }); @@ -46,6 +52,17 @@ describe('Render', () => { expect(await screen.findByRole('link', { name: 'Add new data source' })).toBeInTheDocument(); }); + it('should not render "Add new data source" button if user has no permissions', async () => { + (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(false); + setup({ isSortAscending: true }); + + expect(await screen.findByRole('heading', { name: 'Configuration' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Documentation' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Support' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Community' })).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'Add new data source' })).toBeNull(); + }); + it('should render action bar and datasources', async () => { getDataSourcesMock.mockResolvedValue(getMockDataSources(5));