Skip to content
Snippets Groups Projects
Commit cb2d0def authored by Fred de Gier's avatar Fred de Gier
Browse files

Model registry: Enhance model create flow

Changelog: changed

# Conflicts:
#	app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
#	locale/gitlab.pot
#	spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
parent e9934dcb
No related branches found
No related tags found
1 merge request!171370Model registry: Improve Model creation flow
......@@ -203,7 +203,7 @@ export default {
},
i18n: {
createModelVersionLinkTitle: s__('MlModelRegistry|Create new version'),
editModelButtonLabel: s__('MlModelRegistry|Edit model'),
editModelButtonLabel: s__('MlModelRegistry|Edit'),
tabModelCardTitle: s__('MlModelRegistry|Model card'),
tabVersionsTitle: s__('MlModelRegistry|Versions'),
versionCountTitle: s__('MlModelRegistry|Total versions'),
......@@ -249,7 +249,6 @@ export default {
<gl-button
v-if="canWriteModelRegistry"
data-testid="edit-model-button"
variant="confirm"
:href="editModelPath"
>{{ $options.i18n.editModelButtonLabel }}</gl-button
>
......@@ -305,9 +304,9 @@ export default {
</gl-link>
</div>
</div>
<div class="gl-mt-5">
<div v-if="showModelLatestVersion" class="gl-mt-5" data-testid="latest-version-label">
<div class="gl-text-lg gl-font-bold">{{ $options.i18n.latestVersionTitle }}</div>
<div v-if="showModelLatestVersion" class="gl-pt-2 gl-text-gray-500">
<div class="gl-pt-2 gl-text-gray-500">
<gl-link
data-testid="sidebar-latest-version-link"
:href="model.latestVersion._links.showPath"
......
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { semverRegex, noSpacesRegex } from '~/lib/utils/regexp';
import createModelVersionMutation from '../graphql/mutations/create_model_version.mutation.graphql';
import { noSpacesRegex } from '~/lib/utils/regexp';
import createModelMutation from '../graphql/mutations/create_model.mutation.graphql';
export default {
......@@ -18,7 +17,6 @@ export default {
GlForm,
GlFormGroup,
GlFormInput,
ImportArtifactZone: () => import('./import_artifact_zone.vue'),
},
inject: ['projectPath', 'maxAllowedFileSize', 'markdownPreviewPath'],
props: {
......@@ -31,57 +29,26 @@ export default {
data() {
return {
name: null,
version: null,
description: '',
versionDescription: '',
errorMessage: null,
modelData: null,
versionData: null,
markdownDocPath: helpPagePath('user/markdown'),
markdownEditorRestrictedToolBarItems: ['full-screen'],
importErrorsText: null,
};
},
computed: {
showImportArtifactZone() {
return this.version && this.name;
},
autocompleteDataSources() {
return gl.GfmAutoComplete?.dataSources;
},
modelNameIsValid() {
return this.name && noSpacesRegex.test(this.name);
},
isSemver() {
return semverRegex.test(this.version);
},
isVersionValid() {
return !this.version || this.isSemver;
},
submitButtonDisabled() {
return !this.isVersionValid || !this.modelNameIsValid;
},
validVersionFeedback() {
if (this.isSemver) {
return this.$options.i18n.versionValid;
}
return null;
return !this.modelNameIsValid;
},
modelNameDescription() {
return !this.name || this.modelNameIsValid ? this.$options.i18n.nameDescription : '';
},
versionDescriptionText() {
return !this.version ? this.$options.i18n.versionDescription : '';
},
importErrorsAlert() {
return {
id: 'import-artifact-alert',
variant: this.importErrorsText ? 'danger' : 'info',
message: this.importErrorsText
? `${this.$options.i18n.someFailed} ${this.importErrorsText}`
: this.$options.i18n.allSucceeded,
};
},
},
methods: {
async createModel() {
......@@ -95,18 +62,6 @@ export default {
});
return data;
},
async createModelVersion(modelGid) {
const { data } = await this.$apollo.mutate({
mutation: createModelVersionMutation,
variables: {
projectPath: this.projectPath,
modelId: modelGid,
version: this.version,
description: this.versionDescription,
},
});
return data;
},
async create() {
this.errorMessage = '';
try {
......@@ -118,25 +73,9 @@ export default {
if (modelErrors.length) {
this.errorMessage = modelErrors.join(', ');
this.modelData = null;
} else if (this.version) {
// Attempt creating a version if needed
if (!this.versionData) {
this.versionData = await this.createModelVersion(this.modelData.mlModelCreate.model.id);
}
const versionErrors = this.versionData?.mlModelVersionCreate?.errors || [];
if (versionErrors.length) {
this.errorMessage = versionErrors.join(', ');
this.versionData = null;
} else {
// Attempt importing model artifacts
const { showPath, importPath } =
this.versionData.mlModelVersionCreate.modelVersion._links;
await this.$refs.importArtifactZoneRef.uploadArtifact(importPath);
visitUrlWithAlerts(showPath, [this.importErrorsAlert]);
}
} else {
const { showPath } = this.modelData.mlModelCreate.model._links;
visitUrlWithAlerts(showPath, [this.importErrorsAlert]);
visitUrl(showPath);
}
} catch (error) {
Sentry.captureException(error);
......@@ -145,13 +84,9 @@ export default {
},
resetForm() {
this.name = null;
this.version = null;
this.description = '';
this.versionDescription = '';
this.errorMessage = null;
this.modelData = null;
this.versionData = null;
this.importErrorsText = null;
},
hideAlert() {
this.errorMessage = null;
......@@ -161,14 +96,6 @@ export default {
this.description = newText;
}
},
setVersionDescription(newVersionText) {
if (!this.isSubmitting) {
this.versionDescription = newVersionText;
}
},
onImportError(error) {
this.importErrorsText = error;
},
},
descriptionFormFieldProps: {
placeholder: s__('MlModelRegistry|Enter a model description'),
......@@ -176,36 +103,16 @@ export default {
name: 'model-description',
},
i18n: {
allSucceeded: s__('MlModelRegistry|Artifacts uploaded successfully.'),
someFailed: s__('MlModelRegistry|Artifact uploads completed with errors.'),
actionPrimaryText: s__('MlModelRegistry|Create'),
actionSecondaryText: __('Cancel'),
nameDescriptionLabel: s__('MlModelRegistry|Must be unique. May not contain spaces.'),
nameDescription: s__('MlModelRegistry|Example: my-model'),
nameInvalid: s__('MlModelRegistry|May not contain spaces.'),
namePlaceholder: s__('MlModelRegistry|Enter a model name'),
versionDescription: s__('MlModelRegistry|Example: 1.0.0'),
versionPlaceholder: s__('MlModelRegistry|Enter a semantic version'),
nameDescriptionPlaceholder: s__('MlModelRegistry|Enter a model description'),
versionDescriptionTitle: s__('MlModelRegistry|Version description'),
versionDescriptionLabel: s__(
'MlModelRegistry|Must be a semantic version. Leave blank to skip version creation.',
),
versionValid: s__('MlModelRegistry|Version is a valid semantic version.'),
versionInvalid: s__('MlModelRegistry|Must be a semantic version. Example: 1.0.0'),
versionDescriptionPlaceholder: s__('MlModelRegistry|Enter a version description'),
title: s__('MlModelRegistry|Create model, version & import artifacts'),
title: s__('MlModelRegistry|Create model'),
modelName: s__('MlModelRegistry|Model name'),
modelDescription: __('Model description'),
version: __('Version'),
uploadLabel: __('Upload artifacts'),
modelSuccessButVersionArtifactFailAlert: {
id: 'ml-model-success-version-artifact-failed',
message: s__(
'MlModelRegistry|Model has been created but version or artifacts could not be uploaded. Try creating model version.',
),
variant: 'warning',
},
optionalText: s__('MlModelRegistry|(Optional)'),
},
};
......@@ -256,65 +163,6 @@ export default {
@input="setDescription"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.version"
:label-description="$options.i18n.versionDescriptionLabel"
data-testid="versionGroupId"
label-for="versionId"
:state="isVersionValid"
:invalid-feedback="$options.i18n.versionInvalid"
:valid-feedback="validVersionFeedback"
:description="versionDescriptionText"
optional
:optional-text="$options.i18n.optionalText"
>
<gl-form-input
id="versionId"
v-model="version"
data-testid="versionId"
type="text"
:placeholder="$options.i18n.versionPlaceholder"
autocomplete="off"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.versionDescriptionTitle"
data-testid="versionDescriptionGroupId"
label-for="versionDescriptionId"
optional
:optional-text="$options.i18n.optionalText"
class="common-note-form gfm-form js-main-target-form new-note gl-grow"
>
<markdown-editor
ref="markdownEditor"
data-testid="versionDescriptionId"
:value="versionDescription"
enable-autocomplete
:autocomplete-data-sources="autocompleteDataSources"
:enable-content-editor="true"
:form-field-props="$options.descriptionFormFieldProps"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocPath"
:disable-attachments="disableAttachments"
:placeholder="$options.i18n.versionDescriptionPlaceholder"
:restricted-tool-bar-items="markdownEditorRestrictedToolBarItems"
@input="setVersionDescription"
/>
</gl-form-group>
<gl-form-group
v-if="showImportArtifactZone"
data-testid="importArtifactZoneLabel"
:label="$options.i18n.uploadLabel"
label-for="versionImportArtifactZone"
>
<import-artifact-zone
id="versionImportArtifactZone"
ref="importArtifactZoneRef"
class="gl-px-3 gl-py-0"
:submit-on-select="false"
@error="onImportError"
/>
</gl-form-group>
</gl-form>
<gl-alert v-if="errorMessage" data-testid="create-alert" variant="danger" @dismiss="hideAlert"
......
......@@ -60,7 +60,7 @@ export default {
'MlModelRegistry|No description available. To add a description, click "Edit model" above.',
),
description: s__('MlModelRegistry|Use versions to track performance, parameters, and metadata'),
primaryText: s__('MlModelRegistry|Create model version'),
primaryText: s__('MlModelRegistry|Create new version'),
},
};
</script>
......
......@@ -34889,9 +34889,6 @@ msgstr ""
msgid "MlModelRegistry|Create model version"
msgstr ""
 
msgid "MlModelRegistry|Create model, version & import artifacts"
msgstr ""
msgid "MlModelRegistry|Create new version"
msgstr ""
 
......@@ -34934,6 +34931,9 @@ msgstr ""
msgid "MlModelRegistry|Drop to start upload"
msgstr ""
 
msgid "MlModelRegistry|Edit"
msgstr ""
msgid "MlModelRegistry|Edit model"
msgstr ""
 
......@@ -34949,24 +34949,15 @@ msgstr ""
msgid "MlModelRegistry|Enter a model version description"
msgstr ""
 
msgid "MlModelRegistry|Enter a semantic version"
msgstr ""
msgid "MlModelRegistry|Enter a semantic version."
msgstr ""
 
msgid "MlModelRegistry|Enter a subfolder name to organize your artifacts."
msgstr ""
 
msgid "MlModelRegistry|Enter a version description"
msgstr ""
msgid "MlModelRegistry|Enter some description"
msgstr ""
 
msgid "MlModelRegistry|Example: 1.0.0"
msgstr ""
msgid "MlModelRegistry|Example: my-model"
msgstr ""
 
......@@ -35030,9 +35021,6 @@ msgstr ""
msgid "MlModelRegistry|Model deleted successfully"
msgstr ""
 
msgid "MlModelRegistry|Model has been created but version or artifacts could not be uploaded. Try creating model version."
msgstr ""
msgid "MlModelRegistry|Model name"
msgstr ""
 
......@@ -35051,15 +35039,9 @@ msgstr ""
msgid "MlModelRegistry|Must be a semantic version."
msgstr ""
 
msgid "MlModelRegistry|Must be a semantic version. Example: 1.0.0"
msgstr ""
msgid "MlModelRegistry|Must be a semantic version. Latest version is %{latestVersion}"
msgstr ""
 
msgid "MlModelRegistry|Must be a semantic version. Leave blank to skip version creation."
msgstr ""
msgid "MlModelRegistry|Must be unique. May not contain spaces."
msgstr ""
 
......@@ -35150,12 +35132,6 @@ msgstr ""
msgid "MlModelRegistry|Version created %{timeAgo} by %{author}"
msgstr ""
 
msgid "MlModelRegistry|Version description"
msgstr ""
msgid "MlModelRegistry|Version is a valid semantic version."
msgstr ""
msgid "MlModelRegistry|Version is not a valid semantic version."
msgstr ""
 
......@@ -72,6 +72,7 @@ describe('ml/model_registry/apps/show_ml_model', () => {
modelDetailsResolver = jest.fn().mockResolvedValue(modelDetailQuery),
destroyMutationResolver = jest.fn().mockResolvedValue(destroyModelResponses.success),
canWriteModelRegistry = true,
latestVersion = '1.0.0',
} = {}) => {
const requestHandlers = [
[getModelQuery, modelDetailsResolver],
......@@ -90,7 +91,7 @@ describe('ml/model_registry/apps/show_ml_model', () => {
mlflowTrackingUrl: 'path/to/tracking',
canWriteModelRegistry,
maxAllowedFileSize: 99999,
latestVersion: '1.0.0',
latestVersion,
markdownPreviewPath: '/markdown-preview',
createModelVersionPath: 'project/path/create/model/version',
},
......@@ -163,6 +164,7 @@ describe('ml/model_registry/apps/show_ml_model', () => {
beforeEach(() => createWrapper());
it('displays version creation button', () => {
expect(findModelVersionCreateButton().exists()).toBe(true);
expect(findModelVersionCreateButton().text()).toBe('Create new version');
});
......@@ -180,9 +182,9 @@ describe('ml/model_registry/apps/show_ml_model', () => {
it('displays model edit button', () => {
expect(findModelEditButton().props()).toMatchObject({
variant: 'confirm',
category: 'primary',
});
expect(findModelEditButton().text()).toBe('Edit');
});
describe('when user has no permission to write model registry', () => {
......@@ -323,11 +325,19 @@ describe('ml/model_registry/apps/show_ml_model', () => {
expect(findAvatar().props('src')).toBe('path/to/avatar');
});
it('displays sidebar latest version link', () => {
expect(findLatestVersionLink().attributes('href')).toBe(
'/root/test-project/-/ml/models/1/versions/5000',
);
expect(findLatestVersionLink().text()).toBe('1.0.4999');
describe('latest version', () => {
it('displays sidebar latest version link', () => {
expect(findLatestVersionLink().attributes('href')).toBe(
'/root/test-project/-/ml/models/1/versions/5000',
);
expect(findLatestVersionLink().text()).toBe('1.0.4999');
});
it('does not display sidebar latest version link when model does not have a latest version', () => {
createWrapper({ latestVersion: null });
expect(findLatestVersionLink().exists()).toBe(false);
expect(wrapper.findByTestId('latest-version-label').exists()).toBe(false);
});
});
it('displays sidebar version count', () => {
......
......@@ -2,38 +2,26 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import ModelCreate from '~/ml/model_registry/components/model_create.vue';
import ImportArtifactZone from '~/ml/model_registry/components/import_artifact_zone.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import { uploadModel } from '~/ml/model_registry/services/upload_model';
import createModelMutation from '~/ml/model_registry/graphql/mutations/create_model.mutation.graphql';
import createModelVersionMutation from '~/ml/model_registry/graphql/mutations/create_model_version.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { createModelResponses, createModelVersionResponses } from '../graphql_mock_data';
import { createModelResponses } from '../graphql_mock_data';
Vue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrlWithAlerts: jest.fn(),
}));
jest.mock('~/ml/model_registry/services/upload_model', () => ({
uploadModel: jest.fn(() => Promise.resolve()),
visitUrl: jest.fn(),
}));
describe('ModelCreate', () => {
let wrapper;
let apolloProvider;
const file = { name: 'file.txt', size: 1024 };
const anotherFile = { name: 'another file.txt', size: 10 };
const files = [file, anotherFile];
beforeEach(() => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
......@@ -44,13 +32,9 @@ describe('ModelCreate', () => {
const createWrapper = (
createModelResolver = jest.fn().mockResolvedValue(createModelResponses.success),
createModelVersionResolver = jest.fn().mockResolvedValue(createModelVersionResponses.success),
createModelVisible = false,
) => {
const requestHandlers = [
[createModelMutation, createModelResolver],
[createModelVersionMutation, createModelVersionResolver],
];
const requestHandlers = [[createModelMutation, createModelResolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(ModelCreate, {
......@@ -63,29 +47,19 @@ describe('ModelCreate', () => {
markdownPreviewPath: '/markdown-preview',
},
apolloProvider,
stubs: {
ImportArtifactZone,
},
});
};
const findPrimaryButton = () => wrapper.findByTestId('primary-button');
const findSecondaryButton = () => wrapper.findByTestId('secondary-button');
const findNameInput = () => wrapper.findByTestId('nameId');
const findVersionInput = () => wrapper.findByTestId('versionId');
const findVersionGroup = () => wrapper.findByTestId('versionGroupId');
const findVersionDescriptionGroup = () => wrapper.findByTestId('versionDescriptionGroupId');
const findDescriptionGroup = () => wrapper.findByTestId('descriptionGroupId');
const findDescriptionInput = () => wrapper.findByTestId('descriptionId');
const findVersionDescriptionInput = () => wrapper.findByTestId('versionDescriptionId');
const findImportArtifactZone = () => wrapper.findComponent(ImportArtifactZone);
const zone = () => wrapper.findComponent(UploadDropzone);
const findGlAlert = () => wrapper.findByTestId('create-alert');
const submitForm = async () => {
findPrimaryButton().vm.$emit('click');
await waitForPromises();
};
const findArtifactZoneLabel = () => wrapper.findByTestId('importArtifactZoneLabel');
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
const findModelNameGroup = () => wrapper.findByTestId('nameGroupId');
......@@ -111,11 +85,7 @@ describe('ModelCreate', () => {
describe('Form', () => {
beforeEach(() => {
createWrapper(
jest.fn().mockResolvedValue(createModelResponses.success),
jest.fn().mockResolvedValue(createModelVersionResponses.success),
true,
);
createWrapper(jest.fn().mockResolvedValue(createModelResponses.success), true);
});
it('renders the name input', () => {
......@@ -132,37 +102,6 @@ describe('ModelCreate', () => {
expect(findModelNameGroup().attributes('label')).toBe(ModelCreate.i18n.modelName);
});
it('renders the version input', () => {
expect(findVersionInput().exists()).toBe(true);
});
it('renders the version label', () => {
expect(findVersionGroup().attributes('label')).toBe('Version');
});
it('renders the version placeholder', () => {
expect(findVersionInput().attributes('placeholder')).toBe(
ModelCreate.i18n.versionPlaceholder,
);
});
it('renders the version group', () => {
expect(findVersionGroup().attributes()).toMatchObject({
description: 'Example: 1.0.0',
optional: 'true',
optionaltext: '(Optional)',
label: 'Version',
});
});
it('renders the version description group', () => {
expect(findVersionDescriptionGroup().attributes()).toMatchObject({
optional: 'true',
optionaltext: '(Optional)',
label: 'Version description',
});
});
it('renders the description group', () => {
expect(findDescriptionGroup().attributes()).toMatchObject({
optionaltext: '(Optional)',
......@@ -175,40 +114,6 @@ describe('ModelCreate', () => {
expect(findDescriptionInput().exists()).toBe(true);
});
it('renders the description input text', () => {
expect(findVersionGroup().attributes('valid-feedback')).toBe(ModelCreate.i18n.validVersion);
});
it('renders the version description input', () => {
expect(findVersionDescriptionInput().exists()).toBe(true);
});
it('renders the import artifact zone input', () => {
expect(findImportArtifactZone().exists()).toBe(false);
});
it('does not displays the title of the artifacts uploader', () => {
expect(findArtifactZoneLabel().exists()).toBe(false);
});
it('displays the title of the artifacts uploader when a version is entered', async () => {
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findVersionDescriptionInput().vm.$emit('input', 'My version description');
await Vue.nextTick();
expect(findArtifactZoneLabel().attributes('label')).toBe('Upload artifacts');
});
it('renders the import artifact zone input with version entered', async () => {
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
await waitForPromises();
expect(findImportArtifactZone().props()).toEqual({
path: null,
submitOnSelect: false,
});
});
it('renders the create button', () => {
expect(findPrimaryButton().props()).toMatchObject({
variant: 'confirm',
......@@ -232,58 +137,6 @@ describe('ModelCreate', () => {
beforeEach(() => {
createWrapper();
});
it('renders the version input label for initial state', () => {
expect(findVersionGroup().attributes('state')).toBe('true');
expect(findPrimaryButton().props()).toMatchObject({
variant: 'confirm',
disabled: true,
});
});
it.each(['1.0', '1', 'abc', '1.abc', '1.0.0.0'])(
'renders the version input label for invalid state',
async (version) => {
findVersionInput().vm.$emit('input', version);
await nextTick();
expect(findVersionGroup().attributes()).not.toContain('state');
expect(findVersionGroup().attributes('invalid-feedback')).toBe(
ModelCreate.i18n.versionInvalid,
);
expect(findVersionGroup().attributes('description')).toBe('');
expect(findPrimaryButton().props()).toMatchObject({
variant: 'confirm',
disabled: true,
});
},
);
it.each(['1.0.0', '0.0.0-b', '24.99.99-b99'])(
'renders the version input label for valid state',
async (version) => {
findVersionInput().vm.$emit('input', version);
await nextTick();
expect(findVersionGroup().attributes('state')).toBe('true');
expect(findVersionGroup().attributes('valid-feedback')).toBe(
ModelCreate.i18n.versionValid,
);
expect(findVersionGroup().attributes('description')).toBe('');
expect(findPrimaryButton().props()).toMatchObject({
variant: 'confirm',
disabled: true,
});
},
);
it.each(['1.0.0', '0.0.0-b', '24.99.99-b99'])(
'renders the version input label for valid state',
async (version) => {
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', version);
await nextTick();
expect(findVersionGroup().attributes('state')).toBe('true');
expect(findPrimaryButton().props()).toMatchObject({
variant: 'confirm',
disabled: false,
});
},
);
it.each(['model name', ' modelname', 'modelname ', ' ', ''])(
'renders the modelnames as invalid',
......@@ -310,70 +163,8 @@ describe('ModelCreate', () => {
await findSecondaryButton().vm.$emit('click');
expect(findVersionInput().attributes('value')).toBe(undefined);
});
});
describe('Successful flow with version', () => {
beforeEach(async () => {
createWrapper();
findNameInput().vm.$emit('input', 'gpt-alice-1');
findMarkdownEditor().vm.$emit('input', 'My model description');
findVersionInput().vm.$emit('input', '1.0.0');
findVersionDescriptionInput().vm.$emit('input', 'My version description');
await Vue.nextTick();
zone().vm.$emit('change', files);
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await submitForm();
});
it('Makes a create model mutation upon confirm', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createModelMutation,
variables: {
projectPath: 'some/project',
name: 'gpt-alice-1',
description: 'My model description',
},
}),
);
});
it('Makes a create model version mutation upon confirm', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createModelVersionMutation,
variables: {
modelId: 'gid://gitlab/Ml::Model/1',
projectPath: 'some/project',
version: '1.0.0',
description: 'My version description',
},
}),
);
});
it('Uploads a files mutation upon confirm', () => {
expect(uploadModel).toHaveBeenCalledWith({
file,
importPath: '/api/v4/projects/1/packages/ml_models/1/files/',
subfolder: '',
maxAllowedFileSize: 99999,
onUploadProgress: expect.any(Function),
cancelToken: expect.any(Object),
});
});
it('Visits the model versions page upon successful create mutation', () => {
expect(visitUrlWithAlerts).toHaveBeenCalledWith('/some/project/-/ml/models/1/versions/1', [
{
id: 'import-artifact-alert',
message: 'Artifacts uploaded successfully.',
variant: 'info',
},
]);
expect(findNameInput().element.value).toBe(undefined);
expect(findDescriptionInput().element.value).toBe(undefined);
});
});
......@@ -388,104 +179,7 @@ describe('ModelCreate', () => {
});
it('Visits the model page upon successful create mutation without a version', () => {
expect(visitUrlWithAlerts).toHaveBeenCalledWith('/some/project/-/ml/models/1', [
{
id: 'import-artifact-alert',
message: 'Artifacts uploaded successfully.',
variant: 'info',
},
]);
});
});
describe('Failed flow with version', () => {
beforeEach(async () => {
const failedCreateModelVersionResolver = jest
.fn()
.mockResolvedValue(createModelVersionResponses.failure);
createWrapper(undefined, failedCreateModelVersionResolver);
jest.spyOn(apolloProvider.defaultClient, 'mutate');
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findVersionDescriptionInput().vm.$emit('input', 'My version description');
await Vue.nextTick();
zone().vm.$emit('change', files);
await submitForm();
});
it('Displays an alert upon failed model create mutation', () => {
expect(findGlAlert().text()).toBe('Version is invalid');
});
});
describe('Failed flow with version retried', () => {
beforeEach(async () => {
const failedCreateModelVersionResolver = jest
.fn()
.mockResolvedValueOnce(createModelVersionResponses.failure);
createWrapper(undefined, failedCreateModelVersionResolver);
jest.spyOn(apolloProvider.defaultClient, 'mutate');
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findVersionDescriptionInput().vm.$emit('input', 'My retried version description');
await submitForm();
});
it('Displays an alert upon failed model create mutation', async () => {
expect(findGlAlert().text()).toBe('Version is invalid');
await submitForm();
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createModelVersionMutation,
variables: {
modelId: 'gid://gitlab/Ml::Model/1',
projectPath: 'some/project',
version: '1.0.0',
description: 'My retried version description',
},
}),
);
});
});
describe('Failed flow with file upload retried', () => {
beforeEach(async () => {
createWrapper();
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findDescriptionInput().vm.$emit('input', 'My model description');
findVersionDescriptionInput().vm.$emit('input', 'My version description');
await Vue.nextTick();
zone().vm.$emit('change', files);
uploadModel.mockRejectedValueOnce('Artifact import error.');
await submitForm();
});
it('Visits the model versions page upon successful create mutation', async () => {
await submitForm(); // retry submit
expect(visitUrlWithAlerts).toHaveBeenCalledWith('/some/project/-/ml/models/1/versions/1', [
{
id: 'import-artifact-alert',
message: 'Artifact uploads completed with errors. file.txt: Artifact import error.',
variant: 'danger',
},
]);
});
it('Uploads a file mutation upon confirm', async () => {
await submitForm(); // retry submit
expect(uploadModel).toHaveBeenCalledWith({
file,
importPath: '/api/v4/projects/1/packages/ml_models/1/files/',
subfolder: '',
maxAllowedFileSize: 99999,
onUploadProgress: expect.any(Function),
cancelToken: expect.any(Object),
});
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1');
});
});
......
......@@ -71,7 +71,7 @@ describe('ShowMlModel', () => {
expect(findEmptyState().props()).toMatchObject({
title: 'Manage versions of your machine learning model',
description: 'Use versions to track performance, parameters, and metadata',
primaryText: 'Create model version',
primaryText: 'Create new version',
primaryLink: 'versions/new',
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment