diff --git a/app/inputs/patternfly_check_boxes_input.rb b/app/inputs/patternfly_check_boxes_input.rb new file mode 100644 index 0000000000..448c081729 --- /dev/null +++ b/app/inputs/patternfly_check_boxes_input.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class PatternflyCheckBoxesInput < Formtastic::Inputs::CheckBoxesInput + delegate :tag, to: :template + + def to_html + tag.div(class: 'pf-c-form__group') do + label + control + end + end + + def label + tag.div(class: 'pf-c-form__group-label') do + tag.label(class: 'pf-c-form__label', for: input_html_options[:id]) do + tag.span(label_text, class: 'pf-c-form__label-text') + end + end + end + + def control + tag.div(class: 'pf-c-form__group-control') do + collection.map { |item| choice_html(item) }.reduce(&:concat) << + helper_text_invalid + end + end + + def choice_html(choice) + tag.div(class: 'pf-c-check') do + checkbox_input(choice) + choice_label(choice) + end + end + + def checkbox_input(choice) + value = choice_value(choice) + template.check_box_tag( + input_name, + value, + checked?(value), + extra_html_options(choice).merge(id: choice_input_dom_id(choice), + class: 'pf-c-check__input', + required: false) + ) + end + + def choice_label(choice) + label_text = choice[0] + tag.label(label_text, class: 'pf-c-check__label', + for: choice_input_dom_id(choice)) + end + + def helper_text_invalid + return if errors.empty? + + template.render partial: 'shared/pf_error_helper_text', locals: { error: errors.first } + end +end diff --git a/app/lib/fields/patternfly_form_builder.rb b/app/lib/fields/patternfly_form_builder.rb index a09523dd4a..38f27f0647 100644 --- a/app/lib/fields/patternfly_form_builder.rb +++ b/app/lib/fields/patternfly_form_builder.rb @@ -17,6 +17,11 @@ def commit_button(title, opts = {}) tag.button(title, type: :submit, class: 'pf-c-button pf-m-primary', **opts) end + def delete_button(title, href, opts = {}) + opts.reverse_merge!(method: :delete, class: 'pf-c-button pf-m-danger') + template.link_to(title, href, **opts) + end + def collection_select(*opts) super(*opts.first(4), {}, { class: 'pf-c-form-control' }) end diff --git a/app/models/access_token.rb b/app/models/access_token.rb index 54883d8e23..10db8f52bf 100644 --- a/app/models/access_token.rb +++ b/app/models/access_token.rb @@ -55,7 +55,7 @@ def values scopes.map(&:value) end - def to_a + def to_collection_for_check_boxes map { |scope| [scope.key, scope.value] } end diff --git a/app/views/provider/admin/user/access_tokens/_form.html.slim b/app/views/provider/admin/user/access_tokens/_form.html.slim index 8ebea1ebc6..eccdf3a684 100644 --- a/app/views/provider/admin/user/access_tokens/_form.html.slim +++ b/app/views/provider/admin/user/access_tokens/_form.html.slim @@ -1,17 +1,9 @@ -= semantic_form_for access_token, url: [:provider, :admin, :user, access_token] do |f| += form.input :name, as: :patternfly_input, + input_html: { autofocus: true } - = f.inputs do - = f.input :name, input_html: { autofocus: true } - = f.input :scopes, - as: :check_boxes, - collection: access_token.available_scopes.to_a += form.input :scopes, as: :patternfly_check_boxes, + collection: @access_token.available_scopes.to_collection_for_check_boxes - = f.input :permission, as: :select, collection: access_token.available_permissions, include_blank: false - - - = f.actions - = f.commit_button - - unless access_token.new_record? - = link_to 'Delete', provider_admin_user_access_token_path(@access_token), - data: {confirm: 'Are you sure?'}, method: :delete, - title: 'Delete Access Token', class: 'action delete' += form.input :permission, as: :patternfly_select, + collection: @access_token.available_permissions, + include_blank: false diff --git a/app/views/provider/admin/user/access_tokens/edit.html.slim b/app/views/provider/admin/user/access_tokens/edit.html.slim index dab263d26c..293160ed0f 100644 --- a/app/views/provider/admin/user/access_tokens/edit.html.slim +++ b/app/views/provider/admin/user/access_tokens/edit.html.slim @@ -1,3 +1,18 @@ -- content_for :page_header_title, 'Edit Access Token' +- content_for :page_header_title, t('.page_header_title') + +- content_for :javascripts do + = javascript_packs_with_chunks_tag 'pf_form' + +div class="pf-c-card" + div class="pf-c-card__body" + = semantic_form_for @access_token, builder: Fields::PatternflyFormBuilder, + url: [:provider, :admin, :user, @access_token], + html: { class: 'pf-c-form pf-m-limit-width' } do |f| + = render 'form', form: f + + = f.actions do + = f.commit_button t('.submit_button_label') + = f.delete_button 'Delete', provider_admin_user_access_token_path(@access_token), + data: { confirm: 'Are you sure?' }, + title: 'Delete Access Token' -= render 'form', access_token: @access_token diff --git a/app/views/provider/admin/user/access_tokens/new.html.slim b/app/views/provider/admin/user/access_tokens/new.html.slim index 441da25efe..5b417f9865 100644 --- a/app/views/provider/admin/user/access_tokens/new.html.slim +++ b/app/views/provider/admin/user/access_tokens/new.html.slim @@ -1,3 +1,22 @@ -- content_for :page_header_title, 'New Access Token' +- content_for :page_header_title, t('.page_header_title') -= render 'form', access_token: @access_token +- content_for :javascripts do + = javascript_packs_with_chunks_tag 'pf_form' + +div class="pf-c-card" + div class="pf-c-card__body" + = semantic_form_for @access_token, builder: Fields::PatternflyFormBuilder, + url: [:provider, :admin, :user, @access_token], + html: { class: 'pf-c-form pf-m-limit-width' } do |f| + = f.input :name, as: :patternfly_input, + input_html: { autofocus: true } + + = f.input :scopes, as: :patternfly_check_boxes, + collection: @access_token.available_scopes.to_collection_for_check_boxes + + = f.input :permission, as: :patternfly_select, + collection: @access_token.available_permissions, + include_blank: false + + = f.actions do + = f.commit_button t('.submit_button_label') diff --git a/config/locales/en.yml b/config/locales/en.yml index 12ee5e00fa..276d284e2f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -504,6 +504,13 @@ en: these settings will no longer have any effect.

