diff --git a/.gitlab/issue_templates/QA failure.md b/.gitlab/issue_templates/QA failure.md
new file mode 100644
index 0000000000000000000000000000000000000000..13b5d7bf92c5a53c20dabc6fb3699baa63be1494
--- /dev/null
+++ b/.gitlab/issue_templates/QA failure.md	
@@ -0,0 +1,65 @@
+<!---
+Before opening a new QA failure issue, make sure to first search for it in the
+QA failures board: https://gitlab.com/groups/gitlab-org/-/boards/1385578
+
+The issue should have the following:
+
+- The relative path of the failing spec file in the title, e.g. if the login
+  test fails, include `qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb` in the title.
+  This is required so that existing issues can easily be found by searching for the spec file.
+- If the issue is about multiple test failures, include the path for each failing spec file in the description.
+- A link to the failing job.
+- The stack trace from the job's logs in the "Stack trace" section below.
+- A screenshot (if available), and HTML capture (if available), in the "Screenshot / HTML page" section below.
+--->
+
+### Summary
+
+
+
+### Stack trace
+
+```
+PUT STACK TRACE HERE
+```
+
+### Screenshot / HTML page
+
+<!--
+Attach the screenshot and HTML snapshot of the page from the job's artifacts:
+1. Download the job's artifacts and unarchive them.
+1. Open the `gitlab-qa-run-2020-*/gitlab-{ce,ee}-qa-*/{,ee}/{api,browser_ui}/<path to failed test>` folder.
+1. Select the `.png` and `.html` files that appears in the job logs (look for `HTML screenshot: /path/to/html/page.html` / `Image screenshot: `/path/to/html/page.png`).
+1. Drag and drop them here.
+-->
+
+### Possible fixes
+
+
+<!-- Default due date. -->
+/due in 2 weeks
+
+<!-- Base labels. -->
+/label ~Quality ~QA ~bug ~S1
+
+<!--
+Choose the stage that appears in the test path, e.g. ~"devops::create" for
+`qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
+-->
+/label ~devops::
+
+<!--
+Select a label for where the failure was found, e.g. if the failure occurred in
+a nightly pipeline, select ~"found:nightly".
+-->
+/label ~found:
+
+<!--
+https://about.gitlab.com/handbook/engineering/quality/guidelines/#priorities:
+- ~P1: Tests that are needed to verify fundamental GitLab functionality.
+- ~P2: Tests that deal with external integrations which may take a longer time to debug and fix.
+-->
+/label ~P
+
+<!-- Select the current milestone if ~P1 or the next milestone if ~P2. -->
+/milestone %
diff --git a/Gemfile.lock b/Gemfile.lock
index 4949ac8b9a530c01c9a2ba43b1b3b9f62d8ea997..500f1729d08c23c45cd6311edcef6bfe60186407 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -741,7 +741,7 @@ GEM
     parslet (1.8.2)
     peek (1.1.0)
       railties (>= 4.0.0)
-    pg (1.1.4)
+    pg (1.2.2)
     png_quantizator (0.2.1)
     po_to_json (1.0.1)
       json (>= 1.6.0)
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
index ca495cd2ecaa9c852db6f5b4a81445d16265a0f8..7530c1dfcaf44d72e1ca7b0531b6b6c9890f95cf 100644
--- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -1,20 +1,17 @@
 <script>
-import { mapState, mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+import { FETCH_SETTINGS_ERROR_MESSAGE } from '../constants';
+
 import SettingsForm from './settings_form.vue';
 
 export default {
   components: {
-    GlLoadingIcon,
     SettingsForm,
   },
-  computed: {
-    ...mapState({
-      isLoading: 'isLoading',
-    }),
-  },
   mounted() {
-    this.fetchSettings();
+    this.fetchSettings().catch(() =>
+      this.$toast.show(FETCH_SETTINGS_ERROR_MESSAGE, { type: 'error' }),
+    );
   },
   methods: {
     ...mapActions(['fetchSettings']),
@@ -37,7 +34,6 @@ export default {
         }}
       </li>
     </ul>
-    <gl-loading-icon v-if="isLoading" ref="loading-icon" size="xl" />
-    <settings-form v-else ref="settings-form" />
+    <settings-form ref="settings-form" />
   </div>
 </template>
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index 457bf35daaba280623e0c823d7a3b8daaf32202a..b713cfe2e341571f7b36864600eb1be372f13d37 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,8 +1,20 @@
 <script>
 import { mapActions, mapState } from 'vuex';
-import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton, GlCard } from '@gitlab/ui';
+import {
+  GlFormGroup,
+  GlToggle,
+  GlFormSelect,
+  GlFormTextarea,
+  GlButton,
+  GlCard,
+  GlLoadingIcon,
+} from '@gitlab/ui';
 import { s__, __, sprintf } from '~/locale';
-import { NAME_REGEX_LENGTH } from '../constants';
+import {
+  NAME_REGEX_LENGTH,
+  UPDATE_SETTINGS_ERROR_MESSAGE,
+  UPDATE_SETTINGS_SUCCESS_MESSAGE,
+} from '../constants';
 import { mapComputed } from '~/vuex_shared/bindings';
 
 export default {
@@ -13,13 +25,14 @@ export default {
     GlFormTextarea,
     GlButton,
     GlCard,
+    GlLoadingIcon,
   },
   labelsConfig: {
     cols: 3,
     align: 'right',
   },
   computed: {
-    ...mapState(['formOptions']),
+    ...mapState(['formOptions', 'isLoading']),
     ...mapComputed(
       [
         'enabled',
@@ -64,15 +77,26 @@ export default {
     formIsInvalid() {
       return this.nameRegexState === false;
     },
+    isFormElementDisabled() {
+      return !this.enabled || this.isLoading;
+    },
+    isSubmitButtonDisabled() {
+      return this.formIsInvalid || this.isLoading;
+    },
   },
   methods: {
     ...mapActions(['resetSettings', 'saveSettings']),
+    submit() {
+      this.saveSettings()
+        .then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }))
+        .catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }));
+    },
   },
 };
 </script>
 
 <template>
-  <form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
+  <form ref="form-element" @submit.prevent="submit" @reset.prevent="resetSettings">
     <gl-card>
       <template #header>
         {{ s__('ContainerRegistry|Tag expiration policy') }}
@@ -86,7 +110,7 @@ export default {
           :label="s__('ContainerRegistry|Expiration policy:')"
         >
           <div class="d-flex align-items-start">
-            <gl-toggle id="expiration-policy-toggle" v-model="enabled" />
+            <gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="isLoading" />
             <span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
           </div>
         </gl-form-group>
@@ -98,7 +122,11 @@ export default {
           label-for="expiration-policy-interval"
           :label="s__('ContainerRegistry|Expiration interval:')"
         >
-          <gl-form-select id="expiration-policy-interval" v-model="older_than" :disabled="!enabled">
+          <gl-form-select
+            id="expiration-policy-interval"
+            v-model="older_than"
+            :disabled="isFormElementDisabled"
+          >
             <option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
               {{ option.label }}
             </option>
@@ -112,7 +140,11 @@ export default {
           label-for="expiration-policy-schedule"
           :label="s__('ContainerRegistry|Expiration schedule:')"
         >
-          <gl-form-select id="expiration-policy-schedule" v-model="cadence" :disabled="!enabled">
+          <gl-form-select
+            id="expiration-policy-schedule"
+            v-model="cadence"
+            :disabled="isFormElementDisabled"
+          >
             <option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
               {{ option.label }}
             </option>
@@ -126,7 +158,11 @@ export default {
           label-for="expiration-policy-latest"
           :label="s__('ContainerRegistry|Number of tags to retain:')"
         >
-          <gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled">
+          <gl-form-select
+            id="expiration-policy-latest"
+            v-model="keep_n"
+            :disabled="isFormElementDisabled"
+          >
             <option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
               {{ option.label }}
             </option>
@@ -149,7 +185,7 @@ export default {
             v-model="name_regex"
             :placeholder="nameRegexPlaceholder"
             :state="nameRegexState"
-            :disabled="!enabled"
+            :disabled="isFormElementDisabled"
             trim
           />
           <template #description>
@@ -159,17 +195,18 @@ export default {
       </template>
       <template #footer>
         <div class="d-flex justify-content-end">
-          <gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{
-            __('Cancel')
-          }}</gl-button>
+          <gl-button ref="cancel-button" type="reset" class="mr-2 d-block" :disabled="isLoading">
+            {{ __('Cancel') }}
+          </gl-button>
           <gl-button
             ref="save-button"
             type="submit"
-            :disabled="formIsInvalid"
+            :disabled="isSubmitButtonDisabled"
             variant="success"
-            class="d-block"
+            class="d-flex justify-content-center align-items-center js-no-auto-disable"
           >
             {{ __('Save expiration policy') }}
+            <gl-loading-icon v-if="isLoading" class="ml-2" />
           </gl-button>
         </div>
       </template>
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index 927b6059884b179fe1964d3a9c289af707165c60..6ae1dbb72c472124ace112eaed82e9ba969dc275 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -1,8 +1,10 @@
 import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
 import Translate from '~/vue_shared/translate';
 import store from './store/';
 import RegistrySettingsApp from './components/registry_settings_app.vue';
 
+Vue.use(GlToast);
 Vue.use(Translate);
 
 export default () => {
diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js
index 5e46d564121eb9f84e90f857f96b39ca73125a7d..21a2008fef608b0d909c2a525a703e9d733b824c 100644
--- a/app/assets/javascripts/registry/settings/store/actions.js
+++ b/app/assets/javascripts/registry/settings/store/actions.js
@@ -1,18 +1,10 @@
 import Api from '~/api';
-import createFlash from '~/flash';
-import {
-  FETCH_SETTINGS_ERROR_MESSAGE,
-  UPDATE_SETTINGS_ERROR_MESSAGE,
-  UPDATE_SETTINGS_SUCCESS_MESSAGE,
-} from '../constants';
 import * as types from './mutation_types';
 
 export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
 export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
 export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
 export const receiveSettingsSuccess = ({ commit }, data = {}) => commit(types.SET_SETTINGS, data);
-export const receiveSettingsError = () => createFlash(FETCH_SETTINGS_ERROR_MESSAGE);
-export const updateSettingsError = () => createFlash(UPDATE_SETTINGS_ERROR_MESSAGE);
 export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
 
 export const fetchSettings = ({ dispatch, state }) => {
@@ -21,7 +13,6 @@ export const fetchSettings = ({ dispatch, state }) => {
     .then(({ data: { container_expiration_policy } }) =>
       dispatch('receiveSettingsSuccess', container_expiration_policy),
     )
-    .catch(() => dispatch('receiveSettingsError'))
     .finally(() => dispatch('toggleLoading'));
 };
 
@@ -30,11 +21,9 @@ export const saveSettings = ({ dispatch, state }) => {
   return Api.updateProject(state.projectId, {
     container_expiration_policy_attributes: state.settings,
   })
-    .then(({ data: { container_expiration_policy } }) => {
-      dispatch('receiveSettingsSuccess', container_expiration_policy);
-      createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
-    })
-    .catch(() => dispatch('updateSettingsError'))
+    .then(({ data: { container_expiration_policy } }) =>
+      dispatch('receiveSettingsSuccess', container_expiration_policy),
+    )
     .finally(() => dispatch('toggleLoading'));
 };
 
diff --git a/app/models/note.rb b/app/models/note.rb
index 7731b477ad063b1b19af43ed33ba1ff1c34a7202..de9478ce68d8c1047b4fcc06fb228cccce32c704 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -124,7 +124,7 @@ class Note < ApplicationRecord
   scope :inc_author, -> { includes(:author) }
   scope :inc_relations_for_view, -> do
     includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji,
-             :system_note_metadata, :note_diff_file, :suggestions)
+             { system_note_metadata: :description_version }, :note_diff_file, :suggestions)
   end
 
   scope :with_notes_filter, -> (notes_filter) do
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 1dd65c762582abb5fdbbf36f17ebe4e5a7b0bffd..a495d34c07c5a9854ebdd984ada9f7bf38a0efd3 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
 class ProjectCiCdSetting < ApplicationRecord
-  include IgnorableColumns
-  # https://gitlab.com/gitlab-org/gitlab/issues/36651
-  ignore_column :merge_trains_enabled, remove_with: '12.7', remove_after: '2019-12-22'
   belongs_to :project, inverse_of: :ci_cd_settings
 
   # The version of the schema that first introduced this model/table.
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f26a2201550f760a32e2541061c2427def15f852..f19dd0e4a483800558e57ab302029cd637306d0b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -192,3 +192,4 @@
 - self_monitoring_project_create
 - self_monitoring_project_delete
 - merge_request_mergeability_check
+- phabricator_import_import_tasks
diff --git a/lib/gitlab/phabricator_import/base_worker.rb b/app/workers/gitlab/phabricator_import/base_worker.rb
similarity index 95%
rename from lib/gitlab/phabricator_import/base_worker.rb
rename to app/workers/gitlab/phabricator_import/base_worker.rb
index d2c2ef8db48ac7c8bc71df252dcbd78652c56822..faae71d46272f6eadfead7f007ff90f8bac986fc 100644
--- a/lib/gitlab/phabricator_import/base_worker.rb
+++ b/app/workers/gitlab/phabricator_import/base_worker.rb
@@ -19,8 +19,7 @@
 module Gitlab
   module PhabricatorImport
     class BaseWorker
-      include ApplicationWorker
-      include ProjectImportOptions # This marks the project as failed after too many tries
+      include WorkerAttributes
       include Gitlab::ExclusiveLeaseHelpers
 
       feature_category :importers
diff --git a/lib/gitlab/phabricator_import/import_tasks_worker.rb b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
similarity index 63%
rename from lib/gitlab/phabricator_import/import_tasks_worker.rb
rename to app/workers/gitlab/phabricator_import/import_tasks_worker.rb
index c36954a8d41fe81b069634679a0c3dd7cbdc00ea..b5d9e80797b2012a0205b270a850620b2d13df3a 100644
--- a/lib/gitlab/phabricator_import/import_tasks_worker.rb
+++ b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
@@ -2,6 +2,9 @@
 module Gitlab
   module PhabricatorImport
     class ImportTasksWorker < BaseWorker
+      include ApplicationWorker
+      include ProjectImportOptions # This marks the project as failed after too many tries
+
       def importer_class
         Gitlab::PhabricatorImport::Issues::Importer
       end
diff --git a/changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml b/changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2e2910d17a52e9590cc458c9b7ecda7d4d381548
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-pg-gem-1-2-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update pg gem to v1.2.2
+merge_request: 23237
+author:
+type: other
diff --git a/db/migrate/20191209143606_add_deleted_at_to_description_versions.rb b/db/migrate/20191209143606_add_deleted_at_to_description_versions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02a3d1271c2245acc52376cff16503a4b57b95d4
--- /dev/null
+++ b/db/migrate/20191209143606_add_deleted_at_to_description_versions.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDeletedAtToDescriptionVersions < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    add_column :description_versions, :deleted_at, :datetime_with_timezone
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 39ada44b5aa3e069c27fad9c14a19fa9a8514a4d..ae1b85331029bf42bdb57d0dae84eacc681831a8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1410,6 +1410,7 @@ ActiveRecord::Schema.define(version: 2020_01_21_132641) do
     t.integer "merge_request_id"
     t.integer "epic_id"
     t.text "description"
+    t.datetime_with_timezone "deleted_at"
     t.index ["epic_id"], name: "index_description_versions_on_epic_id", where: "(epic_id IS NOT NULL)"
     t.index ["issue_id"], name: "index_description_versions_on_issue_id", where: "(issue_id IS NOT NULL)"
     t.index ["merge_request_id"], name: "index_description_versions_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 356feae4eafa650e757bb7c80f2e646c62d9f93f..841a05d8e612514224230c5e2f3f78f1f97034b4 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -213,7 +213,7 @@ class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2]
   disable_ddl_transaction!
 
   def up
-    cleanup_concurrent_column_type_change :users
+    cleanup_concurrent_column_type_change :users, :username
   end
 
   def down
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 9d14695c0981044c1c7004b5be86e468213c2c68..c689142d79db15e24dc5020de05bb430afc1f576 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -53,14 +53,14 @@ module Gitlab
         Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key)
       end
 
-      def track_experiment_event(experiment_key, action)
-        track_experiment_event_for(experiment_key, action) do |tracking_data|
+      def track_experiment_event(experiment_key, action, value = nil)
+        track_experiment_event_for(experiment_key, action, value) do |tracking_data|
           ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
         end
       end
 
-      def frontend_experimentation_tracking_data(experiment_key, action)
-        track_experiment_event_for(experiment_key, action) do |tracking_data|
+      def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
+        track_experiment_event_for(experiment_key, action, value) do |tracking_data|
           gon.push(tracking_data: tracking_data)
         end
       end
@@ -77,19 +77,20 @@ module Gitlab
         experimentation_subject_id.delete('-').hex % 100
       end
 
-      def track_experiment_event_for(experiment_key, action)
+      def track_experiment_event_for(experiment_key, action, value)
         return unless Experimentation.enabled?(experiment_key)
 
-        yield experimentation_tracking_data(experiment_key, action)
+        yield experimentation_tracking_data(experiment_key, action, value)
       end
 
-      def experimentation_tracking_data(experiment_key, action)
+      def experimentation_tracking_data(experiment_key, action, value)
         {
           category: tracking_category(experiment_key),
           action: action,
           property: tracking_group(experiment_key),
-          label: experimentation_subject_id
-        }
+          label: experimentation_subject_id,
+          value: value
+        }.compact
       end
 
       def tracking_category(experiment_key)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e00b49b9042b3b357a540003adf23c95ff2f7327..f10eb82e03e1b3eecae7403593d4c2418996a67a 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -78,6 +78,7 @@ module Gitlab
             clusters_applications_runner: count(::Clusters::Applications::Runner.available),
             clusters_applications_knative: count(::Clusters::Applications::Knative.available),
             clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
+            clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
             in_review_folder: count(::Environment.in_review_folder),
             grafana_integrated_projects: count(GrafanaIntegration.enabled),
             groups: count(Group),
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index f718311fbf2e15a39bfdc64459da54a2496a1dd7..9d302acb058103eb359c95da95b29450fc6f8b0b 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -47,6 +47,7 @@ module QA
 
           def protect_branch
             click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+            wait_for_requests
           end
 
           private
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
index 97373f7a0593ead503689ae5eb5c5c92d782fe25..9c599ec2d4d15617b036a5f6506d478def744045 100644
--- a/qa/qa/scenario/template.rb
+++ b/qa/qa/scenario/template.rb
@@ -23,6 +23,8 @@ module QA
       def perform(options, *args)
         extract_address(:gitlab_address, options, args)
 
+        QA::Runtime::Browser.configure!
+
         Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
 
         Specs::Runner.perform do |specs|
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
index 79dad7f46192c9bfd84746c6dc40b13f3821d282..11b6a7f7dfaeb76bff9315947d04a9a5d854ce2c 100644
--- a/qa/qa/scenario/test/instance.rb
+++ b/qa/qa/scenario/test/instance.rb
@@ -20,6 +20,8 @@ module QA
         def self.do_perform(address, *rspec_options)
           Runtime::Scenario.define(:gitlab_address, address)
 
+          QA::Runtime::Browser.configure!
+
           Specs::Runner.perform do |specs|
             specs.tty = true
             specs.options = rspec_options if rspec_options.any?
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 86da866a9274c62d6fcd474725b1d27c3688eb09..89da9d1b996cf43039757d0479c5584881f17c2f 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -17,10 +17,10 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
       expect(settings_block).to have_text 'Container Registry tag expiration policy'
     end
 
-    it 'Save expiration policy submit the form', :js do
+    it 'Save expiration policy submit the form' do
       within '#js-registry-policies' do
         within '.card-body' do
-          click_button(class: 'gl-toggle')
+          find('#expiration-policy-toggle button:not(.is-disabled)').click
           select('7 days until tags are automatically removed', from: 'expiration-policy-interval')
           select('Every day', from: 'expiration-policy-schedule')
           select('50 tags per image name', from: 'expiration-policy-latest')
@@ -30,8 +30,8 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
         expect(submit_button).not_to be_disabled
         submit_button.click
       end
-      flash_text = find('.flash-text')
-      expect(flash_text).to have_content('Expiration policy successfully saved.')
+      toast = find('.gl-toast')
+      expect(toast).to have_content('Expiration policy successfully saved.')
     end
   end
 end
diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
index d26df308b97f54b5df6591febfae3a334800bb6f..1d8627da18152718f083f4c9579920e79404cf80 100644
--- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -161,17 +161,20 @@ exports[`Settings Form renders 1`] = `
           class="mr-2 d-block"
           type="reset"
         >
+          
           Cancel
+        
         </glbutton-stub>
          
         <glbutton-stub
-          class="d-block"
+          class="d-flex justify-content-center align-items-center js-no-auto-disable"
           type="submit"
           variant="success"
         >
           
           Save expiration policy
-        
+          
+          <!---->
         </glbutton-stub>
       </div>
     </div>
diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
index 448ff2b3be9b7297e2dd7f7d5b49352238b274de..eceb5bf643cecafe385f30e38722ed12b165e2b6 100644
--- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
@@ -1,55 +1,55 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
 import component from '~/registry/settings/components/registry_settings_app.vue';
 import { createStore } from '~/registry/settings/store/';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/settings/constants';
 
 describe('Registry Settings App', () => {
   let wrapper;
   let store;
-  let fetchSpy;
 
   const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' });
-  const findLoadingComponent = () => wrapper.find({ ref: 'loading-icon' });
 
-  const mountComponent = (options = {}) => {
-    fetchSpy = jest.fn();
+  const mountComponent = ({ dispatchMock } = {}) => {
+    store = createStore();
+    const dispatchSpy = jest.spyOn(store, 'dispatch');
+    if (dispatchMock) {
+      dispatchSpy[dispatchMock]();
+    }
     wrapper = shallowMount(component, {
-      store,
-      methods: {
-        fetchSettings: fetchSpy,
+      mocks: {
+        $toast: {
+          show: jest.fn(),
+        },
       },
-      ...options,
+      store,
     });
   };
 
-  beforeEach(() => {
-    store = createStore();
-    mountComponent();
-  });
-
   afterEach(() => {
     wrapper.destroy();
   });
 
   it('renders', () => {
+    mountComponent({ dispatchMock: 'mockResolvedValue' });
     expect(wrapper.element).toMatchSnapshot();
   });
 
   it('call the store function to load the data on mount', () => {
-    expect(fetchSpy).toHaveBeenCalled();
+    mountComponent({ dispatchMock: 'mockResolvedValue' });
+    expect(store.dispatch).toHaveBeenCalledWith('fetchSettings');
   });
 
-  it('renders a loader if isLoading is true', () => {
-    store.dispatch('toggleLoading');
-    return wrapper.vm.$nextTick().then(() => {
-      expect(findLoadingComponent().exists()).toBe(true);
-      expect(findSettingsComponent().exists()).toBe(false);
-    });
+  it('show a toast if fetchSettings fails', () => {
+    mountComponent({ dispatchMock: 'mockRejectedValue' });
+    return wrapper.vm.$nextTick().then(() =>
+      expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(FETCH_SETTINGS_ERROR_MESSAGE, {
+        type: 'error',
+      }),
+    );
   });
+
   it('renders the setting form', () => {
+    mountComponent({ dispatchMock: 'mockResolvedValue' });
     expect(findSettingsComponent().exists()).toBe(true);
   });
 });
diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js
index bd733e965a49d742ba1c5ad0e96c247f9f89d803..996804f6d08725795752807f32e8376b7a770b0d 100644
--- a/spec/frontend/registry/settings/components/settings_form_spec.js
+++ b/spec/frontend/registry/settings/components/settings_form_spec.js
@@ -1,39 +1,44 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
 import stubChildren from 'helpers/stub_children';
 import component from '~/registry/settings/components/settings_form.vue';
 import { createStore } from '~/registry/settings/store/';
-import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
+import {
+  NAME_REGEX_LENGTH,
+  UPDATE_SETTINGS_ERROR_MESSAGE,
+  UPDATE_SETTINGS_SUCCESS_MESSAGE,
+} from '~/registry/settings/constants';
 import { stringifiedFormOptions } from '../mock_data';
 
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
 describe('Settings Form', () => {
   let wrapper;
   let store;
-  let saveSpy;
-  let resetSpy;
+  let dispatchSpy;
+
+  const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
+
+  const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
 
-  const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`);
-  const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`);
+  const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
+  const findFormElements = (name, parent = wrapper) =>
+    parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
   const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
   const findSaveButton = () => wrapper.find({ ref: 'save-button' });
   const findForm = () => wrapper.find({ ref: 'form-element' });
+  const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
 
   const mountComponent = (options = {}) => {
-    saveSpy = jest.fn();
-    resetSpy = jest.fn();
     wrapper = mount(component, {
       stubs: {
         ...stubChildren(component),
         GlCard: false,
+        GlLoadingIcon,
       },
-      store,
-      methods: {
-        saveSettings: saveSpy,
-        resetSettings: resetSpy,
+      mocks: {
+        $toast: {
+          show: jest.fn(),
+        },
       },
+      store,
       ...options,
     });
   };
@@ -41,6 +46,7 @@ describe('Settings Form', () => {
   beforeEach(() => {
     store = createStore();
     store.dispatch('setInitialState', stringifiedFormOptions);
+    dispatchSpy = jest.spyOn(store, 'dispatch');
     mountComponent();
   });
 
@@ -59,48 +65,53 @@ describe('Settings Form', () => {
     ${'schedule'}      | ${'cadence'}    | ${'foo'} | ${'disabled'}
     ${'latest'}        | ${'keep_n'}     | ${'foo'} | ${'disabled'}
     ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
-  `('$elementName form element', ({ elementName, modelName, value, disabledByToggle }) => {
-    let formGroup;
-    beforeEach(() => {
-      formGroup = findFormGroup(elementName);
-    });
-    it(`${elementName} form group exist in the dom`, () => {
-      expect(formGroup.exists()).toBe(true);
-    });
+  `(
+    `${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`,
+    ({ elementName, modelName, value, disabledByToggle }) => {
+      let formGroup;
+      beforeEach(() => {
+        formGroup = findFormGroup(elementName);
+      });
+      it(`${elementName} form group exist in the dom`, () => {
+        expect(formGroup.exists()).toBe(true);
+      });
 
-    it(`${elementName} form group has a label-for property`, () => {
-      expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
-    });
+      it(`${elementName} form group has a label-for property`, () => {
+        expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
+      });
 
-    it(`${elementName} form group has a label-cols property`, () => {
-      expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`);
-    });
+      it(`${elementName} form group has a label-cols property`, () => {
+        expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`);
+      });
 
-    it(`${elementName} form group has a label-align property`, () => {
-      expect(formGroup.attributes('label-align')).toBe(`${wrapper.vm.$options.labelsConfig.align}`);
-    });
+      it(`${elementName} form group has a label-align property`, () => {
+        expect(formGroup.attributes('label-align')).toBe(
+          `${wrapper.vm.$options.labelsConfig.align}`,
+        );
+      });
 
-    it(`${elementName} form group contains an input element`, () => {
-      expect(findFormElements(elementName, formGroup).exists()).toBe(true);
-    });
+      it(`${elementName} form group contains an input element`, () => {
+        expect(findFormElements(elementName, formGroup).exists()).toBe(true);
+      });
 
-    it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
-      const element = findFormElements(elementName, formGroup);
-      const modelUpdateEvent = element.vm.$options.model
-        ? element.vm.$options.model.event
-        : 'input';
-      element.vm.$emit(modelUpdateEvent, value);
-      return wrapper.vm.$nextTick().then(() => {
-        expect(wrapper.vm[modelName]).toBe(value);
+      it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
+        const element = findFormElements(elementName, formGroup);
+        const modelUpdateEvent = element.vm.$options.model
+          ? element.vm.$options.model.event
+          : 'input';
+        element.vm.$emit(modelUpdateEvent, value);
+        return wrapper.vm.$nextTick().then(() => {
+          expect(wrapper.vm[modelName]).toBe(value);
+        });
       });
-    });
 
-    it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
-      store.dispatch('updateSettings', { enabled: false });
-      const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
-      expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
-    });
-  });
+      it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
+        store.dispatch('updateSettings', { enabled: false });
+        const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
+        expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
+      });
+    },
+  );
 
   describe('form actions', () => {
     let form;
@@ -112,17 +123,79 @@ describe('Settings Form', () => {
     });
 
     it('form reset event call the appropriate function', () => {
+      dispatchSpy.mockReturnValue();
       form.trigger('reset');
-      expect(resetSpy).toHaveBeenCalled();
+      // expect.any(Object) is necessary because the event payload is passed to the function
+      expect(dispatchSpy).toHaveBeenCalledWith('resetSettings', expect.any(Object));
     });
 
     it('save has type submit', () => {
       expect(findSaveButton().attributes('type')).toBe('submit');
     });
 
-    it('form submit event call the appropriate function', () => {
-      form.trigger('submit');
-      expect(saveSpy).toHaveBeenCalled();
+    describe('when isLoading is true', () => {
+      beforeEach(() => {
+        store.dispatch('toggleLoading');
+      });
+
+      afterEach(() => {
+        store.dispatch('toggleLoading');
+      });
+
+      it.each`
+        elementName
+        ${'toggle'}
+        ${'interval'}
+        ${'schedule'}
+        ${'latest'}
+        ${'name-matching'}
+      `(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
+        expect(findFormElements(elementName).attributes('disabled')).toBe('true');
+      });
+
+      it('submit button is disabled and shows a spinner', () => {
+        const button = findSaveButton();
+        expect(button.attributes('disabled')).toBeTruthy();
+        expect(findLoadingIcon(button)).toExist();
+      });
+
+      it('cancel button is disabled', () => {
+        expect(findCancelButton().attributes('disabled')).toBeTruthy();
+      });
+    });
+
+    describe('form submit event ', () => {
+      it('calls the appropriate function', () => {
+        dispatchSpy.mockResolvedValue();
+        form.trigger('submit');
+        expect(dispatchSpy).toHaveBeenCalled();
+      });
+
+      it('dispatches the saveSettings action', () => {
+        dispatchSpy.mockResolvedValue();
+        form.trigger('submit');
+        expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
+      });
+
+      it('show a success toast when submit succeed', () => {
+        dispatchSpy.mockResolvedValue();
+        form.trigger('submit');
+        return wrapper.vm.$nextTick().then(() => {
+          expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
+            type: 'success',
+          });
+        });
+      });
+
+      it('show an error toast when submit fails', () => {
+        dispatchSpy.mockRejectedValue();
+        form.trigger('submit');
+        return wrapper.vm.$nextTick().then(() => {
+          expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
+            type: 'error',
+          });
+        });
+      });
     });
   });
 
@@ -160,7 +233,7 @@ describe('Settings Form', () => {
     it('toggleDescriptionText text reflects enabled property', () => {
       const toggleHelpText = findFormGroup('toggle').find('span');
       expect(toggleHelpText.html()).toContain('disabled');
-      wrapper.vm.enabled = true;
+      wrapper.setData({ enabled: true });
       return wrapper.vm.$nextTick().then(() => {
         expect(toggleHelpText.html()).toContain('enabled');
       });
diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js
index 80fb800ac3ad7326600821b925f98333ce569772..65b1fc42bfe4a838447eee251dba50dde6a5d682 100644
--- a/spec/frontend/registry/settings/store/actions_spec.js
+++ b/spec/frontend/registry/settings/store/actions_spec.js
@@ -1,15 +1,7 @@
 import Api from '~/api';
-import createFlash from '~/flash';
 import testAction from 'helpers/vuex_action_helper';
 import * as actions from '~/registry/settings/store/actions';
 import * as types from '~/registry/settings/store/mutation_types';
-import {
-  UPDATE_SETTINGS_ERROR_MESSAGE,
-  FETCH_SETTINGS_ERROR_MESSAGE,
-  UPDATE_SETTINGS_SUCCESS_MESSAGE,
-} from '~/registry/settings/constants';
-
-jest.mock('~/flash');
 
 describe('Actions Registry Store', () => {
   describe.each`
@@ -25,19 +17,6 @@ describe('Actions Registry Store', () => {
     });
   });
 
-  describe.each`
-    actionName                | message
-    ${'receiveSettingsError'} | ${FETCH_SETTINGS_ERROR_MESSAGE}
-    ${'updateSettingsError'}  | ${UPDATE_SETTINGS_ERROR_MESSAGE}
-  `('%s action', ({ actionName, message }) => {
-    it(`should call createFlash with ${message}`, done => {
-      testAction(actions[actionName], null, null, [], [], () => {
-        expect(createFlash).toHaveBeenCalledWith(message);
-        done();
-      });
-    });
-  });
-
   describe('fetchSettings', () => {
     const state = {
       projectId: 'bar',
@@ -64,18 +43,6 @@ describe('Actions Registry Store', () => {
         done,
       );
     });
-
-    it('should call receiveSettingsError on error', done => {
-      Api.project = jest.fn().mockRejectedValue();
-      testAction(
-        actions.fetchSettings,
-        null,
-        state,
-        [],
-        [{ type: 'toggleLoading' }, { type: 'receiveSettingsError' }, { type: 'toggleLoading' }],
-        done,
-      );
-    });
   });
 
   describe('saveSettings', () => {
@@ -102,21 +69,6 @@ describe('Actions Registry Store', () => {
           { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
           { type: 'toggleLoading' },
         ],
-        () => {
-          expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
-          done();
-        },
-      );
-    });
-
-    it('should call receiveSettingsError on error', done => {
-      Api.updateProject = jest.fn().mockRejectedValue();
-      testAction(
-        actions.saveSettings,
-        null,
-        state,
-        [],
-        [{ type: 'toggleLoading' }, { type: 'updateSettingsError' }, { type: 'toggleLoading' }],
         done,
       );
     });
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index e4624accd58d66906aac960c528121bc978d6e3a..1506794cbb546e16221a3921a3a669a115e84624 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -96,10 +96,10 @@ describe Gitlab::Experimentation do
             expect(Gitlab::Tracking).to receive(:event).with(
               'Team',
               'start',
-              label: nil,
-              property: 'experimental_group'
+              property: 'experimental_group',
+              value: 'team_id'
             )
-            controller.track_experiment_event(:test_experiment, 'start')
+            controller.track_experiment_event(:test_experiment, 'start', 'team_id')
           end
         end
 
@@ -112,10 +112,10 @@ describe Gitlab::Experimentation do
             expect(Gitlab::Tracking).to receive(:event).with(
               'Team',
               'start',
-              label: nil,
-              property: 'control_group'
+              property: 'control_group',
+              value: 'team_id'
             )
-            controller.track_experiment_event(:test_experiment, 'start')
+            controller.track_experiment_event(:test_experiment, 'start', 'team_id')
           end
         end
       end
@@ -144,13 +144,13 @@ describe Gitlab::Experimentation do
           end
 
           it 'pushes the right parameters to gon' do
-            controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+            controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
             expect(Gon.tracking_data).to eq(
               {
                 category: 'Team',
                 action: 'start',
-                label: nil,
-                property: 'experimental_group'
+                property: 'experimental_group',
+                value: 'team_id'
               }
             )
           end
@@ -164,12 +164,23 @@ describe Gitlab::Experimentation do
           end
 
           it 'pushes the right parameters to gon' do
+            controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
+            expect(Gon.tracking_data).to eq(
+              {
+                category: 'Team',
+                action: 'start',
+                property: 'control_group',
+                value: 'team_id'
+              }
+            )
+          end
+
+          it 'does not send nil value to gon' do
             controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
             expect(Gon.tracking_data).to eq(
               {
                 category: 'Team',
                 action: 'start',
-                label: nil,
                 property: 'control_group'
               }
             )
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index cf1dacd088e45c0f9beb76c8be6c2fae20c6dfbf..9a49d334f524b3e4b74b671b727e9b30d6d92ef3 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -49,6 +49,7 @@ describe Gitlab::UsageData do
       create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
       create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
       create(:clusters_applications_elastic_stack, :installed, cluster: gcp_cluster)
+      create(:clusters_applications_jupyter, :installed, cluster: gcp_cluster)
 
       create(:grafana_integration, project: projects[0], enabled: true)
       create(:grafana_integration, project: projects[1], enabled: true)
@@ -149,6 +150,7 @@ describe Gitlab::UsageData do
         clusters_applications_runner
         clusters_applications_knative
         clusters_applications_elastic_stack
+        clusters_applications_jupyter
         in_review_folder
         grafana_integrated_projects
         groups
@@ -242,6 +244,7 @@ describe Gitlab::UsageData do
       expect(count_data[:clusters_applications_knative]).to eq(1)
       expect(count_data[:clusters_applications_elastic_stack]).to eq(1)
       expect(count_data[:grafana_integrated_projects]).to eq(2)
+      expect(count_data[:clusters_applications_jupyter]).to eq(1)
     end
 
     it 'works when queries time out' do
diff --git a/spec/lib/gitlab/phabricator_import/base_worker_spec.rb b/spec/workers/gitlab/phabricator_import/base_worker_spec.rb
similarity index 100%
rename from spec/lib/gitlab/phabricator_import/base_worker_spec.rb
rename to spec/workers/gitlab/phabricator_import/base_worker_spec.rb
diff --git a/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb b/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb
similarity index 100%
rename from spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb
rename to spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb