diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 1c1b6cd2dad7ace00fac525a1399b6cf93bd91c8..496fa9903cc50950bc9a763169a5aaa7ce38c8cc 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -215,7 +215,9 @@
           new gl.Members();
           new UsersSelect();
           break;
-        case 'projects:project_members:index':
+        case 'projects:members:show':
+          new gl.MemberExpirationDate('.js-access-expiration-date-groups');
+          new GroupsSelect();
           new gl.MemberExpirationDate();
           new gl.Members();
           new UsersSelect();
@@ -261,10 +263,6 @@
         case 'projects:artifacts:browse':
           new BuildArtifacts();
           break;
-        case 'projects:group_links:index':
-          new gl.MemberExpirationDate();
-          new GroupsSelect();
-          break;
         case 'search:show':
           new Search();
           break;
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js.es6
similarity index 66%
rename from app/assets/javascripts/member_expiration_date.js
rename to app/assets/javascripts/member_expiration_date.js.es6
index 7741cd2979334ea5402371b76e1a65d7c3ca3412..bf6c0ec27982d962f0ed6a28b61c98a00415d422 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -1,30 +1,29 @@
-/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
-(function() {
+(() => {
   // Add datepickers to all `js-access-expiration-date` elements. If those elements are
   // children of an element with the `clearable-input` class, and have a sibling
   // `js-clear-input` element, then show that element when there is a value in the
   // datepicker, and make clicking on that element clear the field.
   //
-  gl.MemberExpirationDate = function() {
+  window.gl = window.gl || {};
+  gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
     function toggleClearInput() {
       $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
     }
-
-    var inputs = $('.js-access-expiration-date');
+    const inputs = $(selector);
 
     inputs.datepicker({
       dateFormat: 'yy-mm-dd',
       minDate: 1,
-      onSelect: function () {
+      onSelect: function onSelect() {
         $(this).trigger('change');
         toggleClearInput.call(this);
-      }
+      },
     });
 
-    inputs.next('.js-clear-input').on('click', function(event) {
+    inputs.next('.js-clear-input').on('click', function clicked(event) {
       event.preventDefault();
 
-      var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+      const input = $(this).closest('.clearable-input').find(selector);
       input.datepicker('setDate', null)
         .trigger('change');
       toggleClearInput.call(input);
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 277d4202950dba1383dca590002ccb913fd5007f..cb25b808374704c9daa252d5316f78435661667b 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -163,6 +163,10 @@ ul.content-list {
 
         &:last-child {
           margin-right: 0;
+
+          @media(max-width: $screen-xs-max) {
+            margin: 0 auto;
+          }
         }
       }
 
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 36ee5d17211abcae41d134fdadb86a7f46d6f006..be7193bae04aa93c5396381e371c89e67d845500 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -25,7 +25,7 @@
   }
 
   .form-horizontal {
-    margin-top: 5px;
+    margin-top: 20px;
 
     @media (min-width: $screen-sm-min) {
       display: -webkit-flex;
@@ -98,6 +98,10 @@
     padding-right: 35px;
 
     @media (min-width: $screen-sm-min) {
+      width: 250px;
+    }
+
+    @media (min-width: $screen-md-min) {
       width: 350px;
     }
 
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 9eaf26a0dbf966743dcfe1998824b94771491313..66b7bdbd9889bec4f5e44180a661e32cf26b7938 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
   before_action :authorize_admin_project_member!, only: [:update]
 
   def index
-    @group_links = project.project_group_links.all
-
-    @skip_groups = @group_links.pluck(:group_id)
-    @skip_groups << project.namespace_id unless project.personal?
+    redirect_to namespace_project_settings_members_path
   end
 
   def create
@@ -25,7 +22,7 @@ def create
       flash[:alert] = 'Please select a group.'
     end
 
-    redirect_to namespace_project_group_links_path(project.namespace, project)
+    redirect_to namespace_project_settings_members_path(project.namespace, project)
   end
 
   def update
@@ -39,7 +36,7 @@ def destroy
 
     respond_to do |format|
       format.html do
-        redirect_to namespace_project_group_links_path(project.namespace, project)
+        redirect_to namespace_project_settings_members_path(project.namespace, project)
       end
       format.js { head :ok }
     end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3aec6f18e27a39ab234adf20b60f0ec526fda7f5..6e158e685e99470e5d6b1203266732f5c0b4c819 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
   before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
 
   def index
-    @sort = params[:sort].presence || sort_value_name
-    @group_links = @project.project_group_links
-
-    @project_members = @project.project_members
-    @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
-
-    group = @project.group
-
-    if group
-      # We need `.where.not(user_id: nil)` here otherwise when a group has an
-      # invitee, it would make the following query return 0 rows since a NULL
-      # user_id would be present in the subquery
-      # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
-      # FIXME: This whole logic should be moved to a finder!
-      non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
-      group_members = group.group_members.where.not(user_id: non_null_user_ids)
-      group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
-    end
-
-    if params[:search].present?
-      user_ids = @project.users.search(params[:search]).select(:id)
-      @project_members = @project_members.where(user_id: user_ids)
-
-      if group_members
-        user_ids = group.users.search(params[:search]).select(:id)
-        group_members = group_members.where(user_id: user_ids)
-      end
-
-      @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
-    end
-
-    wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
-    wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
-
-    @project_members = Member.
-      where(wheres.join(' OR ')).
-      sort(@sort).
-      page(params[:page])
-
-    @requesters = AccessRequestsFinder.new(@project).execute(current_user)
-
-    @project_member = @project.project_members.new
+    sort = params[:sort].presence || sort_value_name
+    redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
   end
 
   def create
     status = Members::CreateService.new(@project, current_user, params).execute
 
-    redirect_url = namespace_project_project_members_path(@project.namespace, @project)
+    redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
 
     if status
       redirect_to redirect_url, notice: 'Users were successfully added.'
@@ -76,14 +36,14 @@ def destroy
 
     respond_to do |format|
       format.html do
-        redirect_to namespace_project_project_members_path(@project.namespace, @project)
+        redirect_to namespace_project_settings_members_path(@project.namespace, @project)
       end
       format.js { head :ok }
     end
   end
 
   def resend_invite
-    redirect_path = namespace_project_project_members_path(@project.namespace, @project)
+    redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
 
     @project_member = @project.project_members.find(params[:id])
 
@@ -106,7 +66,7 @@ def apply_import
       return render_404
     end
 
-    redirect_to(namespace_project_project_members_path(project.namespace, project),
+    redirect_to(namespace_project_settings_members_path(project.namespace, project),
                 notice: notice)
   end
 
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5735e281f66e772b5b31600ef71e777a233d94d6
--- /dev/null
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -0,0 +1,55 @@
+module Projects
+  module Settings
+    class MembersController < Projects::ApplicationController
+      include SortingHelper
+
+      def show
+        @sort = params[:sort].presence || sort_value_name
+        @group_links = @project.project_group_links
+
+        @project_members = @project.project_members
+        @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+
+        group = @project.group
+
+        # group links
+        @group_links = @project.project_group_links.all
+
+        @skip_groups = @group_links.pluck(:group_id)
+        @skip_groups << @project.namespace_id unless @project.personal?
+
+        if group
+          # We need `.where.not(user_id: nil)` here otherwise when a group has an
+          # invitee, it would make the following query return 0 rows since a NULL
+          # user_id would be present in the subquery
+          # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+          group_members = MembersFinder.new(@project_members, group).execute(current_user)
+        end
+
+        if params[:search].present?
+          user_ids = @project.users.search(params[:search]).select(:id)
+          @project_members = @project_members.where(user_id: user_ids)
+
+          if group_members
+            user_ids = group.users.search(params[:search]).select(:id)
+            group_members = group_members.where(user_id: user_ids)
+          end
+
+          @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
+        end
+
+        wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+        wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
+
+        @project_members = Member.
+          where(wheres.join(' OR ')).
+          sort(@sort).
+          page(params[:page])
+
+        @requesters = AccessRequestsFinder.new(@project).execute(current_user)
+
+        @project_member = @project.project_members.new
+      end
+    end
+  end
+end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..702944404f50909d3fcb37f1ea5eb2b440f9c1fd
--- /dev/null
+++ b/app/finders/members_finder.rb
@@ -0,0 +1,13 @@
+class MembersFinder < Projects::ApplicationController
+  def initialize(project_members, project_group)
+    @project_members = project_members
+    @project_group = project_group
+  end
+
+  def execute(current_user)
+    non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
+    group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
+    group_members = group_members.non_invite unless can?(current_user, :admin_group,  @project_group)
+    group_members
+  end
+end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 99db73c9ee04b2778e922140cf3fbb9266afc47b..5742fec44581f21ce79bd5ecb8002ebdf41a59ff 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -206,4 +206,9 @@ def artifacts_action_path(path, project, build)
       file_namespace_project_build_artifacts_path(*args)
     end
   end
+
+  # Settings
+  def project_settings_members_path(project, *args)
+    namespace_project_settings_members_path(project.namespace, project, *args)
+  end
 end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cdb9663877cd37b0a2d994c24d6c7a2956f39e1c..8660e13081fce4b378a42f2ea1e15a343fffa6e0 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -75,7 +75,7 @@ def project_autocomplete
         { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
         { category: "Current Project", label: "Milestones",     url: namespace_project_milestones_path(@project.namespace, @project) },
         { category: "Current Project", label: "Snippets",       url: namespace_project_snippets_path(@project.namespace, @project) },
-        { category: "Current Project", label: "Members",        url: namespace_project_project_members_path(@project.namespace, @project) },
+        { category: "Current Project", label: "Members",        url: namespace_project_settings_members_path(@project.namespace, @project) },
         { category: "Current Project", label: "Wiki",           url: namespace_project_wikis_path(@project.namespace, @project) },
       ]
     else
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 613b8b7d3013d1b3c30e5156ef20a0f519e9898f..0fb2bb460cbbd096ff750969b6db6e9d7e02926e 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,14 +1,9 @@
 - if project_nav_tab? :team
-  = nav_link(controller: [:project_members, :teams]) do
-    = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
+  = nav_link(controller: [:members, :teams]) do
+    = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
       %span
         Members
 - if can_edit
-  - if @project.allowed_to_share_with_group?
-    = nav_link(controller: :group_links) do
-      = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
-        %span
-          Groups
   = nav_link(controller: :deploy_keys) do
     = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
       %span
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/_index.html.haml
similarity index 92%
rename from app/views/projects/group_links/index.html.haml
rename to app/views/projects/group_links/_index.html.haml
index 1b0dbbb8111aaebf8eee4dfd145e18d1957471fc..99d0df2ac34e42ef94d2f08ad70424addcd2c9d3 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/_index.html.haml
@@ -20,10 +20,10 @@
       .form-group
         = label_tag :expires_at, 'Access expiration date', class: 'label-light'
         .clearable-input
-          = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+          = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
           %i.clear-icon.js-clear-input
         .help-block
-          On this date, all users in the group will automatically lose access to this project.
+          On this date, all members in the group will automatically lose access to this project.
       = submit_tag "Share", class: "btn btn-create"
   .col-lg-9.col-lg-offset-3
     %hr
diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ab0771b5751c3d4c8ab41ab5d8a0b73c21c3d9a6
--- /dev/null
+++ b/app/views/projects/project_members/_index.html.haml
@@ -0,0 +1,22 @@
+.row.prepend-top-default
+  .col-lg-3.settings-sidebar
+    %h4.prepend-top-0
+      Members
+    - if can?(current_user, :admin_project_member, @project)
+      %p
+        Add a new member to
+        %strong= @project.name
+  .col-lg-9
+    .light.prepend-top-default
+      - if can?(current_user, :admin_project_member, @project)
+        = render "projects/project_members/new_project_member"
+
+        = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+        .append-bottom-default.clearfix
+          %h5.member.existing-title
+            Existing members and groups
+        - if @group_links.any?
+          = render 'projects/project_members/groups', group_links: @group_links
+
+    = render 'projects/project_members/team', members: @project_members
+    = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 79dcd7a6ee9acb501ef63897603233be50dcbd0d..2b1c23f7dda85be6d0eab1da9bdbd7f068e834ad 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,22 +1,18 @@
 = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
-  .row
-    .col-md-4.col-lg-6
-      = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
-      .help-block.append-bottom-10
-        Search for users by name, username, or email, or invite new ones using their email address.
-
-    .col-md-3.col-lg-2
-      = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
-      .help-block.append-bottom-10
-        = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
-        about role permissions
-
-    .col-md-3.col-lg-2
-      .clearable-input
-        = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
-        %i.clear-icon.js-clear-input
-      .help-block.append-bottom-10
-        On this date, the user(s) will automatically lose access to this project.
-
-    .col-md-2
-      = f.submit "Add to project", class: "btn btn-create btn-block"
+  .form-group
+    = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
+    .help-block.append-bottom-10
+      Search for members by name, username, or email, or invite new ones using their email address.
+  .form-group
+    = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+    .help-block.append-bottom-10
+      = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+      about role permissions
+  .form-group
+    .clearable-input
+      = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+      %i.clear-icon.js-clear-input
+    .help-block.append-bottom-10
+      On this date, the member(s) will automatically lose access to this project.
+  = f.submit "Add to project", class: "btn btn-create"
+  = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index c1e894d8f40f1395c5c2dc46800634a21a4c44d9..5292e73be7af837289dfa7b8ca116baebb0918c0 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,7 +1,13 @@
 .panel.panel-default
   .panel-heading
-    Users with access to
+    Members with access to
     %strong #{@project.name}
     %span.badge= @project_members.total_count
+    = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form'  do
+      .form-group
+        = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+        %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+          = icon("search")
+        = render 'shared/members/sort_dropdown'
   %ul.content-list
     = render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index eef97107d7710db73515b0493e0d32cb2a2f95d8..42ce4f8001b8dce7980fe4f36d916498f4a80646 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -12,5 +12,4 @@
 
   .form-actions
     = button_tag 'Import project members', class: "btn btn-create"
-    = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
-
+    = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
deleted file mode 100644
index 4f1cec20f8579d1118d12119a590ddf69dfb310f..0000000000000000000000000000000000000000
--- a/app/views/projects/project_members/index.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- page_title "Members"
-
-.project-members-page.prepend-top-default
-  %h4.project-members-title.clearfix
-    Members
-    = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
-  - if can?(current_user, :admin_project_member, @project)
-    .project-members-new.append-bottom-default
-      %p.clearfix
-        Add new user to
-        %strong= @project.name
-      = render "new_project_member"
-
-      = render 'shared/members/requests', membership_source: @project, requesters: @requesters
-
-  .append-bottom-default.clearfix
-    %h5.member.existing-title
-      Existing users and groups
-    = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form'  do
-      .form-group
-        = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
-        %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
-          = icon("search")
-        = render 'shared/members/sort_dropdown'
-  - if @group_links.any?
-    = render 'groups', group_links: @group_links
-
-  = render 'team', members: @project_members
-  = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d81ed7bb6097af815cb5fc6d40e47037e5082365
--- /dev/null
+++ b/app/views/projects/settings/members/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "Members"
+
+= render "projects/project_members/index"
+- if can?(current_user, :admin_project, @project)
+  - if @project.allowed_to_share_with_group?
+    = render "projects/group_links/index"
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index a46ba3b060595bffaa9d9aec54657367acfc5076..81b5bc1de300c2ccfc8767f83afcafe72a7e2089 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -37,7 +37,6 @@
         %i.clear-icon.js-clear-input
     - if can_admin_member
       = link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
-        remote: true,
         method: :delete,
         data: { confirm: "Are you sure you want to remove #{group.name}?" },
         class: 'btn btn-remove prepend-left-10' do
diff --git a/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..206be8fe3cb769e5925e195886c099c2b0debfdb
--- /dev/null
+++ b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Combined the settings options project members and groups into a single one
+  called members
+merge_request:
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 4d20acbef7a129854e06bf9e867981734c5fb170..26e2dc9e6e72078eeaaff62ca348beb4eee36887 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -307,6 +307,10 @@
         end
       end
 
+      namespace :settings do
+        resource :members, only: [:show]
+      end
+
       # Since both wiki and repository routing contains wildcard characters
       # its preferable to keep it below all other project routes
       draw :wiki
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 22d971fadfb594aca7f3a0f790bc81ea53e884fb..c89f587f14d3c7c137dc781e1726500a3f75b86e 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -113,8 +113,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
     project.team << [user, :reporter]
   end
 
-  step 'I click link "Import team from another project"' do
-    click_link "Import"
+  step 'I click link "Import team from another project"' do    
+    page.within '.users-project-form' do
+      click_link "Import"
+    end
   end
 
   When 'I submit "Website" project for import team' do
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index b9d9117c928c4a14a51e59c4babba304d03cefed..17dc101b7ee63a4e9be6db33d3b77b999d2d8d64 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -31,7 +31,7 @@
 
       it 'redirects to project group links page' do
         expect(response).to redirect_to(
-          namespace_project_group_links_path(project.namespace, project)
+          namespace_project_settings_members_path(project.namespace, project)
         )
       end
     end
@@ -62,7 +62,7 @@
 
       it 'redirects to project group links page' do
         expect(response).to redirect_to(
-          namespace_project_group_links_path(project.namespace, project)
+          namespace_project_settings_members_path(project.namespace, project)
         )
       end
     end
@@ -76,7 +76,7 @@
 
       it 'redirects to project group links page' do
         expect(response).to redirect_to(
-          namespace_project_group_links_path(project.namespace, project)
+          namespace_project_settings_members_path(project.namespace, project)
         )
         expect(flash[:alert]).to eq('Please select a group.')
       end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index b52137fbe7e595fb2cd1455f6e347b478b5f3cca..442f81187dc2def2a9fc9046154b91f98795e834 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -5,11 +5,11 @@
   let(:project) { create(:empty_project, :public, :access_requestable) }
 
   describe 'GET index' do
-    it 'renders index with 200 status code' do
+    it 'should have the settings/members address with a 302 status code' do
       get :index, namespace_id: project.namespace, project_id: project
 
-      expect(response).to have_http_status(200)
-      expect(response).to render_template(:index)
+      expect(response).to have_http_status(302)
+      expect(response.location).to include namespace_project_settings_members_path(project.namespace, project)
     end
   end
 
@@ -44,7 +44,7 @@
                       access_level: Gitlab::Access::GUEST
 
         expect(response).to set_flash.to 'Users were successfully added.'
-        expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+        expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
       end
 
       it 'adds no user to members' do
@@ -56,7 +56,7 @@
                       access_level: Gitlab::Access::GUEST
 
         expect(response).to set_flash.to 'No users or groups specified.'
-        expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+        expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
       end
     end
   end
@@ -99,7 +99,7 @@
                            id: member
 
           expect(response).to redirect_to(
-            namespace_project_project_members_path(project.namespace, project)
+            namespace_project_settings_members_path(project.namespace, project)
           )
           expect(project.members).not_to include member
         end
@@ -259,7 +259,7 @@
         expect(project.team_members).to include member
         expect(response).to set_flash.to 'Successfully imported'
         expect(response).to redirect_to(
-          namespace_project_project_members_path(project.namespace, project)
+          namespace_project_settings_members_path(project.namespace, project)
         )
       end
     end
diff --git a/spec/controllers/projects/settings/members_controller_spec.rb b/spec/controllers/projects/settings/members_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..076d6cd9c6ee0b287f33186cda996a1d8423e202
--- /dev/null
+++ b/spec/controllers/projects/settings/members_controller_spec.rb
@@ -0,0 +1,14 @@
+require('spec_helper')
+
+describe Projects::Settings::MembersController do
+  let(:project) { create(:empty_project, :public, :access_requestable) }
+
+  describe 'GET show' do
+    it 'renders show with 200 status code' do
+      get :show, namespace_id: project.namespace, project_id: project
+
+      expect(response).to have_http_status(200)
+      expect(response).to render_template(:show)
+    end
+  end
+end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index 1a71a03fbd9fc0f03736af9a7471c716ba9c666e..8b302a6aa23c7b0f776fd05c8e7e1f7cefd2e3ce 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -14,10 +14,10 @@
 
   context 'setting an expiration date for a group link' do
     before do
-      visit namespace_project_group_links_path(project.namespace, project)
+      visit namespace_project_settings_members_path(project.namespace, project)
 
       select2 group.id, from: '#link_group_id'
-      fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+      fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
       page.find('body').click
       click_on 'Share'
     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 c5e3d143d919ae4b25d7c6dd764fc6748ce4f68e..d82cf53c690fac4d50506eb0a69f717490b4d5ff 100644
--- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb
+++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
@@ -11,10 +11,10 @@
   end
 
   scenario "anonymous user visits the project's members page and sees the list of members" do
-    visit namespace_project_project_members_path(project.namespace, project)
+    visit namespace_project_settings_members_path(project.namespace, project)
 
     expect(current_path).to eq(
-      namespace_project_project_members_path(project.namespace, project))
+      namespace_project_settings_members_path(project.namespace, project))
     expect(page).to have_content(user.name)
   end
 end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index 94995f7cf95ff5f7b406c3e0879514291553c7b0..cffb935ad5ad1db674c1107af0f4e6238ffd7dfb 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -12,7 +12,7 @@
     @group_link = create(:project_group_link, project: project, group: group)
 
     login_as(user)
-    visit namespace_project_project_members_path(project.namespace, project)
+    visit namespace_project_settings_members_path(project.namespace, project)
   end
 
   it 'updates group access level' do
@@ -24,7 +24,7 @@
 
     wait_for_ajax
 
-    visit namespace_project_project_members_path(project.namespace, project)
+    visit namespace_project_settings_members_path(project.namespace, project)
 
     expect(first('.group_member')).to have_content('Guest')
   end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 7d0065ee2c4c6be39c8a22f5c1da5bcc24c40576..3385e5972ff7438442263ce56e08cc403ca4bf0e 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -19,7 +19,7 @@
   context 'with a group invitee' do
     before do
       group_invitee
-      visit namespace_project_project_members_path(project.namespace, project)
+      visit namespace_project_settings_members_path(project.namespace, project)
     end
 
     scenario 'does not appear in the project members page' do
@@ -33,7 +33,7 @@
     before do
       group_invitee
       project_invitee
-      visit namespace_project_project_members_path(project.namespace, project)
+      visit namespace_project_settings_members_path(project.namespace, project)
     end
 
     scenario 'shows the project invitee, the project developer, and the group owner' do
@@ -54,7 +54,7 @@
   context 'with a group requester' do
     before do
       group.request_access(group_requester)
-      visit namespace_project_project_members_path(project.namespace, project)
+      visit namespace_project_settings_members_path(project.namespace, project)
     end
 
     scenario 'does not appear in the project members page' do
@@ -68,7 +68,7 @@
     before do
       group.request_access(group_requester)
       project.request_access(project_requester)
-      visit namespace_project_project_members_path(project.namespace, project)
+      visit namespace_project_settings_members_path(project.namespace, project)
     end
 
     scenario 'shows the project requester, the project developer, and the group owner' do
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 b7273021c958b485756b3d46e0af31db2734b1ca..f136d9ce0fa20f60973c085a0699b4b3387d385d 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
@@ -14,15 +14,15 @@
     login_as(master)
   end
 
-  scenario 'expiration date is displayed in the members list' do
+  scenario 'expiration date is displayed in the members list', js: true do
     travel_to Time.zone.parse('2016-08-06 08:00') do
-      visit namespace_project_project_members_path(project.namespace, project)
-
+      visit namespace_project_settings_members_path(project.namespace, project)
       page.within '.users-project-form' do
         select2(new_member.id, from: '#user_ids', multiple: true)
         fill_in 'expires_at', with: '2016-08-10'
-        click_on 'Add to project'
       end
+      find('.users-project-form').click
+      click_on 'Add to project'
 
       page.within "#project_member_#{new_member.project_members.first.id}" do
         expect(page).to have_content('Expires in 4 days')
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 97c42bd7f017f14b326ef81b77f5da14e23f858b..0b4dcaa39c693a264d1732569debc43e06d5d682 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -39,7 +39,7 @@
     open_project_settings_menu
     click_link 'Members'
 
-    visit namespace_project_project_members_path(project.namespace, project)
+    visit namespace_project_settings_members_path(project.namespace, project)
     page.within('.content') do
       expect(page).not_to have_content(user.name)
     end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 1897c8119d28b16c09880455dd81cfbc5b0af7c1..ecebabefff87cfb28f5a3bdff37ece893d6010f6 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -82,8 +82,8 @@
     it { is_expected.to be_denied_for(:visitor) }
   end
 
-  describe "GET /:project_path/project_members" do
-    subject { namespace_project_project_members_path(project.namespace, project) }
+  describe "GET /:project_path/settings/members" do
+    subject { namespace_project_settings_members_path(project.namespace, project) }
 
     it { is_expected.to be_allowed_for(:admin) }
     it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index f52e23f94338c2365e0fd9c961054e9991bac2fc..9bc59a7c4f99c992d38008c9189544ce7dfb7d90 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -82,8 +82,8 @@
     it { is_expected.to be_denied_for(:visitor) }
   end
 
-  describe "GET /:project_path/project_members" do
-    subject { namespace_project_project_members_path(project.namespace, project) }
+  describe "GET /:project_path/settings/members" do
+    subject { namespace_project_settings_members_path(project.namespace, project) }
 
     it { is_expected.to be_allowed_for(:admin) }
     it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index bed9e92fcb6071a60630a07550bf9200506f062a..a8d43b3d5816322a52a60db2abbcda63f1e43372 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -82,8 +82,8 @@
     it { is_expected.to be_allowed_for(:visitor) }
   end
 
-  describe "GET /:project_path/project_members" do
-    subject { namespace_project_project_members_path(project.namespace, project) }
+  describe "GET /:project_path/settings/members" do
+    subject { namespace_project_settings_members_path(project.namespace, project) }
 
     it { is_expected.to be_allowed_for(:admin) }
     it { is_expected.to be_allowed_for(:owner).of(project) }