mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Connections: Update "Your connections/Data sources" page (#58589)
* navtree.go: update Data sources title and subtitle * DataSourceList: move add button to header * DataSourcesList: add buttons to items The action buttons are added inside `<Card.Tags>` 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. * DataSourcesListHeader: add sort picker * fix css * tests: look for the updated "Add new data source" text * tests: use an async test method to verify component updates are wrapped in an act() * update e2e selector for add data source button * fix DataSourceList{,Page} tests * add comment for en dash character * simplify sorting * add link to Build a Dashboard button * fix test * test build a dashboard and explore buttons * test sorting data source elements * DataSourceAddButton: hide button when user has no permission * PageActionBar: remove unneeded '?' * DataSourcesList: hide explore button if user has no permission * DataSourcesListPage.test: make setup prop explicit * DataSourcesList: use theme.spacing * datasources: assure explore url includes appSubUrl * fix tests and add test case for missing permissions Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
parent
312dbc979e
commit
c72322874d
@ -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',
|
||||
|
@ -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)};
|
||||
}
|
||||
}
|
||||
|
@ -554,8 +554,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",
|
||||
}},
|
||||
})
|
||||
|
@ -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<SelectableValue[]>;
|
||||
};
|
||||
}
|
||||
|
||||
export default class PageActionBar extends PureComponent<Props> {
|
||||
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<Props> {
|
||||
<div className="gf-form gf-form--grow">
|
||||
<FilterInput value={searchQuery} onChange={setSearchQuery} placeholder={placeholder} />
|
||||
</div>
|
||||
{sortPicker && (
|
||||
<SortPicker
|
||||
onChange={sortPicker.onChange}
|
||||
value={sortPicker.value}
|
||||
getSortOptions={sortPicker.getSortOptions}
|
||||
/>
|
||||
)}
|
||||
{linkButton && <LinkButton {...linkProps}>{linkButton.title}</LinkButton>}
|
||||
</div>
|
||||
);
|
||||
|
@ -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 () => {
|
||||
@ -43,7 +46,8 @@ 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.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 +61,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 +95,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();
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
<Page navId={'connections-your-connections-datasources'}>
|
||||
<Page navId={'connections-your-connections-datasources'} actions={DataSourceAddButton()}>
|
||||
<Page.Contents>
|
||||
<DataSourcesList />
|
||||
</Page.Contents>
|
||||
|
@ -0,0 +1,20 @@
|
||||
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 (
|
||||
canCreateDataSource && (
|
||||
<LinkButton icon="plus" href={dataSourcesRoutes.New}>
|
||||
Add new data source
|
||||
</LinkButton>
|
||||
)
|
||||
);
|
||||
}
|
@ -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,17 +27,38 @@ const setup = () => {
|
||||
};
|
||||
|
||||
describe('<DataSourcesList>', () => {
|
||||
it('should render list of datasources', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(3);
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(3);
|
||||
beforeEach(() => {
|
||||
(contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should render all elements in the list item', () => {
|
||||
it('should render action bar', async () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'dataSource-0' })).toBeInTheDocument();
|
||||
expect(await screen.findByPlaceholderText('Search by name or type')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('combobox', { name: 'Sort' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render list of datasources', async () => {
|
||||
setup();
|
||||
|
||||
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 () => {
|
||||
setup();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { Card, Tag, useStyles2 } from '@grafana/ui';
|
||||
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';
|
||||
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 <PageLoader />;
|
||||
@ -83,6 +85,22 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading,
|
||||
dataSource.isDefault && <Tag key="default-tag" name={'default'} colorIndex={1} />,
|
||||
]}
|
||||
</Card.Meta>
|
||||
<Card.Tags>
|
||||
<LinkButton icon="apps" fill="outline" variant="secondary" href="/dashboard/new">
|
||||
Build a Dashboard
|
||||
</LinkButton>
|
||||
{canExploreDataSources && (
|
||||
<LinkButton
|
||||
icon="compass"
|
||||
fill="outline"
|
||||
variant="secondary"
|
||||
className={styles.button}
|
||||
href={constructDataSourceExploreUrl(dataSource)}
|
||||
>
|
||||
Explore
|
||||
</LinkButton>
|
||||
)}
|
||||
</Card.Tags>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
@ -92,7 +110,7 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading,
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
list: css({
|
||||
listStyle: 'none',
|
||||
@ -102,5 +120,8 @@ const getStyles = () => {
|
||||
logo: css({
|
||||
objectFit: 'contain',
|
||||
}),
|
||||
button: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -1,42 +1,40 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
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, getDataSourcesSort, setDataSourcesSearchQuery, setIsSortAscending } from '../state';
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
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 (
|
||||
<DataSourcesListHeaderView
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
canCreateDataSource={canCreateDataSource}
|
||||
/>
|
||||
const setSort = useCallback(
|
||||
(sort: SelectableValue) => dispatch(setIsSortAscending(sort.value === ascendingSortValue)),
|
||||
[dispatch]
|
||||
);
|
||||
}
|
||||
const isSortAscending = useSelector(({ dataSources }: StoreState) => getDataSourcesSort(dataSources));
|
||||
|
||||
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,
|
||||
const sortPicker = {
|
||||
onChange: setSort,
|
||||
value: isSortAscending ? ascendingSortValue : descendingSortValue,
|
||||
getSortOptions: () => Promise.resolve(sortOptions),
|
||||
};
|
||||
|
||||
return (
|
||||
<PageActionBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} linkButton={linkButton} key="action-bar" />
|
||||
<PageActionBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} key="action-bar" sortPicker={sortPicker} />
|
||||
);
|
||||
}
|
||||
|
@ -2,28 +2,30 @@ 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 { contextSrv } from 'app/core/services/context_srv';
|
||||
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('app/core/services/context_srv');
|
||||
jest.mock('../api', () => ({
|
||||
...jest.requireActual('../api'),
|
||||
getDataSources: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
const setup = (stateOverride?: Partial<DataSourcesState>) => {
|
||||
const getDataSourcesMock = getDataSources as jest.Mock;
|
||||
|
||||
const setup = (options: { isSortAscending: boolean }) => {
|
||||
const store = configureStore({
|
||||
dataSources: {
|
||||
...initialState,
|
||||
dataSources: [] as DataSourceSettings[],
|
||||
layoutMode: LayoutModes.Grid,
|
||||
hasFetched: false,
|
||||
...stateOverride,
|
||||
isSortAscending: options.isSortAscending,
|
||||
},
|
||||
navIndex,
|
||||
});
|
||||
@ -36,28 +38,70 @@ const setup = (stateOverride?: Partial<DataSourcesState>) => {
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
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();
|
||||
beforeEach(() => {
|
||||
(contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should render action bar and datasources', () => {
|
||||
setup({
|
||||
dataSources: getMockDataSources(5),
|
||||
dataSourcesCount: 5,
|
||||
hasFetched: true,
|
||||
});
|
||||
it('should render component', async () => {
|
||||
setup({ isSortAscending: 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);
|
||||
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 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));
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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('Configuration');
|
||||
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('Configuration');
|
||||
expect(dataSourceItems[1]).toHaveTextContent('dataSource-4');
|
||||
expect(dataSourceItems[2]).toHaveTextContent('dataSource-3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
<Page navId="datasources">
|
||||
<Page navId="datasources" actions={DataSourceAddButton()}>
|
||||
<Page.Contents>
|
||||
<DataSourcesList />
|
||||
</Page.Contents>
|
||||
|
@ -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 => {
|
||||
|
@ -19,6 +19,7 @@ export const initialState: DataSourcesState = {
|
||||
hasFetched: false,
|
||||
isLoadingDataSources: false,
|
||||
dataSourceMeta: {} as DataSourcePluginMeta,
|
||||
isSortAscending: true,
|
||||
};
|
||||
|
||||
export const dataSourceLoaded = createAction<DataSourceSettings>('dataSources/dataSourceLoaded');
|
||||
@ -33,6 +34,7 @@ export const setDataSourcesLayoutMode = createAction<LayoutMode>('dataSources/se
|
||||
export const setDataSourceTypeSearchQuery = createAction<string>('dataSources/setDataSourceTypeSearchQuery');
|
||||
export const setDataSourceName = createAction<string>('dataSources/setDataSourceName');
|
||||
export const setIsDefault = createAction<boolean>('dataSources/setIsDefault');
|
||||
export const setIsSortAscending = createAction<boolean>('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;
|
||||
};
|
||||
|
||||
|
@ -4,9 +4,13 @@ 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);
|
||||
});
|
||||
|
||||
return filteredDataSources.sort((a, b) =>
|
||||
state.isSortAscending ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||
);
|
||||
};
|
||||
|
||||
export const getFilteredDataSourcePlugins = (state: DataSourcesState) => {
|
||||
@ -35,3 +39,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;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { DataSourceJsonData, DataSourceSettings, urlUtil, locationUtil } 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<DataSourceJsonData, {}>) => {
|
||||
const exploreState = JSON.stringify({ datasource: dataSource.name, context: 'explore' });
|
||||
const exploreUrl = urlUtil.renderUrl(locationUtil.assureBaseUrl('/explore'), { left: exploreState });
|
||||
|
||||
return exploreUrl;
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ export interface DataSourcesState {
|
||||
isLoadingDataSources: boolean;
|
||||
plugins: DataSourcePluginMeta[];
|
||||
categories: DataSourcePluginCategory[];
|
||||
isSortAscending: boolean;
|
||||
}
|
||||
|
||||
export interface TestingStatus {
|
||||
|
Loading…
Reference in New Issue
Block a user