diff options
73 files changed, 1434 insertions, 336 deletions
diff --git a/app/assets/javascripts/api/harbor_registry.js b/app/assets/javascripts/api/harbor_registry.js index 1148c2c9657..eb241342567 100644 --- a/app/assets/javascripts/api/harbor_registry.js +++ b/app/assets/javascripts/api/harbor_registry.js @@ -35,11 +35,15 @@ export function getHarborArtifacts({ requestPath, repoName, limit, page, sort, s }); } -export function getHarborTags({ requestPath, repoName, digest }) { +export function getHarborTags({ requestPath, repoName, digest, page }) { const url = buildApiUrl(HARBOR_TAGS_PATH) .replace('/:request_path', requestPath) .replace(':repo_name', repoName) .replace(':digest', digest); - return axios.get(url); + return axios.get(url, { + params: { + page, + }, + }); } diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 9b45880a42e..961af800971 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -16,7 +16,7 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge. import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { __ } from '~/locale'; -import { VISIBILITY_LEVELS_ENUM } from '~/visibility_level/constants'; +import { VISIBILITY_LEVELS_STRING_TO_INTEGER } from '~/visibility_level/constants'; import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, ITEM_TYPE } from '../constants'; import eventHub from '../event_hub'; @@ -111,8 +111,8 @@ export default { shouldShowVisibilityWarning() { return ( this.action === 'shared' && - VISIBILITY_LEVELS_ENUM[this.group.visibility] > - VISIBILITY_LEVELS_ENUM[this.currentGroupVisibility] + VISIBILITY_LEVELS_STRING_TO_INTEGER[this.group.visibility] > + VISIBILITY_LEVELS_STRING_TO_INTEGER[this.currentGroupVisibility] ); }, }, diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index 0d09ad9442b..223c2975c11 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -1,8 +1,8 @@ import { __, s__ } from '~/locale'; import { - VISIBILITY_LEVEL_PRIVATE, - VISIBILITY_LEVEL_INTERNAL, - VISIBILITY_LEVEL_PUBLIC, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, } from '~/visibility_level/constants'; export const MAX_CHILDREN_COUNT = 20; @@ -34,29 +34,31 @@ export const ITEM_TYPE = { }; export const GROUP_VISIBILITY_TYPE = { - [VISIBILITY_LEVEL_PUBLIC]: __( + [VISIBILITY_LEVEL_PUBLIC_STRING]: __( 'Public - The group and any public projects can be viewed without any authentication.', ), - [VISIBILITY_LEVEL_INTERNAL]: __( + [VISIBILITY_LEVEL_INTERNAL_STRING]: __( 'Internal - The group and any internal projects can be viewed by any logged in user except external users.', ), - [VISIBILITY_LEVEL_PRIVATE]: __( + [VISIBILITY_LEVEL_PRIVATE_STRING]: __( 'Private - The group and its projects can only be viewed by members.', ), }; export const PROJECT_VISIBILITY_TYPE = { - [VISIBILITY_LEVEL_PUBLIC]: __('Public - The project can be accessed without any authentication.'), - [VISIBILITY_LEVEL_INTERNAL]: __( + [VISIBILITY_LEVEL_PUBLIC_STRING]: __( + 'Public - The project can be accessed without any authentication.', + ), + [VISIBILITY_LEVEL_INTERNAL_STRING]: __( 'Internal - The project can be accessed by any logged in user except external users.', ), - [VISIBILITY_LEVEL_PRIVATE]: __( + [VISIBILITY_LEVEL_PRIVATE_STRING]: __( 'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', ), }; export const VISIBILITY_TYPE_ICON = { - [VISIBILITY_LEVEL_PUBLIC]: 'earth', - [VISIBILITY_LEVEL_INTERNAL]: 'shield', - [VISIBILITY_LEVEL_PRIVATE]: 'lock', + [VISIBILITY_LEVEL_PUBLIC_STRING]: 'earth', + [VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield', + [VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock', }; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue index 4b459860346..b489f126f75 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue @@ -72,7 +72,7 @@ export default { linkTo() { const { project, image } = this.$route.params; - return { name: 'details', params: { project, image, digest: this.artifact.digest } }; + return { name: 'tags', params: { project, image, digest: this.artifact.digest } }; }, }, }; @@ -99,10 +99,10 @@ export default { </template> <template #left-secondary> - <span class="gl-mr-2" data-testid="size"> + <span class="gl-mr-3" data-testid="size"> {{ formattedSize }} </span> - <span id="tagsCount" data-testid="tags-count"> + <span id="tagsCount" class="gl-display-flex gl-align-items-center" data-testid="tags-count"> <gl-icon name="tag" class="gl-mr-2" /> {{ tagsCountText }} </span> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue index cc5398ccf8f..bfb097601d5 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue @@ -1,10 +1,13 @@ <script> import { isEmpty } from 'lodash'; -import { n__, s__ } from '~/locale'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; -import { ROOT_IMAGE_TEXT } from '~/packages_and_registries/harbor_registry/constants/index'; +import { + ROOT_IMAGE_TEXT, + EMPTY_ARTIFACTS_LABEL, + artifactsLabel, +} from '~/packages_and_registries/harbor_registry/constants/index'; export default { name: 'DetailsHeader', @@ -19,9 +22,9 @@ export default { computed: { artifactCountText() { if (isEmpty(this.imagesDetail)) { - return s__('HarborRegistry|-- artifacts'); + return EMPTY_ARTIFACTS_LABEL; } - return n__('%d artifact', '%d artifacts', this.imagesDetail.artifactCount); + return artifactsLabel(this.imagesDetail.artifactCount); }, repositoryFullName() { return this.imagesDetail.name || ROOT_IMAGE_TEXT; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue index 72aaa762547..ac1df5cf93f 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue @@ -1,8 +1,7 @@ <script> -// We are using gl-breadcrumb only at the last child of the handwritten breadcrumb -// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 -// -// See the CSS workaround in app/assets/stylesheets/pages/registry.scss when this file is changed. +// Since app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue +// can only handle two levels of breadcrumbs, but we have three levels here. +// So we extended the registry_breadcrumb.vue component with harbor_registry_breadcrumb.vue to support multiple levels of breadcrumbs import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; import { isArray, last } from 'lodash'; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue new file mode 100644 index 00000000000..e7f6989c49f --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue @@ -0,0 +1,54 @@ +<script> +import { isEmpty } from 'lodash'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { + EMPTY_TAG_LABEL, + tagsCountText, +} from '~/packages_and_registries/harbor_registry/constants'; + +export default { + name: 'TagsHeader', + components: { + TitleArea, + MetadataItem, + }, + mixins: [timeagoMixin], + props: { + artifactDetail: { + type: Object, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + tagsLoading: { + type: Boolean, + required: true, + }, + }, + computed: { + tagCountText() { + if (isEmpty(this.pageInfo)) { + return EMPTY_TAG_LABEL; + } + return tagsCountText(this.pageInfo.total); + }, + }, +}; +</script> + +<template> + <title-area :metadata-loading="tagsLoading"> + <template #title> + <span class="gl-word-break-all" data-testid="title"> + {{ artifactDetail.digest }} + </span> + </template> + <template #metadata-tags-count> + <metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" /> + </template> + </title-area> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue new file mode 100644 index 00000000000..b34d3a950c0 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue @@ -0,0 +1,82 @@ +<script> +import { GlEmptyState } from '@gitlab/ui'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; +import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue'; +import { + NO_ARTIFACTS_TITLE, + NO_TAGS_MATCHING_FILTERS_TITLE, + NO_TAGS_MATCHING_FILTERS_DESCRIPTION, +} from '~/packages_and_registries/harbor_registry/constants'; + +export default { + name: 'TagsList', + components: { + GlEmptyState, + TagsLoader, + TagsListRow, + RegistryList, + }, + inject: ['noContainersImage'], + props: { + tags: { + type: Array, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + hasNoTags() { + return this.tags.length === 0; + }, + emptyStateTitle() { + return this.filter ? NO_TAGS_MATCHING_FILTERS_TITLE : NO_ARTIFACTS_TITLE; + }, + emptyStateDescription() { + return this.filter ? NO_TAGS_MATCHING_FILTERS_DESCRIPTION : ''; + }, + }, + methods: { + fetchNextPage() { + this.$emit('next-page'); + }, + fetchPreviousPage() { + this.$emit('prev-page'); + }, + }, +}; +</script> + +<template> + <div> + <tags-loader v-if="isLoading" /> + <gl-empty-state + v-else-if="hasNoTags" + :title="emptyStateTitle" + :svg-path="noContainersImage" + :description="emptyStateDescription" + class="gl-mx-auto gl-my-0" + /> + <registry-list + v-else + :pagination="pageInfo" + :items="tags" + hidden-delete + id-property="name" + @prev-page="fetchPreviousPage" + @next-page="fetchNextPage" + > + <template #default="{ item }"> + <tags-list-row :tag="item" /> + </template> + </registry-list> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue new file mode 100644 index 00000000000..63e046c1abc --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue @@ -0,0 +1,74 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { CREATED_AT_LABEL } from '~/packages_and_registries/harbor_registry/constants'; +import { tagPullCommand } from '~/packages_and_registries/harbor_registry/utils'; + +export default { + name: 'TagsListRow', + components: { + GlSprintf, + ListItem, + ClipboardButton, + TimeAgoTooltip, + }, + inject: ['harborIntegrationProjectName', 'repositoryUrl'], + props: { + tag: { + type: Object, + required: true, + }, + }, + i18n: { + createdAtLabel: CREATED_AT_LABEL, + }, + methods: { + getPullCommand(tagName) { + if (tagName) { + const { image } = this.$route.params; + + return tagPullCommand({ + imageName: image, + tag: tagName, + repositoryUrl: this.repositoryUrl, + harborProjectName: this.harborIntegrationProjectName, + }); + } + + return ''; + }, + }, +}; +</script> + +<template> + <list-item v-bind="$attrs"> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center"> + <div + data-testid="name" + class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap" + > + {{ tag.name }} + </div> + <clipboard-button + :title="getPullCommand(tag.name)" + :text="getPullCommand(tag.name)" + category="tertiary" + /> + </div> + </template> + + <template #right-primary> + <span data-testid="time"> + <gl-sprintf :message="$options.i18n.createdAtLabel"> + <template #timeInfo> + <time-ago-tooltip :time="tag.pushTime" /> + </template> + </gl-sprintf> + </span> + </template> + </list-item> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js index e10a24315d8..5b4b85ec31e 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js @@ -1,4 +1,4 @@ -import { s__, __ } from '~/locale'; +import { s__, __, n__ } from '~/locale'; export const FETCH_ARTIFACT_LIST_ERROR_MESSAGE = s__( 'HarborRegistry|Something went wrong while fetching the artifact list.', @@ -25,3 +25,13 @@ export const FETCH_TAGS_ERROR_MESSAGE = s__( ); export const TAG_LABEL = s__('HarborRegistry|Tag'); +export const EMPTY_TAG_LABEL = s__('HarborRegistry|-- tags'); + +export const EMPTY_ARTIFACTS_LABEL = s__('HarborRegistry|-- artifacts'); +export const artifactsLabel = (count) => { + return n__('%d artifact', '%d artifacts', count); +}; + +export const tagsCountText = (count) => { + return n__('%d tag', '%d tags', count); +}; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue new file mode 100644 index 00000000000..1323d347d10 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue @@ -0,0 +1,103 @@ +<script> +import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue'; +import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue'; +import { getHarborTags } from '~/rest_api'; +import { FETCH_TAGS_ERROR_MESSAGE } from '~/packages_and_registries/harbor_registry/constants'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { createAlert } from '~/flash'; +import { formatPagination } from '~/packages_and_registries/harbor_registry/utils'; + +export default { + name: 'HarborTagsPage', + components: { + TagsHeader, + TagsList, + }, + inject: ['endpoint', 'breadCrumbState'], + data() { + return { + tagsLoading: false, + pageInfo: {}, + tags: [], + }; + }, + computed: { + currentPage() { + return this.pageInfo?.page || 1; + }, + artifactDetail() { + const { project, image, digest } = this.$route.params; + + return { + project, + image, + digest, + }; + }, + }, + mounted() { + this.updateBreadcrumb(); + this.fetchTagsData(); + }, + methods: { + updateBreadcrumb() { + const artifactPath = `${this.artifactDetail.project}/${this.artifactDetail.image}`; + const nameList = [artifactPath, this.artifactDetail.digest]; + const hrefList = [`/${artifactPath}`, this.$route.path]; + + this.breadCrumbState.updateName(nameList); + this.breadCrumbState.updateHref(hrefList); + }, + fetchPrevPage() { + const prevPageNum = this.currentPage - 1; + this.fetchTagsData(prevPageNum); + }, + fetchNextPage() { + const nextPageNum = this.currentPage + 1; + this.fetchTagsData(nextPageNum); + }, + fetchTagsData(requestPage) { + this.tagsLoading = true; + + const params = { + page: requestPage, + requestPath: this.endpoint, + repoName: this.artifactDetail.image, + digest: this.artifactDetail.digest, + }; + + getHarborTags(params) + .then((res) => { + this.pageInfo = formatPagination(res.headers); + + this.tags = (res?.data || []).map((tagInfo) => { + return convertObjectPropsToCamelCase(tagInfo); + }); + }) + .catch(() => { + createAlert({ message: FETCH_TAGS_ERROR_MESSAGE }); + }) + .finally(() => { + this.tagsLoading = false; + }); + }, + }, +}; +</script> + +<template> + <div> + <tags-header + :artifact-detail="artifactDetail" + :page-info="pageInfo" + :tags-loading="tagsLoading" + /> + <tags-list + :tags="tags" + :is-loading="tagsLoading" + :page-info="pageInfo" + @prev-page="fetchPrevPage" + @next-page="fetchNextPage" + /> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/router.js b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js index 8744204b5c7..5a792e30c62 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/router.js +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js @@ -3,6 +3,7 @@ import VueRouter from 'vue-router'; import { HARBOR_REGISTRY_TITLE } from './constants/index'; import List from './pages/list.vue'; import Details from './pages/details.vue'; +import HarborTags from './pages/harbor_tags.vue'; Vue.use(VueRouter); @@ -29,6 +30,15 @@ export default function createRouter(base, breadCrumbState) { hrefGenerator: () => breadCrumbState.href, }, }, + { + name: 'tags', + path: '/:project/:image/:digest', + component: HarborTags, + meta: { + nameGenerator: () => breadCrumbState.name, + hrefGenerator: () => breadCrumbState.href, + }, + }, ], }); diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue index 6951a8a1bc3..b415e36bf09 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue @@ -18,18 +18,14 @@ import csrf from '~/lib/utils/csrf'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import validation from '~/vue_shared/directives/validation'; +import { + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, + VISIBILITY_LEVELS_STRING_TO_INTEGER, +} from '~/visibility_level/constants'; import ProjectNamespace from './project_namespace.vue'; -const PRIVATE_VISIBILITY = 'private'; -const INTERNAL_VISIBILITY = 'internal'; -const PUBLIC_VISIBILITY = 'public'; - -const VISIBILITY_LEVEL = { - [PRIVATE_VISIBILITY]: 0, - [INTERNAL_VISIBILITY]: 10, - [PUBLIC_VISIBILITY]: 20, -}; - const initFormField = ({ value, required = true, skipValidation = false }) => ({ value, required, @@ -110,11 +106,12 @@ export default { }, computed: { projectVisibilityLevel() { - return VISIBILITY_LEVEL[this.projectVisibility]; + return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility]; }, namespaceVisibilityLevel() { - const visibility = this.form.fields.namespace.value?.visibility || PUBLIC_VISIBILITY; - return VISIBILITY_LEVEL[visibility]; + const visibility = + this.form.fields.namespace.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING; + return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility]; }, visibilityLevelCap() { return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel); @@ -123,7 +120,7 @@ export default { return new Set(this.restrictedVisibilityLevels); }, allowedVisibilityLevels() { - const allowedLevels = Object.entries(VISIBILITY_LEVEL).reduce( + const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce( (levels, [levelName, levelValue]) => { if ( !this.restrictedVisibilityLevelsSet.has(levelValue) && @@ -137,7 +134,7 @@ export default { ); if (!allowedLevels.length) { - return [PRIVATE_VISIBILITY]; + return [VISIBILITY_LEVEL_PRIVATE_STRING]; } return allowedLevels; @@ -146,26 +143,26 @@ export default { return [ { text: s__('ForkProject|Private'), - value: PRIVATE_VISIBILITY, + value: VISIBILITY_LEVEL_PRIVATE_STRING, icon: 'lock', help: s__( 'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', ), - disabled: this.isVisibilityLevelDisabled(PRIVATE_VISIBILITY), + disabled: this.isVisibilityLevelDisabled(VISIBILITY_LEVEL_PRIVATE_STRING), }, { text: s__('ForkProject|Internal'), - value: INTERNAL_VISIBILITY, + value: VISIBILITY_LEVEL_INTERNAL_STRING, icon: 'shield', help: s__('ForkProject|The project can be accessed by any logged in user.'), - disabled: this.isVisibilityLevelDisabled(INTERNAL_VISIBILITY), + disabled: this.isVisibilityLevelDisabled(VISIBILITY_LEVEL_INTERNAL_STRING), }, { text: s__('ForkProject|Public'), - value: PUBLIC_VISIBILITY, + value: VISIBILITY_LEVEL_PUBLIC_STRING, icon: 'earth', help: s__('ForkProject|The project can be accessed without any authentication.'), - disabled: this.isVisibilityLevelDisabled(PUBLIC_VISIBILITY), + disabled: this.isVisibilityLevelDisabled(VISIBILITY_LEVEL_PUBLIC_STRING), }, ]; }, @@ -185,7 +182,7 @@ export default { }, setNamespace(namespace) { this.form.fields.visibility.value = - this.restrictedVisibilityLevels.length !== 0 ? null : PRIVATE_VISIBILITY; + this.restrictedVisibilityLevels.length !== 0 ? null : VISIBILITY_LEVEL_PRIVATE_STRING; this.form.fields.namespace.value = namespace; this.form.fields.namespace.state = true; }, diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index da6fb775258..a82f485bf44 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -5,7 +5,11 @@ import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/s import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __, s__ } from '~/locale'; import { - visibilityOptions, + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, +} from '~/visibility_level/constants'; +import { visibilityLevelDescriptions, featureAccessLevelMembers, featureAccessLevelEveryone, @@ -37,6 +41,7 @@ export default { featureFlagsHelpText: s__( 'ProjectSettings|Roll out new features without redeploying with feature flags.', ), + monitorLabel: s__('ProjectSettings|Monitor'), packagesHelpText: s__( 'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.', ), @@ -62,6 +67,9 @@ export default { ), confirmButtonText: __('Save changes'), }, + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, components: { ProjectFeatureSetting, @@ -104,9 +112,9 @@ export default { type: Array, required: false, default: () => [ - visibilityOptions.PRIVATE, - visibilityOptions.INTERNAL, - visibilityOptions.PUBLIC, + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, ], }, lfsAvailable: { @@ -220,8 +228,7 @@ export default { }, data() { const defaults = { - visibilityOptions, - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, issuesAccessLevel: featureAccessLevel.EVERYONE, repositoryAccessLevel: featureAccessLevel.EVERYONE, forkingAccessLevel: featureAccessLevel.EVERYONE, @@ -239,6 +246,7 @@ export default { environmentsAccessLevel: featureAccessLevel.EVERYONE, featureFlagsAccessLevel: featureAccessLevel.PROJECT_MEMBERS, releasesAccessLevel: featureAccessLevel.EVERYONE, + monitorAccessLevel: featureAccessLevel.EVERYONE, containerRegistryAccessLevel: featureAccessLevel.EVERYONE, warnAboutPotentiallyUnwantedCharacters: true, lfsEnabled: true, @@ -259,7 +267,7 @@ export default { computed: { featureAccessLevelOptions() { const options = [featureAccessLevelMembers]; - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + if (this.visibilityLevel !== VISIBILITY_LEVEL_PRIVATE_INTEGER) { options.push(featureAccessLevelEveryone); } return options; @@ -271,18 +279,12 @@ export default { ); }, - operationsFeatureAccessLevelOptions() { - return this.featureAccessLevelOptions.filter( - ([value]) => value <= this.operationsAccessLevel, - ); - }, - packageRegistryFeatureAccessLevelOptions() { const options = [FEATURE_ACCESS_LEVEL_ANONYMOUS]; - if (this.visibilityLevel === visibilityOptions.PRIVATE) { + if (this.visibilityLevel === VISIBILITY_LEVEL_PRIVATE_INTEGER) { options.unshift(featureAccessLevelMembers); - } else if (this.visibilityLevel === visibilityOptions.INTERNAL) { + } else if (this.visibilityLevel === VISIBILITY_LEVEL_INTERNAL_INTEGER) { options.unshift(featureAccessLevelEveryone); } @@ -293,15 +295,15 @@ export default { const options = [featureAccessLevelMembers]; if (this.pagesAccessControlForced) { - if (this.visibilityLevel === visibilityOptions.INTERNAL) { + if (this.visibilityLevel === VISIBILITY_LEVEL_INTERNAL_INTEGER) { options.push(featureAccessLevelEveryone); } } else { - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + if (this.visibilityLevel !== VISIBILITY_LEVEL_PRIVATE_INTEGER) { options.push(featureAccessLevelEveryone); } - if (this.visibilityLevel !== visibilityOptions.PUBLIC) { + if (this.visibilityLevel !== VISIBILITY_LEVEL_PUBLIC_INTEGER) { options.push(FEATURE_ACCESS_LEVEL_ANONYMOUS); } } @@ -316,6 +318,10 @@ export default { return this.environmentsAccessLevel > featureAccessLevel.NOT_ENABLED; }, + monitorEnabled() { + return this.monitorAccessLevel > featureAccessLevel.NOT_ENABLED; + }, + repositoryEnabled() { return this.repositoryAccessLevel > featureAccessLevel.NOT_ENABLED; }, @@ -326,13 +332,13 @@ export default { showContainerRegistryPublicNote() { return ( - this.visibilityLevel === visibilityOptions.PUBLIC && + this.visibilityLevel === VISIBILITY_LEVEL_PUBLIC_INTEGER && this.containerRegistryAccessLevel === featureAccessLevel.EVERYONE ); }, repositoryHelpText() { - if (this.visibilityLevel === visibilityOptions.PRIVATE) { + if (this.visibilityLevel === VISIBILITY_LEVEL_PRIVATE_INTEGER) { return s__('ProjectSettings|View and edit files in this project.'); } @@ -341,7 +347,7 @@ export default { ); }, cveIdRequestIsDisabled() { - return this.visibilityLevel !== visibilityOptions.PUBLIC; + return this.visibilityLevel !== VISIBILITY_LEVEL_PUBLIC_INTEGER; }, isVisibilityReduced() { return ( @@ -355,11 +361,19 @@ export default { splitOperationsEnabled() { return this.glFeatures.splitOperationsVisibilityPermissions; }, + monitorOperationsFeatureAccessLevelOptions() { + if (this.splitOperationsEnabled) { + return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel); + } + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.operationsAccessLevel, + ); + }, }, watch: { visibilityLevel(value, oldValue) { - if (value === visibilityOptions.PRIVATE) { + if (value === VISIBILITY_LEVEL_PRIVATE_INTEGER) { // when private, features are restricted to "only team members" this.issuesAccessLevel = Math.min( featureAccessLevel.PROJECT_MEMBERS, @@ -381,7 +395,7 @@ export default { if ( this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE || (this.packageRegistryAccessLevel > featureAccessLevel.EVERYONE && - oldValue === visibilityOptions.PUBLIC) + oldValue === VISIBILITY_LEVEL_PUBLIC_INTEGER) ) { this.packageRegistryAccessLevel = featureAccessLevel.PROJECT_MEMBERS; } @@ -423,6 +437,10 @@ export default { featureAccessLevel.PROJECT_MEMBERS, this.releasesAccessLevel, ); + this.monitorAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.monitorAccessLevel, + ); this.containerRegistryAccessLevel = Math.min( featureAccessLevel.PROJECT_MEMBERS, this.containerRegistryAccessLevel, @@ -432,7 +450,7 @@ export default { this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS; } this.highlightChanges(); - } else if (oldValue === visibilityOptions.PRIVATE) { + } else if (oldValue === VISIBILITY_LEVEL_PRIVATE_INTEGER) { // if changing away from private, make enabled features more permissive if (this.issuesAccessLevel > featureAccessLevel.NOT_ENABLED) this.issuesAccessLevel = featureAccessLevel.EVERYONE; @@ -466,19 +484,21 @@ export default { this.operationsAccessLevel = featureAccessLevel.EVERYONE; if (this.environmentsAccessLevel === featureAccessLevel.PROJECT_MEMBERS) this.environmentsAccessLevel = featureAccessLevel.EVERYONE; + if (this.monitorAccessLevel === featureAccessLevel.PROJECT_MEMBERS) + this.monitorAccessLevel = featureAccessLevel.EVERYONE; if (this.containerRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS) this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE; this.highlightChanges(); } else if (this.packageRegistryAccessLevelEnabled) { if ( - value === visibilityOptions.PUBLIC && + value === VISIBILITY_LEVEL_PUBLIC_INTEGER && this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE ) { // eslint-disable-next-line prefer-destructuring this.packageRegistryAccessLevel = FEATURE_ACCESS_LEVEL_ANONYMOUS[0]; } else if ( - value === visibilityOptions.INTERNAL && + value === VISIBILITY_LEVEL_INTERNAL_INTEGER && this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0] ) { this.packageRegistryAccessLevel = featureAccessLevel.EVERYONE; @@ -501,6 +521,16 @@ export default { }, operationsAccessLevel(value, oldValue) { + this.updateSubFeatureAccessLevel(value, oldValue); + }, + + monitorAccessLevel(value, oldValue) { + this.updateSubFeatureAccessLevel(value, oldValue); + }, + }, + + methods: { + updateSubFeatureAccessLevel(value, oldValue) { if (value < oldValue) { // sub-features cannot have more permissive access level this.metricsDashboardAccessLevel = Math.min(this.metricsDashboardAccessLevel, value); @@ -508,9 +538,7 @@ export default { this.metricsDashboardAccessLevel = value; } }, - }, - methods: { highlightChanges() { this.highlightChangesClass = true; this.$nextTick(() => { @@ -548,20 +576,20 @@ export default { data-qa-selector="project_visibility_dropdown" > <option - :value="visibilityOptions.PRIVATE" - :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" + :value="$options.VISIBILITY_LEVEL_PRIVATE_INTEGER" + :disabled="!visibilityAllowed($options.VISIBILITY_LEVEL_PRIVATE_INTEGER)" > {{ s__('ProjectSettings|Private') }} </option> <option - :value="visibilityOptions.INTERNAL" - :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" + :value="$options.VISIBILITY_LEVEL_INTERNAL_INTEGER" + :disabled="!visibilityAllowed($options.VISIBILITY_LEVEL_INTERNAL_INTEGER)" > {{ s__('ProjectSettings|Internal') }} </option> <option - :value="visibilityOptions.PUBLIC" - :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" + :value="$options.VISIBILITY_LEVEL_PUBLIC_INTEGER" + :disabled="!visibilityAllowed($options.VISIBILITY_LEVEL_PUBLIC_INTEGER)" > {{ s__('ProjectSettings|Public') }} </option> @@ -592,7 +620,7 @@ export default { <div class="gl-mt-4"> <strong class="gl-display-block">{{ s__('ProjectSettings|Additional options') }}</strong> <label - v-if="visibilityLevel !== visibilityOptions.PRIVATE" + v-if="visibilityLevel !== $options.VISIBILITY_LEVEL_PRIVATE_INTEGER" class="gl-line-height-28 gl-font-weight-normal gl-mb-0" > <input @@ -604,7 +632,7 @@ export default { {{ s__('ProjectSettings|Users can request access') }} </label> <label - v-if="visibilityLevel !== visibilityOptions.PUBLIC" + v-if="visibilityLevel !== $options.VISIBILITY_LEVEL_PUBLIC_INTEGER" class="gl-line-height-28 gl-font-weight-normal gl-display-block gl-mb-0" > <input @@ -881,6 +909,22 @@ export default { /> </project-setting-row> <project-setting-row + v-if="splitOperationsEnabled" + ref="monitor-settings" + :label="$options.i18n.monitorLabel" + :help-text=" + s__('ProjectSettings|Configure your project resources and monitor their health.') + " + > + <project-feature-setting + v-model="monitorAccessLevel" + :label="$options.i18n.monitorLabel" + :options="featureAccessLevelOptions" + name="project[project_feature_attributes][monitor_access_level]" + /> + </project-setting-row> + <project-setting-row + v-else ref="operations-settings" :label="$options.i18n.operationsLabel" :help-text=" @@ -903,7 +947,7 @@ export default { <project-feature-setting v-model="metricsDashboardAccessLevel" :show-toggle="false" - :options="operationsFeatureAccessLevelOptions" + :options="monitorOperationsFeatureAccessLevelOptions" name="project[project_feature_attributes][metrics_dashboard_access_level]" /> </project-setting-row> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index cfca9d400e3..4c687859344 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js @@ -1,17 +1,16 @@ import { s__, __ } from '~/locale'; - -export const visibilityOptions = { - PRIVATE: 0, - INTERNAL: 10, - PUBLIC: 20, -}; +import { + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, +} from '~/visibility_level/constants'; export const visibilityLevelDescriptions = { - [visibilityOptions.PRIVATE]: __( + [VISIBILITY_LEVEL_PRIVATE_INTEGER]: __( `Only accessible by %{membersPageLinkStart}project members%{membersPageLinkEnd}. Membership must be explicitly granted to each user.`, ), - [visibilityOptions.INTERNAL]: __('Accessible by any user who is logged in.'), - [visibilityOptions.PUBLIC]: __('Accessible by anyone, regardless of authentication.'), + [VISIBILITY_LEVEL_INTERNAL_INTEGER]: __('Accessible by any user who is logged in.'), + [VISIBILITY_LEVEL_PUBLIC_INTEGER]: __('Accessible by anyone, regardless of authentication.'), }; export const featureAccessLevel = { diff --git a/app/assets/javascripts/projects/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js index d1343f07f1d..84b8936c17f 100644 --- a/app/assets/javascripts/projects/project_visibility.js +++ b/app/assets/javascripts/projects/project_visibility.js @@ -1,13 +1,7 @@ import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; import eventHub from '~/projects/new/event_hub'; - -// Values are from lib/gitlab/visibility_level.rb -const visibilityLevel = { - private: 0, - internal: 10, - public: 20, -}; +import { VISIBILITY_LEVELS_STRING_TO_INTEGER } from '~/visibility_level/constants'; function setVisibilityOptions({ name, visibility, showPath, editPath }) { document.querySelectorAll('.visibility-level-setting .gl-form-radio').forEach((option) => { @@ -19,7 +13,7 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) { const optionInput = option.querySelector('input[type=radio]'); const optionValue = optionInput ? parseInt(optionInput.value, 10) : 0; - if (visibilityLevel[visibility] < optionValue) { + if (VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility] < optionValue) { option.classList.add('disabled'); optionInput.disabled = true; const reason = option.querySelector('.option-disabled-reason'); diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index ee8b00c1f5d..853293e5eb6 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -6,7 +6,7 @@ import { SNIPPET_MEASURE_BLOBS_CONTENT, } from '~/performance/constants'; import { performanceMarkAndMeasure } from '~/performance/utils'; -import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; +import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants'; import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import { getSnippetMixin } from '../mixins/snippets'; @@ -31,7 +31,7 @@ export default { mixins: [getSnippetMixin], computed: { embeddable() { - return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC; + return this.snippet.visibilityLevel === VISIBILITY_LEVEL_PUBLIC_STRING; }, canBeCloned() { return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo); diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js index 2a9ecbc27dc..84a940ed1f8 100644 --- a/app/assets/javascripts/snippets/constants.js +++ b/app/assets/javascripts/snippets/constants.js @@ -1,22 +1,23 @@ import { __ } from '~/locale'; - -export const SNIPPET_VISIBILITY_PRIVATE = 'private'; -export const SNIPPET_VISIBILITY_INTERNAL = 'internal'; -export const SNIPPET_VISIBILITY_PUBLIC = 'public'; +import { + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; export const SNIPPET_VISIBILITY = { - [SNIPPET_VISIBILITY_PRIVATE]: { + [VISIBILITY_LEVEL_PRIVATE_STRING]: { label: __('Private'), icon: 'lock', description: __('The snippet is visible only to me.'), description_project: __('The snippet is visible only to project members.'), }, - [SNIPPET_VISIBILITY_INTERNAL]: { + [VISIBILITY_LEVEL_INTERNAL_STRING]: { label: __('Internal'), icon: 'shield', description: __('The snippet is visible to any logged in user except external users.'), }, - [SNIPPET_VISIBILITY_PUBLIC]: { + [VISIBILITY_LEVEL_PUBLIC_STRING]: { label: __('Public'), icon: 'earth', description: __('The snippet can be accessed without any authentication.'), @@ -34,11 +35,6 @@ export const SNIPPET_BLOB_ACTION_DELETE = 'delete'; export const SNIPPET_MAX_BLOBS = 10; -export const SNIPPET_LEVELS_MAP = { - 0: SNIPPET_VISIBILITY_PRIVATE, - 10: SNIPPET_VISIBILITY_INTERNAL, - 20: SNIPPET_VISIBILITY_PUBLIC, -}; export const SNIPPET_LEVELS_RESTRICTED = __( 'Other visibility settings have been disabled by the administrator.', ); diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js index 21f38c4d8c9..89dd5e586fb 100644 --- a/app/assets/javascripts/snippets/index.js +++ b/app/assets/javascripts/snippets/index.js @@ -2,7 +2,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants'; +import { + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVELS_INTEGER_TO_STRING, +} from '~/visibility_level/constants'; import Translate from '~/vue_shared/translate'; Vue.use(VueApollo); @@ -36,7 +39,8 @@ export default function appFactory(el, Component) { apolloProvider, provide: { visibilityLevels: JSON.parse(visibilityLevels), - selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE, + selectedLevel: + VISIBILITY_LEVELS_INTEGER_TO_STRING[selectedLevel] ?? VISIBILITY_LEVEL_PRIVATE_STRING, multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset, reportAbusePath, canReportSpam, diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js index 2a3f590a803..a228d6111ce 100644 --- a/app/assets/javascripts/snippets/utils/blob.js +++ b/app/assets/javascripts/snippets/utils/blob.js @@ -1,12 +1,12 @@ import { uniqueId } from 'lodash'; import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants'; import { performanceMarkAndMeasure } from '~/performance/utils'; +import { VISIBILITY_LEVELS_INTEGER_TO_STRING } from '~/visibility_level/constants'; import { SNIPPET_BLOB_ACTION_CREATE, SNIPPET_BLOB_ACTION_UPDATE, SNIPPET_BLOB_ACTION_MOVE, SNIPPET_BLOB_ACTION_DELETE, - SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY, } from '../constants'; @@ -72,7 +72,7 @@ export const diffAll = (blobs, origBlobs) => { export const defaultSnippetVisibilityLevels = (arr) => { if (Array.isArray(arr)) { return arr.map((l) => { - const translatedLevel = SNIPPET_LEVELS_MAP[l]; + const translatedLevel = VISIBILITY_LEVELS_INTEGER_TO_STRING[l]; return { value: translatedLevel, ...SNIPPET_VISIBILITY[translatedLevel], diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js index 65f0eceae55..77736fb6ef5 100644 --- a/app/assets/javascripts/visibility_level/constants.js +++ b/app/assets/javascripts/visibility_level/constants.js @@ -1,10 +1,20 @@ -export const VISIBILITY_LEVEL_PRIVATE = 'private'; -export const VISIBILITY_LEVEL_INTERNAL = 'internal'; -export const VISIBILITY_LEVEL_PUBLIC = 'public'; +export const VISIBILITY_LEVEL_PRIVATE_STRING = 'private'; +export const VISIBILITY_LEVEL_INTERNAL_STRING = 'internal'; +export const VISIBILITY_LEVEL_PUBLIC_STRING = 'public'; + +export const VISIBILITY_LEVEL_PRIVATE_INTEGER = 0; +export const VISIBILITY_LEVEL_INTERNAL_INTEGER = 10; +export const VISIBILITY_LEVEL_PUBLIC_INTEGER = 20; // Matches `lib/gitlab/visibility_level.rb` -export const VISIBILITY_LEVELS_ENUM = { - [VISIBILITY_LEVEL_PRIVATE]: 0, - [VISIBILITY_LEVEL_INTERNAL]: 10, - [VISIBILITY_LEVEL_PUBLIC]: 20, +export const VISIBILITY_LEVELS_STRING_TO_INTEGER = { + [VISIBILITY_LEVEL_PRIVATE_STRING]: VISIBILITY_LEVEL_PRIVATE_INTEGER, + [VISIBILITY_LEVEL_INTERNAL_STRING]: VISIBILITY_LEVEL_INTERNAL_INTEGER, + [VISIBILITY_LEVEL_PUBLIC_STRING]: VISIBILITY_LEVEL_PUBLIC_INTEGER, +}; + +export const VISIBILITY_LEVELS_INTEGER_TO_STRING = { + [VISIBILITY_LEVEL_PRIVATE_INTEGER]: VISIBILITY_LEVEL_PRIVATE_STRING, + [VISIBILITY_LEVEL_INTERNAL_INTEGER]: VISIBILITY_LEVEL_INTERNAL_STRING, + [VISIBILITY_LEVEL_PUBLIC_INTEGER]: VISIBILITY_LEVEL_PUBLIC_STRING, }; diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 84f5632854b..7211eebdb4b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -78,7 +78,11 @@ class JwtController < ApplicationController end def additional_params - { scopes: scopes_param, deploy_token: @authentication_result.deploy_token }.compact + { + scopes: scopes_param, + deploy_token: @authentication_result.deploy_token, + auth_type: @authentication_result.type + }.compact end # We have to parse scope here, because Docker Client does not send an array of scopes, diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 90be0c3adaf..5ceedbc1e01 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -442,6 +442,7 @@ class ProjectsController < Projects::ApplicationController if Feature.enabled?(:split_operations_visibility_permissions, project) %i[ environments_access_level feature_flags_access_level releases_access_level + monitor_access_level ] else %i[operations_access_level] diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index dd637ec352e..e760fad7be9 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -638,6 +638,7 @@ module ProjectsHelper emailsDisabled: project.emails_disabled?, metricsDashboardAccessLevel: feature.metrics_dashboard_access_level, operationsAccessLevel: feature.operations_access_level, + monitorAccessLevel: feature.monitor_access_level, showDefaultAwardEmojis: project.show_default_award_emojis?, warnAboutPotentiallyUnwantedCharacters: project.warn_about_potentially_unwanted_characters?, enforceAuthChecksOnUploads: project.enforce_auth_checks_on_uploads?, diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 7613691bc2e..2976b6f02a7 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -86,6 +86,10 @@ module ProjectFeaturesCompatibility write_feature_attribute_string(:operations_access_level, value) end + def monitor_access_level=(value) + write_feature_attribute_string(:monitor_access_level, value) + end + def security_and_compliance_access_level=(value) write_feature_attribute_string(:security_and_compliance_access_level, value) end diff --git a/app/models/packages/rpm/metadatum.rb b/app/models/packages/rpm/metadatum.rb index 520f86ad850..07361995a12 100644 --- a/app/models/packages/rpm/metadatum.rb +++ b/app/models/packages/rpm/metadatum.rb @@ -9,6 +9,10 @@ module Packages validates :package, presence: true + validates :epoch, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :release, presence: true, length: { maximum: 128 } diff --git a/app/models/project.rb b/app/models/project.rb index 5bf38c66eab..4d11d158880 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -450,7 +450,7 @@ class Project < ApplicationRecord :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, - :releases_access_level, + :monitor_access_level, :releases_access_level, to: :project_feature, allow_nil: true delegate :show_default_award_emojis, :show_default_award_emojis=, diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 8623e477c06..dad8aaf0625 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -17,6 +17,7 @@ class ProjectFeature < ApplicationRecord pages metrics_dashboard analytics + monitor operations security_and_compliance container_registry diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index ccdad8e6be1..fb162d03955 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -208,6 +208,7 @@ class ProjectPolicy < BasePolicy metrics_dashboard analytics operations + monitor security_and_compliance environments feature_flags @@ -402,6 +403,12 @@ class ProjectPolicy < BasePolicy prevent(*create_read_update_admin_destroy(:release)) end + rule { split_operations_visibility_permissions & monitor_disabled }.policy do + prevent(:metrics_dashboard) + prevent(*create_read_update_admin_destroy(:sentry_issue)) + prevent(*create_read_update_admin_destroy(:alert_management_alert)) + end + rule { can?(:metrics_dashboard) }.policy do enable :read_prometheus enable :read_deployment diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index e806bef46fe..509c2d4d544 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -83,6 +83,7 @@ module Auth token.audience = params[:service] token.subject = current_user.try(:username) token.expire_time = self.class.token_expire_at + token[:auth_type] = params[:auth_type] token[:access] = accesses.compact end end diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb index c43f0d8cb4f..b1efa881180 100644 --- a/app/services/bulk_imports/relation_export_service.rb +++ b/app/services/bulk_imports/relation_export_service.rb @@ -65,7 +65,7 @@ module BulkImports def export_service @export_service ||= if config.tree_relation?(relation) || config.self_relation?(relation) - TreeExportService.new(portable, config.export_path, relation) + TreeExportService.new(portable, config.export_path, relation, user) elsif config.file_relation?(relation) FileExportService.new(portable, config.export_path, relation) else diff --git a/app/services/bulk_imports/tree_export_service.rb b/app/services/bulk_imports/tree_export_service.rb index 8e885e590d1..b6f094da558 100644 --- a/app/services/bulk_imports/tree_export_service.rb +++ b/app/services/bulk_imports/tree_export_service.rb @@ -2,11 +2,12 @@ module BulkImports class TreeExportService - def initialize(portable, export_path, relation) + def initialize(portable, export_path, relation, user) @portable = portable @export_path = export_path @relation = relation @config = FileTransfer.config_for(portable) + @user = user end def execute @@ -27,7 +28,7 @@ module BulkImports private - attr_reader :export_path, :portable, :relation, :config + attr_reader :export_path, :portable, :relation, :config, :user # rubocop: disable CodeReuse/Serializer def serializer @@ -35,7 +36,8 @@ module BulkImports portable, config.portable_tree, json_writer, - exportable_path: '' + exportable_path: '', + current_user: user ) end # rubocop: enable CodeReuse/Serializer diff --git a/db/migrate/20220912180807_add_epoch_column_to_rpm_metadata.rb b/db/migrate/20220912180807_add_epoch_column_to_rpm_metadata.rb new file mode 100644 index 00000000000..842d917c8a3 --- /dev/null +++ b/db/migrate/20220912180807_add_epoch_column_to_rpm_metadata.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddEpochColumnToRpmMetadata < Gitlab::Database::Migration[2.0] + def change + add_column :packages_rpm_metadata, :epoch, :integer, null: false, default: 0 + end +end diff --git a/db/schema_migrations/20220912180807 b/db/schema_migrations/20220912180807 new file mode 100644 index 00000000000..77fb556b1fd --- /dev/null +++ b/db/schema_migrations/20220912180807 @@ -0,0 +1 @@ +6959c82221a22ac1a2aba39a1a023f227989ac26b08fc0aa5a0596f597e0098c
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 8c1c41cd2ff..3274c47b9c9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18927,6 +18927,7 @@ CREATE TABLE packages_rpm_metadata ( arch text DEFAULT ''::text NOT NULL, license text, url text, + epoch integer DEFAULT 0 NOT NULL, CONSTRAINT check_3798bae3d6 CHECK ((char_length(arch) <= 255)), CONSTRAINT check_5d29ba59ac CHECK ((char_length(description) <= 5000)), CONSTRAINT check_6e8cbd536d CHECK ((char_length(url) <= 1000)), diff --git a/doc/ci/environments/deployment_approvals.md b/doc/ci/environments/deployment_approvals.md index 78ce7f81ddc..26461d9827f 100644 --- a/doc/ci/environments/deployment_approvals.md +++ b/doc/ci/environments/deployment_approvals.md @@ -65,9 +65,6 @@ co-exist and multiple approval rules takes the precedence over the unified appro #### Unified approval setting -NOTE: -At this time, it is not possible to require approvals for an existing protected environment. The workaround is to unprotect the environment and configure approvals when re-protecting the environment. - There are two ways to configure approvals for a protected environment: 1. Using the [UI](protected_environments.md#protecting-environments) diff --git a/doc/development/import_export.md b/doc/development/import_export.md index 6cbbb6bf716..c66ac0418ac 100644 --- a/doc/development/import_export.md +++ b/doc/development/import_export.md @@ -305,6 +305,29 @@ export_reorders: nulls_position: :nulls_last ``` +### Conditional export + +When associated resources are from outside the project, you might need to +validate that a user who is exporting the project or group can access these +associations. `include_if_exportable` accepts an array of associations for a +resource. During export, the `exportable_association?` method on the resource +is called with the association's name and user to validate if associated +resource can be included in the export. + +For example: + +```yaml +include_if_exportable: + project: + issues: + - epic_issue +``` + +This definition: + +1. Calls the issue's `exportable_association?(:epic_issue, current_user: current_user)` method. +1. If the method returns true, includes the issue's `epic_issue` association for the issue. + ### Import The import job status moves from `none` to `finished` or `failed` into different states: diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index 4abc3da1190..8843b4f5755 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -12,6 +12,7 @@ module Gitlab @methods = config[:methods] || {} @preloads = config[:preloads] || {} @export_reorders = config[:export_reorders] || {} + @include_if_exportable = config[:include_if_exportable] || {} end def find_root(model_key) @@ -35,7 +36,8 @@ module Gitlab methods: @methods[model_key], include: resolve_model_tree(model_tree), preload: resolve_preloads(model_key, model_tree), - export_reorder: @export_reorders[model_key] + export_reorder: @export_reorders[model_key], + include_if_exportable: @include_if_exportable[model_key] }.compact end diff --git a/lib/gitlab/import_export/group/tree_saver.rb b/lib/gitlab/import_export/group/tree_saver.rb index 796b9258e57..b4c86c3fc7f 100644 --- a/lib/gitlab/import_export/group/tree_saver.rb +++ b/lib/gitlab/import_export/group/tree_saver.rb @@ -46,7 +46,8 @@ module Gitlab group, group_tree, json_writer, - exportable_path: "groups/#{group.id}" + exportable_path: "groups/#{group.id}", + current_user: @current_user ).execute end diff --git a/lib/gitlab/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb index 42e014e91f8..99396d64779 100644 --- a/lib/gitlab/import_export/json/streaming_serializer.rb +++ b/lib/gitlab/import_export/json/streaming_serializer.rb @@ -14,8 +14,9 @@ module Gitlab end end - def initialize(exportable, relations_schema, json_writer, exportable_path:, logger: Gitlab::Export::Logger) + def initialize(exportable, relations_schema, json_writer, current_user:, exportable_path:, logger: Gitlab::Export::Logger) @exportable = exportable + @current_user = current_user @exportable_path = exportable_path @relations_schema = relations_schema @json_writer = json_writer @@ -59,7 +60,7 @@ module Gitlab private - attr_reader :json_writer, :relations_schema, :exportable, :logger + attr_reader :json_writer, :relations_schema, :exportable, :logger, :current_user def serialize_many_relations(key, records, options) log_relation_export(key, records.size) @@ -73,7 +74,7 @@ module Gitlab batch.each do |record| before_read_callback(record) - items << Raw.new(record.to_json(options)) + items << exportable_json_record(record, options, key) after_read_callback(record) end @@ -83,6 +84,27 @@ module Gitlab json_writer.write_relation_array(@exportable_path, key, enumerator) end + def exportable_json_record(record, options, key) + associations = relations_schema[:include_if_exportable]&.dig(key) + return Raw.new(record.to_json(options)) unless associations && options[:include] + + filtered_options = options.deep_dup + associations.each do |association| + filtered_options[:include].delete_if do |option| + !exportable_json_association?(option, record, association.to_sym) + end + end + + Raw.new(record.to_json(filtered_options)) + end + + def exportable_json_association?(option, record, association) + return true unless option.has_key?(association) + return false unless record.respond_to?(:exportable_association?) + + record.exportable_association?(association, current_user: current_user) + end + def batch(relation, key) opts = { of: BATCH_SIZE } order_by = reorders(relation, key) @@ -111,7 +133,7 @@ module Gitlab enumerator = Enumerator.new do |items| records.each do |record| - items << Raw.new(record.to_json(options)) + items << exportable_json_record(record, options, key) end end @@ -121,7 +143,7 @@ module Gitlab def serialize_single_relation(key, record, options) log_relation_export(key) - json = Raw.new(record.to_json(options)) + json = exportable_json_record(record, options, key) json_writer.write_relation(@exportable_path, key, json) end diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 77311e14803..270a5c5c258 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -293,6 +293,7 @@ included_attributes: - :forking_access_level - :metrics_dashboard_access_level - :operations_access_level + - :monitor_access_level - :analytics_access_level - :security_and_compliance_access_level - :container_registry_access_level @@ -705,6 +706,7 @@ included_attributes: - :metrics_dashboard_access_level - :analytics_access_level - :operations_access_level + - :monitor_access_level - :security_and_compliance_access_level - :container_registry_access_level - :package_registry_access_level @@ -1155,3 +1157,28 @@ ee: - :user_id - :action - :created_at + + preloads: + issues: + epic: + + # When associated resources are from outside the project, you might need to + # validate that a user who is exporting the project or group can access these + # associations. `include_if_exportable` accepts an array of associations for a + # resource. During export, the `exportable_association?` method on the + # resource is called with the association's name and user to validate if + # associated resource can be included in the export. + # + # This definition will call issue's `exportable_association?(:epic_issue, + # current_user: current_user)` method and include issue's epic_issue association + # for each issue only if the method returns true: + # + # Example: + # include_if_exportable: + # project: + # issues: + # - epic_issue + include_if_exportable: + project: + issues: + - :epic_issue diff --git a/lib/gitlab/import_export/project/relation_saver.rb b/lib/gitlab/import_export/project/relation_saver.rb index b40827e36f8..8e91adac196 100644 --- a/lib/gitlab/import_export/project/relation_saver.rb +++ b/lib/gitlab/import_export/project/relation_saver.rb @@ -32,7 +32,8 @@ module Gitlab project, reader.project_tree, json_writer, - exportable_path: 'project' + exportable_path: 'project', + current_user: nil ) end diff --git a/lib/gitlab/import_export/project/tree_saver.rb b/lib/gitlab/import_export/project/tree_saver.rb index 1b54e4b975e..bd34cd3ff6e 100644 --- a/lib/gitlab/import_export/project/tree_saver.rb +++ b/lib/gitlab/import_export/project/tree_saver.rb @@ -50,7 +50,8 @@ module Gitlab reader.project_tree, json_writer, exportable_path: "project", - logger: @logger + logger: @logger, + current_user: @current_user ) Retriable.retriable(on: Net::OpenTimeout, on_retry: on_retry) do diff --git a/lib/sidebars/projects/menus/monitor_menu.rb b/lib/sidebars/projects/menus/monitor_menu.rb index 23e1a95c401..ecd062f333e 100644 --- a/lib/sidebars/projects/menus/monitor_menu.rb +++ b/lib/sidebars/projects/menus/monitor_menu.rb @@ -6,7 +6,7 @@ module Sidebars class MonitorMenu < ::Sidebars::Menu override :configure_menu_items def configure_menu_items - return false unless context.project.feature_available?(:operations, context.current_user) + return false unless feature_enabled? add_item(metrics_dashboard_menu_item) add_item(error_tracking_menu_item) @@ -41,6 +41,14 @@ module Sidebars private + def feature_enabled? + if ::Feature.enabled?(:split_operations_visibility_permissions, context.project) + context.project.feature_available?(:monitor, context.current_user) + else + context.project.feature_available?(:operations, context.current_user) + end + end + def metrics_dashboard_menu_item unless can?(context.current_user, :metrics_dashboard, context.project) return ::Sidebars::NilMenuItem.new(item_id: :metrics) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f67e31d4966..cb2052c7aea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19337,6 +19337,9 @@ msgstr[1] "" msgid "HarborRegistry|-- artifacts" msgstr "" +msgid "HarborRegistry|-- tags" +msgstr "" + msgid "HarborRegistry|Digest: %{imageId}" msgstr "" @@ -31261,6 +31264,9 @@ msgstr "" msgid "ProjectSettings|Merging is only allowed when the source branch is up-to-date with its target." msgstr "" +msgid "ProjectSettings|Monitor" +msgstr "" + msgid "ProjectSettings|No merge commits are created." msgstr "" @@ -41711,6 +41717,9 @@ msgstr "" msgid "Total cores (CPUs)" msgstr "" +msgid "Total issue weight" +msgstr "" + msgid "Total memory (GB)" msgstr "" diff --git a/rubocop/cop/gitlab/feature_available_usage.rb b/rubocop/cop/gitlab/feature_available_usage.rb index 3e0385c4018..4dba4baf1e7 100644 --- a/rubocop/cop/gitlab/feature_available_usage.rb +++ b/rubocop/cop/gitlab/feature_available_usage.rb @@ -21,6 +21,7 @@ module RuboCop metrics_dashboard analytics operations + monitor security_and_compliance container_registry environments diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e0adad832f5..b30610d98d7 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -920,6 +920,7 @@ RSpec.describe ProjectsController do environments_access_level feature_flags_access_level releases_access_level + monitor_access_level ] end @@ -947,6 +948,7 @@ RSpec.describe ProjectsController do where(:feature_access_level) do %i[ environments_access_level feature_flags_access_level + monitor_access_level ] end diff --git a/spec/factories/packages/rpm/metadata.rb b/spec/factories/packages/rpm/metadata.rb index 96c785bd1ae..5ee85aed3bb 100644 --- a/spec/factories/packages/rpm/metadata.rb +++ b/spec/factories/packages/rpm/metadata.rb @@ -7,5 +7,6 @@ FactoryBot.define do summary { FFaker::Lorem.sentences(2).join } description { FFaker::Lorem.sentences(4).join } arch { FFaker::Lorem.word } + epoch { 0 } end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 95b72648cf5..871917a725e 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -35,6 +35,7 @@ FactoryBot.define do end metrics_dashboard_access_level { ProjectFeature::PRIVATE } operations_access_level { ProjectFeature::ENABLED } + monitor_access_level { ProjectFeature::ENABLED } container_registry_access_level { ProjectFeature::ENABLED } security_and_compliance_access_level { ProjectFeature::PRIVATE } environments_access_level { ProjectFeature::ENABLED } diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb index b888e2f4171..f612956600f 100644 --- a/spec/features/monitor_sidebar_link_spec.rb +++ b/spec/features/monitor_sidebar_link_spec.rb @@ -4,39 +4,59 @@ require 'spec_helper' RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do let_it_be_with_reload(:project) { create(:project, :internal, :repository) } + let_it_be(:user) { create(:user) } - let(:user) { create(:user) } - let(:access_level) { ProjectFeature::PUBLIC } let(:role) { nil } before do project.add_role(user, role) if role - project.project_feature.update_attribute(:operations_access_level, access_level) - sign_in(user) - visit project_issues_path(project) end shared_examples 'shows Monitor menu based on the access level' do - context 'when operations project feature is PRIVATE' do - let(:access_level) { ProjectFeature::PRIVATE } - - it 'shows the `Monitor` menu' do - expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor') - end + using RSpec::Parameterized::TableSyntax + + let(:enabled) { Featurable::PRIVATE } + let(:disabled) { Featurable::DISABLED } + + where(:flag_enabled, :operations_access_level, :monitor_level, :render) do + true | ref(:disabled) | ref(:enabled) | true + true | ref(:disabled) | ref(:disabled) | false + true | ref(:enabled) | ref(:enabled) | true + true | ref(:enabled) | ref(:disabled) | false + false | ref(:disabled) | ref(:enabled) | false + false | ref(:disabled) | ref(:disabled) | false + false | ref(:enabled) | ref(:enabled) | true + false | ref(:enabled) | ref(:disabled) | true end - context 'when operations project feature is DISABLED' do - let(:access_level) { ProjectFeature::DISABLED } + with_them do + it 'renders when expected to' do + stub_feature_flags(split_operations_visibility_permissions: flag_enabled) + project.project_feature.update_attribute(:operations_access_level, operations_access_level) + project.project_feature.update_attribute(:monitor_access_level, monitor_level) + + visit project_issues_path(project) - it 'does not show the `Monitor` menu' do - expect(page).not_to have_selector('a.shortcuts-monitor') + if render + expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor') + else + expect(page).not_to have_selector('a.shortcuts-monitor') + end end end end - context 'user is not a member' do + context 'when user is not a member' do + let(:access_level) { ProjectFeature::PUBLIC } + + before do + project.project_feature.update_attribute(:operations_access_level, access_level) + project.project_feature.update_attribute(:monitor_access_level, access_level) + end + it 'has the correct `Monitor` menu items', :aggregate_failures do + visit project_issues_path(project) expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor') expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) @@ -48,27 +68,50 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project)) end - context 'when operations project feature is PRIVATE' do - let(:access_level) { ProjectFeature::PRIVATE } + context 'with new monitor visiblity flag disabled' do + stub_feature_flags(split_operations_visibility_permissions: false) - it 'does not show the `Monitor` menu' do - expect(page).not_to have_selector('a.shortcuts-monitor') + context 'when operations project feature is PRIVATE' do + let(:access_level) { ProjectFeature::PRIVATE } + + it 'does not show the `Monitor` menu' do + expect(page).not_to have_selector('a.shortcuts-monitor') + end + end + + context 'when operations project feature is DISABLED' do + let(:access_level) { ProjectFeature::DISABLED } + + it 'does not show the `Operations` menu' do + expect(page).not_to have_selector('a.shortcuts-monitor') + end end end - context 'when operations project feature is DISABLED' do - let(:access_level) { ProjectFeature::DISABLED } + context 'with new monitor visiblity flag enabled' do + context 'when monitor project feature is PRIVATE' do + let(:access_level) { ProjectFeature::PRIVATE } + + it 'does not show the `Monitor` menu' do + expect(page).not_to have_selector('a.shortcuts-monitor') + end + end + + context 'when operations project feature is DISABLED' do + let(:access_level) { ProjectFeature::DISABLED } - it 'does not show the `Operations` menu' do - expect(page).not_to have_selector('a.shortcuts-monitor') + it 'does not show the `Operations` menu' do + expect(page).not_to have_selector('a.shortcuts-monitor') + end end end end - context 'user has guest role' do + context 'when user has guest role' do let(:role) { :guest } it 'has the correct `Monitor` menu items' do + visit project_issues_path(project) expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor') expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) @@ -83,10 +126,11 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do it_behaves_like 'shows Monitor menu based on the access level' end - context 'user has reporter role' do + context 'when user has reporter role' do let(:role) { :reporter } it 'has the correct `Monitor` menu items' do + visit project_issues_path(project) expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) @@ -100,10 +144,11 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do it_behaves_like 'shows Monitor menu based on the access level' end - context 'user has developer role' do + context 'when user has developer role' do let(:role) { :developer } it 'has the correct `Monitor` menu items' do + visit project_issues_path(project) expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).to have_link('Alerts', href: project_alert_management_index_path(project)) expect(page).to have_link('Incidents', href: project_incidents_path(project)) @@ -116,10 +161,11 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do it_behaves_like 'shows Monitor menu based on the access level' end - context 'user has maintainer role' do + context 'when user has maintainer role' do let(:role) { :maintainer } it 'has the correct `Monitor` menu items' do + visit project_issues_path(project) expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).to have_link('Alerts', href: project_alert_management_index_path(project)) expect(page).to have_link('Incidents', href: project_incidents_path(project)) diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 9906f62878f..fcf305d60e6 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -8,9 +8,9 @@ import { getGroupItemMicrodata } from '~/groups/store/utils'; import * as urlUtilities from '~/lib/utils/url_utility'; import { ITEM_TYPE } from '~/groups/constants'; import { - VISIBILITY_LEVEL_PRIVATE, - VISIBILITY_LEVEL_INTERNAL, - VISIBILITY_LEVEL_PUBLIC, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, } from '~/visibility_level/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -19,7 +19,7 @@ import { mockParentGroupItem, mockChildren } from '../mock_data'; const createComponent = ( propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] }, provide = { - currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE, + currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE_STRING, }, ) => { return mountExtended(GroupItem, { @@ -320,16 +320,16 @@ describe('GroupItemComponent', () => { describe('when showing projects', () => { describe.each` - itemVisibility | currentGroupVisibility | isPopoverShown - ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} - ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} - ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} - ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PRIVATE} | ${false} - ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PRIVATE} | ${true} - ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PRIVATE} | ${true} - ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_INTERNAL} | ${false} - ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_INTERNAL} | ${false} - ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_INTERNAL} | ${true} + itemVisibility | currentGroupVisibility | isPopoverShown + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${true} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${true} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${true} `( 'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility', ({ itemVisibility, currentGroupVisibility, isPopoverShown }) => { @@ -374,7 +374,7 @@ describe('GroupItemComponent', () => { wrapper = createComponent({ group: { ...mockParentGroupItem, - visibility: VISIBILITY_LEVEL_PUBLIC, + visibility: VISIBILITY_LEVEL_PUBLIC_STRING, type: ITEM_TYPE.PROJECT, }, parentGroup: mockChildren[0], diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index 6c1eb373b7e..866868eff36 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -6,7 +6,7 @@ import GroupItemComponent from '~/groups/components/group_item.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import GroupsComponent from '~/groups/components/groups.vue'; import eventHub from '~/groups/event_hub'; -import { VISIBILITY_LEVEL_PRIVATE } from '~/visibility_level/constants'; +import { VISIBILITY_LEVEL_PRIVATE_STRING } from '~/visibility_level/constants'; import { mockGroups, mockPageInfo } from '../mock_data'; describe('GroupsComponent', () => { @@ -26,7 +26,7 @@ describe('GroupsComponent', () => { ...propsData, }, provide: { - currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE, + currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE_STRING, }, }); }; diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js index 50210fd5943..a2e5cbdce8b 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js @@ -90,7 +90,7 @@ describe('Harbor artifact list row', () => { }); it('has correct digest', () => { - expect(findByTestId('digest').text()).toBe('Digest: 5d98daa'); + expect(findByTestId('digest').text()).toBe('Digest: mock_sh'); }); describe('time', () => { it('has the correct push time', () => { diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js new file mode 100644 index 00000000000..5e299a269e3 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js @@ -0,0 +1,52 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { mockArtifactDetail, MOCK_SHA_DIGEST } from '../../mock_data'; + +describe('Harbor Tags Header', () => { + let wrapper; + + const findTitle = () => wrapper.findByTestId('title'); + const findTagsCount = () => wrapper.findByTestId('tags-count'); + + const mountComponent = ({ propsData }) => { + wrapper = shallowMountExtended(TagsHeader, { + propsData, + stubs: { + TitleArea, + }, + }); + }; + + const mockPageInfo = { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + mountComponent({ + propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false }, + }); + }); + + describe('tags title', () => { + it('should be artifact digest', () => { + expect(findTitle().text()).toBe(`sha256:${MOCK_SHA_DIGEST}`); + }); + }); + + describe('tags count', () => { + it('would has the correct text', async () => { + await nextTick(); + + expect(findTagsCount().props('text')).toBe('1 tag'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js new file mode 100644 index 00000000000..6fe3dabc603 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js @@ -0,0 +1,75 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue'; +import { defaultConfig, harborTagsList } from '../../mock_data'; + +describe('Harbor tag list row', () => { + let wrapper; + + const findListItem = () => wrapper.find(ListItem); + const findClipboardButton = () => wrapper.find(ClipboardButton); + const findByTestId = (testId) => wrapper.findByTestId(testId); + + const $route = { + params: { + project: defaultConfig.harborIntegrationProjectName, + image: 'test-repository', + }, + }; + + const mountComponent = ({ propsData, config = defaultConfig }) => { + wrapper = shallowMountExtended(TagsListRow, { + stubs: { + ListItem, + GlSprintf, + }, + propsData, + mocks: { + $route, + }, + provide() { + return { + ...config, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('list item', () => { + beforeEach(() => { + mountComponent({ + propsData: { + tag: harborTagsList[0], + }, + }); + }); + + it('exists', () => { + expect(findListItem().exists()).toBe(true); + }); + + it('has the correct tag name', () => { + expect(findByTestId('name').text()).toBe(harborTagsList[0].name); + }); + + describe(' clipboard button', () => { + it('exists', () => { + expect(findClipboardButton().exists()).toBe(true); + }); + + it('has the correct props', () => { + const pullCommand = `docker pull demo.harbor.com/test-project/test-repository:${harborTagsList[0].name}`; + expect(findClipboardButton().attributes()).toMatchObject({ + text: pullCommand, + title: pullCommand, + }); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js new file mode 100644 index 00000000000..6bcf6611d07 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js @@ -0,0 +1,66 @@ +import { shallowMount } from '@vue/test-utils'; +import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue'; +import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; +import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import { defaultConfig, harborTagsResponse } from '../../mock_data'; + +describe('Harbor Tags List', () => { + let wrapper; + + const findTagsLoader = () => wrapper.find(TagsLoader); + const findTagsListRows = () => wrapper.findAllComponents(TagsListRow); + const findRegistryList = () => wrapper.find(RegistryList); + + const mountComponent = ({ propsData, config = defaultConfig }) => { + wrapper = shallowMount(TagsList, { + propsData, + stubs: { RegistryList }, + provide() { + return { + ...config, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when isLoading is true', () => { + beforeEach(() => { + mountComponent({ + propsData: { + isLoading: true, + pageInfo: {}, + tags: [], + }, + }); + }); + + it('show the loader', () => { + expect(findTagsLoader().exists()).toBe(true); + }); + }); + + describe('tags list', () => { + beforeEach(() => { + mountComponent({ + propsData: { + isLoading: false, + pageInfo: {}, + tags: harborTagsResponse, + }, + }); + }); + + it('should render correctly', () => { + expect(findRegistryList().exists()).toBe(true); + }); + + it('one tag row exists', () => { + expect(findTagsListRows()).toHaveLength(harborTagsResponse.length); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js index 0ba274ea176..b8989b6092e 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js @@ -2,6 +2,8 @@ export const harborImageDetailEmptyResponse = { data: null, }; +export const MOCK_SHA_DIGEST = 'mock_sha_digest_value'; + export const harborImageDetailResponse = { artifactCount: 10, creationTime: '2022-03-02T06:35:53.205Z', @@ -16,7 +18,7 @@ export const harborImageDetailResponse = { export const harborArtifactsResponse = [ { id: 1, - digest: 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35', + digest: `sha256:${MOCK_SHA_DIGEST}`, size: 773928, push_time: '2022-05-19T15:54:47.821Z', tags: ['latest'], @@ -26,7 +28,7 @@ export const harborArtifactsResponse = [ export const harborArtifactsList = [ { id: 1, - digest: 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35', + digest: `sha256:${MOCK_SHA_DIGEST}`, size: 773928, pushTime: '2022-05-19T15:54:47.821Z', tags: ['latest'], @@ -104,3 +106,9 @@ export const dockerCommands = { dockerPushCommand: 'barbar', dockerLoginCommand: 'bazbaz', }; + +export const mockArtifactDetail = { + project: 'test-project', + image: 'test-repository', + digest: `sha256:${MOCK_SHA_DIGEST}`, +}; diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js new file mode 100644 index 00000000000..7e0f05e736b --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js @@ -0,0 +1,125 @@ +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import HarborTagsPage from '~/packages_and_registries/harbor_registry/pages/harbor_tags.vue'; +import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue'; +import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import { defaultConfig, harborTagsResponse, mockArtifactDetail } from '../mock_data'; + +let mockHarborTagsResponse; + +jest.mock('~/rest_api', () => ({ + getHarborTags: () => mockHarborTagsResponse, +})); + +describe('Harbor Tags page', () => { + let wrapper; + + const findTagsHeader = () => wrapper.find(TagsHeader); + const findTagsList = () => wrapper.find(TagsList); + + const waitForHarborTagsRequest = async () => { + await waitForPromises(); + await nextTick(); + }; + + const breadCrumbState = { + updateName: jest.fn(), + updateHref: jest.fn(), + }; + + const $route = { + params: mockArtifactDetail, + }; + + const defaultHeaders = { + 'x-page': '1', + 'X-Per-Page': '20', + 'X-TOTAL': '1', + 'X-Total-Pages': '1', + }; + + const mountComponent = ({ endpoint = defaultConfig.endpoint } = {}) => { + wrapper = shallowMount(HarborTagsPage, { + mocks: { + $route, + }, + provide() { + return { + breadCrumbState, + endpoint, + }; + }, + }); + }; + + beforeEach(() => { + mockHarborTagsResponse = Promise.resolve({ + data: harborTagsResponse, + headers: defaultHeaders, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains tags header', () => { + mountComponent(); + + expect(findTagsHeader().exists()).toBe(true); + }); + + it('contains tags list', () => { + mountComponent(); + + expect(findTagsList().exists()).toBe(true); + }); + + describe('header', () => { + it('has the correct props', async () => { + mountComponent(); + + await waitForHarborTagsRequest(); + expect(findTagsHeader().props()).toMatchObject({ + artifactDetail: mockArtifactDetail, + pageInfo: { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }, + tagsLoading: false, + }); + }); + }); + + describe('list', () => { + it('has the correct props', async () => { + mountComponent(); + + await waitForHarborTagsRequest(); + expect(findTagsList().props()).toMatchObject({ + tags: [ + { + repositoryId: 4, + artifactId: 5, + id: 4, + name: 'latest', + pullTime: '0001-01-01T00:00:00.000Z', + pushTime: '2022-05-27T18:21:27.903Z', + signed: false, + immutable: false, + }, + ], + isLoading: false, + pageInfo: { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }, + }); + }); + }); +}); diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index b34c64cb1b0..ed7d4ad269e 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -5,8 +5,12 @@ import settingsPanel from '~/pages/projects/shared/permissions/components/settin import { featureAccessLevel, visibilityLevelDescriptions, - visibilityOptions, } from '~/pages/projects/shared/permissions/constants'; +import { + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, +} from '~/visibility_level/constants'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; const defaultProps = { @@ -134,6 +138,7 @@ describe('Settings Panel', () => { const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' }); const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' }); const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' }); + const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' }); afterEach(() => { wrapper.destroy(); @@ -162,13 +167,13 @@ describe('Settings Panel', () => { }); it.each` - option | allowedOptions | disabled - ${visibilityOptions.PRIVATE} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false} - ${visibilityOptions.PRIVATE} | ${[visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${true} - ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false} - ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.PUBLIC]} | ${true} - ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false} - ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL]} | ${true} + option | allowedOptions | disabled + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${true} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${true} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER]} | ${true} `( 'sets disabled to $disabled for the visibility option $option when given $allowedOptions', ({ option, allowedOptions, disabled }) => { @@ -187,35 +192,37 @@ describe('Settings Panel', () => { it('should set the visibility level description based upon the selected visibility level', () => { wrapper = mountComponent({ stubs: { GlSprintf } }); - findProjectVisibilityLevelInput().setValue(visibilityOptions.INTERNAL); + findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_INTERNAL_INTEGER); expect(findProjectVisibilitySettings().text()).toContain( - visibilityLevelDescriptions[visibilityOptions.INTERNAL], + visibilityLevelDescriptions[VISIBILITY_LEVEL_INTERNAL_INTEGER], ); }); it('should show the request access checkbox if the visibility level is not private', () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER }, }); expect(findRequestAccessEnabledInput().exists()).toBe(true); }); it('should not show the request access checkbox if the visibility level is private', () => { - wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }); + wrapper = mountComponent({ + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER }, + }); expect(findRequestAccessEnabledInput().exists()).toBe(false); }); it('does not require confirmation if the visibility is reduced', async () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER }, }); expect(findConfirmDangerButton().exists()).toBe(false); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); expect(findConfirmDangerButton().exists()).toBe(false); }); @@ -223,7 +230,7 @@ describe('Settings Panel', () => { describe('showVisibilityConfirmModal=true', () => { beforeEach(() => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER }, showVisibilityConfirmModal: true, }); }); @@ -231,7 +238,7 @@ describe('Settings Panel', () => { it('will render the confirmation dialog if the visibility is reduced', async () => { expect(findConfirmDangerButton().exists()).toBe(false); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); expect(findConfirmDangerButton().exists()).toBe(true); }); @@ -239,7 +246,7 @@ describe('Settings Panel', () => { it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => { expect(wrapper.emitted('confirm')).toBeUndefined(); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); await findConfirmDangerButton().vm.$emit('confirm'); expect(wrapper.emitted('confirm')).toHaveLength(1); @@ -259,7 +266,9 @@ describe('Settings Panel', () => { describe('Repository', () => { it('should set the repository help text when the visibility level is set to private', () => { - wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }); + wrapper = mountComponent({ + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER }, + }); expect(findRepositoryFeatureProjectRow().props('helpText')).toBe( 'View and edit files in this project.', @@ -267,7 +276,9 @@ describe('Settings Panel', () => { }); it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => { - wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC } }); + wrapper = mountComponent({ + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER }, + }); expect(findRepositoryFeatureProjectRow().props('helpText')).toBe( 'View and edit files in this project. Non-project members have only read access.', @@ -351,7 +362,7 @@ describe('Settings Panel', () => { it('should show the container registry public note if the visibility level is public and the registry is available', () => { wrapper = mountComponent({ currentSettings: { - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, containerRegistryAccessLevel: featureAccessLevel.EVERYONE, }, registryAvailable: true, @@ -366,7 +377,7 @@ describe('Settings Panel', () => { it('should hide the container registry public note if the visibility level is public but the registry is private', () => { wrapper = mountComponent({ currentSettings: { - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, containerRegistryAccessLevel: featureAccessLevel.PROJECT_MEMBERS, }, registryAvailable: true, @@ -377,7 +388,7 @@ describe('Settings Panel', () => { it('should hide the container registry public note if the visibility level is private and the registry is available', () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.PRIVATE }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER }, registryAvailable: true, }); @@ -386,7 +397,7 @@ describe('Settings Panel', () => { it('has label for the toggle', () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.PUBLIC }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER }, registryAvailable: true, }); @@ -575,10 +586,10 @@ describe('Settings Panel', () => { }); it.each` - visibilityLevel | output - ${visibilityOptions.PRIVATE} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]} - ${visibilityOptions.INTERNAL} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]} - ${visibilityOptions.PUBLIC} | ${[[30, 'Everyone']]} + visibilityLevel | output + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[[30, 'Everyone']]} `( 'renders correct options when visibilityLevel is $visibilityLevel', async ({ visibilityLevel, output }) => { @@ -595,23 +606,23 @@ describe('Settings Panel', () => { ); it.each` - initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption - ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE} + initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE} `( 'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel', async ({ @@ -641,13 +652,13 @@ describe('Settings Panel', () => { describe('Pages', () => { it.each` - visibilityLevel | pagesAccessControlForced | output - ${visibilityOptions.PRIVATE} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} - ${visibilityOptions.INTERNAL} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} - ${visibilityOptions.PUBLIC} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} + visibilityLevel | pagesAccessControlForced | output + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]} `( 'renders correct options when pagesAccessControlForced is $pagesAccessControlForced and visibilityLevel is $visibilityLevel', async ({ visibilityLevel, pagesAccessControlForced, output }) => { @@ -766,13 +777,13 @@ describe('Settings Panel', () => { it('should reduce Metrics visibility level when visibility is set to private', async () => { wrapper = mountComponent({ currentSettings: { - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, operationsAccessLevel: featureAccessLevel.EVERYONE, metricsDashboardAccessLevel: featureAccessLevel.EVERYONE, }, }); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS); }); @@ -830,7 +841,6 @@ describe('Settings Panel', () => { }); }); }); - describe('Releases', () => { describe('with feature flag', () => { it('should show the releases toggle', () => { @@ -849,4 +859,42 @@ describe('Settings Panel', () => { }); }); }); + describe('Monitor', () => { + const expectedAccessLevel = [ + [10, 'Only Project Members'], + [20, 'Everyone With Access'], + ]; + describe('with feature flag', () => { + it('shows Monitor toggle instead of Operations toggle', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + }); + + expect(findMonitorSettings().exists()).toBe(true); + expect(findOperationsSettings().exists()).toBe(false); + expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual( + expectedAccessLevel, + ); + }); + it('when monitorAccessLevel is for project members, it is also for everyone', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS }, + }); + + expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE); + }); + }); + describe('without feature flag', () => { + it('shows Operations toggle instead of Monitor toggle', () => { + wrapper = mountComponent({}); + + expect(findMonitorSettings().exists()).toBe(false); + expect(findOperationsSettings().exists()).toBe(true); + expect( + findOperationsSettings().findComponent(ProjectFeatureSetting).props('options'), + ).toEqual(expectedAccessLevel); + }); + }); + }); }); diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index f49ceb2fede..cf897414ccb 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -16,10 +16,10 @@ import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_e import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; import { - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, -} from '~/snippets/constants'; + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import CreateSnippetMutation from '~/snippets/mutations/create_snippet.mutation.graphql'; import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.graphql'; import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; @@ -41,7 +41,7 @@ const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42'; const createSnippet = () => merge(createGQLSnippet(), { webUrl: TEST_WEB_URL, - visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, + visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING, }); const createQueryResponse = (obj = {}) => @@ -70,7 +70,7 @@ const getApiData = ({ id, title = '', description = '', - visibilityLevel = SNIPPET_VISIBILITY_PRIVATE, + visibilityLevel = VISIBILITY_LEVEL_PRIVATE_STRING, } = {}) => ({ id, title, @@ -128,7 +128,10 @@ describe('Snippet Edit app', () => { const setDescription = (val) => wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val); - const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => { + const createComponent = ({ + props = {}, + selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING, + } = {}) => { if (wrapper) { throw new Error('wrapper already created'); } @@ -260,17 +263,18 @@ describe('Snippet Edit app', () => { }, ); - it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])( - 'marks %s visibility by default', - async (visibility) => { - createComponent({ - props: { snippetGid: '' }, - selectedLevel: visibility, - }); + it.each([ + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, + ])('marks %s visibility by default', async (visibility) => { + createComponent({ + props: { snippetGid: '' }, + selectedLevel: visibility, + }); - expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility); - }, - ); + expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility); + }); describe('form submission handling', () => { describe('when creating a new snippet', () => { diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index d3b5c0e69db..032dcf8e5f5 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -7,10 +7,10 @@ import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetTitle from '~/snippets/components/snippet_title.vue'; import { - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_PUBLIC, -} from '~/snippets/constants'; + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import { stubPerformanceWebAPI } from 'helpers/performance'; @@ -69,7 +69,7 @@ describe('Snippet view app', () => { createComponent({ data: { snippet: { - visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_STRING, webUrl: 'http://foo.bar', }, }, @@ -93,11 +93,11 @@ describe('Snippet view app', () => { describe('Embed dropdown rendering', () => { it.each` - visibilityLevel | condition | isRendered - ${SNIPPET_VISIBILITY_INTERNAL} | ${'not render'} | ${false} - ${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false} - ${'foo'} | ${'not render'} | ${false} - ${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true} + visibilityLevel | condition | isRendered + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${'not render'} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${'not render'} | ${false} + ${'foo'} | ${'not render'} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'render'} | ${true} `('does $condition embed-dropdown by default', ({ visibilityLevel, isRendered }) => { createComponent({ data: { diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index c395112e313..aa31377f390 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -15,7 +15,7 @@ import { BLOB_RENDER_ERRORS, } from '~/blob/components/constants'; import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; -import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; +import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants'; import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; describe('Blob Embeddable', () => { @@ -23,7 +23,7 @@ describe('Blob Embeddable', () => { const snippet = { id: 'gid://foo.bar/snippet', webUrl: 'https://foo.bar', - visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_STRING, }; const dataMock = { activeViewerType: SimpleViewerMock.type, diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js index a86ff566683..2d043a5caba 100644 --- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js @@ -2,10 +2,12 @@ import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; import { + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; +import { SNIPPET_VISIBILITY, - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED, } from '~/snippets/constants'; @@ -75,19 +77,19 @@ describe('Snippet Visibility Edit component', () => { const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]'); const RESULTING_OPTIONS = { 0: { - value: SNIPPET_VISIBILITY_PRIVATE, + value: VISIBILITY_LEVEL_PRIVATE_STRING, icon: SNIPPET_VISIBILITY.private.icon, text: SNIPPET_VISIBILITY.private.label, description: SNIPPET_VISIBILITY.private.description, }, 10: { - value: SNIPPET_VISIBILITY_INTERNAL, + value: VISIBILITY_LEVEL_INTERNAL_STRING, icon: SNIPPET_VISIBILITY.internal.icon, text: SNIPPET_VISIBILITY.internal.label, description: SNIPPET_VISIBILITY.internal.description, }, 20: { - value: SNIPPET_VISIBILITY_PUBLIC, + value: VISIBILITY_LEVEL_PUBLIC_STRING, icon: SNIPPET_VISIBILITY.public.icon, text: SNIPPET_VISIBILITY.public.label, description: SNIPPET_VISIBILITY.public.description, @@ -130,7 +132,7 @@ describe('Snippet Visibility Edit component', () => { createComponent({ propsData: { isProjectSnippet: true }, deep: true }); expect(findRadiosData()[0]).toEqual({ - value: SNIPPET_VISIBILITY_PRIVATE, + value: VISIBILITY_LEVEL_PRIVATE_STRING, icon: SNIPPET_VISIBILITY.private.icon, text: SNIPPET_VISIBILITY.private.label, description: SNIPPET_VISIBILITY.private.description_project, @@ -141,7 +143,7 @@ describe('Snippet Visibility Edit component', () => { describe('functionality', () => { it('pre-selects correct option in the list', () => { - const value = SNIPPET_VISIBILITY_INTERNAL; + const value = VISIBILITY_LEVEL_INTERNAL_STRING; createComponent({ propsData: { value } }); diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb index fcb48678b88..8f848af8bd3 100644 --- a/spec/lib/gitlab/import_export/config_spec.rb +++ b/spec/lib/gitlab/import_export/config_spec.rb @@ -21,10 +21,12 @@ RSpec.describe Gitlab::ImportExport::Config do end it 'parses default config' do + expected_keys = [:tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders] + expected_keys << :include_if_exportable if ee + expect { subject }.not_to raise_error expect(subject).to be_a(Hash) - expect(subject.keys).to contain_exactly( - :tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders) + expect(subject.keys).to match_array(expected_keys) end end end diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb index b8d18718dfb..02ac8065c9f 100644 --- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb @@ -32,18 +32,20 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys } let(:include) { [] } let(:custom_orderer) { nil } + let(:include_if_exportable) { {} } let(:relations_schema) do { only: [:name, :description], include: include, preload: { issues: nil }, - export_reorder: custom_orderer + export_reorder: custom_orderer, + include_if_exportable: include_if_exportable } end subject do - described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger) + described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger, current_user: user) end describe '#execute' do @@ -210,6 +212,63 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do subject.execute end end + + describe 'conditional export of included associations' do + let(:include) do + [{ issues: { include: [{ label_links: { include: [:label] } }] } }] + end + + let(:include_if_exportable) do + { issues: [:label_links] } + end + + let_it_be(:label) { create(:label, project: exportable) } + let_it_be(:link) { create(:label_link, label: label, target: issue) } + + context 'when association is exportable' do + before do + allow_next_found_instance_of(Issue) do |issue| + allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true) + end + end + + it 'includes exportable association' do + expected_issue = issue.to_json(include: [{ label_links: { include: [:label] } }]) + + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue)) + + subject.execute + end + end + + context 'when association is not exportable' do + before do + allow_next_found_instance_of(Issue) do |issue| + allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(false) + end + end + + it 'filters out not exportable association' do + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json)) + + subject.execute + end + end + + context 'when association does not respond to exportable_association?' do + before do + allow_next_found_instance_of(Issue) do |issue| + allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false) + end + end + + it 'filters out not exportable association' do + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json)) + + subject.execute + end + end + end end describe '#serialize_relation' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 361640659b8..352255afc8d 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -586,6 +586,7 @@ ProjectFeature: - environments_access_level - feature_flags_access_level - releases_access_level +- monitor_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb index ba5137e2b92..bd0904b9db2 100644 --- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb @@ -12,11 +12,28 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do subject { described_class.new(context) } describe '#render?' do - context 'when operations feature is disabled' do - it 'returns false' do - project.project_feature.update!(operations_access_level: Featurable::DISABLED) + using RSpec::Parameterized::TableSyntax + let(:enabled) { Featurable::PRIVATE } + let(:disabled) { Featurable::DISABLED } + + where(:flag_enabled, :operations_access_level, :monitor_level, :render) do + true | ref(:disabled) | ref(:enabled) | true + true | ref(:disabled) | ref(:disabled) | false + true | ref(:enabled) | ref(:enabled) | true + true | ref(:enabled) | ref(:disabled) | false + false | ref(:disabled) | ref(:enabled) | false + false | ref(:disabled) | ref(:disabled) | false + false | ref(:enabled) | ref(:enabled) | true + false | ref(:enabled) | ref(:disabled) | true + end + + with_them do + it 'renders when expected to' do + stub_feature_flags(split_operations_visibility_permissions: flag_enabled) + project.project_feature.update!(operations_access_level: operations_access_level) + project.project_feature.update!(monitor_access_level: monitor_level) - expect(subject.render?).to be false + expect(subject.render?).to be render end end diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index b49b9ce8a2a..89f34834aa4 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -8,6 +8,7 @@ RSpec.describe ProjectFeaturesCompatibility do let(:features) do features_enabled + %w( repository pages operations container_registry package_registry environments feature_flags releases + monitor ) end diff --git a/spec/models/packages/rpm/metadatum_spec.rb b/spec/models/packages/rpm/metadatum_spec.rb index 53a40b4a4b3..0e7817fdf86 100644 --- a/spec/models/packages/rpm/metadatum_spec.rb +++ b/spec/models/packages/rpm/metadatum_spec.rb @@ -8,10 +8,14 @@ RSpec.describe Packages::Rpm::Metadatum, type: :model do describe 'validations' do it { is_expected.to validate_presence_of(:package) } + it { is_expected.to validate_presence_of(:epoch) } it { is_expected.to validate_presence_of(:release) } it { is_expected.to validate_presence_of(:summary) } it { is_expected.to validate_presence_of(:description) } it { is_expected.to validate_presence_of(:arch) } + + it { is_expected.to validate_numericality_of(:epoch).only_integer.is_greater_than_or_equal_to(0) } + it { is_expected.to validate_length_of(:release).is_at_most(128) } it { is_expected.to validate_length_of(:summary).is_at_most(1000) } it { is_expected.to validate_length_of(:description).is_at_most(5000) } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 9bf96475fc1..5b032c53352 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1930,14 +1930,10 @@ RSpec.describe ProjectPolicy do describe 'operations feature' do using RSpec::Parameterized::TableSyntax - before do - stub_feature_flags(split_operations_visibility_permissions: false) - end + let(:guest_permissions) { [:read_environment, :read_deployment] } - let(:guest_operations_permissions) { [:read_environment, :read_deployment] } - - let(:developer_operations_permissions) do - guest_operations_permissions + [ + let(:developer_permissions) do + guest_permissions + [ :read_feature_flag, :read_sentry_issue, :read_alert_management_alert, :read_terraform_state, :metrics_dashboard, :read_pod_logs, :read_prometheus, :create_feature_flag, :create_environment, :create_deployment, :update_feature_flag, :update_environment, @@ -1946,13 +1942,17 @@ RSpec.describe ProjectPolicy do ] end - let(:maintainer_operations_permissions) do - developer_operations_permissions + [ + let(:maintainer_permissions) do + developer_permissions + [ :read_cluster, :create_cluster, :update_cluster, :admin_environment, :admin_cluster, :admin_terraform_state, :admin_deployment ] end + before do + stub_feature_flags(split_operations_visibility_permissions: false) + end + where(:project_visibility, :access_level, :role, :allowed) do :public | ProjectFeature::ENABLED | :maintainer | true :public | ProjectFeature::ENABLED | :developer | true @@ -2005,33 +2005,22 @@ RSpec.describe ProjectPolicy do expect_disallowed(*permissions_abilities(role)) end end - - def permissions_abilities(role) - case role - when :maintainer - maintainer_operations_permissions - when :developer - developer_operations_permissions - else - guest_operations_permissions - end - end end end describe 'environments feature' do using RSpec::Parameterized::TableSyntax - let(:guest_environments_permissions) { [:read_environment, :read_deployment] } + let(:guest_permissions) { [:read_environment, :read_deployment] } - let(:developer_environments_permissions) do - guest_environments_permissions + [ + let(:developer_permissions) do + guest_permissions + [ :create_environment, :create_deployment, :update_environment, :update_deployment, :destroy_environment ] end - let(:maintainer_environments_permissions) do - developer_environments_permissions + [:admin_environment, :admin_deployment] + let(:maintainer_permissions) do + developer_permissions + [:admin_environment, :admin_deployment] end where(:project_visibility, :access_level, :role, :allowed) do @@ -2086,15 +2075,73 @@ RSpec.describe ProjectPolicy do expect_disallowed(*permissions_abilities(role)) end end + end + end - def permissions_abilities(role) - case role - when :maintainer - maintainer_environments_permissions - when :developer - developer_environments_permissions + describe 'monitor feature' do + using RSpec::Parameterized::TableSyntax + + let(:guest_permissions) { [] } + + let(:developer_permissions) do + guest_permissions + [ + :read_sentry_issue, :read_alert_management_alert, :metrics_dashboard, + :update_sentry_issue, :update_alert_management_alert + ] + end + + let(:maintainer_permissions) { developer_permissions } + + where(:project_visibility, :access_level, :role, :allowed) do + :public | ProjectFeature::ENABLED | :maintainer | true + :public | ProjectFeature::ENABLED | :developer | true + :public | ProjectFeature::ENABLED | :guest | true + :public | ProjectFeature::ENABLED | :anonymous | true + :public | ProjectFeature::PRIVATE | :maintainer | true + :public | ProjectFeature::PRIVATE | :developer | true + :public | ProjectFeature::PRIVATE | :guest | true + :public | ProjectFeature::PRIVATE | :anonymous | false + :public | ProjectFeature::DISABLED | :maintainer | false + :public | ProjectFeature::DISABLED | :developer | false + :public | ProjectFeature::DISABLED | :guest | false + :public | ProjectFeature::DISABLED | :anonymous | false + :internal | ProjectFeature::ENABLED | :maintainer | true + :internal | ProjectFeature::ENABLED | :developer | true + :internal | ProjectFeature::ENABLED | :guest | true + :internal | ProjectFeature::ENABLED | :anonymous | false + :internal | ProjectFeature::PRIVATE | :maintainer | true + :internal | ProjectFeature::PRIVATE | :developer | true + :internal | ProjectFeature::PRIVATE | :guest | true + :internal | ProjectFeature::PRIVATE | :anonymous | false + :internal | ProjectFeature::DISABLED | :maintainer | false + :internal | ProjectFeature::DISABLED | :developer | false + :internal | ProjectFeature::DISABLED | :guest | false + :internal | ProjectFeature::DISABLED | :anonymous | false + :private | ProjectFeature::ENABLED | :maintainer | true + :private | ProjectFeature::ENABLED | :developer | true + :private | ProjectFeature::ENABLED | :guest | false + :private | ProjectFeature::ENABLED | :anonymous | false + :private | ProjectFeature::PRIVATE | :maintainer | true + :private | ProjectFeature::PRIVATE | :developer | true + :private | ProjectFeature::PRIVATE | :guest | false + :private | ProjectFeature::PRIVATE | :anonymous | false + :private | ProjectFeature::DISABLED | :maintainer | false + :private | ProjectFeature::DISABLED | :developer | false + :private | ProjectFeature::DISABLED | :guest | false + :private | ProjectFeature::DISABLED | :anonymous | false + end + + with_them do + let(:current_user) { user_subject(role) } + let(:project) { project_subject(project_visibility) } + + it 'allows/disallows the abilities based on the monitor feature access level' do + project.project_feature.update!(monitor_access_level: access_level) + + if allowed + expect_allowed(*permissions_abilities(role)) else - guest_environments_permissions + expect_disallowed(*permissions_abilities(role)) end end end @@ -2682,6 +2729,8 @@ RSpec.describe ProjectPolicy do end end + private + def project_subject(project_type) case project_type when :public diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 81c4f5d8188..e6916e02fde 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -84,7 +84,7 @@ RSpec.describe JwtController do context 'project with enabled CI' do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) } + it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build)).permit!) } it_behaves_like 'user logging' end @@ -107,7 +107,12 @@ RSpec.describe JwtController do it 'authenticates correctly' do expect(response).to have_gitlab_http_status(:ok) - expect(service_class).to have_received(:new).with(nil, nil, ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token)).permit!) + expect(service_class).to have_received(:new) + .with( + nil, + nil, + ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token)).permit! + ) end it 'does not log a user' do @@ -127,7 +132,12 @@ RSpec.describe JwtController do it 'authenticates correctly' do expect(response).to have_gitlab_http_status(:ok) - expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) + expect(service_class).to have_received(:new) + .with( + nil, + user, + ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token)).permit! + ) end it_behaves_like 'rejecting a blocked user' @@ -142,7 +152,7 @@ RSpec.describe JwtController do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) } + it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap)).permit!) } it_behaves_like 'rejecting a blocked user' @@ -162,7 +172,7 @@ RSpec.describe JwtController do ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit! end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } it_behaves_like 'user logging' end diff --git a/spec/services/bulk_imports/tree_export_service_spec.rb b/spec/services/bulk_imports/tree_export_service_spec.rb index ffb81fe2b5f..6e26cb6dc2b 100644 --- a/spec/services/bulk_imports/tree_export_service_spec.rb +++ b/spec/services/bulk_imports/tree_export_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe BulkImports::TreeExportService do let(:relation) { 'issues' } - subject(:service) { described_class.new(project, export_path, relation) } + subject(:service) { described_class.new(project, export_path, relation, project.owner) } describe '#execute' do it 'executes export service and archives exported data' do @@ -21,7 +21,7 @@ RSpec.describe BulkImports::TreeExportService do context 'when unsupported relation is passed' do it 'raises an error' do - service = described_class.new(project, export_path, 'unsupported') + service = described_class.new(project, export_path, 'unsupported', project.owner) expect { service.execute }.to raise_error(BulkImports::Error, 'Unsupported relation export type') end diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index 352326de868..58659775d8c 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -54,6 +54,12 @@ RSpec.shared_examples 'a valid token' do end end +RSpec.shared_examples 'with auth_type' do + let(:current_params) { super().merge(auth_type: :foo) } + + it { expect(payload['auth_type']).to eq('foo') } +end + RSpec.shared_examples 'a browsable' do let(:access) do [{ 'type' => 'registry', @@ -286,6 +292,7 @@ RSpec.shared_examples 'a container registry auth service' do shared_examples 'private project' do context 'allow to use scope-less authentication' do it_behaves_like 'a valid token' + it_behaves_like 'with auth_type' end context 'allow developer to push images' do @@ -299,6 +306,7 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'a pushable' it_behaves_like 'container repository factory' + it_behaves_like 'with auth_type' end context 'disallow developer to delete images' do @@ -341,6 +349,7 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' + it_behaves_like 'with auth_type' end end @@ -381,6 +390,7 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' + it_behaves_like 'with auth_type' end context 'disallow guest to pull or push images' do @@ -445,6 +455,7 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' + it_behaves_like 'with auth_type' end context 'disallow anyone to push images' do @@ -495,6 +506,7 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' + it_behaves_like 'with auth_type' end context 'disallow anyone to push images' do @@ -600,6 +612,7 @@ RSpec.shared_examples 'a container registry auth service' do end it_behaves_like 'a valid token' + it_behaves_like 'with auth_type' context 'allow to pull and push images' do let(:current_params) do @@ -944,10 +957,11 @@ RSpec.shared_examples 'a container registry auth service' do shared_examples 'able to login' do context 'registry provides read_container_image authentication_abilities' do - let(:current_params) { { deploy_token: deploy_token } } + let(:current_params) { { deploy_token: deploy_token, auth_type: :deploy_token } } let(:authentication_abilities) { [:read_container_image] } it_behaves_like 'an authenticated' + it { expect(payload['auth_type']).to eq('deploy_token') } end end |