diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index 2dc2c27f7ea..13472b48e84 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -76,14 +76,13 @@ export default { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index dd140bd8f5c..8a9a632003a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -1093,17 +1093,3 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { width: $gl-dropdown-width-wide; } } - -.gl-dropdown-item-deprecated-adapter { - .dropdown-item { - align-items: flex-start; - - .gl-new-dropdown-item-text-primary { - @include gl-font-weight-bold; - } - - .gl-new-dropdown-item-text-secondary { - color: inherit; - } - } -} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 730e10114c3..ffcc20b624b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -516,28 +516,8 @@ left: auto; max-height: $dropdown-max-height-lg; - li.current-user { - padding: $dropdown-item-padding-y $dropdown-item-padding-x; - - .user-name { - display: block; - } - - .user-status { - margin-right: 0; - max-width: 240px; - font-size: $gl-font-size-small; - - gl-emoji { - font-size: $gl-font-size-small; - } - - .user-status-emoji { - gl-emoji { - font-size: $gl-font-size; - } - } - } + .user-status { + max-width: 240px; } svg { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 52bd16d1a79..ef737e11799 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -74,14 +74,10 @@ justify-content: flex-end; } - .encoding-selector, .soft-wrap-toggle { display: inline-block; vertical-align: top; font-family: $regular-font; - } - - .soft-wrap-toggle { margin: 0 $btn-side-margin; .soft-wrap { @@ -128,15 +124,6 @@ margin: 3px 0; } - .encoding-selector { - display: block; - margin: 3px 0; - - button { - width: 100%; - } - } - @media(max-width: map-get($grid-breakpoints, md)-1) { clear: both; } diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 5f8b6b48d6c..c637813ae58 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -159,8 +159,8 @@ class JiraService < IssueTrackerService # support any events. end - def find_issue(issue_key) - jira_request { client.Issue.find(issue_key) } + def find_issue(issue_key, options = {}) + jira_request { client.Issue.find(issue_key, options) } end def close_issue(entity, external_issue, current_user) diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index ccf62ef043a..5ac0db4137f 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -2,18 +2,12 @@ %ul %li.current-user - .user-name.gl-font-weight-bold - = current_user.name - - if current_user&.status && user_status_set_to_busy?(current_user.status) - %span.gl-font-weight-normal.gl-text-gray-500= s_("UserProfile|(Busy)") - = current_user.to_reference - - if current_user.status - .user-status.d-flex.align-items-center.gl-mt-2.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } } - - if show_status_emoji?(current_user.status) - .user-status-emoji.d-flex.align-items-center - = emoji_icon current_user.status.emoji - %span.user-status-message.str-truncated - = current_user.status.message_html.html_safe + - if current_user_menu?(:profile) + = link_to current_user, class: 'gl-line-height-20!', data: { user: current_user.username, testid: 'user-profile-link' } do + = render 'layouts/header/current_user_dropdown_item' + - else + .gl-py-3.gl-px-4 + = render 'layouts/header/current_user_dropdown_item' %li.divider - if can?(current_user, :update_user_status, current_user) %li @@ -22,9 +16,6 @@ = s_('SetStatusModal|Edit status') - else = s_('SetStatusModal|Set status') - - if current_user_menu?(:profile) - %li - = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } - if current_user_menu?(:start_trial) %li %a.trial-link{ href: trials_link_url } @@ -32,7 +23,9 @@ = emoji_icon('rocket') - if current_user_menu?(:settings) %li - = link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' } + = link_to s_("CurrentUser|Edit profile"), profile_path, data: { qa_selector: 'edit_profile_link' } + %li + = link_to s_("CurrentUser|Preferences"), profile_preferences_path = render_if_exists 'layouts/header/buy_pipeline_minutes', project: @project, namespace: @group = render_if_exists 'layouts/header/upgrade' diff --git a/app/views/layouts/header/_current_user_dropdown_item.html.haml b/app/views/layouts/header/_current_user_dropdown_item.html.haml new file mode 100644 index 00000000000..06c597b4932 --- /dev/null +++ b/app/views/layouts/header/_current_user_dropdown_item.html.haml @@ -0,0 +1,12 @@ +.gl-font-weight-bold + = current_user.name + - if current_user&.status && user_status_set_to_busy?(current_user.status) + %span.gl-font-weight-normal.gl-text-gray-500= s_("UserProfile|(Busy)") += current_user.to_reference +- if current_user.status + .user-status.d-flex.align-items-center.gl-mt-2.gl-mr-0.gl-font-sm.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } } + - if show_status_emoji?(current_user.status) + .user-status-emoji.d-flex.align-items-center + = emoji_icon current_user.status.emoji + %span.user-status-message.str-truncated + = current_user.status.message_html.html_safe diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f9d11ec33d2..7d3a0c4a026 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -36,8 +36,6 @@ %span.soft-wrap = custom_icon('icon_soft_wrap') Soft wrap - .encoding-selector - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1' .file-editor.code .js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }< diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 99672ded6db..774fbc79430 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title _("Graph") - page_title _("Graph"), @ref = render "head" -%div{ class: container_class } +.gl-mt-5 .project-network.gl-border-1.gl-border-solid.gl-border-gray-300 .controls.gl-bg-gray-50.gl-p-2.gl-font-base.gl-text-gray-400.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-300 = form_tag project_network_path(@project, @id), method: :get, class: 'form-inline network-form' do |f| diff --git a/changelogs/unreleased/271530-improve-user-menu.yml b/changelogs/unreleased/271530-improve-user-menu.yml new file mode 100644 index 00000000000..78ba0964841 --- /dev/null +++ b/changelogs/unreleased/271530-improve-user-menu.yml @@ -0,0 +1,5 @@ +--- +title: Improve user dropdown items +merge_request: 53175 +author: +type: changed diff --git a/changelogs/unreleased/296645-removed-unused-dropdown.yml b/changelogs/unreleased/296645-removed-unused-dropdown.yml new file mode 100644 index 00000000000..754f1d2118f --- /dev/null +++ b/changelogs/unreleased/296645-removed-unused-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Removed unused Text dropdown +merge_request: 53464 +author: +type: removed diff --git a/changelogs/unreleased/align-graph-page.yml b/changelogs/unreleased/align-graph-page.yml new file mode 100644 index 00000000000..31f00b526e3 --- /dev/null +++ b/changelogs/unreleased/align-graph-page.yml @@ -0,0 +1,5 @@ +--- +title: Add margin and remove padding in project graph page +merge_request: 53557 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/ps-remove-gl-dropdown-item-deprecated-adapter.yml b/changelogs/unreleased/ps-remove-gl-dropdown-item-deprecated-adapter.yml new file mode 100644 index 00000000000..dda80285413 --- /dev/null +++ b/changelogs/unreleased/ps-remove-gl-dropdown-item-deprecated-adapter.yml @@ -0,0 +1,5 @@ +--- +title: Change secondary text color on Gitpod editor dropdown button +merge_request: 53437 +author: +type: other diff --git a/doc/.vale/gitlab/Acronyms.yml b/doc/.vale/gitlab/Acronyms.yml index c6b9bb45040..69e30966abb 100644 --- a/doc/.vale/gitlab/Acronyms.yml +++ b/doc/.vale/gitlab/Acronyms.yml @@ -143,6 +143,7 @@ exceptions: - SVG - SVN - TCP + - TIFF - TIP - TLD - TLS diff --git a/doc/README.md b/doc/README.md index 7a32c317085..0e1bc7b711e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -80,7 +80,7 @@ Learn more about GitLab account management: |:-----------------------------------------------------------|:------------| | [User account](user/profile/index.md) | Manage your account. | | [Authentication](topics/authentication/index.md) | Account security with two-factor authentication, set up your SSH keys, and deploy keys for secure access to your projects. | -| [Profile settings](user/profile/index.md#profile-settings) | Manage your profile settings, two factor authentication, and more. | +| [User settings](user/profile/index.md#user-settings) | Manage your user settings, two factor authentication, and more. | | [User permissions](user/permissions.md) | Learn what each role in a project can do. | ### Git and GitLab diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md deleted file mode 100644 index 955e05491d2..00000000000 --- a/doc/administration/geo/replication/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../index.md' ---- - -This document was moved to [another location](../index.md). - - - diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index ba2cb05b449..50f2022f5a8 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -339,7 +339,7 @@ an expiry for the artifacts, they are marked for deletion right after that date Otherwise, they expire per the [default artifacts expiration setting](../user/admin_area/settings/continuous_integration.md). Artifacts are cleaned up by the `expire_build_artifacts_worker` cron job which Sidekiq -runs every hour at 50 minutes (`50 * * * *`). +runs every 7 minutes (`*/7 * * * *`). To change the default schedule on which the artifacts are expired, follow the steps below. @@ -350,7 +350,7 @@ steps below. your schedule in cron syntax: ```ruby - gitlab_rails['expire_build_artifacts_worker_cron'] = "50 * * * *" + gitlab_rails['expire_build_artifacts_worker_cron'] = "*/7 * * * *" ``` 1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. @@ -362,7 +362,7 @@ steps below. ```yaml expire_build_artifacts_worker: - cron: "50 * * * *" + cron: "*/7 * * * *" ``` 1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect. diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index 7b57cdbf17f..a6073e34d58 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -235,7 +235,7 @@ separate Rails process to debug the issue: 1. Log in to your GitLab account. 1. Copy the URL that is causing problems (e.g. `https://gitlab.com/ABC`). -1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens). +1. Create a Personal Access Token for your user (User Settings -> Access Tokens). 1. Bring up the [GitLab Rails console.](../operations/rails_console.md#starting-a-rails-console-session) 1. At the Rails console, run: diff --git a/doc/api/avatar.md b/doc/api/avatar.md index ccaa50eedd8..14fdc5e8afd 100644 --- a/doc/api/avatar.md +++ b/doc/api/avatar.md @@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Get a single avatar URL -Get a single [avatar](../user/profile/index.md#profile-settings) URL for a user with the given email address. +Get a single [avatar](../user/profile/index.md#user-settings) URL for a user with the given email address. If: diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index 32169db9048..85098689392 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -13,6 +13,7 @@ This document outlines the style guide for the GitLab [GraphQL API](../api/graph We use the [GraphQL Ruby gem](https://graphql-ruby.org/) written by [Robert Mosolgo](https://github.com/rmosolgo/). +In addition, we have a subscription to [GraphQL Pro](https://www.graphql.pro). For details see [GraphQL Pro subscription](graphql_guide/graphql_pro.md). All GraphQL queries are directed to a single endpoint ([`app/controllers/graphql_controller.rb#execute`](https://gitlab.com/gitlab-org/gitlab/blob/master/app%2Fcontrollers%2Fgraphql_controller.rb)), diff --git a/doc/development/graphql_guide/graphql_pro.md b/doc/development/graphql_guide/graphql_pro.md new file mode 100644 index 00000000000..6f62d86af40 --- /dev/null +++ b/doc/development/graphql_guide/graphql_pro.md @@ -0,0 +1,21 @@ +--- +stage: Plan +group: Project Management +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# GraphQL Pro + +GraphQL has become a key technology in GitLab and is implemented using the +[GraphQL Ruby gem](https://graphql-ruby.org). As such, we've purchased a subscription to +[GraphQL Pro](https://graphql.pro). + +The main purpose is for support. Per the website: + +> As a GraphQL::Pro customer, you get direct access to the GraphQL Ruby gem +> creator and maintainer. Get prioritized support for issues and requests. + +Note that we **cannot** use the Pro version directly in our product, since we are +an Open Core product - we can not require customers to purchase the Pro version, nor can we ship it. + +Details on the billing account and gem licensing can be found in the Engineering 1Password vault. diff --git a/doc/development/graphql_guide/index.md b/doc/development/graphql_guide/index.md index 658bba96f33..fd6d8992f94 100644 --- a/doc/development/graphql_guide/index.md +++ b/doc/development/graphql_guide/index.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- @@ -17,3 +17,6 @@ feedback, and suggestions. - [GraphQL API documentation style guide](../documentation/graphql_styleguide.md): documentation style guide for GraphQL. - [GraphQL API](../../api/graphql/index.md): user documentation for the GitLab GraphQL API. +- [GraphQL BatchLoader](batchloader.md): development documentation on the batchloader. +- [GraphQL pagination](pagination.md): development documentation on pagination. +- [GraphQL Pro](graphql_pro.md): information on our GraphQL Pro subscription. diff --git a/doc/development/graphql_guide/pagination.md b/doc/development/graphql_guide/pagination.md index 130ed5721f3..55ff7942418 100644 --- a/doc/development/graphql_guide/pagination.md +++ b/doc/development/graphql_guide/pagination.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/github.md b/doc/integration/github.md index 8c2a48225dd..0239ba0e818 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -179,7 +179,9 @@ If you're getting the message `Signing in using your GitHub account without a pr GitLab account is not allowed. Create a GitLab account first, and then connect it to your GitHub account` when signing in, in GitLab: -1. Go to your **Profile > Account**. -1. Under the "Social sign-in" section, click **Connect** near the GitHub icon. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. +1. In the **Social sign-in** section, select **Connect to GitHub**. After that, you should be able to sign in via GitHub successfully. diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 94a26ee792b..97c5332c438 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -12,11 +12,9 @@ To enable the GitLab.com OmniAuth provider you must register your application wi GitLab.com generates an application ID and secret key for you to use. 1. Sign in to GitLab.com. - -1. On the upper right corner, click on your avatar and go to your **Settings**. - -1. Select **Applications** in the left menu. - +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Applications**. 1. Provide the required details for **Add new application**. - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Redirect URI: @@ -29,10 +27,8 @@ GitLab.com generates an application ID and secret key for you to use. The first link is required for the importer and second for the authorization. 1. Select **Save application**. - 1. You should now see an **Application ID** and **Secret**. Keep this page open as you continue configuration. - 1. On your GitLab server, open the configuration file. For Omnibus package: @@ -50,10 +46,9 @@ GitLab.com generates an application ID and secret key for you to use. ``` 1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. - 1. Add the provider configuration: - For Omnibus package: + For Omnibus installations: ```ruby gitlab_rails['omniauth_providers'] = [ @@ -76,16 +71,13 @@ GitLab.com generates an application ID and secret key for you to use. ``` 1. Change `'YOUR_APP_ID'` to the Application ID from the GitLab.com application page. - 1. Change `'YOUR_APP_SECRET'` to the secret from the GitLab.com application page. - 1. Save the configuration file. - 1. Based on how GitLab was installed, implement these changes by using the appropriate method: - - Omnibus GitLab: [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure). - - Source: [Restart GitLab](../administration/restart_gitlab.md#installations-from-source). + - Omnibus GitLab: [reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure). + - Source: [restart GitLab](../administration/restart_gitlab.md#installations-from-source). On the sign-in page, there should now be a GitLab.com icon following the regular sign-in form. Select the icon to begin the authentication process. diff --git a/doc/integration/gitpod.md b/doc/integration/gitpod.md index cc6e08259fa..7dc710615fb 100644 --- a/doc/integration/gitpod.md +++ b/doc/integration/gitpod.md @@ -33,7 +33,9 @@ To learn more about Gitpod, see their [features](https://www.gitpod.io/features/ With the Gitpod integration enabled for your GitLab instance, to enable it for yourself: -1. Select your avatar in the top-right corner, then select **Settings > Preferences**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. 1. Under **Integrations**, locate the **Gitpod** section. 1. Check the **Enable Gitpod integration** checkbox and select the **Save changes** button. diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index 81d352cfee6..b51ce5de8d7 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -70,9 +70,10 @@ Grant a GitLab user access to the select GitLab projects. Create a personal access token to authorize Jenkins' access to GitLab. -1. Log in to GitLab as the user to be used with Jenkins. -1. Click your avatar, then **Settings**. -1. Click **Access Tokens** in the sidebar. +1. Sign in to GitLab as the user to be used with Jenkins. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Access Tokens**. 1. Create a personal access token with the **API** scope checkbox checked. For more details, see [Personal access tokens](../user/profile/personal_access_tokens.md). 1. Record the personal access token's value, because it's required in [Configure the Jenkins server](#configure-the-jenkins-server) section. diff --git a/doc/integration/jira_development_panel.md b/doc/integration/jira_development_panel.md index 463f8a6efaa..7d581cbc1e3 100644 --- a/doc/integration/jira_development_panel.md +++ b/doc/integration/jira_development_panel.md @@ -77,10 +77,10 @@ To ensure that regular user account maintenance doesn't impact your integration, create and use a single-purpose `jira` user in GitLab. 1. In GitLab, create a new application to allow Jira to connect with your GitLab account. - 1. Sign in to the GitLab account that you want Jira to use to connect to GitLab. -1. In the top right corner, click your profile avatar. -1. Click **Settings > Applications** to display the form to create a new application. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Applications**. 1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`. 1. In the **Redirect URI** field, enter `https:///login/oauth/callback`, replacing `` with your GitLab instance domain. For example, if you are using GitLab.com, @@ -94,8 +94,7 @@ create and use a single-purpose `jira` user in GitLab. ![GitLab application setup](img/jira_dev_panel_gl_setup_1.png) -1. Check **API** in the Scopes section, and uncheck any other checkboxes. - +1. Check **API** in the **Scopes** section, and clear any other checkboxes. 1. Click **Save application**. GitLab displays the generated **Application ID** and **Secret** values. Copy these values, which you use in Jira. diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md index d8297ac87b3..5be076464d8 100644 --- a/doc/integration/kerberos.md +++ b/doc/integration/kerberos.md @@ -115,9 +115,10 @@ existing GitLab account. To do so: If you're not an administrator: -1. Select your avatar in the upper-right corner, and select **Settings**. -1. Select Account. In the **Social sign-in** section, select - **Connect Kerberos SPNEGO**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. +1. In the **Social sign-in** section, select **Connect Kerberos SPNEGO**. If you don't see a **Social sign-in** Kerberos option, follow the requirements in [Enable single sign-on](#enable-single-sign-on). diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index 8f1c625d142..377c8ec82d0 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -43,16 +43,17 @@ levels. The default callback URL is `http://your-gitlab.example.com/users/auth/g ## Add an application through the profile -To add a new application via your profile, select your avatar in the top right, and then select -**Settings > Applications**. You can then enter a **Name**, **Redirect URI** and -OAuth 2 scopes as defined in [Authorized Applications](#authorized-applications). +To add a new application via your profile: -- The **Redirect URI** is the URL where users are sent after they authorize with GitLab. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Applications**. +1. Enter a **Name**, **Redirect URI** and OAuth 2 scopes as defined in [Authorized Applications](#authorized-applications). + The **Redirect URI** is the URL where users are sent after they authorize with GitLab. +1. Select **Save application**. GitLab displays: -When you select **Save application**, GitLab displays: - -- Application ID: OAuth 2 Client ID. -- Secret: OAuth 2 Client Secret. + - Application ID: OAuth 2 Client ID. + - Secret: OAuth 2 Client Secret. ## OAuth applications in the Admin Area diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 54fa5b0a732..cf033018e17 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -138,9 +138,10 @@ provider such as Twitter can be enabled. Follow the steps below to enable an OmniAuth provider for an existing user. 1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider. -1. Go to profile settings (the silhouette icon in the top right corner). -1. Select the "Account" tab. -1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. +1. In the **Connected Accounts** section, select the desired OmniAuth provider, such as Twitter. 1. The user is redirected to the provider. After the user authorizes GitLab, they are redirected back to GitLab. diff --git a/doc/integration/sourcegraph.md b/doc/integration/sourcegraph.md index aff0bb2f841..c64ef729e94 100644 --- a/doc/integration/sourcegraph.md +++ b/doc/integration/sourcegraph.md @@ -87,9 +87,12 @@ You can skip this step if you already have your GitLab repositories searchable i If a GitLab administrator has enabled Sourcegraph, you can enable this feature in your user preferences. -1. In GitLab, click your avatar in the top-right corner, then click **Settings**. On the left-hand nav, click **Preferences**. -1. Under **Integrations**, find the **Sourcegraph** section. -1. Check **Enable Sourcegraph**. +In GitLab: + +1. In the top-right corner, select your avatar. +1. Select **Preferences**. +1. In the **Integrations** section, select the checkbox under **Sourcegraph**. +1. Select **Save changes**. ![Sourcegraph user preferences](img/sourcegraph_user_preferences_v12_5.png) diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md index d30308cea7a..1a1b6cd101f 100644 --- a/doc/integration/trello_power_up.md +++ b/doc/integration/trello_power_up.md @@ -38,8 +38,11 @@ If your instance's URL is `https://example.com`, your API URL is `https://exampl Your GitLab personal access token enables your GitLab account to be accessed from Trello. -> Find it in GitLab by clicking on your avatar (upright corner), from which you access -your user **Settings** > **Access Tokens**. +To find it in GitLab: + +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Access Tokens**. Learn more about generating a personal access token in the [Personal Access Token Documentation](../user/profile/personal_access_tokens.md). diff --git a/doc/integration/vault.md b/doc/integration/vault.md index d3102c2616a..362ae36389b 100644 --- a/doc/integration/vault.md +++ b/doc/integration/vault.md @@ -18,12 +18,15 @@ The following assumes you already have Vault installed and running. 1. **Get the OpenID Connect client ID and secret from GitLab:** - First you must create a GitLab application to obtain an application ID and secret for authenticating into Vault. To do this, sign in to GitLab and follow these steps: + First you must create a GitLab application to obtain an application ID and secret for authenticating into Vault. + To do this, sign in to GitLab and follow these steps: - 1. On GitLab, click your avatar on the top-right corner, and select your user **Settings > Applications**. - 1. Fill out the application **Name** and [**Redirect URI**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris), - making sure to select the **OpenID** scope. - 1. Save application. + 1. In the top-right corner, select your avatar. + 1. Select **Edit profile**. + 1. In the left sidebar, select **Applications**. + 1. Fill out the application **Name** and [**Redirect URI**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris). + 1. Select the **OpenID** scope. + 1. Select **Save application**. 1. Copy client ID and secret, or keep the page open for reference. ![GitLab OAuth provider](img/gitlab_oauth_vault_v12_6.png) diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md index 2fd73d70cda..ba14293a9fd 100644 --- a/doc/raketasks/features.md +++ b/doc/raketasks/features.md @@ -15,7 +15,13 @@ This command enables the namespaces feature introduced in GitLab 4.0. It moves e The **repository location changes as part of this task**, so you must **update all your Git URLs** to point to the new location. -The username can be changed at **Profile > Account**. +To change your username: + +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. +1. In the **Change username** section, type the new username. +1. Select **Update username**. For example: diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index f1a7abbfd33..7a9ed9d435d 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -110,9 +110,10 @@ Each scenario can be a third-level heading, e.g. `### Getting error message X`. If you have none to add when creating a doc, leave this section in place but commented out to help encourage others to add to it in the future. --> -## Two-factor Authentication (2FA) for Git over SSH operations +## Two-factor Authentication (2FA) for Git over SSH operations **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/270554) in GitLab 13.7. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/299088) from GitLab Free to GitLab Premium in 13.9. > - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default. > - It's disabled on GitLab.com. > - It's not recommended for production use. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 8d8fc683158..815d8c13c43 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -182,8 +182,9 @@ Now you can copy the SSH key you created to your GitLab account. If you're using an RSA key, substitute accordingly. 1. Navigate to `https://gitlab.com` or your local GitLab instance URL and sign in. -1. Select your avatar in the upper right corner, and click **Settings** -1. Click **SSH Keys**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **SSH Keys**. 1. Paste the public key that you copied into the **Key** text box. 1. Make sure your key includes a descriptive name in the **Title** text box, such as _Work Laptop_ or _Home Workstation_. diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md index a600ffe4ed3..b7a1243be72 100644 --- a/doc/subscriptions/gitlab_com/index.md +++ b/doc/subscriptions/gitlab_com/index.md @@ -93,7 +93,7 @@ The **Seat usage** page lists all users occupying seats. Details for each user i - Full name - Username -- Public email address (if they have provided one in their [profile settings](../../user/profile/index.md#profile-settings)) +- Public email address (if they have provided one in their [user settings](../../user/profile/index.md#user-settings)) The Seat usage listing is updated live, but the usage statistics on the billing page are updated only once per day. For this reason there can be a minor difference between the seat usage listing @@ -247,8 +247,11 @@ Quotas apply to: subgroups, and nested projects. To view the group's usage, navigate to the group, then **Settings > Usage Quotas**. - Your personal account, where the minutes are available for your personal projects. - To view and buy personal minutes, click your avatar, then - **Settings > [Usage Quotas](https://gitlab.com/profile/usage_quotas#pipelines-quota-tab)**. + To view and buy personal minutes: + + 1. In the top-right corner, select your avatar. + 1. Select **Edit profile**. + 1. In the left sidebar, select **[Usage Quotas](https://gitlab.com/profile/usage_quotas#pipelines-quota-tab)**. Only pipeline minutes for GitLab shared runners are restricted. If you have a specific runner set up for your projects, there is no limit to your build time on GitLab SaaS. @@ -282,10 +285,12 @@ To purchase additional minutes for your group on GitLab SaaS: To purchase additional minutes for your personal namespace: -1. Click your avatar, then go to **Settings > Usage Quotas**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Usage Quotas**. 1. Select **Buy additional minutes** and GitLab redirects you to the Customers Portal. 1. Locate the subscription card that's linked to your personal namespace on GitLab SaaS, click **Buy more CI minutes**, and complete the details about the transaction. Once we have processed your payment, the extra CI minutes are synced to your personal namespace. -1. To confirm the available CI minutes for your personal projects, click your avatar, then go to **Settings > Usage Quotas**. +1. To confirm the available CI minutes for your personal projects, go to the **Usage Quotas** settings again. The **Additional minutes** displayed now includes the purchased additional CI minutes, plus any minutes rolled over from last month. diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index 10344c60e7e..b5f3ad24d32 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -22,7 +22,7 @@ SAML SSO is not supported at the subgroup level. ## Configuring your Identity Provider -1. Navigate to the group and click **Settings > SAML SSO**. +1. Navigate to the group and select **Settings > SAML SSO**. 1. Configure your SAML server using the **Assertion consumer service URL**, **Identifier**, and **GitLab single sign-on URL**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [specific identity provider documentation](#providers) for more details. 1. Configure the SAML response to include a NameID that uniquely identifies each user. 1. Configure [required assertions](group_managed_accounts.md#assertions) if using [Group Managed Accounts](group_managed_accounts.md). @@ -57,7 +57,7 @@ We recommend setting the NameID format to `Persistent` unless using a field (suc GitLab provides metadata XML that can be used to configure your Identity Provider. -1. Navigate to the group and click **Settings > SAML SSO**. +1. Navigate to the group and select **Settings > SAML SSO**. 1. Copy the provided **GitLab metadata URL**. 1. Follow your Identity Provider's documentation and paste the metadata URL when it's requested. @@ -69,8 +69,8 @@ After you set up your identity provider to work with GitLab, you must configure 1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign-on URL** field. 1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field. 1. Select the access level to be applied to newly added users in the **Default membership role** field. The default access level is 'Guest'. -1. Click the **Enable SAML authentication for this group** toggle switch. -1. Click the **Save changes** button. +1. Select the **Enable SAML authentication for this group** toggle switch. +1. Select the **Save changes** button. ![Group SAML Settings for GitLab.com](img/group_saml_settings_v13_3.png) @@ -216,7 +216,7 @@ To link SAML to your existing GitLab.com account: 1. Sign in to your GitLab.com account. 1. Locate and visit the **GitLab single sign-on URL** for the group you're signing in to. A group owner can find this on the group's **Settings > SAML SSO** page. If the sign-in URL is configured, users can connect to the GitLab app from the Identity Provider. -1. Click **Authorize**. +1. Select **Authorize**. 1. Enter your credentials on the Identity Provider if prompted. 1. You are then redirected back to GitLab.com and should now have access to the group. In the future, you can use SAML to sign in to GitLab.com. @@ -225,7 +225,7 @@ On subsequent visits, you should be able to go [sign in to GitLab.com with SAML] ### Signing in to GitLab.com with SAML 1. Sign in to your identity provider. -1. From the list of apps, click on the "GitLab.com" app (The name is set by the administrator of the identity provider). +1. From the list of apps, select the "GitLab.com" app. (The name is set by the administrator of the identity provider.) 1. You are then signed in to GitLab.com and redirected to the group. ### Configure user settings from SAML response @@ -293,10 +293,15 @@ Users can unlink SAML for a group from their profile page. This can be helpful i - Your SAML NameID has changed and so GitLab can no longer find your user. WARNING: -Unlinking an account removes all roles assigned to that user within the group. +Unlinking an account removes all roles assigned to that user in the group. If a user re-links their account, roles need to be reassigned. -For example, to unlink the `MyOrg` account, the following **Disconnect** button is available under **Profile > Accounts**: +For example, to unlink the `MyOrg` account: + +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. +1. In the **Social sign-in** section, select **Disconnect** next to the connected account. ![Unlink Group SAML](img/unlink_group_saml.png) diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md index e347221bd66..a33b6742d61 100644 --- a/doc/user/profile/account/delete_account.md +++ b/doc/user/profile/account/delete_account.md @@ -17,20 +17,21 @@ Deleting a user will delete all projects in that user namespace. ## As a user -As a user, you can delete your own account by: +As a user, to delete your own account: -1. Clicking on your avatar. -1. Navigating to **Settings > Account**. -1. Selecting **Delete account**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Account**. +1. Select **Delete account**. ## As an administrator -As an administrator, you can delete a user account by: +As an administrator, to delete a user account: -1. Navigating to **Admin Area > Overview > Users**. -1. Selecting a user. -1. Under the **Account** tab, clicking: - - **Delete user** to delete only the user but maintaining their +1. Go to **Admin Area > Overview > Users**. +1. Select a user. +1. Under the **Account** tab, select: + - **Delete user** to delete only the user but maintain their [associated records](#associated-records). - **Delete user and contributions** to delete the user and their associated records. diff --git a/doc/user/profile/account/index.md b/doc/user/profile/account/index.md deleted file mode 100644 index b10cc778f45..00000000000 --- a/doc/user/profile/account/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../index.md#profile-settings' ---- - -This document was moved to [../index.md#profile-settings](../index.md#profile-settings). - - - diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 5f468f81e0c..44bf97bdd42 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -44,7 +44,7 @@ To enable 2FA: 1. **In GitLab:** 1. Sign in to your GitLab account. - 1. Go to your [**Profile settings**](../index.md#profile-settings). + 1. Go to your [**User settings**](../index.md#user-settings). 1. Go to **Account**. 1. Select **Enable Two-factor Authentication**. 1. **On your device (usually your phone):** @@ -246,7 +246,7 @@ Search for `security.webauth.u2f` and double click on it to toggle to `true`. To set up 2FA with a U2F device: 1. Sign in to your GitLab account. -1. Go to your [**Profile settings**](../index.md#profile-settings). +1. Go to your [**User settings**](../index.md#user-settings). 1. Go to **Account**. 1. Click **Enable Two-Factor Authentication**. 1. Connect your U2F device. @@ -282,7 +282,7 @@ and the following mobile browsers: To set up 2FA with a WebAuthn compatible device: 1. Sign in to your GitLab account. -1. Go to your [**Profile settings**](../index.md#profile-settings). +1. Go to your [**User settings**](../index.md#user-settings). 1. Go to **Account**. 1. Select **Enable Two-Factor Authentication**. 1. Plug in your WebAuthn device. @@ -349,7 +349,7 @@ request and you're automatically signed in. If you ever need to disable 2FA: 1. Sign in to your GitLab account. -1. Go to your [**Profile settings**](../index.md#profile-settings). +1. Go to your [**User settings**](../index.md#user-settings). 1. Go to **Account**. 1. Click **Disable**, under **Two-Factor Authentication**. @@ -434,7 +434,7 @@ a new set of recovery codes with SSH: When prompted for a two-factor code, enter one of the recovery codes obtained from the command-line output. -After signing in, visit your **Profile settings > Account** immediately to set +After signing in, visit your **User settings > Account** immediately to set up two-factor authentication with a new device. ### Regenerate 2FA recovery codes @@ -443,8 +443,8 @@ To regenerate 2FA recovery codes, you need access to a desktop browser: 1. Navigate to GitLab. 1. Sign in to your GitLab account. -1. Go to your [**Profile settings**](../index.md#profile-settings). -1. Select **{account}** **Account > Two-Factor Authentication (2FA)**. +1. Go to your [**User settings**](../index.md#user-settings). +1. Select **Account > Two-Factor Authentication (2FA)**. 1. If you've already configured 2FA, click **Manage two-factor authentication**. 1. In the **Register Two-Factor Authenticator** pane, click **Regenerate recovery codes**. diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md index 381015f17c3..e55b92378bd 100644 --- a/doc/user/profile/active_sessions.md +++ b/doc/user/profile/active_sessions.md @@ -14,9 +14,11 @@ review the sessions, and revoke any you don't recognize. ## Listing all active sessions -1. Click your avatar. -1. Select **Settings**. -1. Click **Active Sessions** in the sidebar. +To list all active sessions: + +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Active Sessions**. ![Active sessions list](img/active_sessions_list.png) @@ -29,8 +31,12 @@ exceeds 100, the oldest ones are deleted. ## Revoking a session -1. Use the previous steps to navigate to **Active Sessions**. -1. Click on **Revoke** besides a session. The current session cannot be revoked, as this would sign you out of GitLab. +To revoke an active session: + +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Active Sessions**. +1. Select **Revoke** next to a session. The current session cannot be revoked, as this would sign you out of GitLab. NOTE: When any session is revoked all **Remember me** tokens for all diff --git a/doc/user/profile/img/profil-preferences-navigation-theme.png b/doc/user/profile/img/profil-preferences-navigation-theme.png deleted file mode 100644 index 335a19ac290..00000000000 Binary files a/doc/user/profile/img/profil-preferences-navigation-theme.png and /dev/null differ diff --git a/doc/user/profile/img/profile_settings_dropdown.png b/doc/user/profile/img/profile_settings_dropdown.png deleted file mode 100644 index 99b06a1bf58..00000000000 Binary files a/doc/user/profile/img/profile_settings_dropdown.png and /dev/null differ diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 52319b51073..1ca6f968e90 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # User account Each GitLab account has a user profile, and settings. Your [profile](#user-profile) -contains information about you, and your GitLab activity. Your [settings](#profile-settings) +contains information about you, and your GitLab activity. Your [settings](#user-settings) allow you to customize some aspects of GitLab to suit yourself. ## Creating users @@ -29,8 +29,8 @@ See [Unknown Sign-In Notification](unknown_sign_in_notification.md) for more det To access your profile: -1. Click on your avatar. -1. Select **Profile**. +1. In the top-right corner, select your avatar. +1. Select your name or username. On your profile page, you can see the following information: @@ -42,12 +42,12 @@ On your profile page, you can see the following information: - Starred projects: projects you starred - Snippets: your personal code [snippets](../snippets.md#personal-snippets) -## Profile settings +## User settings -To access your profile settings: +To access your user settings: -1. Click on your avatar. -1. Select **Settings**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. From there, you can: @@ -78,11 +78,12 @@ From there, you can: ## Changing your password -1. Navigate to your [profile's](#profile-settings) **Settings > Password**. -1. Enter your current password in the 'Current password' field. -1. Enter your desired new password twice, once in the 'New password' field and - once in the 'Password confirmation' field. -1. Click the 'Save password' button. +1. Go to your [user settings](#user-settings). +1. In the left sidebar, select **Password**. +1. Enter your current password in the **Current password** field. +1. Enter your desired new password twice, once in the **New password** field and + once in the **Password confirmation** field. +1. Select **Save password**. If you don't know your current password, select the 'I forgot my password' link. @@ -97,12 +98,13 @@ before proceeding. To change your `username`: -1. Navigate to your [profile's](#profile-settings) **Settings > Account**. +1. Navigate to your [user settings](#user-settings). +1. In the left sidebar, select **Account**. 1. Enter a new username under **Change username**. -1. Click **Update username**. +1. Select **Update username**. WARNING: -It is currently not possible to change your username if it contains a +It is not possible to change your username if it contains a project with [Container Registry](../packages/container_registry/index.md) tags, because the project cannot be moved. @@ -127,33 +129,31 @@ The following information is hidden from the user profile page (`https://gitlab. - Starred projects tab - Snippets tab -To enable private profile: +To make your profile private: -1. Click your avatar. -1. Select **Profile**. -1. Click **Edit profile** (pencil icon). -1. Check the **Private profile** option in the **Main settings** section. -1. Click **Update profile settings**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. Select the **Private profile** checkbox. +1. Select **Update profile settings**. NOTE: -All your profile information can be seen by yourself, and GitLab admins, even if +All your profile information can be seen by yourself and GitLab administrators even if the **Private profile** option is enabled. ## Add details of external accounts -GitLab allows you to add links to certain other external accounts you might have, like Skype and Twitter. They can help other users connect with you on other platforms. +You can add links to certain other external accounts you might have, like Skype and Twitter. +They can help other users connect with you on other platforms. To add links to other accounts: -1. Click your avatar. -1. Select **Profile**. -1. Click **Edit profile** (pencil icon). -1. Complete the desired fields for external accounts, in the **Main settings** - section: +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. Edit the desired fields for external accounts: - Skype - - Twitter - LinkedIn -1. Click **Update profile settings**. + - Twitter +1. Select **Update profile settings**. ## Private contributions @@ -163,11 +163,10 @@ Enabling private contributions includes contributions to private projects, in th To enable private contributions: -1. Click on your avatar. -1. Select **Profile**. -1. Click **Edit profile** (pencil icon). -1. Check the **Private contributions** option. -1. Click **Update profile settings**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. Select the **Private contributions** checkbox. +1. Select **Update profile settings**. ## Current status @@ -183,23 +182,23 @@ They may however contain emoji codes such as `I'm on vacation :palm_tree:`. To set your current status: -1. Click your avatar. -1. Click **Set status**, or **Edit status** if you have already set a status. -1. Set the desired emoji and/or status message. -1. Click **Set status**. Alternatively, you can click **Remove status** to remove your user status entirely. +1. In the top-right corner, select your avatar. +1. Select **Set status**, or **Edit status** if you have already set a status. +1. Set the desired emoji and status message. +1. Select **Set status**. Alternatively, you can select **Remove status** to remove your user status entirely. or -1. Click your avatar. -1. Select **Profile**. -1. Click **Edit profile** (pencil icon). +1. In the top-right corner, select your avatar. +1. Select your name or username. +1. Select the Edit profile icon (**{pencil}**). 1. Enter your status message in the **Your status** text field. -1. Click **Add status emoji** (smiley face), and select the desired emoji. -1. Click **Update profile settings**. +1. Select Add status emoji icon (**{slight-smile}**), and select the desired emoji. +1. Select **Update profile settings**. You can also set your current status [using the API](../../api/users.md#user-status). -If you previously selected the "Busy" checkbox, remember to deselect it when you become available again. +If you previously selected the **Busy** checkbox, remember to deselect it when you become available again. ## Busy status indicator @@ -210,7 +209,7 @@ If you previously selected the "Busy" checkbox, remember to deselect it when you > - It's not recommended for production use. > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-busy-status-feature). -To indicate to others that you are busy, you can set an indicator +To indicate to others that you are busy, you can set an indicator. ![Busy status indicator](img/busy_status_indicator_v13_6.png) @@ -218,16 +217,15 @@ To set the busy status indicator, either: - Set it directly: - 1. Click your avatar. - 1. Click **Set status**, or **Edit status** if you have already set a status. - 1. Select the **Busy** checkbox + 1. In the top-right corner, select your avatar. + 1. Select **Set status**, or **Edit status** if you have already set a status. + 1. Select the **Busy** checkbox. - Set it on your profile: - 1. Click your avatar. - 1. Select **Profile**. - 1. Click **Edit profile** (**{pencil}**). - 1. Select the **Busy** checkbox + 1. In the top-right corner, select your avatar. + 1. Select **Edit profile**. + 1. Select the **Busy** checkbox. ### Disable busy status feature @@ -256,12 +254,11 @@ Any of your own verified email addresses can be used as the commit email. To change your commit email: -1. Click your avatar. -1. Select **Profile**. -1. Click **Edit profile** (pencil icon). -1. Click **Commit email** dropdown. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. Select the **Commit email** dropdown. 1. Select any of the verified emails. -1. Click **Update profile settings**. +1. Select **Update profile settings**. ### Private commit email @@ -272,14 +269,13 @@ which allows the user to keep their email information private. To enable this option: -1. Click your avatar. -1. Select **Profile**. -1. Click **Edit profile** (pencil icon). -1. Click **Commit email** dropdown. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. Select the **Commit email** dropdown. 1. Select **Use a private email** option. -1. Click **Update profile settings**. +1. Select **Update profile settings**. -Once this option is enabled, every Git-related action is performed using the private commit email. +After this option is enabled, every Git-related action is performed using the private commit email. To stay fully anonymous, you can also copy this private commit email and configure it on your local machine using the following command: @@ -298,7 +294,7 @@ and expires after "Application settings -> Session duration (minutes)"/`session_ (defaults to `10080` minutes = 7 days) of no activity. When signing in to the main GitLab application, you can also check the -"Remember me" option which sets the `remember_user_token` +**Remember me** option which sets the `remember_user_token` cookie (via [`devise`](https://github.com/heartcombo/devise)). `remember_user_token` expires after `config/initializers/devise.rb` -> `config.remember_for` (defaults to 2 weeks). @@ -326,7 +322,8 @@ GitLab uses both session and persistent cookies: - Session cookie: Session cookies are normally removed at the end of the browser session when the browser is closed. The `_gitlab_session` cookie has no fixed expiration date. However, it expires based on its [`session_expire_delay`](#why-do-i-keep-getting-signed-out). -- Persistent cookie: The `remember_user_token` is a cookie with an expiration date of two weeks. GitLab activates this cookie if you click Remember Me when you sign in. +- Persistent cookie: The `remember_user_token` is a cookie with an expiration date of two weeks. + GitLab activates this cookie if you select **Remember Me** when you sign in. By default, the server sets a time-to-live (TTL) of 1-week on any session that is used. @@ -336,15 +333,3 @@ The server continues to reset the TTL for that session, independent of whether 2 If you close your browser and open it up again, the `remember_user_token` cookie allows your user to reauthenticate itself. Without the `config.extend_remember_period` flag, you would be forced to sign in again after two weeks. - - diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 49889cd3017..99db0c4b898 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -33,13 +33,14 @@ You can create as many personal access tokens as you like from your GitLab profile. 1. Sign in to GitLab. -1. In the upper-right corner, click your avatar and select **Settings**. -1. On the **User Settings** menu, select **Access Tokens**. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **Access Tokens**. 1. Choose a name and optional expiry date for the token. 1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token). -1. Click the **Create personal access token** button. +1. Select **Create personal access token**. 1. Save the personal access token somewhere safe. If you navigate away or refresh -your page, and you did not save the token, you must create a new one. + your page, and you did not save the token, you must create a new one. ### Revoking a personal access token diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index fc42ad47d5a..464edf13a7e 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -12,9 +12,8 @@ of GitLab to their liking. To navigate to your profile's preferences: -1. Click your avatar. -1. Select **Settings**. -1. Click **Preferences** in the sidebar. +1. In the top-right corner, select your avatar. +1. Select **Preferences**. ## Navigation theme @@ -36,8 +35,7 @@ The default theme is Indigo. You can choose between 10 themes: - Light Red - Dark - Light - -![Profile preferences navigation themes](img/profil-preferences-navigation-theme.png) +- [Dark Mode](#dark-mode) ## Dark mode @@ -47,7 +45,8 @@ GitLab has started work on dark mode! The dark mode Alpha release is available i spirit of iteration and the lower expectations of [Alpha versions](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha). -Progress on dark mode is tracked in the [Dark theme epic](https://gitlab.com/groups/gitlab-org/-/epics/2902). See the epic for: +Progress on dark mode is tracked in the [Dark theme epic](https://gitlab.com/groups/gitlab-org/-/epics/2902). +See the epic for: - A list of known issues. - Our planned direction and next steps. @@ -60,14 +59,15 @@ the future, we plan to make it configurable in its own section along with suppor [different navigation themes](https://gitlab.com/gitlab-org/gitlab/-/issues/219512). NOTE: -Dark theme currently only works with the 'Dark' syntax highlighting. +Dark theme only works with the **Dark** syntax highlighting theme. ## Syntax highlighting theme NOTE: GitLab uses the [rouge Ruby library](http://rouge.jneen.net/ "Rouge website") for syntax highlighting outside of any Editor context. The WebIDE (like Snippets) -uses [Monaco Editor](https://microsoft.github.io/monaco-editor/) and it's provided [Monarch](https://microsoft.github.io/monaco-editor/monarch.html) library for +uses [Monaco Editor](https://microsoft.github.io/monaco-editor/) and it's provided +[Monarch](https://microsoft.github.io/monaco-editor/monarch.html) library for syntax highlighting. For a list of supported languages, visit the documentation of the respective libraries. @@ -121,7 +121,7 @@ You have 8 options here that you can use for your default dashboard view: - Your projects' activity - Starred projects' activity - Your groups -- Your [to-dos](../todos.md) +- Your [To-Do List](../todos.md) - Assigned Issues - Assigned Merge Requests - Operations Dashboard **(PREMIUM)** diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index 840f46be97b..b22a7c0295e 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -13,7 +13,7 @@ functionality to GitLab. ## Accessing integrations You can find the available integrations under your project's -**Settings ➔ Integrations** page. +**Settings > Integrations** page. There are more than 20 integrations to integrate with. Click on the one that you want to configure. diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index bc732a4afa5..d65fd619579 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -201,7 +201,7 @@ issue's description are listed in the [issue history](#issue-history). **(PREMIU You can mention a user or a group present in your GitLab instance with `@username` or `@groupname`. All mentioned users are notified via to-do items and emails, -unless they have disabled all notifications in their profile settings. +unless they have disabled all [notifications](#notifications) in their user settings. This is controlled in the [notification settings](../../profile/notifications.md). Mentions for yourself (the current logged in user) are highlighted @@ -245,8 +245,8 @@ Also: - You can mention a user or a group present in your GitLab instance with `@username` or `@groupname` and they are notified via to-do items - and emails, unless they have [disabled all notifications](#notifications) - in their profile settings. + and emails, unless they have disabled all [notifications](#notifications) + in their user settings. - Mentions for yourself (the current logged-in user) are highlighted in a different color, which allows you to quickly see which comments involve you. diff --git a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md index 4efeb3b7938..f94b0b40ed6 100644 --- a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md +++ b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md @@ -82,11 +82,10 @@ Click **Expand file** on any file to view the changes for that file. For larger merge requests, consider reviewing one file at a time. To enable this feature: -1. In the top right corner of the navigation bar, click your user avatar. -1. Click **Settings**. -1. In the left sidebar, go to **Preferences**. +1. In the top-right corner, select your avatar. +1. Select **Preferences**. 1. Scroll to the **Behavior** section and select **Show one file at a time on merge request's Changes tab**. -1. Click **Save changes** to apply. +1. Select **Save changes**. After you enable this setting, GitLab displays only one file at a time in the **Changes** tab when you review merge requests. You can click **Prev** and **Next** to view other changed files. @@ -97,7 +96,7 @@ this behavior, you can do so from your **User preferences** (as explained above) merge request: 1. Go to the merge request's **Changes** tab. -1. Click the cog icon (**{settings}**) to reveal the merge request's settings dropdown. +1. Select the cog icon (**{settings}**) to reveal the merge request's settings dropdown. 1. Select or deselect the checkbox **Show one file at a time** to change the setting accordingly. This change overrides the choice you made in your user preferences and persists until you clear your @@ -109,11 +108,11 @@ browser's cookies or change this behavior again. To seamlessly navigate among commits in a merge request: -1. Click the **Commits** tab. -1. Click a commit to open it in the single-commit view. +1. Select the **Commits** tab. +1. Select a commit to open it in the single-commit view. 1. Navigate through the commits by either: - - Clicking **Prev** and **Next** buttons on the top-right of the page. + - Selecting **Prev** and **Next** buttons on the top-right of the page. - Using the X and C keyboard shortcuts. ![Merge requests commit navigation](img/commit_nav_v13_4.png) diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index 57e9d814c95..cd1d7a6c6d8 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -138,27 +138,25 @@ started: gpg --armor --export 30F2B65B9246B6CA ``` -1. Finally, copy the public key and [add it in your profile settings](#adding-a-gpg-key-to-your-account) +1. Finally, copy the public key and [add it in your user settings](#adding-a-gpg-key-to-your-account) ## Adding a GPG key to your account NOTE: -Once you add a key, you cannot edit it, only remove it. In case the paste -didn't work, you'll have to remove the offending key and re-add it. +After you add a key, you cannot edit it, only remove it. In case the paste +didn't work, you have to remove the offending key and re-add it. -You can add a GPG key in your profile's settings: +You can add a GPG key in your user settings: -1. On the upper right corner, click on your avatar and go to your **Settings**. - - ![Settings dropdown](../../../profile/img/profile_settings_dropdown.png) - -1. Navigate to the **GPG keys** tab and paste your _public_ key in the 'Key' - box. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **GPG Keys**. +1. Paste your _public_ key in the **Key** text box. ![Paste GPG public key](img/profile_settings_gpg_keys_paste_pub.png) -1. Finally, click on **Add key** to add it to GitLab. You will be able to see - its fingerprint, the corresponding email address and creation date. +1. Select **Add key** to add it to GitLab. You can see the key's fingerprint, the corresponding + email address, and creation date. ![GPG key single page](img/profile_settings_gpg_keys_single_key.png) @@ -248,22 +246,24 @@ in case your key has been compromised. To revoke a GPG key: -1. On the upper right corner, click on your avatar and go to your **Settings**. -1. Navigate to the **GPG keys** tab. -1. Click on **Revoke** besides the GPG key you want to delete. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **GPG Keys**. +1. Select **Revoke** next to the GPG key you want to delete. ## Removing a GPG key Removing a key **does not unverify** already signed commits. Commits that were -verified by using this key will stay verified. Only unpushed commits will stay -unverified once you remove this key. To unverify already signed commits, you need +verified by using this key stay verified. Only unpushed commits stay +unverified after you remove this key. To unverify already signed commits, you need to [revoke the associated GPG key](#revoking-a-gpg-key) from your account. To remove a GPG key from your account: -1. On the upper right corner, click on your avatar and go to your **Settings**. -1. Navigate to the **GPG keys** tab. -1. Click on the trash icon besides the GPG key you want to delete. +1. In the top-right corner, select your avatar. +1. Select **Edit profile**. +1. In the left sidebar, select **GPG Keys**. +1. Select the trash icon (**{remove}**) next to the GPG key you want to delete. ## Rejecting commits that are not signed **(PREMIUM)** diff --git a/doc/user/upgrade_email_bypass.md b/doc/user/upgrade_email_bypass.md index e50b6959a70..183ce5e4312 100644 --- a/doc/user/upgrade_email_bypass.md +++ b/doc/user/upgrade_email_bypass.md @@ -116,7 +116,7 @@ LDAP Users remain confirmed if all of the following conditions are met: - The ["User email confirmation at sign-up" option](../security/user_email_confirmation.md) is set to false. - The first sign-in is based on user LDAP credentials. -- The user has added and verified [a secondary email address](profile/index.md#profile-settings) some time later. +- The user has added and verified [a secondary email address](profile/index.md#user-settings) some time later. NOTE: Confirmation timestamps (primary vs. secondary) are different. @@ -124,6 +124,6 @@ Confirmation timestamps (primary vs. secondary) are different. Users remain unconfirmed by the background migration if any of the following conditions are met: - They [create an account through GitLab](profile/account/create_accounts.md). -- They [swap their primary email address](profile/index.md#profile-settings) and verify it. +- They [swap their primary email address](profile/index.md#user-settings) and verify it. - If they have two email addresses with the same `confirmed_at` timestamp due to the linked [security issue](https://gitlab.com/gitlab-org/gitlab/-/issues/121664). - [LDAP is introduced](../administration/auth/ldap/index.md), and users' primary email address matches that in LDAP. diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 6de80c17960..a3fee49cd8f 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -116,6 +116,10 @@ module API 'Could not find a user for the given key' unless actor.user end + + def two_factor_otp_check + { success: false, message: 'Feature is not available' } + end end namespace 'internal' do @@ -278,6 +282,11 @@ module API present response, with: Entities::InternalPostReceive::Response end + # This endpoint was added in https://gitlab.com/gitlab-org/gitlab/-/issues/212308 + # It was added with the plan to be used by GitLab PAM module but we + # decided to pursue a different approach, so it's currently not used. + # We might revive the PAM module though as it provides better user + # flow. post '/two_factor_config', feature_category: :authentication_and_authorization do status 200 @@ -303,28 +312,7 @@ module API post '/two_factor_otp_check', feature_category: :authentication_and_authorization do status 200 - break { success: false, message: 'Feature flag is disabled' } unless Feature.enabled?(:two_factor_for_cli) - - actor.update_last_used_at! - user = actor.user - - error_message = validate_actor_key(actor, params[:key_id]) - - break { success: false, message: error_message } if error_message - - break { success: false, message: 'Deploy keys cannot be used for Two Factor' } if actor.key.is_a?(DeployKey) - - break { success: false, message: 'Two-factor authentication is not enabled for this user' } unless user.two_factor_enabled? - - otp_validation_result = ::Users::ValidateOtpService.new(user).execute(params.fetch(:otp_attempt)) - - if otp_validation_result[:status] == :success - ::Gitlab::Auth::Otp::SessionEnforcer.new(actor.key).update_session - - { success: true } - else - { success: false, message: 'Invalid OTP' } - end + two_factor_otp_check end end end diff --git a/lib/gitlab/auth/otp/session_enforcer.rb b/lib/gitlab/auth/otp/session_enforcer.rb deleted file mode 100644 index 6488fa0f945..00000000000 --- a/lib/gitlab/auth/otp/session_enforcer.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Auth - module Otp - class SessionEnforcer - OTP_SESSIONS_NAMESPACE = 'session:otp' - - def initialize(key) - @key = key - end - - def update_session - Gitlab::Redis::SharedState.with do |redis| - redis.setex(key_name, session_expiry_in_seconds, true) - end - end - - def access_restricted? - Gitlab::Redis::SharedState.with do |redis| - !redis.get(key_name) - end - end - - private - - attr_reader :key - - def key_name - @key_name ||= "#{OTP_SESSIONS_NAMESPACE}:#{key.id}" - end - - def session_expiry_in_seconds - Gitlab::CurrentSettings.git_two_factor_session_expiry.minutes.to_i - end - end - end - end -end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index e0b145f69aa..cc1f2fbd029 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -77,7 +77,6 @@ module Gitlab check_authentication_abilities! check_command_disabled! check_command_existence! - check_otp_session! custom_action = check_custom_action return custom_action if custom_action @@ -255,31 +254,6 @@ module Gitlab end end - def check_otp_session! - return unless ssh? - return if !key? || deploy_key? - return unless Feature.enabled?(:two_factor_for_cli) - return unless user.two_factor_enabled? - - if ::Gitlab::Auth::Otp::SessionEnforcer.new(actor).access_restricted? - message = "OTP verification is required to access the repository.\n\n"\ - " Use: #{build_ssh_otp_verify_command}" - - raise ForbiddenError, message - end - end - - def build_ssh_otp_verify_command - user = "#{Gitlab.config.gitlab_shell.ssh_user}@" unless Gitlab.config.gitlab_shell.ssh_user.empty? - user_host = "#{user}#{Gitlab.config.gitlab_shell.ssh_host}" - - if Gitlab.config.gitlab_shell.ssh_port != 22 - "ssh #{user_host} -p #{Gitlab.config.gitlab_shell.ssh_port} 2fa_verify" - else - "ssh #{user_host} 2fa_verify" - end - end - def check_db_accessibility! return unless receive_pack? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index faa82c64d89..1fd89f64c8d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8664,13 +8664,13 @@ msgstr "" msgid "CurrentUser|Buy Pipeline minutes" msgstr "" +msgid "CurrentUser|Edit profile" +msgstr "" + msgid "CurrentUser|One of your groups is running out" msgstr "" -msgid "CurrentUser|Profile" -msgstr "" - -msgid "CurrentUser|Settings" +msgid "CurrentUser|Preferences" msgstr "" msgid "CurrentUser|Start an Ultimate trial" diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 627809dacf2..f0df901a8f0 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -6,7 +6,7 @@ module QA class Menu < Page::Base view 'app/views/layouts/header/_current_user_dropdown.html.haml' do element :sign_out_link - element :settings_link + element :edit_profile_link end view 'app/views/layouts/header/_default.html.haml' do @@ -115,10 +115,10 @@ module QA sign_out if signed_in? end - def click_settings_link + def click_edit_profile_link retry_until(reload: false) do within_user_menu do - click_link 'Settings' + click_element(:edit_profile_link) end has_text?('User Settings') diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb index 488138326df..6b2301ba916 100644 --- a/qa/qa/resource/personal_access_token.rb +++ b/qa/qa/resource/personal_access_token.rb @@ -15,7 +15,7 @@ module QA end def fabricate! - Page::Main::Menu.perform(&:click_settings_link) + Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_access_tokens) Page::Profile::PersonalAccessTokens.perform do |token_page| diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index fcd0a479fec..52526275cb0 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -22,7 +22,7 @@ module QA end def fabricate! - Page::Main::Menu.perform(&:click_settings_link) + Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_ssh_keys) Page::Profile::SSHKeys.perform do |profile_page| diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb index f6d2492c011..3702f95158c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb @@ -48,7 +48,7 @@ module QA def enable_2fa_for_user(user) Flow::Login.while_signed_in(as: user) do - Page::Main::Menu.perform(&:click_settings_link) + Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_account) Page::Profile::Accounts::Show.perform(&:click_enable_2fa_button) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index d9b246fc458..ffc2290b644 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -58,7 +58,7 @@ module QA # this is the only test that exercise this UI. # Other tests should use the API for this purpose. Flow::Login.sign_in(as: user) - Page::Main::Menu.perform(&:click_settings_link) + Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_account) Page::Profile::Accounts::Show.perform do |show| show.delete_account(user.password) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb index 2001069c2a4..158bd19704e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb @@ -21,7 +21,7 @@ module QA # Note this context ensures that the example it contains is executed after the example above. Be aware of the order of execution if you add new examples in either context. context 'after adding an ssh key' do it 'can delete an ssh key', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/930' do - Page::Main::Menu.perform(&:click_settings_link) + Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_ssh_keys) Page::Profile::SSHKeys.perform do |ssh_keys| ssh_keys.remove_key(key.title) diff --git a/qa/qa/tools/revoke_all_personal_access_tokens.rb b/qa/qa/tools/revoke_all_personal_access_tokens.rb index e33d522bece..0aaf1ed6f7f 100644 --- a/qa/qa/tools/revoke_all_personal_access_tokens.rb +++ b/qa/qa/tools/revoke_all_personal_access_tokens.rb @@ -27,7 +27,7 @@ module QA Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - Page::Main::Menu.perform(&:click_settings_link) + Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_access_tokens) token_name = 'api-test-token' diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 74e6aac8845..aab2e6d7cef 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe "Admin::Projects" do + include Spec::Support::Helpers::Features::MembersHelpers include Select2Helper let(:user) { create :user } @@ -10,8 +11,6 @@ RSpec.describe "Admin::Projects" do let(:current_user) { create(:admin) } before do - stub_feature_flags(vue_project_members_list: false) - sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user) end @@ -93,46 +92,97 @@ RSpec.describe "Admin::Projects" do end end - describe 'add admin himself to a project' do - before do - project.add_maintainer(user) - stub_feature_flags(invite_members_group_modal: false) - end - - it 'adds admin a to a project as developer', :js do - visit project_project_members_path(project) - - page.within '.invite-users-form' do - select2(current_user.id, from: '#user_ids', multiple: true) - select 'Developer', from: 'access_level' + context 'when `vue_project_members_list` feature flag is enabled', :js do + describe 'admin adds themselves to the project' do + before do + project.add_maintainer(user) + stub_feature_flags(invite_members_group_modal: false) end - click_button 'Invite' + it 'adds admin to the project as developer', :js do + visit project_project_members_path(project) - page.within '.content-list' do - expect(page).to have_content(current_user.name) - expect(page).to have_content('Developer') + page.within '.invite-users-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Invite' + + expect(find_member_row(current_user)).to have_content('Developer') + end + end + + describe 'admin removes themselves from the project' do + before do + project.add_maintainer(user) + project.add_developer(current_user) + end + + it 'removes admin from the project' do + visit project_project_members_path(project) + + expect(find_member_row(current_user)).to have_content('Developer') + + page.within find_member_row(current_user) do + click_button 'Leave' + end + + page.within('[role="dialog"]') do + click_button('Leave') + end + + expect(current_path).to match dashboard_projects_path end end end - describe 'admin remove himself from a project' do + context 'when `vue_project_members_list` feature flag is disabled' do before do - project.add_maintainer(user) - project.add_developer(current_user) + stub_feature_flags(vue_project_members_list: false) end - it 'removes admin from the project' do - visit project_project_members_path(project) - - page.within '.content-list' do - expect(page).to have_content(current_user.name) - expect(page).to have_content('Developer') + describe 'admin adds themselves to the project' do + before do + project.add_maintainer(user) + stub_feature_flags(invite_members_group_modal: false) end - find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-danger').click + it 'adds admin to the project as developer', :js do + visit project_project_members_path(project) - expect(page).not_to have_selector(:css, '.content-list') + page.within '.invite-users-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Invite' + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + end + + describe 'admin removes themselves from the project' do + before do + project.add_maintainer(user) + project.add_developer(current_user) + end + + it 'removes admin from the project' do + visit project_project_members_path(project) + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + + find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-danger').click + + expect(page).not_to have_selector(:css, '.content-list') + end end end end diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb index e7dd50ed514..befa7bd338b 100644 --- a/spec/features/admin/users/user_spec.rb +++ b/spec/features/admin/users/user_spec.rb @@ -171,7 +171,7 @@ RSpec.describe 'Admin::Users::User' do it 'logs in as the user when impersonate is clicked' do subject - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username) + expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eql(another_user.username) end it 'sees impersonation log out icon' do @@ -205,7 +205,7 @@ RSpec.describe 'Admin::Users::User' do it 'logs out of impersonated user back to original user' do subject - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username) + expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eq(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb index 77da1f138c7..475fda5e7a1 100644 --- a/spec/features/profiles/user_visits_profile_spec.rb +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'User visits their profile' do find(:css, '.header-user-dropdown-toggle').click page.within ".header-user" do - click_link "Profile" + click_link user.username end end diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb index c0849cc7330..d710ecf6c88 100644 --- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb +++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb @@ -3,22 +3,36 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Anonymous user sees members' do + include Spec::Support::Helpers::Features::MembersHelpers + let(:user) { create(:user) } let(:group) { create(:group, :public) } let(:project) { create(:project, :public) } before do - stub_feature_flags(vue_project_members_list: false) - project.add_maintainer(user) create(:project_group_link, project: project, group: group) end - it "anonymous user visits the project's members page and sees the list of members" do - visit project_project_members_path(project) + context 'when `vue_project_members_list` feature flag is enabled', :js do + it "anonymous user visits the project's members page and sees the list of members" do + visit project_project_members_path(project) - expect(current_path).to eq( - project_project_members_path(project)) - expect(page).to have_content(user.name) + expect(find_member_row(user)).to have_content(user.name) + end + end + + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + end + + it "anonymous user visits the project's members page and sees the list of members" do + visit project_project_members_path(project) + + expect(current_path).to eq( + project_project_members_path(project)) + expect(page).to have_content(user.name) + end end end diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index b6a5fbf5584..1abd00421ec 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Projects members', :js do + include Spec::Support::Helpers::Features::MembersHelpers + let(:user) { create(:user) } let(:developer) { create(:user) } let(:group) { create(:group, :public) } @@ -13,124 +15,223 @@ RSpec.describe 'Projects members', :js do let(:group_requester) { create(:user) } before do - stub_feature_flags(vue_project_members_list: false) - project.add_developer(developer) group.add_owner(user) sign_in(user) end - context 'with a group invitee' do + context 'when `vue_project_members_list` feature flag is enabled' do + context 'with a group invitee' do + before do + group_invitee + visit project_project_members_path(project) + end + + it 'does not appear in the project members page' do + expect(members_table).not_to have_content('test2@abc.com') + end + end + + context 'with a group' do + it 'shows group and project members by default' do + visit project_project_members_path(project) + + expect(members_table).to have_content(developer.name) + expect(members_table).to have_content(user.name) + expect(members_table).to have_content(group.name) + end + + it 'shows project members only if requested' do + visit project_project_members_path(project, with_inherited_permissions: 'exclude') + + expect(members_table).to have_content(developer.name) + expect(members_table).not_to have_content(user.name) + expect(members_table).not_to have_content(group.name) + end + + it 'shows group members only if requested' do + visit project_project_members_path(project, with_inherited_permissions: 'only') + + expect(members_table).not_to have_content(developer.name) + expect(members_table).to have_content(user.name) + expect(members_table).to have_content(group.name) + end + end + + context 'with a group, a project invitee, and a project requester' do + before do + group.request_access(group_requester) + project.request_access(project_requester) + group_invitee + project_invitee + visit project_project_members_path(project) + end + + it 'shows the group owner' do + expect(members_table).to have_content(user.name) + expect(members_table).to have_content(group.name) + end + + it 'shows the project developer' do + expect(members_table).to have_content(developer.name) + end + + it 'shows the project invitee' do + click_link 'Invited' + + expect(members_table).to have_content('test1@abc.com') + expect(members_table).not_to have_content('test2@abc.com') + end + + it 'shows the project requester' do + click_link 'Access requests' + + expect(members_table).to have_content(project_requester.name) + expect(members_table).not_to have_content(group_requester.name) + end + end + + context 'with a group requester' do + before do + stub_feature_flags(invite_members_group_modal: false) + group.request_access(group_requester) + visit project_project_members_path(project) + end + + it 'does not appear in the project members page' do + expect(page).not_to have_link('Access requests') + expect(members_table).not_to have_content(group_requester.name) + end + end + + context 'showing status of members' do + it 'shows the status' do + create(:user_status, user: user, emoji: 'smirk', message: 'Authoring this object') + + visit project_project_members_path(project) + + expect(first_row).to have_selector('gl-emoji[data-name="smirk"]') + end + end + end + + context 'when `vue_project_members_list` feature flag is disabled' do before do - group_invitee - visit project_project_members_path(project) + stub_feature_flags(vue_project_members_list: false) end - it 'does not appear in the project members page' do - page.within first('.content-list') do - expect(page).not_to have_content('test2@abc.com') + context 'with a group invitee' do + before do + group_invitee + visit project_project_members_path(project) end - end - end - context 'with a group' do - it 'shows group and project members by default' do - visit project_project_members_path(project) - - page.within first('.content-list') do - expect(page).to have_content(developer.name) - - expect(page).to have_content(user.name) - expect(page).to have_content(group.name) + it 'does not appear in the project members page' do + page.within first('.content-list') do + expect(page).not_to have_content('test2@abc.com') + end end end - it 'shows project members only if requested' do - visit project_project_members_path(project, with_inherited_permissions: 'exclude') + context 'with a group' do + it 'shows group and project members by default' do + visit project_project_members_path(project) - page.within first('.content-list') do - expect(page).to have_content(developer.name) + page.within first('.content-list') do + expect(page).to have_content(developer.name) - expect(page).not_to have_content(user.name) - expect(page).not_to have_content(group.name) + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end + end + + it 'shows project members only if requested' do + visit project_project_members_path(project, with_inherited_permissions: 'exclude') + + page.within first('.content-list') do + expect(page).to have_content(developer.name) + + expect(page).not_to have_content(user.name) + expect(page).not_to have_content(group.name) + end + end + + it 'shows group members only if requested' do + visit project_project_members_path(project, with_inherited_permissions: 'only') + + page.within first('.content-list') do + expect(page).not_to have_content(developer.name) + + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end end end - it 'shows group members only if requested' do - visit project_project_members_path(project, with_inherited_permissions: 'only') - - page.within first('.content-list') do - expect(page).not_to have_content(developer.name) - - expect(page).to have_content(user.name) - expect(page).to have_content(group.name) + context 'with a group, a project invitee, and a project requester' do + before do + group.request_access(group_requester) + project.request_access(project_requester) + group_invitee + project_invitee + visit project_project_members_path(project) end - end - end - context 'with a group, a project invitee, and a project requester' do - before do - group.request_access(group_requester) - project.request_access(project_requester) - group_invitee - project_invitee - visit project_project_members_path(project) - end + it 'shows the group owner' do + page.within first('.content-list') do + # Group owner + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end + end - it 'shows the group owner' do - page.within first('.content-list') do - # Group owner - expect(page).to have_content(user.name) - expect(page).to have_content(group.name) + it 'shows the project developer' do + page.within first('.content-list') do + # Project developer + expect(page).to have_content(developer.name) + end + end + + it 'shows the project invitee' do + click_link 'Invited' + + page.within first('.content-list') do + expect(page).to have_content('test1@abc.com') + expect(page).not_to have_content('test2@abc.com') + end + end + + it 'shows the project requester' do + click_link 'Access requests' + + page.within first('.content-list') do + expect(page).to have_content(project_requester.name) + expect(page).not_to have_content(group_requester.name) + end end end - it 'shows the project developer' do - page.within first('.content-list') do - # Project developer - expect(page).to have_content(developer.name) + context 'with a group requester' do + before do + stub_feature_flags(invite_members_group_modal: false) + group.request_access(group_requester) + visit project_project_members_path(project) + end + + it 'does not appear in the project members page' do + expect(page).not_to have_link('Access requests') + page.within first('.content-list') do + expect(page).not_to have_content(group_requester.name) + end end end - it 'shows the project invitee' do - click_link 'Invited' + context 'showing status of members' do + it_behaves_like 'showing user status' do + let(:user_with_status) { developer } - page.within first('.content-list') do - expect(page).to have_content('test1@abc.com') - expect(page).not_to have_content('test2@abc.com') + subject { visit project_project_members_path(project) } end end - - it 'shows the project requester' do - click_link 'Access requests' - - page.within first('.content-list') do - expect(page).to have_content(project_requester.name) - expect(page).not_to have_content(group_requester.name) - end - end - end - - context 'with a group requester' do - before do - stub_feature_flags(invite_members_group_modal: false) - group.request_access(group_requester) - visit project_project_members_path(project) - end - - it 'does not appear in the project members page' do - expect(page).not_to have_link('Access requests') - page.within first('.content-list') do - expect(page).not_to have_content(group_requester.name) - end - end - end - - describe 'showing status of members' do - it_behaves_like 'showing user status' do - let(:user_with_status) { developer } - - subject { visit project_project_members_path(project) } - end end end diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb index de27692b535..9d087dfd5f6 100644 --- a/spec/features/projects/members/groups_with_access_list_spec.rb +++ b/spec/features/projects/members/groups_with_access_list_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Groups with access list', :js do + include Spec::Support::Helpers::Features::MembersHelpers + let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :public) } @@ -11,92 +13,176 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do let!(:group_link) { create(:project_group_link, project: project, group: group, **additional_link_attrs) } before do - stub_feature_flags(vue_project_members_list: false) - travel_to Time.now.utc.beginning_of_day project.add_maintainer(user) sign_in(user) - visit project_project_members_path(project) - click_groups_tab end - it 'updates group access level' do - click_button group_link.human_access - - page.within '.dropdown-menu' do - click_link 'Guest' + context 'when `vue_project_members_list` feature flag is enabled' do + before do + visit project_project_members_path(project) + click_groups_tab end - wait_for_requests + it 'updates group access level' do + click_button group_link.human_access + click_button 'Guest' - visit project_project_members_path(project) + wait_for_requests - click_groups_tab + visit project_project_members_path(project) - expect(first('.group_member')).to have_content('Guest') - end + click_groups_tab - it 'updates expiry date' do - expires_at_field = "member_expires_at_#{group.id}" - fill_in expires_at_field, with: 3.days.from_now.to_date - - find_field(expires_at_field).native.send_keys :enter - wait_for_requests - - page.within(find('li.group_member')) do - expect(page).to have_content('Expires in 3 days') + expect(find_group_row(group)).to have_content('Guest') end - end - context 'when link has expiry date set' do - let(:additional_link_attrs) { { expires_at: 3.days.from_now.to_date } } - - it 'clears expiry date' do - page.within(find('li.group_member')) do - expect(page).to have_content('Expires in 3 days') - - page.within(find('.js-edit-member-form')) do - find('.js-clear-input').click - end + it 'updates expiry date' do + page.within find_group_row(group) do + fill_in 'Expiration date', with: 5.days.from_now.to_date + find_field('Expiration date').native.send_keys :enter wait_for_requests - expect(page).not_to have_content('Expires in') + expect(page).to have_content(/in \d days/) + end + end + + context 'when link has expiry date set' do + let(:additional_link_attrs) { { expires_at: 5.days.from_now.to_date } } + + it 'clears expiry date' do + page.within find_group_row(group) do + expect(page).to have_content(/in \d days/) + + find('[data-testid="clear-button"]').click + + wait_for_requests + + expect(page).to have_content('No expiration set') + end + end + end + + it 'deletes group link' do + expect(page).to have_content(group.full_name) + + page.within find_group_row(group) do + click_button 'Remove group' + end + + page.within('[role="dialog"]') do + click_button('Remove group') + end + + expect(page).not_to have_content(group.full_name) + end + + context 'search in existing members' do + it 'finds no results' do + fill_in_filtered_search 'Search groups', with: 'testing 123' + + click_groups_tab + + expect(page).not_to have_content(group.full_name) + end + + it 'finds results' do + fill_in_filtered_search 'Search groups', with: group.full_name + + click_groups_tab + + expect(members_table).to have_content(group.full_name) end end end - it 'deletes group link' do - page.within(first('.group_member')) do - accept_confirm { find('.btn-danger').click } + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + + visit project_project_members_path(project) + click_groups_tab end - wait_for_requests - expect(page).not_to have_selector('.group_member') - end + it 'updates group access level' do + click_button group_link.human_access - context 'search in existing members' do - it 'finds no results' do - page.within '.user-search-form' do - fill_in 'search_groups', with: 'testing 123' - find('.user-search-btn').click + page.within '.dropdown-menu' do + click_link 'Guest' end + wait_for_requests + + visit project_project_members_path(project) + click_groups_tab + expect(first('.group_member')).to have_content('Guest') + end + + it 'updates expiry date' do + expires_at_field = "member_expires_at_#{group.id}" + fill_in expires_at_field, with: 3.days.from_now.to_date + + find_field(expires_at_field).native.send_keys :enter + wait_for_requests + + page.within(find('li.group_member')) do + expect(page).to have_content('Expires in 3 days') + end + end + + context 'when link has expiry date set' do + let(:additional_link_attrs) { { expires_at: 3.days.from_now.to_date } } + + it 'clears expiry date' do + page.within(find('li.group_member')) do + expect(page).to have_content('Expires in 3 days') + + page.within(find('.js-edit-member-form')) do + find('.js-clear-input').click + end + + wait_for_requests + + expect(page).not_to have_content('Expires in') + end + end + end + + it 'deletes group link' do + page.within(first('.group_member')) do + accept_confirm { find('.btn-danger').click } + end + wait_for_requests + expect(page).not_to have_selector('.group_member') end - it 'finds results' do - page.within '.user-search-form' do - fill_in 'search_groups', with: group.name - find('.user-search-btn').click + context 'search in existing members' do + it 'finds no results' do + page.within '.user-search-form' do + fill_in 'search_groups', with: 'testing 123' + find('.user-search-btn').click + end + + click_groups_tab + + expect(page).not_to have_selector('.group_member') end - click_groups_tab + it 'finds results' do + page.within '.user-search-form' do + fill_in 'search_groups', with: group.name + find('.user-search-btn').click + end - expect(page).to have_selector('.group_member', count: 1) + click_groups_tab + + expect(page).to have_selector('.group_member', count: 1) + end end end diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index 8e956c4a7cd..f0d115fef1d 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -5,12 +5,12 @@ require 'spec_helper' RSpec.describe 'Project > Members > Invite group', :js do include Select2Helper include ActionView::Helpers::DateHelper + include Spec::Support::Helpers::Features::MembersHelpers let(:maintainer) { create(:user) } before do stub_feature_flags(invite_members_group_modal: false) - stub_feature_flags(vue_project_members_list: false) end describe 'Share with group lock' do @@ -41,21 +41,45 @@ RSpec.describe 'Project > Members > Invite group', :js do context 'when the group has "Share with group lock" disabled' do it_behaves_like 'the project can be shared with groups' - it 'the project can be shared with another group' do - visit project_project_members_path(project) + context 'when `vue_project_members_list` feature flag is enabled' do + it 'the project can be shared with another group' do + visit project_project_members_path(project) - expect(page).not_to have_link 'Groups' + expect(page).not_to have_link 'Groups' - click_on 'invite-group-tab' + click_on 'invite-group-tab' - select2 group_to_share_with.id, from: '#link_group_id' - page.find('body').click - find('.btn-success').click + select2 group_to_share_with.id, from: '#link_group_id' + page.find('body').click + find('.btn-success').click - click_link 'Groups' + click_link 'Groups' - page.within('[data-testid="project-member-groups"]') do - expect(page).to have_content(group_to_share_with.name) + expect(members_table).to have_content(group_to_share_with.name) + end + end + + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + end + + it 'the project can be shared with another group' do + visit project_project_members_path(project) + + expect(page).not_to have_link 'Groups' + + click_on 'invite-group-tab' + + select2 group_to_share_with.id, from: '#link_group_id' + page.find('body').click + find('.btn-success').click + + click_link 'Groups' + + page.within('[data-testid="project-member-groups"]') do + expect(page).to have_content(group_to_share_with.name) + end end end end @@ -122,7 +146,7 @@ RSpec.describe 'Project > Members > Invite group', :js do freeze_time { example.run } end - before do + def setup project.add_maintainer(maintainer) group.add_guest(maintainer) sign_in(maintainer) @@ -133,20 +157,37 @@ RSpec.describe 'Project > Members > Invite group', :js do select2 group.id, from: '#link_group_id' - fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d') + fill_in 'expires_at_groups', with: 5.days.from_now.strftime('%Y-%m-%d') click_on 'invite-group-tab' find('.btn-success').click end - it 'the group link shows the expiration time with a warning class' do - click_link 'Groups' + context 'when `vue_project_members_list` feature flag is enabled' do + it 'the group link shows the expiration time with a warning class' do + setup + click_link 'Groups' - page.within('[data-testid="project-member-groups"]') do - # Using distance_of_time_in_words_to_now because it is not the same as - # subtraction, and this way avoids time zone issues as well - expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at) - expect(page).to have_content(expires_in_text) - expect(page).to have_selector('.text-warning') + expect(find_group_row(group)).to have_content(/in \d days/) + expect(find_group_row(group)).to have_selector('.gl-text-orange-500') + end + end + + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + end + + it 'the group link shows the expiration time with a warning class' do + setup + click_link 'Groups' + + page.within('[data-testid="project-member-groups"]') do + # Using distance_of_time_in_words_to_now because it is not the same as + # subtraction, and this way avoids time zone issues as well + expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at) + expect(page).to have_content(expires_in_text) + expect(page).to have_selector('.text-warning') + end end end end diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb index a62ccfab244..b0fe5b9c48a 100644 --- a/spec/features/projects/members/list_spec.rb +++ b/spec/features/projects/members/list_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe 'Project members list' do include Select2Helper - include Spec::Support::Helpers::Features::ListRowsHelpers let(:user1) { create(:user, name: 'John Doe') } let(:user2) { create(:user, name: 'Mary Jane') } @@ -13,110 +12,215 @@ RSpec.describe 'Project members list' do before do stub_feature_flags(invite_members_group_modal: false) - stub_feature_flags(vue_project_members_list: false) sign_in(user1) group.add_owner(user1) end - it 'pushes `vue_project_members_list` feature flag to the frontend' do - visit_members_page + context 'when `vue_project_members_list` feature flag is enabled', :js do + include Spec::Support::Helpers::Features::MembersHelpers - expect(page).to have_pushed_frontend_feature_flags(vueProjectMembersList: false) - end + it 'pushes `vue_project_members_list` feature flag to the frontend' do + visit_members_page - it 'show members from project and group' do - project.add_developer(user2) - - visit_members_page - - expect(first_row.text).to include(user1.name) - expect(second_row.text).to include(user2.name) - end - - it 'show user once if member of both group and project' do - project.add_developer(user1) - - visit_members_page - - expect(first_row.text).to include(user1.name) - expect(second_row).to be_blank - end - - it 'update user access level', :js do - project.add_developer(user2) - - visit_members_page - - page.within(second_row) do - click_button('Developer') - click_link('Reporter') - - expect(page).to have_button('Reporter') - end - end - - it 'add user to project', :js do - visit_members_page - - add_user(user2.id, 'Reporter') - - page.within(second_row) do - expect(page).to have_content(user2.name) - expect(page).to have_button('Reporter') - end - end - - it 'remove user from project', :js do - other_user = create(:user) - project.add_developer(other_user) - - visit_members_page - - # Open modal - find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-danger').click - - expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' - - click_on('Remove member') - - wait_for_requests - - expect(page).not_to have_content(other_user.name) - expect(project.users).not_to include(other_user) - end - - it 'invite user to project', :js do - visit_members_page - - add_user('test@example.com', 'Reporter') - - click_link 'Invited' - - page.within(first_row) do - expect(page).to have_content('test@example.com') - expect(page).to have_content('Invited') - expect(page).to have_button('Reporter') - end - end - - context 'project bots' do - let(:project_bot) { create(:user, :project_bot, name: 'project_bot') } - - before do - project.add_maintainer(project_bot) + expect(page).to have_pushed_frontend_feature_flags(vueProjectMembersList: true) end - it 'does not show form used to change roles and "Expiration date" or the remove user button' do - project_member = project.project_members.find_by(user_id: project_bot.id) + it 'show members from project and group' do + project.add_developer(user2) visit_members_page - expect(page).not_to have_selector("#edit_project_member_#{project_member.id}") - expect(page).to have_no_selector("#project_member_#{project_member.id} .btn-danger") + expect(first_row).to have_content(user1.name) + expect(second_row).to have_content(user2.name) + end + + it 'show user once if member of both group and project' do + project.add_developer(user1) + + visit_members_page + + expect(first_row).to have_content(user1.name) + expect(second_row).to be_blank + end + + it 'update user access level', :js do + project.add_developer(user2) + + visit_members_page + + page.within find_member_row(user2) do + click_button('Developer') + click_button('Reporter') + + expect(page).to have_button('Reporter') + end + end + + it 'add user to project', :js do + visit_members_page + + add_user(user2.id, 'Reporter') + + page.within find_member_row(user2) do + expect(page).to have_button('Reporter') + end + end + + it 'remove user from project', :js do + other_user = create(:user) + project.add_developer(other_user) + + visit_members_page + + # Open modal + page.within find_member_row(other_user) do + click_button 'Remove member' + end + + page.within('[role="dialog"]') do + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + click_button('Remove member') + end + + wait_for_requests + + expect(members_table).not_to have_content(other_user.name) + end + + it 'invite user to project', :js do + visit_members_page + + add_user('test@example.com', 'Reporter') + + click_link 'Invited' + + page.within find_invited_member_row('test@example.com') do + expect(page).to have_button('Reporter') + end + end + + context 'project bots' do + let(:project_bot) { create(:user, :project_bot, name: 'project_bot') } + + before do + project.add_maintainer(project_bot) + end + + it 'does not show form used to change roles and "Expiration date" or the remove user button' do + visit_members_page + + page.within find_member_row(project_bot) do + expect(page).not_to have_button('Maintainer') + expect(page).to have_field('Expiration date', disabled: true) + expect(page).not_to have_button('Remove member') + end + end end end + context 'when `vue_project_members_list` feature flag is disabled' do + include Spec::Support::Helpers::Features::ListRowsHelpers + + before do + stub_feature_flags(vue_project_members_list: false) + end + + it 'show members from project and group' do + project.add_developer(user2) + + visit_members_page + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + it 'show user once if member of both group and project' do + project.add_developer(user1) + + visit_members_page + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + it 'update user access level', :js do + project.add_developer(user2) + + visit_members_page + + page.within(second_row) do + click_button('Developer') + click_link('Reporter') + + expect(page).to have_button('Reporter') + end + end + + it 'add user to project', :js do + visit_members_page + + add_user(user2.id, 'Reporter') + + page.within(second_row) do + expect(page).to have_content(user2.name) + expect(page).to have_button('Reporter') + end + end + + it 'remove user from project', :js do + other_user = create(:user) + project.add_developer(other_user) + + visit_members_page + + # Open modal + find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-danger').click + + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + + click_on('Remove member') + + wait_for_requests + + expect(page).not_to have_content(other_user.name) + expect(project.users).not_to include(other_user) + end + + it 'invite user to project', :js do + visit_members_page + + add_user('test@example.com', 'Reporter') + + click_link 'Invited' + + page.within(first_row) do + expect(page).to have_content('test@example.com') + expect(page).to have_content('Invited') + expect(page).to have_button('Reporter') + end + end + + context 'project bots' do + let(:project_bot) { create(:user, :project_bot, name: 'project_bot') } + + before do + project.add_maintainer(project_bot) + end + + it 'does not show form used to change roles and "Expiration date" or the remove user button' do + project_member = project.project_members.find_by(user_id: project_bot.id) + + visit_members_page + + expect(page).not_to have_selector("#edit_project_member_#{project_member.id}") + expect(page).to have_no_selector("#project_member_#{project_member.id} .btn-danger") + end + end + end + + private + def add_user(id, role) page.within ".invite-users-form" do select2(id, from: "#user_ids", multiple: true) diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index e7970b28e37..1127c64e0c7 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -5,65 +5,120 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Maintainer adds member with expiration date', :js do include Select2Helper include ActiveSupport::Testing::TimeHelpers + include Spec::Support::Helpers::Features::MembersHelpers let_it_be(:maintainer) { create(:user) } let_it_be(:project) { create(:project) } let(:new_member) { create(:user) } before do - stub_feature_flags(vue_project_members_list: false) - travel_to Time.now.utc.beginning_of_day project.add_maintainer(maintainer) sign_in(maintainer) end - it 'expiration date is displayed in the members list' do - stub_feature_flags(invite_members_group_modal: false) + context 'when `vue_project_members_list` feature flag is enabled' do + it 'expiration date is displayed in the members list' do + stub_feature_flags(invite_members_group_modal: false) - visit project_project_members_path(project) + visit project_project_members_path(project) - page.within '.invite-users-form' do - select2(new_member.id, from: '#user_ids', multiple: true) + page.within '.invite-users-form' do + select2(new_member.id, from: '#user_ids', multiple: true) - fill_in 'expires_at', with: 3.days.from_now.to_date - find_field('expires_at').native.send_keys :enter + fill_in 'expires_at', with: 5.days.from_now.to_date + find_field('expires_at').native.send_keys :enter - click_on 'Invite' + click_on 'Invite' + end + + page.within find_member_row(new_member) do + expect(page).to have_content(/in \d days/) + end end - page.within "#project_member_#{project_member_id}" do - expect(page).to have_content('Expires in 3 days') + it 'changes expiration date' do + project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date) + visit project_project_members_path(project) + + page.within find_member_row(new_member) do + fill_in 'Expiration date', with: 5.days.from_now.to_date + find_field('Expiration date').native.send_keys :enter + + wait_for_requests + + expect(page).to have_content(/in \d days/) + end + end + + it 'clears expiration date' do + project.team.add_users([new_member.id], :developer, expires_at: 5.days.from_now.to_date) + visit project_project_members_path(project) + + page.within find_member_row(new_member) do + expect(page).to have_content(/in \d days/) + + find('[data-testid="clear-button"]').click + + wait_for_requests + + expect(page).to have_content('No expiration set') + end end end - it 'changes expiration date' do - project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_date) - visit project_project_members_path(project) - - page.within "#project_member_#{project_member_id}" do - fill_in 'Expiration date', with: 3.days.from_now.to_date - find_field('Expiration date').native.send_keys :enter - - wait_for_requests - - expect(page).to have_content('Expires in 3 days') + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) end - end - it 'clears expiration date' do - project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date) - visit project_project_members_path(project) + it 'expiration date is displayed in the members list' do + stub_feature_flags(invite_members_group_modal: false) - page.within "#project_member_#{project_member_id}" do - expect(page).to have_content('Expires in 3 days') + visit project_project_members_path(project) - find('.js-clear-input').click + page.within '.invite-users-form' do + select2(new_member.id, from: '#user_ids', multiple: true) - wait_for_requests + fill_in 'expires_at', with: 3.days.from_now.to_date + find_field('expires_at').native.send_keys :enter - expect(page).not_to have_content('Expires in') + click_on 'Invite' + end + + page.within "#project_member_#{project_member_id}" do + expect(page).to have_content('Expires in 3 days') + end + end + + it 'changes expiration date' do + project.team.add_users([new_member.id], :developer, expires_at: 1.day.from_now.to_date) + visit project_project_members_path(project) + + page.within "#project_member_#{project_member_id}" do + fill_in 'Expiration date', with: 3.days.from_now.to_date + find_field('Expiration date').native.send_keys :enter + + wait_for_requests + + expect(page).to have_content('Expires in 3 days') + end + end + + it 'clears expiration date' do + project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date) + visit project_project_members_path(project) + + page.within "#project_member_#{project_member_id}" do + expect(page).to have_content('Expires in 3 days') + + find('.js-clear-input').click + + wait_for_requests + + expect(page).not_to have_content('Expires in') + end end end diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index 9b9f1f26d66..3c132747bc4 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -3,89 +3,180 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Sorting' do + include Spec::Support::Helpers::Features::MembersHelpers + let(:maintainer) { create(:user, name: 'John Doe') } let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) } before do - stub_feature_flags(vue_project_members_list: false) - create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) sign_in(maintainer) end - it 'sorts alphabetically by default' do - visit_members_list(sort: nil) + context 'when `vue_project_members_list` feature flag is enabled', :js do + it 'sorts by account by default' do + visit_members_list(sort: nil) - expect(first_member).to include(maintainer.name) - expect(second_member).to include(developer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + expect(first_row).to have_content(maintainer.name) + expect(second_row).to have_content(developer.name) + + expect_sort_by('Account', :asc) + end + + it 'sorts by max role ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_row).to have_content(developer.name) + expect(second_row).to have_content(maintainer.name) + + expect_sort_by('Max role', :asc) + end + + it 'sorts by max role descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_row).to have_content(maintainer.name) + expect(second_row).to have_content(developer.name) + + expect_sort_by('Max role', :desc) + end + + it 'sorts by access granted ascending' do + visit_members_list(sort: :last_joined) + + expect(first_row).to have_content(maintainer.name) + expect(second_row).to have_content(developer.name) + + expect_sort_by('Access granted', :asc) + end + + it 'sorts by access granted descending' do + visit_members_list(sort: :oldest_joined) + + expect(first_row).to have_content(developer.name) + expect(second_row).to have_content(maintainer.name) + + expect_sort_by('Access granted', :desc) + end + + it 'sorts by account ascending' do + visit_members_list(sort: :name_asc) + + expect(first_row).to have_content(maintainer.name) + expect(second_row).to have_content(developer.name) + + expect_sort_by('Account', :asc) + end + + it 'sorts by account descending' do + visit_members_list(sort: :name_desc) + + expect(first_row).to have_content(developer.name) + expect(second_row).to have_content(maintainer.name) + + expect_sort_by('Account', :desc) + end + + it 'sorts by last sign-in ascending', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :recent_sign_in) + + expect(first_row).to have_content(maintainer.name) + expect(second_row).to have_content(developer.name) + + expect_sort_by('Last sign-in', :asc) + end + + it 'sorts by last sign-in descending', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :oldest_sign_in) + + expect(first_row).to have_content(developer.name) + expect(second_row).to have_content(maintainer.name) + + expect_sort_by('Last sign-in', :desc) + end end - it 'sorts by access level ascending' do - visit_members_list(sort: :access_level_asc) + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + end - expect(first_member).to include(developer.name) - expect(second_member).to include(maintainer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') + it 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(maintainer.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + it 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(maintainer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') + end + + it 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(maintainer.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') + end + + it 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(maintainer.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined') + end + + it 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(maintainer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') + end + + it 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(maintainer.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + it 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(maintainer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') + end + + it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(maintainer.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') + end + + it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(maintainer.name) + expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') + end end - it 'sorts by access level descending' do - visit_members_list(sort: :access_level_desc) - - expect(first_member).to include(maintainer.name) - expect(second_member).to include(developer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') - end - - it 'sorts by last joined' do - visit_members_list(sort: :last_joined) - - expect(first_member).to include(maintainer.name) - expect(second_member).to include(developer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined') - end - - it 'sorts by oldest joined' do - visit_members_list(sort: :oldest_joined) - - expect(first_member).to include(developer.name) - expect(second_member).to include(maintainer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') - end - - it 'sorts by name ascending' do - visit_members_list(sort: :name_asc) - - expect(first_member).to include(maintainer.name) - expect(second_member).to include(developer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') - end - - it 'sorts by name descending' do - visit_members_list(sort: :name_desc) - - expect(first_member).to include(developer.name) - expect(second_member).to include(maintainer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') - end - - it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do - visit_members_list(sort: :recent_sign_in) - - expect(first_member).to include(maintainer.name) - expect(second_member).to include(developer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') - end - - it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do - visit_members_list(sort: :oldest_sign_in) - - expect(first_member).to include(developer.name) - expect(second_member).to include(maintainer.name) - expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') - end + private def visit_members_list(sort:) visit project_project_members_path(project, sort: sort) @@ -98,4 +189,11 @@ RSpec.describe 'Projects > Members > Sorting' do def second_member page.all('ul.content-list > li').last.text end + + def expect_sort_by(text, sort_direction) + within('[data-testid="members-sort-dropdown"]') do + expect(page).to have_css('button[aria-haspopup="true"]', text: text) + expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") + end + end end diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb index 7c72ba31029..eef3395de91 100644 --- a/spec/features/projects/members/tabs_spec.rb +++ b/spec/features/projects/members/tabs_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Tabs' do + include Spec::Support::Helpers::Features::MembersHelpers using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } @@ -19,56 +20,93 @@ RSpec.describe 'Projects > Members > Tabs' do end end - before do - stub_feature_flags(vue_project_members_list: false) - allow(Kaminari.config).to receive(:default_per_page).and_return(1) - - sign_in(user) - visit project_project_members_path(project) - end - - where(:tab, :count) do - 'Members' | 3 - 'Invited' | 2 - 'Groups' | 2 - 'Access requests' | 2 - end - - with_them do - it "renders #{params[:tab]} tab" do - expect(page).to have_selector('.nav-link', text: "#{tab} #{count}") - end - end - - context 'displays "Members" tab by default' do - it_behaves_like 'active "Members" tab' - end - - context 'when searching "Groups"', :js do + context 'tabs' do before do - click_link 'Groups' + sign_in(user) + visit project_project_members_path(project) + end - page.within '[data-testid="group-link-search-form"]' do - fill_in 'search_groups', with: 'group' - find('button[type="submit"]').click + where(:tab, :count) do + 'Members' | 3 + 'Invited' | 2 + 'Groups' | 2 + 'Access requests' | 2 + end + + with_them do + it "renders #{params[:tab]} tab" do + expect(page).to have_selector('.nav-link', text: "#{tab} #{count}") end end - it 'displays "Groups" tab' do - expect(page).to have_selector('.nav-link.active', text: 'Groups') + context 'displays "Members" tab by default' do + it_behaves_like 'active "Members" tab' + end + end + + context 'when `vue_project_members_list` feature flag is enabled' do + before do + sign_in(user) + visit project_project_members_path(project) end - context 'and then searching "Members"' do + context 'when searching "Groups"', :js do before do - click_link 'Members 3' + click_link 'Groups' - page.within '[data-testid="user-search-form"]' do - fill_in 'search', with: 'user' + fill_in_filtered_search 'Search groups', with: 'group' + end + + it 'displays "Groups" tab' do + expect(page).to have_selector('.nav-link.active', text: 'Groups') + end + + context 'and then searching "Members"' do + before do + click_link 'Members 3' + + fill_in_filtered_search 'Filter members', with: 'user' + end + + it_behaves_like 'active "Members" tab' + end + end + end + + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + + sign_in(user) + visit project_project_members_path(project) + end + + context 'when searching "Groups"', :js do + before do + click_link 'Groups' + + page.within '[data-testid="group-link-search-form"]' do + fill_in 'search_groups', with: 'group' find('button[type="submit"]').click end end - it_behaves_like 'active "Members" tab' + it 'displays "Groups" tab' do + expect(page).to have_selector('.nav-link.active', text: 'Groups') + end + + context 'and then searching "Members"' do + before do + click_link 'Members 3' + + page.within '[data-testid="user-search-form"]' do + fill_in 'search', with: 'user' + find('button[type="submit"]').click + end + end + + it_behaves_like 'active "Members" tab' + end end end end diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index a4abdf9f571..0d22da34b91 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -3,6 +3,9 @@ require 'spec_helper' RSpec.describe 'Projects > Settings > User manages project members' do + include Spec::Support::Helpers::Features::MembersHelpers + include Select2Helper + let(:group) { create(:group, name: 'OpenSource') } let(:project) { create(:project) } let(:project2) { create(:project) } @@ -11,71 +14,128 @@ RSpec.describe 'Projects > Settings > User manages project members' do let(:user_mike) { create(:user, name: 'Mike') } before do - stub_feature_flags(vue_project_members_list: false) - project.add_maintainer(user) project.add_developer(user_dmitriy) sign_in(user) end - it 'cancels a team member', :js do - visit(project_project_members_path(project)) + context 'when `vue_project_members_list` feature flag is enabled' do + it 'cancels a team member', :js do + visit(project_project_members_path(project)) - project_member = project.project_members.find_by(user_id: user_dmitriy.id) + page.within find_member_row(user_dmitriy) do + click_button 'Remove member' + end - page.within("#project_member_#{project_member.id}") do - # Open modal - click_on('Remove user from project') + page.within('[role="dialog"]') do + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + click_button('Remove member') + end + + visit(project_project_members_path(project)) + + expect(members_table).not_to have_content(user_dmitriy.name) + expect(members_table).not_to have_content(user_dmitriy.username) end - expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + it 'imports a team from another project', :js do + stub_feature_flags(invite_members_group_modal: false) - click_on('Remove member') + project2.add_maintainer(user) + project2.add_reporter(user_mike) - visit(project_project_members_path(project)) + visit(project_project_members_path(project)) - expect(page).not_to have_content(user_dmitriy.name) - expect(page).not_to have_content(user_dmitriy.username) - end + page.within('.invite-users-form') do + click_link('Import') + end - it 'imports a team from another project' do - stub_feature_flags(invite_members_group_modal: false) + select2(project2.id, from: '#source_project_id') + click_button('Import project members') - project2.add_maintainer(user) - project2.add_reporter(user_mike) - - visit(project_project_members_path(project)) - - page.within('.invite-users-form') do - click_link('Import') + expect(find_member_row(user_mike)).to have_content('Reporter') end - select(project2.full_name, from: 'source_project_id') - click_button('Import') + it 'shows all members of project shared group', :js do + group.add_owner(user) + group.add_developer(user_dmitriy) - project_member = project.project_members.find_by(user_id: user_mike.id) + share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) + share_link.group_id = group.id + share_link.save! - page.within("#project_member_#{project_member.id}") do - expect(page).to have_content('Mike') - expect(page).to have_content('Reporter') + visit(project_project_members_path(project)) + + click_link 'Groups' + + expect(find_group_row(group)).to have_content('Maintainer') end end - it 'shows all members of project shared group', :js do - group.add_owner(user) - group.add_developer(user_dmitriy) + context 'when `vue_project_members_list` feature flag is disabled' do + before do + stub_feature_flags(vue_project_members_list: false) + end - share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) - share_link.group_id = group.id - share_link.save! + it 'cancels a team member', :js do + visit(project_project_members_path(project)) - visit(project_project_members_path(project)) + project_member = project.project_members.find_by(user_id: user_dmitriy.id) - click_link 'Groups' + page.within("#project_member_#{project_member.id}") do + # Open modal + click_on('Remove user from project') + end - page.within('[data-testid="project-member-groups"]') do - expect(page).to have_content('OpenSource') - expect(first('.group_member')).to have_content('Maintainer') + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + + click_on('Remove member') + + visit(project_project_members_path(project)) + + expect(page).not_to have_content(user_dmitriy.name) + expect(page).not_to have_content(user_dmitriy.username) + end + + it 'imports a team from another project' do + stub_feature_flags(invite_members_group_modal: false) + + project2.add_maintainer(user) + project2.add_reporter(user_mike) + + visit(project_project_members_path(project)) + + page.within('.invite-users-form') do + click_link('Import') + end + + select(project2.full_name, from: 'source_project_id') + click_button('Import') + + project_member = project.project_members.find_by(user_id: user_mike.id) + + page.within("#project_member_#{project_member.id}") do + expect(page).to have_content('Mike') + expect(page).to have_content('Reporter') + end + end + + it 'shows all members of project shared group', :js do + group.add_owner(user) + group.add_developer(user_dmitriy) + + share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) + share_link.group_id = group.id + share_link.save! + + visit(project_project_members_path(project)) + + click_link 'Groups' + + page.within('[data-testid="project-member-groups"]') do + expect(page).to have_content('OpenSource') + expect(first('.group_member')).to have_content('Maintainer') + end end end end diff --git a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb deleted file mode 100644 index ece2c9bd8d7..00000000000 --- a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do - let_it_be(:key) { create(:key)} - - describe '#update_session' do - it 'registers a session in Redis' do - redis = double(:redis) - expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) - session_expiry_in_seconds = Gitlab::CurrentSettings.git_two_factor_session_expiry.minutes.to_i - - expect(redis).to( - receive(:setex) - .with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", - session_expiry_in_seconds, - true) - .once) - - described_class.new(key).update_session - end - end - - describe '#access_restricted?' do - subject { described_class.new(key).access_restricted? } - - context 'with existing session' do - before do - Gitlab::Redis::SharedState.with do |redis| - redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true ) - end - end - - it { is_expected.to be_falsey } - end - - context 'without an existing session' do - it { is_expected.to be_truthy } - end - end -end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 07f1fcda3ce..9a1ecfe6459 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -388,161 +388,6 @@ RSpec.describe Gitlab::GitAccess do end end - describe '#check_otp_session!' do - let_it_be(:user) { create(:user, :two_factor_via_otp)} - let_it_be(:key) { create(:key, user: user) } - let_it_be(:actor) { key } - - before do - project.add_developer(user) - stub_feature_flags(two_factor_for_cli: true) - end - - context 'with an OTP session', :clean_gitlab_redis_shared_state do - before do - Gitlab::Redis::SharedState.with do |redis| - redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true) - end - end - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - - context 'based on the duration set by the `git_two_factor_session_expiry` setting' do - let_it_be(:git_two_factor_session_expiry) { 20 } - let_it_be(:redis_key_expiry_at) { git_two_factor_session_expiry.minutes.from_now } - - before do - stub_application_setting(git_two_factor_session_expiry: git_two_factor_session_expiry) - end - - def value_of_key - key_expired = Time.current > redis_key_expiry_at - return if key_expired - - true - end - - def stub_redis - redis = double(:redis) - expect(Gitlab::Redis::SharedState).to receive(:with).at_most(:twice).and_yield(redis) - - expect(redis).to( - receive(:get) - .with("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}")) - .at_most(:twice) - .and_return(value_of_key) - end - - context 'at a time before the stipulated expiry' do - it 'allows push and pull access' do - travel_to(10.minutes.from_now) do - stub_redis - - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - end - - context 'at a time after the stipulated expiry' do - it 'does not allow push and pull access' do - travel_to(30.minutes.from_now) do - stub_redis - - aggregate_failures do - expect { push_access_check }.to raise_error - expect { pull_access_check }.to raise_error - end - end - end - end - end - end - - context 'without OTP session' do - it 'does not allow push or pull access' do - user = 'jane.doe' - host = 'fridge.ssh' - port = 42 - - stub_config( - gitlab_shell: { - ssh_user: user, - ssh_host: host, - ssh_port: port - } - ) - - error_message = "OTP verification is required to access the repository.\n\n"\ - " Use: ssh #{user}@#{host} -p #{port} 2fa_verify" - - aggregate_failures do - expect { push_access_check }.to raise_forbidden(error_message) - expect { pull_access_check }.to raise_forbidden(error_message) - end - end - - context 'when protocol is HTTP' do - let(:protocol) { 'http' } - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'when actor is not an SSH key' do - let(:deploy_key) { create(:deploy_key, user: user) } - let(:actor) { deploy_key } - - before do - deploy_key.deploy_keys_projects.create(project: project, can_push: true) - end - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'when 2FA is not enabled for the user' do - let(:user) { create(:user)} - let(:actor) { create(:key, user: user) } - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(two_factor_for_cli: false) - end - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - end - end - describe '#check_db_accessibility!' do context 'when in a read-only GitLab instance' do before do diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 48b487f90a6..445f4f249ee 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -458,6 +458,16 @@ RSpec.describe JiraService do expect(WebMock).to have_requested(:get, issue_url) end + + context 'with options' do + let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}?expand=renderedFields" } + + it 'calls the Jira API with the options to get the issue' do + jira_service.find_issue(issue_key, { expand: 'renderedFields' }) + + expect(WebMock).to have_requested(:get, issue_url) + end + end end describe '#close_issue' do diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index be4ecd0a734..b97620ddfe7 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -50,41 +50,6 @@ RSpec.describe API::Internal::Base do end end - shared_examples 'actor key validations' do - context 'key id is not provided' do - let(:key_id) { nil } - - it 'returns an error message' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq('Could not find a user without a key') - end - end - - context 'key does not exist' do - let(:key_id) { non_existing_record_id } - - it 'returns an error message' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq('Could not find the given key') - end - end - - context 'key without user' do - let(:key_id) { create(:key, user: nil).id } - - it 'returns an error message' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq('Could not find a user for the given key') - end - end - end - describe 'GET /internal/two_factor_recovery_codes' do let(:key_id) { key.id } @@ -1406,10 +1371,6 @@ RSpec.describe API::Internal::Base do let(:key_id) { key.id } let(:otp) { '123456'} - before do - stub_feature_flags(two_factor_for_cli: true) - end - subject do post api('/internal/two_factor_otp_check'), params: { @@ -1419,76 +1380,10 @@ RSpec.describe API::Internal::Base do } end - it_behaves_like 'actor key validations' + it 'is not available' do + subject - context 'when the key is a deploy key' do - let(:key_id) { create(:deploy_key).id } - - it 'returns an error message' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq('Deploy keys cannot be used for Two Factor') - end - end - - context 'when the two factor is enabled' do - before do - allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true) - end - - context 'when the OTP is valid' do - it 'registers a new OTP session and returns success' do - allow_any_instance_of(Users::ValidateOtpService).to receive(:execute).with(otp).and_return(status: :success) - - expect_next_instance_of(::Gitlab::Auth::Otp::SessionEnforcer) do |session_enforcer| - expect(session_enforcer).to receive(:update_session).once - end - - subject - - expect(json_response['success']).to be_truthy - end - end - - context 'when the OTP is invalid' do - it 'is not success' do - allow_any_instance_of(Users::ValidateOtpService).to receive(:execute).with(otp).and_return(status: :error) - - subject - - expect(json_response['success']).to be_falsey - end - end - end - - context 'when the two factor is disabled' do - before do - allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false) - end - - it 'returns an error message' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq 'Two-factor authentication is not enabled for this user' - end - end - - context 'two_factor_for_cli feature is disabled' do - before do - stub_feature_flags(two_factor_for_cli: false) - end - - context 'when two-factor is enabled for the user' do - it 'returns user two factor config' do - allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true) - - subject - - expect(json_response['success']).to be_falsey - end - end + expect(json_response['success']).to be_falsey end end diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_table_helpers.rb index 5394e370900..4a0e218ed3e 100644 --- a/spec/support/helpers/features/members_table_helpers.rb +++ b/spec/support/helpers/features/members_table_helpers.rb @@ -30,6 +30,32 @@ module Spec def invite_users_form page.find('[data-testid="invite-users-form"]') end + + def find_row(name) + page.within(members_table) do + page.find('tbody > tr', text: name) + end + end + + def find_member_row(user) + find_row(user.name) + end + + def find_invited_member_row(email) + find_row(email) + end + + def find_group_row(group) + find_row(group.full_name) + end + + def fill_in_filtered_search(label, with:) + page.within '[data-testid="members-filtered-search-bar"]' do + find_field(label).click + find('input').native.send_keys(with) + click_button 'Search' + end + end end end end diff --git a/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb new file mode 100644 index 00000000000..dfa1388e0bb --- /dev/null +++ b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'actor key validations' do + context 'key id is not provided' do + let(:key_id) { nil } + + it 'returns an error message' do + subject + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find a user without a key') + end + end + + context 'key does not exist' do + let(:key_id) { non_existing_record_id } + + it 'returns an error message' do + subject + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find the given key') + end + end + + context 'key without user' do + let(:key_id) { create(:key, user: nil).id } + + it 'returns an error message' do + subject + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find a user for the given key') + end + end +end