Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connections: Update "Your connections/Data sources" page #58589

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c34e0da
navtree.go: update Data sources title and subtitle
mikkancso Nov 10, 2022
84adf11
DataSourceList: move add button to header
mikkancso Nov 10, 2022
ae7c531
DataSourcesList: add buttons to items
mikkancso Nov 10, 2022
52814ff
DataSourcesListHeader: add sort picker
mikkancso Nov 10, 2022
c12acb7
fix css
mikkancso Nov 10, 2022
f94dec8
tests: look for the updated "Add new data source" text
leventebalogh Nov 22, 2022
82065bb
tests: use an async test method to verify component updates are wrapp…
leventebalogh Nov 22, 2022
6dcfeff
update e2e selector for add data source button
mikkancso Nov 22, 2022
4617339
fix DataSourceList{,Page} tests
mikkancso Nov 22, 2022
6131879
add comment for en dash character
mikkancso Nov 22, 2022
b069ea6
simplify sorting
mikkancso Nov 22, 2022
af33549
add link to Build a Dashboard button
mikkancso Nov 22, 2022
44175ca
fix test
mikkancso Nov 22, 2022
6d8bb44
test build a dashboard and explore buttons
mikkancso Nov 22, 2022
eb50cd6
test sorting data source elements
mikkancso Nov 22, 2022
e55d713
DataSourceAddButton: hide button when user has no permission
mikkancso Nov 24, 2022
2bb795e
PageActionBar: remove unneeded '?'
mikkancso Nov 24, 2022
670bb06
DataSourcesList: hide explore button if user has no permission
mikkancso Nov 24, 2022
83c23f3
DataSourcesListPage.test: make setup prop explicit
mikkancso Nov 24, 2022
ad433c3
DataSourcesList: use theme.spacing
mikkancso Nov 24, 2022
e1473c6
datasources: assure explore url includes appSubUrl
mikkancso Nov 24, 2022
feaed68
fix tests and add test case for missing permissions
mikkancso Nov 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/grafana-e2e-selectors/src/selectors/pages.ts
Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion packages/grafana-ui/src/themes/GlobalStyles/page.ts
Expand Up @@ -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)};
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/services/navtree/navtreeimpl/navtree.go
Expand Up @@ -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",
}},
})
Expand Down
24 changes: 23 additions & 1 deletion 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<SelectableValue[]>;
leventebalogh marked this conversation as resolved.
Show resolved Hide resolved
};
}

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) {
Expand All @@ -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}
mikkancso marked this conversation as resolved.
Show resolved Hide resolved
value={sortPicker?.value}
getSortOptions={sortPicker?.getSortOptions}
/>
)}
{linkButton && <LinkButton {...linkProps}>{linkButton.title}</LinkButton>}
</div>
);
Expand Down
18 changes: 15 additions & 3 deletions public/app/features/connections/Connections.test.tsx
Expand Up @@ -43,7 +43,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();
});

Expand All @@ -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',
Expand All @@ -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();
});
});
@@ -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()}>
Copy link
Contributor

@jackw jackw Nov 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we pass the DatasourceAddButton component as is to the actions prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't, because if we do then it's not gonna be rendered.

<Page.Contents>
<DataSourcesList />
</Page.Contents>
Expand Down
@@ -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 (
<LinkButton icon="plus" href={dataSourcesRoutes.New} disabled={!canCreateDataSource}>
mikkancso marked this conversation as resolved.
Show resolved Hide resolved
Add new data source
</LinkButton>
);
}
Expand Up @@ -24,17 +24,26 @@ const setup = () => {
};

describe('<DataSourcesList>', () => {
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);
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();
});
});
22 changes: 21 additions & 1 deletion public/app/features/datasources/components/DataSourcesList.tsx
Expand Up @@ -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 { 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';

Expand Down Expand Up @@ -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 />;
Expand Down Expand Up @@ -83,6 +85,21 @@ 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>
<LinkButton
icon="compass"
fill="outline"
variant="secondary"
className={styles.button}
href={constructDataSourceExploreUrl(dataSource)}
disabled={!canExploreDataSources}
mikkancso marked this conversation as resolved.
Show resolved Hide resolved
mikkancso marked this conversation as resolved.
Show resolved Hide resolved
>
Explore
</LinkButton>
</Card.Tags>
</Card>
</li>
);
Expand All @@ -102,5 +119,8 @@ const getStyles = () => {
logo: css({
objectFit: 'contain',
}),
button: css({
marginLeft: '16px',
mikkancso marked this conversation as resolved.
Show resolved Hide resolved
}),
};
};
@@ -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} />
);
}