mirror of https://github.com/grafana/grafana
parent
11ee65d35a
commit
166f93cf54
@ -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(); |
||||
}); |
||||
}); |
@ -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); |
@ -0,0 +1,32 @@ |
||||
import React, { SFC } 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; |
||||
} |
||||
|
||||
const DataSourcesList: SFC<Props> = props => { |
||||
const { dataSources, layoutMode } = 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; |
@ -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(); |
||||
}); |
||||
}); |
@ -0,0 +1,34 @@ |
||||
import React, { SFC } from 'react'; |
||||
import { DataSource } from 'app/types'; |
||||
|
||||
export interface Props { |
||||
dataSource: DataSource; |
||||
} |
||||
|
||||
const DataSourcesListItem: SFC<Props> = props => { |
||||
const { dataSource } = 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; |
@ -0,0 +1,35 @@ |
||||
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, |
||||
}; |
||||
|
||||
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), |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -0,0 +1,72 @@ |
||||
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, 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[]; |
||||
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 { navModel, dataSources, layoutMode } = this.props; |
||||
|
||||
if (dataSources.length === 0) { |
||||
return <EmptyListCTA model={emptyListModel} />; |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
<PageHeader model={navModel} /> |
||||
<div className="page-container page-body"> |
||||
<DataSourcesActionBar /> |
||||
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} /> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
navModel: getNavModel(state.navIndex, 'datasources'), |
||||
dataSources: getDataSources(state.dataSources), |
||||
layoutMode: getDataSourcesLayoutMode(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,152 @@ |
||||
// 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) /> |
||||
<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": "", |
||||
}, |
||||
] |
||||
} |
||||
layoutMode="grid" |
||||
/> |
||||
</div> |
||||
</div> |
||||
`; |
||||
|
||||
exports[`Render should render component 1`] = ` |
||||
<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", |
||||
} |
||||
} |
||||
/> |
||||
`; |
@ -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)); |
||||
}; |
||||
} |
@ -0,0 +1,28 @@ |
||||
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: '', |
||||
}; |
||||
|
||||
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { |
||||
switch (action.type) { |
||||
case ActionTypes.LoadDataSources: |
||||
return { ...state, dataSources: action.payload }; |
||||
|
||||
case ActionTypes.SetDataSourcesSearchQuery: |
||||
return { ...state, searchQuery: action.payload }; |
||||
|
||||
case ActionTypes.SetDataSourcesLayoutMode: |
||||
return { ...state, layoutMode: action.payload }; |
||||
} |
||||
|
||||
return state; |
||||
}; |
||||
|
||||
export default { |
||||
dataSources: dataSourcesReducer, |
||||
}; |
@ -0,0 +1,10 @@ |
||||
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; |
@ -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> |
@ -1,7 +1,24 @@ |
||||
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; |
||||
} |
||||
|
Loading…
Reference in new issue