diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index 022c5981d329533adee8f0c4317dd1eb201148c5..44888e59d6672c0171c76b86d739f7551c34a746 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -306,7 +306,6 @@ export default {
         <emoji-picker
           v-if="canAwardEmoji"
           toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary"
-          boundary="viewport"
           :right="false"
           data-testid="note-emoji-button"
           @click="handleAwardEmoji"
@@ -333,6 +332,7 @@ export default {
           no-caret
           left
           :items="dropdownItems"
+          data-testid="more-actions"
         />
       </div>
     </div>
diff --git a/app/assets/javascripts/emoji/components/category.vue b/app/assets/javascripts/emoji/components/category.vue
index 80850475b9656f2a45f024b98c4cbce9da2e76bb..eefb487b79beebb4d605a053461f36d7d2c86ffe 100644
--- a/app/assets/javascripts/emoji/components/category.vue
+++ b/app/assets/javascripts/emoji/components/category.vue
@@ -43,7 +43,9 @@ export default {
 
 <template>
   <gl-intersection-observer class="gl-px-5 gl-h-full" @appear="categoryAppeared">
-    <div class="gl-top-0 gl-py-3 gl-w-full gl-z-index-1 emoji-picker-category-header">
+    <div
+      class="gl-top-0 gl-py-3 gl-w-full gl-z-index-1 gl-font-sm gl-ml-n2 emoji-picker-category-header"
+    >
       <b>{{ categoryTitle }}</b>
     </div>
     <template v-if="emojis.length">
diff --git a/app/assets/javascripts/emoji/components/emoji_group.vue b/app/assets/javascripts/emoji/components/emoji_group.vue
index bb0c3b0a694c2bb157fb78c0218d85883ceb08ab..a6eb96cf947df0339faf1ff21fd922e3aee40f79 100644
--- a/app/assets/javascripts/emoji/components/emoji_group.vue
+++ b/app/assets/javascripts/emoji/components/emoji_group.vue
@@ -36,6 +36,7 @@ export default {
         data-testid="emoji-button"
         button-text-classes="gl-display-none!"
         @click="clickEmoji(emoji)"
+        @keydown.enter="clickEmoji(emoji)"
       >
         <template #emoji>
           <gl-emoji :data-name="emoji" class="gl-mr-0!" />
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 8b1784ae5510eb0d8817a0c5455774633adb3c64..2c7e424dff51a2ba73a34958e4697d4fe78f40de 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -1,9 +1,18 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <script>
-import { GlButton, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import {
+  GlButton,
+  GlIcon,
+  GlDisclosureDropdown,
+  GlDisclosureDropdownGroup,
+  GlDisclosureDropdownItem,
+  GlSearchBoxByType,
+  GlTooltipDirective,
+} from '@gitlab/ui';
 import { findLastIndex } from 'lodash';
 import VirtualList from 'vue-virtual-scroll-list';
 import { getEmojiCategoryMap, state } from '~/emoji';
+import { __ } from '~/locale';
 import { CATEGORY_NAMES, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
 import Category from './category.vue';
 import EmojiList from './emoji_list.vue';
@@ -13,13 +22,17 @@ export default {
   components: {
     GlButton,
     GlIcon,
-    GlDropdown,
-    GlDropdownItem,
+    GlDisclosureDropdown,
+    GlDisclosureDropdownGroup,
+    GlDisclosureDropdownItem,
     GlSearchBoxByType,
     VirtualList,
     Category,
     EmojiList,
   },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
   inject: {
     newCustomEmojiPath: {
       default: '',
@@ -41,14 +54,10 @@ export default {
       required: false,
       default: true,
     },
-    boundary: {
-      type: String,
-      required: false,
-      default: '',
-    },
   },
   data() {
     return {
+      isVisible: false,
       currentCategory: 0,
       searchValue: '',
     };
@@ -64,6 +73,18 @@ export default {
         icon: CATEGORY_ICON_MAP[category],
       }));
     },
+    placement() {
+      return this.right ? 'right' : 'left';
+    },
+    newCustomEmoji() {
+      return {
+        text: __('Create new emoji'),
+        href: this.newCustomEmojiPath,
+        extraAttrs: {
+          'data-testid': 'create-new-emoji',
+        },
+      };
+    },
   },
   methods: {
     categoryAppeared(category) {
@@ -77,15 +98,12 @@ export default {
     },
     selectEmoji({ category, emoji }) {
       this.$emit('click', emoji);
-      this.$refs.dropdown.hide();
+      this.$refs.dropdown.close();
 
       if (category !== 'custom') {
         addToFrequentlyUsed(emoji);
       }
     },
-    getBoundaryElement() {
-      return this.boundary || document.querySelector('.content-wrapper') || 'scrollParent';
-    },
     onSearchInput() {
       this.$refs.virtualScoller.setScrollTop(0);
       this.$refs.virtualScoller.forceRender();
@@ -95,55 +113,72 @@ export default {
 
       this.currentCategory = findLastIndex(Object.values(categories), ({ top }) => offset >= top);
     },
+    onShow() {
+      this.isVisible = true;
+      this.$refs.searchValue.focusInput();
+
+      this.$emit('shown');
+    },
     onHide() {
+      this.isVisible = false;
       this.currentCategory = 0;
       this.searchValue = '';
       this.$emit('hidden');
     },
   },
+  i18n: {
+    addReaction: __('Add reaction'),
+  },
 };
 </script>
 
 <template>
   <div class="emoji-picker">
-    <gl-dropdown
+    <gl-disclosure-dropdown
       ref="dropdown"
-      :toggle-class="toggleClass"
-      :boundary="getBoundaryElement()"
       :class="dropdownClass"
-      menu-class="dropdown-extended-height"
-      category="secondary"
-      no-flip
-      :right="right"
-      lazy
-      @shown="$emit('shown')"
+      :placement="placement"
+      @shown="onShow"
       @hidden="onHide"
     >
-      <template #button-content>
-        <slot name="button-content">
-          <gl-icon class="award-control-icon-neutral gl-button-icon gl-icon" name="slight-smile" />
-          <gl-icon
-            class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!"
-            name="smiley"
-          />
-          <gl-icon
-            class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!"
-            name="smile"
-          />
-        </slot>
-        <span class="gl-sr-only">{{ __('Add reaction') }}</span>
+      <template #toggle>
+        <gl-button
+          v-gl-tooltip
+          :title="$options.i18n.addReaction"
+          :class="toggleClass"
+          class="gl-relative gl-h-full"
+          data-testid="add-reaction-button"
+        >
+          <slot name="button-content">
+            <span class="reaction-control-icon reaction-control-icon-neutral">
+              <gl-icon class="award-control-icon-neutral gl-button-icon" name="slight-smile" />
+            </span>
+            <span class="reaction-control-icon reaction-control-icon-positive">
+              <gl-icon class="award-control-icon-positive gl-button-icon" name="smiley" />
+            </span>
+            <span class="reaction-control-icon reaction-control-icon-super-positive">
+              <gl-icon class="award-control-icon-super-positive gl-button-icon" name="smile" />
+            </span>
+          </slot>
+        </gl-button>
+      </template>
+
+      <template #header>
+        <gl-search-box-by-type
+          ref="searchValue"
+          v-model="searchValue"
+          class="add-reaction-search gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"
+          borderless
+          autofocus
+          debounce="500"
+          :aria-label="__('Search for an emoji')"
+          @input="onSearchInput"
+        />
       </template>
-      <gl-search-box-by-type
-        v-model="searchValue"
-        class="gl-mx-5! gl-mb-2!"
-        autofocus
-        debounce="500"
-        :aria-label="__('Search for an emoji')"
-        @input="onSearchInput"
-      />
+
       <div
         v-show="!searchValue"
-        class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
+        class="award-list gl-display-flex gl-border-b-solid gl-border-gray-100 gl-border-b-1"
       >
         <gl-button
           v-for="(category, index) in categoryNames"
@@ -154,9 +189,10 @@ export default {
           :icon="category.icon"
           :aria-label="category.name"
           @click="scrollToCategory(category.name)"
+          @keydown.enter="scrollToCategory(category.name)"
         />
       </div>
-      <emoji-list :search-value="searchValue">
+      <emoji-list v-if="isVisible" :search-value="searchValue">
         <template #default="{ filteredCategories }">
           <virtual-list
             ref="virtualScoller"
@@ -176,11 +212,10 @@ export default {
           </virtual-list>
         </template>
       </emoji-list>
-      <template v-if="newCustomEmojiPath" #footer>
-        <gl-dropdown-item :href="newCustomEmojiPath">
-          {{ __('Create new emoji') }}
-        </gl-dropdown-item>
-      </template>
-    </gl-dropdown>
+
+      <gl-disclosure-dropdown-group v-if="newCustomEmojiPath" bordered class="gl-mt-0!">
+        <gl-disclosure-dropdown-item :item="newCustomEmoji" />
+      </gl-disclosure-dropdown-group>
+    </gl-disclosure-dropdown>
   </div>
 </template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 5a1795d747915617be77adb14796774cec7ecce0..963557ff1b3d81afe7ae2f1e0fc1b2ba977affa6 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -22,7 +22,6 @@ import TimelineEventButton from './note_actions/timeline_event_button.vue';
 
 export default {
   i18n: {
-    addReactionLabel: __('Add reaction'),
     editCommentLabel: __('Edit comment'),
     deleteCommentLabel: __('Delete comment'),
     moreActionsLabel: __('More actions'),
@@ -317,8 +316,6 @@ export default {
     />
     <emoji-picker
       v-if="canAwardEmoji"
-      v-gl-tooltip
-      :title="$options.i18n.addReactionLabel"
       toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary"
       data-testid="note-emoji-button"
       @click="setAwardEmoji"
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 19062e10758e30143c1693b922aacc5d1870c243..8786dccecc4fe117a8346d836827203be729921c 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -185,7 +185,6 @@ export default {
         <emoji-picker
           dropdown-class="gl-h-full"
           toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
-          boundary="viewport"
           :right="false"
           @click="handleEmojiClick"
         >
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
index 88efcfa46e7f810bc2e94ce8c7d4d1c5657b95c0..88fa9ab7995224af2b37bff73c7a1c5f9c983aec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -18,7 +18,7 @@ export const FAILURE_REASONS = {
   need_rebase: __('Merge request must be rebased, because a fast-forward merge is not possible.'),
   not_approved: __('All required approvals must be given.'),
   policies_denied: __('Denied licenses must be removed or approved.'),
-  merge_request_blocked: __('Merge request is blocked by another merge request.'),
+  merge_request_blocked: __('Merge request dependencies have been merged.'),
   status_checks_must_pass: __('Status checks must pass.'),
   jira_association_missing: __('Either the title or description must reference a Jira issue.'),
 };
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index 3c19df9c19689dd5e22916a37e8ab674bc0ff20e..1750551db7bbfb55ef96ec1b67f91b357ad7d647 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,5 +1,5 @@
 <script>
-import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
 import { groupBy } from 'lodash';
 import SafeHtml from '~/vue_shared/directives/safe_html';
 import EmojiPicker from '~/emoji/components/picker.vue';
@@ -13,7 +13,6 @@ const NO_USER_ID = -1;
 export default {
   components: {
     GlButton,
-    GlIcon,
     EmojiPicker,
   },
   directives: {
@@ -45,11 +44,6 @@ export default {
       required: false,
       default: 'selected',
     },
-    boundary: {
-      type: String,
-      required: false,
-      default: '',
-    },
   },
   data() {
     return {
@@ -197,28 +191,13 @@ export default {
     </gl-button>
     <div v-if="canAwardEmoji" class="award-menu-holder gl-my-2">
       <emoji-picker
-        v-gl-tooltip.viewport
-        :title="__('Add reaction')"
         :toggle-class="['add-reaction-button btn-icon gl-relative!', { 'is-active': isMenuOpen }]"
         :right="false"
-        :boundary="boundary"
         data-testid="emoji-picker"
         @click="handleAward"
         @shown="setIsMenuOpen(true)"
         @hidden="setIsMenuOpen(false)"
-      >
-        <template #button-content>
-          <span class="reaction-control-icon reaction-control-icon-neutral">
-            <gl-icon name="slight-smile" />
-          </span>
-          <span class="reaction-control-icon reaction-control-icon-positive">
-            <gl-icon name="smiley" />
-          </span>
-          <span class="reaction-control-icon reaction-control-icon-super-positive">
-            <gl-icon name="smile" />
-          </span>
-        </template>
-      </emoji-picker>
+      />
     </div>
   </div>
 </template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index c3b3c0e6db7f7dc89c178bbc5ebd943c16b58ecf..ab93ab3cbdd01e4e6c67865844c1a4c880ef28b5 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -29,9 +29,6 @@ export default {
     },
   },
   computed: {
-    awardsListBoundary() {
-      return this.isModal ? '.modal-body' : '';
-    },
     awards() {
       return this.note.awardEmoji.nodes.map((award) => {
         return {
@@ -93,7 +90,6 @@ export default {
     :awards="awards"
     :can-award-emoji="hasAwardEmojiPermission"
     :current-user-id="currentUserId"
-    :boundary="awardsListBoundary"
     class="gl-px-2"
     @award="handleAward($event)"
   />
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index e11fa7d880180521a07f955d5d355bcf04cc4ed2..5f68ff64759795ace893ec363747d655ee094c0f 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -269,7 +269,6 @@
   // other gl-buttons despite all child elements being set to
   // `position:absolute`
 
-
   .reaction-control-icon {
     .gl-icon {
       height: $default-icon-size;
@@ -335,3 +334,25 @@
     }
   }
 }
+
+.add-reaction-search {
+  $input-focus-ring-border-radius: calc(#{$gl-border-radius-large} - #{$gl-border-size-1});
+
+  input {
+    border-top-left-radius: $input-focus-ring-border-radius !important;
+    border-top-right-radius: $input-focus-ring-border-radius !important;
+  }
+}
+
+.award-list .gl-button:focus {
+  @include gl-focus(
+    $outline: true,
+    $outline-offset: -2px,
+    $important: true
+  );
+  box-shadow: none !important;
+}
+
+.design-note.note-form .emoji-picker .gl-new-dropdown-panel {
+  left: 0 !important;
+}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 779567978175591c0e910d7d76bef068182ac2c5..39b60aa3fae125ce3391227ba2c064fd525259a3 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -50,8 +50,9 @@ gl-emoji {
   }
 }
 
-.emoji-picker .gl-dropdown .dropdown-menu {
-  width: 350px;
+.emoji-picker .gl-dropdown .dropdown-menu,
+.emoji-picker .gl-new-dropdown .gl-new-dropdown-panel {
+  width: 350px !important;
 }
 
 .emoji-picker-category-tab {
diff --git a/app/finders/concerns/valid_or_default.rb b/app/finders/concerns/valid_or_default.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0795468689149056ee02e4d825e898ee256f9b5e
--- /dev/null
+++ b/app/finders/concerns/valid_or_default.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ValidOrDefault
+  def valid_or_default(value, valid_values, default)
+    return value if valid_values.include?(value)
+
+    default
+  end
+end
diff --git a/app/finders/projects/ml/model_version_finder.rb b/app/finders/projects/ml/model_version_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2e6d757ee9e6fa7ddb71ccc83f2c25a0b2ddab9c
--- /dev/null
+++ b/app/finders/projects/ml/model_version_finder.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Projects
+  module Ml
+    class ModelVersionFinder
+      include Gitlab::Utils::StrongMemoize
+      include ValidOrDefault
+
+      VALID_ORDER_BY = %w[version created_at id].freeze
+      VALID_SORT = %w[asc desc].freeze
+
+      def initialize(model, params = {})
+        @model = model
+        @params = params
+      end
+
+      def execute
+        relation
+      end
+
+      private
+
+      def relation
+        @versions = ::Ml::ModelVersion.for_model(model).including_relations
+
+        @versions = by_version
+        ordered
+      end
+      strong_memoize_attr :relation
+
+      def by_version
+        return versions unless params[:version].present?
+
+        versions.by_version(params[:version])
+      end
+
+      def ordered
+        order_by = valid_or_default(params[:order_by]&.downcase, VALID_ORDER_BY, 'id')
+        sort = valid_or_default(params[:sort]&.downcase, VALID_SORT, 'desc')
+
+        return versions.order_by_version(sort) if order_by == 'version'
+
+        versions.order_by("#{order_by}_#{sort}").with_order_id_desc
+      end
+
+      attr_reader :params, :model, :versions
+    end
+  end
+end
diff --git a/app/graphql/resolvers/ml/find_model_versions_resolver.rb b/app/graphql/resolvers/ml/find_model_versions_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5356a06bbc7b665a37ddbdba0fa480339a34e900
--- /dev/null
+++ b/app/graphql/resolvers/ml/find_model_versions_resolver.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Ml
+    class FindModelVersionsResolver < Resolvers::BaseResolver
+      extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+
+      type ::Types::Ml::ModelType.connection_type, null: true
+
+      argument :version, GraphQL::Types::String,
+        required: false,
+        description: 'Search for versions where the name includes the string.'
+
+      argument :order_by, ::Types::Ml::ModelVersionsOrderByEnum,
+        required: false,
+        description: 'Ordering column. Default is created_at.'
+
+      argument :sort, ::Types::SortDirectionEnum,
+        required: false,
+        description: 'Ordering column. Default is desc.'
+
+      def resolve(**args)
+        return unless Ability.allowed?(current_user, :read_model_registry, object.project)
+
+        find_params = {
+          version: args[:version],
+          order_by: args[:order_by].to_s,
+          sort: args[:sort].to_s
+        }
+
+        ::Projects::Ml::ModelVersionFinder.new(object, find_params).execute
+      end
+    end
+  end
+end
diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
index d2d0cfd23a47249bbd46b73556f5ca8e44e798a1..50e8c1387f25482f80b2d115d41409398a525a0a 100644
--- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
+++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
@@ -38,7 +38,7 @@ class DetailedMergeStatusEnum < BaseEnum
             description: 'Merge request must be approved before merging.'
       value 'BLOCKED_STATUS',
             value: :merge_request_blocked,
-            description: 'Merge request is blocked by another merge request.'
+            description: 'Merge request dependencies have been merged.'
       value 'POLICIES_DENIED',
             value: :policies_denied,
             description: 'There are denied policies for the merge request.'
diff --git a/app/graphql/types/ml/model_type.rb b/app/graphql/types/ml/model_type.rb
index a26d50cbdc474756236f549d519a977f5414d709..5ed8a7c2883d5a87168f913070649bd9e10f2c13 100644
--- a/app/graphql/types/ml/model_type.rb
+++ b/app/graphql/types/ml/model_type.rb
@@ -27,7 +27,8 @@ class ModelType < ::Types::BaseObject
         description: 'Map of links to perform actions on the model.'
 
       field :versions, ::Types::Ml::ModelVersionType.connection_type, null: true,
-        description: 'Versions of the model.'
+        description: 'Versions of the model.',
+        resolver: ::Resolvers::Ml::FindModelVersionsResolver
 
       field :candidates, ::Types::Ml::CandidateType.connection_type, null: true,
         description: 'Version candidates of the model.'
diff --git a/app/graphql/types/ml/model_versions_order_by_enum.rb b/app/graphql/types/ml/model_versions_order_by_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38f8d19579a9671bf07e16a9d2603e1773a6f2e8
--- /dev/null
+++ b/app/graphql/types/ml/model_versions_order_by_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+  module Ml
+    class ModelVersionsOrderByEnum < BaseEnum
+      graphql_name 'MlModelVersionsOrderBy'
+      description 'Field names for ordering machine learning model versions'
+
+      value 'VERSION', 'Ordered by name.', value: :name
+      value 'CREATED_AT', 'Ordered by creation time.', value: :created_at
+      value 'ID', 'Ordered by id.', value: :id
+    end
+  end
+end
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index 1b3313c803a0364db145c74600f674a88182ab09..037cf58f532c00770a0067e6f389f69481180ccb 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -3,6 +3,7 @@
 module Ml
   class ModelVersion < ApplicationRecord
     include Presentable
+    include Sortable
 
     validates :project, :model, presence: true
 
@@ -27,6 +28,10 @@ class ModelVersion < ApplicationRecord
 
     scope :order_by_model_id_id_desc, -> { order('model_id, id DESC') }
     scope :latest_by_model, -> { order_by_model_id_id_desc.select('DISTINCT ON (model_id) *') }
+    scope :by_version, ->(version) { where("version LIKE ?", "#{sanitize_sql_like(version)}%") } # rubocop:disable GitlabSecurity/SqlInjection -- we are sanitizing
+    scope :for_model, ->(model) { where(project: model.project, model: model) }
+    scope :including_relations, -> { includes(:project, :model, :candidate) }
+    scope :order_by_version, ->(order) { reorder(version: order) }
 
     def add_metadata(metadata_key_value)
       return unless metadata_key_value.present?
diff --git a/db/migrate/20240119144837_add_index_to_ml_model_versions_on_created_at_on_model_id.rb b/db/migrate/20240119144837_add_index_to_ml_model_versions_on_created_at_on_model_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4d7179e5f2fc242a7115020adde60ad483dff544
--- /dev/null
+++ b/db/migrate/20240119144837_add_index_to_ml_model_versions_on_created_at_on_model_id.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToMlModelVersionsOnCreatedAtOnModelId < Gitlab::Database::Migration[2.2]
+  milestone '16.9'
+
+  disable_ddl_transaction!
+
+  INDEX_NAME = 'index_ml_model_versions_on_created_at_on_model_id'
+
+  def up
+    add_concurrent_index :ml_model_versions, [:model_id, :created_at], name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :ml_model_versions, name: INDEX_NAME
+  end
+end
diff --git a/db/schema_migrations/20240119144837 b/db/schema_migrations/20240119144837
new file mode 100644
index 0000000000000000000000000000000000000000..2c7397c07477d15e1cc7b9cef03cafed27ed02bc
--- /dev/null
+++ b/db/schema_migrations/20240119144837
@@ -0,0 +1 @@
+433baa66de293d4fcc9aa2b4eae44cf1f99ad2defb42645d8192c1d8956bcb38
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7f2dde4b44595a3aa7fe4a220e39e7ab77e17dea..ee9265df8d0a182dcc66746220b50ad5a450e42d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -34404,6 +34404,8 @@ CREATE INDEX index_ml_experiments_on_user_id ON ml_experiments USING btree (user
 
 CREATE INDEX index_ml_model_version_metadata_on_project_id ON ml_model_version_metadata USING btree (project_id);
 
+CREATE INDEX index_ml_model_versions_on_created_at_on_model_id ON ml_model_versions USING btree (model_id, created_at);
+
 CREATE INDEX index_ml_model_versions_on_package_id ON ml_model_versions USING btree (package_id);
 
 CREATE INDEX index_ml_model_versions_on_project_id ON ml_model_versions USING btree (project_id);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b1128c1db031c208164ff73b048fe8a668216545..32f1ab9c5f41fff8e48e2712c3e6a21e79896564 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -23166,7 +23166,26 @@ Machine learning model in the model registry.
 | <a id="mlmodellatestversion"></a>`latestVersion` | [`MlModelVersion`](#mlmodelversion) | Latest version of the model. |
 | <a id="mlmodelname"></a>`name` | [`String!`](#string) | Name of the model. |
 | <a id="mlmodelversioncount"></a>`versionCount` | [`Int`](#int) | Count of versions in the model. |
-| <a id="mlmodelversions"></a>`versions` | [`MlModelVersionConnection`](#mlmodelversionconnection) | Versions of the model. (see [Connections](#connections)) |
+
+#### Fields with arguments
+
+##### `MlModel.versions`
+
+Versions of the model.
+
+Returns [`MlModelVersionConnection`](#mlmodelversionconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mlmodelversionsorderby"></a>`orderBy` | [`MlModelVersionsOrderBy`](#mlmodelversionsorderby) | Ordering column. Default is created_at. |
+| <a id="mlmodelversionssort"></a>`sort` | [`SortDirectionEnum`](#sortdirectionenum) | Ordering column. Default is desc. |
+| <a id="mlmodelversionsversion"></a>`version` | [`String`](#string) | Search for versions where the name includes the string. |
 
 ### `MlModelVersion`
 
@@ -30424,7 +30443,7 @@ Detailed representation of whether a GitLab merge request can be merged.
 
 | Value | Description |
 | ----- | ----------- |
-| <a id="detailedmergestatusblocked_status"></a>`BLOCKED_STATUS` | Merge request is blocked by another merge request. |
+| <a id="detailedmergestatusblocked_status"></a>`BLOCKED_STATUS` | Merge request dependencies have been merged. |
 | <a id="detailedmergestatusbroken_status"></a>`BROKEN_STATUS` | Can not merge the source into the target branch, potential conflict. |
 | <a id="detailedmergestatuschecking"></a>`CHECKING` | Currently checking for mergeability. |
 | <a id="detailedmergestatusci_must_pass"></a>`CI_MUST_PASS` | Pipeline must succeed before merging. |
@@ -31162,6 +31181,16 @@ Milestone ID wildcard values.
 | <a id="milestonewildcardidstarted"></a>`STARTED` | Milestone assigned is open and started (start date <= today). |
 | <a id="milestonewildcardidupcoming"></a>`UPCOMING` | Milestone assigned is due in the future (due date > today). |
 
+### `MlModelVersionsOrderBy`
+
+Field names for ordering machine learning model versions.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="mlmodelversionsorderbycreated_at"></a>`CREATED_AT` | Ordered by creation time. |
+| <a id="mlmodelversionsorderbyid"></a>`ID` | Ordered by id. |
+| <a id="mlmodelversionsorderbyversion"></a>`VERSION` | Ordered by name. |
+
 ### `MlModelsOrderBy`
 
 Values for ordering machine learning models by a specific field.
diff --git a/doc/architecture/blueprints/cells/index.md b/doc/architecture/blueprints/cells/index.md
index 6f00fe4e61e508990463ea13db495cb1c74fc27f..fa206f9f7492e0ae83fd70471344e3e4b690e131 100644
--- a/doc/architecture/blueprints/cells/index.md
+++ b/doc/architecture/blueprints/cells/index.md
@@ -18,6 +18,10 @@ Cells is a new architecture for our software as a service platform. This archite
 
 For more information about Cells, see also:
 
+## Cells Iterations
+
+See Cell 1.0, Cell 1.5, and Cell 2.0.
+
 ## Goals
 
 See [Goals, Glossary and Requirements](goals.md).
@@ -30,8 +34,8 @@ See [Deployment Architecture](deployment-architecture.md).
 
 We can't ship the entire Cells architecture in one go - it is too large.
 Instead, we are defining key work streams required by the project.
+For each work stream, we need to define the effort necessary to make features compliant with Cell 1.0, Cell 1.5, and Cell 2.0, respectively.
 
-Not all objectives need to be fulfilled to reach production readiness.
 It is expected that some objectives will not be completed for General Availability (GA), but will be enough to run Cells in production.
 
 ### 1. Data access layer
@@ -70,10 +74,10 @@ Under this objective the following steps are expected:
 
     Ensure that migrations can be run independently between Cells, and we safely handle migrations of shared data in a way that does not impact other Cells.
 
-### 2. Essential workflows
+### 2. Workflows
 
 To make Cells viable we require to define and support essential workflows before we can consider the Cells to be of Beta quality.
-Essential workflows are meant to cover the majority of application functionality that makes the product mostly useable, but with some caveats.
+Workflows are meant to cover the majority of application functionality that makes the product mostly useable, but with some caveats.
 
 The current approach is to define workflows from top to bottom.
 The order defines the presumed priority of the items.
@@ -118,7 +122,7 @@ The first 2-3 quarters are required to define a general split of data, and build
 
 1. **User can push to Git repository.**
 
-    The purpose is to ensure that essential joins from the Projects table are properly attributed to be Cell-local, and as a result the essential Git workflow is supported.
+    The purpose is to ensure that essential joins from the Projects table are properly attributed to be Cell-local, and as a result the Git workflow is supported.
 
 1. **User can run CI pipeline.**
 
@@ -144,9 +148,21 @@ The first 2-3 quarters are required to define a general split of data, and build
 
     The purpose is to have many Organizations per Cell, but never have a single Organization spanning across many Cells. This is required to ensure that information shown within an Organization is isolated, and does not require fetching information from other Cells.
 
+Some of the following workflows might need to be supported, depending on the group's decision.
+This list is not exhaustive of work needed to be done.
+
+1. **User can use all Group-level features.**
+1. **User can use all Project-level features.**
+1. **User can share Groups with other Groups in an Organization.**
+1. **User can create system webhook.**
+1. **User can upload and manage packages.**
+1. **User can manage security detection features.**
+1. **User can manage Kubernetes integration.**
+1. TBD
+
 #### Dependencies
 
-We have identified the following dependencies between the essential workflows.
+We have identified the following dependencies between workflows.
 
 ```mermaid
 flowchart TD
@@ -166,25 +182,11 @@ flowchart TD
     L --> E[Create file in repository]
 ```
 
-### 3. Additional workflows
-
-Some of these additional workflows might need to be supported, depending on the group decision.
-This list is not exhaustive of work needed to be done.
-
-1. **User can use all Group-level features.**
-1. **User can use all Project-level features.**
-1. **User can share Groups with other Groups in an Organization.**
-1. **User can create system webhook.**
-1. **User can upload and manage packages.**
-1. **User can manage security detection features.**
-1. **User can manage Kubernetes integration.**
-1. TBD
-
-### 4. Routing layer
+### 3. Routing layer
 
 See [Cells: Routing Service](routing-service.md).
 
-### 5. Cell deployment
+### 4. Cell deployment
 
 We will run many Cells.
 To manage them easier, we need to have consistent deployment procedures for Cells, including a way to deploy, manage, migrate, and monitor.
@@ -194,7 +196,7 @@ We are very likely to use tooling made for [GitLab Dedicated](https://about.gitl
 1. **Extend GitLab Dedicated to support GCP.**
 1. TBD
 
-### 6. Migration
+### 5. Migration
 
 When we reach production and are able to store new Organizations on new Cells, we need to be able to divide big Cells into many smaller ones.
 
@@ -220,8 +222,8 @@ We are following the [Support for Experiment, Beta, and Generally Available feat
 
 Expectations:
 
-- We can deploy a Cell on staging or another testing environment by using a separate domain (for example `cell2.staging.gitlab.com`) using [Cell deployment](#5-cell-deployment) tooling.
-- User can create Organization, Group and Project, and run some of the [essential workflows](#2-essential-workflows).
+- We can deploy a Cell on staging or another testing environment by using a separate domain (for example `cell2.staging.gitlab.com`) using [Cell deployment](#4-cell-deployment) tooling.
+- User can create Organization, Group and Project, and run some of the [workflows](#2-workflows).
 - It is not expected to be able to run a router to serve all requests under a single domain.
 - We expect data loss of data stored on additional Cells.
 - We expect to tear down and create many new Cells to validate tooling.
@@ -231,8 +233,8 @@ Expectations:
 Expectations:
 
 - We can run many Cells under a single domain (ex. `staging.gitlab.com`).
-- All features defined in [essential workflows](#2-essential-workflows) are supported.
-- Not all aspects of the [routing layer](#4-routing-layer) are finalized.
+- All features defined in [workflows](#2-workflows) are supported.
+- Not all aspects of the [routing layer](#3-routing-layer) are finalized.
 - We expect additional Cells to be stable with minimal data loss.
 
 ### 3. GA
@@ -240,17 +242,14 @@ Expectations:
 Expectations:
 
 - We can run many Cells under a single domain (for example, `staging.gitlab.com`).
-- All features defined in [essential workflows](#2-essential-workflows) are supported.
-- All features of the [routing layer](#4-routing-layer) are supported.
-- Most of the [additional workflows](#3-additional-workflows) are supported.
-- We don't expect to support any of the [migration](#6-migration) aspects.
+- All features of the [routing layer](#3-routing-layer) are supported.
+- We don't expect to support any of the [migration](#5-migration) aspects.
 
 ### 4. Post GA
 
 Expectations:
 
-- We support all [additional workflows](#3-additional-workflows).
-- We can [migrate](#6-migration) existing Organizations onto new Cells.
+- We can [migrate](#5-migration) existing Organizations onto new Cells.
 
 ## Iteration plan
 
@@ -260,22 +259,22 @@ It is expected that initial iterations will be rather slow, because they require
 ### [Iteration 1](https://gitlab.com/groups/gitlab-org/-/epics/9667) (FY24Q1)
 
 - Data access layer: Initial Admin Area settings are shared across cluster.
-- Essential workflows: Allow to share cluster-wide data with database-level data access layer.
+- Workflow: Allow to share cluster-wide data with database-level data access layer.
 
 ### [Iteration 2](https://gitlab.com/groups/gitlab-org/-/epics/9813) (FY24Q2-FY24Q3)
 
-- Essential workflows: User accounts are shared across cluster.
-- Essential workflows: User can create Group.
+- Workflow: User accounts are shared across cluster.
+- Workflow: User can create Group.
 
 ### [Iteration 3](https://gitlab.com/groups/gitlab-org/-/epics/10997) (FY24Q4-FY25Q1)
 
-- Essential workflows: User can create Project.
+- Workflow: User can create Project.
 - Routing: Technology.
 - Routing: Cell discovery.
 
 ### [Iteration 4](https://gitlab.com/groups/gitlab-org/-/epics/10998) (FY25Q1-FY25Q2)
 
-- Essential workflows: User can create Organization on Cell 2.
+- Workflow: User can create Organization on Cell 2.
 
 ### Iteration 5..N - starting FY25Q3
 
@@ -284,16 +283,16 @@ It is expected that initial iterations will be rather slow, because they require
 - Data access layer: Data access layer.
 - Routing: User can use single domain to interact with many Cells.
 - Cell deployment: Extend GitLab Dedicated to support GCP.
-- Essential workflows: User can create Project with a README file.
-- Essential workflows: User can push to Git repository.
-- Essential workflows: User can run CI pipeline.
-- Essential workflows: Instance-wide settings are shared across cluster.
-- Essential workflows: User can change profile avatar that is shared in cluster.
-- Essential workflows: User can create issue.
-- Essential workflows: User can create merge request, and merge it after it is green.
-- Essential workflows: User can manage Group and Project members.
-- Essential workflows: User can manage instance-wide runners.
-- Essential workflows: User is part of Organization and can only see information from the Organization.
+- Workflow: User can create Project with a README file.
+- Workflow: User can push to Git repository.
+- Workflow: User can run CI pipeline.
+- Workflow: Instance-wide settings are shared across cluster.
+- Workflow: User can change profile avatar that is shared in cluster.
+- Workflow: User can create issue.
+- Workflow: User can create merge request, and merge it after it is green.
+- Workflow: User can manage Group and Project members.
+- Workflow: User can manage instance-wide runners.
+- Workflow: User is part of Organization and can only see information from the Organization.
 - Routing: Router endpoints classification.
 - Routing: GraphQL and other ambiguous endpoints.
 - Data access layer: Allow to share cluster-wide data with database-level data access layer.
diff --git a/doc/development/cells/index.md b/doc/development/cells/index.md
index cd222e7b209ace28ede23693941551adb5ee5d07..fea5594abe4ccd0ba73faf9eeb97ff4f3038c6b7 100644
--- a/doc/development/cells/index.md
+++ b/doc/development/cells/index.md
@@ -11,7 +11,7 @@ For background of GitLab Cells, refer to the [blueprint](../../architecture/blue
 ## Essential and additional workflows
 
 To make the application work within the GitLab Cells architecture, we need to fix various
-[workflows](../../architecture/blueprints/cells/index.md#2-essential-workflows).
+[workflows](../../architecture/blueprints/cells/index.md#2-workflows).
 
 Here is the suggested approach:
 
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index ad41d21a9e7462266062116ac3c865ec8766d09f..60da0cd146f6f1fe14dac7aab0cf4976175bd98d 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -6,7 +6,7 @@ info: Any user with at least the Maintainer role can merge updates to this conte
 
 # Distributed tracing development guidelines
 
-GitLab is instrumented for distributed tracing. Distributed Tracing in GitLab is currently considered **experimental**, as it has not yet been tested at scale on GitLab.com.
+GitLab is instrumented for distributed tracing. Distributed tracing in GitLab is currently considered **experimental**, as it has not yet been tested at scale on GitLab.com.
 
 According to [Open Tracing](https://opentracing.io/docs/overview/what-is-tracing/):
 
diff --git a/doc/operations/tracing.md b/doc/operations/tracing.md
index f6ac0b6174df61e56d694dd04aa0b61ac0001861..df778d978af7f0deeedb598ac8e6473a52a48703 100644
--- a/doc/operations/tracing.md
+++ b/doc/operations/tracing.md
@@ -40,7 +40,7 @@ To enable tracing in a project:
 1. Select **Monitor > Tracing**.
 1. Select **Enable**.
 
-## Configure your application to use the OpenTelemetry exporter
+### Configure your application to use the OpenTelemetry exporter
 
 Next, configure your application to send traces to GitLab.
 
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 57fe4e8c277fa99722ccc9f8fbc1a619425ba641..fc297317dead5b43dc045f5c3fcae76b3a0a14b2 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -165,6 +165,8 @@ In wikis, you can also add and edit diagrams created with the [diagrams.net edit
 
 ### Mermaid
 
+> Support for Entity Relationship diagrams and mindmaps [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384386) in GitLab 16.0.
+
 [View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#mermaid).
 
 Visit the [official page](https://mermaidjs.github.io/) for more details. The
diff --git a/ee/app/controllers/concerns/ee/lfs_request.rb b/ee/app/controllers/concerns/ee/lfs_request.rb
index 42bb6f69657592aac90419b506276f646e744e79..9459f0e9048a22ba25d2e726402b64a01cb4d0b3 100644
--- a/ee/app/controllers/concerns/ee/lfs_request.rb
+++ b/ee/app/controllers/concerns/ee/lfs_request.rb
@@ -21,15 +21,14 @@ def lfs_forbidden!
 
     override :limit_exceeded?
     def limit_exceeded?
-      strong_memoize(:limit_exceeded) do
-        size_checker.changes_will_exceed_size_limit?(lfs_push_size, project)
-      end
+      size_checker.changes_will_exceed_size_limit?(lfs_objects_change_size, project)
     end
+    strong_memoize_attr :limit_exceeded?
 
     def render_size_error
       render(
         json: {
-          message: size_checker.error_message.push_error(lfs_push_size),
+          message: size_checker.error_message.push_error(lfs_objects_change_size),
           documentation_url: help_url
         },
         content_type: ::LfsRequest::CONTENT_TYPE,
@@ -57,10 +56,23 @@ def size_checker
       project.repository_size_checker
     end
 
+    def lfs_objects_change_size
+      return 0 if lfs_push_size == 0
+      return lfs_push_size if existing_pushed_lfs_objects_size == 0
+
+      lfs_push_size - existing_pushed_lfs_objects_size
+    end
+    strong_memoize_attr :lfs_objects_change_size
+
+    def existing_pushed_lfs_objects_size
+      oids = objects_oids
+      project.lfs_objects.for_oids(oids).sum(:size)
+    end
+
+    # objects can contain LFS files that may not have been saved yet.
     def lfs_push_size
-      strong_memoize(:lfs_push_size) do
-        objects.sum { |o| o[:size] }
-      end
+      objects.sum { |o| o[:size] }
     end
+    strong_memoize_attr :lfs_push_size
   end
 end
diff --git a/ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb b/ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb
index ca57f00e77c2bbad583b3854de900fc32876a8eb..631de04bbaa73e364ab58e8e7f0802ed57998c7e 100644
--- a/ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb
+++ b/ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb
@@ -22,7 +22,7 @@
   it 'disables merge button when blocking merge request is open' do
     click_button 'Expand merge checks'
 
-    expect(page).to have_content('Merge request is blocked by another merge request.')
+    expect(page).to have_content('Merge request dependencies have been merged.')
   end
 
   context 'merged blocking merge request' do
diff --git a/ee/spec/requests/lfs_http_spec.rb b/ee/spec/requests/lfs_http_spec.rb
index 95475e5dafc5778d0162a1f721ed76321ad7f586..2095ec5fa5fd39e8524fccc669955ac19a9aa21c 100644
--- a/ee/spec/requests/lfs_http_spec.rb
+++ b/ee/spec/requests/lfs_http_spec.rb
@@ -180,6 +180,82 @@
             end
           end
 
+          context 'when push includes an lfs object that already exists' do
+            let(:existing_object) { create(:lfs_object, :with_file, size: 150.megabytes) }
+
+            let(:body) do
+              {
+                'operation' => 'upload',
+                'objects' => [
+                  {
+                    'oid' => sample_oid,
+                    'size' => sample_size
+                  },
+                  {
+                    'oid' => existing_object.oid,
+                    'size' => existing_object.size
+                  }
+                ]
+              }
+            end
+
+            before do
+              create(
+                :lfs_objects_project,
+                project: project,
+                lfs_object: existing_object
+              )
+            end
+
+            it 'uses new objects for change size' do
+              expect_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
+                expect(checker).to receive(:changes_will_exceed_size_limit?).with(sample_size, project)
+              end
+
+              batch_request
+            end
+
+            context 'when the push will not go over the repository size limit' do
+              let(:sample_size) { 75.megabytes }
+
+              before do
+                allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
+                  allow(checker).to receive_messages(
+                    enabled?: true,
+                    current_size: 150.megabytes,
+                    limit: 300.megabytes
+                  )
+                end
+              end
+
+              it 'responds with status 200' do
+                batch_request
+
+                expect(response).to have_gitlab_http_status(:ok)
+              end
+            end
+
+            context 'when the push will go over the repository size limit' do
+              let(:sample_size) { 275.megabytes }
+
+              before do
+                allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
+                  allow(checker).to receive_messages(
+                    enabled?: true,
+                    current_size: 150.megabytes,
+                    limit: 300.megabytes
+                  )
+                end
+              end
+
+              it 'responds with status 406' do
+                batch_request
+
+                expect(response).to have_gitlab_http_status(:not_acceptable)
+              end
+            end
+          end
+
           context 'when pushing to a subgroup project' do
             let(:sample_size) { 150.megabytes }
             let(:sample_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
@@ -274,7 +350,7 @@
     end
   end
 
-  describe 'when pushing a lfs object' do
+  describe 'when pushing an lfs object' do
     before do
       enable_lfs
     end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 47404a0b5ff200a9b4623731a687c1ff0965bcea..cbf68ac3e59a3db1ba0e1d88796de6c2cac3a766 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -30450,10 +30450,10 @@ msgstr ""
 msgid "Merge request dependencies"
 msgstr ""
 
-msgid "Merge request events"
+msgid "Merge request dependencies have been merged."
 msgstr ""
 
-msgid "Merge request is blocked by another merge request."
+msgid "Merge request events"
 msgstr ""
 
 msgid "Merge request must be open."
diff --git a/spec/features/merge_request/user_creates_custom_emoji_spec.rb b/spec/features/merge_request/user_creates_custom_emoji_spec.rb
index 35593836dab8bdeb5fe34544e1cc4ecb79bc4616..142a2b20e3d1ad9a468d2f3fdf9554b2e1be569a 100644
--- a/spec/features/merge_request/user_creates_custom_emoji_spec.rb
+++ b/spec/features/merge_request/user_creates_custom_emoji_spec.rb
@@ -20,8 +20,8 @@
       wait_for_requests
     end
 
-    it 'shows link to create custom emoji' do
-      first('.add-reaction-button').click
+    it 'shows link to create custom emoji', :js do
+      find_by_testid('add-reaction-button').click
 
       wait_for_requests
 
@@ -48,8 +48,8 @@
       wait_for_requests
     end
 
-    it 'shows link to create custom emoji' do
-      first('.add-reaction-button').click
+    it 'shows link to create custom emoji', :js do
+      find_by_testid('add-reaction-button').click
 
       wait_for_requests
 
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
index bd9d1092e17a817a975a4081268094afc8ead3ce..ab95b20ad40f8474a9d3093de59871389db4f919 100644
--- a/spec/features/projects/issues/design_management/user_views_design_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb
@@ -16,7 +16,7 @@ def add_diff_note_emoji(diff_note, emoji_name)
     page.within(first(".image-notes li#note_#{diff_note.id}.design-note")) do
       page.find('[data-testid="note-emoji-button"] .note-emoji-button').click
 
-      page.within('ul.dropdown-menu') do
+      page.within('.emoji-picker') do
         page.find('input[type="search"]').set(emoji_name)
         page.find('button[data-testid="emoji-button"]:first-child').click
       end
diff --git a/spec/finders/concerns/valid_or_default_spec.rb b/spec/finders/concerns/valid_or_default_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e702d254f7888254f72575d39e6934264421ca8e
--- /dev/null
+++ b/spec/finders/concerns/valid_or_default_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ValidOrDefault, feature_category: :shared do
+  using RSpec::Parameterized::TableSyntax
+
+  let(:object) do
+    Class.new do
+      include ValidOrDefault
+    end.new
+  end
+
+  let(:valid_values) { %w[a b c] }
+  let(:default_value) { 'a' }
+
+  describe '.valid_or_default' do
+    where(:value, :output) do
+      'a' | 'a'
+      'b' | 'b'
+      'c'       | 'c'
+      'invalid' | 'a'
+      nil       | 'a'
+    end
+
+    with_them do
+      it 'returns value if value is valid otherwise default' do
+        expect(object.valid_or_default(value, valid_values, default_value)).to be(output)
+      end
+    end
+  end
+end
diff --git a/spec/finders/projects/ml/model_version_finder_spec.rb b/spec/finders/projects/ml/model_version_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be3864487257cfe9ec534c2e9afad7906361d8b0
--- /dev/null
+++ b/spec/finders/projects/ml/model_version_finder_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ModelVersionFinder, feature_category: :mlops do
+  let_it_be(:project) { create(:project) }
+  let_it_be(:model) { create(:ml_models, project: project) }
+  let_it_be(:model_version_2_0_1) { create(:ml_model_versions, model: model, version: '2.0.1') }
+  let_it_be(:model_version_3_0_0) { create(:ml_model_versions, model: model, version: '3.0.0') }
+  let_it_be(:model_version_1_0_1) { create(:ml_model_versions, model: model, version: '1.0.1') }
+  let_it_be(:other_model_version) { create(:ml_model_versions) }
+  let_it_be(:model_versions) { [model_version_2_0_1, model_version_3_0_0, model_version_1_0_1] }
+
+  let(:params) { {} }
+
+  subject(:loaded_versions) { described_class.new(model, params).execute.to_a }
+
+  describe 'default params' do
+    it 'returns models for project ordered by id desc' do
+      is_expected.to contain_exactly(model_version_1_0_1, model_version_3_0_0, model_version_2_0_1)
+    end
+
+    it 'including the latest version and project', :aggregate_failures do
+      expect(loaded_versions[0].association_cached?(:project)).to be(true)
+      expect(loaded_versions[0].association_cached?(:model)).to be(true)
+      expect(loaded_versions[1].association_cached?(:project)).to be(true)
+      expect(loaded_versions[1].association_cached?(:model)).to be(true)
+    end
+  end
+
+  context 'when version is passed' do
+    let(:params) { { version: '2.0' } }
+
+    it 'searches by name' do
+      is_expected.to contain_exactly(model_version_2_0_1)
+    end
+  end
+
+  describe 'sorting' do
+    using RSpec::Parameterized::TableSyntax
+
+    where(:test_case, :order_by, :direction, :expected_order) do
+      'default params'      | nil          | nil    | [2, 1, 0]
+      'ascending order'     | 'id'         | 'ASC'  | [0, 1, 2]
+      'by version'          | 'version'    | 'ASC'  | [2, 0, 1]
+      'by version desc'     | 'version'    | 'DESC' | [1, 0, 2]
+      'invalid sort'        | nil          | 'UP'   | [2, 1, 0]
+      'invalid order by'    | 'INVALID'    | nil    | [2, 1, 0]
+      'order by updated_at' | 'created_at' | nil    | [2, 1, 0]
+    end
+    with_them do
+      let(:params) { { order_by: order_by, sort: direction } }
+
+      it { expect(loaded_versions).to eq(model_versions.values_at(*expected_order)) }
+    end
+  end
+end
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 7ff488f7fcb4f00062b0ef2cfc5f02e5e9125583..8c9bf8da3fb0d72c43e9afb8fd2b5db4c896c866 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -48,7 +48,7 @@ describe('Design note component', () => {
   const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
   const findEditButton = () => wrapper.findByTestId('note-edit');
   const findNoteContent = () => wrapper.findByTestId('note-text');
-  const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+  const findDropdown = () => wrapper.findByTestId('more-actions');
   const findDropdownItems = () => findDropdown().findAllComponents(GlDisclosureDropdownItem);
   const findEditDropdownItem = () => findDropdownItems().at(0);
   const findCopyLinkDropdownItem = () => findDropdownItems().at(1);
@@ -353,7 +353,6 @@ describe('Design note component', () => {
 
       expect(emojiPicker.exists()).toBe(true);
       expect(emojiPicker.props()).toMatchObject({
-        boundary: 'viewport',
         right: false,
       });
     });
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 7ae2884170e1dc8e8b70a722000327001308f597..2542fc237a10f2a4f2d75f3e2c240a89a8dc69bc 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -99,7 +99,6 @@ describe('SetStatusModalWrapper', () => {
     it('renders emoji picker dropdown with custom positioning', () => {
       expect(getEmojiPicker().props()).toMatchObject({
         right: false,
-        boundary: 'viewport',
       });
     });
 
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
index a756bfa6889b6343fad4a1360aa27e5bf2057772..e6c2743975bf1edc92cf3177d6c2b7eaa2d26043 100644
--- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -144,7 +144,6 @@ describe('WorkItemAwardEmoji component', () => {
 
     expect(findAwardsList().exists()).toBe(true);
     expect(findAwardsList().props()).toEqual({
-      boundary: '',
       canAwardEmoji: true,
       currentUserId: 5,
       defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
diff --git a/spec/graphql/resolvers/ml/find_model_versions_resolver_spec.rb b/spec/graphql/resolvers/ml/find_model_versions_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e882b1f980f26ae11376921319dbe1ad65b451b2
--- /dev/null
+++ b/spec/graphql/resolvers/ml/find_model_versions_resolver_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ml::FindModelVersionsResolver, feature_category: :mlops do
+  include GraphqlHelpers
+
+  describe '#resolve' do
+    let_it_be(:project) { create(:project) }
+    let_it_be(:model) { create(:ml_models, project: project) }
+    let_it_be(:model_versions) { create_list(:ml_model_versions, 2, model: model, project: model.project) }
+    let_it_be(:user) { project.owner }
+
+    let(:args) { { version: '1.0', orderBy: 'CREATED_AT', sort: 'desc', invalid: 'blah' } }
+
+    subject(:resolve_model_versions) do
+      force(resolve(described_class, obj: model, ctx: { current_user: user }, args: args))&.to_a
+    end
+
+    context 'when user is allowed and model exists' do
+      it { is_expected.to eq(model_versions.reverse) }
+
+      it 'only passes name, sort_by and order to finder' do
+        expect(::Projects::Ml::ModelVersionFinder).to receive(:new)
+          .with(model, { version: '1.0', order_by: 'created_at', sort: 'desc' })
+          .and_call_original
+
+        resolve_model_versions
+      end
+    end
+
+    context 'when user does not have permission' do
+      before do
+        allow(Ability).to receive(:allowed?).and_call_original
+        allow(Ability).to receive(:allowed?)
+                            .with(user, :read_model_registry, project)
+                            .and_return(false)
+      end
+
+      it { is_expected.to be_nil }
+    end
+  end
+end
diff --git a/spec/models/ml/model_version_spec.rb b/spec/models/ml/model_version_spec.rb
index 9db9f7e34abb45641f7bca04ff441cde95174dab..1c520e29b52d2dfc556d97140a971e3399ab7795 100644
--- a/spec/models/ml/model_version_spec.rb
+++ b/spec/models/ml/model_version_spec.rb
@@ -9,10 +9,10 @@
   let_it_be(:model1) { create(:ml_models, project: base_project) }
   let_it_be(:model2) { create(:ml_models, project: base_project) }
 
-  let_it_be(:model_version1) { create(:ml_model_versions, model: model1) }
-  let_it_be(:model_version2) { create(:ml_model_versions, model: model_version1.model) }
-  let_it_be(:model_version3) { create(:ml_model_versions, model: model2) }
-  let_it_be(:model_version4) { create(:ml_model_versions, model: model_version3.model) }
+  let_it_be(:model_version1) { create(:ml_model_versions, model: model1, version: '4.0.0') }
+  let_it_be(:model_version2) { create(:ml_model_versions, model: model_version1.model, version: '6.0.0') }
+  let_it_be(:model_version3) { create(:ml_model_versions, model: model2, version: '5.0.0') }
+  let_it_be(:model_version4) { create(:ml_model_versions, model: model_version3.model, version: '4.0.1') }
 
   describe 'associations' do
     it { is_expected.to belong_to(:project) }
@@ -229,4 +229,45 @@
       is_expected.to match_array([model_version4, model_version2])
     end
   end
+
+  describe '.including_relations' do
+    subject(:scoped) { described_class.including_relations }
+
+    it 'loads latest version', :aggregate_failures do
+      expect(scoped.first.association_cached?(:project)).to be(true)
+      expect(scoped.first.association_cached?(:model)).to be(true)
+    end
+  end
+
+  describe '.by_version' do
+    subject(:filtered) { described_class.by_version('4.0') }
+
+    it 'returns versions with the prefix' do
+      expect(filtered).to contain_exactly(model_version1, model_version4)
+    end
+  end
+
+  describe '.order_by_version' do
+    subject(:ordered) { described_class.order_by_version(order) }
+
+    context 'when order is asc' do
+      let(:order) { 'asc' }
+
+      it { is_expected.to match_array([model_version1, model_version4, model_version3, model_version2]) }
+    end
+
+    context 'when order is desc' do
+      let(:order) { 'desc' }
+
+      it { is_expected.to match_array([model_version2, model_version3, model_version4, model_version1]) }
+    end
+
+    context 'when order is invalid' do
+      let(:order) { 'invalid' }
+
+      it 'throws error' do
+        expect { ordered }.to raise_error(ArgumentError)
+      end
+    end
+  end
 end
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index 1df92ce70cf9bfd7ac251d2e730b83f71eec0cf0..abc0ed11a6cc8c35a8fee4a4e5ec708d70eeaedc 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -17,9 +17,16 @@ def webmock_allowed_hosts
       hosts.concat(allowed_host_and_ip(ENV['ZOEKT_SEARCH_BASE_URL']))
     end
 
+    # The test Rails server usually runs on 127.0.0.1.
+    # On some development configurations Webpack or Vite may be running on a
+    # different address so explicitly allow connections to that host.
     if Gitlab.config.webpack&.dev_server&.enabled
       hosts << Gitlab.config.webpack.dev_server.host
     end
+
+    if ViteRuby.env['VITE_ENABLED'] == "true"
+      hosts << ViteRuby.instance.config.host
+    end
   end.compact.uniq
 end