Add a Async Select that fetches options from given endpoint (#1909)

* Add a Async Select that fetches options from given endpoint

* update it statement
This commit is contained in:
vera-liu 2017-01-11 10:31:30 -08:00 committed by GitHub
parent 94d20168da
commit f0917c62f2
6 changed files with 112 additions and 106 deletions

View File

@ -1,61 +0,0 @@
const $ = window.$ = require('jquery');
import React from 'react';
import Select from 'react-select';
const propTypes = {
onChange: React.PropTypes.func,
actions: React.PropTypes.object,
databaseId: React.PropTypes.number,
valueRenderer: React.PropTypes.func,
};
class DatabaseSelect extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
databaseLoading: false,
databaseOptions: [],
};
}
componentDidMount() {
this.fetchDatabaseOptions();
}
changeDb(db) {
this.props.onChange(db);
}
fetchDatabaseOptions() {
this.setState({ databaseLoading: true });
const url = '/databaseasync/api/read?_flt_0_expose_in_sqllab=1';
$.get(url, (data) => {
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
this.setState({ databaseOptions: options, databaseLoading: false });
this.props.actions.setDatabases(data.result);
if (data.result.length === 0) {
this.props.actions.addAlert({
bsStyle: 'danger',
msg: "It seems you don't have access to any database",
});
}
});
}
render() {
return (
<div>
<Select
name="select-db"
placeholder={`Select a database (${this.state.databaseOptions.length})`}
options={this.state.databaseOptions}
value={this.props.databaseId}
isLoading={this.state.databaseLoading}
autosize={false}
onChange={this.changeDb.bind(this)}
valueRenderer={this.props.valueRenderer}
/>
</div>
);
}
}
DatabaseSelect.propTypes = propTypes;
export default DatabaseSelect;

View File

@ -3,10 +3,10 @@ import React from 'react';
import { Button } from 'react-bootstrap';
import Select from 'react-select';
import QueryTable from './QueryTable';
import DatabaseSelect from './DatabaseSelect';
import { now, epochTimeXHoursAgo,
epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
import AsyncSelect from '../../components/AsyncSelect';
const propTypes = {
actions: React.PropTypes.object.isRequired,
@ -28,9 +28,6 @@ class QuerySearch extends React.PureComponent {
queriesLoading: true,
};
}
componentWillMount() {
this.fetchUsers();
}
componentDidMount() {
this.refreshQueries();
}
@ -89,18 +86,23 @@ class QuerySearch extends React.PureComponent {
changeSearch(event) {
this.setState({ searchText: event.target.value });
}
fetchUsers() {
this.setState({ userLoading: true });
const url = '/users/api/read';
$.getJSON(url, (data, status) => {
if (status === 'success') {
const options = [];
for (let i = 0; i < data.pks.length; i++) {
options.push({ value: data.pks[i], label: data.result[i].username });
}
this.setState({ userOptions: options, userLoading: false });
}
});
userMutator(data) {
const options = [];
for (let i = 0; i < data.pks.length; i++) {
options.push({ value: data.pks[i], label: data.result[i].username });
}
return options;
}
dbMutator(data) {
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
this.props.actions.setDatabases(data.result);
if (data.result.length === 0) {
this.props.actions.addAlert({
bsStyle: 'danger',
msg: "It seems you don't have access to any database",
});
}
return options;
}
refreshQueries() {
this.setState({ queriesLoading: true });
@ -125,21 +127,19 @@ class QuerySearch extends React.PureComponent {
<div>
<div id="search-header" className="row space-1">
<div className="col-sm-2">
<Select
name="select-user"
placeholder="[User]"
options={this.state.userOptions}
<AsyncSelect
dataEndpoint="/users/api/read"
mutator={this.userMutator}
value={this.state.userId}
isLoading={this.state.userLoading}
autosize={false}
onChange={this.changeUser.bind(this)}
/>
</div>
<div className="col-sm-2">
<DatabaseSelect
<AsyncSelect
onChange={this.onChange.bind(this)}
databaseId={this.state.databaseId}
actions={this.props.actions}
dataEndpoint="/databaseasync/api/read?_flt_0_expose_in_sqllab=1"
value={this.state.databaseId}
mutator={this.dbMutator.bind(this)}
/>
</div>
<div className="col-sm-4">

View File

@ -3,7 +3,7 @@ import React from 'react';
import Select from 'react-select';
import { Label, Button } from 'react-bootstrap';
import TableElement from './TableElement';
import DatabaseSelect from './DatabaseSelect';
import AsyncSelect from '../../components/AsyncSelect';
const propTypes = {
queryEditor: React.PropTypes.object.isRequired,
@ -44,6 +44,17 @@ class SqlEditorLeftBar extends React.PureComponent {
this.fetchSchemas(val);
}
}
dbMutator(data) {
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
this.props.actions.setDatabases(data.result);
if (data.result.length === 0) {
this.props.actions.addAlert({
bsStyle: 'danger',
msg: "It seems you don't have access to any database",
});
}
return options;
}
resetState() {
this.props.actions.resetState();
}
@ -103,15 +114,17 @@ class SqlEditorLeftBar extends React.PureComponent {
<div className="clearfix sql-toolbar scrollbar-content">
{networkAlert}
<div>
<DatabaseSelect
<AsyncSelect
dataEndpoint="/databaseasync/api/read?_flt_0_expose_in_sqllab=1"
onChange={this.onChange.bind(this)}
databaseId={this.props.queryEditor.dbId}
actions={this.props.actions}
value={this.props.queryEditor.dbId}
valueRenderer={(o) => (
<div>
<span className="text-muted">Database:</span> {o.label}
</div>
)}
mutator={this.dbMutator.bind(this)}
placeholder="Select a database"
/>
</div>
<div className="m-t-5">

View File

@ -0,0 +1,59 @@
const $ = window.$ = require('jquery');
import React from 'react';
import Select from 'react-select';
const propTypes = {
dataEndpoint: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
mutator: React.PropTypes.func.isRequired,
value: React.PropTypes.number,
valueRenderer: React.PropTypes.func,
placeholder: React.PropTypes.string,
};
const defaultProps = {
placeholder: 'Select ...',
valueRenderer: (o) => (<div>{o.label}</div>),
};
class AsyncSelect extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isLoading: false,
options: [],
};
}
componentDidMount() {
this.fetchOptions();
}
fetchOptions() {
this.setState({ isLoading: true });
const mutator = this.props.mutator;
$.get(this.props.dataEndpoint, (data) => {
this.setState({ options: mutator ? mutator(data) : data, isLoading: false });
});
}
onChange(opt) {
this.props.onChange(opt);
}
render() {
return (
<div>
<Select
placeholder={this.props.placeholder}
options={this.state.options}
value={this.props.value}
isLoading={this.state.isLoading}
onChange={this.onChange.bind(this)}
valueRenderer={this.props.valueRenderer}
/>
</div>
);
}
}
AsyncSelect.propTypes = propTypes;
AsyncSelect.defaultProps = defaultProps;
export default AsyncSelect;

View File

@ -1,34 +1,35 @@
import React from 'react';
import Select from 'react-select';
import DatabaseSelect from '../../../javascripts/SqlLab/components/DatabaseSelect';
import AsyncSelect from '../../../javascripts/components/AsyncSelect';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
describe('DatabaseSelect', () => {
describe('AsyncSelect', () => {
const mockedProps = {
actions: {},
dataEndpoint: '/slicemodelview/api/read',
onChange: sinon.spy(),
mutator: () => {},
};
it('is valid element', () => {
expect(
React.isValidElement(<DatabaseSelect {...mockedProps} />)
React.isValidElement(<AsyncSelect {...mockedProps} />)
).to.equal(true);
});
it('has one select', () => {
const wrapper = shallow(
<DatabaseSelect {...mockedProps} />
<AsyncSelect {...mockedProps} />
);
expect(wrapper.find(Select)).to.have.length(1);
});
it('calls onChange on select change', () => {
const onChange = sinon.spy();
const wrapper = shallow(
<DatabaseSelect onChange={onChange} />
<AsyncSelect {...mockedProps} />
);
wrapper.find(Select).simulate('change', { value: 1 });
expect(onChange).to.have.property('callCount', 1);
expect(mockedProps.onChange).to.have.property('callCount', 1);
});
});

View File

@ -18,14 +18,8 @@ describe('QuerySearch', () => {
});
const wrapper = shallow(<QuerySearch {...mockedProps} />);
it('should have four Select', () => {
expect(wrapper.find(Select)).to.have.length(4);
});
it('updates userId on user selects change', () => {
wrapper.find('[name="select-user"]')
.simulate('change', { value: 1 });
expect(wrapper.state().userId).to.equal(1);
it('should have three Select', () => {
expect(wrapper.find(Select)).to.have.length(3);
});
it('updates fromTime on user selects from time', () => {