user: + access_tokens: + edit: + page_header_title: Edit Access Token + submit_button_label: Update Access Token + new: + page_header_title: New Access Token + submit_button_label: Create Access Token notification_preferences: show: no_notification_preferences_html: | @@ -1087,7 +1094,11 @@ en: invalid_transition: 'cannot transition via "%{event}"' not_path_format: 'must be a path separated by "/". E.g. "" or "my/path"' models: - authentication/by_access_token/access_token: + access_token: + attributes: + scopes: + too_short: select at least one scope + authentication/by_access_token: attributes: scopes: too_short: "Select at least %{count} scope" diff --git a/features/access_tokens/show.feature b/features/access_tokens/show.feature deleted file mode 100644 index 3fca4e36e6..0000000000 --- a/features/access_tokens/show.feature +++ /dev/null @@ -1,13 +0,0 @@ -@javascript -Feature: Access tokens - As a admin - I'd like to see the access tokens page correctly - - Scenario: I should be able to see all scopes - Given a provider is logged in - And I go to the provider personal page - And I follow "Tokens" - And I follow "Add Access Token" - Then I should see "Billing API" - Then I should see "Account Management API" - Then I should see "Analytics API" diff --git a/features/provider/admin/user/access_tokens.feature b/features/provider/admin/user/access_tokens.feature new file mode 100644 index 0000000000..191277e5f2 --- /dev/null +++ b/features/provider/admin/user/access_tokens.feature @@ -0,0 +1,100 @@ +@javascript +Feature: Provider Admin Access tokens + + As an admin I want to be able to read, create and edit access tokens + + Background: + Given a provider is logged in + And they go to the provider personal page + + Rule: Index page + Background: + Given the provider has the following access tokens: + | Name | Scopes | Permission | + | Potato | Analytics API | Read Only | + | Banana | Billing API | Read & Write | + And they go to the personal tokens page + + Scenario: Navigation to index page + Given they go to the provider dashboard + When they select "Account Settings" from the context selector + And press "Personal" within the main menu + And follow "Tokens" within the main menu's section Personal + Then the current page is the personal tokens page + + Scenario: Tokens are listed in a table + Then the table should contain the following: + | Name | Scopes | Permission | + | Potato | Analytics API | Read Only | + | Banana | Billing API | Read & Write | + + Rule: New page + Background: + Given they go to the new access token page + + Scenario: Navigation to the new page + Given they go to the personal tokens page + When they follow "Add Access Token" + Then the current page is the new access token page + + Scenario: New access token required fields + When the current page is the new access token page + Then there is a required field "Name" + And there is a required field "Scopes" + And there is a required field "Permission" + And the submit button is enabled + + Scenario: Create access tokens without required fields + When they press "Create Access Token" + Then field "Name" has inline error "can't be blank" + And field "Scopes" has inline error "select at least one scope" + And field "Permission" has no inline error + + Scenario: Create access token + When they press "Create Access Token" + And the form is submitted with: + | Name | LeToken | + | Analytics API | Yes | + | Permission | Read & Write | + Then the current page is the personal tokens page + And they should see the flash message "Access token was successfully created" + And should see the following details: + | Name | LeToken | + | Scopes | Analytics API | + | Permission | Read & Write | + And there should be a link to "I have copied the token" + + Rule: Edit page + Background: + Given the provider has the following access tokens: + | Name | Scopes | Permission | + | LeToken | Billing API, Analytics API | Read Only | + And they go to the access token's edit page + + Scenario: Navigation to edit page + Given they go to the personal tokens page + When they follow "Edit" in the 1st row within the access tokens table + Then the current page is the access token's edit page + + Scenario: Edit access token + When the form is submitted with: + | Name | New Token Name | + | Billing API | No | + | Permission | Read & Write | + Then they should see the flash message "Access Token was successfully updated." + Then the table should contain the following: + | Name | Scopes | Permission | + | New Token Name | Analytics API | Read & Write | + + Scenario: Edit access tokens without required fields + When the form is submitted with: + | Name | | + Then field "Name" has inline error "can't be blank" + + Scenario: Delete access token + Given the current page is access token "LeToken" edit page + When they follow "Delete" + And confirm the dialog + Then the current page is the personal tokens page + And they should see the flash message "Access token was successfully deleted" + But should not see "LeToken" diff --git a/features/step_definitions/provider/admin/user/access_tokens_steps.rb b/features/step_definitions/provider/admin/user/access_tokens_steps.rb new file mode 100644 index 0000000000..433c567502 --- /dev/null +++ b/features/step_definitions/provider/admin/user/access_tokens_steps.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Example: +# +# Given a provider +# And the provider has the following access tokens: +# | Name | Scopes | Permission | +# | LeToken | Billing API, Analytics API | Read Only | +# +Given "{provider} has the following access tokens:" do |provider, table| + transform_access_tokens_table(table) + + table.hashes.each do |options| + @access_token = FactoryBot.create(:access_token, owner: @provider.admin_users.first!, **options) + end +end diff --git a/features/support/capybara_extensions.rb b/features/support/capybara_extensions.rb index c36c849a90..ed76a52bcc 100644 --- a/features/support/capybara_extensions.rb +++ b/features/support/capybara_extensions.rb @@ -57,11 +57,16 @@ def has_select?(locator = nil, **options, &optional_filter_block) # Capybara::Node::Matchers#has_field? def has_field?(locator = nil, **options, &optional_filter_block) - if (form_group = find_all('.pf-c-form__group', text: locator, wait: 0).first) && form_group.has_css?('.CodeMirror', wait: 0) + form_group = find_all('.pf-c-form__group', text: locator, wait: 0).first + + if form_group&.has_css?('.CodeMirror', wait: 0) # If the field is enhanced by CodeMirror, the textarea will be hidden. options[:visible] = :hidden end + # Matches patternfly_check_boxes_input + return true if form_group&.has_css?('.pf-c-form__group-control .pf-c-check', wait: 0) + super end end diff --git a/features/support/data_table_transforms.rb b/features/support/data_table_transforms.rb index 9afbf534d4..e2a75fe8df 100644 --- a/features/support/data_table_transforms.rb +++ b/features/support/data_table_transforms.rb @@ -6,6 +6,17 @@ # Original transformers: https://github.com/3scale/porta/blob/a5d6622d5a56bbda401f7d95e09b0ab19d05adba/features/support/transforms.rb#L185-L202 module DataTableTransforms + def transform_access_tokens_table(table) + parameterize_headers(table) + + codes = I18n.t('.access_token_options') + + table.map_column!(:scopes) do |scopes| + scopes.split(',').map(&:strip).map { |scope| codes.key(scope).to_s } + end + table.map_column!(:permission) { |permission| codes.key(permission) } + end + def transform_invoices_table(table) parameterize_headers(table, 'Buyer' => 'buyer_account', 'Month' => 'period') diff --git a/features/support/paths.rb b/features/support/paths.rb index bd4d7513b0..69de82d2f7 100644 --- a/features/support/paths.rb +++ b/features/support/paths.rb @@ -220,6 +220,16 @@ def path_to(page_name, *args) # rubocop:disable Metrics/AbcSize, Metrics/Cycloma when 'the new email configurations page' new_provider_admin_account_email_configurations_path + when 'the personal tokens page' + provider_admin_user_access_tokens_path + + when 'the new access token page' + new_provider_admin_user_access_token_path + + when /^(access token "(.*)"|the access token's) edit page$/ + access_token = AccessToken.find_by(name: $2) || @access_token + edit_provider_admin_user_access_token_path(access_token) + # # SSO Integrations (Admin portal) # diff --git a/features/support/selectors.rb b/features/support/selectors.rb index 5d6759ce18..8dfd579246 100644 --- a/features/support/selectors.rb +++ b/features/support/selectors.rb @@ -53,6 +53,9 @@ def selector_for(scope) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticCom when /the bulk operations/ # Legacy bulk operations card, not the toolbar dropdown '.pf-c-card#bulk-operations' + when /the access tokens table/ + '.pf-c-table[aria-label="Access tokens table"]' + # # Product #