Commit 6db4b40d by Alex Khomenko Committed by GitHub

Data source list: Use Card component (#31326)

* Replace DataSourcesListItem with Card

* Add tests

* Remove unused styles

* Make card heading semi bold

* Make heading semi-bold

* Show type name instead of type id

* Fix key warning

* Update Card

* Fix tests

* Make typeName optional

* remove styling that was just a test

* Make typeName non-optional and fix tests

* Update list key

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 6e549bc9
......@@ -549,6 +549,7 @@ export interface DataSourceSettings<T extends DataSourceJsonData = DataSourceJso
name: string;
typeLogoUrl: string;
type: string;
typeName: string;
access: string;
url: string;
password: string;
......
......@@ -97,7 +97,7 @@ export const Card: CardInterface = ({
);
const hasActions = Boolean(actions || secondaryActions);
const disableHover = disabled || !onClick;
const disableHover = disabled || (!onClick && !href);
const disableEvents = disabled && !actions;
const containerStyles = getContainerStyles(theme, disableEvents, disableHover);
......@@ -196,6 +196,8 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
margin-bottom: 0;
font-size: ${theme.typography.size.md};
line-height: ${theme.typography.lineHeight.xs};
color: ${theme.colors.text};
font-weight: ${theme.typography.weight.semibold};
`,
info: css`
display: flex;
......@@ -221,7 +223,7 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
`,
media: css`
margin-right: ${theme.spacing.md};
max-width: 40px;
width: 40px;
& > * {
width: 100%;
}
......@@ -299,7 +301,11 @@ const Meta: FC<ChildProps & { separator?: string }> = memo(({ children, styles,
// Join meta data elements by separator
if (Array.isArray(children) && separator) {
meta = React.Children.toArray(children).reduce((prev, curr, i) => [
const filtered = React.Children.toArray(children).filter(Boolean);
if (!filtered.length) {
return null;
}
meta = filtered.reduce((prev, curr, i) => [
prev,
<span key={`separator_${i}`} className={styles?.separator}>
{separator}
......
......@@ -9,6 +9,7 @@ const setup = (propOverrides?: object) => {
orgId: 1,
name: 'gdev-influxdb',
type: 'influxdb',
typeName: 'Influxdb',
typeLogoUrl: '',
access: 'direct',
url: 'http://localhost:8086',
......
......@@ -10,6 +10,7 @@ const settingsMock: DataSourceSettings<any, any> = {
orgId: 1,
name: 'gdev-influxdb',
type: 'influxdb',
typeName: 'Influxdb',
typeLogoUrl: '',
access: 'direct',
url: 'http://localhost:8086',
......
......@@ -36,6 +36,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response {
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
TypeName: ds.Type,
Access: ds.Access,
Password: ds.Password,
Database: ds.Database,
......@@ -48,6 +49,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response {
if plugin, exists := plugins.DataSources[ds.Type]; exists {
dsItem.TypeLogoUrl = plugin.Info.Logos.Small
dsItem.TypeName = plugin.Name
} else {
dsItem.TypeLogoUrl = "public/img/icn-datasource.svg"
}
......
......@@ -36,6 +36,7 @@ type DataSourceListItemDTO struct {
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Type string `json:"type"`
TypeName string `json:"typeName"`
TypeLogoUrl string `json:"typeLogoUrl"`
Access models.DsAccess `json:"access"`
Url string `json:"url"`
......
import React from 'react';
import { shallow } from 'enzyme';
import { render, screen } from '@testing-library/react';
import DataSourcesList from './DataSourcesList';
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
......@@ -10,13 +10,20 @@ const setup = () => {
layoutMode: LayoutModes.Grid,
};
return shallow(<DataSourcesList {...props} />);
return render(<DataSourcesList {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
describe('DataSourcesList', () => {
it('should render list of datasources', () => {
setup();
expect(screen.getAllByRole('listitem')).toHaveLength(3);
expect(screen.getAllByRole('heading')).toHaveLength(3);
});
expect(wrapper).toMatchSnapshot();
it('should render all elements in the list item', () => {
setup();
expect(screen.getByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'dataSource-0 dataSource-0' })).toBeInTheDocument();
expect(screen.getByAltText('dataSource-0')).toBeInTheDocument();
});
});
// Libraries
import React, { PureComponent } from 'react';
import classNames from 'classnames';
// Components
import DataSourcesListItem from './DataSourcesListItem';
import React, { FC } from 'react';
// Types
import { DataSourceSettings } from '@grafana/data';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
import { Card, Tag, useStyles } from '@grafana/ui';
import { css } from 'emotion';
export interface Props {
dataSources: DataSourceSettings[];
layoutMode: LayoutMode;
}
export class DataSourcesList extends PureComponent<Props> {
render() {
const { dataSources, layoutMode } = this.props;
const listStyle = classNames({
'card-section': true,
'card-list-layout-grid': layoutMode === LayoutModes.Grid,
'card-list-layout-list': layoutMode === LayoutModes.List,
});
export const DataSourcesList: FC<Props> = ({ dataSources, layoutMode }) => {
const styles = useStyles(getStyles);
return (
<section className={listStyle}>
<ol className="card-list">
<ul className={styles.list}>
{dataSources.map((dataSource, index) => {
return <DataSourcesListItem dataSource={dataSource} key={`${dataSource.id}-${index}`} />;
return (
<li key={dataSource.id}>
<Card heading={dataSource.name} href={`datasources/edit/${dataSource.id}`}>
<Card.Figure>
<img src={dataSource.typeLogoUrl} alt={dataSource.name} />
</Card.Figure>
<Card.Meta>
{[
dataSource.typeName,
dataSource.url,
dataSource.isDefault && <Tag key="default-tag" name={'default'} colorIndex={1} />,
]}
</Card.Meta>
</Card>
</li>
);
})}
</ol>
</section>
</ul>
);
}
}
};
export default DataSourcesList;
const getStyles = () => {
return {
list: css`
list-style: none;
`,
};
};
import React from 'react';
import { shallow } from 'enzyme';
import DataSourcesListItem from './DataSourcesListItem';
import { getMockDataSource } from './__mocks__/dataSourcesMocks';
const setup = () => {
const props = {
dataSource: getMockDataSource(),
};
return shallow(<DataSourcesListItem {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});
import React, { PureComponent } from 'react';
import { DataSourceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
export interface Props {
dataSource: DataSourceSettings;
}
export class DataSourcesListItem extends PureComponent<Props> {
render() {
const { dataSource } = this.props;
return (
<li className="card-item-wrapper">
<a className="card-item" href={`datasources/edit/${dataSource.id}`}>
<div className="card-item-header">
<div className="card-item-type">{dataSource.type}</div>
</div>
<div className="card-item-body">
<figure className="card-item-figure">
<img src={dataSource.typeLogoUrl} alt={dataSource.name} />
</figure>
<div className="card-item-details">
<div className="card-item-name" aria-label={selectors.pages.DataSources.dataSources(dataSource.name)}>
{dataSource.name}
{dataSource.isDefault && <span className="btn btn-secondary btn-small card-item-label">default</span>}
</div>
<div className="card-item-sub-name">{dataSource.url}</div>
</div>
</div>
</a>
</li>
);
}
}
export default DataSourcesListItem;
......@@ -49,11 +49,7 @@ const emptyListModel = {
export class DataSourcesListPage extends PureComponent<Props> {
componentDidMount() {
this.fetchDataSources();
}
async fetchDataSources() {
return await this.props.loadDataSources();
this.props.loadDataSources();
}
render() {
......
......@@ -3,7 +3,7 @@ import { DataSourceSettings } from '@grafana/data';
export const getMockDataSources = (amount: number) => {
const dataSources = [];
for (let i = 0; i <= amount; i++) {
for (let i = 0; i < amount; i++) {
dataSources.push({
access: '',
basicAuth: false,
......@@ -37,6 +37,7 @@ export const getMockDataSource = (): DataSourceSettings => {
isDefault: false,
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
name: 'gdev-cloudwatch',
typeName: 'Cloudwatch',
orgId: 1,
password: '',
readOnly: false,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<section
className="card-section card-list-layout-grid"
>
<ol
className="card-list"
>
<DataSourcesListItem
dataSource={
Object {
"access": "",
"basicAuth": false,
"database": "database-0",
"id": 0,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "dataSource-0",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
}
}
key="0-0"
/>
<DataSourcesListItem
dataSource={
Object {
"access": "",
"basicAuth": false,
"database": "database-1",
"id": 1,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "dataSource-1",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
}
}
key="1-1"
/>
<DataSourcesListItem
dataSource={
Object {
"access": "",
"basicAuth": false,
"database": "database-2",
"id": 2,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "dataSource-2",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
}
}
key="2-2"
/>
<DataSourcesListItem
dataSource={
Object {
"access": "",
"basicAuth": false,
"database": "database-3",
"id": 3,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "dataSource-3",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
}
}
key="3-3"
/>
</ol>
</section>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<li
className="card-item-wrapper"
>
<a
className="card-item"
href="datasources/edit/13"
>
<div
className="card-item-header"
>
<div
className="card-item-type"
>
cloudwatch
</div>
</div>
<div
className="card-item-body"
>
<figure
className="card-item-figure"
>
<img
alt="gdev-cloudwatch"
src="public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"
/>
</figure>
<div
className="card-item-details"
>
<div
aria-label="Data source list item gdev-cloudwatch"
className="card-item-name"
>
gdev-cloudwatch
</div>
<div
className="card-item-sub-name"
/>
</div>
</div>
</a>
</li>
`;
......@@ -125,25 +125,6 @@ exports[`Render should render action bar and datasources 1`] = `
"url": "",
"user": "",
},
Object {
"access": "",
"basicAuth": false,
"database": "database-5",
"id": 5,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "dataSource-5",
"orgId": 1,
"password": "",
"readOnly": false,
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"url": "",
"user": "",
},
]
}
key="list"
......
......@@ -7,6 +7,7 @@ export function createDatasourceSettings<T>(jsonData: T): DataSourceSettings<T>
name: 'datasource-test',
typeLogoUrl: '',
type: 'datasource',
typeName: 'Datasource',
access: 'server',
url: 'http://localhost',
password: '',
......
......@@ -48,6 +48,7 @@ exports[`Render should render alpha info text 1`] = `
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
......@@ -81,6 +82,7 @@ exports[`Render should render alpha info text 1`] = `
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
......@@ -198,6 +200,7 @@ exports[`Render should render beta info text 1`] = `
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
......@@ -257,6 +260,7 @@ exports[`Render should render component 1`] = `
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
......@@ -321,6 +325,7 @@ exports[`Render should render is ready only message 1`] = `
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
......@@ -354,6 +359,7 @@ exports[`Render should render is ready only message 1`] = `
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
......
......@@ -83,6 +83,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
password: '',
readOnly: false,
type: 'Loading',
typeName: 'Loading',
typeLogoUrl: 'public/img/icn-datasource.svg',
url: '',
user: '',
......
......@@ -40,7 +40,7 @@ const mockPlugin = () =>
describe('dataSourcesReducer', () => {
describe('when dataSourcesLoaded is dispatched', () => {
it('then state should be correct', () => {
const dataSources = getMockDataSources(0);
const dataSources = getMockDataSources(1);
reducerTester<DataSourcesState>()
.givenReducer(dataSourcesReducer, initialState)
......
......@@ -28,6 +28,7 @@ const setup = (propOverrides?: object) => {
url: '',
database: '',
type: 'cloudwatch',
typeName: 'Cloudwatch',
user: '',
password: '',
basicAuth: false,
......
......@@ -9,6 +9,7 @@ const setup = (propOverrides?: object) => {
orgId: 1,
name: 'Azure Monitor-10-10',
type: 'grafana-azure-monitor-datasource',
typeName: 'Azure',
typeLogoUrl: '',
access: 'proxy',
url: '',
......
......@@ -10,6 +10,7 @@ const setup = () => {
name: 'Azure Monitor-10-10',
type: 'grafana-azure-monitor-datasource',
typeLogoUrl: '',
typeName: 'Azure',
access: 'proxy',
url: '',
password: '',
......
......@@ -10,6 +10,7 @@ const setup = (propOverrides?: object) => {
name: 'Azure Monitor-10-10',
type: 'grafana-azure-monitor-datasource',
typeLogoUrl: '',
typeName: 'Azure',
access: 'proxy',
url: '',
password: '',
......
......@@ -30,6 +30,7 @@ exports[`Render should render component 1`] = `
"secureJsonFields": Object {},
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"url": "",
"user": "",
"version": 1,
......@@ -67,6 +68,7 @@ exports[`Render should render component 1`] = `
"secureJsonFields": Object {},
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"url": "/api/datasources/proxy/21",
"user": "",
"version": 1,
......@@ -106,6 +108,7 @@ exports[`Render should render component 1`] = `
"secureJsonFields": Object {},
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"url": "",
"user": "",
"version": 1,
......@@ -141,6 +144,7 @@ exports[`Render should render component 1`] = `
"secureJsonFields": Object {},
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"url": "",
"user": "",
"version": 1,
......
......@@ -9,6 +9,7 @@ const setup = (propOverrides?: object) => {
orgId: 1,
name: 'InfluxDB-3',
type: 'influxdb',
typeName: 'Influx',
typeLogoUrl: '',
access: 'proxy',
url: '',
......
......@@ -92,6 +92,7 @@ exports[`Render should disable basic auth password input 1`] = `
"secureJsonFields": Object {},
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"url": "",
"user": "",
"version": 1,
......@@ -387,6 +388,7 @@ exports[`Render should hide basic auth fields when switch off 1`] = `
"secureJsonFields": Object {},
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"url": "",
"user": "",
"version": 1,
......@@ -682,6 +684,7 @@ exports[`Render should hide white listed cookies input when browser access chose
"secureJsonFields": Object {},
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"url": "",
"user": "",
"version": 1,
......@@ -977,6 +980,7 @@ exports[`Render should render component 1`] = `
"secureJsonFields": Object {},
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"url": "",
"user": "",
"version": 1,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment