From 2ead04ed249392d0dd2c111469a1a147ce176abe Mon Sep 17 00:00:00 2001
From: Alper Akgun <aakgun@gitlab.com>
Date: Tue, 17 Dec 2024 09:15:44 +0300
Subject: [PATCH 1/2] Model Experiments: Add menu item to MLflow usage example

Changelog: changed
---
 .../components/model_experiments_header.vue   | 74 +++++++++++++++----
 .../index/components/ml_experiments_index.vue | 10 +--
 .../experiments/show/ml_experiments_show.vue  | 10 +++
 .../projects/ml/experiments/show/index.js     | 11 ++-
 .../projects/ml/experiments/show.html.haml    |  1 +
 locale/gitlab.pot                             |  3 +
 .../model_experiments_header_spec.js          | 30 ++++++--
 .../components/ml_experiments_index_spec.js   |  5 +-
 .../show/ml_experiments_show_spec.js          | 11 ++-
 9 files changed, 126 insertions(+), 29 deletions(-)

diff --git a/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue b/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue
index 31e3fe2c40697a..0d681c38fddbac 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue
@@ -1,13 +1,28 @@
 <script>
-import { GlBadge } from '@gitlab/ui';
-import { __ } from '~/locale';
+import {
+  GlBadge,
+  GlDisclosureDropdown,
+  GlDisclosureDropdownGroup,
+  GlDisclosureDropdownItem,
+  GlModalDirective,
+} from '@gitlab/ui';
+import { __, s__ } from '~/locale';
 import { helpPagePath } from '~/helpers/help_page_helper';
-import PageHeading from '~/vue_shared/components/page_heading.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { MLFLOW_USAGE_MODAL_ID } from '../routes/experiments/index/constants';
+import MlflowModal from '../routes/experiments/index/components/mlflow_usage_modal.vue';
 
 export default {
   components: {
     GlBadge,
-    PageHeading,
+    GlDisclosureDropdown,
+    GlDisclosureDropdownGroup,
+    GlDisclosureDropdownItem,
+    MlflowModal,
+    TitleArea,
+  },
+  directives: {
+    GlModal: GlModalDirective,
   },
   props: {
     pageTitle: {
@@ -15,23 +30,54 @@ export default {
       required: true,
     },
   },
+  computed: {
+    mlflowUsageModalItem() {
+      return {
+        text: this.$options.i18n.importMlflow,
+      };
+    },
+  },
   i18n: {
     experimentBadgeLabel: __('Experiment'),
+    createTitle: s__('MlModelRegistry|Create'),
+    importMlflow: s__('MlModelRegistry|Create experiments using MLflow'),
   },
   experimentDocHref: helpPagePath('user/project/ml/experiment_tracking/index.md'),
+  mlflowModalId: MLFLOW_USAGE_MODAL_ID,
 };
 </script>
 
 <template>
-  <page-heading>
-    <template #heading>
-      <span class="gl-inline-flex gl-items-center gl-gap-3">
-        {{ pageTitle }}
-        <gl-badge variant="info" :href="$options.experimentDocHref">
-          {{ $options.i18n.experimentBadgeLabel }}
-        </gl-badge>
-        <slot></slot>
-      </span>
+  <title-area>
+    <template #title>
+      <div class="gl-flex gl-grow gl-items-center">
+        <span class="gl-inline-flex gl-items-center gl-gap-3" data-testid="page-heading">
+          {{ pageTitle }}
+          <gl-badge variant="info" :href="$options.experimentDocHref">
+            {{ $options.i18n.experimentBadgeLabel }}
+          </gl-badge>
+          <slot></slot>
+        </span>
+      </div>
+    </template>
+    <template #right-actions>
+      <gl-disclosure-dropdown
+        :toggle-text="$options.i18n.createTitle"
+        toggle-class="gl-w-full"
+        data-testid="create-dropdown"
+        variant="confirm"
+        category="primary"
+        placement="bottom-end"
+      >
+        <gl-disclosure-dropdown-group>
+          <gl-disclosure-dropdown-item
+            v-gl-modal="$options.mlflowModalId"
+            data-testid="create-menu-item"
+            :item="mlflowUsageModalItem"
+          />
+        </gl-disclosure-dropdown-group>
+        <mlflow-modal />
+      </gl-disclosure-dropdown>
     </template>
-  </page-heading>
+  </title-area>
 </template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
index 4336f47794da8e..8f230dd0b545ee 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
@@ -6,7 +6,6 @@ import * as translations from '~/ml/experiment_tracking/routes/experiments/index
 import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
 import Pagination from '~/ml/experiment_tracking/components/pagination.vue';
 import { MLFLOW_USAGE_MODAL_ID } from '../constants';
-import MlflowModal from './mlflow_usage_modal.vue';
 
 export default {
   name: 'MlExperimentsIndexApp',
@@ -17,7 +16,6 @@ export default {
     GlEmptyState,
     GlLink,
     GlButton,
-    MlflowModal,
   },
   directives: {
     GlModal: GlModalDirective,
@@ -93,12 +91,14 @@ export default {
       class="gl-py-8"
     >
       <template #actions>
-        <gl-button v-gl-modal="$options.mlflowModalId" class="gl-mx-2 gl-mb-3 gl-mr-3">
+        <gl-button
+          v-gl-modal="$options.mlflowModalId"
+          data-testid="empty-create-using-button"
+          class="gl-mx-2 gl-mb-3 gl-mr-3"
+        >
           {{ $options.i18n.CREATE_USING_MLFLOW_LABEL }}
         </gl-button>
       </template>
     </gl-empty-state>
-
-    <mlflow-modal />
   </div>
 </template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index 1727371b51990b..b938b45e1b89c6 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -31,6 +31,11 @@ export default {
     DeleteButton,
     PerformanceGraph,
   },
+  provide() {
+    return {
+      mlflowTrackingUrl: this.mlflowTrackingUrl,
+    };
+  },
   props: {
     experiment: {
       type: Object,
@@ -56,6 +61,11 @@ export default {
       type: String,
       required: true,
     },
+    mlflowTrackingUrl: {
+      type: String,
+      required: false,
+      default: '',
+    },
   },
   data() {
     const query = queryToObject(window.location.search);
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
index b3f15a9f65e975..41df99fcc3890f 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -8,7 +8,15 @@ const initShowExperiment = () => {
     return undefined;
   }
 
-  const { experiment, candidates, metrics, params, pageInfo, emptyStateSvgPath } = element.dataset;
+  const {
+    experiment,
+    candidates,
+    metrics,
+    params,
+    pageInfo,
+    emptyStateSvgPath,
+    mlflowTrackingUrl,
+  } = element.dataset;
 
   const props = {
     experiment: JSON.parse(experiment),
@@ -17,6 +25,7 @@ const initShowExperiment = () => {
     paramNames: JSON.parse(params),
     pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
     emptyStateSvgPath,
+    mlflowTrackingUrl,
   };
 
   return new Vue({
diff --git a/app/views/projects/ml/experiments/show.html.haml b/app/views/projects/ml/experiments/show.html.haml
index 6d9e8915520a3b..1f7d42f1bf12d1 100644
--- a/app/views/projects/ml/experiments/show.html.haml
+++ b/app/views/projects/ml/experiments/show.html.haml
@@ -16,4 +16,5 @@
   params: params,
   page_info: page_info,
   empty_state_svg_path: image_path('illustrations/status/status-new-md.svg'),
+  mlflow_tracking_url: mlflow_tracking_url(@project),
 } }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 23cacc068b1fc8..9cb1bbfad97f31 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -35256,6 +35256,9 @@ msgstr ""
 msgid "MlModelRegistry|Create & import"
 msgstr ""
 
+msgid "MlModelRegistry|Create experiments using MLflow"
+msgstr ""
+
 msgid "MlModelRegistry|Create model"
 msgstr ""
 
diff --git a/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js b/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
index 8bf95cfb26ed00..df6e742090c8c5 100644
--- a/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
@@ -1,7 +1,7 @@
 import { GlBadge } from '@gitlab/ui';
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
-import PageHeading from '~/vue_shared/components/page_heading.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
 
 describe('ml/experiment_tracking/components/model_experiments_header.vue', () => {
   let wrapper;
@@ -12,9 +12,6 @@ describe('ml/experiment_tracking/components/model_experiments_header.vue', () =>
       slots: {
         default: 'Slot content',
       },
-      stubs: {
-        PageHeading,
-      },
     });
   };
 
@@ -22,11 +19,34 @@ describe('ml/experiment_tracking/components/model_experiments_header.vue', () =>
 
   const findBadge = () => wrapper.findComponent(GlBadge);
   const findTitle = () => wrapper.findByTestId('page-heading');
+  const findTitleArea = () => wrapper.findComponent(TitleArea);
+  const findDropdown = () => wrapper.findByTestId('create-dropdown');
+  const findMenuItem = () => wrapper.findByTestId('create-menu-item');
+
+  it('title area exists', () => {
+    expect(findTitleArea().exists()).toBe(true);
+  });
 
-  it('renders title', () => {
+  it('title is set', () => {
     expect(findTitle().text()).toContain('Some Title');
   });
 
+  it('a dropdown exists', () => {
+    expect(findDropdown().props()).toMatchObject({
+      toggleText: 'Create',
+      variant: 'confirm',
+      category: 'primary',
+    });
+  });
+
+  it('a menu item for creating experiments exist', () => {
+    expect(findMenuItem().props()).toMatchObject({
+      item: {
+        text: 'Create experiments using MLflow',
+      },
+    });
+  });
+
   it('link points to documentation', () => {
     expect(findBadge().attributes().href).toBe(
       '/help/user/project/ml/experiment_tracking/index.md',
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
index 7302b6242ad4bb..fb5b9d1e813cec 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlLink, GlTableLite, GlButton } from '@gitlab/ui';
+import { GlEmptyState, GlLink, GlTableLite } from '@gitlab/ui';
 import MlExperimentsIndexApp from '~/ml/experiment_tracking/routes/experiments/index';
 import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
 import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -33,8 +33,7 @@ const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col)
 const hrefInRowAndColumn = (row, col) =>
   findColumnInRow(row, col).findComponent(GlLink).attributes().href;
 const findTitleHeader = () => wrapper.findComponent(ModelExperimentsHeader);
-
-const findDocsButton = () => wrapper.findAllComponents(GlButton).at(0);
+const findDocsButton = () => wrapper.findByTestId('empty-create-using-button');
 
 describe('MlExperimentsIndex', () => {
   describe('empty state', () => {
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
index f13051f1e01a9e..3e0129c063c2f8 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
@@ -26,10 +26,19 @@ describe('MlExperimentsShow', () => {
     pageInfo = MOCK_PAGE_INFO,
     experiment = MOCK_EXPERIMENT,
     emptyStateSvgPath = 'path',
+    mlflowTrackingUrl = 'mlflow/tracking/url',
     // eslint-disable-next-line max-params
   ) => {
     wrapper = mount(MlExperimentsShow, {
-      propsData: { experiment, candidates, metricNames, paramNames, pageInfo, emptyStateSvgPath },
+      propsData: {
+        experiment,
+        candidates,
+        metricNames,
+        paramNames,
+        pageInfo,
+        emptyStateSvgPath,
+        mlflowTrackingUrl,
+      },
     });
   };
 
-- 
GitLab


From 565ee02c946111286449f3bece2db476c00c7639 Mon Sep 17 00:00:00 2001
From: Alper Akgun <aakgun@gitlab.com>
Date: Tue, 17 Dec 2024 16:46:42 +0300
Subject: [PATCH 2/2] Hide the usage in candidate details screen

---
 .../components/model_experiments_header.vue           |  6 ++++++
 .../routes/candidates/show/ml_candidates_show.vue     |  2 +-
 .../components/model_experiments_header_spec.js       | 11 ++++++++---
 3 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue b/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue
index 0d681c38fddbac..2fffc1490d4e28 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue
@@ -29,6 +29,11 @@ export default {
       type: String,
       required: true,
     },
+    hideMlflowUsage: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   computed: {
     mlflowUsageModalItem() {
@@ -62,6 +67,7 @@ export default {
     </template>
     <template #right-actions>
       <gl-disclosure-dropdown
+        v-if="!hideMlflowUsage"
         :toggle-text="$options.i18n.createTitle"
         toggle-class="gl-w-full"
         data-testid="create-dropdown"
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
index ea942012af366c..fdd7cad6d65813 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
@@ -35,7 +35,7 @@ export default {
 
 <template>
   <div>
-    <model-experiments-header :page-title="$options.i18n.TITLE_LABEL">
+    <model-experiments-header :page-title="$options.i18n.TITLE_LABEL" hide-mlflow-usage>
       <delete-button
         :delete-path="info.path"
         :delete-confirmation-text="$options.i18n.DELETE_CANDIDATE_CONFIRMATION_MESSAGE"
diff --git a/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js b/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
index df6e742090c8c5..1b11d1c32de9de 100644
--- a/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
@@ -6,9 +6,9 @@ import TitleArea from '~/vue_shared/components/registry/title_area.vue';
 describe('ml/experiment_tracking/components/model_experiments_header.vue', () => {
   let wrapper;
 
-  const createWrapper = () => {
+  const createWrapper = ({ propsData = {} } = {}) => {
     wrapper = shallowMountExtended(ModelExperimentsHeader, {
-      propsData: { pageTitle: 'Some Title' },
+      propsData: { pageTitle: 'Some Title', ...propsData },
       slots: {
         default: 'Slot content',
       },
@@ -31,7 +31,7 @@ describe('ml/experiment_tracking/components/model_experiments_header.vue', () =>
     expect(findTitle().text()).toContain('Some Title');
   });
 
-  it('a dropdown exists', () => {
+  it('dropdown exists', () => {
     expect(findDropdown().props()).toMatchObject({
       toggleText: 'Create',
       variant: 'confirm',
@@ -39,6 +39,11 @@ describe('ml/experiment_tracking/components/model_experiments_header.vue', () =>
     });
   });
 
+  it('dropdown is hidden when hideMlflowUsage is true', () => {
+    createWrapper({ propsData: { hideMlflowUsage: true } });
+    expect(findDropdown().exists()).toBe(false);
+  });
+
   it('a menu item for creating experiments exist', () => {
     expect(findMenuItem().props()).toMatchObject({
       item: {
-- 
GitLab