Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
42a4fe5b39
commit
95e5fa3fb3
|
|
@ -1231,6 +1231,7 @@ lib/gitlab/checks/**
|
|||
/lib/tasks/gitlab/password.rake
|
||||
/lib/tasks/tokens.rake
|
||||
|
||||
# Necessary for GitLab availability
|
||||
[Verify] @gitlab-org/maintainers/cicd-verify @shinya.maeda @stanhu @ayufan
|
||||
# With these catch-all rules we will require backend approval and use it as an
|
||||
# opportunity to refine specific rules defined in this section.
|
||||
|
|
@ -1382,6 +1383,7 @@ lib/gitlab/checks/**
|
|||
/lib/gitlab/ci/templates/Jobs/SAST.*.yml @gitlab-org/secure/static-analysis
|
||||
/lib/gitlab/ci/templates/Jobs/Secret-Detection.*.yml @gitlab-org/secure/static-analysis
|
||||
|
||||
# Require approvals for Project API entities affecting availability
|
||||
[Data Stores::Tenant Scale] @abdwdd @alexpooley @manojmj
|
||||
lib/api/entities/basic_project_details.rb
|
||||
lib/api/entities/project_with_access.rb
|
||||
|
|
@ -1440,17 +1442,18 @@ ee/lib/ee/api/entities/project.rb
|
|||
/ee/app/assets/javascripts/usage_quotas/seats/
|
||||
/ee/app/assets/javascripts/usage_quotas/storage/
|
||||
|
||||
[Manage::Foundations] @gitlab-org/manage/foundations/engineering
|
||||
^[Manage::Foundations] @gitlab-org/manage/foundations/engineering
|
||||
/lib/sidebars/
|
||||
/ee/lib/sidebars/
|
||||
/ee/lib/ee/sidebars/
|
||||
|
||||
# Necessary for availablity, similar to DB migrations
|
||||
[Global Search] @gitlab-org/search-team/migration-maintainers
|
||||
/ee/elastic/migrate/
|
||||
/ee/spec/elastic/migrate/
|
||||
/ee/spec/support/elastic.rb
|
||||
|
||||
[Create::IDE - Remote Development Backend] @gitlab-org/maintainers/remote-development/backend
|
||||
^[Create::IDE - Remote Development Backend] @gitlab-org/maintainers/remote-development/backend
|
||||
/ee/app/models/remote_development/
|
||||
/ee/app/policies/remote_development/
|
||||
/ee/app/finders/remote_development/
|
||||
|
|
@ -1476,14 +1479,14 @@ ee/lib/ee/api/entities/project.rb
|
|||
/ee/spec/services/remote_development/
|
||||
/qa/qa/specs/features/**/remote_development/ @gitlab-org/maintainers/remote-development/backend @gl-quality/qe-maintainers
|
||||
|
||||
[Create::IDE - Remote Development Frontend] @gitlab-org/maintainers/remote-development/frontend
|
||||
^[Create::IDE - Remote Development Frontend] @gitlab-org/maintainers/remote-development/frontend
|
||||
/ee/app/assets/remote_development/
|
||||
/ee/app/assets/**/remote_development/
|
||||
/ee/app/views/remote_development/
|
||||
/ee/spec/frontend/remote_development/
|
||||
/ee/spec/frontend/**/remote_development/
|
||||
|
||||
[Govern::Anti-abuse] @gitlab-org/modelops/anti-abuse
|
||||
^[Govern::Anti-abuse] @gitlab-org/modelops/anti-abuse
|
||||
/ee/app/controllers/users/identity_verification_controller.rb
|
||||
/ee/app/models/concerns/identity_verifiable.rb
|
||||
/ee/config/routes/identity_verification.rb
|
||||
|
|
|
|||
|
|
@ -88,31 +88,43 @@ linters:
|
|||
# These cops are incredibly noisy when it comes to HAML templates, so we
|
||||
# ignore them.
|
||||
- Layout/BlockAlignment
|
||||
- Layout/EndAlignment
|
||||
- Layout/HashAlignment
|
||||
- Layout/IndentationConsistency
|
||||
- Layout/IndentationWidth
|
||||
- Layout/LineLength
|
||||
- Layout/TrailingWhitespace
|
||||
- Lint/Void
|
||||
- Naming/FileName
|
||||
- Style/AlignParameters
|
||||
- Style/BlockNesting
|
||||
- Style/ElseAlignment
|
||||
- Style/FileName
|
||||
- Style/FinalNewline
|
||||
- Layout/ElseAlignment
|
||||
- Style/FrozenStringLiteralComment
|
||||
- Style/IfUnlessModifier
|
||||
- Style/IndentationWidth
|
||||
- Style/Next
|
||||
- Style/SoleNestedConditional
|
||||
- Style/TrailingWhitespace
|
||||
- Style/StringLiteralsInInterpolation
|
||||
- Style/WhileUntilModifier
|
||||
- Cop/StaticTranslationDefinition
|
||||
|
||||
# These cops should eventually get enabled
|
||||
|
||||
# haml-lint force enables these: https://github.com/sds/haml-lint/blob/v0.51.0/config/forced_rubocop_config.yml
|
||||
- Layout/ArgumentAlignment
|
||||
- Layout/ArrayAlignment
|
||||
- Layout/EndAlignment
|
||||
|
||||
- Cop/LineBreakAfterGuardClauses
|
||||
- Cop/LineBreakAroundConditionalBlock
|
||||
- Cop/ProjectPathHelper
|
||||
- Gitlab/FeatureAvailableUsage
|
||||
- Gitlab/Json
|
||||
- GitlabSecurity/PublicSend
|
||||
- Layout/FirstHashElementIndentation
|
||||
- Layout/EmptyLineAfterGuardClause
|
||||
- Layout/EmptyLines
|
||||
- Layout/EmptyLinesAroundBlockBody
|
||||
- Layout/ExtraSpacing
|
||||
- Layout/InitialIndentation
|
||||
- Layout/LeadingCommentSpace
|
||||
- Layout/MultilineHashBraceLayout
|
||||
- Layout/SpaceAroundOperators
|
||||
- Layout/SpaceBeforeComma
|
||||
- Layout/SpaceBeforeFirstArg
|
||||
|
|
@ -123,6 +135,7 @@ linters:
|
|||
- Lint/AssignmentInCondition
|
||||
- Lint/LiteralInInterpolation
|
||||
- Lint/ParenthesesAsGroupedExpression
|
||||
- Lint/RedundantStringCoercion
|
||||
- Lint/SafeNavigationConsistency
|
||||
- Lint/SymbolConversion
|
||||
- Lint/UnusedBlockArgument
|
||||
|
|
@ -140,6 +153,7 @@ linters:
|
|||
- Style/IdenticalConditionalBranches
|
||||
- Style/IfInsideElse
|
||||
- Style/InlineDisableAnnotation
|
||||
- Style/MultilineTernaryOperator
|
||||
- Style/NegatedIf
|
||||
- Style/NestedTernaryOperator
|
||||
- Style/RedundantInterpolation
|
||||
|
|
@ -148,7 +162,6 @@ linters:
|
|||
- Style/TernaryParentheses
|
||||
- Style/TrailingCommaInHashLiteral
|
||||
- Style/UnlessElse
|
||||
- Style/UnneededCondition
|
||||
- Style/WordArray
|
||||
- Style/ZeroLengthPredicate
|
||||
|
||||
|
|
@ -178,6 +191,9 @@ linters:
|
|||
TagName:
|
||||
enabled: true
|
||||
|
||||
TrailingEmptyLines:
|
||||
enabled: false
|
||||
|
||||
TrailingWhitespace:
|
||||
enabled: true
|
||||
|
||||
|
|
|
|||
2
DEI.md
2
DEI.md
|
|
@ -67,7 +67,7 @@ Inclusive leadership is addressed in our project through a variety of different
|
|||
- One or more project maintainers in the GitLab organization have completed neurodiversity and other DEI training.
|
||||
- One or more project maintainers in the GitLab organization are a member of a working group related to a DEI initiative.
|
||||
- One or more project maintainers in the GitLab organization participate in DEI group meetings or events.
|
||||
- GitLab's hiring managers ensure a diverse candidate slate and interview panel.
|
||||
- GitLab's hiring managers seek candidates from a diverse range of candidate pools to ensure the most qualified candidate is hired and GitLab's interview panels are representative of GitLab and society's diversity.
|
||||
|
||||
Our project recognizes that the inclusion of the DEI.md file and the provided reflection on the specific DEI metrics does not ensure community safety nor community inclusiveness. The inclusion of the DEI.md file signals that we, as a project, are committed to centering DEI in our project and regularly reviewing and reflecting on our project DEI practices.
|
||||
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -434,7 +434,7 @@ group :development, :test do
|
|||
|
||||
gem 'gitlab-styles', '~> 11.0.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
gem 'haml_lint', '~> 0.40.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'haml_lint', '~> 0.51', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'bundler-audit', '~> 0.9.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
# Benchmarking & profiling
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@
|
|||
{"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"},
|
||||
{"name":"guard-rspec","version":"4.7.3","platform":"ruby","checksum":"a47ba03cbd1e3c71e6ae8645cea97e203098a248aede507461a43e906e2f75ca"},
|
||||
{"name":"haml","version":"5.2.2","platform":"ruby","checksum":"6e759246556145642ef832d670fc06f9bd8539159a0e600847a00291dd7aae0c"},
|
||||
{"name":"haml_lint","version":"0.40.1","platform":"ruby","checksum":"b658322eb245399e40b19a27a341039c76aead5794bc622d469e877162e34802"},
|
||||
{"name":"haml_lint","version":"0.51.0","platform":"ruby","checksum":"6c5e73b979dcd806ddf0043971bfc2076f832c24722314503ebb1087c361a8e7"},
|
||||
{"name":"hamlit","version":"2.15.0","platform":"java","checksum":"fda165464e59337ab7cda6304a66bfdb607bb7155f25566da19c9ee7b98e03d1"},
|
||||
{"name":"hamlit","version":"2.15.0","platform":"ruby","checksum":"d2e8505362338945fa309c68b2b8be07ebdc181200ec6021223567bf66dac38e"},
|
||||
{"name":"hana","version":"1.3.7","platform":"ruby","checksum":"5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d"},
|
||||
|
|
|
|||
|
|
@ -872,11 +872,11 @@ GEM
|
|||
haml (5.2.2)
|
||||
temple (>= 0.8.0)
|
||||
tilt
|
||||
haml_lint (0.40.1)
|
||||
haml (>= 4.0, < 5.3)
|
||||
haml_lint (0.51.0)
|
||||
haml (>= 4.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
rubocop (>= 0.50.0)
|
||||
rubocop (>= 1.0)
|
||||
sysexits (~> 1.1)
|
||||
hamlit (2.15.0)
|
||||
temple (>= 0.8.2)
|
||||
|
|
@ -1923,7 +1923,7 @@ DEPENDENCIES
|
|||
grpc (~> 1.58.0)
|
||||
gssapi (~> 1.3.1)
|
||||
guard-rspec
|
||||
haml_lint (~> 0.40.0)
|
||||
haml_lint (~> 0.51)
|
||||
hamlit (~> 2.15.0)
|
||||
hashie (~> 5.0.0)
|
||||
health_check (~> 3.0)
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
|
||||
import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export default {
|
||||
name: 'ShowMlModelApp',
|
||||
components: {
|
||||
ModelVersionList,
|
||||
ModelVersionList: () => import('../components/model_version_list.vue'),
|
||||
CandidateList: () => import('../components/candidate_list.vue'),
|
||||
TitleArea,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
|
|
@ -74,6 +74,8 @@ export default {
|
|||
{{ $options.i18n.MODEL_CANDIDATES_TAB_LABEL }}
|
||||
<gl-badge size="sm" class="gl-tab-counter-badge">{{ candidateCount }}</gl-badge>
|
||||
</template>
|
||||
|
||||
<candidate-list :model-id="model.id" />
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { n__ } from '~/locale';
|
||||
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
|
||||
import { makeLoadCandidatesErrorMessage, NO_CANDIDATES_LABEL } from '../translations';
|
||||
import getModelCandidatesQuery from '../graphql/queries/get_model_candidates.query.graphql';
|
||||
import { GRAPHQL_PAGE_SIZE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'MlCandidateList',
|
||||
components: {
|
||||
GlAlert,
|
||||
CandidateListRow,
|
||||
PackagesListLoader,
|
||||
RegistryList,
|
||||
},
|
||||
props: {
|
||||
modelId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modelVersions: {},
|
||||
errorMessage: undefined,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
candidates: {
|
||||
query: getModelCandidatesQuery,
|
||||
variables() {
|
||||
return this.queryVariables;
|
||||
},
|
||||
update(data) {
|
||||
return data.mlModel?.candidates ?? {};
|
||||
},
|
||||
error(error) {
|
||||
this.errorMessage = makeLoadCandidatesErrorMessage(error.message);
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
gid() {
|
||||
return convertToGraphQLId('Ml::Model', this.modelId);
|
||||
},
|
||||
isListEmpty() {
|
||||
return this.count === 0;
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.candidates.loading;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.candidates?.pageInfo ?? {};
|
||||
},
|
||||
listTitle() {
|
||||
return n__('%d candidate', '%d candidates', this.count);
|
||||
},
|
||||
queryVariables() {
|
||||
return {
|
||||
id: this.gid,
|
||||
first: GRAPHQL_PAGE_SIZE,
|
||||
};
|
||||
},
|
||||
items() {
|
||||
return this.candidates?.nodes ?? [];
|
||||
},
|
||||
count() {
|
||||
return this.candidates?.count ?? 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchPage({ first = null, last = null, before = null, after = null } = {}) {
|
||||
const variables = {
|
||||
...this.queryVariables,
|
||||
first,
|
||||
last,
|
||||
before,
|
||||
after,
|
||||
};
|
||||
|
||||
this.$apollo.queries.candidates.fetchMore({
|
||||
variables,
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
return fetchMoreResult;
|
||||
},
|
||||
});
|
||||
},
|
||||
fetchPreviousCandidatesPage() {
|
||||
this.fetchPage({
|
||||
last: GRAPHQL_PAGE_SIZE,
|
||||
before: this.pageInfo?.startCursor,
|
||||
});
|
||||
},
|
||||
fetchNextCandidatesPage() {
|
||||
this.fetchPage({
|
||||
first: GRAPHQL_PAGE_SIZE,
|
||||
after: this.pageInfo?.endCursor,
|
||||
});
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
NO_CANDIDATES_LABEL,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isLoading">
|
||||
<packages-list-loader />
|
||||
</div>
|
||||
<gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">{{
|
||||
errorMessage
|
||||
}}</gl-alert>
|
||||
<div v-else-if="isListEmpty" class="gl-text-secondary">
|
||||
{{ $options.i18n.NO_CANDIDATES_LABEL }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<registry-list
|
||||
:hidden-delete="true"
|
||||
:is-loading="isLoading"
|
||||
:items="items"
|
||||
:pagination="pageInfo"
|
||||
:title="listTitle"
|
||||
@prev-page="fetchPreviousCandidatesPage"
|
||||
@next-page="fetchNextCandidatesPage"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<candidate-list-row :candidate="item" />
|
||||
</template>
|
||||
</registry-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
||||
export default {
|
||||
name: 'MlCandidateListRow',
|
||||
components: {
|
||||
ListItem,
|
||||
GlLink,
|
||||
GlTruncate,
|
||||
GlSprintf,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
props: {
|
||||
candidate: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pathToDetails() {
|
||||
return this.candidate._links?.showPath;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<list-item v-bind="$attrs">
|
||||
<template #left-primary>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<gl-link class="gl-text-body" :href="pathToDetails">
|
||||
<gl-truncate :text="candidate.name" />
|
||||
</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #left-secondary>
|
||||
<span>
|
||||
<gl-sprintf :message="__('Created %{timestamp}')">
|
||||
<template #timestamp>
|
||||
<time-ago-tooltip :time="candidate.createdAt" />
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
</list-item>
|
||||
</template>
|
||||
|
|
@ -4,10 +4,7 @@ import { n__ } from '~/locale';
|
|||
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import {
|
||||
FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE,
|
||||
NO_VERSIONS_LABEL,
|
||||
} from '~/ml/model_registry/translations';
|
||||
import { makeLoadVersionsErrorMessage, NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import getModelVersionsQuery from '../graphql/queries/get_model_versions.query.graphql';
|
||||
import { GRAPHQL_PAGE_SIZE } from '../constants';
|
||||
|
|
@ -29,7 +26,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
modelVersions: {},
|
||||
fetchModelVersionsError: false,
|
||||
errorMessage: undefined,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -42,7 +39,7 @@ export default {
|
|||
return data.mlModel?.versions ?? {};
|
||||
},
|
||||
error(error) {
|
||||
this.fetchModelVersionsError = true;
|
||||
this.errorMessage = makeLoadVersionsErrorMessage(error.message);
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
},
|
||||
|
|
@ -108,7 +105,6 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE,
|
||||
NO_VERSIONS_LABEL,
|
||||
},
|
||||
};
|
||||
|
|
@ -118,8 +114,8 @@ export default {
|
|||
<div v-if="isLoading">
|
||||
<packages-list-loader />
|
||||
</div>
|
||||
<gl-alert v-else-if="fetchModelVersionsError" variant="danger" :dismissible="false">{{
|
||||
$options.i18n.FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE
|
||||
<gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">{{
|
||||
errorMessage
|
||||
}}</gl-alert>
|
||||
<div v-else-if="isListEmpty" class="gl-text-secondary">
|
||||
{{ $options.i18n.NO_VERSIONS_LABEL }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
query getModelCandidates(
|
||||
$id: MlModelID!
|
||||
$first: Int
|
||||
$last: Int
|
||||
$after: String
|
||||
$before: String
|
||||
) {
|
||||
mlModel(id: $id) {
|
||||
id
|
||||
candidates(after: $after, before: $before, first: $first, last: $last) {
|
||||
count
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
_links {
|
||||
showPath
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
endCursor
|
||||
startCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { __, s__, n__ } from '~/locale';
|
||||
import { __, s__, n__, sprintf } from '~/locale';
|
||||
|
||||
export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
|
||||
export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
|
||||
|
|
@ -34,6 +34,14 @@ export const CI_SECTION_LABEL = s__('MlModelRegistry|CI Info');
|
|||
export const JOB_LABEL = __('Job');
|
||||
export const CI_USER_LABEL = s__('MlModelRegistry|Triggered by');
|
||||
export const CI_MR_LABEL = __('Merge request');
|
||||
export const FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE = s__(
|
||||
'MlModelRegistry|Failed to load model versions',
|
||||
);
|
||||
|
||||
export const makeLoadVersionsErrorMessage = (message) =>
|
||||
sprintf(s__('MlModelRegistry|Failed to load model versions 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}'), {
|
||||
message,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
CVE_ID_REQUEST_BUTTON_I18N,
|
||||
featureAccessLevelDescriptions,
|
||||
modelExperimentsHelpPath,
|
||||
modelRegistryHelpPath,
|
||||
} from '../constants';
|
||||
import { toggleHiddenClassBySelector } from '../external';
|
||||
import ProjectFeatureSetting from './project_feature_setting.vue';
|
||||
|
|
@ -63,6 +64,8 @@ export default {
|
|||
modelExperimentsHelpText: s__(
|
||||
'ProjectSettings|Track machine learning model experiments and artifacts.',
|
||||
),
|
||||
modelRegistryLabel: s__('ProjectSettings|Model registry'),
|
||||
modelRegistryHelpText: s__('ProjectSettings|Manage machine learning models.'),
|
||||
pagesLabel: s__('ProjectSettings|Pages'),
|
||||
repositoryLabel: s__('ProjectSettings|Repository'),
|
||||
requirementsLabel: s__('ProjectSettings|Requirements'),
|
||||
|
|
@ -83,7 +86,7 @@ export default {
|
|||
VISIBILITY_LEVEL_INTERNAL_INTEGER,
|
||||
VISIBILITY_LEVEL_PUBLIC_INTEGER,
|
||||
modelExperimentsHelpPath,
|
||||
|
||||
modelRegistryHelpPath,
|
||||
components: {
|
||||
CiCatalogSettings,
|
||||
ProjectFeatureSetting,
|
||||
|
|
@ -259,6 +262,7 @@ export default {
|
|||
mergeRequestsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
packageRegistryAccessLevel: featureAccessLevel.EVERYONE,
|
||||
modelExperimentsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
modelRegistryAccessLevel: featureAccessLevel.EVERYONE,
|
||||
buildsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
wikiAccessLevel: featureAccessLevel.EVERYONE,
|
||||
snippetsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
|
|
@ -411,6 +415,10 @@ export default {
|
|||
featureAccessLevel.PROJECT_MEMBERS,
|
||||
this.modelExperimentsAccessLevel,
|
||||
);
|
||||
this.modelRegistryAccessLevel = Math.min(
|
||||
featureAccessLevel.PROJECT_MEMBERS,
|
||||
this.modelRegistryAccessLevel,
|
||||
);
|
||||
this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel);
|
||||
this.snippetsAccessLevel = Math.min(
|
||||
featureAccessLevel.PROJECT_MEMBERS,
|
||||
|
|
@ -475,6 +483,8 @@ export default {
|
|||
this.wikiAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.modelExperimentsAccessLevel > featureAccessLevel.NOT_ENABLED)
|
||||
this.modelExperimentsAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.modelRegistryAccessLevel > featureAccessLevel.NOT_ENABLED)
|
||||
this.modelRegistryAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.snippetsAccessLevel > featureAccessLevel.NOT_ENABLED)
|
||||
this.snippetsAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.pagesAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
|
||||
|
|
@ -913,6 +923,19 @@ export default {
|
|||
name="project[project_feature_attributes][model_experiments_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="model-registry-settings"
|
||||
:label="$options.i18n.modelRegistryLabel"
|
||||
:help-text="$options.i18n.modelRegistryHelpText"
|
||||
:help-path="$options.modelRegistryHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="modelRegistryAccessLevel"
|
||||
:label="$options.i18n.modelRegistryLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][model_registry_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
<project-setting-row
|
||||
v-if="pagesAvailable && pagesAccessControlEnabled"
|
||||
ref="pages-settings"
|
||||
|
|
|
|||
|
|
@ -48,3 +48,5 @@ export const CVE_ID_REQUEST_BUTTON_I18N = {
|
|||
export const modelExperimentsHelpPath = helpPagePath(
|
||||
'user/project/ml/experiment_tracking/index.md',
|
||||
);
|
||||
|
||||
export const modelRegistryHelpPath = helpPagePath('user/project/ml/model_registry/index.md');
|
||||
|
|
|
|||
|
|
@ -467,6 +467,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
monitor_access_level
|
||||
infrastructure_access_level
|
||||
model_experiments_access_level
|
||||
model_registry_access_level
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -9,20 +9,16 @@ module Types
|
|||
value issue_type.upcase, value: issue_type, description: "#{issue_type.titleize} issue type"
|
||||
end
|
||||
|
||||
value 'TASK', value: 'task',
|
||||
description: 'Task issue type.',
|
||||
alpha: { milestone: '15.2' }
|
||||
|
||||
value 'OBJECTIVE', value: 'objective',
|
||||
description: 'Objective issue type. Available only when feature flag `okrs_mvc` is enabled.',
|
||||
alpha: { milestone: '15.6' }
|
||||
description: 'Objective issue type. Available only when feature flag `okrs_mvc` is enabled.',
|
||||
alpha: { milestone: '15.6' }
|
||||
|
||||
value 'KEY_RESULT', value: 'key_result',
|
||||
description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
|
||||
alpha: { milestone: '15.7' }
|
||||
description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
|
||||
alpha: { milestone: '15.7' }
|
||||
value 'EPIC', value: 'epic',
|
||||
description: 'Epic issue type. ' \
|
||||
'Available only when feature flag `namespace_level_work_items` is enabled.',
|
||||
alpha: { milestone: '16.7' }
|
||||
description: 'Epic issue type. ' \
|
||||
'Available only when feature flag `namespace_level_work_items` is enabled.',
|
||||
alpha: { milestone: '16.7' }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -687,7 +687,8 @@ module ProjectsHelper
|
|||
featureFlagsAccessLevel: feature.feature_flags_access_level,
|
||||
releasesAccessLevel: feature.releases_access_level,
|
||||
infrastructureAccessLevel: feature.infrastructure_access_level,
|
||||
modelExperimentsAccessLevel: feature.model_experiments_access_level
|
||||
modelExperimentsAccessLevel: feature.model_experiments_access_level,
|
||||
modelRegistryAccessLevel: feature.model_registry_access_level
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ module WorkItems
|
|||
end
|
||||
|
||||
def self.allowed_types_for_issues
|
||||
base_types.keys.excluding('task', 'objective', 'key_result', 'epic', 'ticket')
|
||||
base_types.keys.excluding('objective', 'key_result', 'epic', 'ticket')
|
||||
end
|
||||
|
||||
def default?
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_editor_assistant_tool
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130162
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423524
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::environments
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: create_project_subscription_graphql_endpoint
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133308
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429339
|
||||
milestone: '16.6'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
||||
|
|
@ -30452,7 +30452,7 @@ Issue type.
|
|||
| <a id="issuetypekey_result"></a>`KEY_RESULT` **{warning-solid}** | **Introduced** in 15.7. This feature is an Experiment. It can be changed or removed at any time. Key Result issue type. Available only when feature flag `okrs_mvc` is enabled. |
|
||||
| <a id="issuetypeobjective"></a>`OBJECTIVE` **{warning-solid}** | **Introduced** in 15.6. This feature is an Experiment. It can be changed or removed at any time. Objective issue type. Available only when feature flag `okrs_mvc` is enabled. |
|
||||
| <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. |
|
||||
| <a id="issuetypetask"></a>`TASK` **{warning-solid}** | **Introduced** in 15.2. This feature is an Experiment. It can be changed or removed at any time. Task issue type. |
|
||||
| <a id="issuetypetask"></a>`TASK` | Task issue type. |
|
||||
| <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. |
|
||||
|
||||
### `IterationSearchableField`
|
||||
|
|
|
|||
|
|
@ -200,6 +200,35 @@ by a reviewer before passing it to a maintainer as described in the
|
|||
Designers do not require a Product Designer to approve feature changes, unless the changes are community contributions.
|
||||
1. End-to-end changes include all files in the `qa` directory.
|
||||
|
||||
#### CODEOWNERS approval
|
||||
|
||||
Some merge requests require mandatory approval by specific groups.
|
||||
See `.gitlab/CODEOWNERS` for definitions.
|
||||
|
||||
Mandatory sections in `.gitlab/CODEOWNERS` should only be limited to cases where
|
||||
it is necessary due to:
|
||||
|
||||
- compliance
|
||||
- availability
|
||||
- security
|
||||
|
||||
When adding a mandatory section, you should track the impact on the new mandatory section
|
||||
on merge request rates.
|
||||
See the [Verify issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411559) for a good example.
|
||||
|
||||
All other cases should not use mandatory sections as we favor
|
||||
[responsbility over ridigity](https://handbook.gitlab.com/handbook/values/#freedom-and-responsibility-over-rigidity).
|
||||
|
||||
Additionally, the current structure of the monolith means that merge requests
|
||||
are likely to touch seemingly un-related parts.
|
||||
Multiple mandatory approvals means that such merge requests require the author
|
||||
to seek approvals, which is not efficient.
|
||||
|
||||
Efforts to improve this are in:
|
||||
|
||||
- <https://gitlab.com/groups/gitlab-org/-/epics/11624>
|
||||
- <https://gitlab.com/gitlab-org/gitlab/-/issues/377326>
|
||||
|
||||
#### Acceptance checklist
|
||||
|
||||
This checklist encourages the authors, reviewers, and maintainers of merge requests (MRs) to confirm changes were analyzed for high-impact risks to quality, performance, reliability, security, observability, and maintainability.
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ cannot change them:
|
|||
non-modifiable and are always run.
|
||||
- Explicitly set any [variables](../../ci/yaml/index.md#variables) the job references. This:
|
||||
- Ensures that project-level pipeline configurations do not set them and alter their
|
||||
behavior.
|
||||
behavior. For example, see `before_script` and `after_script` configuration in the [example configuration](#example-configuration).
|
||||
- Includes any jobs that drive the logic of your job.
|
||||
- Explicitly set the [container image](../../ci/yaml/index.md#image) to run the job in. This ensures that your script
|
||||
steps execute in the correct environment.
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ which you can customize to meet the specific needs of each project.
|
|||
- Verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/)
|
||||
is defined so that volumes can be dynamically provisioned for each workspace.
|
||||
- Install an Ingress controller of your choice (for example, `ingress-nginx`) and make
|
||||
that controller accessible over a domain. For example, point `*.workspaces.example.dev`
|
||||
and `workspaces.example.dev` to the load balancer exposed by the Ingress controller.
|
||||
that controller accessible over a domain.
|
||||
- In development environments, add an entry to the `/etc/hosts` file or update your DNS records.
|
||||
- In production environments, point `*.<workspaces.example.dev>` and `<workspaces.example.dev>`
|
||||
to the load balancer exposed by the Ingress controller.
|
||||
- [Install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions).
|
||||
- [Install](../clusters/agent/install/index.md) and [configure](gitlab_agent_configuration.md) the GitLab agent.
|
||||
- You must have at least the Developer role in the root group.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def execute
|
||||
return unless project.import_data
|
||||
return unless import_data_valid?
|
||||
|
||||
log_info(import_stage: 'import_pull_request_notes', message: 'starting', iid: object[:iid])
|
||||
|
||||
|
|
@ -45,6 +45,10 @@ module Gitlab
|
|||
|
||||
attr_reader :object, :project, :formatter, :user_finder
|
||||
|
||||
def import_data_valid?
|
||||
project.import_data&.credentials && project.import_data&.data
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def import_merge_event(merge_request, merge_event)
|
||||
log_info(import_stage: 'import_merge_event', message: 'starting', iid: merge_request.iid)
|
||||
|
|
|
|||
|
|
@ -172,6 +172,11 @@ msgid_plural "%d authors"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d candidate"
|
||||
msgid_plural "%d candidates"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d changed file"
|
||||
msgid_plural "%d changed files"
|
||||
msgstr[0] ""
|
||||
|
|
@ -30925,7 +30930,10 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Experiment"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Failed to load model versions"
|
||||
msgid "MlModelRegistry|Failed to load model candidates with error: %{message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Failed to load model versions with error: %{message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|ID"
|
||||
|
|
@ -30973,6 +30981,9 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|This model has no candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|This model has no versions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38014,6 +38025,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Make sure this pattern does not contradict the %{link_start}Push rules > Branch name%{link_end} setting."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Manage machine learning models."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Manage who can see the project in the public access directory."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38059,6 +38073,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Model experiments"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Model registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Monitor"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1079,10 +1079,10 @@ RSpec.describe Projects::IssuesController, :request_store, feature_category: :te
|
|||
end
|
||||
|
||||
context 'when trying to create a task' do
|
||||
it 'defaults to issue type' do
|
||||
it 'sets the correct issue_type' do
|
||||
issue = post_new_issue(issue_type: 'task')
|
||||
|
||||
expect(issue.work_item_type.base_type).to eq('issue')
|
||||
expect(issue.work_item_type.base_type).to eq('task')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1093,6 +1093,7 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
|
|||
monitor_access_level
|
||||
infrastructure_access_level
|
||||
model_experiments_access_level
|
||||
model_registry_access_level
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
import { GlBadge, GlTab } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { ShowMlModel } from '~/ml/model_registry/apps';
|
||||
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
|
||||
import CandidateList from '~/ml/model_registry/components/candidate_list.vue';
|
||||
import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { MODEL, makeModel } from '../mock_data';
|
||||
|
||||
const apolloProvider = createMockApollo([]);
|
||||
let wrapper;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const createWrapper = (model = MODEL) => {
|
||||
wrapper = shallowMount(ShowMlModel, { propsData: { model }, stubs: { GlTab } });
|
||||
wrapper = shallowMount(ShowMlModel, {
|
||||
apolloProvider,
|
||||
propsData: { model },
|
||||
stubs: { GlTab },
|
||||
});
|
||||
};
|
||||
|
||||
const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0);
|
||||
|
|
@ -19,6 +31,7 @@ const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge);
|
|||
const findModelVersionList = () => findVersionsTab().findComponent(ModelVersionList);
|
||||
const findModelVersionDetail = () => findDetailTab().findComponent(ModelVersionDetail);
|
||||
const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2);
|
||||
const findCandidateList = () => findCandidateTab().findComponent(CandidateList);
|
||||
const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge);
|
||||
const findTitleArea = () => wrapper.findComponent(TitleArea);
|
||||
const findVersionCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
|
||||
|
|
@ -90,5 +103,9 @@ describe('ShowMlModel', () => {
|
|||
it('shows the number of candidates in the tab', () => {
|
||||
expect(findCandidatesCountBadge().text()).toBe(MODEL.candidateCount.toString());
|
||||
});
|
||||
|
||||
it('shows a list of candidates', () => {
|
||||
expect(findCandidateList().props('modelId')).toBe(MODEL.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
|
||||
import { graphqlCandidates } from '../graphql_mock_data';
|
||||
|
||||
const CANDIDATE = graphqlCandidates[0];
|
||||
|
||||
let wrapper;
|
||||
const createWrapper = (candidate = CANDIDATE) => {
|
||||
wrapper = shallowMount(CandidateListRow, {
|
||||
propsData: { candidate },
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
GlTruncate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findListItem = () => wrapper.findComponent(ListItem);
|
||||
const findLink = () => findListItem().findComponent(GlLink);
|
||||
const findTruncated = () => findLink().findComponent(GlTruncate);
|
||||
const findTooltip = () => findListItem().findComponent(TimeAgoTooltip);
|
||||
|
||||
describe('ml/model_registry/components/candidate_list_row.vue', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('Has a link to the candidate', () => {
|
||||
expect(findTruncated().props('text')).toBe(CANDIDATE.name);
|
||||
expect(findLink().attributes('href')).toBe(CANDIDATE._links.showPath);
|
||||
});
|
||||
|
||||
it('Shows created at', () => {
|
||||
expect(findTooltip().props('time')).toBe(CANDIDATE.createdAt);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CandidateList from '~/ml/model_registry/components/candidate_list.vue';
|
||||
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
|
||||
import getModelCandidatesQuery from '~/ml/model_registry/graphql/queries/get_model_candidates.query.graphql';
|
||||
import { GRAPHQL_PAGE_SIZE } from '~/ml/model_registry/constants';
|
||||
import {
|
||||
emptyCandidateQuery,
|
||||
modelCandidatesQuery,
|
||||
graphqlCandidates,
|
||||
graphqlPageInfo,
|
||||
} from '../graphql_mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('ml/model_registry/components/candidate_list.vue', () => {
|
||||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findLoader = () => wrapper.findComponent(PackagesListLoader);
|
||||
const findRegistryList = () => wrapper.findComponent(RegistryList);
|
||||
const findListRow = () => wrapper.findComponent(CandidateListRow);
|
||||
const findAllRows = () => wrapper.findAllComponents(CandidateListRow);
|
||||
|
||||
const mountComponent = ({
|
||||
props = {},
|
||||
resolver = jest.fn().mockResolvedValue(modelCandidatesQuery()),
|
||||
} = {}) => {
|
||||
const requestHandlers = [[getModelCandidatesQuery, resolver]];
|
||||
apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
wrapper = shallowMount(CandidateList, {
|
||||
apolloProvider,
|
||||
propsData: {
|
||||
modelId: 2,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
RegistryList,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Sentry, 'captureException').mockImplementation();
|
||||
});
|
||||
|
||||
describe('when list is loaded and has no data', () => {
|
||||
const resolver = jest.fn().mockResolvedValue(emptyCandidateQuery);
|
||||
beforeEach(async () => {
|
||||
mountComponent({ resolver });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays empty slot message', () => {
|
||||
expect(wrapper.text()).toContain('This model has no candidates');
|
||||
});
|
||||
|
||||
it('does not display loader', () => {
|
||||
expect(findLoader().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not display rows', () => {
|
||||
expect(findListRow().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not display registry list', () => {
|
||||
expect(findRegistryList().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not display alert', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if load fails, alert', () => {
|
||||
beforeEach(async () => {
|
||||
const error = new Error('Failure!');
|
||||
mountComponent({ resolver: jest.fn().mockRejectedValue(error) });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('is displayed', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows error message', () => {
|
||||
expect(findAlert().text()).toContain('Failed to load model candidates with error: Failure!');
|
||||
});
|
||||
|
||||
it('is not dismissible', () => {
|
||||
expect(findAlert().props('dismissible')).toBe(false);
|
||||
});
|
||||
|
||||
it('is of variant danger', () => {
|
||||
expect(findAlert().attributes('variant')).toBe('danger');
|
||||
});
|
||||
|
||||
it('error is logged in sentry', () => {
|
||||
expect(Sentry.captureException).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when list is loaded with data', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays package registry list', () => {
|
||||
expect(findRegistryList().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('binds the right props', () => {
|
||||
expect(findRegistryList().props()).toMatchObject({
|
||||
items: graphqlCandidates,
|
||||
pagination: {},
|
||||
isLoading: false,
|
||||
hiddenDelete: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('displays candidate rows', () => {
|
||||
expect(findAllRows().exists()).toEqual(true);
|
||||
expect(findAllRows()).toHaveLength(graphqlCandidates.length);
|
||||
});
|
||||
|
||||
it('binds the correct props', () => {
|
||||
expect(findAllRows().at(0).props()).toMatchObject({
|
||||
candidate: expect.objectContaining(graphqlCandidates[0]),
|
||||
});
|
||||
|
||||
expect(findAllRows().at(1).props()).toMatchObject({
|
||||
candidate: expect.objectContaining(graphqlCandidates[1]),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display loader', () => {
|
||||
expect(findLoader().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not display empty message', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user interacts with pagination', () => {
|
||||
const resolver = jest.fn().mockResolvedValue(modelCandidatesQuery());
|
||||
|
||||
beforeEach(async () => {
|
||||
mountComponent({ resolver });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('when list emits next-page fetches the next set of records', async () => {
|
||||
findRegistryList().vm.$emit('next-page');
|
||||
await waitForPromises();
|
||||
|
||||
expect(resolver).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ after: graphqlPageInfo.endCursor, first: GRAPHQL_PAGE_SIZE }),
|
||||
);
|
||||
});
|
||||
|
||||
it('when list emits prev-page fetches the prev set of records', async () => {
|
||||
findRegistryList().vm.$emit('prev-page');
|
||||
await waitForPromises();
|
||||
|
||||
expect(resolver).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ before: graphqlPageInfo.startCursor, last: GRAPHQL_PAGE_SIZE }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
modelVersionsQuery,
|
||||
graphqlModelVersions,
|
||||
graphqlPageInfo,
|
||||
} from '../mock_data';
|
||||
} from '../graphql_mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
@ -85,7 +85,8 @@ describe('ModelVersionList', () => {
|
|||
|
||||
describe('if load fails, alert', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponent({ resolver: jest.fn().mockRejectedValue() });
|
||||
const error = new Error('Failure!');
|
||||
mountComponent({ resolver: jest.fn().mockRejectedValue(error) });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
|
@ -95,7 +96,7 @@ describe('ModelVersionList', () => {
|
|||
});
|
||||
|
||||
it('shows error message', () => {
|
||||
expect(findAlert().text()).toMatchInterpolatedText('Failed to load model versions');
|
||||
expect(findAlert().text()).toContain('Failed to load model versions with error: Failure!');
|
||||
});
|
||||
|
||||
it('is not dismissible', () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import ModelVersionRow from '~/ml/model_registry/components/model_version_row.vue';
|
||||
import { graphqlModelVersions } from '../mock_data';
|
||||
import { graphqlModelVersions } from '../graphql_mock_data';
|
||||
|
||||
let wrapper;
|
||||
const createWrapper = (modelVersion = graphqlModelVersions[0]) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { defaultPageInfo } from './mock_data';
|
||||
|
||||
export const graphqlPageInfo = {
|
||||
...defaultPageInfo,
|
||||
__typename: 'PageInfo',
|
||||
};
|
||||
|
||||
export const graphqlModelVersions = [
|
||||
{
|
||||
createdAt: '2021-08-10T09:33:54Z',
|
||||
id: 'gid://gitlab/Ml::ModelVersion/243',
|
||||
version: '1.0.1',
|
||||
_links: {
|
||||
showPath: '/path/to/modelversion/243',
|
||||
},
|
||||
__typename: 'MlModelVersion',
|
||||
},
|
||||
{
|
||||
createdAt: '2021-08-10T09:33:54Z',
|
||||
id: 'gid://gitlab/Ml::ModelVersion/244',
|
||||
version: '1.0.2',
|
||||
_links: {
|
||||
showPath: '/path/to/modelversion/244',
|
||||
},
|
||||
__typename: 'MlModelVersion',
|
||||
},
|
||||
];
|
||||
|
||||
export const modelVersionsQuery = (versions = graphqlModelVersions) => ({
|
||||
data: {
|
||||
mlModel: {
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
versions: {
|
||||
count: versions.length,
|
||||
nodes: versions,
|
||||
pageInfo: graphqlPageInfo,
|
||||
__typename: 'MlModelConnection',
|
||||
},
|
||||
__typename: 'MlModelType',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const graphqlCandidates = [
|
||||
{
|
||||
id: 'gid://gitlab/Ml::Candidate/1',
|
||||
name: 'narwhal-aardvark-heron-6953',
|
||||
createdAt: '2023-12-06T12:41:48Z',
|
||||
_links: {
|
||||
showPath: '/path/to/candidate/1',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ml::Candidate/2',
|
||||
name: 'anteater-chimpanzee-snake-1254',
|
||||
createdAt: '2023-12-06T12:41:48Z',
|
||||
_links: {
|
||||
showPath: '/path/to/candidate/2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const modelCandidatesQuery = (candidates = graphqlCandidates) => ({
|
||||
data: {
|
||||
mlModel: {
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
candidates: {
|
||||
count: candidates.length,
|
||||
nodes: candidates,
|
||||
pageInfo: graphqlPageInfo,
|
||||
__typename: 'MlCandidateConnection',
|
||||
},
|
||||
__typename: 'MlModelType',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const emptyModelVersionsQuery = {
|
||||
data: {
|
||||
mlModel: {
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
versions: {
|
||||
count: 0,
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
endCursor: 'endCursor',
|
||||
startCursor: 'startCursor',
|
||||
},
|
||||
__typename: 'MlModelConnection',
|
||||
},
|
||||
__typename: 'MlModelType',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const emptyCandidateQuery = {
|
||||
data: {
|
||||
mlModel: {
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
candidates: {
|
||||
count: 0,
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
endCursor: 'endCursor',
|
||||
startCursor: 'startCursor',
|
||||
},
|
||||
__typename: 'MlCandidateConnection',
|
||||
},
|
||||
__typename: 'MlModelType',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -102,64 +102,3 @@ export const defaultPageInfo = Object.freeze({
|
|||
hasNextPage: true,
|
||||
hasPreviousPage: true,
|
||||
});
|
||||
|
||||
export const graphqlPageInfo = {
|
||||
...defaultPageInfo,
|
||||
__typename: 'PageInfo',
|
||||
};
|
||||
|
||||
export const graphqlModelVersions = [
|
||||
{
|
||||
createdAt: '2021-08-10T09:33:54Z',
|
||||
id: 'gid://gitlab/Ml::ModelVersion/243',
|
||||
version: '1.0.1',
|
||||
_links: {
|
||||
showPath: '/path/to/modelversion/243',
|
||||
},
|
||||
__typename: 'MlModelVersion',
|
||||
},
|
||||
{
|
||||
createdAt: '2021-08-10T09:33:54Z',
|
||||
id: 'gid://gitlab/Ml::ModelVersion/244',
|
||||
version: '1.0.2',
|
||||
_links: {
|
||||
showPath: '/path/to/modelversion/244',
|
||||
},
|
||||
__typename: 'MlModelVersion',
|
||||
},
|
||||
];
|
||||
|
||||
export const modelVersionsQuery = (versions = graphqlModelVersions) => ({
|
||||
data: {
|
||||
mlModel: {
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
versions: {
|
||||
count: versions.length,
|
||||
nodes: versions,
|
||||
pageInfo: graphqlPageInfo,
|
||||
__typename: 'MlModelConnection',
|
||||
},
|
||||
__typename: 'MlModelType',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const emptyModelVersionsQuery = {
|
||||
data: {
|
||||
mlModel: {
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
versions: {
|
||||
count: 0,
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
endCursor: 'endCursor',
|
||||
startCursor: 'startCursor',
|
||||
},
|
||||
__typename: 'MlModelConnection',
|
||||
},
|
||||
__typename: 'MlModelType',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ describe('Settings Panel', () => {
|
|||
const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
|
||||
const findModelExperimentsSettings = () =>
|
||||
wrapper.findComponent({ ref: 'model-experiments-settings' });
|
||||
const findModelRegistrySettings = () => wrapper.findComponent({ ref: 'model-registry-settings' });
|
||||
|
||||
describe('Project Visibility', () => {
|
||||
it('should set the project visibility help path', () => {
|
||||
|
|
@ -758,4 +759,11 @@ describe('Settings Panel', () => {
|
|||
expect(findModelExperimentsSettings().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('Model registry', () => {
|
||||
it('shows model registry toggle', () => {
|
||||
wrapper = mountComponent({});
|
||||
|
||||
expect(findModelRegistrySettings().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -892,7 +892,8 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
featureFlagsAccessLevel: project.project_feature.feature_flags_access_level,
|
||||
releasesAccessLevel: project.project_feature.releases_access_level,
|
||||
infrastructureAccessLevel: project.project_feature.infrastructure_access_level,
|
||||
modelExperimentsAccessLevel: project.project_feature.model_experiments_access_level
|
||||
modelExperimentsAccessLevel: project.project_feature.model_experiments_access_level,
|
||||
modelRegistryAccessLevel: project.project_feature.model_registry_access_level
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporter, feature_category: :importers do
|
||||
include AfterNextHelpers
|
||||
|
||||
let_it_be(:project) do
|
||||
let_it_be_with_reload(:project) do
|
||||
create(:project, :repository, :import_started,
|
||||
import_data_attributes: {
|
||||
data: { 'project_key' => 'key', 'repo_slug' => 'slug' },
|
||||
|
|
@ -82,19 +82,6 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporte
|
|||
subject(:importer) { described_class.new(project.reload, pull_request.to_hash) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'when the project has been marked as failed' do
|
||||
before do
|
||||
project.import_state.mark_as_failed('error')
|
||||
end
|
||||
|
||||
it 'does not log and does not import notes' do
|
||||
expect(Gitlab::BitbucketServerImport::Logger)
|
||||
.not_to receive(:info).with(include(import_stage: 'import_pull_request_notes', message: 'starting'))
|
||||
|
||||
expect { importer.execute }.not_to change { Note.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a matching merge request is not found' do
|
||||
it 'does nothing' do
|
||||
expect { importer.execute }.not_to change { Note.count }
|
||||
|
|
@ -312,5 +299,40 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporte
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'import is skipped' do
|
||||
it 'does not log and does not import notes' do
|
||||
expect(Gitlab::BitbucketServerImport::Logger)
|
||||
.not_to receive(:info).with(include(import_stage: 'import_pull_request_notes', message: 'starting'))
|
||||
|
||||
expect { importer.execute }.not_to change { Note.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has been marked as failed' do
|
||||
before do
|
||||
project.import_state.mark_as_failed('error')
|
||||
end
|
||||
|
||||
include_examples 'import is skipped'
|
||||
end
|
||||
|
||||
context 'when the import data does not have credentials' do
|
||||
before do
|
||||
project.import_data.credentials = nil
|
||||
project.import_data.save!
|
||||
end
|
||||
|
||||
include_examples 'import is skipped'
|
||||
end
|
||||
|
||||
context 'when the import data does not have data' do
|
||||
before do
|
||||
project.import_data.data = nil
|
||||
project.import_data.save!
|
||||
end
|
||||
|
||||
include_examples 'import is skipped'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
|
|||
|
||||
let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) }
|
||||
let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) }
|
||||
let_it_be(:task) { create(:issue, :task, author: user, project: project) }
|
||||
let_it_be(:objective) { create(:issue, :objective, author: user, project: project) }
|
||||
|
||||
let_it_be(:closed_issue) do
|
||||
create :closed_issue,
|
||||
|
|
|
|||
Loading…
Reference in New Issue