Related: #1 and #391
1 new commit added
8 new commits added
Hub Config - https://i.imgur.com/inHSGY3.png Widget Config - https://i.imgur.com/rdzNGyc.png Widget - https://i.imgur.com/lyGKzB3.png
Sorry for the redundant messages, I was amending a few commits. I could definitely use reviews on the React code, this is my first time. (Specifically on whether to break any of the stuff down into more components - say an Issue component)
Also, since Github doesn't emit any fedmsgs, how should we decide when to invalidate its cache (setting an expiration time of say 2 minutes) or whether to not cache that data at all?
rebased onto 79f36ceab0fd047cdf130c7bab68468550bfb6b8
rebased onto 8673df484cb82e17915910a6dcb26d8ecfb37c0e
rebased onto 07bea7ada5b17469ff5b8ca8624d755f6a830f5c
14 new commits added
Please name variables in a more explicit manner, so that a reader won't have to go back to the function arguments to see what x is. I suggest configParam for example, but anything works as long as it makes it clear what the variable is about.
x
configParam
Also, you don't actually need i apparently, you could just use the config parameter as a key.
i
I think it would be safer to record e.target.value in a variable before using it in setConfig(), because in React event instances are re-used (for performance reasons). So e.target.value may change down the road when async kicks in.
e.target.value
setConfig()
Do you actually need injectIntl ? you don't seem to use the intl property anywhere, and the FormattedMessage components don't require the injectIntl HOC.
injectIntl
intl
FormattedMessage
You could rewrite this logic as a if (...) {...} else if (...) {...} else {...} block, that would save some indentation and brain processing for the reader (which is more important! :-) )
if (...) {...} else if (...) {...} else {...}
In general I recommend the "Line of sight" practice when writing code: https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88
This is a very big block which would be more readable in its own sub-component.
When you have a big block of output that is generated in a loop like here, it's in general a sign that a sub-component would be useful (and make the code more clear).
I would encourage you to do this kind of actions in React and not rely on jQuery. You could replicate this with a onClick handler that would toggle a boolean in the sate, and the "more" section would only be displayed when the state variable is true.
onClick
In this case, it will stop working if you have more than one issues widget on the page, because the data-target attrbute points to an HTML id attribute, and there can only be one element with the same HTML id on a page.
data-target
id
This footer is pretty big and could have its own component. On top of that it would separate the footer state (used for toggling, see comment below) from the widget state (that would be passed down as props to the footer component).
This could be done in Javascript since it's basically presentation and does not use information on the server that the JS does not have (it only uses the mode variable).
mode
Actually, we do have a hook to send fedmsgs on Github changes: http://fedora-fedmsg.readthedocs.io/en/latest/topics.html#github
It would be nice to be able to set this 15 limit in the widget parameters.
15
I made some comments in the code. In general, this is a very good quality PR, thanks @shaily for your efforts.
2 new commits added
rebased onto 66b689c9543b8fd3ebb173b74b7c6b49b903e360
rebased onto e46644d01a81adc4d610f51b26fba70da09b3fe9
rebased onto fcf3eb43abc0781bc942b2f222be2ef782de1399
rebased onto 52e6c911545f4698dc3253bea6d6f620b3819bfe
19 new commits added
If github_username is not set, its value here is None which will cause the Github API to return a failure a couple lines below. I think both problems should be handled: return an empty list of issues when the username is not set, and display an error if the github API returns an error. FYI the message I'm getting from Github is:
None
{'message': 'Validation Failed', 'documentation_url': 'https://developer.github.com/v3/search/', 'errors': [{'code': 'invalid', 'message': 'The listed users cannot be searched either because the users do not exist or you do not have permission to view the users.', 'field': 'q', 'resource': 'Search'}]}
Oh, and I see you're not calling the function if the VCS username is not set, but the cached function will be called nonetheless by the backend when a matching fedmsg arrives. So you need to handle that case in the function anyway.
Please return False explicitely at the end (for the other cases). Otherwise it will return None, and even if it might work, that's not the method's signature. This is valid for the other VCS types too.
False
It would make sense for stream hubs too.
stream
Unfortunately, if you want to handle the "stream hub" case, you can't just get self.instance.hub.config because the stream hub does not have that info, only the user hub does. Until I figure out a better way (probably by mirroring part of a user hub config into the stream hub config) you'd have to make a special case for the stream hub, do a DB lookup for the corresponding user hub and use that config to get the usernames.
self.instance.hub.config
This API has changed a bit, now we don't use another function, we just react to the new hubs.hub.updated fedmsg topic. You can check out how the repositories widget does it. This comment is valid for the other functions too.
hubs.hub.updated
repositories
The display of the VCS links in the footer seems strange: I would expect the "view more" to be centered and to open a dropdown with the 3 VCS links. At the moment they don't have enough width to display :
https://lut.im/lCGtoSJbRx/rMliYOFCDZvts9Vo.png
Also, please handle the case when there are no open issues : a message should be displayed.
rebased onto 83dcd78cc27ac5aabf7143c7f7ff3dd01b5cabe1
Well, not exactly : if it's a hub.updated message, but not about this instance, it will not reach the final else branch. I would just put the return False outside of the branch, as a final action.
hub.updated
else
return False
rebased onto d87787a80436dbad2febba8af3bfa295a58e9e65
I have tested the widget and found a couple issues. To explain what they are, I'll use a diff of what solution I would suggest, but you're free to address them in another manner if you prefer.
First issue: the configuration panel for VCS does not show validation errors if there are any. I used the MailingList panel as an example of how to show validation errors in the fields, and I came up with this patch:
diff --git a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js index fb7d1478..b4916c26 100644 --- a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js +++ b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js @@ -59,6 +59,10 @@ export default class VCSPanel extends React.Component { <FormattedMessage {...messages.title} tagName="h3" /> <FormattedMessage {...messages.intro} tagName="p" /> {["pagure_username", "github_username", "bugzilla_email"].map(function(configParam) { + let invalid = null; + if (this.props.error && this.props.error.fields) { + invalid = this.props.error.fields[configParam]; + } return ( <div key={configParam} className="form-group row"> <label htmlFor={"hub-settings-vcs-" + configParam}> @@ -67,11 +71,16 @@ export default class VCSPanel extends React.Component { <input type="text" name={configParam} - className={"form-control"} + className={"form-control" + (invalid ? " is-invalid" : "")} id={"hub-settings-vcs-" + configParam} onChange={this.handleChange} value={this.props.hubConfig[configParam] || ""} /> + { invalid && + <div className="invalid-feedback w-100"> + {invalid} + </div> + } <p className="form-text text-muted"> <FormattedMessage {...messages[configParam + "_help"]} /> </p>
It basically detects errors returned by the backend and sets the appropriate classes and feedback field.
Next issue : the widget does not update on a backend change (notified via SSE). The reason is double : the current version does not react to the needsUpdate property being set, and it uses the PureComponent class instead of the Component class, which causes it to ignore redraws on internal state changes (check out the React docs on that one). Here's my proposed solution:
needsUpdate
PureComponent
Component
diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js index 889d7490..98bc88b0 100644 --- a/hubs/static/client/app/widgets/issues/Widget.js +++ b/hubs/static/client/app/widgets/issues/Widget.js @@ -1,5 +1,9 @@ import React from 'react'; import { apiCall } from '../../core/utils'; +import { + widgetDidUpdate, + widgetWillUpdate + } from "../../core/actions/widget"; import WidgetChrome from '../../components/WidgetChrome'; import PropTypes from "prop-types"; import Spinner from "../../components/Spinner"; @@ -7,7 +11,7 @@ import Issue from "./Issue"; import Footer from "./Footer"; -export default class IssuesWidget extends React.PureComponent { +export default class IssuesWidget extends React.Component { constructor(props) { super(props); @@ -26,11 +30,26 @@ export default class IssuesWidget extends React.PureComponent { } } + componentDidUpdate(prevProps, prevState) { + if (this.props.needsUpdate && !this.props.editMode && !this.state.isLoading) { + this.loadFromServer(); + } + } + loadFromServer() { + this.props.dispatch(widgetWillUpdate(this.props.widget.idx)); this.setState({isLoading: true}); apiCall(this.props.widget.urls.data).then( (data) => { - this.setState({issues: data.issues, counts: data.counts, isLoading: false}); + this.setState({ + issues: data.issues, + counts: data.counts, + isLoading: false, + }, + () => ( + this.props.dispatch(widgetDidUpdate(this.props.widget.idx)) + ) + ); }, (error) => { this.setState({error: error.message, isLoading: false});
Changes are: - using Component, - calling loadFromServer() when the needsUpdate property is set - dispatching the widgetWillUpdate and widgetDidUpdate actions to inform Redux of the changes, which will reset the needsUpdate property when the update is done.
loadFromServer()
widgetWillUpdate
widgetDidUpdate
Next issue: the cached function was not invalidated when a VCS username is removed. The reason is that the check on the hubs.hub.updated message topic was done after the test on username is None. In this patch I have fixed another issue: on stream hubs the config would be properly pulled from the user hub, but since the hubs.hub.updated test was only checking the current stream hub id, it would never invalidate when the user hub is updated. Here's my proposed solution:
username is None
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py index e29caa80..aaa1987f 100644 --- a/hubs/widgets/issues/functions.py +++ b/hubs/widgets/issues/functions.py @@ -39,19 +39,19 @@ class GetBugzillaBugs(CachedFunction): def should_invalidate(self, message): if self.instance.hub.hub_type == "stream": - user_hub = Hub.query.filter_by(name=self.instance.hub.name).first() - email = user_hub.config.get('bugzilla_email') + watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first() else: - email = self.instance.hub.config.get('bugzilla_email') + watched_hub = self.instance.hub + if message["topic"].endswith('.hubs.hub.updated'): + return (message['msg']['hub_id'] == watched_hub.id and + 'bugzilla_email' in message['msg']['changed_keys']) + email = watched_hub.config.get('bugzilla_email') if email is None: return False mode = self.instance.config['mode'] if message['topic'].endswith('bugzilla.bug.new') or \ message['topic'].endswith('bugzilla.bug.update'): return message['msg']['bug']['creator' if mode == 'author' else 'assigned_to'] == email - elif message["topic"].endswith('.hubs.hub.updated'): - return message['msg']['hub_id'] == self.instance.hub.id and \ - 'bugzilla_email' in message['msg']['changed_keys'] else: return False @@ -88,10 +88,13 @@ class GetPagureIssues(CachedFunction): def should_invalidate(self, message): mode = self.instance.config['mode'] if self.instance.hub.hub_type == "stream": - user_hub = Hub.query.filter_by(name=self.instance.hub.name).first() - username = user_hub.config.get('pagure_username') + watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first() else: - username = self.instance.hub.config.get('pagure_username') + watched_hub = self.instance.hub + if message["topic"].endswith('.hubs.hub.updated'): + return (message['msg']['hub_id'] == watched_hub.id and + 'pagure_username' in message['msg']['changed_keys']) + username = watched_hub.config.get('pagure_username') if username is None: return False if message['topic'].endswith('pagure.issue.new') or \ @@ -101,9 +104,6 @@ class GetPagureIssues(CachedFunction): message['topic'].endswith('pagure.issue.drop'): entry = message['msg']['issue']['user' if mode == 'author' else 'assignee'] return entry and entry['name'] == username - elif message["topic"].endswith('.hubs.hub.updated'): - return message['msg']['hub_id'] == self.instance.hub.id and \ - 'pagure_username' in message['msg']['changed_keys'] else: return False @@ -139,10 +139,13 @@ class GetGithubIssues(CachedFunction): def should_invalidate(self, message): mode = self.instance.config['mode'] if self.instance.hub.hub_type == "stream": - user_hub = Hub.query.filter_by(name=self.instance.hub.name).first() - username = user_hub.config.get('github_username') + watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first() else: - username = self.instance.hub.config.get('github_username') + watched_hub = self.instance.hub + if message["topic"].endswith('.hubs.hub.updated'): + return (message['msg']['hub_id'] == watched_hub.id and + 'github_username' in message['msg']['changed_keys']) + username = watched_hub.config.get('github_username') if username is None: return False if message['topic'].endswith('github.issue.opened') or \ @@ -151,8 +154,5 @@ class GetGithubIssues(CachedFunction): message['topic'].endswith('github.issue.closed'): entry = message['msg']['issue']['user' if mode == 'author' else 'assignee'] return entry and entry['login'] == username - elif message["topic"].endswith('.hubs.hub.updated'): - return message['msg']['hub_id'] == self.instance.hub.id and \ - 'github_username' in message['msg']['changed_keys'] else: return False
It moves the check for the hubs.hub.updated up in the method, and it uses a watched_hub variable to abstract out the "stream" use case.
watched_hub
Please note that I also just have fixed a bug in the main API views code that would have prevented a config value from being deleted, so you need to rebase if you want to test that use case.
We're almost there! Thanks for persisting. :100:
One last thing: when you're done, run the tox -e lint command and add another commit to fix the errors that flake8 has found. It's mostly about line lengths, etc. If it points out errors that you don't know how to handle properly, feel free to ask me.
tox -e lint
rebased onto 5c4d7bd0c2b28654c2f3c6dbd3dc1db14453c6e2
4 new commits added
Thanks a lot for reviewing!
A small request, could you please verify if the cache invalidation is functioning correctly on widget / hub config change? I think there's some problem on my setup. I verified that the appropriate widget.updated message is being published, but I can't reproduce the correct functioning of the triage queue picking it up and passing on a widget-cache type task to the worker. The issue doesn't appear to be in the way the CachedFunction is implemented.
widget.updated
widget-cache
I think I found why (or at least a potential reason). When updating the widget state after calling the backend, you don't clear the error state variable. So if you went back to having no VCS configured, you can't ever go to the issues display again unless you reload the page. This is a possible patch:
error
--- a/hubs/static/client/app/widgets/issues/Widget.js +++ b/hubs/static/client/app/widgets/issues/Widget.js @@ -45,6 +45,7 @@ export default class IssuesWidget extends React.Component { issues: data.issues, counts: data.counts, isLoading: false, + error: null, }, () => ( this.props.dispatch(widgetDidUpdate(this.props.widget.idx))
rebased onto c66b212b29b1028efbf9d1eb9655aa82fdf3a163
rebased onto 8c911650e5909464113e1ad9eb468c9f6c3cb555
Excellent, thanks a lot @shaily :100:
Pull-Request has been merged by abompard
Related: #1 and #391