Update the API Tokens UI for setting token expiration dates.

Review Request #12597 — Created Sept. 15, 2022 and submitted

Information

Review Board
release-5.0.x

Reviewers

This change allows people to set token expiration dates from the API tokens
config page through a date picker. Consequently, expiration dates can also be
updated through the API. We create a RB.DateInlineEditorView to handle the
UI for the date editing.

This also updates the page to show the invalidated/expired text in
in red so that they stand out against valid tokens.

While running the JS unit tests it was discovered that the
RB.InlineEditorView would not disconnect from a window resize event that
it was handling, which caused problems with other JS tests when running the
full suite. This change adds a RB.InlineEditorView.remove() method to
handle this and disconnect from any events.

  • Created new unit tests for setting the expiration date via the API
    and ran all tests in reviewboard.webapi.tests.test_api_token.py.
  • Manually tested the API tokens page, setting various expiration dates and
    making sure they were displayed and saved to the database properly.
  • Created DateInlineEditorView JS tests and ran all JS tests.
  • Manually tested resizing my browser that had the new review request page
    open on it and saw that any inline editors that I was using on the page
    would resize accordingly.
Summary ID
Add UI for setting token expiration dates.
c0df10e2110a52e84e275b56efc4f739ee29647d

Description From Last Updated

Missing semicolon. Column: 82 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 49 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 23 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 44 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 56 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 80 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 66 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 41 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 44 Error code: W033

reviewbotreviewbot

Question: Where should I document the options that I added (descriptorText, minDate, maxDate)? I'll remove the comments that are next …

maubinmaubin

Missing semicolon. Column: 44 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 53 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 60 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 34 Error code: W033

reviewbotreviewbot

I'll cover some of the stuff in the HTML and the CSS in this comment. Our modern CSS component conventions …

chipx86chipx86

Since it's just 0, no need for em.

chipx86chipx86

This line is too long.

chipx86chipx86

For JavaScript, multi-line comments should look like: /* * ... * ... */

chipx86chipx86

Date objects in JavaScript are the absolute worst, and doing operations on them scares me. So many things can, and …

chipx86chipx86

No need fro the parens here. We should include a default value inside of <time> though.

chipx86chipx86

Two notes: We should keep this alphabetical. The last item should have a trailing comma (safe for ES6 files).

chipx86chipx86

As a ES6 file, you can do: $(window).on(`resize.${this.cid}`, ...) Also, this can be one line.

chipx86chipx86

As this will be (eventually) ReStructuredText, code literals need double backticks.

chipx86chipx86

5.0.

chipx86chipx86

This won't ultimately parse right. Instead, let's use our standard doc syntax we'd use for any attribute. /** * Summary …

chipx86chipx86

I suspect this can fit in one line.

chipx86chipx86

Let's be explicit about local time vs. UTC in these.

chipx86chipx86

<input> tags don't have an end tag, so this should be <input type="date"/>. How important is it that we have …

chipx86chipx86

.attr takes a dictionary of keys/values, which is good for multiple attributes.

chipx86chipx86

When there's no need to continue chaining, you can keep this as one line.

chipx86chipx86

If this never changes, it should be const.

chipx86chipx86

These cam use <URL> if decorating with @webapi_test_template.

chipx86chipx86

Let's add/update tests to do some explicit checking around timezones and conversion.

chipx86chipx86

Missing semicolon. Column: 74 Error code: W033

reviewbotreviewbot

Missing semicolon. Column: 15 Error code: W033

reviewbotreviewbot

I think it makes more sense and follows our CSS docs standards more if I document these modifiers above the …

maubinmaubin

Should be </p>

chipx86chipx86

I don't see usage in here. Even if we don't style it, we should have a documented block and an …

chipx86chipx86

The * should all be aligned.

chipx86chipx86

We should pick one, and use styling to ensure consistent presentation (being explicit about any margins/padding that would otherwise be …

chipx86chipx86

</...> for end tags, <.../> for self-closing tags (like <input> or <img>).

chipx86chipx86

These can be one rule: &.-is-expired, &.-is-invalid { color: red; }

chipx86chipx86

The caller may pass null (as per the event handler further down). We should document what happens if passed.

chipx86chipx86

saveExpires is indented 1 space too far.

chipx86chipx86

expiresTimeHTML needs to be passed in when instantiating the template. I don't see that happening in this change. That would …

chipx86chipx86

datetime would be more technically correct.

chipx86chipx86

Maybe it'd be better to just store _$tokenState, and then we can determine things as-needed based on the modifiers?

chipx86chipx86

No need for a blank line here. These are related statements.

chipx86chipx86

I'm not sure if async is needed here, since I don't think we're dealing with any functions that also need …

chipx86chipx86

Let's put parens around this so order of operations is very clear.

chipx86chipx86

check_post_result does a good job of making sure the response and the resulting object match expectations, but we still need …

chipx86chipx86

Let's hard-code the dates we want to set and compare, to ensure there's no variability based on time wonkiness. "now" …

chipx86chipx86

Same comments about "now" timestamps. Let's hard-code known ISO 8601 timestamps and check results explicitly.

chipx86chipx86

One last thing :) We want to keep the model as the owner of this value. Models keep state/state-related logic, …

chipx86chipx86

dedent is no longer needed. You can just use backticks. Right before this statement is where we should pull out …

chipx86chipx86

Rather than depend back on expires, we want to check the exact generated timestamp string we want to see. The …

chipx86chipx86

We want to avoid a multi-line string here (this will capture all whitespace inside the of the backticks), so let's …

chipx86chipx86

We just need to make sure the user's profile's timezone is actually taken into account. So what I suggest is …

chipx86chipx86
Checks run (1 failed, 1 succeeded)
flake8 passed.
JSHint failed.

JSHint

maubin
Review request changed
Description:
   

Add UI for setting token expiration dates.

   
~  

This also updates the API token UI to show in red text when tokens

~   became invalidated/expired so that they stand out against valid tokens.

  ~

This also updates the API token UI to show the text when tokens

  ~ became invalidated/expired in red so that they stand out against valid tokens.

  +
  +

Note: I'm going to fill out this description more and add screenshots

  + showing off the UI tomorrow.

Testing Done:
  +
  • Created new unit tests for setting the expiration date via the API
    and ran all tests in reviewboard.webapi.tests.test_api_token.py.
  +
  • Manually tested the API tokens page, setting various expiration dates and
    making sure they were displayed and saved to the database properly.
  +
  • QUESTION: I'm unsure of what to do for frontend tests for
    DateInlineEditorView and the apiTokensView page. The inlineEditorView
    doesn't have any tests but the other UI views in that directory have some
    so I might base some tests off that.
  +
  +

Note: Will run all js tests and unit tests tomorrow.

Commits:
Summary ID
Add UI for setting token expiration dates.
113cc0c3d70cbff350a93a6bac8340258d1f1efa
Add UI for setting token expiration dates.
0b01b729032cfd24ff9c40388dbce3eeb9a604a3

Checks run (1 failed, 1 succeeded)

flake8 passed.
JSHint failed.

JSHint

maubin
  1. 
      
  2. reviewboard/static/rb/js/ui/views/inlineEditorView.es6.js (Diff revision 2)
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
    Show all issues

    Question: Where should I document the options that I added (descriptorText, minDate, maxDate)? I'll remove the comments that are next to them afterwards.

    1. We don't really have a pattern for this specific situation. For now, maybe just put a doc comment above each one you want to document, in the standard summary/description/Type form.

      As to the question in the review request, you can create a new test file for this subclass. There's a lot of ways these can be organized, but textEditorViewTest.es6.js is probably a good template. You can just test the operations that invoke these fields. So you could test:

      • Construction, making sure the DOM looks the way you expect, with the right attributes.
      • Event handling on the field (though ideally we could reuse the built-in events and not have to maintain that event handler in two places).
      • Loading/rendering from an existing value
      • Saving a new date
      • Saving an empty value
      • Saving but without any changes made
  3. 
      
maubin
maubin
Review request changed
Change Summary:
  • Added JS tests for DateInlineEditorView
  • Instead of repeating the InlineEditorView's event handler code we call it's event handler
    method and then add our own event handling.
Summary:
[WIP] Update the API Tokens UI for setting token expiration dates.
Update the API Tokens UI for setting token expiration dates.
Testing Done:
   
  • Created new unit tests for setting the expiration date via the API
    and ran all tests in reviewboard.webapi.tests.test_api_token.py.
   
  • Manually tested the API tokens page, setting various expiration dates and
    making sure they were displayed and saved to the database properly.
~  
  • QUESTION: I'm unsure of what to do for frontend tests for
    DateInlineEditorView and the apiTokensView page. The inlineEditorView
    doesn't have any tests but the other UI views in that directory have some
    so I might base some tests off that.
  ~
  • Created DateInlineEditorView JS tests and ran all JS tests.
-  
-  

Note: Will run all js tests and unit tests tomorrow.

Commits:
Summary ID
Add UI for setting token expiration dates.
c5889832619c08676070f9d594fca6d85fbf687e
Add UI for setting token expiration dates.
80b22d1eb1365113d442b48c7765abe7429bd823

Checks run (1 failed, 1 succeeded)

flake8 passed.
JSHint failed.

JSHint

maubin
maubin
maubin
maubin
chipx86
  1. 
      
  2. reviewboard/static/rb/css/pages/my-account.less (Diff revision 8)
     
     
     
     
     
    Show all issues

    I'll cover some of the stuff in the HTML and the CSS in this comment.

    Our modern CSS component conventions basically define two ways of naming a CSS class for something within a component:

    1. component__child, of which there should really only ever be one level of nesting
    2. -modifier, which is tacked onto a component/child CSS name to modify something about it.

    A big part of the approach with this is to think hard about the structure of the component, giving everything a purpose. So rather than "these things go in <p> tags", we want to think "these things go in these named child objects."

    So let's go through and see how we can take advantage of that here.

    The HTML in this change is introducing:

    • rb-c-config-api-tokens__info-is-invalid
    • rb-c-config-api-tokens__info__expires

    This is close, but not quite to spec.

    Looking at this, it seems this specific section has essentially four states:

    1. Token is expired ("Expired ...")
    2. Token is valid and will expire ("Expires ...")
    3. Token is valid and will not expire ("Never expires")
    4. Token is invalid ("Invalidated ...")

    We could go a couple ways here. We could combine these under one concept ("lifecycle" or "state" or something?) or break them into multiple types of children.

    Let's go with the former. We'll call this rb-c-config-api-tokens__token-state, and control the state through modifiers:

    • -is-expired
    • -is-invalid

    We'd only ever use one at once. Think of it like enum values. If neither are set, it's in a default situation.

    (We should also have rb-c-config-api-tokens__usage, to cover the "Last used"/"Never used". Even if we don't style it differently. We're defining the component structure.)

    Now I think ideally, we'd actually give each token item its own full CSS component, moving things like the state modifiers (-is-expired, ...) to the main component class. That's a bigger change, though, and I'd rather think about that separately.

  3. Show all issues

    Since it's just 0, no need for em.

  4. Show all issues

    This line is too long.

  5. Show all issues

    For JavaScript, multi-line comments should look like:

    /*
     * ...
     * ...
     */
    
  6. Show all issues

    Date objects in JavaScript are the absolute worst, and doing operations on them scares me. So many things can, and will, go wrong.

    I'd recommend we instead use the moment.js library. This library lets us do operations on dates without worrying about the problems that tend to come up, and keeping code very self-explanatory. We already bundle this with Review Board.

    We also have to be very careful with timezones. Some notes on that:

    1. new Date() always gives us local time in the browser.
    2. Timestamps we get from the server contain timezone information.
    3. Timestamps we're sending currently do not, but they need to.
    4. If they don't, the server will interpret them based off the timezone in the Profile, which may not match
    5. We want to be sure we always show local time to the user.

    Okay, so let's update the code to be more readable, safer, and timezone-aware:

    This first block can work like:

    // Here, we're operating in local time, and can perform some safe date math.
    const tomorrow = moment.local().add(1, 'days');
    
    /*
     * Since we're dealing with a server-side value (as per #2 above), we know
     * we have the timezone, which moment() will handle. We also want to convert
     * to local time.
     */
    const expires = moment(this.model.get('expires')).local();
    
    ...
    
    // Now we can format that how we wish.
    {
        ...
        minDate: tomorrow.format('YYYY-MM-DD'),
        rawValue: expires.format('YYYY-MM-DD'),
    }
    
    // To build a timezone-aware midnight ISO 8601 timestamp off a local timestamp to send to the server:
    moment.local(value).startOf('day').format()
    
    // *If* we had wanted to explicitly convert to UTC, but with midnight local time:
    moment.local(value).startOf('day').utc().format()
    
    etc.
    

    It's quite a nice library, and if we're careful and explicit, we avoid a lot of timezone conversion issues.

  7. Show all issues

    No need fro the parens here.

    We should include a default value inside of <time> though.

    1. What default value should we use? And in what case would the default value be called.

    2. Actually, go ahead and scrap this.

      The idea was to have a human-readable value we could put between the tags in case the timesince functionality fails, but it's probably not important here.

  8. Show all issues

    Two notes:

    1. We should keep this alphabetical.
    2. The last item should have a trailing comma (safe for ES6 files).
  9. Show all issues

    As a ES6 file, you can do:

    $(window).on(`resize.${this.cid}`, ...)
    

    Also, this can be one line.

  10. Show all issues

    As this will be (eventually) ReStructuredText, code literals need double backticks.

  11. Show all issues

    5.0.

  12. Show all issues

    This won't ultimately parse right. Instead, let's use our standard doc syntax we'd use for any attribute.

    /**
     * Summary here.
     *
     * Optional description.
     *
     * Type:
     *     ...
     */
    
  13. Show all issues

    I suspect this can fit in one line.

  14. Show all issues

    Let's be explicit about local time vs. UTC in these.

  15. Show all issues

    <input> tags don't have an end tag, so this should be <input type="date"/>.

    How important is it that we have the <span>?

  16. Show all issues

    .attr takes a dictionary of keys/values, which is good for multiple attributes.

  17. Show all issues

    When there's no need to continue chaining, you can keep this as one line.

  18. Show all issues

    If this never changes, it should be const.

  19. reviewboard/webapi/tests/test_api_token.py (Diff revision 8)
     
     
     
    Show all issues

    These cam use <URL> if decorating with @webapi_test_template.

  20. reviewboard/webapi/tests/test_api_token.py (Diff revision 8)
     
     
     
     
     
    Show all issues

    Let's add/update tests to do some explicit checking around timezones and conversion.

    1. What should I check specifically? I know the DateTimeFieldType tests do some testing with naive/aware dates, should it be similar to that?

    2. I'd do a test with each of the following conditions:

      1. Set an ISO 8601 timestamp with UTC timezone, check result in database.
      2. Set an ISO 8601 timestamp with a non-UTC timezone, check result in database.
      3. Set an ISO 8601 timestamp without a timezone, check result in database (should be based on profile's set timezone).
  21. 
      
maubin
Review request changed
Commits:
Summary ID
Add UI for setting token expiration dates.
ffc92bb5ca55b3adc37ee23d2816cf85a51d0e7b
Add UI for setting token expiration dates.
fc727909011d591baf6be378682d119d2b381839

Checks run (1 failed, 1 succeeded)

flake8 passed.
JSHint failed.

JSHint

maubin
maubin
  1. 
      
  2. reviewboard/static/rb/css/pages/my-account.less (Diff revision 10)
     
     
     
     
     
    Show all issues

    I think it makes more sense and follows our CSS docs standards more if I document these modifiers above the &__token-state component. But then where should I document the -has-last-used modifier for rb-c-config-api-tokens__usage?

    1. Modifiers should be documented on the element they'd apply to. So if -has-expires goes on token-state, that's where you'd document it.

      -has-expires would make sense to document on the main component only if we had a component per-token, and had the modifier on the component itself.

      We document each level. The main component and each child. The Structure part only needs to show that level and the general layout of children that live directly underneath, rather than the entirety of the structure of the component.

    2. Also, children should be listed in alphabetical order. This is because we keep nesting low, and thus the structure doesn't manifest in the rule order. So alphabetical is our best way of ensuring we know where any new child should go.

  3. 
      
maubin
chipx86
  1. 
      
  2. Show all issues

    Should be </p>

  3. Show all issues

    I don't see usage in here.

    Even if we don't style it, we should have a documented block and an empty rule for it, so the structure is clearly-defined.

  4. reviewboard/static/rb/css/pages/my-account.less (Diff revision 11)
     
     
     
     
    Show all issues

    The * should all be aligned.

  5. Show all issues

    We should pick one, and use styling to ensure consistent presentation (being explicit about any margins/padding that would otherwise be up to the browser for something like a <p>).

    Especially since <span> is inline and <p> is block-level. That can make it hard to maintain. Can we stick with <p> for all?

  6. Show all issues

    </...> for end tags, <.../> for self-closing tags (like <input> or <img>).

  7. reviewboard/static/rb/css/pages/my-account.less (Diff revision 11)
     
     
     
     
     
     
     
     
    Show all issues

    These can be one rule:

    &.-is-expired,
    &.-is-invalid {
        color: red;
    }
    
  8. Show all issues

    The caller may pass null (as per the event handler further down). We should document what happens if passed.

  9. Show all issues

    saveExpires is indented 1 space too far.

  10. Show all issues

    Maybe it'd be better to just store _$tokenState, and then we can determine things as-needed based on the modifiers?

  11. Show all issues

    No need for a blank line here. These are related statements.

  12. Show all issues

    I'm not sure if async is needed here, since I don't think we're dealing with any functions that also need to operate async. I might be wrong. If so, can we document this?

  13. Show all issues

    Let's put parens around this so order of operations is very clear.

  14. reviewboard/webapi/tests/test_api_token.py (Diff revision 11)
     
     
     
     
    Show all issues

    Let's hard-code the dates we want to set and compare, to ensure there's no variability based on time wonkiness. "now" dates in unit tests can become difficult to debug when things go wrong.

  15. reviewboard/webapi/tests/test_api_token.py (Diff revision 11)
     
     
     
    Show all issues

    Same comments about "now" timestamps. Let's hard-code known ISO 8601 timestamps and check results explicitly.

  16. 
      
maubin
chipx86
  1. 
      
  2. Show all issues

    expiresTimeHTML needs to be passed in when instantiating the template. I don't see that happening in this change. That would need to be done by returning it in getRenderContext().

    By default, a template for a ListItemView gets possible <%= ... %> variables from the model attributes or from getRenderContext() results.

    It would only make sense to do this if you are trying to also set datetime= here, and if we're taking this same approach for all other <time> elements in this template.

    Otherwise, it doesn't really make sense to feed in <%= expiresTimeHTML %> at all, just for an empty-by-default <time>. If the goal is to just save a few characters but to populate this tag dynamically after, just hard-code <time></time> here and let that code take care of it.

  3. Show all issues

    datetime would be more technically correct.

  4. reviewboard/webapi/tests/test_api_token.py (Diff revisions 11 - 12)
     
     
     
     
     
     
     
     
     
     
    Show all issues

    check_post_result does a good job of making sure the response and the resulting object match expectations, but we still need to explicitly compare the resulting timestamp in these tests and make sure they contain the value we expect them to.

  5. 
      
maubin
chipx86
  1. 
      
  2. Show all issues

    One last thing :)

    We want to keep the model as the owner of this value. Models keep state/state-related logic, and views represent values. By duplicated storage of state, we risk things getting out of sync.

    The ideal setup is to have the UI always update based on the model. To do this, a lot of views will listen to a change:<attr> for a given attr name that the UI depends on. Some UIs will then selectively update appropriate parts of the DOM, and some will just call render() again.

    This view doesn't do that, which is fine (that very well may change down the road), but the ability to do that depends on avoiding duplication of state from the model. If we copy it here, and use this copy, we get out of sync.

    So let's remove this here, and instead only pull out at the times we need it.

  3. Show all issues

    dedent is no longer needed. You can just use backticks.

    Right before this statement is where we should pull out expires. Just to keep as much logic out of the string as possible, referencing only variables.

  4. reviewboard/webapi/tests/test_api_token.py (Diff revisions 12 - 13)
     
     
    Show all issues

    Rather than depend back on expires, we want to check the exact generated timestamp string we want to see.

    The goal is to be very explicit about what we expect to come back, so we can protect ourselves from any issues introduced by logic changes down the road to either the executed code or the test itself. It also gives us a reference if debugging things down the road, so we know precisely what format of string we think we're working with.

  5. 
      
maubin
chipx86
  1. 
      
  2. Show all issues

    We want to avoid a multi-line string here (this will capture all whitespace inside the of the backticks), so let's put the backtick as close to the tags as possible. It's okay to start this on the second line. So:

    return {
        expiresTimeHTML:
            `<time class="timesince" datetime="${expires}"></time>`,
    };
    
  3. reviewboard/webapi/tests/test_api_token.py (Diff revisions 13 - 14)
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
    Show all issues

    We just need to make sure the user's profile's timezone is actually taken into account. So what I suggest is something like:

    profile = self.user.get_profile()
    profile.timezone = 'US/Eastern'
    profile.save(update_fields=('timezone',))
    
    # ....
    
    # Make sure resulting timezone is offset from Eastern.
    

    Same idea for the PUT.

  4. 
      
maubin
chipx86
  1. Ship It!
  2. 
      
maubin
Review request changed
Status:
Completed
Change Summary:
Pushed to release-5.0.x (5a08a1e)