-
Notifications
You must be signed in to change notification settings - Fork 26
Manage Experiments from the front #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/pages/ExperimentsAdmin.js
Outdated
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.user, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like you don't use it anywhere
src/state/experiments.js
Outdated
export const initialState = { | ||
loading: true, | ||
error: null, | ||
createOnProgress: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't it be in progress
?
src/pages/ExperimentsAdmin.js
Outdated
|
||
class ExperimentsAdmin extends Component { | ||
componentDidMount() { | ||
this.props.load(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to fix it, just to let you know. Here we have kind of "bug" here. Because user will come here from another page in most of the cases we will have experiments in the store already. So a user will see the list of experiments and in few milliseconds it will be replaced by a loader and then by list again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was actually copied from Experiments.js.
I guess one way to handle this would be to have an expiration date for the data, and then the function could load only if the list is empty, or the expiration date passed... But for now in my opinion a forced load is not that inconvenient for the user experience.
return api | ||
.createExperiment(name, description) | ||
.then(() => { | ||
dispatch({ type: CREATE_SUCCESS }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to fix. Just let you know, an optimistic update would fit nicely here.
This is how it looks, based on the wireframes provided by @ricardobaeta here #160 (comment), but keeping the name 'experiment' for consistency with the rest of the app. |
Conflicts appeared :( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some comments.
also tests are missed.
src/state/experiments.js
Outdated
uploadResult: UPLOAD_RES_NONE, | ||
|
||
editModalShow: false, | ||
editModalExpID: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you please keep names consistent with all other js files?
In js in general and in code-annotation frontend camelCase is a standard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must be missing something, sorry, can you paste me the exact change you have in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
editModalExpID
-> editModalExpId. Just because it's
Idnot
ID` everywhere.
src/state/experiments.js
Outdated
export const UPLOAD = 'ca/experiments/UPLOAD'; | ||
export const UPLOAD_SUCCESS = 'ca/experiments/UPLOAD_SUCCESS'; | ||
export const UPLOAD_ERROR = 'ca/experiments/UPLOAD_ERROR'; | ||
export const OPEN_EDIT_MODAL = 'ca/experiments/OPEN_EDIT_MODAL'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usually, it's a good idea to keep store agnostic to presentation level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest I didn't like that much the previous code, but I didn't want to spend much more time refactoring it. I've tried to improve it in ce357ff, moving the modal state to Experiments.js
this.state.nameVal, | ||
this.state.descriptionVal | ||
); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please follow early return
rule as all other js code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in 0fb33e0
Can you please point me to a guideline (like the airbnb one we use in eslint.rb) where this rule is explained?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the rule usually you look how the current code is written and do the same.
there is a rule about no-else return in airbnb:
https://github.com/airbnb/javascript#blocks--no-else-return
no always early return because sometimes it makes sense to not return early (though it was there before)
you can read more why people do that by simple googling early return
. It's very popular pattern in javascript. Java style of else
everywhere is much less common.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks for clarifying. I'm aware of the rules you mention, but I just didn't see them applying here and wanted to check if you were talking about something else. From my point of view the code was:
if (condition)
somehting()
else
other()
end
// Posibly some more code
Now I understand your point of view, as there was no more code after the else.
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
const exp = nextProps.editModalExperiment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this code 2 times. The copy-past itself is not a problem. But component depends on both constructor and componentWillReceiveProps. So any the change must be done in both places. It would be much better to extract this code to function which returns state. And do this.state = fn(props)
in constructor and this.setState(fn(nextProps))
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, it's an easy improvement. Changed in dd9e4fb
|
||
componentWillReceiveProps(nextProps) { | ||
const exp = nextProps.editModalExperiment; | ||
if (exp !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the check should compare nextProps with current props. If you need more info, come over and I can explain it verbally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in dd9e4fb
|
||
let uploadIcon; | ||
let uploadClass; | ||
if (uploadInProgress) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like switch
will be better fit here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? The chained if's condition use two different variables, I don't think it will be cleaner with a switch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and if
→ else switch
would be clearer, but I'd not block this wonderful work for this cosmetic suggestion 😉
if (uploadInProgress) { /* something */ }
else switch (uploadResult) {
case UPLOAD_RES_SUCCESS: /* something */ break;
case UPLOAD_RES_FAILURE: /* something */ break;
default: /* something */
}
} | ||
|
||
return ( | ||
<Modal show={editModalShow} onHide={this.handleClose}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend controlling show
outside of the modal component. In general it's better to do editModalShow && <Modal>
. Otherwise it might not render virtual/real dom if the underlying component is written well, but all life cycle methods would be executed on every parent update.
Not very important in this particular case though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I decided to leave it as a property of the Modal component to avoid introducing more and more changes. Let me know if you think this should be changed considering the last changes.
src/state/experiments.js
Outdated
.then(res => { | ||
dispatch({ type: CREATE_SUCCESS }); | ||
dispatch(load()); | ||
dispatch(showEditModal(true, res.id)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here is a good example why it's better to keep store view agnostic. create
action can be called from anywhere. But it will set the modal show to true. It is super confusing and can cause weird bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in ce357ff
Thank you @carlosms :) It looks great! |
@dpordomingo I rebased and there are no more conflicts (I was avoiding it to not loose the commit hashes). @smacker feel free to give it another pass. I was delaying making the tests to wait for change requests, I'll upload them shortly. |
export const LOAD_SUCCESS = 'ca/experiments/LOAD_SUCCESS'; | ||
export const LOAD_ERROR = 'ca/experiments/LOAD_ERROR'; | ||
export const SET = 'ca/experiments/SET'; | ||
export const CREATE = 'ca/experiments/CREATE'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not really wrong, but kind of weird. You can see how it's done in redux usually in official examples: https://github.com/reactjs/redux/tree/master/examples/todomvc/src
this.props.onOpenEdit(expId); | ||
}); | ||
|
||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not required here, but usually it's a good idea when you have a promise to return it. For example, you would need it for tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍
if (create) { | ||
this.props | ||
.experimentCreate(this.state.nameVal, this.state.descriptionVal) | ||
.then(expId => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually put such logic in dispatchToProps, to keep code of the component abstract. Not a big problem though.
Object { | ||
"createInProgress": false, | ||
"error": "test error", | ||
"initialState": Object { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's incorrect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I missed the spread operator while c&p and of course the tests were not failing 😄
exports[`experiments/reducer CREATE_SUCCESS 1`] = ` | ||
Object { | ||
"createInProgress": false, | ||
"initialState": Object { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's incorrect.
exports[`experiments/reducer UPDATE_ERROR 1`] = ` | ||
Object { | ||
"error": "test error", | ||
"initialState": Object { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's incorrect.
|
||
exports[`experiments/reducer UPDATE_SUCCESS 1`] = ` | ||
Object { | ||
"initialState": Object { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's incorrect.
if (create) { | ||
this.props | ||
.experimentCreate(this.state.nameVal, this.state.descriptionVal) | ||
.then(expId => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in case of error here will be undefined and this.props.onOpenEdit
will fail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- tests are incorrect
- found 1 bug
- few suggestions (not required, up to you)
const errText = 'some error'; | ||
fetch.mockReject(errText); | ||
|
||
store.dispatch(create('new_name', 'new_desc')).catch(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
problem with this test: if the action doesn't throw any error, test still pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed: 435f086
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow!!! it worked perfectly! Many many thanks!
I only detected some (imho) UX problems that should be confirmed by @ricardobaeta in a next iteration.
</p> | ||
</Col> | ||
<Col xs={5} className="text-right"> | ||
<FormGroup controlId="uploadTest"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If uploadDisabled
is true because it is being created a new experiment, the upload button should not appear at all.
{Math.round(exp.progress)}% | ||
</td> | ||
<td className="experiments-list__additional-actions"> | ||
{/* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's in master, I just changed the indentation 🤷♂️
return this.props | ||
.experimentCreate(this.state.nameVal, this.state.descriptionVal) | ||
.then(expId => { | ||
this.props.onOpenEdit(expId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is kind of weird that after creating an experiment, the same modal is opened again without giving any special feedback to the user.
After creating a new experiment, I wouldn't open the modal automatically (as in edition-mode), so the user should open it manually -> she/he will be then aware of the edit-state.
this.props.editModalExperiment.id, | ||
this.state.nameVal, | ||
this.state.descriptionVal | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The save button should not open the same modal again without clear feedback.
- If the update succeeds, it should be said so, and the modal should be closed
- if the update fails, it should be shown an error
<FormGroup controlId="uploadTest"> | ||
<FormControl | ||
type="file" | ||
onChange={this.handleUpload} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not push/save the new DB till the user press "save".
@carlosms you did a great job, many thanks. Since most important requirements were already addressed, we can merge, so let's proceed as it follows:
|
Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Add api call & reducer for sqlite DB upload Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Rename OnProgress -> InProgress Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Update experiments test snapshot Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Remove /experiments route, move btns to /dashboard Adds tmp buttons to the dashbard to test the API Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> New modal component to create new experiments Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Add experiment update action to the frontend Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Add the upload DB option to the modal Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Add styling Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Refactor ExperimentEditModal.handleSave Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Refactor ExperimentEditModal state from props Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Refactor experiments modal state out of the store Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Rename ID -> Id for consistency Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Update tests Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Fix bug in tests Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Fix bug when create action fails Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com> Add expect.hasAssertions to tests Signed-off-by: Carlos Martín <carlos.martin.sanchez@gmail.com>
Part of #160, this PR will add a new route to create & edit experiments, and a way to upload a new SQLite DB with file pairs.