diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index dbc575a14879a6c2f993ea01b5b2768213b7c7f7..29138e7b0143db932d7f257dd11a3e5374c18261 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -51,13 +51,7 @@ def tabs_json(partial, data = {})
     }
   end
 
-  # rubocop:disable Gitlab/ModuleWithInstanceVariables
   def milestone_redirect_path
-    if @milestone.global_milestone?
-      url_for(action: :show, title: @milestone.title)
-    else
-      url_for(action: :show)
-    end
+    url_for(action: :show)
   end
-  # rubocop:enable Gitlab/ModuleWithInstanceVariables
 end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index d34a07324dadb318145eafa52071924798df8015..14f9a026688ba4afd383db3a7b9b331194dae91e 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,48 +1,32 @@
 # frozen_string_literal: true
 
 class Dashboard::MilestonesController < Dashboard::ApplicationController
-  include MilestoneActions
-
   before_action :projects
   before_action :groups, only: :index
-  before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
 
   def index
     respond_to do |format|
       format.html do
-        @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id))
-        @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+        @milestone_states = Milestone.states_count(@projects.select(:id), groups.select(:id))
+        @milestones = milestones.page(params[:page])
       end
       format.json do
-        render json: milestones
+        render json: milestones.to_json(only: [:id, :title], methods: :name)
       end
     end
   end
 
-  def show
-  end
-
   private
 
-  def group_milestones
-    DashboardGroupMilestone.build_collection(groups, params)
-  end
-
-  # See [#39545](https://gitlab.com/gitlab-org/gitlab-foss/issues/39545) for info about the deprecation of dynamic milestones
-  def dynamic_milestones
-    DashboardMilestone.build_collection(@projects, params)
-  end
-
   def milestones
-    @milestones = group_milestones + dynamic_milestones
-  end
-
-  def milestone
-    @milestone = DashboardMilestone.build(@projects, params[:title])
-    render_404 unless @milestone
+    MilestonesFinder.new(search_params).execute
   end
 
   def groups
     @groups ||= GroupsFinder.new(current_user, all_available: false).execute
   end
+
+  def search_params
+    params.permit(:state, :search_title).merge(group_ids: groups, project_ids: projects)
+  end
 end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 8cfbd29359773c61acc980c674b5dc399f5e07a9..8eddc07e926b94b987e35a01ad11bcdad584c111 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -13,10 +13,10 @@ def index
     respond_to do |format|
       format.html do
         @milestone_states = Milestone.states_count(group_projects_with_access, [group])
-        @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+        @milestones = milestones.page(params[:page])
       end
       format.json do
-        render json: milestones.map { |m| m.for_display.slice(:id, :title, :name) }
+        render json: milestones.to_json(only: [:id, :title], methods: :name)
       end
     end
   end
@@ -29,7 +29,7 @@ def create
     @milestone = Milestones::CreateService.new(group, current_user, milestone_params).execute
 
     if @milestone.persisted?
-      redirect_to milestone_path
+      redirect_to milestone_path(@milestone)
     else
       render "new"
     end
@@ -39,23 +39,15 @@ def show
   end
 
   def edit
-    render_404 if @milestone.legacy_group_milestone?
   end
 
   def update
-    # Keep this compatible with legacy group milestones where we have to update
-    # all projects milestones states at once.
-    milestones, update_params = get_milestones_for_update
-    milestones.each do |milestone|
-      Milestones::UpdateService.new(milestone.resource_parent, current_user, update_params).execute(milestone)
-    end
+    Milestones::UpdateService.new(@milestone.parent, current_user, milestone_params).execute(@milestone)
 
-    redirect_to milestone_path
+    redirect_to milestone_path(@milestone)
   end
 
   def destroy
-    return render_404 if @milestone.legacy_group_milestone?
-
     Milestones::DestroyService.new(group, current_user).execute(@milestone)
 
     respond_to do |format|
@@ -66,14 +58,6 @@ def destroy
 
   private
 
-  def get_milestones_for_update
-    if @milestone.legacy_group_milestone?
-      [@milestone.milestones, legacy_milestone_params]
-    else
-      [[@milestone], milestone_params]
-    end
-  end
-
   def authorize_admin_milestones!
     return render_404 unless can?(current_user, :admin_milestone, group)
   end
@@ -82,27 +66,21 @@ def milestone_params
     params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
   end
 
-  def legacy_milestone_params
-    params.require(:milestone).permit(:state_event)
+  def milestones
+    MilestonesFinder.new(search_params).execute
   end
 
-  def milestone_path
-    if @milestone.legacy_group_milestone?
-      group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
-    else
-      group_milestone_path(group, @milestone.iid)
-    end
+  def milestone
+    @milestone = group.milestones.find_by_iid(params[:id])
+
+    render_404 unless @milestone
   end
 
-  def milestones
-    milestones = MilestonesFinder.new(search_params).execute
+  def search_params
+    groups = request.format.json? ? group_ids(include_ancestors: true) : group_ids
 
     @sort = params[:sort] || 'due_date_asc'
-    MilestoneArray.sort(milestones + legacy_milestones, @sort)
-  end
-
-  def legacy_milestones
-    GroupMilestone.build_collection(group, group_projects_with_access, params)
+    params.permit(:state, :search_title).merge(sort: @sort, group_ids: groups, project_ids: group_projects_with_access)
   end
 
   def group_projects_with_access
@@ -116,23 +94,6 @@ def group_ids(include_ancestors: false)
       group.self_and_descendants.public_or_visible_to_user(current_user).select(:id)
     end
   end
-
-  def milestone
-    @milestone =
-      if params[:title]
-        GroupMilestone.build(group, group_projects_with_access, params[:title])
-      else
-        group.milestones.find_by_iid(params[:id])
-      end
-
-    render_404 unless @milestone
-  end
-
-  def search_params
-    groups = request.format.json? ? group_ids(include_ancestors: true) : group_ids
-
-    params.permit(:state, :search_title).merge(group_ids: groups)
-  end
 end
 
 Groups::MilestonesController.prepend_if_ee('EE::Groups::MilestonesController')
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 56f1f1a1019c36f44dc8a690f7b9a1a920c9b3cc..886ae262a487285e57303e7863ccbc324656b4c5 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -34,7 +34,7 @@ def index
         @milestones = @milestones.page(params[:page])
       end
       format.json do
-        render json: @milestones.to_json(methods: :name)
+        render json: @milestones.to_json(only: [:id, :title], methods: :name)
       end
     end
   end
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index cfe648d9f7917f6f854a082ec8bf67c1094562ef..8f0cdf3b255c8998e48bb488abf84f9efbd01336 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -58,10 +58,8 @@ def by_state(items)
     Milestone.filter_by_state(items, params[:state])
   end
 
-  # rubocop: disable CodeReuse/ActiveRecord
   def order(items)
