提交 0d09054d 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 f44809bf
...@@ -15,7 +15,7 @@ stages: ...@@ -15,7 +15,7 @@ stages:
# in cases where jobs require Docker-in-Docker, the job # in cases where jobs require Docker-in-Docker, the job
# definition must be extended with `.use-docker-in-docker` # definition must be extended with `.use-docker-in-docker`
default: default:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
tags: tags:
- gitlab-org - gitlab-org
# All jobs are interruptible by default # All jobs are interruptible by default
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- .default-retry - .default-retry
- .default-before_script - .default-before_script
- .assets-compile-cache - .assets-compile-cache
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-graphicsmagick-1.3.34-docker-19.03.1 image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-graphicsmagick-1.3.34-docker-19.03.1
stage: prepare stage: prepare
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
policy: pull policy: pull
.use-pg9: .use-pg9:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
services: services:
- name: postgres:9.6.17 - name: postgres:9.6.17
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
key: "debian-stretch-ruby-2.6.6-pg9-node-12.x" key: "debian-stretch-ruby-2.6.6-pg9-node-12.x"
.use-pg10: .use-pg10:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
services: services:
- name: postgres:10.12 - name: postgres:10.12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
key: "debian-stretch-ruby-2.6.6-pg10-node-12.x" key: "debian-stretch-ruby-2.6.6-pg10-node-12.x"
.use-pg11: .use-pg11:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34"
services: services:
- name: postgres:11.6 - name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
key: "debian-stretch-ruby-2.6.6-pg11-node-12.x" key: "debian-stretch-ruby-2.6.6-pg11-node-12.x"
.use-pg9-ee: .use-pg9-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
services: services:
- name: postgres:9.6.17 - name: postgres:9.6.17
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
key: "debian-stretch-ruby-2.6.6-pg9-node-12.x" key: "debian-stretch-ruby-2.6.6-pg9-node-12.x"
.use-pg10-ee: .use-pg10-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
services: services:
- name: postgres:10.12 - name: postgres:10.12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
key: "debian-stretch-ruby-2.6.6-pg10-node-12.x" key: "debian-stretch-ruby-2.6.6-pg10-node-12.x"
.use-pg11-ee: .use-pg11-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-81.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34"
services: services:
- name: postgres:11.6 - name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......
...@@ -145,7 +145,6 @@ db:check-schema: ...@@ -145,7 +145,6 @@ db:check-schema:
- .db-job-base - .db-job-base
- .rails:rules:ee-mr-and-master-only - .rails:rules:ee-mr-and-master-only
script: script:
- scripts/regenerate-schema
- source scripts/schema_changed.sh - source scripts/schema_changed.sh
db:migrate-from-v11.11.0: db:migrate-from-v11.11.0:
......
...@@ -39,14 +39,20 @@ export const DELETE_IMAGE_SUCCESS_MESSAGE = s__( ...@@ -39,14 +39,20 @@ export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
// Image details page // Image details page
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
export const DELETE_TAG_ERROR_MESSAGE = s__( export const DELETE_TAG_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while deleting the tag.', 'ContainerRegistry|Something went wrong while marking the tag for deletion.',
);
export const DELETE_TAG_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tag successfully marked for deletion.',
); );
export const DELETE_TAG_SUCCESS_MESSAGE = s__('ContainerRegistry|Tag deleted successfully');
export const DELETE_TAGS_ERROR_MESSAGE = s__( export const DELETE_TAGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while deleting the tags.', 'ContainerRegistry|Something went wrong while marking the tags for deletion.',
);
export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
); );
export const DELETE_TAGS_SUCCESS_MESSAGE = s__('ContainerRegistry|Tags deleted successfully');
export const DEFAULT_PAGE = 1; export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10; export const DEFAULT_PAGE_SIZE = 10;
...@@ -65,6 +71,27 @@ export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID'); ...@@ -65,6 +71,27 @@ export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size'); export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated'); export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
// Expiration policies // Expiration policies
export const EXPIRATION_POLICY_ALERT_TITLE = s__( export const EXPIRATION_POLICY_ALERT_TITLE = s__(
......
...@@ -9,12 +9,14 @@ import { ...@@ -9,12 +9,14 @@ import {
GlPagination, GlPagination,
GlModal, GlModal,
GlSprintf, GlSprintf,
GlAlert,
GlLink,
GlEmptyState, GlEmptyState,
GlResizeObserverDirective, GlResizeObserverDirective,
GlSkeletonLoader, GlSkeletonLoader,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { n__, s__ } from '~/locale'; import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
...@@ -35,6 +37,14 @@ import { ...@@ -35,6 +37,14 @@ import {
DELETE_TAG_ERROR_MESSAGE, DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE, DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE, DELETE_TAGS_ERROR_MESSAGE,
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '../constants'; } from '../constants';
export default { export default {
...@@ -49,6 +59,8 @@ export default { ...@@ -49,6 +59,8 @@ export default {
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
GlEmptyState, GlEmptyState,
GlAlert,
GlLink,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -60,6 +72,19 @@ export default { ...@@ -60,6 +72,19 @@ export default {
width: 1000, width: 1000,
height: 40, height: 40,
}, },
i18n: {
DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
},
alertMessages: {
success_tag: DELETE_TAG_SUCCESS_MESSAGE,
danger_tag: DELETE_TAG_ERROR_MESSAGE,
success_tags: DELETE_TAGS_SUCCESS_MESSAGE,
danger_tags: DELETE_TAGS_ERROR_MESSAGE,
},
data() { data() {
return { return {
selectedItems: [], selectedItems: [],
...@@ -67,6 +92,7 @@ export default { ...@@ -67,6 +92,7 @@ export default {
selectAllChecked: false, selectAllChecked: false,
modalDescription: null, modalDescription: null,
isDesktop: true, isDesktop: true,
deleteAlertType: false,
}; };
}, },
computed: { computed: {
...@@ -110,20 +136,40 @@ export default { ...@@ -110,20 +136,40 @@ export default {
this.requestTagsList({ pagination: { page }, params: this.$route.params.id }); this.requestTagsList({ pagination: { page }, params: this.$route.params.id });
}, },
}, },
deleteAlertConfig() {
const config = {
title: '',
message: '',
type: 'success',
};
if (this.deleteAlertType) {
[config.type] = this.deleteAlertType.split('_');
const defaultMessage = this.$options.alertMessages[this.deleteAlertType];
if (this.config.isAdmin && config.type === 'success') {
config.title = defaultMessage;
config.message = ADMIN_GARBAGE_COLLECTION_TIP;
} else {
config.message = defaultMessage;
}
}
return config;
},
}, },
methods: { methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']), ...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
setModalDescription(itemIndex = -1) { setModalDescription(itemIndex = -1) {
if (itemIndex === -1) { if (itemIndex === -1) {
this.modalDescription = { this.modalDescription = {
message: s__(`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`), message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length, item: this.itemsToBeDeleted.length,
}; };
} else { } else {
const { path } = this.tags[itemIndex]; const { path } = this.tags[itemIndex];
this.modalDescription = { this.modalDescription = {
message: s__(`ContainerRegistry|You are about to remove %{item}. Are you sure?`), message: REMOVE_TAG_CONFIRMATION_TEXT,
item: path, item: path,
}; };
} }
...@@ -179,19 +225,17 @@ export default { ...@@ -179,19 +225,17 @@ export default {
this.track('click_button'); this.track('click_button');
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
handleSingleDelete(itemToDelete) { handleSingleDelete(index) {
const itemToDelete = this.tags[index];
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
this.selectedItems = this.selectedItems.filter(i => i !== index);
return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id }) return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
.then(() => .then(() => {
this.$toast.show(DELETE_TAG_SUCCESS_MESSAGE, { this.deleteAlertType = 'success_tag';
type: 'success', })
}), .catch(() => {
) this.deleteAlertType = 'danger_tag';
.catch(() => });
this.$toast.show(DELETE_TAG_ERROR_MESSAGE, {
type: 'error',
}),
);
}, },
handleMultipleDelete() { handleMultipleDelete() {
const { itemsToBeDeleted } = this; const { itemsToBeDeleted } = this;
...@@ -202,24 +246,19 @@ export default { ...@@ -202,24 +246,19 @@ export default {
ids: itemsToBeDeleted.map(x => this.tags[x].name), ids: itemsToBeDeleted.map(x => this.tags[x].name),
params: this.$route.params.id, params: this.$route.params.id,
}) })
.then(() => .then(() => {
this.$toast.show(DELETE_TAGS_SUCCESS_MESSAGE, { this.deleteAlertType = 'success_tags';
type: 'success', })
}), .catch(() => {
) this.deleteAlertType = 'danger_tags';
.catch(() => });
this.$toast.show(DELETE_TAGS_ERROR_MESSAGE, {
type: 'error',
}),
);
}, },
onDeletionConfirmed() { onDeletionConfirmed() {
this.track('confirm_delete'); this.track('confirm_delete');
if (this.isMultiDelete) { if (this.isMultiDelete) {
this.handleMultipleDelete(); this.handleMultipleDelete();
} else { } else {
const index = this.itemsToBeDeleted[0]; this.handleSingleDelete(this.itemsToBeDeleted[0]);
this.handleSingleDelete(this.tags[index]);
} }
}, },
handleResize() { handleResize() {
...@@ -231,9 +270,24 @@ export default { ...@@ -231,9 +270,24 @@ export default {
<template> <template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element"> <div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
<gl-alert
v-if="deleteAlertType"
:variant="deleteAlertConfig.type"
:title="deleteAlertConfig.title"
class="my-2"
@dismiss="deleteAlertType = null"
>
<gl-sprintf :message="deleteAlertConfig.message">
<template #docLink="{content}">
<gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<div class="d-flex my-3 align-items-center"> <div class="d-flex my-3 align-items-center">
<h4> <h4>
<gl-sprintf :message="s__('ContainerRegistry|%{imageName} tags')"> <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName> <template #imageName>
{{ imageName }} {{ imageName }}
</template> </template>
...@@ -256,8 +310,8 @@ export default { ...@@ -256,8 +310,8 @@ export default {
:disabled="!selectedItems || selectedItems.length === 0" :disabled="!selectedItems || selectedItems.length === 0"
class="float-right" class="float-right"
variant="danger" variant="danger"
:title="s__('ContainerRegistry|Remove selected tags')" :title="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
:aria-label="s__('ContainerRegistry|Remove selected tags')" :aria-label="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
@click="deleteMultipleItems()" @click="deleteMultipleItems()"
> >
<gl-icon name="remove" /> <gl-icon name="remove" />
...@@ -306,8 +360,8 @@ export default { ...@@ -306,8 +360,8 @@ export default {
<template #cell(actions)="{index, item}"> <template #cell(actions)="{index, item}">
<gl-deprecated-button <gl-deprecated-button
ref="singleDeleteButton" ref="singleDeleteButton"
:title="s__('ContainerRegistry|Remove tag')" :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:aria-label="s__('ContainerRegistry|Remove tag')" :aria-label="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:disabled="!item.destroy_path" :disabled="!item.destroy_path"
variant="danger" variant="danger"
class="js-delete-registry float-right btn-inverted btn-border-color btn-icon" class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
...@@ -337,15 +391,9 @@ export default { ...@@ -337,15 +391,9 @@ export default {
</template> </template>
<gl-empty-state <gl-empty-state
v-else v-else
:title="s__('ContainerRegistry|This image has no active tags')" :title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
:svg-path="config.noContainersImage" :svg-path="config.noContainersImage"
:description=" :description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
)
"
class="mx-auto my-0" class="mx-auto my-0"
/> />
</template> </template>
......
<script> <script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; export default {};
import { mapState, mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
},
i18n: {
garbageCollectionTipText: s__(
'ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
),
},
computed: {
...mapState(['config']),
...mapGetters(['showGarbageCollection']),
},
methods: {
...mapActions(['setShowGarbageCollectionTip']),
},
};
</script> </script>
<template> <template>
<div> <div>
<gl-alert
v-if="showGarbageCollection"
variant="tip"
class="my-2"
@dismiss="setShowGarbageCollectionTip(false)"
>
<gl-sprintf :message="$options.i18n.garbageCollectionTipText">
<template #docLink="{content}">
<gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<transition name="slide"> <transition name="slide">
<router-view ref="router-view" /> <router-view ref="router-view" />
</transition> </transition>
......
...@@ -66,7 +66,7 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) = ...@@ -66,7 +66,7 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
dispatch('setShowGarbageCollectionTip', true); dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params }); return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .finally(() => {
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
}); });
}; };
...@@ -83,7 +83,7 @@ export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) ...@@ -83,7 +83,7 @@ export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params })
dispatch('setShowGarbageCollectionTip', true); dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params }); return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .finally(() => {
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
}); });
}; };
......
...@@ -15,7 +15,6 @@ class Group < Namespace ...@@ -15,7 +15,6 @@ class Group < Namespace
include WithUploads include WithUploads
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility include GroupAPICompatibility
include HasWiki
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
......
# frozen_string_literal: true
class GroupWiki < Wiki
alias_method :group, :container
override :storage
def storage
@storage ||= Storage::Hashed.new(container, prefix: Storage::Hashed::GROUP_REPOSITORY_PATH_PREFIX)
end
override :repository_storage
def repository_storage
# TODO: Add table to track storage
# https://gitlab.com/gitlab-org/gitlab/-/issues/207865
'default'
end
override :hashed_storage?
def hashed_storage?
true
end
override :disk_path
def disk_path(*args, &block)
storage.disk_path + '.wiki'
end
end
...@@ -86,9 +86,9 @@ ...@@ -86,9 +86,9 @@
.content-block.emoji-block.emoji-block-sticky .content-block.emoji-block.emoji-block-sticky
.row .row
.col-md-12.col-lg-6.js-noteable-awards .col-md-12.col-lg-4.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true = render 'award_emoji/awards_block', awardable: @issue, inline: true
.col-md-12.col-lg-6.new-branch-col .col-md-12.col-lg-8.new-branch-col
#js-vue-sort-issue-discussions #js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } } #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button? = render 'new_branch' if show_new_branch_button?
......
-# TODO: Remove after base URL is included in the model !30735 #static-site-editor{ data: @config.payload }
- data = @config.payload.merge({ base_url: namespace_project_show_sse_path })
#static-site-editor{ data: data }
---
title: Use alerts instead of toasts in Image Repository details
merge_request: 29685
author:
type: changed
---
title: Fix layout in issue view, on large screen some buttons were misaligned
merge_request: 30947
author: Michele (macno) Azzolari
type: fixed
---
title: Revert CODEOWNERS validation of Web requests in diff check
merge_request: 31087
author:
type: fixed
...@@ -153,7 +153,13 @@ used by the `review-deploy` and `review-stop` jobs. ...@@ -153,7 +153,13 @@ used by the `review-deploy` and `review-stop` jobs.
### Get access to the GCP Review Apps cluster ### Get access to the GCP Review Apps cluster
You need to [open an access request (internal link)](https://gitlab.com/gitlab-com/access-requests/issues/new) You need to [open an access request (internal link)](https://gitlab.com/gitlab-com/access-requests/issues/new)
for the `gcp-review-apps-sg` GCP group. for the `gcp-review-apps-sg` GCP group. In order to join a group, you must specify the desired GCP role in your access request.
The role is what will grant you specific permissions in order to engage with Review App containers.
Here are some permissions you may want to have, and the roles that grant them:
- `container.pods.getLogs` - Required to [retrieve pod logs](#dig-into-a-pods-logs). Granted by [Viewer (`roles/viewer`)](https://cloud.google.com/iam/docs/understanding-roles#kubernetes-engine-roles).
- `container.pods.exec` - Required to [run a Rails console](#run-a-rails-console). Granted by [Kubernetes Engine Developer (`roles/container.developer`)](https://cloud.google.com/iam/docs/understanding-roles#kubernetes-engine-roles).
### Log into my Review App ### Log into my Review App
...@@ -175,7 +181,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`. ...@@ -175,7 +181,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`.
### Run a Rails console ### Run a Rails console
1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) first. 1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.exec` permission first.
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps), 1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps),
e.g. `review-qa-raise-e-12chm0`. e.g. `review-qa-raise-e-12chm0`.
1. Find and open the `task-runner` Deployment, e.g. `review-qa-raise-e-12chm0-task-runner`. 1. Find and open the `task-runner` Deployment, e.g. `review-qa-raise-e-12chm0-task-runner`.
...@@ -191,7 +197,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`. ...@@ -191,7 +197,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`.
### Dig into a Pod's logs ### Dig into a Pod's logs
1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) first. 1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.getLogs` permission first.
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps), 1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps),
e.g. `review-qa-raise-e-12chm0`. e.g. `review-qa-raise-e-12chm0`.
1. Find and open the `migrations` Deployment, e.g. 1. Find and open the `migrations` Deployment, e.g.
......
...@@ -34,14 +34,16 @@ module Gitlab ...@@ -34,14 +34,16 @@ module Gitlab
def init_metrics def init_metrics
metrics = { metrics = {
file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels), file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels),
memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels), memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used (RSS)', labels),
process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'), process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'), process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels), process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used (RSS)', labels),
process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'), process_unique_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :unique_memory_bytes), 'Memory used (USS)', labels),
sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels), process_proportional_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :proportional_memory_bytes), 'Memory used (PSS)', labels),
gc_duration_seconds: ::Gitlab::Metrics.histogram(with_prefix(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS) process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
gc_duration_seconds: ::Gitlab::Metrics.histogram(with_prefix(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS)
} }
GC.stat.keys.each do |key| GC.stat.keys.each do |key|
...@@ -85,10 +87,15 @@ module Gitlab ...@@ -85,10 +87,15 @@ module Gitlab
end end
def set_memory_usage_metrics def set_memory_usage_metrics
memory_usage = System.memory_usage memory_rss = System.memory_usage
metrics[:memory_bytes].set(labels, memory_rss)
metrics[:memory_bytes].set(labels, memory_usage) metrics[:process_resident_memory_bytes].set(labels, memory_rss)
metrics[:process_resident_memory_bytes].set(labels, memory_usage)
if Feature.enabled?(:collect_memory_uss_pss)
memory_uss_pss = System.memory_usage_uss_pss
metrics[:process_unique_memory_bytes].set(labels, memory_uss_pss[:uss])
metrics[:process_proportional_memory_bytes].set(labels, memory_uss_pss[:pss])
end
end end
end end
end end
......
...@@ -7,47 +7,37 @@ module Gitlab ...@@ -7,47 +7,37 @@ module Gitlab
# This module relies on the /proc filesystem being available. If /proc is # This module relies on the /proc filesystem being available. If /proc is
# not available the methods of this module will be stubbed. # not available the methods of this module will be stubbed.
module System module System
if File.exist?('/proc') PROC_STATUS_PATH = '/proc/self/status'
# Returns the current process' memory usage in bytes. PROC_SMAPS_ROLLUP_PATH = '/proc/self/smaps_rollup'
def self.memory_usage PROC_LIMITS_PATH = '/proc/self/limits'
mem = 0 PROC_FD_GLOB = '/proc/self/fd/*'
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
PRIVATE_PAGES_PATTERN = /^(Private_Clean|Private_Dirty|Private_Hugetlb):\s+(?<value>\d+)/.freeze
if match && match[1] PSS_PATTERN = /^Pss:\s+(?<value>\d+)/.freeze
mem = match[1].to_f * 1024 RSS_PATTERN = /VmRSS:\s+(?<value>\d+)/.freeze
end MAX_OPEN_FILES_PATTERN = /Max open files\s*(?<value>\d+)/.freeze
mem # Returns the current process' RSS (resident set size) in bytes.
end def self.memory_usage
sum_matches(PROC_STATUS_PATH, rss: RSS_PATTERN)[:rss].kilobytes
def self.file_descriptor_count end
Dir.glob('/proc/self/fd/*').length
end
def self.max_open_file_descriptors
match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
return unless match && match[1]
match[1].to_i # Returns the current process' USS/PSS (unique/proportional set size) in bytes.
end def self.memory_usage_uss_pss
else sum_matches(PROC_SMAPS_ROLLUP_PATH, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN)
def self.memory_usage .transform_values(&:kilobytes)
0.0 end
end
def self.file_descriptor_count def self.file_descriptor_count
0 Dir.glob(PROC_FD_GLOB).length
end end
def self.max_open_file_descriptors def self.max_open_file_descriptors
0 sum_matches(PROC_LIMITS_PATH, max_fds: MAX_OPEN_FILES_PATTERN)[:max_fds]
end
end end
def self.cpu_time def self.cpu_time
Process Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
end end
# Returns the current real time in a given precision. # Returns the current real time in a given precision.
...@@ -78,6 +68,27 @@ module Gitlab ...@@ -78,6 +68,27 @@ module Gitlab
end_time - start_time end_time - start_time
end end
# Given a path to a file in /proc and a hash of (metric, pattern) pairs,
# sums up all values found for those patterns under the respective metric.
def self.sum_matches(proc_file, **patterns)
results = patterns.transform_values { 0 }
begin
File.foreach(proc_file) do |line|
patterns.each do |metric, pattern|
match = line.match(pattern)
value = match&.named_captures&.fetch('value', 0)
results[metric] += value.to_i
end
end
rescue Errno::ENOENT
# This means the procfile we're reading from did not exist;
# this is safe to ignore, since we initialize each metric to 0
end
results
end
end end
end end
end end
...@@ -22,7 +22,8 @@ module Gitlab ...@@ -22,7 +22,8 @@ module Gitlab
project: project.path, project: project.path,
namespace: project.namespace.path, namespace: project.namespace.path,
return_url: return_url, return_url: return_url,
is_supported_content: supported_content?.to_s is_supported_content: supported_content?.to_s,
base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path)
} }
end end
...@@ -47,6 +48,10 @@ module Gitlab ...@@ -47,6 +48,10 @@ module Gitlab
def file_exists? def file_exists?
commit_id.present? && repository.blob_at(commit_id, file_path).present? commit_id.present? && repository.blob_at(commit_id, file_path).present?
end end
def full_path
"#{ref}/#{file_path}"
end
end end
end end
end end
...@@ -72,13 +72,8 @@ module Gitlab ...@@ -72,13 +72,8 @@ module Gitlab
end end
def wiki_url(object, **options) def wiki_url(object, **options)
case object.container if object.container.is_a?(Project)
when Project
instance.project_wiki_url(object.container, Wiki::HOMEPAGE, **options) instance.project_wiki_url(object.container, Wiki::HOMEPAGE, **options)
when Group
# TODO: Use the new route for group wikis once we add it.
# https://gitlab.com/gitlab-org/gitlab/-/issues/211360
instance.group_canonical_url(object.container, **options) + "/-/wikis/#{Wiki::HOMEPAGE}"
else else
raise NotImplementedError.new("No URL builder defined for #{object.inspect}") raise NotImplementedError.new("No URL builder defined for #{object.inspect}")
end end
......
...@@ -5713,6 +5713,9 @@ msgstr "" ...@@ -5713,6 +5713,9 @@ msgstr ""
msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}" msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
msgstr ""
msgid "ContainerRegistry|Remove repository" msgid "ContainerRegistry|Remove repository"
msgstr "" msgstr ""
...@@ -5727,19 +5730,19 @@ msgstr[1] "" ...@@ -5727,19 +5730,19 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled" msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while deleting the tag." msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while deleting the tags." msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the expiration policy." msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the repository list." msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the tags list." msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again." msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
...@@ -5751,16 +5754,16 @@ msgstr "" ...@@ -5751,16 +5754,16 @@ msgstr ""
msgid "ContainerRegistry|Tag" msgid "ContainerRegistry|Tag"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tag deleted successfully"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy" msgid "ContainerRegistry|Tag expiration policy"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:" msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags deleted successfully" msgid "ContainerRegistry|Tag successfully marked for deletion."
msgstr ""
msgid "ContainerRegistry|Tags successfully marked for deletion."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}" msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
...@@ -5793,9 +5796,6 @@ msgstr "" ...@@ -5793,9 +5796,6 @@ msgstr ""
msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again." msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr "" msgstr ""
msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
msgstr ""
msgid "ContainerRegistry|This image has no active tags" msgid "ContainerRegistry|This image has no active tags"
msgstr "" msgstr ""
......
...@@ -51,11 +51,5 @@ FactoryBot.define do ...@@ -51,11 +51,5 @@ FactoryBot.define do
trait :owner_subgroup_creation_only do trait :owner_subgroup_creation_only do
subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS} subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS}
end end
trait :wiki_repo do
after(:create) do |group|
raise 'Failed to create wiki repository!' unless group.create_wiki
end
end
end end
end end
...@@ -17,9 +17,5 @@ FactoryBot.define do ...@@ -17,9 +17,5 @@ FactoryBot.define do
container { project } container { project }
end end
factory :group_wiki do
container { association(:group, :wiki_repo) }
end
end end
end end
...@@ -31,8 +31,6 @@ describe 'Groups > Members > Leave group' do ...@@ -31,8 +31,6 @@ describe 'Groups > Members > Leave group' do
page.accept_confirm page.accept_confirm
expect(find('.flash-notice')).to have_content "You left the \"#{group.full_name}\" group"
expect(page).to have_content left_group_message(group)
expect(current_path).to eq(dashboard_groups_path) expect(current_path).to eq(dashboard_groups_path)
expect(group.users).not_to include(user) expect(group.users).not_to include(user)
end end
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui'; import { GlTable, GlPagination, GlSkeletonLoader, GlAlert, GlLink } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue'; import component from '~/registry/explorer/pages/details.vue';
import store from '~/registry/explorer/stores/'; import { createStore } from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { SET_MAIN_LOADING, SET_INITIAL_STATE } from '~/registry/explorer/stores/mutation_types/';
import { import {
DELETE_TAG_SUCCESS_MESSAGE, DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE, DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE, DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE, DELETE_TAGS_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
import { tagsListResponse } from '../mock_data'; import { tagsListResponse } from '../mock_data';
import { GlModal } from '../stubs'; import { GlModal } from '../stubs';
...@@ -18,6 +19,7 @@ import { $toast } from '../../shared/mocks'; ...@@ -18,6 +19,7 @@ import { $toast } from '../../shared/mocks';
describe('Details Page', () => { describe('Details Page', () => {
let wrapper; let wrapper;
let dispatchSpy; let dispatchSpy;
let store;
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(GlModal);
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
...@@ -30,6 +32,7 @@ describe('Details Page', () => { ...@@ -30,6 +32,7 @@ describe('Details Page', () => {
const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox'); const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox');
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked')); const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column'); const findFirsTagColumn = () => wrapper.find('.js-tag-column');
const findAlert = () => wrapper.find(GlAlert);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' })); const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
...@@ -55,6 +58,7 @@ describe('Details Page', () => { ...@@ -55,6 +58,7 @@ describe('Details Page', () => {
}; };
beforeEach(() => { beforeEach(() => {
store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch'); dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveTagsListSuccess', tagsListResponse); store.dispatch('receiveTagsListSuccess', tagsListResponse);
jest.spyOn(Tracking, 'event'); jest.spyOn(Tracking, 'event');
...@@ -62,6 +66,7 @@ describe('Details Page', () => { ...@@ -62,6 +66,7 @@ describe('Details Page', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('when isLoading is true', () => { describe('when isLoading is true', () => {
...@@ -328,25 +333,9 @@ describe('Details Page', () => { ...@@ -328,25 +333,9 @@ describe('Details Page', () => {
}); });
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(wrapper.vm.selectedItems).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0); expect(findCheckedCheckboxes()).toHaveLength(0);
}); });
it('show success toast on successful delete', () => {
return wrapper.vm.handleSingleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAG_SUCCESS_MESSAGE, {
type: 'success',
});
});
});
it('show error toast on erred delete', () => {
dispatchSpy.mockRejectedValue();
return wrapper.vm.handleSingleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAG_ERROR_MESSAGE, {
type: 'error',
});
});
});
}); });
describe('when multiple elements are selected', () => { describe('when multiple elements are selected', () => {
...@@ -365,23 +354,6 @@ describe('Details Page', () => { ...@@ -365,23 +354,6 @@ describe('Details Page', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0); expect(findCheckedCheckboxes()).toHaveLength(0);
}); });
it('show success toast on successful delete', () => {
return wrapper.vm.handleMultipleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAGS_SUCCESS_MESSAGE, {
type: 'success',
});
});
});
it('show error toast on erred delete', () => {
dispatchSpy.mockRejectedValue();
return wrapper.vm.handleMultipleDelete(0).then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAGS_ERROR_MESSAGE, {
type: 'error',
});
});
});
}); });
}); });
...@@ -395,4 +367,108 @@ describe('Details Page', () => { ...@@ -395,4 +367,108 @@ describe('Details Page', () => {
}); });
}); });
}); });
describe('Delete alert', () => {
const config = {
garbageCollectionHelpPagePath: 'foo',
};
describe('when the user is an admin', () => {
beforeEach(() => {
store.commit(SET_INITIAL_STATE, { ...config, isAdmin: true });
});
afterEach(() => {
store.commit(SET_INITIAL_STATE, config);
});
describe.each`
deleteType | successTitle | errorTitle
${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
`('behaves correctly on $deleteType', ({ deleteType, successTitle, errorTitle }) => {
describe('when delete is successful', () => {
beforeEach(() => {
dispatchSpy.mockResolvedValue();
mountComponent();
return wrapper.vm[deleteType]('foo');
});
it('alert exists', () => {
expect(findAlert().exists()).toBe(true);
});
it('alert body contains admin tip', () => {
expect(
findAlert()
.text()
.replace(/\s\s+/gm, ' '),
).toBe(ADMIN_GARBAGE_COLLECTION_TIP.replace(/%{\w+}/gm, ''));
});
it('alert body contains link', () => {
const alertLink = findAlert().find(GlLink);
expect(alertLink.exists()).toBe(true);
expect(alertLink.attributes('href')).toBe(config.garbageCollectionHelpPagePath);
});
it('alert title is appropriate', () => {
expect(findAlert().attributes('title')).toBe(successTitle);
});
});
describe('when delete is not successful', () => {
beforeEach(() => {
mountComponent();
dispatchSpy.mockRejectedValue();
return wrapper.vm[deleteType]('foo');
});
it('alert exist and text is appropriate', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errorTitle);
});
});
});
});
describe.each`
deleteType | successTitle | errorTitle
${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
`(
'when the user is not an admin alert behaves correctly on $deleteType',
({ deleteType, successTitle, errorTitle }) => {
beforeEach(() => {
store.commit('SET_INITIAL_STATE', { ...config });
});
describe('when delete is successful', () => {
beforeEach(() => {
dispatchSpy.mockResolvedValue();
mountComponent();
return wrapper.vm[deleteType]('foo');
});
it('alert exist and text is appropriate', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(successTitle);
});
});
describe('when delete is not successful', () => {
beforeEach(() => {
mountComponent();
dispatchSpy.mockRejectedValue();
return wrapper.vm[deleteType]('foo');
});
it('alert exist and text is appropriate', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errorTitle);
});
});
},
);
});
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import component from '~/registry/explorer/pages/index.vue'; import component from '~/registry/explorer/pages/index.vue';
import store from '~/registry/explorer/stores/'; import store from '~/registry/explorer/stores/';
describe('List Page', () => { describe('List Page', () => {
let wrapper; let wrapper;
let dispatchSpy;
const findRouterView = () => wrapper.find({ ref: 'router-view' }); const findRouterView = () => wrapper.find({ ref: 'router-view' });
const findAlert = () => wrapper.find(GlAlert);
const findLink = () => wrapper.find(GlLink);
const mountComponent = () => { const mountComponent = () => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
store, store,
stubs: { stubs: {
RouterView: true, RouterView: true,
GlSprintf,
}, },
}); });
}; };
beforeEach(() => { beforeEach(() => {
dispatchSpy = jest.spyOn(store, 'dispatch');
mountComponent(); mountComponent();
}); });
it('has a router view', () => { it('has a router view', () => {
expect(findRouterView().exists()).toBe(true); expect(findRouterView().exists()).toBe(true);
}); });
describe('garbageCollectionTip alert', () => {
beforeEach(() => {
store.dispatch('setInitialState', { isAdmin: true, garbageCollectionHelpPagePath: 'foo' });
store.dispatch('setShowGarbageCollectionTip', true);
});
afterEach(() => {
store.dispatch('setInitialState', {});
store.dispatch('setShowGarbageCollectionTip', false);
});
it('is visible when the user is an admin and the user performed a delete action', () => {
expect(findAlert().exists()).toBe(true);
});
it('on dismiss disappears ', () => {
findAlert().vm.$emit('dismiss');
expect(dispatchSpy).toHaveBeenCalledWith('setShowGarbageCollectionTip', false);
return wrapper.vm.$nextTick().then(() => {
expect(findAlert().exists()).toBe(false);
});
});
it('contains a link to the docs', () => {
const link = findLink();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(store.state.config.garbageCollectionHelpPagePath);
});
});
}); });
...@@ -191,7 +191,10 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -191,7 +191,10 @@ describe('Actions RegistryExplorer Store', () => {
{ {
tagsPagination: {}, tagsPagination: {},
}, },
[{ type: types.SET_MAIN_LOADING, payload: true }], [
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[ [
{ {
type: 'setShowGarbageCollectionTip', type: 'setShowGarbageCollectionTip',
...@@ -220,8 +223,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -220,8 +223,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[], [],
done, ).catch(() => done());
);
}); });
}); });
...@@ -241,7 +243,10 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -241,7 +243,10 @@ describe('Actions RegistryExplorer Store', () => {
{ {
tagsPagination: {}, tagsPagination: {},
}, },
[{ type: types.SET_MAIN_LOADING, payload: true }], [
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[ [
{ {
type: 'setShowGarbageCollectionTip', type: 'setShowGarbageCollectionTip',
...@@ -273,8 +278,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -273,8 +278,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[], [],
done, ).catch(() => done());
);
}); });
}); });
...@@ -311,9 +315,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -311,9 +315,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false }, { type: types.SET_MAIN_LOADING, payload: false },
], ],
[], [],
).catch(() => { ).catch(() => done());
done();
});
}); });
}); });
}); });
...@@ -19,20 +19,19 @@ describe Gitlab::Metrics::Samplers::RubySampler do ...@@ -19,20 +19,19 @@ describe Gitlab::Metrics::Samplers::RubySampler do
end end
describe '#sample' do describe '#sample' do
it 'samples various statistics' do it 'adds a metric containing the process resident memory bytes' do
expect(Gitlab::Metrics::System).to receive(:cpu_time) expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
expect(Gitlab::Metrics::System).to receive(:memory_usage) expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
expect(sampler).to receive(:sample_gc)
sampler.sample sampler.sample
end end
it 'adds a metric containing the process resident memory bytes' do it 'adds a metric containing the process unique and proportional memory bytes' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000) expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return(uss: 9000, pss: 10_000)
expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000) expect(sampler.metrics[:process_unique_memory_bytes]).to receive(:set).with({}, 9000)
expect(sampler.metrics[:process_proportional_memory_bytes]).to receive(:set).with({}, 10_000)
sampler.sample sampler.sample
end end
......
...@@ -3,33 +3,122 @@ ...@@ -3,33 +3,122 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Metrics::System do describe Gitlab::Metrics::System do
if File.exist?('/proc') context 'when /proc files exist' do
# Fixtures pulled from:
# Linux carbon 5.3.0-7648-generic #41~1586789791~19.10~9593806-Ubuntu SMP Mon Apr 13 17:50:40 UTC x86_64 x86_64 x86_64 GNU/Linux
let(:proc_status) do
# most rows omitted for brevity
<<~SNIP
Name: less
VmHWM: 2468 kB
VmRSS: 2468 kB
RssAnon: 260 kB
SNIP
end
let(:proc_smaps_rollup) do
# full snapshot
<<~SNIP
Rss: 2564 kB
Pss: 503 kB
Pss_Anon: 312 kB
Pss_File: 191 kB
Pss_Shmem: 0 kB
Shared_Clean: 2100 kB
Shared_Dirty: 0 kB
Private_Clean: 152 kB
Private_Dirty: 312 kB
Referenced: 2564 kB
Anonymous: 312 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
SNIP
end
let(:proc_limits) do
# full snapshot
<<~SNIP
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 126519 126519 processes
Max open files 1024 1048576 files
Max locked memory 67108864 67108864 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 126519 126519 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us
SNIP
end
describe '.memory_usage' do describe '.memory_usage' do
it "returns the process' memory usage in bytes" do it "returns the process' resident set size (RSS) in bytes" do
expect(described_class.memory_usage).to be > 0 mock_existing_proc_file('/proc/self/status', proc_status)
expect(described_class.memory_usage).to eq(2527232)
end end
end end
describe '.file_descriptor_count' do describe '.file_descriptor_count' do
it 'returns the amount of open file descriptors' do it 'returns the amount of open file descriptors' do
expect(described_class.file_descriptor_count).to be > 0 expect(Dir).to receive(:glob).and_return(['/some/path', '/some/other/path'])
expect(described_class.file_descriptor_count).to eq(2)
end end
end end
describe '.max_open_file_descriptors' do describe '.max_open_file_descriptors' do
it 'returns the max allowed open file descriptors' do it 'returns the max allowed open file descriptors' do
expect(described_class.max_open_file_descriptors).to be > 0 mock_existing_proc_file('/proc/self/limits', proc_limits)
expect(described_class.max_open_file_descriptors).to eq(1024)
end
end
describe '.memory_usage_uss_pss' do
it "returns the process' unique and porportional set size (USS/PSS) in bytes" do
mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup)
# (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024
expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072)
end end
end end
else end
context 'when /proc files do not exist' do
before do
mock_missing_proc_file
end
describe '.memory_usage' do describe '.memory_usage' do
it 'returns 0.0' do it 'returns 0' do
expect(described_class.memory_usage).to eq(0.0) expect(described_class.memory_usage).to eq(0)
end
end
describe '.memory_usage_uss_pss' do
it "returns 0 for all components" do
expect(described_class.memory_usage_uss_pss).to eq(uss: 0, pss: 0)
end end
end end
describe '.file_descriptor_count' do describe '.file_descriptor_count' do
it 'returns 0' do it 'returns 0' do
expect(Dir).to receive(:glob).and_return([])
expect(described_class.file_descriptor_count).to eq(0) expect(described_class.file_descriptor_count).to eq(0)
end end
end end
...@@ -98,4 +187,12 @@ describe Gitlab::Metrics::System do ...@@ -98,4 +187,12 @@ describe Gitlab::Metrics::System do
expect(described_class.thread_cpu_duration(start_time)).to be_nil expect(described_class.thread_cpu_duration(start_time)).to be_nil
end end
end end
def mock_existing_proc_file(path, content)
allow(File).to receive(:foreach).with(path) { |_path, &block| content.each_line(&block) }
end
def mock_missing_proc_file
allow(File).to receive(:foreach).and_raise(Errno::ENOENT)
end
end end
...@@ -10,7 +10,6 @@ describe Gitlab::RepositoryUrlBuilder do ...@@ -10,7 +10,6 @@ describe Gitlab::RepositoryUrlBuilder do
:project | ->(project) { project.full_path } :project | ->(project) { project.full_path }
:project_snippet | ->(snippet) { "#{snippet.project.full_path}/snippets/#{snippet.id}" } :project_snippet | ->(snippet) { "#{snippet.project.full_path}/snippets/#{snippet.id}" }
:project_wiki | ->(wiki) { "#{wiki.container.full_path}.wiki" } :project_wiki | ->(wiki) { "#{wiki.container.full_path}.wiki" }
:group_wiki | ->(wiki) { "#{wiki.container.full_path}.wiki" }
:personal_snippet | ->(snippet) { "snippets/#{snippet.id}" } :personal_snippet | ->(snippet) { "snippets/#{snippet.id}" }
end end
......
...@@ -5,9 +5,10 @@ require 'spec_helper' ...@@ -5,9 +5,10 @@ require 'spec_helper'
describe Gitlab::StaticSiteEditor::Config do describe Gitlab::StaticSiteEditor::Config do
subject(:config) { described_class.new(repository, ref, file_path, return_url) } subject(:config) { described_class.new(repository, ref, file_path, return_url) }
let(:project) { create(:project, :public, :repository, name: 'project', namespace: namespace) } let_it_be(:namespace) { create(:namespace, name: 'namespace') }
let(:namespace) { create(:namespace, name: 'namespace') } let_it_be(:project) { create(:project, :public, :repository, name: 'project', namespace: namespace) }
let(:repository) { project.repository } let_it_be(:repository) { project.repository }
let(:ref) { 'master' } let(:ref) { 'master' }
let(:file_path) { 'README.md' } let(:file_path) { 'README.md' }
let(:return_url) { 'http://example.com' } let(:return_url) { 'http://example.com' }
...@@ -24,10 +25,17 @@ describe Gitlab::StaticSiteEditor::Config do ...@@ -24,10 +25,17 @@ describe Gitlab::StaticSiteEditor::Config do
project: 'project', project: 'project',
project_id: project.id, project_id: project.id,
return_url: 'http://example.com', return_url: 'http://example.com',
is_supported_content: 'true' is_supported_content: 'true',
base_url: '/namespace/project/-/sse/master%2FREADME.md'
) )
end end
context 'when file path is nested' do
let(:file_path) { 'lib/README.md' }
it { is_expected.to include(base_url: '/namespace/project/-/sse/master%2Flib%2FREADME.md') }
end
context 'when branch is not master' do context 'when branch is not master' do
let(:ref) { 'my-branch' } let(:ref) { 'my-branch' }
...@@ -53,7 +61,7 @@ describe Gitlab::StaticSiteEditor::Config do ...@@ -53,7 +61,7 @@ describe Gitlab::StaticSiteEditor::Config do
end end
context 'when repository is empty' do context 'when repository is empty' do
let(:project) { create(:project_empty_repo) } let(:repository) { create(:project_empty_repo).repository }
it { is_expected.to include(is_supported_content: 'false') } it { is_expected.to include(is_supported_content: 'false') }
end end
......
...@@ -28,7 +28,6 @@ describe Gitlab::UrlBuilder do ...@@ -28,7 +28,6 @@ describe Gitlab::UrlBuilder do
:group | ->(group) { "/groups/#{group.full_path}" } :group | ->(group) { "/groups/#{group.full_path}" }
:group_milestone | ->(milestone) { "/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}" } :group_milestone | ->(milestone) { "/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}" }
:group_wiki | ->(wiki) { "/groups/#{wiki.container.full_path}/-/wikis/home" }
:user | ->(user) { "/#{user.full_path}" } :user | ->(user) { "/#{user.full_path}" }
:personal_snippet | ->(snippet) { "/snippets/#{snippet.id}" } :personal_snippet | ->(snippet) { "/snippets/#{snippet.id}" }
......
...@@ -27,11 +27,6 @@ describe Group do ...@@ -27,11 +27,6 @@ describe Group do
it { is_expected.to have_many(:milestones) } it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:sprints) } it { is_expected.to have_many(:sprints) }
it_behaves_like 'model with wiki' do
let(:container) { create(:group, :nested, :wiki_repo) }
let(:container_without_wiki) { create(:group, :nested) }
end
describe '#members & #requesters' do describe '#members & #requesters' do
let(:requester) { create(:user) } let(:requester) { create(:user) }
let(:developer) { create(:user) } let(:developer) { create(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
describe GroupWiki do
it_behaves_like 'wiki model' do
let(:wiki_container) { create(:group, :wiki_repo) }
let(:wiki_container_without_repo) { create(:group) }
before do
wiki_container.add_owner(user)
end
describe '#storage' do
it 'uses the group repository prefix' do
expect(subject.storage.base_dir).to start_with('@groups/')
end
end
describe '#repository_storage' do
it 'returns the default storage' do
expect(subject.repository_storage).to eq('default')
end
end
describe '#hashed_storage?' do
it 'returns true' do
expect(subject.hashed_storage?).to be(true)
end
end
describe '#disk_path' do
it 'returns the repository storage path' do
expect(subject.disk_path).to eq("#{subject.storage.disk_path}.wiki")
end
end
end
end
...@@ -150,15 +150,7 @@ describe WikiPage do ...@@ -150,15 +150,7 @@ describe WikiPage do
enable_front_matter_for(container) enable_front_matter_for(container)
end end
context 'with a project container' do it_behaves_like 'a page with front-matter'
it_behaves_like 'a page with front-matter'
end
context 'with a group container' do
let(:container) { create(:group) }
it_behaves_like 'a page with front-matter'
end
end end
end end
end end
...@@ -512,15 +504,7 @@ describe WikiPage do ...@@ -512,15 +504,7 @@ describe WikiPage do
enable_front_matter_for(container) enable_front_matter_for(container)
end end
context 'with a project container' do it_behaves_like 'able to update front-matter'
it_behaves_like 'able to update front-matter'
end
context 'with a group container' do
let(:container) { create(:group) }
it_behaves_like 'able to update front-matter'
end
end end
end end
...@@ -826,22 +810,13 @@ describe WikiPage do ...@@ -826,22 +810,13 @@ describe WikiPage do
expect(subject).not_to eq(other_page) expect(subject).not_to eq(other_page)
end end
it 'returns false for page with the same slug on a different container of the same type' do it 'returns false for page with the same slug on a different container' do
other_page = create(:wiki_page, title: existing_page.slug) other_page = create(:wiki_page, title: existing_page.slug)
expect(subject.slug).to eq(other_page.slug) expect(subject.slug).to eq(other_page.slug)
expect(subject.container).not_to eq(other_page.container) expect(subject.container).not_to eq(other_page.container)
expect(subject).not_to eq(other_page) expect(subject).not_to eq(other_page)
end end
it 'returns false for page with the same slug on a different container type' do
group = create(:group, name: container.name)
other_page = create(:wiki_page, title: existing_page.slug, container: group)
expect(subject.slug).to eq(other_page.slug)
expect(subject.container).not_to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
end end
describe '#last_commit_sha' do describe '#last_commit_sha' do
......
...@@ -782,10 +782,10 @@ ...@@ -782,10 +782,10 @@
eslint-plugin-vue "^6.2.1" eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0" vue-eslint-parser "^7.0.0"
"@gitlab/svgs@1.123.0": "@gitlab/svgs@1.125.0":
version "1.123.0" version "1.125.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.123.0.tgz#465946ae7afc486d6769dc38685f71747fa2fec7" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.125.0.tgz#59c667dae8f7e4c80b482f5f6cc35367c016387b"
integrity sha512-lBTNnh7sEgUX3LVj6tEis9dcDDc5gKhCSUInGzswZVy9KeDAXbY850pKGPRKg/O1nVDPIe9yh7ieieWy25bkuQ== integrity sha512-MKfFYa8f+9P2tJ/JN/E9oDBSSo/gRz2zuGui4XHQPoaw/DkIMn7EyAzeSpRgbgs1LgMcEqqKsIEx+spCga3jsQ==
"@gitlab/ui@13.9.0": "@gitlab/ui@13.9.0":
version "13.9.0" version "13.9.0"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册