diff options
Diffstat (limited to 'app/assets/javascripts/ml')
7 files changed, 276 insertions, 71 deletions
diff --git a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue index 59b68fc0063..7a04ccfe163 100644 --- a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue +++ b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue @@ -1,32 +1,29 @@ <script> -import { isEmpty } from 'lodash'; -import { GlBadge, GlButton, GlTooltipDirective } from '@gitlab/ui'; -import Pagination from '~/vue_shared/components/incubation/pagination.vue'; +import { GlExperimentBadge, GlButton } from '@gitlab/ui'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import EmptyState from '../components/empty_state.vue'; import * as i18n from '../translations'; -import { BASE_SORT_FIELDS, MODEL_ENTITIES } from '../constants'; -import SearchBar from '../components/search_bar.vue'; +import { BASE_SORT_FIELDS, GRAPHQL_PAGE_SIZE, MODEL_ENTITIES } from '../constants'; import ModelRow from '../components/model_row.vue'; import ActionsDropdown from '../components/actions_dropdown.vue'; +import getModelsQuery from '../graphql/queries/get_models.query.graphql'; +import { makeLoadModelErrorMessage } from '../translations'; +import SearchableList from '../components/searchable_list.vue'; export default { name: 'IndexMlModels', components: { - Pagination, ModelRow, - SearchBar, MetadataItem, TitleArea, - GlBadge, - EmptyState, + GlExperimentBadge, GlButton, + EmptyState, ActionsDropdown, - }, - directives: { - GlTooltip: GlTooltipDirective, + SearchableList, }, provide() { return { @@ -34,23 +31,14 @@ export default { }; }, props: { - models: { - type: Array, - required: true, - }, - pageInfo: { - type: Object, + projectPath: { + type: String, required: true, }, createModelPath: { type: String, required: true, }, - modelCount: { - type: Number, - required: false, - default: 0, - }, canWriteModelRegistry: { type: Boolean, required: false, @@ -62,9 +50,68 @@ export default { default: '', }, }, + apollo: { + models: { + query: getModelsQuery, + variables() { + return this.queryVariables; + }, + update(data) { + return data?.project?.mlModels ?? []; + }, + error(error) { + this.handleError(error); + }, + }, + }, + data() { + return { + models: [], + errorMessage: undefined, + }; + }, computed: { - hasModels() { - return !isEmpty(this.models); + pageInfo() { + return this.models?.pageInfo ?? {}; + }, + items() { + return this.models?.nodes ?? []; + }, + count() { + return this.models?.count ?? 0; + }, + isLoading() { + return this.$apollo.queries.models.loading; + }, + queryVariables() { + return { + fullPath: this.projectPath, + first: GRAPHQL_PAGE_SIZE, + }; + }, + }, + methods: { + fetchPage(variables) { + const vars = { + ...this.queryVariables, + ...variables, + name: variables.name, + orderBy: variables.orderBy?.toUpperCase() || 'CREATED_AT', + sort: variables.sort?.toUpperCase() || 'DESC', + }; + + this.$apollo.queries.models + .fetchMore({ + variables: vars, + updateQuery: (previousResult, { fetchMoreResult }) => { + return fetchMoreResult; + }, + }) + .catch(this.handleError); + }, + handleError(error) { + this.errorMessage = makeLoadModelErrorMessage(error.message); + Sentry.captureException(error); }, }, i18n, @@ -80,28 +127,39 @@ export default { <template #title> <div class="gl-flex-grow-1 gl-display-flex gl-align-items-center"> <span>{{ $options.i18n.TITLE_LABEL }}</span> - <gl-badge variant="neutral" class="gl-mx-4" size="lg" :href="$options.docHref"> - {{ __('Experiment') }} - </gl-badge> + <gl-experiment-badge :help-page-url="$options.docHref" /> </div> </template> <template #metadata-models-count> - <metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(modelCount)" /> + <metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(count)" /> </template> <template #right-actions> - <gl-button v-if="canWriteModelRegistry" :href="createModelPath">{{ - $options.i18n.CREATE_MODEL_LABEL - }}</gl-button> + <gl-button + v-if="canWriteModelRegistry" + :href="createModelPath" + data-testid="create-model-button" + >{{ $options.i18n.CREATE_MODEL_LABEL }}</gl-button + > <actions-dropdown /> </template> </title-area> - <template v-if="hasModels"> - <search-bar :sortable-fields="$options.sortableFields" /> - <model-row v-for="model in models" :key="model.name" :model="model" /> - <pagination v-bind="pageInfo" /> - </template> + <searchable-list + show-search + :page-info="pageInfo" + :items="items" + :error-message="errorMessage" + :is-loading="isLoading" + :sortable-fields="$options.sortableFields" + @fetch-page="fetchPage" + > + <template #empty-state> + <empty-state :entity-type="$options.modelEntity" /> + </template> - <empty-state v-else :entity-type="$options.modelEntity" /> + <template #item="{ item }"> + <model-row :model="item" /> + </template> + </searchable-list> </div> </template> diff --git a/app/assets/javascripts/ml/model_registry/components/candidate_list.vue b/app/assets/javascripts/ml/model_registry/components/candidate_list.vue index fca4462d7d2..d05a827c545 100644 --- a/app/assets/javascripts/ml/model_registry/components/candidate_list.vue +++ b/app/assets/javascripts/ml/model_registry/components/candidate_list.vue @@ -35,8 +35,7 @@ export default { return data.mlModel?.candidates ?? {}; }, error(error) { - this.errorMessage = makeLoadCandidatesErrorMessage(error.message); - Sentry.captureException(error); + this.handleError(error); }, }, }, @@ -67,12 +66,18 @@ export default { ...newPageInfo, }; - this.$apollo.queries.candidates.fetchMore({ - variables, - updateQuery: (previousResult, { fetchMoreResult }) => { - return fetchMoreResult; - }, - }); + this.$apollo.queries.candidates + .fetchMore({ + variables, + updateQuery: (previousResult, { fetchMoreResult }) => { + return fetchMoreResult; + }, + }) + .catch(this.handleError); + }, + handleError(error) { + this.errorMessage = makeLoadCandidatesErrorMessage(error.message); + Sentry.captureException(error); }, }, i18n: { diff --git a/app/assets/javascripts/ml/model_registry/components/model_row.vue b/app/assets/javascripts/ml/model_registry/components/model_row.vue index 15be7bd0b47..49f72c7cef2 100644 --- a/app/assets/javascripts/ml/model_registry/components/model_row.vue +++ b/app/assets/javascripts/ml/model_registry/components/model_row.vue @@ -1,11 +1,14 @@ <script> -import { GlLink } from '@gitlab/ui'; +import { GlLink, GlTruncate } from '@gitlab/ui'; import { s__, n__ } from '~/locale'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; export default { name: 'MlModelRow', components: { GlLink, + ListItem, + GlTruncate, }, props: { model: { @@ -15,7 +18,7 @@ export default { }, computed: { hasVersions() { - return this.model.version != null; + return this.model.versionCount > 0; }, modelVersionCountMessage() { if (!this.model.versionCount) return s__('MlModelRegistry|No registered versions'); @@ -31,15 +34,23 @@ export default { </script> <template> - <div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-py-3"> - <gl-link :href="model.path" class="gl-text-body gl-font-weight-bold gl-line-height-24"> - {{ model.name }} - </gl-link> + <list-item v-bind="$attrs"> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center"> + <gl-link class="gl-text-body" :href="model._links.showPath"> + <gl-truncate :text="model.name" /> + </gl-link> + </div> + </template> - <div class="gl-text-secondary"> - <gl-link v-if="hasVersions" :href="model.versionPath">{{ model.version }}</gl-link> + <template #left-secondary> + <div class="gl-text-secondary"> + <gl-link v-if="hasVersions" :href="model.latestVersion._links.showPath">{{ + model.latestVersion.version + }}</gl-link> - {{ modelVersionCountMessage }} - </div> - </div> + {{ modelVersionCountMessage }} + </div> + </template> + </list-item> </template> diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_list.vue b/app/assets/javascripts/ml/model_registry/components/model_version_list.vue index 5a649a9596a..ea5258a299e 100644 --- a/app/assets/javascripts/ml/model_registry/components/model_version_list.vue +++ b/app/assets/javascripts/ml/model_registry/components/model_version_list.vue @@ -36,8 +36,7 @@ export default { return data.mlModel?.versions ?? {}; }, error(error) { - this.errorMessage = makeLoadVersionsErrorMessage(error.message); - Sentry.captureException(error); + this.handleError(error); }, }, }, @@ -68,12 +67,18 @@ export default { ...pageInfo, }; - this.$apollo.queries.modelVersions.fetchMore({ - variables, - updateQuery: (previousResult, { fetchMoreResult }) => { - return fetchMoreResult; - }, - }); + this.$apollo.queries.modelVersions + .fetchMore({ + variables, + updateQuery: (previousResult, { fetchMoreResult }) => { + return fetchMoreResult; + }, + }) + .catch(this.handleError); + }, + handleError(error) { + this.errorMessage = makeLoadVersionsErrorMessage(error.message); + Sentry.captureException(error); }, }, modelVersionEntity: MODEL_ENTITIES.modelVersion, diff --git a/app/assets/javascripts/ml/model_registry/components/searchable_list.vue b/app/assets/javascripts/ml/model_registry/components/searchable_list.vue index 05062ae6fbf..1ff8cc578a1 100644 --- a/app/assets/javascripts/ml/model_registry/components/searchable_list.vue +++ b/app/assets/javascripts/ml/model_registry/components/searchable_list.vue @@ -2,11 +2,14 @@ import { GlAlert } from '@gitlab/ui'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; -import { GRAPHQL_PAGE_SIZE } from '~/ml/model_registry/constants'; +import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; +import { GRAPHQL_PAGE_SIZE, LIST_KEY_CREATED_AT } from '~/ml/model_registry/constants'; +import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; +import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; export default { name: 'SearchableList', - components: { PackagesListLoader, RegistryList, GlAlert }, + components: { PackagesListLoader, RegistryList, RegistrySearch, GlAlert }, props: { items: { type: Array, @@ -26,30 +29,92 @@ export default { required: false, default: '', }, + showSearch: { + type: Boolean, + required: false, + default: false, + }, + sortableFields: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + const query = queryToObject(window.location.search); + + const filter = query.name ? [{ value: { data: query.name }, type: FILTERED_SEARCH_TERM }] : []; + + const orderBy = query.orderBy || LIST_KEY_CREATED_AT; + + return { + filters: filter, + sorting: { + orderBy, + sort: (query.sort || 'desc').toLowerCase(), + }, + }; }, computed: { isListEmpty() { return this.items.length === 0; }, + parsedQuery() { + const name = this.filters + .map((f) => f.value.data) + .join(' ') + .trim(); + + const filterByQuery = name === '' ? {} : { name }; + + return { ...filterByQuery, ...this.sorting }; + }, + }, + created() { + this.nextPage(); }, methods: { prevPage() { - const pageInfo = { + const variables = { first: null, last: GRAPHQL_PAGE_SIZE, before: this.pageInfo.startCursor, + ...this.parsedQuery, }; - this.$emit('fetch-page', pageInfo); + this.fetchPage(variables); }, nextPage() { - const pageInfo = { + const variables = { first: GRAPHQL_PAGE_SIZE, last: null, after: this.pageInfo.endCursor, + ...this.parsedQuery, }; - this.$emit('fetch-page', pageInfo); + this.fetchPage(variables); + }, + fetchPage(variables) { + updateHistory({ + url: setUrlParams(variables, window.location.href, true), + title: document.title, + replace: true, + }); + + this.$emit('fetch-page', variables); + }, + submitFilters() { + this.fetchPage(this.parsedQuery); + }, + updateFilters(newValue) { + this.filters = newValue; + }, + updateSorting(newValue) { + this.sorting = { ...this.sorting, ...newValue }; + }, + updateSortingAndEmitUpdate(newValue) { + this.updateSorting(newValue); + this.submitFilters(); }, }, }; @@ -57,6 +122,16 @@ export default { <template> <div> + <registry-search + v-if="showSearch" + :filters="filters" + :sorting="sorting" + :sortable-fields="sortableFields" + @sorting:changed="updateSortingAndEmitUpdate" + @filter:changed="updateFilters" + @filter:submit="submitFilters" + @filter:clear="filters = []" + /> <packages-list-loader v-if="isLoading" /> <gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false"> {{ errorMessage }} diff --git a/app/assets/javascripts/ml/model_registry/graphql/queries/get_models.query.graphql b/app/assets/javascripts/ml/model_registry/graphql/queries/get_models.query.graphql new file mode 100644 index 00000000000..a9559bd7f5d --- /dev/null +++ b/app/assets/javascripts/ml/model_registry/graphql/queries/get_models.query.graphql @@ -0,0 +1,46 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" + +query getModels( + $fullPath: ID! + $name: String + $orderBy: MlModelsOrderBy + $sort: SortDirectionEnum + $first: Int + $last: Int + $after: String + $before: String +) { + project(fullPath: $fullPath) { + id + mlModels( + name: $name + orderBy: $orderBy + sort: $sort + after: $after + before: $before + first: $first + last: $last + ) { + count + nodes { + id + name + versionCount + createdAt + latestVersion { + id + version + _links { + showPath + } + } + _links { + showPath + } + } + pageInfo { + ...PageInfo + } + } + } +} diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js index 006142979e2..9d3e1e7badb 100644 --- a/app/assets/javascripts/ml/model_registry/translations.js +++ b/app/assets/javascripts/ml/model_registry/translations.js @@ -47,6 +47,11 @@ export const makeLoadVersionsErrorMessage = (message) => message, }); +export const makeLoadModelErrorMessage = (message) => + sprintf(s__('MlModelRegistry|Failed to load model with error: %{message}'), { + message, + }); + export const NO_CANDIDATES_LABEL = s__('MlModelRegistry|This model has no candidates'); export const makeLoadCandidatesErrorMessage = (message) => sprintf(s__('MlModelRegistry|Failed to load model candidates with error: %{message}'), { |