Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/api/harbor_registry.js8
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue6
-rw-r--r--app/assets/javascripts/groups/constants.js26
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue54
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue82
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue74
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js12
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue103
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/router.js10
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue41
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue118
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js17
-rw-r--r--app/assets/javascripts/projects/project_visibility.js10
-rw-r--r--app/assets/javascripts/snippets/components/show.vue4
-rw-r--r--app/assets/javascripts/snippets/constants.js20
-rw-r--r--app/assets/javascripts/snippets/index.js8
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js4
-rw-r--r--app/assets/javascripts/visibility_level/constants.js24
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/models/concerns/project_features_compatibility.rb4
-rw-r--r--app/models/packages/rpm/metadatum.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_feature.rb1
-rw-r--r--app/policies/project_policy.rb7
-rw-r--r--app/services/auth/container_registry_authentication_service.rb1
-rw-r--r--app/services/bulk_imports/relation_export_service.rb2
-rw-r--r--app/services/bulk_imports/tree_export_service.rb8
-rw-r--r--db/migrate/20220912180807_add_epoch_column_to_rpm_metadata.rb7
-rw-r--r--db/schema_migrations/202209121808071
-rw-r--r--db/structure.sql1
-rw-r--r--doc/ci/environments/deployment_approvals.md3
-rw-r--r--doc/development/import_export.md23
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb4
-rw-r--r--lib/gitlab/import_export/group/tree_saver.rb3
-rw-r--r--lib/gitlab/import_export/json/streaming_serializer.rb32
-rw-r--r--lib/gitlab/import_export/project/import_export.yml27
-rw-r--r--lib/gitlab/import_export/project/relation_saver.rb3
-rw-r--r--lib/gitlab/import_export/project/tree_saver.rb3
-rw-r--r--lib/sidebars/projects/menus/monitor_menu.rb10
-rw-r--r--locale/gitlab.pot9
-rw-r--r--rubocop/cop/gitlab/feature_available_usage.rb1
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/factories/packages/rpm/metadata.rb1
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb102
-rw-r--r--spec/frontend/groups/components/group_item_spec.js30
-rw-r--r--spec/frontend/groups/components/groups_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js52
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js75
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js66
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/mock_data.js12
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js125
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js156
-rw-r--r--spec/frontend/snippets/components/edit_spec.js38
-rw-r--r--spec/frontend/snippets/components/show_spec.js20
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js18
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb63
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/sidebars/projects/menus/monitor_menu_spec.rb25
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb1
-rw-r--r--spec/models/packages/rpm/metadatum_spec.rb4
-rw-r--r--spec/policies/project_policy_spec.rb113
-rw-r--r--spec/requests/jwt_controller_spec.rb20
-rw-r--r--spec/services/bulk_imports/tree_export_service_spec.rb4
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb16
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