Skip to content

Commit

Permalink
Updating react-select to 1.0.0-rc (Graylog2#4189)
Browse files Browse the repository at this point in the history
* Update react-select css path

* Adapt onChange callback to changes in react-select

Before react-select passed the object's value, now it passes the whole
object.

* Filter options passed to react-select

Do not pass options not being accepted by the ReactSelect component,
letting the prop check to itself.

* Deprecate `onValueChange` prop on Select component

The purpose of that prop was to offer some internal API we could rely
on, but there were many accidental usages of `onChange`, which was going
directly to react-select.

In this commit we are deprecating `onValueChange` and start using `onChange`
instead. This allows us to keep the old API, fix code that was directly
using react-select's callback, and avoid accidental pass of props to the
encapsulated component.

It is still possible to get a direct callback from react-select by using
the `onReactSelectChange` prop.

* Also filter `value` prop from react-select

* Use function ref in Select component

* Fix select sm styling

* Fix case when user clears select input

* Update react select to 1.0.0-rc.10

* Adapt Select onChange callback on multi selects

This allows us to encapsulate all related react-select changes in the
Select component.

* Migrate Select's onValueChange props to onChange

* Add now supported autoFocus and required props

* Replace deprecated string ref

* SelectableList should not be clearable

Selected items go into a different component, so there's no need to
clear the select input.

* Support creating options from Select component

This was previously possible by using the `allowCreate` prop, and it
suffered many changes in react-select:

- Now we can get that functionality by using react-select's `Creatable`
component

- In order to display the newly added options, react-select requires
that created values have the same format as options. This requires some
formatting in our side, as our Select component returns and accepts
string values

* Pass string value to Select component
  • Loading branch information
edmundoa authored and bernd committed Oct 2, 2017
1 parent 12f67f3 commit 3cfb581
Show file tree
Hide file tree
Showing 21 changed files with 107 additions and 40 deletions.
2 changes: 1 addition & 1 deletion graylog2-web-interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"react-dnd-html5-backend": "^2.0.0",
"react-grid-layout": "^0.14.3",
"react-overlays": "^0.6.5",
"react-select": "^0.8.2",
"react-select": "^v1.0.0-rc.10",
"rickshaw": "^1.5.1",
"sockjs-client": "1.1.x",
"string": "^3.3.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const CreateAlertConditionInput = React.createClass({
<Col md={6}>
<form>
<Input label="Alert on stream" help="Select the stream that the condition will use to trigger alerts.">
<Select placeholder="Select a stream" options={formattedStreams} onValueChange={this._onStreamChange} />
<Select placeholder="Select a stream" options={formattedStreams} onChange={this._onStreamChange} />
</Input>

<Input type="select" value={this.state.type} onChange={this._onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const CreateAlertNotificationInput = React.createClass({
<form>
<Input label="Notify on stream"
help="Select the stream that should use this notification when its alert conditions are triggered.">
<Select placeholder="Select a stream" options={formattedStreams} onValueChange={this._onStreamChange} />
<Select placeholder="Select a stream" options={formattedStreams} onChange={this._onStreamChange} />
</Input>

<Input type="select" value={this.state.type} onChange={this._onChange}
Expand Down
5 changes: 3 additions & 2 deletions graylog2-web-interface/src/components/common/MultiSelect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import Select from 'components/common/Select';

const MultiSelect = React.createClass({
propTypes: Select.propTypes,
_select: undefined,
getValue() {
return this.refs.select.getValue();
return this._select.getValue();
},
render() {
return <Select ref="select" multi {...this.props} />;
return <Select ref={(c) => { this._select = c; }} multi {...this.props} />;
},
});

Expand Down
10 changes: 5 additions & 5 deletions graylog2-web-interface/src/components/common/Select.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.select-sm .Select-control {
.select-sm .Select-control, .select-sm .Select-input, .select-sm .Select-input > input {
height: 28px;
}

.select-sm .Select-placeholder {
line-height: 28px;
.select-sm .Select-placeholder, .select-sm .Select-value {
line-height: 28px !important;
}

.select-sm .Select-input, .select-sm .Select-input > input {
height: 28px;
.select-sm .Select-input > input {
padding: 0;
}
85 changes: 74 additions & 11 deletions graylog2-web-interface/src/components/common/Select.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactSelect from 'react-select';
import lodash from 'lodash';

const propTypes = ReactSelect.propTypes;
propTypes.onValueChange = PropTypes.func;
propTypes.size = PropTypes.oneOf(['normal', 'small']);
import AppConfig from 'util/AppConfig';

// Pass all props react-select accepts, excepting `onChange`
const filteredProps = ['onChange', 'value'];
const acceptedReactSelectProps = lodash.without(lodash.keys(ReactSelect.propTypes), ...filteredProps);

const Select = React.createClass({
propTypes: propTypes,
propTypes: {
onChange: PropTypes.func,
onValueChange: PropTypes.func, // deprecated
onReactSelectChange: PropTypes.func,
size: PropTypes.oneOf(['normal', 'small']),
value: PropTypes.string,
multi: PropTypes.bool,
displayKey: PropTypes.string,
valueKey: PropTypes.string,
delimiter: PropTypes.string,
allowCreate: PropTypes.bool,
},

getDefaultProps() {
return {
multi: false,
size: 'normal',
displayKey: 'label',
valueKey: 'value',
delimiter: ',',
allowCreate: false,
};
},
getInitialState() {
Expand All @@ -37,24 +57,67 @@ const Select = React.createClass({
clearValue() {
// Clear value needs an event, so we just give it one :grumpy:
// As someone said: "This can't do any more harm that we already do"
this.refs.select.clearValue(new CustomEvent('fake'));
this._select.clearValue(new CustomEvent('fake'));
},
// This helps us emulate the behaviour of react-select < 1.0:
// - On simple selects it returns the value
// - On multi selects it returns all selected values split by a delimiter (',' by default)
_extractOptionValue(option) {
const { multi, valueKey, delimiter } = this.props;

if (option) {
return multi ? option.map(i => i[valueKey]).join(delimiter) : option[valueKey];
}
return '';
},
_onChange(value) {
_onChange(selectedOption) {
const value = this._extractOptionValue(selectedOption);
this.setState({ value: value });

if (this.props.onValueChange) {
if (this.props.onChange) {
this.props.onChange(value);
} else if (this.props.onValueChange) {
if (AppConfig.gl2DevMode()) {
console.error('Select prop `onValueChange` is deprecated. Please use `onChange` instead.');
}
this.props.onValueChange(value);
}
},
reactSelectStyles: require('!style/useable!css!react-select/dist/default.css'),
_select: undefined,
reactSelectStyles: require('!style/useable!css!react-select/dist/react-select.css'),
reactSelectSmStyles: require('!style/useable!css!./Select.css'),

// Using ReactSelect.Creatable now needs to get values as objects or they are not display
// This method takes care of formatting a string value into options react-select supports.
_formatInputValue(value) {
const { options, displayKey, valueKey, delimiter } = this.props;

return value.split(delimiter).map((v) => {
const predicate = {};
predicate[valueKey] = v;
const option = lodash.find(options, predicate);

predicate[displayKey] = v;
return option || predicate;
});
},
render() {
// eslint-disable-next-line no-unused-vars
const { onValueChange, size, ...reactSelectProps } = this.props;
const { allowCreate, size, onReactSelectChange, multi } = this.props;
const value = this.state.value;
const reactSelectProps = lodash.pick(this.props, acceptedReactSelectProps);
const SelectComponent = allowCreate ? ReactSelect.Creatable : ReactSelect;

let formattedValue = value;
if (value && multi && allowCreate) {
formattedValue = this._formatInputValue(value);
}

return (
<div className={size === 'small' ? 'select-sm' : ''}>
<ReactSelect ref="select" onChange={this._onChange} {...reactSelectProps} value={this.state.value} />
<SelectComponent ref={(c) => { this._select = c; }}
onChange={onReactSelectChange || this._onChange}
{...reactSelectProps}
value={formattedValue} />
</div>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const SelectableList = React.createClass({
});
return (
<div>
<Select ref="select" autofocus={this.props.autoFocus} options={this.props.options} onValueChange={this._onAddOption} />
<Select ref="select" autoFocus={this.props.autoFocus} options={this.props.options} onChange={this._onAddOption} clearable={false} />
{formattedOptions.length > 0 &&
<ListGroup style={{ marginTop: 10 }}>{formattedOptions}</ListGroup>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ListField = React.createClass({
onChange: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
typeName: PropTypes.string.isRequired,
value: PropTypes.any,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
addPlaceholder: PropTypes.bool,
disabled: PropTypes.bool,
},
Expand Down Expand Up @@ -48,11 +48,12 @@ const ListField = React.createClass({
render() {
const field = this.state.field;
const typeName = this.state.typeName;
const value = this.state.value;
const isRequired = !field.is_optional;
const allowCreate = field.attributes.includes('allow_create');
const options = (field.additional_info && field.additional_info.values ? field.additional_info.values : {});
const formattedOptions = Object.keys(options).map(key => this._formatOption(key, options[key]));

// TODO: Update react-select to support `autofocus` and `required` attributes
return (
<div className="form-group">
<label htmlFor={`${typeName}-${field.title}`}>
Expand All @@ -62,10 +63,12 @@ const ListField = React.createClass({
</label>

<MultiSelect id={field.title}
required={isRequired}
autoFocus={this.props.autoFocus}
options={formattedOptions}
value={this.state.value}
value={value ? (Array.isArray(value) ? value.join(',') : value) : undefined}
placeholder={`${allowCreate ? 'Add' : 'Select'} ${field.human_name}`}
onValueChange={this._handleChange}
onChange={this._handleChange}
disabled={this.props.disabled}
allowCreate={allowCreate} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const LookupTableConverterConfiguration = React.createClass({
clearable={false}
options={lookupTables}
matchProp="value"
onValueChange={this._onSelect('lookup_table_name')}
onChange={this._onSelect('lookup_table_name')}
value={this.props.configuration.lookup_table_name} />
</Input>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const LookupTableExtractorConfiguration = React.createClass({
clearable={false}
options={lookupTables}
matchProp="value"
onValueChange={this._onSelect('lookup_table_name')}
onChange={this._onSelect('lookup_table_name')}
value={this.props.configuration.lookup_table_name} />
</Col>
<Col md={1} className="text-right">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const IndexMaintenanceStrategiesConfiguration = React.createClass({
options={this._availableSelectOptions()}
matchProp="value"
value={this._activeSelection()}
onValueChange={this._onSelect} />
onChange={this._onSelect} />
</Input>
{this._getConfigurationComponent(this._activeSelection())}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const CreateInputControl = React.createClass({
<form className="form-inline" onSubmit={this._openModal}>
<div className="form-group" style={{ width: 300 }}>
<Select placeholder="Select input" options={this._formatSelectOptions()} matchProp="label"
onValueChange={this._onInputSelect} value={this.state.selectedInput} />
onChange={this._onInputSelect} value={this.state.selectedInput} />
</div>
&nbsp;
<Button bsStyle="success" type="submit" disabled={!this.state.selectedInput}>Launch new input</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const CacheCreate = React.createClass({
clearable={false}
options={sortedCaches}
matchProp="value"
onValueChange={this._onTypeSelect}
onChange={this._onTypeSelect}
value={null} />
</Input>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const CachePicker = React.createClass({
clearable={false}
options={sortedCaches}
matchProp="value"
onValueChange={this.props.onSelect}
onChange={this.props.onSelect}
value={this.props.selectedId} />
</Input>
</fieldset>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const DataAdapterCreate = React.createClass({
clearable={false}
options={sortedAdapters}
matchProp="value"
onValueChange={this._onTypeSelect}
onChange={this._onTypeSelect}
value={null} />
</Input>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const DataAdapterPicker = React.createClass({
clearable={false}
options={sortedAdapters}
matchProp="value"
onValueChange={this.props.onSelect}
onChange={this.props.onSelect}
value={this.props.selectedId} />
</Input>
</fieldset>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ const RawMessageLoader = React.createClass({
<Input id="input" name="input" label={<span>Message input <small>(optional)</small></span>}
help="Select the message input ID that should be assigned to the parsed message.">
<Select id="input" placeholder="Select input" options={this._formatInputSelectOptions()}
matchProp="label" onValueChange={this._onInputSelect} value={this.state.inputId} />
matchProp="label" onChange={this._onInputSelect} value={this.state.inputId} />
</Input>
);
}
Expand All @@ -198,7 +198,7 @@ const RawMessageLoader = React.createClass({
<Input id="codec" name="codec" label="Message codec"
help="Select the codec that should be used to decode the message." required>
<Select id="codec" placeholder="Select codec" options={this._formatSelectOptions()}
matchProp="label" onValueChange={this._onCodecSelect} value={this.state.codec} />
matchProp="label" onChange={this._onCodecSelect} value={this.state.codec} />
</Input>
{codecConfigurationOptions}
</fieldset>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const AddDecoratorButton = React.createClass({
<div className={DecoratorStyles.addDecoratorSelect}>
<Select ref="select"
placeholder="Select decorator"
onValueChange={this._onTypeChange}
onChange={this._onTypeChange}
options={decoratorTypes}
matchProp="label"
disabled={this.props.disabled}
Expand Down
2 changes: 1 addition & 1 deletion graylog2-web-interface/src/components/search/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ const SearchBar = React.createClass({

return (
<Select placeholder="Saved searches" options={formattedSavedSearches} value={this.state.savedSearch}
onValueChange={this._onSavedSearchSelect} size="small" />
onChange={this._onSavedSearchSelect} size="small" />
);
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const StreamForm = React.createClass({
<div className="form-group">
<label>Index Set</label>
<Select placeholder="Select index set" options={this._formatSelectOptions()} matchProp="label"
onValueChange={this._onIndexSetSelect} value={this.state.index_set_id} />
onChange={this._onIndexSetSelect} value={this.state.index_set_id} />
<p className="help-block">Messages that match this stream will be written to the configured index set.</p>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const RolesSelect = React.createClass({
ref="select"
options={rolesOptions}
value={rolesValue}
onValueChange={this.props.onValueChange}
onChange={this.props.onValueChange}
placeholder="Choose roles..."
/>
);
Expand Down

0 comments on commit 3cfb581

Please sign in to comment.