-
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
Merged
+483
−54
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
87 changes: 50 additions & 37 deletions
87
app/controllers/provider/admin/user/access_tokens_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,57 @@ | ||
class Provider::Admin::User::AccessTokensController < Provider::Admin::User::BaseController | ||
inherit_resources | ||
defaults route_prefix: 'provider_admin_user', resource_class: AccessToken | ||
actions :index, :new, :create, :edit, :update, :destroy | ||
|
||
authorize_resource | ||
activate_menu :account, :personal, :tokens | ||
before_action :authorize_access_tokens | ||
before_action :disable_client_cache | ||
|
||
def create | ||
create! do |success, _failure| | ||
success.html do | ||
flash[:token] = @access_token.id | ||
flash[:notice] = 'Access Token was successfully created.' | ||
redirect_to(collection_url) | ||
end | ||
end | ||
end | ||
# frozen_string_literal: true | ||
|
||
def index | ||
index! | ||
@last_access_key = flash[:token] | ||
end | ||
module Provider | ||
module Admin | ||
module User | ||
class AccessTokensController < BaseController | ||
inherit_resources | ||
defaults route_prefix: 'provider_admin_user', resource_class: AccessToken | ||
actions :index, :new, :create, :edit, :update, :destroy | ||
|
||
def update | ||
update! do |success, _failure| | ||
success.html do | ||
flash[:notice] = 'Access Token was successfully updated.' | ||
redirect_to(collection_url) | ||
end | ||
end | ||
end | ||
authorize_resource | ||
activate_menu :account, :personal, :tokens | ||
before_action :authorize_access_tokens | ||
before_action :disable_client_cache | ||
|
||
private | ||
def new | ||
@presenter = AccessTokensNewPresenter.new(current_account) | ||
end | ||
|
||
def authorize_access_tokens | ||
authorize! :manage, :access_tokens, current_user | ||
end | ||
def create | ||
@presenter = AccessTokensNewPresenter.new(current_account) | ||
create! do |success, _failure| | ||
success.html do | ||
flash[:token] = @access_token.id | ||
flash[:notice] = 'Access Token was successfully created.' | ||
redirect_to(collection_url) | ||
end | ||
end | ||
end | ||
|
||
def index | ||
index! | ||
@last_access_key = flash[:token] | ||
end | ||
|
||
def update | ||
update! do |success, _failure| | ||
success.html do | ||
flash[:notice] = 'Access Token was successfully updated.' | ||
redirect_to(collection_url) | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
def begin_of_association_chain | ||
current_user | ||
def authorize_access_tokens | ||
authorize! :manage, :access_tokens, current_user | ||
end | ||
|
||
def begin_of_association_chain | ||
current_user | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { ExpirationDatePickerWrapper } from 'AccessTokens/components/ExpirationDatePicker' | ||
import { safeFromJsonString } from 'utilities/json-utils' | ||
|
||
import type { Props } from 'AccessTokens/components/ExpirationDatePicker' | ||
|
||
document.addEventListener('DOMContentLoaded', () => { | ||
const containerId = 'expiration-date-picker-container' | ||
const container = document.getElementById(containerId) | ||
|
||
if (!container) { | ||
throw new Error(`Missing container with id "${containerId}"`) | ||
} | ||
|
||
const props = safeFromJsonString<Props>(container.dataset.props) | ||
|
||
if (!props) { | ||
throw new Error('Missing props') | ||
} | ||
|
||
ExpirationDatePickerWrapper(props, containerId) | ||
}) |
15 changes: 15 additions & 0 deletions
15
app/javascript/src/AccessTokens/components/ExpirationDatePicker.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
@import '~@patternfly/patternfly/patternfly-addons.css'; | ||
|
||
.pf-c-calendar-month, | ||
.pf-c-form-control-expiration { | ||
width: 50%; | ||
} | ||
|
||
.pf-c-form-control-expiration { | ||
margin-right: 1em; | ||
} | ||
|
||
button.pf-c-form__group-label-help { | ||
min-width: auto; | ||
width: auto; | ||
} |
177 changes: 177 additions & 0 deletions
177
app/javascript/src/AccessTokens/components/ExpirationDatePicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { useState } from 'react' | ||
import { | ||
Alert, | ||
CalendarMonth, | ||
FormGroup, | ||
FormSelect, | ||
FormSelectOption, | ||
Popover | ||
} from '@patternfly/react-core' | ||
import ExclamationTriangleIcon from '@patternfly/react-icons/dist/js/icons/exclamation-triangle-icon' | ||
|
||
import { createReactWrapper } from 'utilities/createReactWrapper' | ||
|
||
import type { FunctionComponent } from 'react' | ||
|
||
import './ExpirationDatePicker.scss' | ||
|
||
interface ExpirationItem { | ||
id: string; | ||
label: string; | ||
period: number; // In days | ||
} | ||
|
||
const collection: ExpirationItem[] = [ | ||
{ id: '7', label: '7 days', period: 7 }, | ||
jlledom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ id: '30', label: '30 days', period: 30 }, | ||
{ id: '60', label: '60 days', period: 60 }, | ||
{ id: '90', label: '90 days', period: 90 }, | ||
{ id: 'custom', label: 'Custom...', period: 0 }, | ||
{ id: 'no-exp', label: 'No expiration', period: 0 } | ||
] | ||
|
||
const today: Date = new Date() | ||
const tomorrow: Date = new Date(today) | ||
tomorrow.setDate(today.getDate() + 1) | ||
const dayMs = 60 * 60 * 24 * 1000 | ||
|
||
const computeDropdownDate = (dropdownSelectedItem: ExpirationItem) => { | ||
if (dropdownSelectedItem.period === 0) return null | ||
|
||
return new Date(today.getTime() + dropdownSelectedItem.period * dayMs) | ||
} | ||
|
||
const computeSelectedDate = (dropdownDate: Date | null, dropdownSelectedItem: ExpirationItem, calendarPickedDate: Date) => { | ||
if (dropdownDate) return dropdownDate | ||
|
||
return dropdownSelectedItem.id === 'custom' ? calendarPickedDate : null | ||
} | ||
|
||
const computeFormattedDateValue = (selectedDate: Date | null) => { | ||
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) | ||
} | ||
|
||
const computeFieldHint = (formattedDateValue: string) => { | ||
if (!formattedDateValue) return | ||
|
||
return `The token will expire on ${formattedDateValue}` | ||
} | ||
|
||
const computeTzMismatch = (tzOffset: number) => { | ||
// Timezone offset in the same format as ActiveSupport | ||
const jsTzOffset = new Date().getTimezoneOffset() * -60 | ||
Comment on lines
+67
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) 🤪 |
||
|
||
return jsTzOffset !== tzOffset | ||
} | ||
|
||
const computeTzMismatchIcon = (tzMismatch: boolean) => { | ||
if (!tzMismatch) return | ||
|
||
return ( | ||
<Popover | ||
bodyContent={( | ||
<p> | ||
Your local time zone differs from the account settings. | ||
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" | ||
> | ||
<ExclamationTriangleIcon noVerticalAlign /> | ||
</button> | ||
</Popover> | ||
) | ||
} | ||
|
||
interface Props { | ||
id: string; | ||
label: string | null; | ||
tzOffset: number; | ||
} | ||
|
||
const ExpirationDatePicker: FunctionComponent<Props> = ({ id, label, tzOffset }) => { | ||
const [dropdownSelectedItem, setDropdownSelectedItem] = useState(collection[0]) | ||
const [calendarPickedDate, setCalendarPickedDate] = useState(tomorrow) | ||
|
||
const dropdownDate = computeDropdownDate(dropdownSelectedItem) | ||
const selectedDate = computeSelectedDate(dropdownDate, dropdownSelectedItem, calendarPickedDate) | ||
const formattedDateValue = computeFormattedDateValue(selectedDate) | ||
const fieldHint = computeFieldHint(formattedDateValue) | ||
const tzMismatch = computeTzMismatch(tzOffset) | ||
const tzMismatchIcon = computeTzMismatchIcon(tzMismatch) | ||
const inputDateValue = selectedDate ? selectedDate.toISOString() : '' | ||
const fieldName = `human_${id}` | ||
const fieldLabel = label ?? 'Expires in' | ||
|
||
const handleOnChange = (value: string) => { | ||
const selected = collection.find(i => i.id === value) ?? null | ||
|
||
if (selected === null) return | ||
|
||
setDropdownSelectedItem(selected) | ||
setCalendarPickedDate(tomorrow) | ||
} | ||
|
||
const dateValidator = (date: Date): boolean => { | ||
return date >= today | ||
} | ||
|
||
return ( | ||
<> | ||
<FormGroup | ||
isRequired | ||
fieldId={fieldName} | ||
helperText={fieldHint} | ||
label={fieldLabel} | ||
labelIcon={tzMismatchIcon} | ||
> | ||
<FormSelect | ||
className="pf-c-form-control-expiration" | ||
id={fieldName} | ||
value={dropdownSelectedItem.id} | ||
onChange={handleOnChange} | ||
> | ||
{collection.map((item: ExpirationItem) => { | ||
return ( | ||
<FormSelectOption | ||
key={item.id} | ||
label={item.label} | ||
value={item.id} | ||
/> | ||
) | ||
})} | ||
</FormSelect> | ||
</FormGroup> | ||
<input id={id} name={id} type="hidden" value={inputDateValue} /> | ||
{dropdownSelectedItem.id === 'custom' && ( | ||
<CalendarMonth className="pf-u-mt-md" date={calendarPickedDate} validators={[dateValidator]} onChange={setCalendarPickedDate} /> | ||
)} | ||
{!selectedDate && ( | ||
<Alert className="pf-u-mt-md" title="Expiration is recommended" variant="warning"> | ||
It is strongly recommended that you set an expiration date for your token to help keep your information | ||
secure | ||
</Alert> | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
const ExpirationDatePickerWrapper = (props: Props, containerId: string): void => { createReactWrapper(<ExpirationDatePicker {...props} />, containerId) } | ||
|
||
export type { ExpirationItem, Props } | ||
export { ExpirationDatePicker, ExpirationDatePickerWrapper } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
app/presenters/provider/admin/user/access_tokens_new_presenter.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# frozen_string_literal: true | ||
|
||
class Provider::Admin::User::AccessTokensNewPresenter | ||
|
||
def initialize(provider) | ||
@timezone = ActiveSupport::TimeZone.new(provider.timezone) | ||
end | ||
|
||
def provider_timezone_offset | ||
@timezone.utc_offset | ||
end | ||
|
||
def date_picker_props | ||
{ | ||
id: 'access_token[expires_at]', | ||
label: I18n.t('access_token_options.expires_in'), | ||
tzOffset: provider_timezone_offset | ||
} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 looks like something we would import at an upper level, like a base css file. I am not sure what it is for (it's used only in dev portal). I'm worried that it's going to be duplicated someday, and unfortunately there's no way to dedupe @imports from individual scss 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.
This is to load the
pf-u-mt-md
class. I could include it from somewhere else, do you have a suggestion?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 best way to do this would be to create a new scss pack that imports patternfly-addons alone, then import the pack alongside expiration_date_picker.ts. HOWEVER, it's not something we have to do right now. Let's keep it like this and improve only if it's ever necessary.