Skip to content

Commit

Permalink
Improved feedback (#2357)
Browse files Browse the repository at this point in the history
* Ensure Reflux uses bluebird to create promises

When using the `triggerPromise` method in a Reflux action, Reflux
creates a new promise underneath using the native promise the browser
provides.

The main reason to avoid this is consistency, as bluebird is our library
of choice for promises, and it's strange seeing a promise that behaves
differently. Most importantly, most browsers' promises only have support
for `then` and `catch` methods, and that makes complicated using other
methods as `finally`, as you need to think if the promise you receive is
a bluebird promise or not.

* Add feedback to login button

* Add feedback to start/pause stream buttons

* Add feedback to start/stop input buttons

* Add feedback to message loaders buttons
  • Loading branch information
edmundoa authored and dennisoelkers committed Jun 27, 2016
1 parent 78f694c commit 21c0bcf
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const MessageLoader = React.createClass({
getInitialState() {
return ({
hidden: this.props.hidden,
loading: false,
});
},

Expand All @@ -33,8 +34,10 @@ const MessageLoader = React.createClass({
if (messageId === '' || index === '') {
return;
}
this.setState({ loading: true });
const promise = MessagesStore.loadMessage(index, messageId);
promise.then(data => this.props.onMessageLoaded(data));
promise.finally(() => this.setState({ loading: false }));

event.preventDefault();
},
Expand All @@ -58,8 +61,8 @@ const MessageLoader = React.createClass({
<form className="form-inline message-loader-form" onSubmit={this.loadMessage}>
<input type="text" ref="messageId" className="form-control" placeholder="Message ID" required/>
<input type="text" ref="index" className="form-control" placeholder="Index" required/>
<button ref="submitButton" type="submit" className="btn btn-info">
Load message
<button ref="submitButton" type="submit" className="btn btn-info" disabled={this.state.loading}>
{this.state.loading ? 'Loading message...' : 'Load message'}
</button>
</form>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const InputDropdown = React.createClass({
title: PropTypes.string,
preselectedInputId: PropTypes.string,
onLoadMessage: PropTypes.func,
disabled: PropTypes.bool,
},
mixins: [LinkedStateMixin],
getInitialState() {
Expand Down Expand Up @@ -57,7 +58,7 @@ const InputDropdown = React.createClass({
{inputs.toArray()}
</Input>

<Button bsStyle="info" disabled={this.state.selectedInput === this.PLACEHOLDER}
<Button bsStyle="info" disabled={this.props.disabled || this.state.selectedInput === this.PLACEHOLDER}
onClick={this._onLoadMessage}>{this.props.title}</Button>
</div>
);
Expand Down
30 changes: 26 additions & 4 deletions graylog2-web-interface/src/components/inputs/InputStateControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ const InputStateControl = React.createClass({
input: PropTypes.object.isRequired,
},
mixins: [Reflux.connectFilter(InputStatesStore, 'inputState', inputStateFilter)],

getInitialState() {
return {
loading: false,
};
},

_isInputRunning() {
if (!this.state.inputState) {
return false;
Expand All @@ -29,18 +36,33 @@ const InputStateControl = React.createClass({
return nodeState.state === 'RUNNING';
});
},

_startInput() {
InputStatesStore.start(this.props.input);
this.setState({ loading: true });
InputStatesStore.start(this.props.input)
.finally(() => this.setState({ loading: false }));
},

_stopInput() {
InputStatesStore.stop(this.props.input);
this.setState({ loading: true });
InputStatesStore.stop(this.props.input)
.finally(() => this.setState({ loading: false }));
},

render() {
if (this._isInputRunning()) {
return <Button bsStyle="primary" onClick={this._stopInput}>Stop input</Button>;
return (
<Button bsStyle="primary" onClick={this._stopInput} disabled={this.state.loading}>
{this.state.loading ? 'Stopping...' : 'Stop input'}
</Button>
);
}

return <Button bsStyle="success" onClick={this._startInput}>Start input</Button>;
return (
<Button bsStyle="success" onClick={this._startInput} disabled={this.state.loading}>
{this.state.loading ? 'Starting...' : 'Start input'}
</Button>
);
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {PropTypes} from 'react';
import React, { PropTypes } from 'react';
import InputDropdown from 'components/inputs/InputDropdown';
import UserNotification from 'util/UserNotification';

Expand All @@ -11,21 +11,29 @@ const RecentMessageLoader = React.createClass({
onMessageLoaded: PropTypes.func.isRequired,
selectedInputId: PropTypes.string,
},
getInitialState() {
return {
loading: false,
};
},

onClick(inputId) {
const input = this.props.inputs.get(inputId);
if (!input) {
UserNotification.error('Invalid input selected: ' + inputId,
'Could not load message from invalid Input ' + inputId);
}
UniversalSearchStore.search('relative', 'gl2_source_input:' + inputId + ' OR gl2_source_radio_input:' + inputId, { range: 0 }, undefined, 1, undefined, undefined)
.then((response) => {
if (response.total_results > 0) {
this.props.onMessageLoaded(response.messages[0]);
} else {
UserNotification.error('Input did not return a recent message.');
this.props.onMessageLoaded(undefined);
}
});
this.setState({ loading: true });
const promise = UniversalSearchStore.search('relative', 'gl2_source_input:' + inputId + ' OR gl2_source_radio_input:' + inputId, { range: 0 }, undefined, 1, undefined, undefined);
promise.then((response) => {
if (response.total_results > 0) {
this.props.onMessageLoaded(response.messages[0]);
} else {
UserNotification.error('Input did not return a recent message.');
this.props.onMessageLoaded(undefined);
}
});
promise.finally(() => this.setState({ loading: false }));
},
render() {
let helpMessage;
Expand All @@ -37,7 +45,9 @@ const RecentMessageLoader = React.createClass({
return (
<div style={{marginTop: 5}}>
{helpMessage}
<InputDropdown inputs={this.props.inputs} preselectedInputId={this.props.selectedInputId} onLoadMessage={this.onClick} title="Load Message"/>
<InputDropdown inputs={this.props.inputs} preselectedInputId={this.props.selectedInputId}
onLoadMessage={this.onClick} title={this.state.loading ? 'Loading message...' : 'Load Message'}
disabled={this.state.loading} />
</div>
);
},
Expand Down
25 changes: 19 additions & 6 deletions graylog2-web-interface/src/components/streams/Stream.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const Stream = React.createClass({
};
},
mixins: [PermissionsMixin],

getInitialState() {
return {
loading: false,
};
},

_formatNumberOfStreamRules(stream) {
let verbalMatchingType;
switch (stream.matching_type) {
Expand All @@ -41,8 +48,9 @@ const Stream = React.createClass({
}
},
_onResume() {
StreamsStore.resume(this.props.stream.id, () => {
});
this.setState({ loading: true });
StreamsStore.resume(this.props.stream.id, () => {})
.finally(() => this.setState({ loading: false }));
},
_onUpdate(streamId, stream) {
StreamsStore.update(streamId, stream, () => UserNotification.success(`Stream '${stream.title}' was updated successfully.`, 'Success'));
Expand All @@ -52,8 +60,9 @@ const Stream = React.createClass({
},
_onPause() {
if (window.confirm(`Do you really want to pause stream '${this.props.stream.title}'?`)) {
StreamsStore.pause(this.props.stream.id, () => {
});
this.setState({ loading: true });
StreamsStore.pause(this.props.stream.id, () => {})
.finally(() => this.setState({ loading: false }));
}
},
_onQuickAdd() {
Expand Down Expand Up @@ -94,11 +103,15 @@ const Stream = React.createClass({
if (this.isAnyPermitted(permissions, [`streams:changestate:${stream.id}`, `streams:edit:${stream.id}`])) {
if (stream.disabled) {
toggleStreamLink = (
<a className="btn btn-success toggle-stream-button" onClick={this._onResume}>Start Stream</a>
<Button bsStyle="success" className="toggle-stream-button" onClick={this._onResume} disabled={this.state.loading}>
{this.state.loading ? 'Starting...' : 'Start Stream'}
</Button>
);
} else {
toggleStreamLink = (
<a className="btn btn-primary toggle-stream-button" onClick={this._onPause}>Pause Stream</a>
<Button bsStyle="primary" className="toggle-stream-button" onClick={this._onPause} disabled={this.state.loading}>
{this.state.loading ? 'Pausing...' : 'Pause Stream'}
</Button>
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions graylog2-web-interface/src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import AppFacade from 'routing/AppFacade';
import Promise from 'bluebird';
import Reflux from 'reflux';

Reflux.setPromiseFactory((handlers) => new Promise(handlers));

window.onload = () => {
const appContainer = document.createElement('div');
Expand Down
16 changes: 14 additions & 2 deletions graylog2-web-interface/src/pages/LoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import authStyle from '!style/useable!css!less!stylesheets/auth.less';

const LoginPage = React.createClass({
mixins: [Reflux.connect(SessionStore), Reflux.ListenerMethods],

getInitialState() {
return {
loading: false,
};
},

componentDidMount() {
disconnectedStyle.use();
authStyle.use();
Expand All @@ -24,16 +31,19 @@ const LoginPage = React.createClass({
onSignInClicked(event) {
event.preventDefault();
this.resetLastError();
this.setState({ loading: true });
const username = this.refs.username.getValue();
const password = this.refs.password.getValue();
const location = document.location.host;
SessionActions.login.triggerPromise(username, password, location).catch((error) => {
const promise = SessionActions.login.triggerPromise(username, password, location);
promise.catch((error) => {
if (error.additional.status === 401) {
this.setState({lastError: 'Invalid credentials, please verify them and retry.'});
} else {
this.setState({lastError: 'Error - the server returned: ' + error.additional.status + ' - ' + error.message});
}
});
promise.finally(() => this.setState({ loading: false }));
},
formatLastError(error) {
if (error) {
Expand Down Expand Up @@ -65,7 +75,9 @@ const LoginPage = React.createClass({

<Input ref="password" type="password" placeholder="Password" />

<ButtonInput type="submit" bsStyle="info">Sign in</ButtonInput>
<ButtonInput type="submit" bsStyle="info" disabled={this.state.loading}>
{this.state.loading ? 'Signing in...' : 'Sign in'}
</ButtonInput>

</form>
</Row>
Expand Down
4 changes: 2 additions & 2 deletions graylog2-web-interface/src/stores/streams/StreamsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class StreamsStore {
};

const url = URLUtils.qualifyUrl(ApiRoutes.StreamsApiController.pause(streamId).url);
fetch('POST', url).then(callback, failCallback).then(this._emitChange.bind(this));
return fetch('POST', url).then(callback, failCallback).then(this._emitChange.bind(this));
}
resume(streamId: string, callback: (() => void)) {
const failCallback = (errorThrown) => {
Expand All @@ -77,7 +77,7 @@ class StreamsStore {
};

const url = URLUtils.qualifyUrl(ApiRoutes.StreamsApiController.resume(streamId).url);
fetch('POST', url)
return fetch('POST', url)
.then(callback, failCallback).then(this._emitChange.bind(this));
}
save(stream: any, callback: ((streamId: string) => void)) {
Expand Down

0 comments on commit 21c0bcf

Please sign in to comment.