-    order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC')
-    items.reorder(order_statement).order('title ASC')
+    sort_by = params[:sort].presence || 'due_date_asc'
+    items.sort_by_attribute(sort_by)
   end
-  # rubocop: enable CodeReuse/ActiveRecord
 end
diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb
index 87ea22d8f83e3171dabb02c55eede2f55131be5c..0bffdba7349c15859cc6fadb997c149aac7e87da 100644
--- a/app/helpers/timeboxes_helper.rb
+++ b/app/helpers/timeboxes_helper.rb
@@ -229,11 +229,7 @@ def timebox_date_range(timebox)
   alias_method :milestone_date_range, :timebox_date_range
 
   def milestone_tab_path(milestone, tab)
-    if milestone.global_milestone?
-      url_for(action: tab, title: milestone.title, format: :json)
-    else
-      url_for(action: tab, format: :json)
-    end
+    url_for(action: tab, format: :json)
   end
 
   def update_milestone_path(milestone, params = {})
@@ -247,11 +243,7 @@ def update_milestone_path(milestone, params = {})
   def group_milestone_route(milestone, params = {})
     params = nil if params.empty?
 
-    if milestone.legacy_group_milestone?
-      group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
-    else
-      group_milestone_path(milestone.group, milestone.iid, milestone: params)
-    end
+    group_milestone_path(milestone.group, milestone.iid, milestone: params)
   end
 
   def group_or_project_milestone_path(milestone)
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index fa5a79cc12bfb1348f06fc6cdd4fbd7851dd451a..5f24564dc56088843739ffb960dc68ce5cc74000 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -97,26 +97,6 @@ def expired?
     due_date && due_date.past?
   end
 
-  def group_milestone?
-    false
-  end
-
-  def project_milestone?
-    false
-  end
-
-  def legacy_group_milestone?
-    false
-  end
-
-  def dashboard_milestone?
-    false
-  end
-
-  def global_milestone?
-    false
-  end
-
   def total_time_spent
     @total_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + merge_requests.joins(:timelogs).sum(:time_spent)
   end
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
deleted file mode 100644
index 48c09f4cd6bee32b50628c56f1ffdb663e05d553..0000000000000000000000000000000000000000
--- a/app/models/dashboard_group_milestone.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-# Dashboard Group Milestones are milestones that allow us to pull more info out for the UI that the Milestone object doesn't allow for
-class DashboardGroupMilestone < GlobalMilestone
-  extend ::Gitlab::Utils::Override
-
-  attr_reader :group_name
-
-  def initialize(milestone)
-    super
-
-    @group_name = milestone.group.full_name
-  end
-
-  def self.build_collection(groups, params)
-    milestones = Milestone.of_groups(groups.select(:id))
-             .reorder_by_due_date_asc
-             .order_by_name_asc
-    milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
-    Milestone.filter_by_state(milestones, params[:state]).map { |m| new(m) }
-  end
-
-  def dashboard_milestone?
-    true
-  end
-
-  def merge_requests_enabled?
-    true
-  end
-end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
deleted file mode 100644
index fd59b94b737a49c20c903dcc26d89721e6aa4d10..0000000000000000000000000000000000000000
--- a/app/models/dashboard_milestone.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class DashboardMilestone < GlobalMilestone
-  attr_reader :project_name
-
-  def initialize(milestone)
-    super
-
-    @project_name = milestone.project.full_name
-  end
-
-  def project_milestone?
-    true
-  end
-
-  def merge_requests_enabled?
-    project.merge_requests_enabled?
-  end
-end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
deleted file mode 100644
index 43de7454cb753f289947b236ba8ced01ffdfb77f..0000000000000000000000000000000000000000
--- a/app/models/global_milestone.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-# frozen_string_literal: true
-# Global Milestones are milestones that can be shared across multiple projects
-class GlobalMilestone
-  include Milestoneish
-
-  STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
-
-  attr_reader :milestone
-  alias_attribute :name, :title
-
-  delegate :title, :state, :due_date, :start_date, :participants, :project,
-           :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
-           :timebox_id, :milestoneish_id, :resource_parent, :releases, to: :milestone
-
-  def to_hash
-    {
-       name: title,
-       title: title,
-       group_name: group&.full_name,
-       project_name: project&.full_name
-    }
-  end
-
-  def for_display
-    @milestone
-  end
-
-  def self.build_collection(projects, params)
-    items = Milestone.of_projects(projects)
-                .reorder_by_due_date_asc
-                .order_by_name_asc
-    items = items.search_title(params[:search_title]) if params[:search_title].present?
-
-    Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
-  end
-
-  # necessary for legacy milestones
-  def self.build(projects, title)
-    milestones = Milestone.of_projects(projects).where(title: title)
-    return if milestones.blank?
-
-    new(milestones.first)
-  end
-
-  def self.states_count(projects, group = nil)
-    legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
-    group_milestones_count = group_milestones_states_count(group)
-
-    legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
-      legacy_group_milestones_count + group_milestones_count
-    end
-  end
-
-  def self.group_milestones_states_count(group)
-    return STATE_COUNT_HASH unless group
-
-    counts_by_state = Milestone.of_groups(group).count_by_state
-
-    {
-      opened: counts_by_state['active'] || 0,
-      closed: counts_by_state['closed'] || 0,
-      all: counts_by_state.values.sum
-    }
-  end
-
-  def self.legacy_group_milestone_states_count(projects)
-    return STATE_COUNT_HASH unless projects
-
-    # We need to reorder(nil) on the projects, because the controller passes them in sorted.
-    relation = Milestone.of_projects(projects.reorder(nil)).count_by_state
-
-    {
-      opened: relation['active'] || 0,
-      closed: relation['closed'] || 0,
-      all: relation.values.sum
-    }
-  end
-
-  def initialize(milestone)
-    @milestone = milestone
-  end
-
-  def active?
-    state == 'active'
-  end
-
-  def closed?
-    state == 'closed'
-  end
-
-  def issues
-    @issues ||= Issue.of_milestones(milestone).includes(:project, :assignees, :labels)
-  end
-
-  def merge_requests
-    @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignees, :labels)
-  end
-
-  def labels
-    @labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
-  end
-
-  def global_milestone?
-    true
-  end
-end
-
-GlobalMilestone.include_if_ee('::EE::GlobalMilestone')
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
deleted file mode 100644
index 60e97174e5080e5964545fea575f22f44afacdb3..0000000000000000000000000000000000000000
--- a/app/models/group_milestone.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-# Group Milestones are milestones that can be shared among many projects within the same group
-class GroupMilestone < GlobalMilestone
-  attr_reader :group, :milestones
-
-  def self.build_collection(group, projects, params)
-    params =
-      { state: params[:state], search_title: params[:search_title] }
-
-    project_milestones = Milestone.of_projects(projects)
-    project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present?
-    child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
-    grouped_milestones = child_milestones.group_by(&:title)
-
-    grouped_milestones.map do |title, grouped|
-      new(title, grouped, group)
-    end
-  end
-
-  def self.build(group, projects, title)
-    child_milestones = Milestone.of_projects(projects).where(title: title)
-    return if child_milestones.blank?
-
-    new(title, child_milestones, group)
-  end
-
-  def initialize(title, milestones, group)
-    @milestones = milestones
-    @group = group
-  end
-
-  def milestone
-    @milestone ||= milestones.find { |m| m.description.present? } || milestones.first
-  end
-
-  def issues_finder_params
-    { group_id: group.id }
-  end
-
-  def legacy_group_milestone?
-    true
-  end
-
-  def merge_requests_enabled?
-    true
-  end
-end
-
-GroupMilestone.include_if_ee('::EE::GroupMilestone')
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index 89212eb6bf9aa6369436e0e253b5403a2fd0a4b4..4921de32f65391291582a2baecaeac9cb9701e86 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,5 +1,4 @@
 = render 'shared/milestones/milestone',
-          milestone_path: group_or_project_milestone_path(milestone),
           issues_path: issues_dashboard_path(milestone_title: milestone.title),
           merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
           milestone: milestone,
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
deleted file mode 100644
index 2129920afd274b5512b085b693858eb277ce8b1a..0000000000000000000000000000000000000000
--- a/app/views/dashboard/milestones/show.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-- header_title "Milestones", dashboard_milestones_path
-
-= render 'shared/milestones/top', milestone: @milestone
-= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
-= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 51
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index bae8997e24c093d5aba8fc08851a2dc336bb0ee0..b73626dab8143b4269a7e8981a9ee188532d81f1 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,6 +1,4 @@
-
 = render 'shared/milestones/milestone',
-          milestone_path: group_milestone_route(milestone),
           issues_path: issues_group_path(@group, milestone_title: milestone.title),
           merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
           milestone: milestone
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index b6fb908c8f644a5b16008cd162543e7726b3c8bc..03407adb57d58d74ebef39ab9535268c208d03a0 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -16,5 +16,8 @@
         .nothing-here-block No milestones to show
     - else
       - @milestones.each do |milestone|
-        = render 'milestone', milestone: milestone
+        - if milestone.project_milestone?
+          = render 'projects/milestones/milestone', milestone: milestone
+        - else
+          = render 'milestone', milestone: milestone
   = paginate @milestones, theme: "gitlab"
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index bc82b45f9023dfe0f7129e1f278dbf19d607031f..00937c5bf732e56893025851bdaab4a22622e500 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,5 +1,4 @@
 = render 'shared/milestones/milestone',
-          milestone_path: project_milestone_path(milestone.project, milestone),
           issues_path: project_issues_path(milestone.project, milestone_title: milestone.title),
           merge_requests_path: project_merge_requests_path(milestone.project, milestone_title: milestone.title),
           milestone: milestone
diff --git a/app/views/shared/milestones/_header.html.haml b/app/views/shared/milestones/_header.html.haml
index 2da857261d166bee9b35386ad0e4a3ec3f0e591d..99a46f1fb8522cee74ca769f4d4b0adf10850f7e 100644
--- a/app/views/shared/milestones/_header.html.haml
+++ b/app/views/shared/milestones/_header.html.haml
@@ -11,8 +11,7 @@
 
   .milestone-buttons
     - if can?(current_user, :admin_milestone, @group || @project)
-      - unless milestone.legacy_group_milestone?
-        = link_to _('Edit'), edit_milestone_path(milestone), class: 'btn btn-grouped'
+      = link_to _('Edit'), edit_milestone_path(milestone), class: 'btn btn-grouped'
 
       - if milestone.project_milestone? && milestone.project.group
         %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
@@ -31,8 +30,7 @@
       - else
         = link_to _('Reopen milestone'), update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'btn btn-grouped btn-reopen'
 
-      - unless milestone.legacy_group_milestone?
-        = render 'shared/milestones/delete_button'
+      = render 'shared/milestones/delete_button'
 
     %button.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ type: 'button' }
       = icon('angle-double-left')
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 9f61082d605d1b30c9527f4182aa5be4382e59be..a951c0a5119f2d1ad40cd55ebe3a6ace1aa5ea5e 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -6,39 +6,33 @@
   .row
     .col-sm-6
       .append-bottom-5
-        %strong= link_to truncate(milestone.title, length: 100), milestone_path
+        %strong= link_to truncate(milestone.title, length: 100), milestone_path(milestone)
         - if @group
           = " - #{milestone_type}"
 
-      - if @project || milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
-        - if milestone.due_date || milestone.start_date
-          .text-tertiary.append-bottom-5
-            = milestone_date_range(milestone)
-        - recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
-        - unless total_count.zero?
-          .text-tertiary.append-bottom-5.milestone-release-links
-            = icon('rocket')
-            = n_('Release', 'Releases', total_count)
-            - recent_releases.each do |release|
-              = link_to release.name, project_releases_path(release.project, anchor: release.tag)
-              - unless release == recent_releases.last
-                &bull;
-            - if total_count > recent_releases.count
+      - if milestone.due_date || milestone.start_date
+        .text-tertiary.append-bottom-5
+          = milestone_date_range(milestone)
+      - recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
+      - unless total_count.zero?
+        .text-tertiary.append-bottom-5.milestone-release-links
+          = icon('rocket')
+          = n_('Release', 'Releases', total_count)
+          - recent_releases.each do |release|
+            = link_to release.name, project_releases_path(release.project, anchor: release.tag)
+            - unless release == recent_releases.last
               &bull;
-              = link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project)
-        %div
-          = render('shared/milestone_expired', milestone: milestone)
-          - if milestone.group_milestone?
-            .label-badge.label-badge-blue.d-inline-block
-              = milestone.group.full_name
-          - if milestone.legacy_group_milestone?
-            .projects
-              - link_to milestone_path(milestone.milestone) do
-                %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
-                  = dashboard ? milestone.project.full_name : milestone.project.name
-          - if milestone.project
-            .label-badge.label-badge-gray.d-inline-block
-              = milestone.project.full_name
+          - if total_count > recent_releases.count
+            &bull;
+            = link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project)
+      %div
+        = render('shared/milestone_expired', milestone: milestone)
+        - if milestone.group_milestone?
+          .label-badge.label-badge-blue.d-inline-block
+            = milestone.group.full_name
+        - if milestone.project_milestone?
+          .label-badge.label-badge-gray.d-inline-block
+            = milestone.project.full_name
 
     .col-sm-4.milestone-progress
       = milestone_progress_bar(milestone)
