-
Notifications
You must be signed in to change notification settings - Fork 74
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
THREESCALE-10245: access tokens expiration UI #3943
Conversation
058be50
to
54db7d7
Compare
c9d2ba3
to
995530a
Compare
Great job @jlledom ! 🏆
Depending if it's US or not, the date can be interpreted differently. I think the following presentation as it shows on the different screen is better. Also, I think it would be nice to show the expiration date on the table listing all tokens... |
app/javascript/src/AccessTokens/components/ExpirationDatePicker.tsx
Outdated
Show resolved
Hide resolved
I couldn't do it. Basically I though on two approaches:
Both failed. 1 can't be done because the Patternfly react component doesn't accept ant time zone, it will always use the OS timezone.
Done 88badaf |
app/javascript/src/AccessTokens/components/ExpirationDatePicker.tsx
Outdated
Show resolved
Hide resolved
app/javascript/src/AccessTokens/components/ExpirationDatePicker.tsx
Outdated
Show resolved
Hide resolved
From #3951:
I didn't do this one because I think the current format is clear enough and it's formatted in client's time zone. Not sure if using UTC instead would make things clearer, as UTC time can belong to a different day to the client time zone.
I agree on these two, but I didn't applied them because they make tests much more difficult. Check pickDate on the test suite. |
Ooh, well, that's sad, because I think they are very useful, especially not being able to pick later dates - because it doesn't make sense. |
Hm, well, I think it's confusing... I don't know where the locale setting for I'd prefer at least |
I'd say it comes from the OS.
Which screens? maybe those screens take the locale from the provider settings. |
I correct myself, only the timezone can be configured for the provider, locale is always the same in porta for everybody. However, we don't use a single date format in all screens. I think I'm gonna implement one of your suggestions. |
@mayorova I updated the date format in the field hint. Also, since I don't think we can, or should, fix the time zone mismatch problem, I added a warning to clarify which time will the token expire at. The tooltip only appears when there's a timezone mismatch between the browser and the provider configuration. |
app/javascript/src/AccessTokens/components/ExpirationDatePicker.tsx
Outdated
Show resolved
Hide resolved
Thank you for applying the suggestions! I have some more 😬 #3954 Basically, when first reviewing (and actually at each consequent review) I had a hard time understanding, what each variable is for... So, I am suggesting some refactoring. Also, I think some suggestions from #3951 are still relevant... Specifically,
I remember that complicated the tests... but maybe we can somehow still incorporate this? I think it's not a great UX, if you can set past dates, thus making the token invalid automatically. |
I applied your suggestions with great joy and rejoicing.
I'll try it again then... |
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 am sorry... 😅 reviewers will review!
app/javascript/src/AccessTokens/components/ExpirationDatePicker.scss
Outdated
Show resolved
Hide resolved
app/javascript/src/AccessTokens/components/ExpirationDatePicker.tsx
Outdated
Show resolved
Hide resolved
const dropdownDate = useMemo(() => { | ||
if (dropdownSelectedItem.period === 0) return null | ||
|
||
return new Date(today.getTime() + dropdownSelectedItem.period * dayMs) | ||
}, [dropdownSelectedItem]) | ||
|
||
const selectedDate = useMemo(() => { | ||
let value = null | ||
|
||
if (dropdownDate) { | ||
value = dropdownDate | ||
} else if (dropdownSelectedItem.id === 'custom' ) { | ||
value = calendarPickedDate | ||
} | ||
|
||
return value | ||
}, [dropdownDate, dropdownSelectedItem, calendarPickedDate]) | ||
|
||
const formattedDateValue = useMemo(() => { | ||
if (!selectedDate) return | ||
|
||
const formatter = Intl.DateTimeFormat('en-US', { | ||
month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false | ||
}) | ||
|
||
return formatter.format(selectedDate) | ||
}, [selectedDate]) | ||
|
||
const fieldHint = useMemo(() => { | ||
if (!formattedDateValue) return | ||
|
||
return `The token will expire on ${formattedDateValue}` | ||
}, [formattedDateValue]) | ||
|
||
const inputDateValue = useMemo(() => { | ||
if (!selectedDate) return '' | ||
|
||
return selectedDate.toISOString() | ||
}, [selectedDate]) | ||
|
||
const tzMismatch = useMemo(() => { | ||
if (tzOffset === undefined) return | ||
|
||
// Timezone offset in the same format as ActiveSupport | ||
const jsTzOffset = new Date().getTimezoneOffset() * -60 | ||
|
||
return jsTzOffset !== tzOffset | ||
}, [tzOffset]) | ||
|
||
const labelIcon = useMemo(() => { | ||
if (!tzMismatch) return | ||
|
||
return ( | ||
<Popover | ||
bodyContent={( | ||
<p> | ||
Your local time zone differs from the provider default. | ||
The token will expire at the time you selected in your local time zone. | ||
</p> | ||
)} | ||
headerContent={( | ||
<span>Time zone mismatch</span> | ||
)} | ||
> | ||
<button | ||
aria-describedby="form-group-label-info" | ||
aria-label="Time zone mismatch warning" | ||
className="pf-c-form__group-label-help" | ||
type="button" | ||
> | ||
<OutlinedQuestionCircleIcon noVerticalAlign /> | ||
</button> | ||
</Popover> | ||
) | ||
}, [tzMismatch]) |
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 sure what's the rationale here, but using useMemo
is generally a bad idea. Memoization in React is nothing like Rails' (or other proper languages) and in the end it makes it less performant.
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 use it to declare a variable as reactive, e.g.:
const selectedDate = useMemo(() => {
...
}, [dropdownDate, dropdownSelectedItem, calendarPickedDate])
selectedDate
must be re-evaluated whenever dropdownDate
, dropdownSelectedItem
or calendarPickedDate
change.
Is this not the right way to do it?
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.
useMemo
is for memoization of very heavy computations. What you want you usually do in the onChange methods as a callback or simply declaring the variables in the body of the component. Every time there is a change, the component will re-render and every variable re-evaluated.
For instance inputDateValue
depends directly on selectedDate
, so you simply do:
const inputDateValue = selectedDate?.toISOString() // -> string | 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.
I refactored here: 4d8a47c
const today: Date = new Date() | ||
const tomorrow: Date = new Date(today) | ||
tomorrow.setDate(today.getDate() + 1) |
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 is be redefined every time the component re-renders, do we want this? Otherwise I would suggest creating a state.
const [today, _] = useState(new Date())
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 which event does the component re-render? only when the screen is loaded? or once after every state change?
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.
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 which event does the component re-render? only when the screen is loaded? or once after every state change?
With every state change.
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.
Thanks, I didn't know this and that's crucial to understand how the whole thing works.
) | ||
} | ||
|
||
const ExpirationDatePickerWrapper = (props: Props, containerId: string): void => { createReactWrapper(<ExpirationDatePicker id={props.id} label={props.label} tzOffset={props.tzOffset} />, containerId) } |
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.
You can use prop spread here, it used to be dangerous but Typescript is able to check the type.
const ExpirationDatePickerWrapper = (props: Props, containerId: string): void => { createReactWrapper(<ExpirationDatePicker id={props.id} label={props.label} tzOffset={props.tzOffset} />, containerId) } | |
const ExpirationDatePickerWrapper = (props: Props, containerId: string): void => { createReactWrapper(<ExpirationDatePicker {...prop} />, containerId) } |
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.
Yes, I saw that in other components, but when I try to do it here I get a linter error: react/jsx-props-no-spreading
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, that rule was enforced back when we didn't use Typescript but Flow. Type checking was so bad that doing the spread will not check for missing/incorrect props. But Typescript does do that, so feel free to disable that linter error like it's done in other files.
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.
@@ -7,3 +7,14 @@ | |||
= form.input :permission, as: :patternfly_select, | |||
collection: @access_token.available_permissions, | |||
include_blank: false | |||
|
|||
- if @access_token.persisted? |
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 is the kind of stuff that belongs to the presenter IMHO. Also, it's not recommended to use instance variables in partials, pass it as a local instead.
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 form is loaded from #new
and #edit
actions, and they would probably need a different presenter each. I didn't move this logic to a presenter to avoid creating a new presenter for #edit and having to duplicate a new method access_token_persisted?
in both presenters.
I moved the instance var to a local, though: 4acb540
const fieldLabel = label ?? 'Expires in' | ||
|
||
const handleOnChange = (_value: string, event: FormEvent<HTMLSelectElement>) => { | ||
const value = (event.target as HTMLSelectElement).value |
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.
Aren't event.target.value
and value
the same?
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.
Yes: 3047613
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.
Code-wise it looks good to me, but I didn't test the branch locally.
const formattedDateValue = computeFormattedDateValue(selectedDate) | ||
const fieldHint = computeFieldHint(formattedDateValue) | ||
const tzMismatch = computeTzMismatch(tzOffset) | ||
const labelIcon = computeLabelIcon(tzMismatch) |
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 probably suggest a more specific name to clarify what this is for, e.g. tzMilsmatchIcon
and computeTzMismatchIcon
// Timezone offset in the same format as ActiveSupport | ||
const jsTzOffset = new Date().getTimezoneOffset() * -60 |
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, that's confusing 😅 Not only they are not in the same granularity (seconds vs minutes), but also different signs (negative vs positive) 🤪
<Popover | ||
bodyContent={( | ||
<p> | ||
Your local time zone differs from the provider default. |
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 don't quite like the reference to "provider" here, I think it's confusing.
Maybe we can say "Your local time zone differs from the account settings." ?
Also, this feature is new since I reviewed 😅 I don't think it's necessary, honestly. The time zone in account details only affects analytics graphs, I believe... (or maybe I'm wrong, and it affects something else). But as it's already implemented, I think it's OK.
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 problem is we have three timezones here:
- DB records are stored in UTC
- Account settings timezone
- Browser timezone
e.g. If a provider has timezone set to UTC-3, the browser has timezone UTC+1 and you select February 11, 2031, 00:00, You will see this in the browser:
This after sending the form:
And this in the DB:
Those three times are the same moment but in different TZs. It's confusing for the user. I tried to configure the patternfly calendar to receive the account timezone and work on that TZ but it's not possible AFAIK. So I added the warning to heads up the user that they might see different timestamps than the one they selected, but that's ok and the actual moment when the token will expire is what they selected in the form.
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.
e.g. If a provider has timezone set to UTC-3, the browser has timezone UTC+1 and you select February 11, 2031, 00:00, You will see this in the browser:
Oh, OK... I didn't know that this timestamp in the view/index templates would show in the provider account TZ?... I thought it wouldn't be converted, and would be shown in UTC. Yeah, then it makes sense. I guess that's because of
porta/app/controllers/application_controller.rb
Lines 123 to 131 in 15d75c5
def set_timezone | |
if current_account && (current_account.provider? || current_account.master?) | |
Time.zone = current_account.timezone if current_account | |
elsif current_account | |
Time.zone = current_account.provider_account.timezone | |
else | |
Time.zone = Time.zone_default | |
end | |
end |
Other than a couple of inline comments - it looks good! |
@mayorova I implemented your suggestions |
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.
Great job @jlledom !
665c6d7
to
70bf3ec
Compare
What this PR does / why we need it:
After we merged #3939 now we want to update the Access Tokens screens to add a new field for the expiration date. For this, I created a new React component based on what Github does:
Which issue(s) this PR fixes
THREESCALE-10245
Verification steps
Just set the an expiration date when creating a token and verify the data is correctly stored in DB and is properly shown in
#index
and#edit
screensScreenshots