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 31e3fe2c40697a79e1ade696b7c398d9af0856b1..2fffc1490d4e284f5b1b864441926c39ea37b57b 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,37 +1,89 @@ <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: { type: String, required: true, }, + hideMlflowUsage: { + type: Boolean, + required: false, + default: false, + }, + }, + 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 + v-if="!hideMlflowUsage" + :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/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue index ea942012af366cec23c0843d8e3e32ecb0d85214..fdd7cad6d6581315608487eec0dcf18960c5e11c 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/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 4336f47794da8ebb5622ac1618f2643ceadd11d5..8f230dd0b545ee120f0645839693156c558eec37 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 1727371b51990bb26f8fa93c86a55eb40a64fbff..b938b45e1b89c6911ccf3c6defe265732a3ea74f 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 b3f15a9f65e9758f4a6f34392e6fd0ecfb812828..41df99fcc3890f4e377c19ae458f6650f6bbcbd8 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 6d9e8915520a3be366e8881e19449d0ca1dfae3c..1f7d42f1bf12d1b21d23b68331fdfbbdb8dee9a5 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 59cbb12b855c13e3e0a3cdda8b3000fed42f9622..71f864d9ba27324d0a0958e4a5309ea53f7897ad 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -35352,6 +35352,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 8bf95cfb26ed00cd589daa47edff129c227bf656..1b11d1c32de9de9aa9c665a78ef1ea42e062acd5 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,20 +1,17 @@ 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; - const createWrapper = () => { + const createWrapper = ({ propsData = {} } = {}) => { wrapper = shallowMountExtended(ModelExperimentsHeader, { - propsData: { pageTitle: 'Some Title' }, + propsData: { pageTitle: 'Some Title', ...propsData }, slots: { default: 'Slot content', }, - stubs: { - PageHeading, - }, }); }; @@ -22,11 +19,39 @@ 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('dropdown exists', () => { + expect(findDropdown().props()).toMatchObject({ + toggleText: 'Create', + variant: 'confirm', + category: 'primary', + }); + }); + + 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: { + 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 7302b6242ad4bb9b0edca58bf363f9cde6676b2a..fb5b9d1e813cec44f1f80fca85af5587a238e0b1 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 f13051f1e01a9ea78bd823bc4ac3a75bc967a7a1..3e0129c063c2f8a71dfd7b48978d2b92affa21e9 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, + }, }); };