@@ -49,29 +43,25 @@
       .float-lg-right.light #{milestone.percent_complete}% complete
     .col-sm-2
       .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
-        - if @project
-          - if can_admin_project_milestones? and milestone.active?
-            - if can_admin_group_milestones?
-              %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'),
-                disabled: true,
-                type: 'button',
-                data: { url: promote_project_milestone_path(milestone.project, milestone),
-                        milestone_title: milestone.title,
-                        group_name: @project.group.name,
-                        target: '#promote-milestone-modal',
-                        container: 'body',
-                        toggle: 'modal' } }
-                = sprite_icon('level-up', size: 14)
+        - if @project # if in milestones list on project level
+          - if can_admin_group_milestones?
+            %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'),
+              disabled: true,
+              type: 'button',
+              data: { url: promote_project_milestone_path(milestone.project, milestone),
+                      milestone_title: milestone.title,
+                      group_name: @project.group.name,
+                      target: '#promote-milestone-modal',
+                      container: 'body',
+                      toggle: 'modal' } }
+              = sprite_icon('level-up', size: 14)
+
+        - if can?(current_user, :admin_milestone, milestone)
+          - if milestone.closed?
+            = link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
+          - else
+            = link_to s_('Milestones|Close Milestone'), milestone_path(milestone, milestone: { state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
 
-            = link_to s_('Milestones|Close Milestone'), project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
-          - unless milestone.active?
-            = link_to s_('Milestones|Reopen Milestone'), project_milestone_path(@project, milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
-        - if @group
-          - if can?(current_user, :admin_milestone, @group)
-            - if milestone.closed?
-              = link_to s_('Milestones|Reopen Milestone'), group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
-            - else
-              = link_to s_('Milestones|Close Milestone'), group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
         - if dashboard
           .label-badge.label-badge-gray
             = milestone_type
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 5f53e6316afce9c3d91b52ed6760952e009e5b82..49df00940b7b8a0ba764d3f88bf8d04583e862b0 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,11 +1,9 @@
 - page_title milestone.title
-- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
+- @breadcrumb_link = milestone_path(milestone)
 
 - group = local_assigns[:group]
-- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
 
 = render 'shared/milestones/header', milestone: milestone
-= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
 = render 'shared/milestones/description', milestone: milestone
 
 - if milestone.complete? && milestone.active?
@@ -15,26 +13,3 @@
       = group ? _('You may close the milestone now.') : _('Navigate to the project to close the milestone.')
 
 = render_if_exists 'shared/milestones/burndown', milestone: milestone, project: @project
-
-- if is_dynamic_milestone
-  .table-holder
-    %table.table
-      %thead
-        %tr
-          %th= _('Project')
-          %th= _('Open issues')
-          %th= _('State')
-          %th= _('Due date')
-        %tr
-          %td
-            - project_name = group ? milestone.project.name : milestone.project.full_name
-            = link_to project_name, milestone_path(milestone.milestone)
-          %td
-            = milestone.milestone.issues_visible_to_user(current_user).opened.count
-          %td
-            - if milestone.closed?
-              = _('Closed')
-            - else
-              = _('Open')
-          %td
-            = milestone.expires_at
diff --git a/changelogs/unreleased/39545-cleanup-dynamic-milestone-pages.yml b/changelogs/unreleased/39545-cleanup-dynamic-milestone-pages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e2ab210a3330c13b43855b5324fee749ceca49ac
--- /dev/null
+++ b/changelogs/unreleased/39545-cleanup-dynamic-milestone-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated dashboard & group milestone pages
+merge_request: 13237
+author:
+type: removed
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index f1e8c2b9d8251f9f242d649520d85ab75e46b469..7e29a36f02022e1e978011b04de13d0da637f697 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -5,13 +5,7 @@
   get :activity
 
   scope module: :dashboard do
-    resources :milestones, only: [:index, :show] do
-      member do
-        get :merge_requests
-        get :participants
-        get :labels
-      end
-    end
+    resources :milestones, only: [:index]
     resources :labels, only: [:index]
 
     resources :groups, only: [:index]
diff --git a/ee/app/controllers/ee/groups/milestones_controller.rb b/ee/app/controllers/ee/groups/milestones_controller.rb
index 317491181be16e9e76891b1dde8175a58a9d7881..8496cc132d7138a1aabb4539a924fbd50c7f2605 100644
--- a/ee/app/controllers/ee/groups/milestones_controller.rb
+++ b/ee/app/controllers/ee/groups/milestones_controller.rb
@@ -5,9 +5,9 @@ module Groups
     module MilestonesController
       extend ::Gitlab::Utils::Override
 
-      override :legacy_milestones
-      def legacy_milestones
-        params[:only_group_milestones] ? [] : super
+      override :search_params
+      def search_params
+        params[:only_group_milestones].present? ? super.merge(project_ids: []) : super
       end
     end
   end
diff --git a/ee/app/models/ee/global_milestone.rb b/ee/app/models/ee/global_milestone.rb
deleted file mode 100644
index a98b75bf6b4f58c9b1596ce6f28597c923626eb2..0000000000000000000000000000000000000000
--- a/ee/app/models/ee/global_milestone.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module GlobalMilestone
-    def supports_weight?
-      false
-    end
-
-    # Legacy group milestones or dashboard milestones (grouped by title)
-    # can't present Burndown charts since they don't have
-    # proper limits set.
-    def supports_burndown_charts?
-      false
-    end
-  end
-end
diff --git a/ee/app/models/ee/group_milestone.rb b/ee/app/models/ee/group_milestone.rb
deleted file mode 100644
index c74f4129b7e9bf7cd334f376755c7da11920ea91..0000000000000000000000000000000000000000
--- a/ee/app/models/ee/group_milestone.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module GroupMilestone
-    def supports_weight?
-      group&.feature_available?(:issue_weights)
-    end
-  end
-end
diff --git a/ee/spec/features/burndown_charts_spec.rb b/ee/spec/features/burndown_charts_spec.rb
index 62e5f636aad1d515466db352ace89358e385c408..82c3c9649d80829f068e14d19e01b975c1fb9198 100644
--- a/ee/spec/features/burndown_charts_spec.rb
+++ b/ee/spec/features/burndown_charts_spec.rb
@@ -70,23 +70,4 @@
       expect(page).to have_content('Improve milestones with Burndown Charts')
     end
   end
-
-  describe 'grouped by title milestones' do
-    let(:group) { nil }
-    let(:project) { create(:project) }
-
-    before do
-      project.add_maintainer(current_user)
-    end
-
-    it 'does not present burndown chart or promotion' do
-      allow(License).to receive(:current) { nil }
-      allow(Gitlab::CurrentSettings).to receive(:should_check_namespace_plan?) { true }
-
-      visit dashboard_milestone_path(milestone.safe_title, title: milestone.title)
-
-      expect(page).not_to have_css('.burndown-chart')
-      expect(page).not_to have_content('Improve milestones with Burndown Charts')
-    end
-  end
 end
diff --git a/lib/milestone_array.rb b/lib/milestone_array.rb
deleted file mode 100644
index 461e73e96703b6af602007503c606f051b368fb7..0000000000000000000000000000000000000000
--- a/lib/milestone_array.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module MilestoneArray
-  class << self
-    def sort(array, sort_method)
-      case sort_method
-      when 'due_date_asc'
-        sort_asc_nulls_last(array, 'due_date')
-      when 'due_date_desc'
-        sort_desc_nulls_last(array, 'due_date')
-      when 'start_date_asc'
-        sort_asc_nulls_last(array, 'start_date')
-      when 'start_date_desc'
-        sort_desc_nulls_last(array, 'start_date')
-      when 'name_asc'
-        sort_asc(array, 'title')
-      when 'name_desc'
-        sort_asc(array, 'title').reverse
-      else
-        array
-      end
-    end
-
-    private
-
-    def sort_asc_nulls_last(array, attribute)
-      attribute = attribute.to_sym
-
-      array.select(&attribute).sort_by(&attribute) + array.reject(&attribute)
-    end
-
-    def sort_desc_nulls_last(array, attribute)
-      attribute = attribute.to_sym
-
-      array.select(&attribute).sort_by(&attribute).reverse + array.reject(&attribute)
-    end
-
-    def sort_asc(array, attribute)
-      array.sort_by(&attribute.to_sym)
-    end
-  end
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e8191e1cb4fc9f6e2682624c7abb7674d7fa4780..f06836e6e8c7d022aeea655ef5acdb4427de2176 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -20730,9 +20730,6 @@ msgstr ""
 msgid "Starts at (UTC)"
 msgstr ""
 
-msgid "State"
-msgstr ""
-
 msgid "State your message to activate"
 msgstr ""
 
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index f4b04ad6dee3bf2fe4967883c64b5d4075c625d3..36c1e2d716900034e78504601d80db62c89fea64 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -8,12 +8,7 @@
   let(:user) { create(:user) }
   let(:project_milestone) { create(:milestone, project: project) }
   let(:group_milestone) { create(:milestone, group: group) }
-  let(:milestone) do
-    DashboardMilestone.build(
-      [project],
-      project_milestone.title
-    )
-  end
+  let(:milestone) { create(:milestone, group: group) }
   let(:issue) { create(:issue, project: project, milestone: project_milestone) }
   let(:group_issue) { create(:issue, milestone: group_milestone, project: create(:project, group: group)) }
 
@@ -28,22 +23,6 @@
     group.add_developer(user)
   end
 
-  it_behaves_like 'milestone tabs'
-
-  describe "#show" do
-    render_views
-
-    def view_milestone
-      get :show, params: { id: milestone.safe_title, title: milestone.title }
-    end
-
-    it 'shows milestone page' do
-      view_milestone
-
-      expect(response).to have_gitlab_http_status(:ok)
-    end
-  end
-
   describe "#index" do
     let(:public_group) { create(:group, :public) }
     let!(:public_milestone) { create(:milestone, group: public_group) }
@@ -58,7 +37,6 @@ def view_milestone
       expect(response).to have_gitlab_http_status(:ok)
       expect(json_response.size).to eq(2)
       expect(json_response.map { |i| i["name"] }).to match_array([group_milestone.name, project_milestone.name])
-      expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
     end
 
     it 'returns closed group and project milestones to which the user belongs' do
@@ -67,7 +45,6 @@ def view_milestone
       expect(response).to have_gitlab_http_status(:ok)
       expect(json_response.size).to eq(2)
       expect(json_response.map { |i| i["name"] }).to match_array([closed_group_milestone.name, closed_project_milestone.name])
-      expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
     end
 
     it 'searches legacy project milestones by title when search_title is given' do
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index afb950bc5389934413be64444acc57c165980d5a..fa297fa2b5ec65bfc152ff337b0c723760e8ad1b 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -8,15 +8,7 @@
   let!(:project2) { create(:project, group: group) }
   let(:user)    { create(:user) }
   let(:title) { '肯定不是中文的问题' }
-  let(:milestone) do
-    project_milestone = create(:milestone, project: project)
-
-    GroupMilestone.build(
-      group,
-      [project],
-      project_milestone.title
-    )
-  end
+  let(:milestone) { create(:milestone, project: project) }
   let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) }
 
   let(:milestone_params) do
@@ -168,17 +160,16 @@
 
     context 'as JSON' do
       let!(:milestone) { create(:milestone, group: group, title: 'group milestone') }
-      let!(:legacy_milestone1) { create(:milestone, project: project, title: 'legacy') }
-      let!(:legacy_milestone2) { create(:milestone, project: project2, title: 'legacy') }
+      let!(:project_milestone1) { create(:milestone, project: project, title: 'same name') }
+      let!(:project_milestone2) { create(:milestone, project: project2, title: 'same name') }
 
-      it 'lists legacy group milestones and group milestones' do
+      it 'lists project and group milestones' do
         get :index, params: { group_id: group.to_param }, format: :json
 
         milestones = json_response
 
-        expect(milestones.count).to eq(2)
-        expect(milestones.first["title"]).to eq("group milestone")
-        expect(milestones.second["title"]).to eq("legacy")
+        expect(milestones.count).to eq(3)
+        expect(milestones.collect { |m| m['title'] }).to match_array(['same name', 'same name', 'group milestone'])
         expect(response).to have_gitlab_http_status(:ok)
         expect(response.content_type).to eq 'application/json'
       end
@@ -191,8 +182,9 @@
           get :index, params: { group_id: group.to_param }, format: :json
           milestones = json_response
 
-          expect(milestones.count).to eq(3)
-          expect(milestones.second["title"]).to eq("subgroup milestone")
+          milestone_titles = milestones.map { |m| m['title'] }
+          expect(milestones.count).to eq(4)
+          expect(milestone_titles).to match_array(['same name', 'same name', 'group milestone', 'subgroup milestone'])
         end
       end
 
@@ -218,31 +210,18 @@
   end
 
   describe '#show' do
-    let(:milestone1) { create(:milestone, project: project, title: 'legacy') }
-    let(:milestone2) { create(:milestone, project: project, title: 'legacy') }
-    let(:group_milestone) { create(:milestone, group: group) }
+    render_views
 
-    context 'when there is a title parameter' do
-      it 'searches for a legacy group milestone' do
-        expect(GroupMilestone).to receive(:build)
-        expect(Milestone).not_to receive(:find_by_iid)
+    let!(:group_milestone) { create(:milestone, group: group) }
 
-        get :show, params: { group_id: group.to_param, id: title, title: milestone1.safe_title }
-      end
-    end
+    it 'renders for a group milestone' do
+      get :show, params: { group_id: group.to_param, id: group_milestone.iid }
 
-    context 'when there is not a title parameter' do
-      it 'searches for a group milestone' do
-        expect(GlobalMilestone).not_to receive(:build)
-        expect(Milestone).to receive(:find_by_iid)
-
-        get :show, params: { group_id: group.to_param, id: group_milestone.id }
-      end
+      expect(response).to have_gitlab_http_status(:ok)
+      expect(response.body).to include(group_milestone.title)
     end
   end
 
-  it_behaves_like 'milestone tabs'
-
   describe "#create" do
     it "creates group milestone with Chinese title" do
       post :create,
@@ -277,34 +256,6 @@
       expect(response).to redirect_to(group_milestone_path(group, milestone.iid))
       expect(milestone.title).to eq("title changed")
     end
-
-    context "legacy group milestones" do
-      let!(:milestone1) { create(:milestone, project: project, title: 'legacy milestone', description: "old description") }
-      let!(:milestone2) { create(:milestone, project: project2, title: 'legacy milestone', description: "old description") }
-
-      it "updates only group milestones state" do
-        milestone_params[:title] = "title changed"
-        milestone_params[:description] = "description changed"
-        milestone_params[:state_event] = "close"
-
-        put :update,
-             params: {
-               id: milestone1.title.to_slug.to_s,
-               group_id: group.to_param,
-               milestone: milestone_params,
-               title: milestone1.title
-             }
-
-        expect(response).to redirect_to(group_milestone_path(group, milestone1.safe_title, title: milestone1.title))
-
-        [milestone1, milestone2].each do |milestone|
-          milestone.reload
-          expect(milestone.title).to eq("legacy milestone")
-          expect(milestone.description).to eq("old description")
-          expect(milestone.state).to eq("closed")
-        end
-      end
-    end
   end
 
   describe "#destroy" do
diff --git a/spec/features/dashboard/milestone_tabs_spec.rb b/spec/features/dashboard/milestone_tabs_spec.rb
deleted file mode 100644
index a83e4c1f7c9cfdeeab8056409696ae49cf304f54..0000000000000000000000000000000000000000
--- a/spec/features/dashboard/milestone_tabs_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe 'Dashboard milestone tabs', :js do
-  let(:user) { create(:user) }
-  let(:project) { create(:project) }
-  let!(:label) { create(:label, project: project) }
-  let(:project_milestone) { create(:milestone, project: project) }
-  let(:milestone) do
-    DashboardMilestone.build(
-      [project],
-      project_milestone.title
-    )
-  end
-  let!(:merge_request) { create(:labeled_merge_request, source_project: project, target_project: project, milestone: project_milestone, labels: [label]) }
-
-  before do
-    project.add_maintainer(user)
-    sign_in(user)
-
-    visit dashboard_milestone_path(milestone.safe_title, title: milestone.title)
-  end
-
-  it 'loads merge requests async' do
-    click_link 'Merge Requests'
-
-    expect(page).to have_selector('.milestone-merge_requests-list')
-  end
-
-  it 'loads participants async' do
-    click_link 'Participants'
-
-    expect(page).to have_selector('#tab-participants .bordered-list')
-  end
-
-  it 'loads labels async' do
-    click_link 'Labels'
-
-    expect(page).to have_selector('#tab-labels .bordered-list')
-  end
-end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 65ef0af5be3e690bcda10ffc611bd3f78fa9ddcf..b5790c403557d947c0a1539af16cd65d2d7cfcc4 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -102,11 +102,9 @@
         expect(find('.top-area .all .badge').text).to eq("6")
       end
 
-      it 'lists legacy group milestones and group milestones' do
-        legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first
-
+      it 'lists group and project milestones' do
         expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1)
-        expect(page).to have_selector("#milestone_#{legacy_milestone.milestone.id}", count: 1)
+        expect(page).to have_selector("#milestone_#{active_project_milestone2.id}", count: 1)
       end
 
       it 'shows milestone detail and supports its edit' do
@@ -125,75 +123,35 @@
         expect(page).to have_content('v1.0')
         expect(page).to have_content('v1.1')
         expect(page).to have_content('GL-113')
+        expect(page).to have_link(
+          'v1.0',
+          href: project_milestone_path(project, active_project_milestone1)
+        )
         expect(page).to have_link(
           '1 Issue',
-          href: issues_group_path(group, milestone_title: 'v1.0')
+          href: project_issues_path(project, milestone_title: 'v1.0')
         )
         expect(page).to have_link(
           '0 Merge Requests',
-          href: merge_requests_group_path(group, milestone_title: 'v1.0')
+          href: project_merge_requests_path(project, milestone_title: 'v1.0')
+        )
+        expect(page).to have_link(
+          'GL-113',
+          href: group_milestone_path(group, active_group_milestone)
+        )
+        expect(page).to have_link(
+          '0 Issues',
+          href: issues_group_path(group, milestone_title: 'GL-113')
+        )
+        expect(page).to have_link(
+          '0 Merge Requests',
+          href: merge_requests_group_path(group, milestone_title: 'GL-113')
         )
