mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'data-sources-list-to-react'
This commit is contained in:
commit
f37a60dcd5
22
public/app/features/datasources/DataSourceList.test.tsx
Normal file
22
public/app/features/datasources/DataSourceList.test.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import DataSourcesList from './DataSourcesList';
|
||||
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
|
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
const setup = () => {
|
||||
const props = {
|
||||
dataSources: getMockDataSources(3),
|
||||
layoutMode: LayoutModes.Grid,
|
||||
};
|
||||
|
||||
return shallow(<DataSourcesList {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataSourcesActionBar, Props } from './DataSourcesActionBar';
|
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
layoutMode: LayoutModes.Grid,
|
||||
searchQuery: '',
|
||||
setDataSourcesLayoutMode: jest.fn(),
|
||||
setDataSourcesSearchQuery: jest.fn(),
|
||||
};
|
||||
|
||||
return shallow(<DataSourcesActionBar {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
62
public/app/features/datasources/DataSourcesActionBar.tsx
Normal file
62
public/app/features/datasources/DataSourcesActionBar.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
|
||||
import { getDataSourcesLayoutMode, getDataSourcesSearchQuery } from './state/selectors';
|
||||
|
||||
export interface Props {
|
||||
searchQuery: string;
|
||||
layoutMode: LayoutMode;
|
||||
setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode;
|
||||
setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery;
|
||||
}
|
||||
|
||||
export class DataSourcesActionBar extends PureComponent<Props> {
|
||||
onSearchQueryChange = event => {
|
||||
this.props.setDataSourcesSearchQuery(event.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { searchQuery, layoutMode, setDataSourcesLayoutMode } = this.props;
|
||||
|
||||
return (
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
placeholder="Filter by name or type"
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<LayoutSelector
|
||||
mode={layoutMode}
|
||||
onLayoutModeChanged={(mode: LayoutMode) => setDataSourcesLayoutMode(mode)}
|
||||
/>
|
||||
</div>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<a className="page-header__cta btn btn-success" href="datasources/new">
|
||||
<i className="fa fa-plus" />
|
||||
Add data source
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
searchQuery: getDataSourcesSearchQuery(state.dataSources),
|
||||
layoutMode: getDataSourcesLayoutMode(state.dataSources),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setDataSourcesLayoutMode,
|
||||
setDataSourcesSearchQuery,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DataSourcesActionBar);
|
34
public/app/features/datasources/DataSourcesList.tsx
Normal file
34
public/app/features/datasources/DataSourcesList.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import DataSourcesListItem from './DataSourcesListItem';
|
||||
import { DataSource } from 'app/types';
|
||||
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
export interface Props {
|
||||
dataSources: DataSource[];
|
||||
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,
|
||||
});
|
||||
|
||||
return (
|
||||
<section className={listStyle}>
|
||||
<ol className="card-list">
|
||||
{dataSources.map((dataSource, index) => {
|
||||
return <DataSourcesListItem dataSource={dataSource} key={`${dataSource.id}-${index}`} />;
|
||||
})}
|
||||
</ol>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DataSourcesList;
|
20
public/app/features/datasources/DataSourcesListItem.test.tsx
Normal file
20
public/app/features/datasources/DataSourcesListItem.test.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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();
|
||||
});
|
||||
});
|
35
public/app/features/datasources/DataSourcesListItem.tsx
Normal file
35
public/app/features/datasources/DataSourcesListItem.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DataSource } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
dataSource: DataSource;
|
||||
}
|
||||
|
||||
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} />
|
||||
</figure>
|
||||
<div className="card-item-details">
|
||||
<div className="card-item-name">
|
||||
{dataSource.name}
|
||||
{dataSource.isDefault && <span className="btn btn-secondary btn-mini">default</span>}
|
||||
</div>
|
||||
<div className="card-item-sub-name">{dataSource.url}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DataSourcesListItem;
|
37
public/app/features/datasources/DataSourcesListPage.test.tsx
Normal file
37
public/app/features/datasources/DataSourcesListPage.test.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataSourcesListPage, Props } from './DataSourcesListPage';
|
||||
import { DataSource, NavModel } from 'app/types';
|
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
dataSources: [] as DataSource[],
|
||||
layoutMode: LayoutModes.Grid,
|
||||
loadDataSources: jest.fn(),
|
||||
navModel: {} as NavModel,
|
||||
dataSourcesCount: 0,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<DataSourcesListPage {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render action bar and datasources', () => {
|
||||
const wrapper = setup({
|
||||
dataSources: getMockDataSources(5),
|
||||
dataSourcesCount: 5,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
76
public/app/features/datasources/DataSourcesListPage.tsx
Normal file
76
public/app/features/datasources/DataSourcesListPage.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import PageHeader from '../../core/components/PageHeader/PageHeader';
|
||||
import DataSourcesActionBar from './DataSourcesActionBar';
|
||||
import DataSourcesList from './DataSourcesList';
|
||||
import { loadDataSources } from './state/actions';
|
||||
import { getDataSources, getDataSourcesCount, getDataSourcesLayoutMode } from './state/selectors';
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import { DataSource, NavModel } from 'app/types';
|
||||
import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
dataSources: DataSource[];
|
||||
dataSourcesCount: number;
|
||||
layoutMode: LayoutMode;
|
||||
loadDataSources: typeof loadDataSources;
|
||||
}
|
||||
|
||||
const emptyListModel = {
|
||||
title: 'There are no data sources defined yet',
|
||||
buttonIcon: 'gicon gicon-add-datasources',
|
||||
buttonLink: 'datasources/new',
|
||||
buttonTitle: 'Add data source',
|
||||
proTip: 'You can also define data sources through configuration files.',
|
||||
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
|
||||
proTipLinkTitle: 'Learn more',
|
||||
proTipTarget: '_blank',
|
||||
};
|
||||
|
||||
export class DataSourcesListPage extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.fetchDataSources();
|
||||
}
|
||||
|
||||
async fetchDataSources() {
|
||||
return await this.props.loadDataSources();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dataSources, dataSourcesCount, navModel, layoutMode } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
<div className="page-container page-body">
|
||||
{dataSourcesCount === 0 ? (
|
||||
<EmptyListCTA model={emptyListModel} />
|
||||
) : (
|
||||
[
|
||||
<DataSourcesActionBar key="action-bar" />,
|
||||
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'datasources'),
|
||||
dataSources: getDataSources(state.dataSources),
|
||||
layoutMode: getDataSourcesLayoutMode(state.dataSources),
|
||||
dataSourcesCount: getDataSourcesCount(state.dataSources),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadDataSources,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage));
|
@ -0,0 +1,45 @@
|
||||
import { DataSource } from 'app/types';
|
||||
|
||||
export const getMockDataSources = (amount: number): DataSource[] => {
|
||||
const dataSources = [];
|
||||
|
||||
for (let i = 0; i <= amount; i++) {
|
||||
dataSources.push({
|
||||
access: '',
|
||||
basicAuth: false,
|
||||
database: `database-${i}`,
|
||||
id: i,
|
||||
isDefault: false,
|
||||
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
|
||||
name: `dataSource-${i}`,
|
||||
orgId: 1,
|
||||
password: '',
|
||||
readOnly: false,
|
||||
type: 'cloudwatch',
|
||||
typeLogoUrl: 'public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png',
|
||||
url: '',
|
||||
user: '',
|
||||
});
|
||||
}
|
||||
|
||||
return dataSources;
|
||||
};
|
||||
|
||||
export const getMockDataSource = (): DataSource => {
|
||||
return {
|
||||
access: '',
|
||||
basicAuth: false,
|
||||
database: '',
|
||||
id: 13,
|
||||
isDefault: false,
|
||||
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
|
||||
name: 'gdev-cloudwatch',
|
||||
orgId: 1,
|
||||
password: '',
|
||||
readOnly: false,
|
||||
type: 'cloudwatch',
|
||||
typeLogoUrl: 'public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png',
|
||||
url: '',
|
||||
user: '',
|
||||
};
|
||||
};
|
@ -0,0 +1,108 @@
|
||||
// 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>
|
||||
`;
|
@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<LayoutSelector
|
||||
mode="grid"
|
||||
onLayoutModeChanged={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="page-header__cta btn btn-success"
|
||||
href="datasources/new"
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
Add data source
|
||||
</a>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,45 @@
|
||||
// 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
|
||||
src="public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"
|
||||
/>
|
||||
</figure>
|
||||
<div
|
||||
className="card-item-details"
|
||||
>
|
||||
<div
|
||||
className="card-item-name"
|
||||
>
|
||||
gdev-cloudwatch
|
||||
</div>
|
||||
<div
|
||||
className="card-item-sub-name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
@ -0,0 +1,164 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render action bar and datasources 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<Connect(DataSourcesActionBar)
|
||||
key="action-bar"
|
||||
/>
|
||||
<DataSourcesList
|
||||
dataSources={
|
||||
Array [
|
||||
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": "",
|
||||
},
|
||||
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": "",
|
||||
},
|
||||
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": "",
|
||||
},
|
||||
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": "",
|
||||
},
|
||||
Object {
|
||||
"access": "",
|
||||
"basicAuth": false,
|
||||
"database": "database-4",
|
||||
"id": 4,
|
||||
"isDefault": false,
|
||||
"jsonData": Object {
|
||||
"authType": "credentials",
|
||||
"defaultRegion": "eu-west-2",
|
||||
},
|
||||
"name": "dataSource-4",
|
||||
"orgId": 1,
|
||||
"password": "",
|
||||
"readOnly": false,
|
||||
"type": "cloudwatch",
|
||||
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
"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"
|
||||
layoutMode="grid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<EmptyListCTA
|
||||
model={
|
||||
Object {
|
||||
"buttonIcon": "gicon gicon-add-datasources",
|
||||
"buttonLink": "datasources/new",
|
||||
"buttonTitle": "Add data source",
|
||||
"proTip": "You can also define data sources through configuration files.",
|
||||
"proTipLink": "http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list",
|
||||
"proTipLinkTitle": "Learn more",
|
||||
"proTipTarget": "_blank",
|
||||
"title": "There are no data sources defined yet",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
51
public/app/features/datasources/state/actions.ts
Normal file
51
public/app/features/datasources/state/actions.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { DataSource, StoreState } from 'app/types';
|
||||
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadDataSources = 'LOAD_DATA_SOURCES',
|
||||
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
|
||||
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
|
||||
}
|
||||
|
||||
export interface LoadDataSourcesAction {
|
||||
type: ActionTypes.LoadDataSources;
|
||||
payload: DataSource[];
|
||||
}
|
||||
|
||||
export interface SetDataSourcesSearchQueryAction {
|
||||
type: ActionTypes.SetDataSourcesSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetDataSourcesLayoutModeAction {
|
||||
type: ActionTypes.SetDataSourcesLayoutMode;
|
||||
payload: LayoutMode;
|
||||
}
|
||||
|
||||
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
|
||||
type: ActionTypes.LoadDataSources,
|
||||
payload: dataSources,
|
||||
});
|
||||
|
||||
export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({
|
||||
type: ActionTypes.SetDataSourcesSearchQuery,
|
||||
payload: searchQuery,
|
||||
});
|
||||
|
||||
export const setDataSourcesLayoutMode = (layoutMode: LayoutMode): SetDataSourcesLayoutModeAction => ({
|
||||
type: ActionTypes.SetDataSourcesLayoutMode,
|
||||
payload: layoutMode,
|
||||
});
|
||||
|
||||
export type Action = LoadDataSourcesAction | SetDataSourcesSearchQueryAction | SetDataSourcesLayoutModeAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
|
||||
export function loadDataSources(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const response = await getBackendSrv().get('/api/datasources');
|
||||
dispatch(dataSourcesLoaded(response));
|
||||
};
|
||||
}
|
29
public/app/features/datasources/state/reducers.ts
Normal file
29
public/app/features/datasources/state/reducers.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { DataSource, DataSourcesState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
const initialState: DataSourcesState = {
|
||||
dataSources: [] as DataSource[],
|
||||
layoutMode: LayoutModes.Grid,
|
||||
searchQuery: '',
|
||||
dataSourcesCount: 0,
|
||||
};
|
||||
|
||||
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadDataSources:
|
||||
return { ...state, dataSources: action.payload, dataSourcesCount: action.payload.length };
|
||||
|
||||
case ActionTypes.SetDataSourcesSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
|
||||
case ActionTypes.SetDataSourcesLayoutMode:
|
||||
return { ...state, layoutMode: action.payload };
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
dataSources: dataSourcesReducer,
|
||||
};
|
11
public/app/features/datasources/state/selectors.ts
Normal file
11
public/app/features/datasources/state/selectors.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const getDataSources = state => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.dataSources.filter(dataSource => {
|
||||
return regex.test(dataSource.name) || regex.test(dataSource.database);
|
||||
});
|
||||
};
|
||||
|
||||
export const getDataSourcesSearchQuery = state => state.searchQuery;
|
||||
export const getDataSourcesLayoutMode = state => state.layoutMode;
|
||||
export const getDataSourcesCount = state => state.dataSourcesCount;
|
@ -3,6 +3,5 @@ import './plugin_page_ctrl';
|
||||
import './import_list/import_list';
|
||||
import './ds_edit_ctrl';
|
||||
import './ds_dashboards_ctrl';
|
||||
import './ds_list_ctrl';
|
||||
import './datasource_srv';
|
||||
import './plugin_component';
|
||||
|
@ -1,61 +0,0 @@
|
||||
import coreModule from '../../core/core_module';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class DataSourcesCtrl {
|
||||
datasources: any;
|
||||
unfiltered: any;
|
||||
navModel: any;
|
||||
searchQuery: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private backendSrv, private datasourceSrv, private navModelSrv) {
|
||||
this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0);
|
||||
backendSrv.get('/api/datasources').then(result => {
|
||||
this.datasources = result;
|
||||
this.unfiltered = result;
|
||||
});
|
||||
}
|
||||
|
||||
onQueryUpdated() {
|
||||
const regex = new RegExp(this.searchQuery, 'ig');
|
||||
this.datasources = _.filter(this.unfiltered, item => {
|
||||
regex.lastIndex = 0;
|
||||
return regex.test(item.name) || regex.test(item.type);
|
||||
});
|
||||
}
|
||||
|
||||
removeDataSourceConfirmed(ds) {
|
||||
this.backendSrv
|
||||
.delete('/api/datasources/' + ds.id)
|
||||
.then(
|
||||
() => {
|
||||
this.$scope.appEvent('alert-success', ['Datasource deleted', '']);
|
||||
},
|
||||
() => {
|
||||
this.$scope.appEvent('alert-error', ['Unable to delete datasource', '']);
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.backendSrv.get('/api/datasources').then(result => {
|
||||
this.datasources = result;
|
||||
});
|
||||
this.backendSrv.get('/api/frontend/settings').then(settings => {
|
||||
this.datasourceSrv.init(settings.datasources);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeDataSource(ds) {
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete datasource ' + ds.name + '?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-trash',
|
||||
onConfirm: () => {
|
||||
this.removeDataSourceConfirmed(ds);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DataSourcesCtrl', DataSourcesCtrl);
|
@ -1,63 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<div ng-if="ctrl.unfiltered.length">
|
||||
<div class="page-action-bar">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form--has-input-icon">
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by name or type" />
|
||||
<i class="gf-form-input-icon fa fa-search"></i>
|
||||
</label>
|
||||
<layout-selector />
|
||||
</div>
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a class="page-header__cta btn btn-success" href="datasources/new">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add data source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<section class="card-section" layout-mode>
|
||||
<ol class="card-list">
|
||||
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
|
||||
<a class="card-item" href="datasources/edit/{{ds.id}}/">
|
||||
<div class="card-item-header">
|
||||
<div class="card-item-type">
|
||||
{{ds.type}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-item-body">
|
||||
<figure class="card-item-figure">
|
||||
<img ng-src="{{ds.typeLogoUrl}}">
|
||||
</figure>
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-name">
|
||||
{{ds.name}}
|
||||
<span ng-if="ds.isDefault">
|
||||
<span class="btn btn-secondary btn-mini">default</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-item-sub-name">
|
||||
{{ds.url}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.unfiltered.length === 0">
|
||||
<empty-list-cta model="{
|
||||
title: 'There are no data sources defined yet',
|
||||
buttonIcon: 'gicon gicon-add-datasources',
|
||||
buttonLink: 'datasources/new',
|
||||
buttonTitle: 'Add data source',
|
||||
proTip: 'You can also define data sources through configuration files.',
|
||||
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
|
||||
proTipLinkTitle: 'Learn more',
|
||||
proTipTarget: '_blank'
|
||||
}" />
|
||||
</div>
|
||||
</div>
|
@ -9,6 +9,7 @@ import ApiKeys from 'app/features/api-keys/ApiKeysPage';
|
||||
import PluginListPage from 'app/features/plugins/PluginListPage';
|
||||
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
|
||||
import FolderPermissions from 'app/features/folders/FolderPermissions';
|
||||
import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
|
||||
|
||||
/** @ngInject */
|
||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
@ -63,9 +64,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/datasources', {
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_list.html',
|
||||
controller: 'DataSourcesCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () => DataSourcesListPage,
|
||||
},
|
||||
})
|
||||
.when('/datasources/edit/:id', {
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
|
||||
|
@ -8,6 +8,7 @@ import apiKeysReducers from 'app/features/api-keys/state/reducers';
|
||||
import foldersReducers from 'app/features/folders/state/reducers';
|
||||
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||
import pluginReducers from 'app/features/plugins/state/reducers';
|
||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
@ -17,6 +18,7 @@ const rootReducer = combineReducers({
|
||||
...foldersReducers,
|
||||
...dashboardReducers,
|
||||
...pluginReducers,
|
||||
...dataSourcesReducers,
|
||||
});
|
||||
|
||||
export let store;
|
||||
|
@ -1,7 +1,25 @@
|
||||
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
export interface DataSource {
|
||||
id: number;
|
||||
orgId: number;
|
||||
name: string;
|
||||
typeLogoUrl: string;
|
||||
type: string;
|
||||
access: string;
|
||||
url: string;
|
||||
password: string;
|
||||
user: string;
|
||||
database: string;
|
||||
basicAuth: false;
|
||||
isDefault: false;
|
||||
jsonData: { authType: string; defaultRegion: string };
|
||||
readOnly: false;
|
||||
}
|
||||
|
||||
export interface DataSourcesState {
|
||||
dataSources: DataSource[];
|
||||
searchQuery: string;
|
||||
layoutMode: LayoutMode;
|
||||
dataSourcesCount: number;
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ import { NavModel, NavModelItem, NavIndex } from './navModel';
|
||||
import { FolderDTO, FolderState, FolderInfo } from './folders';
|
||||
import { DashboardState } from './dashboard';
|
||||
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
||||
import { DataSource } from './datasources';
|
||||
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
|
||||
import { User } from './user';
|
||||
import { DataSource, DataSourcesState } from './datasources';
|
||||
import { PluginMeta, Plugin, PluginsState } from './plugins';
|
||||
|
||||
export {
|
||||
@ -41,6 +41,7 @@ export {
|
||||
User,
|
||||
Plugin,
|
||||
PluginsState,
|
||||
DataSourcesState,
|
||||
};
|
||||
|
||||
export interface StoreState {
|
||||
|
Loading…
Reference in New Issue
Block a user