From a9ab01ab6eac34e09b0d7cf01e1e352680045ec2 Mon Sep 17 00:00:00 2001 From: Eduardo Bonet <ebonet@gitlab.com> Date: Tue, 5 Dec 2023 14:22:49 +0000 Subject: [PATCH] Adds candidate info to model version view model Reuses CandidateDetailPresenter to include a model versions candidate info and display in the model page and model detail page. --- .../components/model_version_detail.vue | 21 +++++++- .../ml/model_registry/translations.js | 2 + .../projects/ml/show_ml_model_component.rb | 8 +-- .../ml/show_ml_model_version_component.rb | 12 +++-- .../find_or_create_model_version_service.rb | 10 ++-- .../projects/ml/model_versions/show.html.haml | 2 +- app/views/projects/ml/models/show.html.haml | 2 +- locale/gitlab.pot | 3 ++ .../ml/show_ml_model_component_spec.rb | 26 ++++++++-- .../show_ml_model_version_component_spec.rb | 31 +++++++++-- spec/factories/ml/model_versions.rb | 4 ++ .../components/model_version_detail_spec.js | 52 ++++++++++++------- spec/frontend/ml/model_registry/mock_data.js | 10 +++- spec/models/ml/candidate_spec.rb | 4 +- .../ml/create_candidate_service_spec.rb | 2 +- 15 files changed, 144 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue index 19d91df43b250..8d3e8cf2023cc 100644 --- a/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue +++ b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue @@ -1,12 +1,15 @@ <script> import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_PACKAGES_PACKAGE } from '~/graphql_shared/constants'; +import * as i18n from '../translations'; +import CandidateDetail from './candidate_detail.vue'; export default { name: 'ModelVersionDetail', components: { PackageFiles: () => import('~/packages_and_registries/package_registry/components/details/package_files.vue'), + CandidateDetail, }, props: { modelVersion: { @@ -25,14 +28,21 @@ export default { return 'ml_model'; }, }, + i18n, }; </script> <template> <div> - <p> + <h3 class="gl-font-lg gl-mt-5">{{ $options.i18n.DESCRIPTION_LABEL }}</h3> + + <div v-if="modelVersion.description"> {{ modelVersion.description }} - </p> + </div> + <div v-else class="gl-text-secondary"> + {{ $options.i18n.NO_DESCRIPTION_PROVIDED_LABEL }} + </div> + <template v-if="modelVersion.packageId"> <package-files :package-id="packageId" @@ -40,5 +50,12 @@ export default { :package-type="packageType" /> </template> + + <div class="gl-mt-5"> + <span class="gl-font-weight-bold">{{ $options.i18n.MLFLOW_ID_LABEL }}:</span> + {{ modelVersion.candidate.info.eid }} + </div> + + <candidate-detail :candidate="modelVersion.candidate" :show-info-section="false" /> </div> </template> diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js index bbafde0b94338..f4ff0f4aa973d 100644 --- a/app/assets/javascripts/ml/model_registry/translations.js +++ b/app/assets/javascripts/ml/model_registry/translations.js @@ -15,6 +15,8 @@ export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this export const modelsCountLabel = (modelCount) => n__('MlModelRegistry|%d model', 'MlModelRegistry|%d models', modelCount); +export const DESCRIPTION_LABEL = __('Description'); +export const NO_DESCRIPTION_PROVIDED_LABEL = s__('MlModelRegistry|No description provided'); export const INFO_LABEL = s__('MlModelRegistry|Info'); export const ID_LABEL = s__('MlModelRegistry|ID'); export const MLFLOW_ID_LABEL = s__('MlModelRegistry|MLflow run ID'); diff --git a/app/components/projects/ml/show_ml_model_component.rb b/app/components/projects/ml/show_ml_model_component.rb index 38a81a5837d02..03300f01f6445 100644 --- a/app/components/projects/ml/show_ml_model_component.rb +++ b/app/components/projects/ml/show_ml_model_component.rb @@ -3,10 +3,11 @@ module Projects module Ml class ShowMlModelComponent < ViewComponent::Base - attr_reader :model + attr_reader :model, :current_user - def initialize(model:) + def initialize(model:, current_user:) @model = model.present + @current_user = current_user end private @@ -35,7 +36,8 @@ def latest_version_view_model version: model_version.version, description: model_version.description, project_path: project_path(model_version.project), - package_id: model_version.package_id + package_id: model_version.package_id, + **::Ml::CandidateDetailsPresenter.new(model_version.candidate, current_user).present } end end diff --git a/app/components/projects/ml/show_ml_model_version_component.rb b/app/components/projects/ml/show_ml_model_version_component.rb index 0e39a1cbcc62a..a4c641f6d66a6 100644 --- a/app/components/projects/ml/show_ml_model_version_component.rb +++ b/app/components/projects/ml/show_ml_model_version_component.rb @@ -3,11 +3,12 @@ module Projects module Ml class ShowMlModelVersionComponent < ViewComponent::Base - attr_reader :model_version, :model + attr_reader :model_version, :model, :current_user - def initialize(model_version:) + def initialize(model_version:, current_user:) @model_version = model_version.present @model = model_version.model.present + @current_user = current_user end private @@ -24,12 +25,17 @@ def view_model model: { name: model.name, path: model.path - } + }, + **candidate_data } } Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) }) end + + def candidate_data + ::Ml::CandidateDetailsPresenter.new(model_version.candidate, current_user).present + end end end end diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb index a5e9bf997cc9c..1c6f5bb96dd60 100644 --- a/app/services/ml/find_or_create_model_version_service.rb +++ b/app/services/ml/find_or_create_model_version_service.rb @@ -15,10 +15,12 @@ def execute model_version = Ml::ModelVersion.find_or_create!(model, @version, @package, @description) - model_version.candidate = ::Ml::CreateCandidateService.new( - model.default_experiment, - { model_version: model_version } - ).execute + unless model_version.candidate + model_version.candidate = ::Ml::CreateCandidateService.new( + model.default_experiment, + { model_version: model_version } + ).execute + end model_version end diff --git a/app/views/projects/ml/model_versions/show.html.haml b/app/views/projects/ml/model_versions/show.html.haml index 0b3d5462a8968..1b4bdd29842d8 100644 --- a/app/views/projects/ml/model_versions/show.html.haml +++ b/app/views/projects/ml/model_versions/show.html.haml @@ -3,4 +3,4 @@ - breadcrumb_title @model_version.version - page_title "#{@model_version.name} / #{@model_version.version}" -= render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version)) += render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version, current_user: current_user)) diff --git a/app/views/projects/ml/models/show.html.haml b/app/views/projects/ml/models/show.html.haml index be611e55304dc..e0067143450c1 100644 --- a/app/views/projects/ml/models/show.html.haml +++ b/app/views/projects/ml/models/show.html.haml @@ -2,4 +2,4 @@ - breadcrumb_title @model.name - page_title @model.name -= render(Projects::Ml::ShowMlModelComponent.new(model: @model)) += render(Projects::Ml::ShowMlModelComponent.new(model: @model, current_user: current_user)) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 29ad8b192e2f1..2a5a78b5990e7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -30847,6 +30847,9 @@ msgstr "" msgid "MlModelRegistry|Model registry" msgstr "" +msgid "MlModelRegistry|No description provided" +msgstr "" + msgid "MlModelRegistry|No logged metadata" msgstr "" diff --git a/spec/components/projects/ml/show_ml_model_component_spec.rb b/spec/components/projects/ml/show_ml_model_component_spec.rb index 02fad55e0be4f..ed9894216794c 100644 --- a/spec/components/projects/ml/show_ml_model_component_spec.rb +++ b/spec/components/projects/ml/show_ml_model_component_spec.rb @@ -3,11 +3,14 @@ require "spec_helper" RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_category: :mlops do - let_it_be(:project) { build_stubbed(:project) } - let_it_be(:model1) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) } + let_it_be(:project) { create(:project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + let_it_be(:model1) { create(:ml_models, :with_latest_version_and_package, project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + + let_it_be(:experiment) { model1.default_experiment } + let_it_be(:candidate) { model1.latest_version.candidate } subject(:component) do - described_class.new(model: model1) + described_class.new(model: model1, current_user: model1.user) end describe 'rendered' do @@ -28,7 +31,22 @@ 'version' => model1.latest_version.version, 'description' => model1.latest_version.description, 'projectPath' => "/#{project.full_path}", - 'packageId' => model1.latest_version.package_id + 'packageId' => model1.latest_version.package_id, + 'candidate' => { + 'info' => { + 'iid' => candidate.iid, + 'eid' => candidate.eid, + 'pathToArtifact' => nil, + 'experimentName' => candidate.experiment.name, + 'pathToExperiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", + 'status' => 'running', + 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}", + 'ciJob' => nil + }, + 'metrics' => [], + 'params' => [], + 'metadata' => [] + } }, 'versionCount' => 1 } diff --git a/spec/components/projects/ml/show_ml_model_version_component_spec.rb b/spec/components/projects/ml/show_ml_model_version_component_spec.rb index a7dad5e4b2baf..89f0c8633c27b 100644 --- a/spec/components/projects/ml/show_ml_model_version_component_spec.rb +++ b/spec/components/projects/ml/show_ml_model_version_component_spec.rb @@ -3,12 +3,20 @@ require "spec_helper" RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feature_category: :mlops do - let_it_be(:project) { build_stubbed(:project) } - let_it_be(:model) { build_stubbed(:ml_models, project: project) } - let_it_be(:version) { build_stubbed(:ml_model_versions, :with_package, model: model, description: 'abc') } + let_it_be(:project) { create(:project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + let_it_be(:user) { project.owner } + let_it_be(:model) { create(:ml_models, project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + let_it_be(:experiment) { model.default_experiment } + let_it_be(:candidate) do + create(:ml_candidates, :with_artifact, experiment: experiment, user: user, project: project) # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + end + + let_it_be(:version) do + build_stubbed(:ml_model_versions, :with_package, model: model, candidate: candidate, description: 'abc') + end subject(:component) do - described_class.new(model_version: version) + described_class.new(model_version: version, current_user: user) end describe 'rendered' do @@ -30,6 +38,21 @@ 'model' => { 'name' => model.name, 'path' => "/#{project.full_path}/-/ml/models/#{model.id}" + }, + 'candidate' => { + 'info' => { + 'iid' => candidate.iid, + 'eid' => candidate.eid, + 'pathToArtifact' => "/#{project.full_path}/-/packages/#{candidate.artifact.id}", + 'experimentName' => candidate.experiment.name, + 'pathToExperiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", + 'status' => 'running', + 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}", + 'ciJob' => nil + }, + 'metrics' => [], + 'params' => [], + 'metadata' => [] } } }) diff --git a/spec/factories/ml/model_versions.rb b/spec/factories/ml/model_versions.rb index a097640b1345a..fd7ed857ee272 100644 --- a/spec/factories/ml/model_versions.rb +++ b/spec/factories/ml/model_versions.rb @@ -8,6 +8,10 @@ project { model.project } description { 'Some description' } + candidate do + association :ml_candidates, experiment: model.default_experiment, project: project, model_version: instance + end + trait :with_package do package do association :ml_model_package, name: model.name, version: version, project: project diff --git a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js index aeb9d13ad9783..d1874346ad740 100644 --- a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js +++ b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; +import CandidateDetail from '~/ml/model_registry/components/candidate_detail.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { makeModelVersion, MODEL_VERSION } from '../mock_data'; @@ -15,36 +16,51 @@ const createWrapper = (modelVersion = MODEL_VERSION) => { }; const findPackageFiles = () => wrapper.findComponent(PackageFiles); +const findCandidateDetail = () => wrapper.findComponent(CandidateDetail); describe('ml/model_registry/components/model_version_detail.vue', () => { - describe('description', () => { + describe('base behaviour', () => { beforeEach(() => createWrapper()); it('shows the description', () => { expect(wrapper.text()).toContain(MODEL_VERSION.description); }); - }); - describe('package files', () => { - describe('if package exists', () => { - beforeEach(() => createWrapper()); - - it('renders files', () => { - expect(findPackageFiles().props()).toEqual({ - packageId: 'gid://gitlab/Packages::Package/12', - projectPath: MODEL_VERSION.projectPath, - packageType: 'ml_model', - canDelete: false, - }); - }); + it('shows the candidate', () => { + expect(findCandidateDetail().props('candidate')).toBe(MODEL_VERSION.candidate); }); - describe('if package does not exist', () => { - beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 }))); + it('shows the mlflow label string', () => { + expect(wrapper.text()).toContain('MLflow run ID'); + }); - it('does not render files', () => { - expect(findPackageFiles().exists()).toBe(false); + it('shows the mlflow id', () => { + expect(wrapper.text()).toContain(MODEL_VERSION.candidate.info.eid); + }); + + it('renders files', () => { + expect(findPackageFiles().props()).toEqual({ + packageId: 'gid://gitlab/Packages::Package/12', + projectPath: MODEL_VERSION.projectPath, + packageType: 'ml_model', + canDelete: false, }); }); }); + + describe('if package does not exist', () => { + beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 }))); + + it('does not render files', () => { + expect(findPackageFiles().exists()).toBe(false); + }); + }); + + describe('if model version does not have description', () => { + beforeEach(() => createWrapper(makeModelVersion({ description: null }))); + + it('renders no description provided label', () => { + expect(wrapper.text()).toContain('No description provided'); + }); + }); }); diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js index 07cf59388ea59..78e22eda7b9b0 100644 --- a/spec/frontend/ml/model_registry/mock_data.js +++ b/spec/frontend/ml/model_registry/mock_data.js @@ -56,12 +56,18 @@ export const makeModel = ({ latestVersion } = { latestVersion: LATEST_VERSION }) export const MODEL = makeModel(); -export const makeModelVersion = ({ version = '1.2.3', model = MODEL, packageId = 12 } = {}) => ({ +export const makeModelVersion = ({ + version = '1.2.3', + model = MODEL, + packageId = 12, + description = 'Model version description', +} = {}) => ({ version, model, packageId, - description: 'Model version description', + description, projectPath: 'path/to/project', + candidate: newCandidate(), }); export const MODEL_VERSION = makeModelVersion(); diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb index 503d3514a72e4..678224a3c8eff 100644 --- a/spec/models/ml/candidate_spec.rb +++ b/spec/models/ml/candidate_spec.rb @@ -38,8 +38,8 @@ describe 'validation' do let_it_be(:model) { create(:ml_models, project: candidate.project) } - let_it_be(:model_version1) { create(:ml_model_versions, model: model) } - let_it_be(:model_version2) { create(:ml_model_versions, model: model) } + let_it_be(:model_version1) { create(:ml_model_versions, model: model, candidate: nil) } + let_it_be(:model_version2) { create(:ml_model_versions, model: model, candidate: nil) } let_it_be(:validation_candidate) do create(:ml_candidates, model_version: model_version1, project: candidate.project) end diff --git a/spec/services/ml/create_candidate_service_spec.rb b/spec/services/ml/create_candidate_service_spec.rb index fb3456b0bccc4..b1a053711d72d 100644 --- a/spec/services/ml/create_candidate_service_spec.rb +++ b/spec/services/ml/create_candidate_service_spec.rb @@ -4,7 +4,7 @@ RSpec.describe ::Ml::CreateCandidateService, feature_category: :mlops do describe '#execute' do - let_it_be(:model_version) { create(:ml_model_versions) } + let_it_be(:model_version) { create(:ml_model_versions, candidate: nil) } let_it_be(:experiment) { create(:ml_experiments, project: model_version.project) } let(:params) { {} } -- GitLab