diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 4aa572ade7317aacec85cad377a7bfff6192462e..d8812c023ca75c100cb74dc687d6355590ffb2e5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -13,6 +13,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
   before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
     push_frontend_feature_flag(:metrics_time_window)
     push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
+    push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
   end
 
   def index
@@ -158,15 +159,28 @@ def additional_metrics
   end
 
   def metrics_dashboard
-    return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, @project)
+    return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, project)
 
-    result = Gitlab::Metrics::Dashboard::Service.new(@project, @current_user, environment: environment).get_dashboard
+    if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
+      result = dashboard_finder.find(project, current_user, environment, params[:dashboard])
+
+      result[:all_dashboards] = project.repository.metrics_dashboard_paths
+    else
+      result = dashboard_finder.find(project, current_user, environment)
+    end
 
     respond_to do |format|
       if result[:status] == :success
-        format.json { render status: :ok, json: result }
+        format.json do
+          render status: :ok, json: result.slice(:all_dashboards, :dashboard, :status)
+        end
       else
-        format.json { render status: result[:http_status], json: result }
+        format.json do
+          render(
+            status: result[:http_status],
+            json: result.slice(:all_dashboards, :message, :status)
+          )
+        end
       end
     end
   end
@@ -211,6 +225,10 @@ def metrics_params
     params.require([:start, :end])
   end
 
+  def dashboard_finder
+    Gitlab::Metrics::Dashboard::Finder
+  end
+
   def search_environment_names
     return [] unless params[:query]
 
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 8b728c4f6b232473f2847f86de74efaccd9dd095..f495a03ad8ed056892eb13603bbe029fa5e5ae7e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -39,7 +39,8 @@ class Repository
                       changelog license_blob license_key gitignore
                       gitlab_ci_yml branch_names tag_names branch_count
                       tag_count avatar exists? root_ref has_visible_content?
-                      issue_template_names merge_request_template_names xcode_project?).freeze
+                      issue_template_names merge_request_template_names
+                      metrics_dashboard_paths xcode_project?).freeze
 
   # Methods that use cache_method but only memoize the value
   MEMOIZED_CACHED_METHODS = %i(license).freeze
@@ -57,6 +58,7 @@ class Repository
     avatar: :avatar,
     issue_template: :issue_template_names,
     merge_request_template: :merge_request_template_names,
+    metrics_dashboard: :metrics_dashboard_paths,
     xcode_config: :xcode_project?
   }.freeze
 
@@ -602,6 +604,11 @@ def merge_request_template_names
   end
   cache_method :merge_request_template_names, fallback: []
 
+  def metrics_dashboard_paths
+    Gitlab::Metrics::Dashboard::Finder.find_all_paths_from_source(project)
+  end
+  cache_method :metrics_dashboard_paths
+
   def readme
     head_tree&.readme
   end
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 2770469ca9f040d78879d8c20bbeae82d79d8203..9fc2217ad433257470e66bdd82de981cf8f4c19f 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -16,6 +16,7 @@ module FileDetector
       avatar: /\Alogo\.(png|jpg|gif)\z/,
       issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
       merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
+      metrics_dashboard: %r{\A\.gitlab/dashboards/[^/]+\.yml\z},
       xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
 
       # Configuration files