-      end
-
-      it 'renders group milestone details' do
-        click_link 'v1.0'
-
-        expect(page).to have_content('expires on Aug 20, 2114')
-        expect(page).to have_content('v1.0')
-        expect(page).to have_content('Issues 1 Open: 1 Closed: 0')
-        expect(page).to have_link(issue.title, href: project_issue_path(issue.project, issue))
       end
     end
   end
 
   describe 'milestone tabs', :js do
-    context 'for a legacy group milestone' do
-      let_it_be(:milestone) { create(:milestone, project: project) }
-      let_it_be(:label)     { create(:label, project: project) }
-      let_it_be(:issue)     { create(:labeled_issue, project: project, milestone: milestone, labels: [label], assignees: [create(:user)]) }
-      let_it_be(:mr)        { create(:merge_request, source_project: project, milestone: milestone) }
-
-      before do
-        visit group_milestone_path(group, milestone.title, title: milestone.title)
-      end
-
-      it 'renders the issues tab' do
-        within('#tab-issues') do
-          expect(page).to have_content issue.title
-        end
-      end
-
-      it 'renders the merge requests tab' do
-        within('.js-milestone-tabs') do
-          click_link('Merge Requests')
-        end
-
-        within('#tab-merge-requests') do
-          expect(page).to have_content mr.title
-        end
-      end
-
-      it 'renders the participants tab' do
-        within('.js-milestone-tabs') do
-          click_link('Participants')
-        end
-
-        within('#tab-participants') do
-          expect(page).to have_content issue.assignees.first.name
-        end
-      end
-
-      it 'renders the labels tab' do
-        within('.js-milestone-tabs') do
-          click_link('Labels')
-        end
-
-        within('#tab-labels') do
-          expect(page).to have_content label.title
-        end
-      end
-    end
-
     context 'for a group milestone' do
       let_it_be(:other_project) { create(:project_empty_repo, group: group) }
       let_it_be(:milestone) { create(:milestone, group: group) }
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index d27511be0b0cc6a99ce65bd0455a7e6f0f57887b..9ebb04e6f97861de5351b9a4aca678e23270f942 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -24,21 +24,12 @@
 
     # assert default sorting
     within '.milestones' do
-      expect(page.all('ul.content-list > li').first.text).to include('v2.0')
-      expect(page.all('ul.content-list > li')[1].text).to include('v3.0')
-      expect(page.all('ul.content-list > li').last.text).to include('v1.0')
+      expect(page.all('ul.content-list > li strong > a').map(&:text)).to eq(['v2.0', 'v2.0', 'v3.0', 'v1.0', 'v1.0'])
     end
 
     click_button 'Due soon'
 
-    sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text)
-
-    expect(sort_options[0]).to eq('Due soon')
-    expect(sort_options[1]).to eq('Due later')
-    expect(sort_options[2]).to eq('Start soon')
-    expect(sort_options[3]).to eq('Start later')
-    expect(sort_options[4]).to eq('Name, ascending')
-    expect(sort_options[5]).to eq('Name, descending')
+    expect(find('ul.dropdown-menu-sort li').all('a').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
 
     click_link 'Due later'
 
@@ -46,9 +37,7 @@
 
     # assert descending sorting
     within '.milestones' do
-      expect(page.all('ul.content-list > li').first.text).to include('v1.0')
-      expect(page.all('ul.content-list > li')[1].text).to include('v3.0')
-      expect(page.all('ul.content-list > li').last.text).to include('v2.0')
+      expect(page.all('ul.content-list > li strong > a').map(&:text)).to eq(['v1.0', 'v1.0', 'v3.0', 'v2.0', 'v2.0'])
     end
   end
 end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index bfff33f39566743fea83221aa3002e42390ff730..6956a7e18f6c1acfe8c74a76e5877f99731440a9 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -111,20 +111,6 @@
     end
   end
 
-  describe 'deprecation popover', :js do
-    it 'opens deprecation popover' do
-      milestone = create(:milestone, project: project)
-
-      visit group_milestone_path(group, milestone, title: milestone.title)
-
-      expect(page).to have_selector('.milestone-deprecation-message')
-
-      find('.milestone-deprecation-message .js-popover-link').click
-
-      expect(page).to have_selector('.popover')
-    end
-  end
-
   describe 'reopen closed milestones' do
     before do
       create(:milestone, :closed, project: project)
diff --git a/spec/lib/milestone_array_spec.rb b/spec/lib/milestone_array_spec.rb
deleted file mode 100644
index 375cb87dde63ee1ddb8cdfb3c36a201a1812406f..0000000000000000000000000000000000000000
--- a/spec/lib/milestone_array_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe MilestoneArray do
-  let(:object1) { instance_double("BirdMilestone", due_date: Time.now, start_date: Time.now - 15.days, title: 'v2.0') }
-  let(:object2) { instance_double("CatMilestone", due_date: Time.now - 1.day, start_date: nil, title: 'v1.0') }
-  let(:object3) { instance_double("DogMilestone", due_date: nil, start_date: Time.now - 30.days, title: 'v3.0') }
-  let(:array) { [object1, object3, object2] }
-
-  describe '#sort' do
-    it 'reorders array with due date in ascending order with nulls last' do
-      expect(described_class.sort(array, 'due_date_asc')).to eq([object2, object1, object3])
-    end
-
-    it 'reorders array with due date in desc order with nulls last' do
-      expect(described_class.sort(array, 'due_date_desc')).to eq([object1, object2, object3])
-    end
-
-    it 'reorders array with start date in ascending order with nulls last' do
-      expect(described_class.sort(array, 'start_date_asc')).to eq([object3, object1, object2])
-    end
-
-    it 'reorders array with start date in descending order with nulls last' do
-      expect(described_class.sort(array, 'start_date_desc')).to eq([object1, object3, object2])
-    end
-
-    it 'reorders array with title in ascending order' do
-      expect(described_class.sort(array, 'name_asc')).to eq([object2, object1, object3])
-    end
-
-    it 'reorders array with title in descending order' do
-      expect(described_class.sort(array, 'name_desc')).to eq([object3, object1, object2])
-    end
-  end
-end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
deleted file mode 100644
index 34dbdfec60d6580a4592e377e5b0774478b6ce6d..0000000000000000000000000000000000000000
--- a/spec/models/global_milestone_spec.rb
+++ /dev/null
@@ -1,208 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe GlobalMilestone do
-  let(:user) { create(:user) }
-  let(:user2) { create(:user) }
-  let(:group) { create(:group) }
-  let(:project1) { create(:project, group: group) }
-  let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
-  let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
-
-  describe '.build_collection' do
-    let(:milestone1_due_date) { 2.weeks.from_now.to_date }
-
-    let!(:milestone1_project1) do
-      create(
-        :milestone,
-        title: "Milestone v1.2",
-        project: project1,
-        due_date: milestone1_due_date
-      )
-    end
-
-    let!(:milestone1_project2) do
-      create(
-        :milestone,
-        title: "Milestone v1.2",
-        project: project2,
-        due_date: milestone1_due_date
-      )
-    end
-
-    let!(:milestone1_project3) do
-      create(
-        :milestone,
-        title: "Milestone v1.2",
-        project: project3,
-        due_date: milestone1_due_date
-      )
-    end
-
-    let!(:milestone2_project1) do
-      create(
-        :milestone,
-        title: "VD-123",
-        project: project1,
-        due_date: nil
-      )
-    end
-
-    let!(:milestone2_project2) do
-      create(
-        :milestone,
-        title: "VD-123",
-        project: project2,
-        due_date: nil
-      )
-    end
-
-    let!(:milestone2_project3) do
-      create(
-        :milestone,
-        title: "VD-123",
-        project: project3,
-        due_date: nil
-      )
-    end
-
-    let!(:projects) do
-      [
-        project1,
-        project2,
-        project3
-      ]
-    end
-
-    let!(:global_milestones) { described_class.build_collection(projects, {}) }
-
-    context 'when building a collection of milestones' do
-      it 'has all project milestones' do
-        expect(global_milestones.count).to eq(6)
-      end
-
-      it 'has all project milestones titles' do
-        expect(global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'Milestone v1.2', 'Milestone v1.2', 'VD-123', 'VD-123', 'VD-123'])
-      end
-
-      it 'has all project milestones' do
-        expect(global_milestones.size).to eq(6)
-      end
-
-      it 'sorts collection by due date' do
-        expect(global_milestones.map(&:due_date)).to eq [milestone1_due_date, milestone1_due_date, milestone1_due_date, nil, nil, nil]
-      end
-
-      it 'filters milestones by search_title when params[:search_title] is present' do
-        global_milestones = described_class.build_collection(projects, { search_title: 'v1.2' })
-
-        expect(global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'Milestone v1.2', 'Milestone v1.2'])
-      end
-    end
-
-    context 'when adding new milestones' do
-      it 'does not add more queries' do
-        control_count = ActiveRecord::QueryRecorder.new do
-          described_class.build_collection(projects, {})
-        end.count
-
-        create_list(:milestone, 3, project: project3)
-
-        expect do
-          described_class.build_collection(projects, {})
-        end.not_to exceed_all_query_limit(control_count)
-      end
-    end
-  end
-
-  describe '.states_count' do
-    context 'when the projects have milestones' do
-      before do
-        create(:closed_milestone, title: 'Active Group Milestone', project: project3)
-        create(:active_milestone, title: 'Active Group Milestone', project: project1)
-        create(:active_milestone, title: 'Active Group Milestone', project: project2)
-        create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
-        create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
-        create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
-        create(:closed_milestone, title: 'Closed Group Milestone 4', group: group)
-      end
-
-      it 'returns the quantity of global milestones and group milestones in each possible state' do
-        expected_count = { opened: 2, closed: 5, all: 7 }
-
-        count = described_class.states_count(Project.all, group)
-
-        expect(count).to eq(expected_count)
-      end
-
-      it 'returns the quantity of global milestones in each possible state' do
-        expected_count = { opened: 2, closed: 4, all: 6 }
-
-        count = described_class.states_count(Project.all)
-
-        expect(count).to eq(expected_count)
-      end
-    end
-
-    context 'when the projects do not have milestones' do
-      before do
-        project1
-      end
-
-      it 'returns 0 as the quantity of global milestones in each state' do
-        expected_count = { opened: 0, closed: 0, all: 0 }
-
-        count = described_class.states_count(Project.all)
-
-        expect(count).to eq(expected_count)
-      end
-    end
-  end
-
-  describe '#initialize' do
-    let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
-
-    subject(:global_milestone) { described_class.new(milestone1_project1) }
-
-    it 'has exactly one group milestone' do
-      expect(global_milestone.title).to eq('Milestone v1.2')
-    end
-
-    it 'has all project milestones with the same title' do
-      expect(global_milestone.milestone).to eq(milestone1_project1)
-    end
-  end
-
-  describe '#safe_title' do
-    let(:milestone) { create(:milestone, title: "git / test", project: project1) }
-
-    it 'strips out slashes and spaces' do
-      global_milestone = described_class.new(milestone)
-
-      expect(global_milestone.safe_title).to eq('git-test')
-    end
-  end
-
-  describe '#state' do
-    context 'when at least one milestone is active' do
-      it 'returns active' do
-        title = 'Active Group Milestone'
-
-        global_milestone = described_class.new(create(:active_milestone, title: title))
-
-        expect(global_milestone.state).to eq('active')
-      end
-    end
-
-    context 'when all milestones are closed' do
-      it 'returns closed' do
-        title = 'Closed Group Milestone'
-
-        global_milestone = described_class.new(create(:closed_milestone, title: title))
-
-        expect(global_milestone.state).to eq('closed')
-      end
-    end
-  end
-end
diff --git a/spec/models/group_milestone_spec.rb b/spec/models/group_milestone_spec.rb
deleted file mode 100644
index 01856870fe046a8de315e19399c794157e53fc7c..0000000000000000000000000000000000000000
--- a/spec/models/group_milestone_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe GroupMilestone do
-  let(:group) { create(:group) }
-  let(:project) { create(:project, group: group) }
-  let(:project_milestone) do
-    create(:milestone, title: "Milestone v1.2", project: project)
-  end
-
-  describe '.build' do
-    it 'returns milestone with group assigned' do
-      milestone = described_class.build(
-        group,
-        [project],
-        project_milestone.title
-      )
-
-      expect(milestone.group).to eq group
-    end
-  end
-
-  describe '.build_collection' do
-    let(:group) { create(:group) }
-    let(:project1) { create(:project, group: group) }
-    let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
-    let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
-
-    let!(:projects) do
-      [
-          project1,
-          project2,
-          project3
-      ]
-    end
-
-    it 'returns array of milestones, each with group assigned' do
-      milestones = described_class.build_collection(group, [project], {})
-      expect(milestones).to all(have_attributes(group: group))
-    end
-
-    context 'when adding new milestones' do
-      it 'does not add more queries' do
-        control_count = ActiveRecord::QueryRecorder.new do
-          described_class.build_collection(group, projects, {})
-        end.count
-
-        create(:milestone, title: 'This title', project: project1)
-
-        expect do
-          described_class.build_collection(group, projects, {})
-        end.not_to exceed_all_query_limit(control_count)
-      end
-    end
-  end
-end
diff --git a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
index d9656824452b13453e0619ec4cd6b2c7d4a8b31d..925c45005f07ffc6060000a0ffca78790694a820 100644
--- a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
@@ -2,15 +2,7 @@
 
 RSpec.shared_examples 'milestone tabs' do
   def go(path, extra_params = {})
-    params =
-      case milestone
-      when DashboardMilestone
-        { id: milestone.safe_title, title: milestone.title }
-      when GroupMilestone
-        { group_id: group.to_param, id: milestone.safe_title, title: milestone.title }
-      else
-        { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
-      end
+    params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
 
     get path, params: params.merge(extra_params)
   end
diff --git a/spec/views/shared/milestones/_top.html.haml_spec.rb b/spec/views/shared/milestones/_top.html.haml_spec.rb
index 1e209ad6f3ff0f61cd3b7474372ad8b63f893f5a..2d72e27870644928ba5d637040ce1fd8a2604ec7 100644
--- a/spec/views/shared/milestones/_top.html.haml_spec.rb
+++ b/spec/views/shared/milestones/_top.html.haml_spec.rb
@@ -12,22 +12,6 @@
     allow(milestone).to receive(:milestone) { milestone }
   end
 
-  it 'renders a deprecation message for a legacy milestone' do
-    allow(milestone).to receive(:legacy_group_milestone?) { true }
-
-    render 'shared/milestones/top', milestone: milestone
-
-    expect(rendered).to have_css('.milestone-deprecation-message')
-  end
-
-  it 'renders a deprecation message for a dashboard milestone' do
-    allow(milestone).to receive(:dashboard_milestone?) { true }
-
-    render 'shared/milestones/top', milestone: milestone
-
-    expect(rendered).to have_css('.milestone-deprecation-message')
-  end
-
   it 'does not render a deprecation message for a non-legacy and non-dashboard milestone' do
     assign :group, group