Skip to content
代码片段 群组 项目
未验证 提交 fdd98eb1 编辑于 作者: Coung Ngo's avatar Coung Ngo 提交者: GitLab
浏览文件

Merge branch '456437-model-registry-empty-state-2' into 'master'

Moves empty state instructions on MLflow usage to a modal

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154559



Merged-by: default avatarCoung Ngo <cngo@gitlab.com>
Approved-by: default avatarCoung Ngo <cngo@gitlab.com>
Reviewed-by: default avatarAlper Akgun <aakgun@gitlab.com>
Reviewed-by: default avatarCoung Ngo <cngo@gitlab.com>
Reviewed-by: default avatarEduardo Bonet <ebonet@gitlab.com>
Co-authored-by: default avatarEduardo Bonet <ebonet@gitlab.com>
No related branches found
No related tags found
无相关合并请求
<script> <script>
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { MLFLOW_USAGE_MODAL_ID } from '../constants';
import MlflowUsageModal from './mlflow_usage_modal.vue';
export default { export default {
components: { components: {
GlDisclosureDropdownItem, GlDisclosureDropdownItem,
GlDisclosureDropdown, GlDisclosureDropdown,
MlflowUsageModal,
},
directives: {
GlModal: GlModalDirective,
}, },
inject: ['mlflowTrackingUrl'],
computed: { computed: {
copyIdItem() { mlflowUsageModal() {
return { return {
text: s__('MlModelRegistry|Copy MLflow tracking URL'), text: s__('MlModelRegistry|Using the MLflow client'),
action: () => {
this.$toast.show(s__('MlModelRegistry|Copied MLflow tracking URL to clipboard'));
},
}; };
}, },
}, },
modalId: MLFLOW_USAGE_MODAL_ID,
}; };
</script> </script>
...@@ -29,7 +32,9 @@ export default { ...@@ -29,7 +32,9 @@ export default {
icon="ellipsis_v" icon="ellipsis_v"
no-caret no-caret
> >
<gl-disclosure-dropdown-item :item="copyIdItem" :data-clipboard-text="mlflowTrackingUrl" /> <gl-disclosure-dropdown-item v-gl-modal="$options.modalId" :item="mlflowUsageModal" />
<slot></slot> <slot></slot>
<mlflow-usage-modal />
</gl-disclosure-dropdown> </gl-disclosure-dropdown>
</template> </template>
<script>
import { GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { visitUrl } from '~/lib/utils/url_utility';
import { MLFLOW_USAGE_MODAL_ID } from '../constants';
export default {
components: {
GlModal,
},
inject: ['mlflowTrackingUrl'],
computed: {
instructions() {
return [
{
label: s__('MlModelRegistry|Setting up the client'),
cmd: [
// eslint-disable-next-line @gitlab/require-i18n-strings
'import os',
'from mlflow import MlflowClient',
'',
`os.environ["MLFLOW_TRACKING_URI"] = "${this.mlflowTrackingUrl}"`,
'os.environ["MLFLOW_TRACKING_TOKEN"] = <your_gitlab_token>',
'',
'client = MlflowClient()',
].join('\n'),
},
{
label: s__('MlModelRegistry|Creating a model'),
cmd: [
"model_name = '<your_model_name>'",
// eslint-disable-next-line @gitlab/require-i18n-strings
"description = 'Model description'",
'model = client.create_registered_model(model_name, description=description)',
].join('\n'),
},
{
label: s__('MlModelRegistry|Creating a model version'),
cmd: [
'tags = { "gitlab.version": version }',
'model_version = client.create_model_version(model_name, version, tags=tags)',
].join('\n'),
},
{
label: s__('MlModelRegistry|Logging artifacts'),
cmd: [
'run_id = model_version.run_id',
'client.log_artifact(run_id, \'<local/path/to/file.txt>\', artifact_path="")',
].join('\n'),
},
];
},
},
methods: {
openDocs() {
visitUrl(
helpPagePath('user/project/ml/model_registry/index', {
anchor: 'creating-machine-learning-models-and-model-versions',
}),
true,
);
},
},
modal: {
title: s__('MlModelRegistry|Using the MLflow client'),
id: MLFLOW_USAGE_MODAL_ID,
firstLine: s__(
'MlModelRegistry|Creating models, model versions and candidates is also possible using the MLflow client:',
),
actionPrimary: {
text: s__('MlModelRegistry|MLflow compatibility documentation'),
attributes: {
variant: 'confirm',
},
},
},
};
</script>
<template>
<gl-modal
:modal-id="$options.modal.id"
:title="$options.modal.title"
:action-primary="$options.modal.actionPrimary"
@primary="openDocs"
>
<p>{{ $options.modal.firstLine }}</p>
<template v-for="instruction in instructions">
<div :key="instruction.label">
<label> {{ instruction.label }}</label>
<pre
class="code highlight gl-flex gl-border-none gl-text-left gl-p-2 gl-font-monospace"
data-testid="preview-code"
>
<code class="gl-grow">{{ instruction.cmd }}</code>
</pre>
</div>
</template>
</gl-modal>
</template>
<script> <script>
import { GlEmptyState, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlButton, GlModalDirective } from '@gitlab/ui';
import emptySvgUrl from '@gitlab/svgs/dist/illustrations/empty-state/empty-dag-md.svg?url'; import emptySvgUrl from '@gitlab/svgs/dist/illustrations/empty-state/empty-dag-md.svg?url';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { MLFLOW_USAGE_MODAL_ID } from '../constants';
export default { export default {
components: { components: {
GlEmptyState, GlEmptyState,
ClipboardButton,
GlButton, GlButton,
}, },
directives: {
GlModal: GlModalDirective,
},
inject: ['mlflowTrackingUrl'], inject: ['mlflowTrackingUrl'],
title: s__('MlModelRegistry|No models registered'), title: s__('MlModelRegistry|Import your machine learning models'),
description: s__( description: s__(
'MlModelRegistry|Import your machine learning using GitLab directly or using the MLflow client:', 'MlModelRegistry|Create your machine learning using GitLab directly or using the MLflow client',
), ),
createNew: s__('MlModelRegistry|Create model'), createNew: s__('MlModelRegistry|Create model'),
mlflowDocs: s__('MlModelRegistry|MLflow compatibility'), mlflowDocs: s__('MlModelRegistry|Create model with MLflow'),
helpPath: helpPagePath('user/project/ml/model_registry/index', { helpPath: helpPagePath('user/project/ml/model_registry/index', {
anchor: 'creating-machine-learning-models-and-model-versions', anchor: 'creating-machine-learning-models-and-model-versions',
}), }),
emptySvgPath: emptySvgUrl, emptySvgPath: emptySvgUrl,
computed: {
mlflowCommand() {
return [
// eslint-disable-next-line @gitlab/require-i18n-strings
'import os',
'from mlflow import MlflowClient',
'',
`os.environ["MLFLOW_TRACKING_URI"] = "${this.mlflowTrackingUrl}"`,
'os.environ["MLFLOW_TRACKING_TOKEN"] = <your_gitlab_token>',
'',
s__('MlModelRegistry|# Create a model'),
'client = MlflowClient()',
"model_name = '<your_model_name>'",
// eslint-disable-next-line @gitlab/require-i18n-strings
"description = 'Model description'",
'model = client.create_registered_model(model_name, description=description)',
'',
s__('MlModelRegistry|# Create a version'),
'tags = { "gitlab.version": version }',
'model_version = client.create_model_version(model_name, version, tags=tags)',
'',
s__('MlModelRegistry|# Log artifacts'),
'client.log_artifact(run_id, \'<local/path/to/file.txt>\', artifact_path="")',
].join('\n');
},
},
methods: { methods: {
emitOpenCreateModel() { emitOpenCreateModel() {
this.$emit('open-create-model'); this.$emit('open-create-model');
}, },
}, },
modalId: MLFLOW_USAGE_MODAL_ID,
}; };
</script> </script>
...@@ -62,29 +39,14 @@ export default { ...@@ -62,29 +39,14 @@ export default {
:svg-path="$options.emptySvgPath" :svg-path="$options.emptySvgPath"
:svg-height="null" :svg-height="null"
class="gl-py-8" class="gl-py-8"
:description="$options.description"
> >
<template #description>
<p>{{ $options.description }}</p>
<pre
class="code highlight gl-flex gl-border-none gl-text-left gl-p-2"
data-testid="preview-code"
>
<code>{{ mlflowCommand }}</code>
<clipboard-button
category="tertiary"
:text="mlflowCommand"
class="gl-self-start"
:title="__('Copy')"
/>
</pre>
</template>
<template #actions> <template #actions>
<gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="emitOpenCreateModel">{{ <gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="emitOpenCreateModel">{{
$options.createNew $options.createNew
}}</gl-button> }}</gl-button>
<gl-button class="gl-mb-3 gl-mr-3 gl-mx-2" :href="$options.helpPath" <gl-button v-gl-modal="$options.modalId" class="gl-mb-3 gl-mr-3 gl-mx-2">
>{{ $options.mlflowDocs }} {{ $options.mlflowDocs }}
</gl-button> </gl-button>
</template> </template>
</gl-empty-state> </gl-empty-state>
......
...@@ -23,6 +23,8 @@ export const MODEL_ENTITIES = { ...@@ -23,6 +23,8 @@ export const MODEL_ENTITIES = {
modelVersion: 'modelVersion', modelVersion: 'modelVersion',
}; };
export const MLFLOW_USAGE_MODAL_ID = 'model-registry-mlflow-usage-modal';
export const emptyArtifactFile = { export const emptyArtifactFile = {
file: null, file: null,
subfolder: '', subfolder: '',
......
...@@ -33152,15 +33152,6 @@ msgstr "" ...@@ -33152,15 +33152,6 @@ msgstr ""
msgid "MlExperimentTracking|Version" msgid "MlExperimentTracking|Version"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|# Create a model"
msgstr ""
msgid "MlModelRegistry|# Create a version"
msgstr ""
msgid "MlModelRegistry|# Log artifacts"
msgstr ""
msgid "MlModelRegistry|%d model" msgid "MlModelRegistry|%d model"
msgid_plural "MlModelRegistry|%d models" msgid_plural "MlModelRegistry|%d models"
msgstr[0] "" msgstr[0] ""
...@@ -33189,12 +33180,6 @@ msgstr "" ...@@ -33189,12 +33180,6 @@ msgstr ""
msgid "MlModelRegistry|Candidate not linked to a CI build" msgid "MlModelRegistry|Candidate not linked to a CI build"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Copied MLflow tracking URL to clipboard"
msgstr ""
msgid "MlModelRegistry|Copy MLflow tracking URL"
msgstr ""
msgid "MlModelRegistry|Create & import" msgid "MlModelRegistry|Create & import"
msgstr "" msgstr ""
   
...@@ -33210,12 +33195,27 @@ msgstr "" ...@@ -33210,12 +33195,27 @@ msgstr ""
msgid "MlModelRegistry|Create model version & import artifacts" msgid "MlModelRegistry|Create model version & import artifacts"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Create model with MLflow"
msgstr ""
msgid "MlModelRegistry|Create model, version & import artifacts" msgid "MlModelRegistry|Create model, version & import artifacts"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Create your machine learning using GitLab directly or using the MLflow client"
msgstr ""
msgid "MlModelRegistry|Creating a model"
msgstr ""
msgid "MlModelRegistry|Creating a model version"
msgstr ""
msgid "MlModelRegistry|Creating models is also possible through the MLflow client. %{linkStart}Follow the documentation to learn more.%{linkEnd}" msgid "MlModelRegistry|Creating models is also possible through the MLflow client. %{linkStart}Follow the documentation to learn more.%{linkEnd}"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Creating models, model versions and candidates is also possible using the MLflow client:"
msgstr ""
msgid "MlModelRegistry|Delete model" msgid "MlModelRegistry|Delete model"
msgstr "" msgstr ""
   
...@@ -33270,7 +33270,7 @@ msgstr "" ...@@ -33270,7 +33270,7 @@ msgstr ""
msgid "MlModelRegistry|ID" msgid "MlModelRegistry|ID"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Import your machine learning using GitLab directly or using the MLflow client:" msgid "MlModelRegistry|Import your machine learning models"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Info" msgid "MlModelRegistry|Info"
...@@ -33288,7 +33288,10 @@ msgstr "" ...@@ -33288,7 +33288,10 @@ msgstr ""
msgid "MlModelRegistry|Leave empty to skip version creation." msgid "MlModelRegistry|Leave empty to skip version creation."
msgstr "" msgstr ""
   
msgid "MlModelRegistry|MLflow compatibility" msgid "MlModelRegistry|Logging artifacts"
msgstr ""
msgid "MlModelRegistry|MLflow compatibility documentation"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|MLflow run ID" msgid "MlModelRegistry|MLflow run ID"
...@@ -33336,15 +33339,15 @@ msgstr "" ...@@ -33336,15 +33339,15 @@ msgstr ""
msgid "MlModelRegistry|No logged parameters" msgid "MlModelRegistry|No logged parameters"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|No models registered"
msgstr ""
msgid "MlModelRegistry|No registered versions" msgid "MlModelRegistry|No registered versions"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Parameters" msgid "MlModelRegistry|Parameters"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Setting up the client"
msgstr ""
msgid "MlModelRegistry|Start tracking your machine learning models" msgid "MlModelRegistry|Start tracking your machine learning models"
msgstr "" msgstr ""
   
...@@ -33366,6 +33369,9 @@ msgstr "" ...@@ -33366,6 +33369,9 @@ msgstr ""
msgid "MlModelRegistry|Use versions to track performance, parameters, and metadata" msgid "MlModelRegistry|Use versions to track performance, parameters, and metadata"
msgstr "" msgstr ""
   
msgid "MlModelRegistry|Using the MLflow client"
msgstr ""
msgid "MlModelRegistry|Version Description" msgid "MlModelRegistry|Version Description"
msgstr "" msgstr ""
   
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlDisclosureDropdownItem } from '@gitlab/ui'; import { GlDisclosureDropdownItem } from '@gitlab/ui';
import ActionsDropdown from '~/ml/model_registry/components/actions_dropdown.vue'; import ActionsDropdown from '~/ml/model_registry/components/actions_dropdown.vue';
import MlflowUsageModal from '~/ml/model_registry/components/mlflow_usage_modal.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { MLFLOW_USAGE_MODAL_ID } from '~/ml/model_registry/constants';
describe('ml/model_registry/components/actions_dropdown', () => { describe('ml/model_registry/components/actions_dropdown', () => {
let wrapper; let wrapper;
const showToast = jest.fn();
const createWrapper = () => { const createWrapper = () => {
wrapper = mount(ActionsDropdown, { wrapper = mount(ActionsDropdown, {
mocks: {
$toast: {
show: showToast,
},
},
provide: { provide: {
mlflowTrackingUrl: 'path/to/mlflow', mlflowTrackingUrl: 'path/to/mlflow',
}, },
directives: {
GlModal: createMockDirective('gl-modal'),
},
slots: { slots: {
default: 'Slot content', default: 'Slot content',
}, },
}); });
}; };
const findCopyLinkDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const findUsageModalDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const findModal = () => wrapper.findComponent(MlflowUsageModal);
it('has data-clipboard-text set to the correct url', () => { beforeEach(() => {
createWrapper(); createWrapper();
expect(findCopyLinkDropdownItem().text()).toBe('Copy MLflow tracking URL');
expect(findCopyLinkDropdownItem().attributes()['data-clipboard-text']).toBe('path/to/mlflow');
}); });
it('shows a success toast after copying the url to the clipboard', () => { it('renders open mlflow usage item', () => {
createWrapper(); expect(findUsageModalDropdownItem().text()).toBe('Using the MLflow client');
expect(getBinding(findUsageModalDropdownItem().element, 'gl-modal').value).toBe(
findCopyLinkDropdownItem().find('button').trigger('click'); MLFLOW_USAGE_MODAL_ID,
);
});
expect(showToast).toHaveBeenCalledWith('Copied MLflow tracking URL to clipboard'); it('renders modal', () => {
expect(findModal().exists()).toBe(true);
}); });
it('renders slots', () => { it('renders slots', () => {
createWrapper();
expect(wrapper.html()).toContain('Slot content'); expect(wrapper.html()).toContain('Slot content');
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import MlflowUsageModal from '~/ml/model_registry/components/mlflow_usage_modal.vue';
import { MLFLOW_USAGE_MODAL_ID } from '~/ml/model_registry/constants';
let wrapper;
const createWrapper = () => {
wrapper = shallowMount(MlflowUsageModal, {
provide: { mlflowTrackingUrl: 'path/to/mlflow' },
});
};
const findModal = () => wrapper.findComponent(GlModal);
describe('ml/model_registry/components/mlflow_usage_modal.vue', () => {
beforeEach(() => {
createWrapper();
});
it('renders the modal with correct props', () => {
expect(findModal().props()).toMatchObject({
title: 'Using the MLflow client',
modalId: MLFLOW_USAGE_MODAL_ID,
actionPrimary: {
text: 'MLflow compatibility documentation',
attributes: {
variant: 'confirm',
},
},
});
});
it('renders the text', () => {
const text = findModal().text();
const expectedLines = [
'Creating models, model versions and candidates is also possible using the MLflow client',
'Setting up the client',
'import os',
'from mlflow import MlflowClient',
'os.environ["MLFLOW_TRACKING_URI"] = "path/to/mlflow"',
'os.environ["MLFLOW_TRACKING_TOKEN"] = <your_gitlab_token>',
'Creating a model',
'client = MlflowClient()',
"model_name = '<your_model_name>'",
"description = 'Model description'",
'model = client.create_registered_model(model_name, description=description)',
'Creating a model version',
'tags = { "gitlab.version": version }',
'model_version = client.create_model_version(model_name, version, tags=tags)',
'Logging artifacts',
'client.log_artifact(run_id, \'<local/path/to/file.txt>\', artifact_path="")',
];
expectedLines.forEach((line) => expect(text).toContain(line));
});
});
import { GlEmptyState, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import EmptyState from '~/ml/model_registry/components/model_list_empty_state.vue'; import EmptyState from '~/ml/model_registry/components/model_list_empty_state.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { MLFLOW_USAGE_MODAL_ID } from '~/ml/model_registry/constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
let wrapper; let wrapper;
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMount(EmptyState, { wrapper = shallowMount(EmptyState, {
provide: { mlflowTrackingUrl: 'path/to/mlflow', createModelPath: '/path/to/create' }, provide: { mlflowTrackingUrl: 'path/to/mlflow' },
directives: {
GlModal: createMockDirective('gl-modal'),
},
}); });
}; };
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCopyButton = () => wrapper.findComponent(ClipboardButton);
const findCreateButton = () => wrapper.findComponent(GlButton); const findCreateButton = () => wrapper.findComponent(GlButton);
const findDocsButton = () => wrapper.findAllComponents(GlButton).at(1); const findDocsButton = () => wrapper.findAllComponents(GlButton).at(1);
const mlflowCmd = [
'import os',
'from mlflow import MlflowClient',
'',
'os.environ["MLFLOW_TRACKING_URI"] = "path/to/mlflow"',
'os.environ["MLFLOW_TRACKING_TOKEN"] = <your_gitlab_token>',
'',
'# Create a model',
'client = MlflowClient()',
"model_name = '<your_model_name>'",
"description = 'Model description'",
'model = client.create_registered_model(model_name, description=description)',
'',
'# Create a version',
'tags = { "gitlab.version": version }',
'model_version = client.create_model_version(model_name, version, tags=tags)',
'',
'# Log artifacts',
'client.log_artifact(run_id, \'<local/path/to/file.txt>\', artifact_path="")',
].join('\n');
describe('ml/model_registry/components/model_list_empty_state.vue', () => { describe('ml/model_registry/components/model_list_empty_state.vue', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper();
}); });
describe('when entity type is model', () => { it('renders empty state', () => {
it('shows the correct empty state', () => { expect(findEmptyState().props()).toMatchObject({
expect(findEmptyState().props()).toMatchObject({ title: 'Import your machine learning models',
title: 'No models registered', svgPath: 'file-mock',
svgPath: 'file-mock', description: 'Create your machine learning using GitLab directly or using the MLflow client',
});
expect(findEmptyState().text()).toContain(
'Import your machine learning using GitLab directly or using the MLflow client:',
);
expect(findEmptyState().text()).toContain(mlflowCmd);
});
it('creates the copy text button', () => {
expect(findCopyButton().props('text')).toBe(mlflowCmd);
}); });
});
it('creates button to open model creation', () => { it('creates button to open model creation', () => {
expect(findCreateButton().text()).toBe('Create model'); expect(findCreateButton().text()).toBe('Create model');
}); });
it('clicking creates triggers open-create-model', async () => { it('clicking creates triggers open-create-model', async () => {
await findCreateButton().vm.$emit('click'); await findCreateButton().vm.$emit('click');
expect(wrapper.emitted('open-create-model')).toHaveLength(1); expect(wrapper.emitted('open-create-model')).toHaveLength(1);
}); });
it('creates button to docs', () => { it('creates button to docs', () => {
expect(findDocsButton().text()).toBe('MLflow compatibility'); expect(findDocsButton().text()).toBe('Create model with MLflow');
expect(findDocsButton().attributes('href')).toBe( expect(getBinding(findDocsButton().element, 'gl-modal').value).toBe(MLFLOW_USAGE_MODAL_ID);
'/help/user/project/ml/model_registry/index#creating-machine-learning-models-and-model-versions',
);
});
}); });
}); });
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册