diff --git a/lib/gitlab/metrics/dashboard/base_service.rb b/lib/gitlab/metrics/dashboard/base_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94aabd0466c48397832b82b4d015e01e15beb994
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/base_service.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+module Gitlab
+  module Metrics
+    module Dashboard
+      class BaseService < ::BaseService
+        DASHBOARD_LAYOUT_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+        def get_dashboard
+          return error("#{dashboard_path} could not be found.", :not_found) unless path_available?
+
+          success(dashboard: process_dashboard)
+        rescue DASHBOARD_LAYOUT_ERROR => e
+          error(e.message, :unprocessable_entity)
+        end
+
+        # Summary of all known dashboards for the service.
+        # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+        def all_dashboard_paths(_project)
+          raise NotImplementedError
+        end
+
+        private
+
+        # Returns a new dashboard Hash, supplemented with DB info
+        def process_dashboard
+          Gitlab::Metrics::Dashboard::Processor
+            .new(project, params[:environment], raw_dashboard)
+            .process(insert_project_metrics: insert_project_metrics?)
+        end
+
+        # @return [String] Relative filepath of the dashboard yml
+        def dashboard_path
+          params[:dashboard_path]
+        end
+
+        # Returns an un-processed dashboard from the cache.
+        def raw_dashboard
+          Rails.cache.fetch(cache_key) { get_raw_dashboard }
+        end
+
+        # @return [Hash] an unmodified dashboard
+        def get_raw_dashboard
+          raise NotImplementedError
+        end
+
+        # @return [String]
+        def cache_key
+          raise NotImplementedError
+        end
+
+        # Determines whether custom metrics should be included
+        # in the processed output.
+        def insert_project_metrics?
+          false
+        end
+
+        # Checks if dashboard path exists or should be rejected
+        # as a result of file-changes to the project repository.
+        # @return [Boolean]
+        def path_available?
+          available_paths = Gitlab::Metrics::Dashboard::Finder.find_all_paths(project)
+
+          available_paths.any? do |path_params|
+            path_params[:path] == dashboard_path
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4a41590f0008f1fef1cb7ed66f11997c6126934f
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Returns DB-supplmented dashboard info for determining
+# the layout of UI. Intended entry-point for the Metrics::Dashboard
+# module.
+module Gitlab
+  module Metrics
+    module Dashboard
+      class Finder
+        class << self
+          # Returns a formatted dashboard packed with DB info.
+          # @return [Hash]
+          def find(project, user, environment, dashboard_path = nil)
+            service = system_dashboard?(dashboard_path) ? system_service : project_service
+
+            service
+              .new(project, user, environment: environment, dashboard_path: dashboard_path)
+              .get_dashboard
+          end
+
+          # Summary of all known dashboards.
+          # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+          def find_all_paths(project)
+            project.repository.metrics_dashboard_paths
+          end
+
+          # Summary of all known dashboards. Used to populate repo cache.
+          # Prefer #find_all_paths.
+          def find_all_paths_from_source(project)
+            system_service.all_dashboard_paths(project)
+            .+ project_service.all_dashboard_paths(project)
+          end
+
+          private
+
+          def system_service
+            Gitlab::Metrics::Dashboard::SystemDashboardService
+          end
+
+          def project_service
+            Gitlab::Metrics::Dashboard::ProjectDashboardService
+          end
+
+          def system_dashboard?(filepath)
+            !filepath || system_service.system_dashboard?(filepath)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index cc34ac53051519b91bf29f4f15c37dac6c08b1e3..dd9860206937d49976db80f095dea934705070ec 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -8,12 +8,17 @@ module Dashboard
       # the UI. These includes shared metric info, custom metrics
       # info, and alerts (only in EE).
       class Processor
-        SEQUENCE = [
+        SYSTEM_SEQUENCE = [
           Stages::CommonMetricsInserter,
           Stages::ProjectMetricsInserter,
           Stages::Sorter
         ].freeze
 
+        PROJECT_SEQUENCE = [
+          Stages::CommonMetricsInserter,
+          Stages::Sorter
+        ].freeze
+
         def initialize(project, environment, dashboard)
           @project = project
           @environment = environment
@@ -22,9 +27,9 @@ def initialize(project, environment, dashboard)
 
         # Returns a new dashboard hash with the results of
         # running transforms on the dashboard.
-        def process
+        def process(insert_project_metrics:)
           @dashboard.deep_symbolize_keys.tap do |dashboard|
-            sequence.each do |stage|
+            sequence(insert_project_metrics).each do |stage|
               stage.new(@project, @environment, dashboard).transform!
             end
           end
@@ -32,8 +37,8 @@ def process
 
         private
 
-        def sequence
-          SEQUENCE
+        def sequence(insert_project_metrics)
+          insert_project_metrics ? SYSTEM_SEQUENCE : PROJECT_SEQUENCE
         end
       end
     end
diff --git a/lib/gitlab/metrics/dashboard/project_dashboard_service.rb b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fdffd067c933c7f7114348b4569d67b0bbeca396
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+  module Metrics
+    module Dashboard
+      class ProjectDashboardService < Gitlab::Metrics::Dashboard::BaseService
+        DASHBOARD_ROOT = ".gitlab/dashboards"
+
+        class << self
+          def all_dashboard_paths(project)
+            file_finder(project)
+              .list_files_for(DASHBOARD_ROOT)
+              .map do |filepath|
+                Rails.cache.delete(cache_key(project.id, filepath))
+
+                { path: filepath, default: false }
+              end
+          end
+
+          def file_finder(project)
+            Gitlab::Template::Finders::RepoTemplateFinder.new(project, DASHBOARD_ROOT, '.yml')
+          end
+
+          def cache_key(id, dashboard_path)
+            "project_#{id}_metrics_dashboard_#{dashboard_path}"
+          end
+        end
+
+        private
+
+        # Searches the project repo for a custom-defined dashboard.
+        def get_raw_dashboard
+          yml = self.class.file_finder(project).read(dashboard_path)
+
+          YAML.safe_load(yml)
+        end
+
+        def cache_key
+          self.class.cache_key(project.id, dashboard_path)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/metrics/dashboard/service.rb b/lib/gitlab/metrics/dashboard/service.rb
deleted file mode 100644
index 79d563cce4fc00c846333ce8968e4302694aa8cf..0000000000000000000000000000000000000000
--- a/lib/gitlab/metrics/dashboard/service.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-# Fetches the metrics dashboard layout and supplemented the output with DB info.
-module Gitlab
-  module Metrics
-    module Dashboard
-      class Service < ::BaseService
-        SYSTEM_DASHBOARD_NAME = 'common_metrics'
-        SYSTEM_DASHBOARD_PATH = Rails.root.join('config', 'prometheus', "#{SYSTEM_DASHBOARD_NAME}.yml")
-
-        # Returns a DB-supplemented json representation of a dashboard config file.
-        def get_dashboard
-          dashboard_string = Rails.cache.fetch(cache_key) { system_dashboard }
-
-          dashboard = process_dashboard(dashboard_string)
-
-          success(dashboard: dashboard)
-        rescue Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError => e
-          error(e.message, :unprocessable_entity)
-        end
-
-        private
-
-        # Returns the base metrics shipped with every GitLab service.
-        def system_dashboard
-          YAML.safe_load(File.read(SYSTEM_DASHBOARD_PATH))
-        end
-
-        def cache_key
-          "metrics_dashboard_#{SYSTEM_DASHBOARD_NAME}"
-        end
-
-        # Returns a new dashboard Hash, supplemented with DB info
-        def process_dashboard(dashboard)
-          Gitlab::Metrics::Dashboard::Processor.new(project, params[:environment], dashboard).process
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/metrics/dashboard/stages/base_stage.rb b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
index dd4aae6c1153bc5a6014274983367adf674e188d..a6d1f974556e1f23d7dfa575281d92a2afad3fed 100644
--- a/lib/gitlab/metrics/dashboard/stages/base_stage.rb
+++ b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
@@ -36,7 +36,7 @@ def missing_metrics!
             raise DashboardLayoutError.new('Each "panel" must define an array :metrics')
           end
 
-          def for_metrics(dashboard)
+          def for_metrics
             missing_panel_groups! unless dashboard[:panel_groups].is_a?(Array)
 
             dashboard[:panel_groups].each do |panel_group|
diff --git a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
index 3406021bbea4f5c48d0456260fbce40961ee641f..188912bedb42cf50b0e2cbb618d8a113c9fd8924 100644
--- a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
@@ -11,7 +11,7 @@ class CommonMetricsInserter < BaseStage
           def transform!
             common_metrics = ::PrometheusMetric.common
 
-            for_metrics(dashboard) do |metric|
+            for_metrics do |metric|
               metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
               metric[:metric_id] = metric_record.id if metric_record
             end
diff --git a/lib/gitlab/metrics/dashboard/system_dashboard_service.rb b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..67509ed4230ff1ec79b20e80c51701be6d39b29f
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Fetches the system metrics dashboard and formats the output.
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+  module Metrics
+    module Dashboard
+      class SystemDashboardService < Gitlab::Metrics::Dashboard::BaseService
+        SYSTEM_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
+
+        class << self
+          def all_dashboard_paths(_project)
+            [{
+              path: SYSTEM_DASHBOARD_PATH,
+              default: true
+            }]
+          end
+
+          def system_dashboard?(filepath)
+            filepath == SYSTEM_DASHBOARD_PATH
+          end
+        end
+
+        private
+
+        def dashboard_path
+          SYSTEM_DASHBOARD_PATH
+        end
+
+        # Returns the base metrics shipped with every GitLab service.
+        def get_raw_dashboard
+          yml = File.read(Rails.root.join(dashboard_path))
+
+          YAML.safe_load(yml)
+        end
+
+        def cache_key
+          "metrics_dashboard_#{dashboard_path}"
+        end
+
+        def insert_project_metrics?
+          true
+        end
+      end
+    end
+  end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index a62422d022961ecb27d4dd8a2082fff30b6a140c..cf23d9370375da8dfa83c5cab63659604bd0e573 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -474,25 +474,102 @@
       end
     end
 
-    context 'when prometheus endpoint is enabled' do
+    shared_examples_for '200 response' do |contains_all_dashboards: false|
+      let(:expected_keys) { %w(dashboard status) }
+
+      before do
+        expected_keys << 'all_dashboards' if contains_all_dashboards
+      end
+
       it 'returns a json representation of the environment dashboard' do
-        get :metrics_dashboard, params: environment_params(format: :json)
+        get :metrics_dashboard, params: environment_params(dashboard_params)
 
         expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response.keys).to contain_exactly('dashboard', 'status')
+        expect(json_response.keys).to contain_exactly(*expected_keys)
         expect(json_response['dashboard']).to be_an_instance_of(Hash)
       end
+    end
+
+    shared_examples_for 'error response' do |status_code, contains_all_dashboards: false|
+      let(:expected_keys) { %w(message status) }
+
+      before do
+        expected_keys << 'all_dashboards' if contains_all_dashboards
+      end
+
+      it 'returns an error response' do
+        get :metrics_dashboard, params: environment_params(dashboard_params)
+
+        expect(response).to have_gitlab_http_status(status_code)
+        expect(json_response.keys).to contain_exactly(*expected_keys)
+      end
+    end
+
+    shared_examples_for 'has all dashboards' do
+      it 'includes an index of all available dashboards' do
+        get :metrics_dashboard, params: environment_params(dashboard_params)
+
+        expect(json_response.keys).to include('all_dashboards')
+        expect(json_response['all_dashboards']).to be_an_instance_of(Array)
+        expect(json_response['all_dashboards']).to all( include('path', 'default') )
+      end
+    end
+
+    context 'when multiple dashboards is disabled' do
+      before do
+        stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
+      end
+
+      let(:dashboard_params) { { format: :json } }
+
+      it_behaves_like '200 response'
 
       context 'when the dashboard could not be provided' do
         before do
           allow(YAML).to receive(:safe_load).and_return({})
         end
 
-        it 'returns an error response' do
-          get :metrics_dashboard, params: environment_params(format: :json)
+        it_behaves_like 'error response', :unprocessable_entity
+      end
+
+      context 'when a dashboard param is specified' do
+        let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } }
+
+        it_behaves_like '200 response'
+      end
+    end
+
+    context 'when multiple dashboards is enabled' do
+      let(:dashboard_params) { { format: :json } }
+
+      it_behaves_like '200 response', contains_all_dashboards: true
+      it_behaves_like 'has all dashboards'
+
+      context 'when a dashboard could not be provided' do
+        before do
+          allow(YAML).to receive(:safe_load).and_return({})
+        end
+
+        it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true
+        it_behaves_like 'has all dashboards'
+      end
+
+      context 'when a dashboard param is specified' do
+        let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } }
+
+        context 'when the dashboard is available' do
+          let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+          let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } }
+          let(:project) { create(:project, :custom_repo, files: dashboard_file) }
+          let(:environment) { create(:environment, name: 'production', project: project) }
+
+          it_behaves_like '200 response', contains_all_dashboards: true
+          it_behaves_like 'has all dashboards'
+        end
 
-          expect(response).to have_gitlab_http_status(:unprocessable_entity)
-          expect(json_response.keys).to contain_exactly('message', 'status', 'http_status')
+        context 'when the dashboard does not exist' do
+          it_behaves_like 'error response', :not_found, contains_all_dashboards: true
+          it_behaves_like 'has all dashboards'
         end
       end
     end
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
index c2d3d3d8aca988519f6c8df86f0b38b4a6ab4217..638ecbcc11fcbcfb93f7f3b38087ccb0f7f2c469 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
@@ -2,7 +2,7 @@ dashboard: 'Test Dashboard'
 priority: 1
 panel_groups:
 - group: Group A
-  priority: 10
+  priority: 1
   panels:
   - title: "Super Chart A1"
     type: "area-chart"
@@ -23,7 +23,7 @@ panel_groups:
       label: Legend Label
       unit: unit
 - group: Group B
-  priority: 1
+  priority: 10
   panels:
   - title: "Super Chart B"
     type: "area-chart"
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e88eb140b3537fda1f7491219cd0b241ce684c94
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
+  include MetricsDashboardHelpers
+
+  set(:project) { build(:project) }
+  set(:environment) { build(:environment, project: project) }
+  let(:system_dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
+
+  describe '.find' do
+    let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+    let(:service_call) { described_class.find(project, nil, environment, dashboard_path) }
+
+    it_behaves_like 'misconfigured dashboard service response', :not_found
+
+    context 'when the dashboard exists' do
+      let(:project) { project_with_dashboard(dashboard_path) }
+
+      it_behaves_like 'valid dashboard service response'
+    end
+
+    context 'when the dashboard is configured incorrectly' do
+      let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+      it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+    end
+
+    context 'when the system dashboard is specified' do
+      let(:dashboard_path) { system_dashboard_path }
+
+      it_behaves_like 'valid dashboard service response'
+    end
+
+    context 'when no dashboard is specified' do
+      let(:service_call) { described_class.find(project, nil, environment) }
+
+      it_behaves_like 'valid dashboard service response'
+    end
+  end
+
+  describe '.find_all_paths' do
+    let(:all_dashboard_paths) { described_class.find_all_paths(project) }
+    let(:system_dashboard) { { path: system_dashboard_path, default: true } }
+
+    it 'includes only the system dashboard by default' do
+      expect(all_dashboard_paths).to eq([system_dashboard])
+    end
+
+    context 'when the project contains dashboards' do
+      let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+      let(:project) { project_with_dashboard(dashboard_path) }
+
+      it 'includes system and project dashboards' do
+        project_dashboard = { path: dashboard_path, default: false }
+
+        expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
index ee7c93fce8d0859084bb14270c2417963beb0669..be3c1095bd7baddb311e70f5c74effc92e0690e3 100644
--- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -4,12 +4,12 @@
 
 describe Gitlab::Metrics::Dashboard::Processor do
   let(:project) { build(:project) }
-  let(:environment) { build(:environment) }
+  let(:environment) { build(:environment, project: project) }
   let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
 
   describe 'process' do
     let(:process_params) { [project, environment, dashboard_yml] }
-    let(:dashboard) { described_class.new(*process_params).process }
+    let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: true) }
 
     context 'when dashboard config corresponds to common metrics' do
       let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
@@ -35,9 +35,9 @@
 
       it 'orders groups by priority and panels by weight' do
         expected_metrics_order = [
-          'metric_a2', # group priority 10, panel weight 2
-          'metric_a1', # group priority 10, panel weight 1
-          'metric_b', # group priority 1, panel weight 1
+          'metric_b', # group priority 10, panel weight 1
+          'metric_a2', # group priority 1, panel weight 2
+          'metric_a1', # group priority 1, panel weight 1
           project_business_metric.id, # group priority 0, panel weight nil (0)
           project_response_metric.id, # group priority -5, panel weight nil (0)
           project_system_metric.id, # group priority -10, panel weight nil (0)
@@ -46,6 +46,17 @@
 
         expect(actual_metrics_order).to eq expected_metrics_order
       end
+
+      context 'when the dashboard should not include project metrics' do
+        let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: false) }
+
+        it 'includes only dashboard metrics' do
+          metrics = all_metrics.map { |m| m[:id] }
+
+          expect(metrics.length).to be(3)
+          expect(metrics).to eq %w(metric_b metric_a2 metric_a1)
+        end
+      end
     end
 
     shared_examples_for 'errors with message' do |expected_message|
diff --git a/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..162beb0268a3e2ee309d9686db809cbcf66d0b47
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_store_caching do
+  include MetricsDashboardHelpers
+
+  set(:user) { build(:user) }
+  set(:project) { build(:project) }
+  set(:environment) { build(:environment, project: project) }
+
+  before do
+    project.add_maintainer(user)
+  end
+
+  describe 'get_dashboard' do
+    let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+    let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] }
+    let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+    context 'when the dashboard does not exist' do
+      it_behaves_like 'misconfigured dashboard service response', :not_found
+    end
+
+    context 'when the dashboard exists' do
+      let(:project) { project_with_dashboard(dashboard_path) }
+
+      it_behaves_like 'valid dashboard service response'
+
+      it 'caches the unprocessed dashboard for subsequent calls' do
+        expect_any_instance_of(described_class)
+          .to receive(:get_raw_dashboard)
+          .once
+          .and_call_original
+
+        described_class.new(*service_params).get_dashboard
+        described_class.new(*service_params).get_dashboard
+      end
+
+      context 'and the dashboard is then deleted' do
+        it 'does not return the previously cached dashboard' do
+          described_class.new(*service_params).get_dashboard
+
+          delete_project_dashboard(project, user, dashboard_path)
+
+          expect_any_instance_of(described_class)
+          .to receive(:get_raw_dashboard)
+          .once
+          .and_call_original
+
+          described_class.new(*service_params).get_dashboard
+        end
+      end
+    end
+
+    context 'when the dashboard is configured incorrectly' do
+      let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+      it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/service_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_spec.rb
deleted file mode 100644
index e66c356bf4923bfb521395dabee9e241052ef623..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/metrics/dashboard/service_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Metrics::Dashboard::Service, :use_clean_rails_memory_store_caching do
-  let(:project) { build(:project) }
-  let(:environment) { build(:environment) }
-
-  describe 'get_dashboard' do
-    let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
-
-    it 'returns a json representation of the environment dashboard' do
-      result = described_class.new(project, environment).get_dashboard
-
-      expect(result.keys).to contain_exactly(:dashboard, :status)
-      expect(result[:status]).to eq(:success)
-
-      expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
-    end
-
-    it 'caches the dashboard for subsequent calls' do
-      expect(YAML).to receive(:safe_load).once.and_call_original
-
-      described_class.new(project, environment).get_dashboard
-      described_class.new(project, environment).get_dashboard
-    end
-
-    context 'when the dashboard is configured incorrectly' do
-      before do
-        allow(YAML).to receive(:safe_load).and_return({})
-      end
-
-      it 'returns an appropriate message and status code' do
-        result = described_class.new(project, environment).get_dashboard
-
-        expect(result.keys).to contain_exactly(:message, :http_status, :status)
-        expect(result[:status]).to eq(:error)
-        expect(result[:http_status]).to eq(:unprocessable_entity)
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e71ce2481a3b2e8352d4c50e4a1c5d4dfc4a4e5b
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do
+  include MetricsDashboardHelpers
+
+  set(:project) { build(:project) }
+  set(:environment) { build(:environment, project: project) }
+
+  describe 'get_dashboard' do
+    let(:dashboard_path) { described_class::SYSTEM_DASHBOARD_PATH }
+    let(:service_params) { [project, nil, { environment: environment, dashboard_path: dashboard_path }] }
+    let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+    it_behaves_like 'valid dashboard service response'
+
+    it 'caches the unprocessed dashboard for subsequent calls' do
+      expect(YAML).to receive(:safe_load).once.and_call_original
+
+      described_class.new(*service_params).get_dashboard
+      described_class.new(*service_params).get_dashboard
+    end
+
+    context 'when called with a non-system dashboard' do
+      let(:dashboard_path) { 'garbage/dashboard/path' }
+
+      # We want to alwaus return the system dashboard.
+      it_behaves_like 'valid dashboard service response'
+    end
+  end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 43ec1125087c95ab8f4d2abf6842b8061a601547..a6c3d5756aac6fa214594c79c9465acc03658ad3 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1637,6 +1637,7 @@ def merge(repository, user, merge_request, message)
         :has_visible_content?,
         :issue_template_names,
         :merge_request_template_names,
+        :metrics_dashboard_paths,
         :xcode_project?
       ])
 
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f36b0e217c843bc2220e889b3f7e916f5e2fd0b
--- /dev/null
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module MetricsDashboardHelpers
+  def project_with_dashboard(dashboard_path, dashboard_yml = nil)
+    dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')
+
+    create(:project, :custom_repo, files: { dashboard_path => dashboard_yml })
+  end
+
+  def delete_project_dashboard(project, user, dashboard_path)
+    project.repository.delete_file(
+      user,
+      dashboard_path,
+      branch_name: 'master',
+      message: 'Delete dashboard'
+    )
+
+    project.repository.refresh_method_caches([:metrics_dashboard])
+  end
+
+  shared_examples_for 'misconfigured dashboard service response' do |status_code|
+    it 'returns an appropriate message and status code' do
+      result = service_call
+
+      expect(result.keys).to contain_exactly(:message, :http_status, :status)
+      expect(result[:status]).to eq(:error)
+      expect(result[:http_status]).to eq(status_code)
+    end
+  end
+
+  shared_examples_for 'valid dashboard service response' do
+    let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+    it 'returns a json representation of the dashboard' do
+      result = service_call
+
+      expect(result.keys).to contain_exactly(:dashboard, :status)
+      expect(result[:status]).to eq(:success)
+
+      expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+    end
+  end
+end