Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
251d3d2b23
commit
6092dcc437
171 changed files with 1538 additions and 1247 deletions
|
@ -18,11 +18,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
diffFile: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
line: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
|
|
@ -1,114 +1,43 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import { sprintf, n__ } from '~/locale';
|
||||
import DraftsCount from './drafts_count.vue';
|
||||
import PublishButton from './publish_button.vue';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
|
||||
import PreviewItem from './preview_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
DraftsCount,
|
||||
PublishButton,
|
||||
PreviewItem,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isNotesFetched']),
|
||||
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
|
||||
...mapState('batchComments', ['showPreviewDropdown']),
|
||||
dropdownTitle() {
|
||||
return sprintf(
|
||||
n__('%{count} pending comment', '%{count} pending comments', this.draftsCount),
|
||||
{ count: this.draftsCount },
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showPreviewDropdown() {
|
||||
if (this.showPreviewDropdown && this.$refs.dropdown) {
|
||||
this.$nextTick(() => this.$refs.dropdown.$el.focus());
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.onClickDocument);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.onClickDocument);
|
||||
},
|
||||
methods: {
|
||||
...mapActions('batchComments', ['toggleReviewDropdown']),
|
||||
...mapActions('batchComments', ['scrollToDraft']),
|
||||
isLast(index) {
|
||||
return index === this.sortedDrafts.length - 1;
|
||||
},
|
||||
onClickDocument({ target }) {
|
||||
if (
|
||||
this.showPreviewDropdown &&
|
||||
!target.closest('.review-preview-dropdown, .js-publish-draft-button')
|
||||
) {
|
||||
this.toggleReviewDropdown();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dropdown float-right review-preview-dropdown"
|
||||
:class="{
|
||||
show: showPreviewDropdown,
|
||||
}"
|
||||
<gl-dropdown
|
||||
:header-text="n__('%d pending comment', '%d pending comments', draftsCount)"
|
||||
dropup
|
||||
toggle-class="qa-review-preview-toggle"
|
||||
>
|
||||
<gl-button
|
||||
ref="dropdown"
|
||||
type="button"
|
||||
category="primary"
|
||||
variant="success"
|
||||
class="review-preview-dropdown-toggle qa-review-preview-toggle"
|
||||
@click="toggleReviewDropdown"
|
||||
<template #button-content>
|
||||
{{ __('Pending comments') }}
|
||||
<gl-icon class="dropdown-chevron" name="chevron-up" />
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
v-for="(draft, index) in sortedDrafts"
|
||||
:key="draft.id"
|
||||
@click="scrollToDraft(draft)"
|
||||
>
|
||||
{{ __('Finish review') }}
|
||||
<drafts-count />
|
||||
<gl-icon name="angle-up" />
|
||||
</gl-button>
|
||||
<div
|
||||
class="dropdown-menu dropdown-menu-large dropdown-menu-right dropdown-open-top"
|
||||
:class="{
|
||||
show: showPreviewDropdown,
|
||||
}"
|
||||
>
|
||||
<div class="dropdown-title gl-display-flex gl-align-items-center">
|
||||
<span class="gl-ml-auto">{{ dropdownTitle }}</span>
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
type="button"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="dropdown-title-button gl-ml-auto gl-p-0!"
|
||||
icon="close"
|
||||
@click="toggleReviewDropdown"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown-content">
|
||||
<ul v-if="isNotesFetched">
|
||||
<li v-for="(draft, index) in sortedDrafts" :key="draft.id">
|
||||
<preview-item :draft="draft" :is-last="isLast(index)" />
|
||||
</li>
|
||||
</ul>
|
||||
<gl-loading-icon v-else size="lg" class="gl-mt-3 gl-mb-3" />
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<publish-button
|
||||
:show-count="false"
|
||||
:should-publish="true"
|
||||
:label="__('Submit review')"
|
||||
class="float-right gl-mr-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<preview-item :draft="draft" :is-last="isLast(index)" />
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { GlSprintf, GlIcon } from '@gitlab/ui';
|
||||
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
|
@ -78,7 +78,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('batchComments', ['scrollToDraft']),
|
||||
getLineClasses(lineNumber) {
|
||||
return getLineClasses(lineNumber);
|
||||
},
|
||||
|
@ -88,17 +87,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="review-preview-item menu-item"
|
||||
:class="[
|
||||
componentClasses,
|
||||
{
|
||||
'is-last': isLast,
|
||||
},
|
||||
]"
|
||||
@click="scrollToDraft(draft)"
|
||||
>
|
||||
<span>
|
||||
<span class="review-preview-item-header">
|
||||
<gl-icon class="flex-shrink-0" :name="iconName" />
|
||||
<span
|
||||
|
@ -139,5 +128,5 @@ export default {
|
|||
>
|
||||
<gl-icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }}
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import DraftsCount from './drafts_count.vue';
|
||||
|
||||
export default {
|
||||
|
@ -15,11 +14,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Finish review'),
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -30,22 +24,14 @@ export default {
|
|||
required: false,
|
||||
default: 'success',
|
||||
},
|
||||
shouldPublish: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState('batchComments', ['isPublishing']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('batchComments', ['publishReview', 'toggleReviewDropdown']),
|
||||
...mapActions('batchComments', ['publishReview']),
|
||||
onClick() {
|
||||
if (this.shouldPublish) {
|
||||
this.publishReview();
|
||||
} else {
|
||||
this.toggleReviewDropdown();
|
||||
}
|
||||
this.publishReview();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -59,7 +45,7 @@ export default {
|
|||
:variant="variant"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ label }}
|
||||
{{ __('Submit review') }}
|
||||
<drafts-count v-if="showCount" />
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import PreviewDropdown from './preview_dropdown.vue';
|
||||
import PublishButton from './publish_button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlModal,
|
||||
PreviewDropdown,
|
||||
},
|
||||
directives: {
|
||||
'gl-modal': GlModalDirective,
|
||||
PublishButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isNotesFetched']),
|
||||
...mapState('batchComments', ['isDiscarding']),
|
||||
...mapGetters('batchComments', ['draftsCount']),
|
||||
},
|
||||
watch: {
|
||||
|
@ -27,45 +20,17 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('batchComments', ['discardReview', 'expandAllDiscussions']),
|
||||
...mapActions('batchComments', ['expandAllDiscussions']),
|
||||
},
|
||||
modalId: 'discard-draft-review',
|
||||
text: sprintf(
|
||||
s__(
|
||||
`BatchComments|You're about to discard your review which will delete all of your pending comments.
|
||||
The deleted comments %{strong_start}cannot%{strong_end} be restored.`,
|
||||
),
|
||||
{
|
||||
strong_start: '<strong>',
|
||||
strong_end: '</strong>',
|
||||
},
|
||||
false,
|
||||
),
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div v-show="draftsCount > 0">
|
||||
<nav class="review-bar-component">
|
||||
<div class="review-bar-content qa-review-bar">
|
||||
<div class="review-bar-content qa-review-bar d-flex gl-justify-content-end">
|
||||
<preview-dropdown />
|
||||
<gl-button
|
||||
v-gl-modal="$options.modalId"
|
||||
:loading="isDiscarding"
|
||||
class="qa-discard-review float-right"
|
||||
>
|
||||
{{ __('Discard review') }}
|
||||
</gl-button>
|
||||
<publish-button class="gl-ml-3" show-count />
|
||||
</div>
|
||||
</nav>
|
||||
<gl-modal
|
||||
:title="s__('BatchComments|Discard review?')"
|
||||
:ok-title="s__('BatchComments|Delete all pending comments')"
|
||||
:modal-id="$options.modalId"
|
||||
title-tag="h4"
|
||||
ok-variant="danger qa-modal-delete-pending-comments"
|
||||
@ok="discardReview"
|
||||
>
|
||||
<p v-html="$options.text"></p>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -75,15 +75,6 @@ export const updateDiscussionsAfterPublish = ({ dispatch, getters, rootGetters }
|
|||
}),
|
||||
);
|
||||
|
||||
export const discardReview = ({ commit, getters }) => {
|
||||
commit(types.REQUEST_DISCARD_REVIEW);
|
||||
|
||||
return service
|
||||
.discard(getters.getNotesData.draftsDiscardPath)
|
||||
.then(() => commit(types.RECEIVE_DISCARD_REVIEW_SUCCESS))
|
||||
.catch(() => commit(types.RECEIVE_DISCARD_REVIEW_ERROR));
|
||||
};
|
||||
|
||||
export const updateDraft = (
|
||||
{ commit, getters },
|
||||
{ note, noteText, resolveDiscussion, position, callback },
|
||||
|
@ -108,8 +99,6 @@ export const scrollToDraft = ({ dispatch, rootGetters }, draft) => {
|
|||
const draftID = `note_${draft.id}`;
|
||||
const el = document.querySelector(`#${tabEl} #${draftID}`);
|
||||
|
||||
dispatch('closeReviewDropdown');
|
||||
|
||||
window.location.hash = draftID;
|
||||
|
||||
if (window.mrTabs.currentAction !== tab) {
|
||||
|
@ -125,17 +114,6 @@ export const scrollToDraft = ({ dispatch, rootGetters }, draft) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const toggleReviewDropdown = ({ dispatch, state }) => {
|
||||
if (state.showPreviewDropdown) {
|
||||
dispatch('closeReviewDropdown');
|
||||
} else {
|
||||
dispatch('openReviewDropdown');
|
||||
}
|
||||
};
|
||||
|
||||
export const openReviewDropdown = ({ commit }) => commit(types.OPEN_REVIEW_DROPDOWN);
|
||||
export const closeReviewDropdown = ({ commit }) => commit(types.CLOSE_REVIEW_DROPDOWN);
|
||||
|
||||
export const expandAllDiscussions = ({ dispatch, state }) =>
|
||||
state.drafts
|
||||
.filter(draft => draft.discussion_id)
|
||||
|
|
|
@ -11,13 +11,6 @@ export const REQUEST_PUBLISH_REVIEW = 'REQUEST_PUBLISH_REVIEW';
|
|||
export const RECEIVE_PUBLISH_REVIEW_SUCCESS = 'RECEIVE_PUBLISH_REVIEW_SUCCESS';
|
||||
export const RECEIVE_PUBLISH_REVIEW_ERROR = 'RECEIVE_PUBLISH_REVIEW_ERROR';
|
||||
|
||||
export const REQUEST_DISCARD_REVIEW = 'REQUEST_DISCARD_REVIEW';
|
||||
export const RECEIVE_DISCARD_REVIEW_SUCCESS = 'RECEIVE_DISCARD_REVIEW_SUCCESS';
|
||||
export const RECEIVE_DISCARD_REVIEW_ERROR = 'RECEIVE_DISCARD_REVIEW_ERROR';
|
||||
|
||||
export const RECEIVE_DRAFT_UPDATE_SUCCESS = 'RECEIVE_DRAFT_UPDATE_SUCCESS';
|
||||
|
||||
export const OPEN_REVIEW_DROPDOWN = 'OPEN_REVIEW_DROPDOWN';
|
||||
export const CLOSE_REVIEW_DROPDOWN = 'CLOSE_REVIEW_DROPDOWN';
|
||||
|
||||
export const TOGGLE_RESOLVE_DISCUSSION = 'TOGGLE_RESOLVE_DISCUSSION';
|
||||
|
|
|
@ -43,16 +43,6 @@ export default {
|
|||
[types.RECEIVE_PUBLISH_REVIEW_ERROR](state) {
|
||||
state.isPublishing = false;
|
||||
},
|
||||
[types.REQUEST_DISCARD_REVIEW](state) {
|
||||
state.isDiscarding = true;
|
||||
},
|
||||
[types.RECEIVE_DISCARD_REVIEW_SUCCESS](state) {
|
||||
state.isDiscarding = false;
|
||||
state.drafts = [];
|
||||
},
|
||||
[types.RECEIVE_DISCARD_REVIEW_ERROR](state) {
|
||||
state.isDiscarding = false;
|
||||
},
|
||||
[types.RECEIVE_DRAFT_UPDATE_SUCCESS](state, data) {
|
||||
const index = state.drafts.findIndex(draft => draft.id === data.id);
|
||||
|
||||
|
@ -60,12 +50,6 @@ export default {
|
|||
state.drafts.splice(index, 1, processDraft(data));
|
||||
}
|
||||
},
|
||||
[types.OPEN_REVIEW_DROPDOWN](state) {
|
||||
state.showPreviewDropdown = true;
|
||||
},
|
||||
[types.CLOSE_REVIEW_DROPDOWN](state) {
|
||||
state.showPreviewDropdown = false;
|
||||
},
|
||||
[types.TOGGLE_RESOLVE_DISCUSSION](state, draftId) {
|
||||
state.drafts = state.drafts.map(draft => {
|
||||
if (draft.id === draftId) {
|
||||
|
|
|
@ -4,6 +4,4 @@ export default () => ({
|
|||
drafts: [],
|
||||
isPublishing: false,
|
||||
currentlyPublishingDrafts: [],
|
||||
isDiscarding: false,
|
||||
showPreviewDropdown: false,
|
||||
});
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import Vue from 'vue';
|
||||
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
|
||||
|
||||
const mountConfirmModal = () => {
|
||||
return new Vue({
|
||||
const mountConfirmModal = optionalProps =>
|
||||
new Vue({
|
||||
render(h) {
|
||||
return h(ConfirmModal, {
|
||||
props: { selector: '.js-confirm-modal-button' },
|
||||
props: {
|
||||
selector: '.js-confirm-modal-button',
|
||||
...optionalProps,
|
||||
},
|
||||
});
|
||||
},
|
||||
}).$mount();
|
||||
};
|
||||
|
||||
export default () => mountConfirmModal();
|
||||
export default (optionalProps = {}) => mountConfirmModal(optionalProps);
|
||||
|
|
|
@ -53,6 +53,7 @@ export default {
|
|||
:aria-label="leaveBtnTitle"
|
||||
data-container="body"
|
||||
data-placement="bottom"
|
||||
data-testid="leave-group-btn"
|
||||
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
|
||||
@click.prevent="onLeaveGroup"
|
||||
>
|
||||
|
@ -66,6 +67,7 @@ export default {
|
|||
:aria-label="editBtnTitle"
|
||||
data-container="body"
|
||||
data-placement="bottom"
|
||||
data-testid="edit-group-btn"
|
||||
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
|
||||
>
|
||||
<gl-icon name="settings" class="position-top-0 align-middle" />
|
||||
|
|
|
@ -57,6 +57,7 @@ export default {
|
|||
:title="title"
|
||||
data-container="body"
|
||||
>
|
||||
<gl-icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span>
|
||||
<gl-icon :name="iconName" />
|
||||
<span v-if="isValuePresent" class="stat-value" data-testid="itemStatValue"> {{ value }} </span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings */
|
||||
import $ from 'jquery';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
|
||||
|
@ -62,11 +61,15 @@ export default {
|
|||
data-toggle="dropdown"
|
||||
>
|
||||
<span class="dropdown-toggle-text">{{ __('Choose a template') }}</span>
|
||||
<i aria-hidden="true" class="fa fa-chevron-down"> </i>
|
||||
<gl-icon
|
||||
name="chevron-down"
|
||||
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-select">
|
||||
<div class="dropdown-title gl-display-flex gl-justify-content-center">
|
||||
<span class="gl-ml-auto">Choose a template</span>
|
||||
<span class="gl-ml-auto">{{ __('Choose a template') }}</span>
|
||||
<button
|
||||
class="dropdown-title-button dropdown-menu-close gl-ml-auto"
|
||||
:aria-label="__('Close')"
|
||||
|
@ -82,7 +85,7 @@ export default {
|
|||
:placeholder="__('Filter')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<i aria-hidden="true" class="fa fa-search dropdown-input-search"> </i>
|
||||
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" />
|
||||
<gl-icon
|
||||
name="close"
|
||||
class="dropdown-input-clear js-dropdown-input-clear"
|
||||
|
|
|
@ -255,6 +255,15 @@ export function getBaseURL() {
|
|||
return `${protocol}//${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a URL and returns content from the start until the final '/'
|
||||
*
|
||||
* @param {String} url - full url, including protocol and host
|
||||
*/
|
||||
export function stripFinalUrlSegment(url) {
|
||||
return new URL('.', url).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if url is an absolute URL
|
||||
*
|
||||
|
|
12
app/assets/javascripts/pages/projects/tags/index/index.js
Normal file
12
app/assets/javascripts/pages/projects/tags/index/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { initRemoveTag } from '../remove_tag';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initRemoveTag({
|
||||
onDelete: path => {
|
||||
document
|
||||
.querySelector(`[data-path="${path}"]`)
|
||||
.closest('.js-tag-list')
|
||||
.remove();
|
||||
},
|
||||
});
|
||||
});
|
16
app/assets/javascripts/pages/projects/tags/remove_tag.js
Normal file
16
app/assets/javascripts/pages/projects/tags/remove_tag.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import initConfirmModal from '~/confirm_modal';
|
||||
|
||||
export const initRemoveTag = ({ onDelete = () => {} }) => {
|
||||
return initConfirmModal({
|
||||
handleSubmit: (path = '') =>
|
||||
axios
|
||||
.delete(path)
|
||||
.then(() => onDelete(path))
|
||||
.catch(({ response: { data } }) => {
|
||||
const { message } = data;
|
||||
createFlash({ message });
|
||||
}),
|
||||
});
|
||||
};
|
10
app/assets/javascripts/pages/projects/tags/show/index.js
Normal file
10
app/assets/javascripts/pages/projects/tags/show/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { redirectTo, getBaseURL, stripFinalUrlSegment } from '~/lib/utils/url_utility';
|
||||
import { initRemoveTag } from '../remove_tag';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initRemoveTag({
|
||||
onDelete: (path = '') => {
|
||||
redirectTo(stripFinalUrlSegment([getBaseURL(), path].join('')));
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
<script>
|
||||
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
|
||||
|
||||
import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '../../constants/index';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlAlert,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
runCleanupPoliciesHelpPagePath: { type: String, required: false, default: '' },
|
||||
cleanupPoliciesHelpPagePath: { type: String, required: false, default: '' },
|
||||
},
|
||||
i18n: {
|
||||
DELETE_ALERT_TITLE,
|
||||
DELETE_ALERT_LINK_TEXT,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert variant="warning" :title="$options.i18n.DELETE_ALERT_TITLE" @dismiss="$emit('dismiss')">
|
||||
<gl-sprintf :message="$options.i18n.DELETE_ALERT_LINK_TEXT">
|
||||
<template #adminLink="{content}">
|
||||
<gl-link data-testid="run-link" :href="runCleanupPoliciesHelpPagePath" target="_blank">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
<template #docLink="{content}">
|
||||
<gl-link data-testid="help-link" :href="cleanupPoliciesHelpPagePath" target="_blank">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
</template>
|
|
@ -42,6 +42,7 @@ export default {
|
|||
name: this.item.path,
|
||||
tags_path: this.item.tags_path,
|
||||
id: this.item.id,
|
||||
cleanup_policy_started_at: this.item.cleanup_policy_started_at,
|
||||
});
|
||||
return window.btoa(params);
|
||||
},
|
||||
|
|
|
@ -9,3 +9,7 @@ export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
|
|||
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
|
||||
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
|
||||
);
|
||||
export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted');
|
||||
export const DELETE_ALERT_LINK_TEXT = s__(
|
||||
'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}',
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui';
|
|||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import Tracking from '~/tracking';
|
||||
import DeleteAlert from '../components/details_page/delete_alert.vue';
|
||||
import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
|
||||
import DeleteModal from '../components/details_page/delete_modal.vue';
|
||||
import DetailsHeader from '../components/details_page/details_header.vue';
|
||||
import TagsList from '../components/details_page/tags_list.vue';
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
export default {
|
||||
components: {
|
||||
DeleteAlert,
|
||||
PartialCleanupAlert,
|
||||
DetailsHeader,
|
||||
GlPagination,
|
||||
DeleteModal,
|
||||
|
@ -37,13 +39,16 @@ export default {
|
|||
itemsToBeDeleted: [],
|
||||
isDesktop: true,
|
||||
deleteAlertType: null,
|
||||
dismissPartialCleanupWarning: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['tagsPagination', 'isLoading', 'config', 'tags']),
|
||||
imageName() {
|
||||
const { name } = decodeAndParse(this.$route.params.id);
|
||||
return name;
|
||||
queryParameters() {
|
||||
return decodeAndParse(this.$route.params.id);
|
||||
},
|
||||
showPartialCleanupWarning() {
|
||||
return this.queryParameters.cleanup_policy_started_at && !this.dismissPartialCleanupWarning;
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
|
@ -120,7 +125,14 @@ export default {
|
|||
class="gl-my-2"
|
||||
/>
|
||||
|
||||
<details-header :image-name="imageName" />
|
||||
<partial-cleanup-alert
|
||||
v-if="showPartialCleanupWarning"
|
||||
:run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
|
||||
:cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
|
||||
@dismiss="dismissPartialCleanupWarning = true"
|
||||
/>
|
||||
|
||||
<details-header :image-name="queryParameters.name" />
|
||||
|
||||
<tags-loader v-if="isLoading" />
|
||||
<template v-else>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetMissingBranch',
|
||||
directives: {
|
||||
tooltip,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
|
@ -52,7 +51,7 @@ export default {
|
|||
<span class="bold js-branch-text">
|
||||
<span class="capitalize"> {{ missingBranchName }} </span>
|
||||
{{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }}
|
||||
<gl-icon v-tooltip :title="message" :aria-label="message" name="question-o" />
|
||||
<gl-icon v-gl-tooltip :title="message" :aria-label="message" name="question-o" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
handleSubmit: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -41,7 +46,11 @@ export default {
|
|||
this.$refs.modal.hide();
|
||||
},
|
||||
submitModal() {
|
||||
this.$refs.form.submit();
|
||||
if (this.handleSubmit) {
|
||||
this.handleSubmit(this.path);
|
||||
} else {
|
||||
this.$refs.form.submit();
|
||||
}
|
||||
},
|
||||
},
|
||||
csrf,
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
color: $gl-text-color;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-default;
|
||||
margin-bottom: $gl-padding-8;
|
||||
|
||||
.card.card-body-segment {
|
||||
padding: $gl-padding;
|
||||
|
|
|
@ -778,7 +778,7 @@
|
|||
}
|
||||
|
||||
.btn {
|
||||
margin-top: $gl-padding-8;
|
||||
margin-bottom: $gl-padding-8;
|
||||
padding: $gl-btn-vert-padding $gl-btn-padding;
|
||||
line-height: $gl-btn-line-height;
|
||||
|
||||
|
|
|
@ -15,12 +15,14 @@ module ApplicationCable
|
|||
private
|
||||
|
||||
def find_user_from_session_store
|
||||
session = ActiveSession.sessions_from_ids([session_id.private_id]).first
|
||||
session = ActiveSession.sessions_from_ids(Array.wrap(session_id)).first
|
||||
Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
|
||||
end
|
||||
|
||||
def session_id
|
||||
Rack::Session::SessionId.new(cookies[Gitlab::Application.config.session_options[:key]])
|
||||
session_cookie = cookies[Gitlab::Application.config.session_options[:key]]
|
||||
|
||||
Rack::Session::SessionId.new(session_cookie).private_id if session_cookie.present?
|
||||
end
|
||||
|
||||
def notification_payload(_)
|
||||
|
|
|
@ -21,6 +21,8 @@ module Boards
|
|||
before_action :validate_id_list, only: [:bulk_move]
|
||||
before_action :can_move_issues?, only: [:bulk_move]
|
||||
|
||||
feature_category :boards
|
||||
|
||||
def index
|
||||
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
|
||||
issues = issues_from(list_service)
|
||||
|
|
|
@ -8,6 +8,8 @@ module Boards
|
|||
before_action :authorize_read_list, only: [:index]
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
|
||||
feature_category :boards
|
||||
|
||||
def index
|
||||
lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
|
||||
|
||||
|
|
|
@ -5,35 +5,38 @@ module ControllerWithFeatureCategory
|
|||
include Gitlab::ClassAttributes
|
||||
|
||||
class_methods do
|
||||
def feature_category(category, config = {})
|
||||
validate_config!(config)
|
||||
def feature_category(category, actions = [])
|
||||
feature_category_configuration[category] ||= []
|
||||
feature_category_configuration[category] += actions.map(&:to_s)
|
||||
|
||||
category_config = Config.new(category, config[:only], config[:except], config[:if], config[:unless])
|
||||
# Add the config to the beginning. That way, the last defined one takes precedence.
|
||||
feature_category_configuration.unshift(category_config)
|
||||
validate_config!(feature_category_configuration)
|
||||
end
|
||||
|
||||
def feature_category_for_action(action)
|
||||
category_config = feature_category_configuration.find { |config| config.matches?(action) }
|
||||
category_config = feature_category_configuration.find do |_, actions|
|
||||
actions.empty? || actions.include?(action)
|
||||
end
|
||||
|
||||
category_config&.category || superclass_feature_category_for_action(action)
|
||||
category_config&.first || superclass_feature_category_for_action(action)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_config!(config)
|
||||
invalid_keys = config.keys - [:only, :except, :if, :unless]
|
||||
if invalid_keys.any?
|
||||
raise ArgumentError, "unknown arguments: #{invalid_keys} "
|
||||
empty = config.find { |_, actions| actions.empty? }
|
||||
duplicate_actions = config.values.flatten.group_by(&:itself).select { |_, v| v.count > 1 }.keys
|
||||
|
||||
if config.length > 1 && empty
|
||||
raise ArgumentError, "#{empty.first} is defined for all actions, but other categories are set"
|
||||
end
|
||||
|
||||
if config.key?(:only) && config.key?(:except)
|
||||
raise ArgumentError, "cannot configure both `only` and `except`"
|
||||
if duplicate_actions.any?
|
||||
raise ArgumentError, "Actions have multiple feature categories: #{duplicate_actions.join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
def feature_category_configuration
|
||||
class_attributes[:feature_category_config] ||= []
|
||||
class_attributes[:feature_category_config] ||= {}
|
||||
end
|
||||
|
||||
def superclass_feature_category_for_action(action)
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ControllerWithFeatureCategory
|
||||
class Config
|
||||
attr_reader :category
|
||||
|
||||
def initialize(category, only, except, if_proc, unless_proc)
|
||||
@category = category.to_sym
|
||||
@only, @except = only&.map(&:to_s), except&.map(&:to_s)
|
||||
@if_proc, @unless_proc = if_proc, unless_proc
|
||||
end
|
||||
|
||||
def matches?(action)
|
||||
included?(action) && !excluded?(action) &&
|
||||
if_proc?(action) && !unless_proc?(action)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :only, :except, :if_proc, :unless_proc
|
||||
|
||||
def if_proc?(action)
|
||||
if_proc.nil? || if_proc.call(action)
|
||||
end
|
||||
|
||||
def unless_proc?(action)
|
||||
unless_proc.present? && unless_proc.call(action)
|
||||
end
|
||||
|
||||
def included?(action)
|
||||
only.nil? || only.include?(action)
|
||||
end
|
||||
|
||||
def excluded?(action)
|
||||
except.present? && except.include?(action)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,8 @@
|
|||
class ConfirmationsController < Devise::ConfirmationsController
|
||||
include AcceptsPendingInvitations
|
||||
|
||||
feature_category :users
|
||||
|
||||
def almost_there
|
||||
flash[:notice] = nil
|
||||
render layout: "devise_empty"
|
||||
|
|
|
@ -5,6 +5,8 @@ class Dashboard::GroupsController < Dashboard::ApplicationController
|
|||
|
||||
skip_cross_project_access_check :index
|
||||
|
||||
feature_category :subgroups
|
||||
|
||||
def index
|
||||
groups = GroupsFinder.new(current_user, all_available: false).execute
|
||||
render_group_tree(groups)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Dashboard::LabelsController < Dashboard::ApplicationController
|
||||
feature_category :issue_tracking
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.json { render json: LabelSerializer.new.represent_appearance(labels) }
|
||||
|
|
|
@ -4,6 +4,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
|
|||
before_action :projects
|
||||
before_action :groups, only: :index
|
||||
|
||||
feature_category :issue_tracking
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
@ -14,6 +14,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
|||
before_action :projects, only: [:index]
|
||||
skip_cross_project_access_check :index, :starred
|
||||
|
||||
feature_category :projects
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
@ -7,6 +7,8 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController
|
|||
|
||||
skip_cross_project_access_check :index
|
||||
|
||||
feature_category :snippets
|
||||
|
||||
def index
|
||||
@snippet_counts = Snippets::CountService
|
||||
.new(current_user, author: current_user)
|
||||
|
|
|
@ -9,6 +9,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
before_action :authorize_read_group!, only: :index
|
||||
before_action :find_todos, only: [:index, :destroy_all]
|
||||
|
||||
feature_category :issue_tracking
|
||||
|
||||
def index
|
||||
@sort = params[:sort]
|
||||
@todos = @todos.page(params[:page])
|
||||
|
|
|
@ -15,6 +15,10 @@ class DashboardController < Dashboard::ApplicationController
|
|||
|
||||
respond_to :html
|
||||
|
||||
feature_category :audit_events, [:activity]
|
||||
feature_category :issue_tracking, [:issues, :issues_calendar]
|
||||
feature_category :code_review, [:merge_requests]
|
||||
|
||||
def activity
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class Explore::GroupsController < Explore::ApplicationController
|
||||
include GroupTree
|
||||
|
||||
feature_category :subgroups
|
||||
|
||||
def index
|
||||
render_group_tree GroupsFinder.new(current_user).execute
|
||||
end
|
||||
|
|
|
@ -18,6 +18,8 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
|
||||
rescue_from PageOutOfBoundsError, with: :page_out_of_bounds
|
||||
|
||||
feature_category :projects
|
||||
|
||||
def index
|
||||
@projects = load_projects
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class Explore::SnippetsController < Explore::ApplicationController
|
||||
include Gitlab::NoteableMetadata
|
||||
|
||||
feature_category :snippets
|
||||
|
||||
def index
|
||||
@snippets = SnippetsFinder.new(current_user, explore: true)
|
||||
.execute
|
||||
|
|
|
@ -50,12 +50,17 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
|
||||
after_action :log_merge_request_show, only: [:show]
|
||||
|
||||
feature_category :source_code_management,
|
||||
unless: -> (action) { action.ends_with?("_reports") }
|
||||
feature_category :code_testing,
|
||||
only: [:test_reports, :coverage_reports, :terraform_reports]
|
||||
feature_category :accessibility_testing,
|
||||
only: [:accessibility_reports]
|
||||
feature_category :code_review, [
|
||||
:assign_related_issues, :bulk_update, :cancel_auto_merge,
|
||||
:ci_environments_status, :commit_change_content, :commits,
|
||||
:context_commits, :destroy, :diff_for_path, :discussions,
|
||||
:edit, :exposed_artifacts, :index, :merge,
|
||||
:pipeline_status, :pipelines, :rebase, :remove_wip, :show,
|
||||
:toggle_award_emoji, :toggle_subscription, :update
|
||||
]
|
||||
|
||||
feature_category :code_testing, [:test_reports, :coverage_reports, :terraform_reports]
|
||||
feature_category :accessibility_testing, [:accessibility_reports]
|
||||
|
||||
def index
|
||||
@merge_requests = @issuables
|
||||
|
|
|
@ -76,25 +76,10 @@ class Projects::TagsController < Projects::ApplicationController
|
|||
def destroy
|
||||
result = ::Tags::DestroyService.new(project, current_user).execute(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
if result[:status] == :success
|
||||
format.html do
|
||||
redirect_to project_tags_path(@project), status: :see_other
|
||||
end
|
||||
|
||||
format.js
|
||||
else
|
||||
@error = result[:message]
|
||||
|
||||
format.html do
|
||||
redirect_to project_tags_path(@project),
|
||||
alert: @error, status: :see_other
|
||||
end
|
||||
|
||||
format.js do
|
||||
render status: :ok
|
||||
end
|
||||
end
|
||||
if result[:status] == :success
|
||||
render json: result
|
||||
else
|
||||
render json: { message: result[:message] }, status: result[:return_code]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -167,7 +167,7 @@ module ApplicationSettingsHelper
|
|||
end
|
||||
|
||||
def visible_attributes
|
||||
[
|
||||
attributes = [
|
||||
:abuse_notification_email,
|
||||
:after_sign_out_path,
|
||||
:after_sign_up_text,
|
||||
|
@ -331,6 +331,9 @@ module ApplicationSettingsHelper
|
|||
:wiki_page_max_content_bytes,
|
||||
:container_registry_delete_tags_service_timeout
|
||||
]
|
||||
|
||||
attributes << :require_admin_approval_after_user_signup if Feature.enabled?(:admin_approval_for_new_user_signups)
|
||||
attributes
|
||||
end
|
||||
|
||||
def external_authorization_service_attributes
|
||||
|
|
|
@ -38,4 +38,13 @@ module TagsHelper
|
|||
|
||||
text.html_safe
|
||||
end
|
||||
|
||||
def delete_tag_modal_attributes(tag_name)
|
||||
{
|
||||
title: s_('TagsPage|Delete tag'),
|
||||
message: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag_name },
|
||||
okVariant: 'danger',
|
||||
okTitle: s_('TagsPage|Delete tag')
|
||||
}.to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -56,6 +56,7 @@ class Issue < ApplicationRecord
|
|||
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
has_many :issue_assignees
|
||||
has_many :issue_email_participants
|
||||
has_many :assignees, class_name: "User", through: :issue_assignees
|
||||
has_many :zoom_meetings
|
||||
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
|
13
app/models/issue_email_participant.rb
Normal file
13
app/models/issue_email_participant.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class IssueEmailParticipant < ApplicationRecord
|
||||
belongs_to :issue
|
||||
|
||||
validates :email, presence: true, uniqueness: { scope: [:issue_id] }
|
||||
validates :issue, presence: true
|
||||
validate :validate_email_format
|
||||
|
||||
def validate_email_format
|
||||
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
|
||||
end
|
||||
end
|
|
@ -199,6 +199,7 @@ class Project < ApplicationRecord
|
|||
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :export_jobs, class_name: 'ProjectExportJob'
|
||||
has_one :project_repository, inverse_of: :project
|
||||
has_one :tracing_setting, class_name: 'ProjectTracingSetting'
|
||||
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
|
||||
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
|
||||
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
|
||||
|
@ -564,6 +565,7 @@ class Project < ApplicationRecord
|
|||
}
|
||||
|
||||
scope :imported_from, -> (type) { where(import_type: type) }
|
||||
scope :with_tracing_enabled, -> { joins(:tracing_setting) }
|
||||
|
||||
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
|
||||
|
||||
|
@ -2523,6 +2525,10 @@ class Project < ApplicationRecord
|
|||
instance.token
|
||||
end
|
||||
|
||||
def tracing_external_url
|
||||
tracing_setting&.external_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_service(services, name)
|
||||
|
|
15
app/models/project_tracing_setting.rb
Normal file
15
app/models/project_tracing_setting.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectTracingSetting < ApplicationRecord
|
||||
belongs_to :project
|
||||
|
||||
validates :external_url, length: { maximum: 255 }, public_url: true
|
||||
|
||||
before_validation :sanitize_external_url
|
||||
|
||||
private
|
||||
|
||||
def sanitize_external_url
|
||||
self.external_url = Rails::Html::FullSanitizer.new.sanitize(self.external_url)
|
||||
end
|
||||
end
|
|
@ -64,11 +64,6 @@ class User < ApplicationRecord
|
|||
# and should be added after Devise modules are initialized.
|
||||
include AsyncDeviseEmail
|
||||
|
||||
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
|
||||
"administrator if you think this is an error."
|
||||
LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \
|
||||
"administrator if you think this is an error."
|
||||
|
||||
MINIMUM_INACTIVE_DAYS = 90
|
||||
|
||||
# Override Devise::Models::Trackable#update_tracked_fields!
|
||||
|
@ -381,11 +376,12 @@ class User < ApplicationRecord
|
|||
super && can?(:log_in)
|
||||
end
|
||||
|
||||
# The messages for these keys are defined in `devise.en.yml`
|
||||
def inactive_message
|
||||
if blocked?
|
||||
BLOCKED_MESSAGE
|
||||
:blocked
|
||||
elsif internal?
|
||||
LOGIN_FORBIDDEN
|
||||
:forbidden
|
||||
else
|
||||
super
|
||||
end
|
||||
|
|
|
@ -15,6 +15,7 @@ module Releases
|
|||
# - Project
|
||||
# - Milestones
|
||||
# - Issues
|
||||
# TODO: remove issues from this check: https://gitlab.com/gitlab-org/gitlab/-/issues/259674
|
||||
condition(:allowed_to_read_evidence) do
|
||||
can?(:read_release) &&
|
||||
can?(:download_code) &&
|
||||
|
|
|
@ -4,6 +4,7 @@ class ContainerRepositoryEntity < Grape::Entity
|
|||
include RequestAwareEntity
|
||||
|
||||
expose :id, :name, :path, :location, :created_at, :status, :tags_count
|
||||
expose :expiration_policy_started_at, as: :cleanup_policy_started_at
|
||||
|
||||
expose :tags_path do |repository|
|
||||
project_registry_repository_tags_path(project, repository, format: :json)
|
||||
|
|
|
@ -70,7 +70,9 @@ module Ci
|
|||
end
|
||||
|
||||
def validate_build_trace!
|
||||
if chunks_persisted?
|
||||
return unless has_chunks?
|
||||
|
||||
unless live_chunks_pending?
|
||||
metrics.increment_trace_operation(operation: :finalized)
|
||||
end
|
||||
|
||||
|
@ -110,8 +112,8 @@ module Ci
|
|||
build.trace_chunks.live.any?
|
||||
end
|
||||
|
||||
def chunks_persisted?
|
||||
build.trace_chunks.any? && !live_chunks_pending?
|
||||
def has_chunks?
|
||||
build.trace_chunks.any?
|
||||
end
|
||||
|
||||
def pending_state_outdated?
|
||||
|
|
|
@ -246,7 +246,7 @@ module DesignManagement
|
|||
new_designs = DesignManagement::Design.unscoped.find(design_ids)
|
||||
|
||||
# Execute another query to filter only designs with notes
|
||||
DesignManagement::Design.unscoped.where(id: designs).joins(:notes).find_each(batch_size: 100) do |old_design|
|
||||
DesignManagement::Design.unscoped.where(id: designs).joins(:notes).distinct.find_each(batch_size: 100) do |old_design|
|
||||
new_design = new_designs.find { |d| d.filename == old_design.filename }
|
||||
|
||||
Notes::CopyService.new(current_user, old_design, new_design).execute
|
||||
|
|
|
@ -10,8 +10,6 @@ module SystemNotes
|
|||
#
|
||||
# Returns the created Note object
|
||||
def change_incident_severity
|
||||
return unless Feature.enabled?(:add_severity_system_note, noteable.project)
|
||||
|
||||
severity = noteable.severity
|
||||
|
||||
if severity_label = IssuableSeverity::SEVERITY_LABELS[severity.to_sym]
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
Sign-up enabled
|
||||
.form-text.text-muted
|
||||
= _("When enabled, any user visiting %{host} will be able to create an account.") % { host: "#{new_user_session_url(host: Gitlab.config.gitlab.host)}" }
|
||||
- if Feature.enabled?(:admin_approval_for_new_user_signups)
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :require_admin_approval_after_user_signup, class: 'form-check-input'
|
||||
= f.label :require_admin_approval_after_user_signup, class: 'form-check-label' do
|
||||
= _('Require admin approval for new sign-ups')
|
||||
.form-text.text-muted
|
||||
= _("When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by the admin before they can login. This setting is effective only if sign-ups are enabled.") % { host: "#{new_user_session_url(host: Gitlab.config.gitlab.host)}" }
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :send_user_confirmation_email, class: 'form-check-input'
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"registry_host_url_with_port" => escape_once(registry_config.host_port),
|
||||
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
|
||||
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
|
||||
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
|
||||
"is_admin": current_user&.admin.to_s,
|
||||
is_group_page: "true",
|
||||
character_error: @character_error.to_s } }
|
||||
|
|
|
@ -64,12 +64,12 @@
|
|||
- else
|
||||
= _('Register Universal Two-Factor (U2F) Device')
|
||||
%p
|
||||
= _('Use a hardware device to add the second factor of authentication.')
|
||||
= _('Set up a hardware device as a second factor to sign in.')
|
||||
%p
|
||||
- if webauthn_enabled
|
||||
= _("As WebAuthn devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a WebAuthn device. That way you'll always be able to log in - even when you're using an unsupported browser.")
|
||||
= _("Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even from an unsupported browser.")
|
||||
- else
|
||||
= _("As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser.")
|
||||
= _("Not all browsers support U2F devices. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even when you're using an unsupported browser.")
|
||||
.col-lg-8
|
||||
- registration = webauthn_enabled ? @webauthn_registration : @u2f_registration
|
||||
- if registration.errors.present?
|
||||
|
|
6
app/views/projects/buttons/_remove_tag.html.haml
Normal file
6
app/views/projects/buttons/_remove_tag.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
- project = local_assigns.fetch(:project, nil)
|
||||
- tag = local_assigns.fetch(:tag, nil)
|
||||
- return unless project && tag
|
||||
|
||||
%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-remove remove-row has-tooltip gl-ml-3 #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } }
|
||||
= sprite_icon("remove")
|
|
@ -15,5 +15,8 @@
|
|||
"registry_host_url_with_port" => escape_once(registry_config.host_port),
|
||||
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
|
||||
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
|
||||
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
|
||||
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
|
||||
|
||||
"is_admin": current_user&.admin.to_s,
|
||||
character_error: @character_error.to_s } }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- commit = @repository.commit(tag.dereferenced_target)
|
||||
- release = @releases.find { |release| release.tag == tag.name }
|
||||
%li.flex-row.allow-wrap
|
||||
|
||||
%li.flex-row.allow-wrap.js-tag-list
|
||||
.row-main-content
|
||||
= sprite_icon('tag')
|
||||
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'
|
||||
|
@ -38,5 +39,4 @@
|
|||
- if can?(current_user, :admin_tag, @project)
|
||||
= link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
|
||||
= sprite_icon("pencil")
|
||||
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip gl-ml-3 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
|
||||
= sprite_icon("remove")
|
||||
= render 'projects/buttons/remove_tag', project: @project, tag: tag
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
- if @error.present?
|
||||
new Flash({ message: '#{escape_javascript(@error)}', type: 'alert' });
|
||||
- elsif @repository.tags.empty?
|
||||
$('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
|
|
@ -52,8 +52,7 @@
|
|||
= render 'projects/buttons/download', project: @project, ref: @tag.name
|
||||
- if can?(current_user, :admin_tag, @project)
|
||||
.btn-container.controls-item-full
|
||||
= link_to project_tag_path(@project, @tag.name), class: "btn btn-icon btn-danger gl-button remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do
|
||||
= sprite_icon('remove', css_class: 'gl-icon')
|
||||
= render 'projects/buttons/remove_tag', project: @project, tag: @tag
|
||||
|
||||
- if @tag.message.present?
|
||||
%pre.wrap{ data: { qa_selector: 'tag_message_content' } }
|
||||
|
|
5
changelogs/unreleased/225293-emaill-participants.yml
Normal file
5
changelogs/unreleased/225293-emaill-participants.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add issue_email_participants table and related model
|
||||
merge_request: 42943
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display alert for partially executed cleanup policies
|
||||
merge_request: 43831
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "Description Templates: Replace fontawesome icons with GitLab SVGs"
|
||||
merge_request: 43379
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve two button review submit in merge requests
|
||||
merge_request: 43149
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/pl-tracing-core-2-usage-data.yml
Normal file
5
changelogs/unreleased/pl-tracing-core-2-usage-data.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Tracing usage data ping to Core
|
||||
merge_request: 44006
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add system note on incident severity change
|
||||
merge_request: 43998
|
||||
author:
|
||||
type: added
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: add_severity_system_note
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42358
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/251110
|
||||
group: group::health
|
||||
name: admin_approval_for_new_user_signups
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43827
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258980
|
||||
type: development
|
||||
group: group::access
|
||||
default_enabled: false
|
|
@ -46,7 +46,7 @@ end
|
|||
module ActiveRecord
|
||||
class Base
|
||||
module SkipTransactionCheckAfterCommit
|
||||
def committed!(*)
|
||||
def committed!(*args, **kwargs)
|
||||
Sidekiq::Worker.skipping_transaction_check { super }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,8 @@ en:
|
|||
timeout: "Your session expired. Please sign in again to continue."
|
||||
unauthenticated: "You need to sign in or sign up before continuing."
|
||||
unconfirmed: "You have to confirm your email address before continuing. Please check your email for the link we sent you, or click 'Resend confirmation email'."
|
||||
blocked: "Your account has been blocked. Please contact your GitLab administrator if you think this is an error."
|
||||
forbidden: "Your account does not have the required permission to login. Please contact your GitLab administrator if you think this is an error."
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
subject: "Confirmation instructions"
|
||||
|
|
32
db/migrate/20200922052316_create_issue_email_participants.rb
Normal file
32
db/migrate/20200922052316_create_issue_email_participants.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateIssueEmailParticipants < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless table_exists?(:issue_email_participants)
|
||||
with_lock_retries do
|
||||
create_table :issue_email_participants do |t|
|
||||
t.references :issue, index: false, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.datetime_with_timezone :created_at, null: false
|
||||
t.datetime_with_timezone :updated_at, null: false
|
||||
t.text :email, null: false
|
||||
|
||||
t.index [:issue_id, :email], unique: true
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit(:issue_email_participants, :email, 255)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :issue_email_participants
|
||||
end
|
||||
end
|
||||
end
|
1
db/schema_migrations/20200922052316
Normal file
1
db/schema_migrations/20200922052316
Normal file
|
@ -0,0 +1 @@
|
|||
384d022662437de21b4b3b97bf2f1dec2925be6afe4b62828c97dc9b3b3fc77c
|
|
@ -12676,6 +12676,24 @@ CREATE TABLE issue_assignees (
|
|||
issue_id integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE issue_email_participants (
|
||||
id bigint NOT NULL,
|
||||
issue_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
email text NOT NULL,
|
||||
CONSTRAINT check_2c321d408d CHECK ((char_length(email) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE issue_email_participants_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE issue_email_participants_id_seq OWNED BY issue_email_participants.id;
|
||||
|
||||
CREATE TABLE issue_links (
|
||||
id integer NOT NULL,
|
||||
source_id integer NOT NULL,
|
||||
|
@ -17447,6 +17465,8 @@ ALTER TABLE ONLY ip_restrictions ALTER COLUMN id SET DEFAULT nextval('ip_restric
|
|||
|
||||
ALTER TABLE ONLY issuable_severities ALTER COLUMN id SET DEFAULT nextval('issuable_severities_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY issue_email_participants ALTER COLUMN id SET DEFAULT nextval('issue_email_participants_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY issue_links ALTER COLUMN id SET DEFAULT nextval('issue_links_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY issue_metrics ALTER COLUMN id SET DEFAULT nextval('issue_metrics_id_seq'::regclass);
|
||||
|
@ -18565,6 +18585,9 @@ ALTER TABLE ONLY ip_restrictions
|
|||
ALTER TABLE ONLY issuable_severities
|
||||
ADD CONSTRAINT issuable_severities_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY issue_email_participants
|
||||
ADD CONSTRAINT issue_email_participants_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY issue_links
|
||||
ADD CONSTRAINT issue_links_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -20349,6 +20372,8 @@ CREATE UNIQUE INDEX index_issue_assignees_on_issue_id_and_user_id ON issue_assig
|
|||
|
||||
CREATE INDEX index_issue_assignees_on_user_id ON issue_assignees USING btree (user_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_issue_email_participants_on_issue_id_and_email ON issue_email_participants USING btree (issue_id, email);
|
||||
|
||||
CREATE INDEX index_issue_links_on_source_id ON issue_links USING btree (source_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_issue_links_on_source_id_and_target_id ON issue_links USING btree (source_id, target_id);
|
||||
|
@ -22621,6 +22646,9 @@ ALTER TABLE ONLY user_synced_attributes_metadata
|
|||
ALTER TABLE ONLY project_authorizations
|
||||
ADD CONSTRAINT fk_rails_0f84bb11f3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY issue_email_participants
|
||||
ADD CONSTRAINT fk_rails_0fdfd8b811 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY merge_request_context_commits
|
||||
ADD CONSTRAINT fk_rails_0fe0039f60 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
- [Background migrations](../background_migrations.md)
|
||||
- [Swapping tables](../swapping_tables.md)
|
||||
- [Deleting migrations](../deleting_migrations.md)
|
||||
- [Partitioning tables](table_partitioning.md)
|
||||
|
||||
## Debugging
|
||||
|
||||
|
|
253
doc/development/database/table_partitioning.md
Normal file
253
doc/development/database/table_partitioning.md
Normal file
|
@ -0,0 +1,253 @@
|
|||
# Database table partitioning
|
||||
|
||||
Table partitioning is a powerful database feature that allows a table's
|
||||
data to be split into smaller physical tables that act as a single large
|
||||
table. If the application is designed to work with partitioning in mind,
|
||||
there can be multiple benefits, such as:
|
||||
|
||||
- Query performance can be improved greatly, because the database can
|
||||
cheaply eliminate much of the data from the search space, while still
|
||||
providing full SQL capabilities.
|
||||
|
||||
- Bulk deletes can be achieved with minimal impact on the database by
|
||||
dropping entire partitions. This is a natural fit for features that need
|
||||
to periodically delete data that falls outside the retention window.
|
||||
|
||||
- Administrative tasks like `VACUUM` and index rebuilds can operate on
|
||||
individual partitions, rather than across a single massive table.
|
||||
|
||||
Unfortunately, not all models fit a partitioning scheme, and there are
|
||||
significant drawbacks if implemented incorrectly. Additionally, tables
|
||||
can only be partitioned at their creation, making it nontrivial to apply
|
||||
partitioning to a busy database. A suite of migration tools are available
|
||||
to enable backend developers to partition existing tables, but the
|
||||
migration process is rather heavy, taking multiple steps split across
|
||||
several releases. Due to the limitations of partitioning and the related
|
||||
migrations, you should understand how partitioning fits your use case
|
||||
before attempting to leverage this feature.
|
||||
|
||||
## Determining when to use partitioning
|
||||
|
||||
While partitioning can be very useful when properly applied, it's
|
||||
imperative to identify if the data and workload of a table naturally fit a
|
||||
partitioning scheme. There are a few details you'll have to understand
|
||||
in order to decide if partitioning is a good fit for your particular
|
||||
problem.
|
||||
|
||||
First, a table is partitioned on a partition key, which is a column or
|
||||
set of columns which determine how the data will be split across the
|
||||
partitions. The partition key is used by the database when reading or
|
||||
writing data, to decide which partition(s) need to be accessed. The
|
||||
partition key should be a column that would be included in a `WHERE`
|
||||
clause on almost all queries accessing that table.
|
||||
|
||||
Second, it's necessary to understand the strategy the database will
|
||||
use to split the data across the partitions. The scheme supported by the
|
||||
GitLab migration helpers is date-range partitioning, where each partition
|
||||
in the table contains data for a single month. In this case, the partitioning
|
||||
key would need to be a timestamp or date column. In order for this type of
|
||||
partitioning to work well, most queries would need to access data within a
|
||||
certain date range.
|
||||
|
||||
For a more concrete example, the `audit_events` table can be used, which
|
||||
was the first table to be partitioned in the application database
|
||||
(scheduled for deployment with the GitLab 13.5 release). This
|
||||
table tracks audit entries of security events that happen in the
|
||||
application. In almost all cases, users want to see audit activity that
|
||||
occurs in a certain timeframe. As a result, date-range partitioning
|
||||
was a natural fit for how the data would be accessed.
|
||||
|
||||
To look at this in more detail, imagine a simplified `audit_events` schema:
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_events (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
author_id INT NOT NULL,
|
||||
details jsonb NOT NULL,
|
||||
created_at timestamptz NOT NULL);
|
||||
```
|
||||
|
||||
Now imagine typical queries in the UI would display the data within a
|
||||
certain date range, like a single week:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM audit_events
|
||||
WHERE created_at >= '2020-01-01 00:00:00'
|
||||
AND created_at < '2020-01-08 00:00:00'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
```
|
||||
|
||||
If the table is partioned on the `created_at` column the base table would
|
||||
look like:
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_events (
|
||||
id SERIAL NOT NULL,
|
||||
author_id INT NOT NULL,
|
||||
details jsonb NOT NULL,
|
||||
created_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (id, created_at))
|
||||
PARTITION BY RANGE(created_at);
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
The primary key of a partitioned table must include the partition key as
|
||||
part of the primary key definition.
|
||||
|
||||
And we might have a list of partitions for the table, such as:
|
||||
|
||||
```sql
|
||||
audit_events_202001 FOR VALUES FROM ('2020-01-01') TO ('2020-02-01')
|
||||
audit_events_202002 FOR VALUES FROM ('2020-02-01') TO ('2020-03-01')
|
||||
audit_events_202003 FOR VALUES FROM ('2020-03-01') TO ('2020-04-01')
|
||||
```
|
||||
|
||||
Each partition is a separate physical table, with the same structure as
|
||||
the base `audit_events` table, but contains only data for rows where the
|
||||
partition key falls in the specified range. For example, the partition
|
||||
`audit_events_202001` contains rows where the `created_at` column is
|
||||
greater than or equal to `2020-01-01` and less than `2020-02-01`.
|
||||
|
||||
Now, if we look at the previous example query again, the database can
|
||||
use the `WHERE` to recognize that all matching rows will be in the
|
||||
`audit_events_202001` partition. Rather than searching all of the data
|
||||
in all of the partitions, it can search only the single month's worth
|
||||
of data in the appropriate partition. In a large table, this can
|
||||
dramatically reduce the amount of data the database needs to access.
|
||||
However, imagine a query that does not filter based on the partitioning
|
||||
key, such as:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM audit_events
|
||||
WHERE author_id = 123
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
```
|
||||
|
||||
In this example, the database can't prune any partitions from the search,
|
||||
because matching data could exist in any of them. As a result, it has to
|
||||
query each partition individually, and aggregate the rows into a single result
|
||||
set. Since `author_id` would be indexed, the performance impact could
|
||||
likely be acceptable, but on more complex queries the overhead can be
|
||||
substantial. Partitioning should only be leveraged if the access patterns
|
||||
of the data support the partitioning strategy, otherwise performance will
|
||||
suffer.
|
||||
|
||||
## Partitioning a table
|
||||
|
||||
Unfortunately, tables can only be partitioned at their creation, making
|
||||
it nontrivial to apply to a busy database. A suite of migration
|
||||
tools have been developed to enable backend developers to partition
|
||||
existing tables. This migration process takes multiple steps which must
|
||||
be split across several releases.
|
||||
|
||||
### Caveats
|
||||
|
||||
The partitioning migration helpers work by creating a partitioned duplicate
|
||||
of the original table and using a combination of a trigger and a background
|
||||
migration to copy data into the new table. Changes to the original table
|
||||
schema can be made in parallel with the partitioning migration, but they
|
||||
must take care to not break the underlying mechanism that makes the migration
|
||||
work. For example, if a column is added to the table that is being
|
||||
partitioned, both the partitioned table and the trigger definition need to
|
||||
be updated to match.
|
||||
|
||||
### Step 1: Creating the partitioned copy (Release N)
|
||||
|
||||
The first step is to add a migration to create the partitioned copy of
|
||||
the original table. This migration will also create the appropriate
|
||||
partitions based on the data in the original table, and install a
|
||||
trigger that will sync writes from the original table into the
|
||||
partitioned copy.
|
||||
|
||||
An example migration of partitioning the `audit_events` table by its
|
||||
`created_at` column would look like:
|
||||
|
||||
```ruby
|
||||
class PartitionAuditEvents < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers
|
||||
|
||||
def up
|
||||
partition_table_by_date :audit_events, :created_at
|
||||
end
|
||||
|
||||
def down
|
||||
drop_partitioned_table_for :audit_events
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Once this has executed, any inserts, updates or deletes in the
|
||||
original table will also be duplicated in the new table. For updates and
|
||||
deletes, the operation will only have an effect if the corresponding row
|
||||
exists in the partitioned table.
|
||||
|
||||
### Step 2: Backfill the partitioned copy (Release N)
|
||||
|
||||
The second step is to add a post-deployment migration that will schedule
|
||||
the background jobs that will backfill existing data from the original table
|
||||
into the partitioned copy.
|
||||
|
||||
Continuing the above example, the migration would look like:
|
||||
|
||||
```ruby
|
||||
class BackfillPartitionAuditEvents < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers
|
||||
|
||||
def up
|
||||
enqueue_partitioning_data_migration :audit_events
|
||||
end
|
||||
|
||||
def down
|
||||
cleanup_partitioning_data_migration :audit_events
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This step uses the same mechanism as any background migration, so you
|
||||
may want to read the [Background Migration](../background_migrations.md)
|
||||
guide for details on that process. Background jobs are scheduled every
|
||||
2 minutes and copy `50_000` records at a time, which can be used to
|
||||
estimate the timing of the background migration portion of the
|
||||
partitioning migration.
|
||||
|
||||
### Step 3: Post-backfill cleanup (Release N+1)
|
||||
|
||||
The third step must occur at least one release after the release that
|
||||
includes the background migration. This gives time for the background
|
||||
migration to execute properly in self-managed installations. In this step,
|
||||
add another post-deployment migration that will cleanup after the
|
||||
background migration. This includes forcing any remaining jobs to
|
||||
execute, and copying data that may have been missed, due to dropped or
|
||||
failed jobs.
|
||||
|
||||
Once again, continuing the example, this migration would look like:
|
||||
|
||||
```ruby
|
||||
class CleanupPartitionedAuditEventsBackfill < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers
|
||||
|
||||
def up
|
||||
finalize_backfilling_partitioned_table :audit_events
|
||||
end
|
||||
|
||||
def down
|
||||
# no op
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
After this migration has completed, the original table and partitioned
|
||||
table should contain identical data. The trigger installed on the
|
||||
original table guarantees that the data will remain in sync going
|
||||
forward.
|
||||
|
||||
### Step 4: Swap the partitioned and non-partitioned tables (Release N+1)
|
||||
|
||||
The final step of the migration will make the partitioned table ready
|
||||
for use by the application. This section will be updated when the
|
||||
migration helper is ready, for now development can be followed in the
|
||||
[Tracking Issue](https://gitlab.com/gitlab-org/gitlab/-/issues/241267).
|
|
@ -75,38 +75,23 @@ A feature category can be specified on an entire controller
|
|||
using:
|
||||
|
||||
```ruby
|
||||
class Projects::MergeRequestsController < ApplicationController
|
||||
feature_category :source_code_management
|
||||
class Boards::ListsController < ApplicationController
|
||||
feature_category :kanban_boards
|
||||
end
|
||||
```
|
||||
|
||||
The feature category can be limited to a list of actions using the
|
||||
`only` argument, actions can be excluded using the `except` argument.
|
||||
second argument:
|
||||
|
||||
```ruby
|
||||
class Projects::MergeRequestsController < ApplicationController
|
||||
feature_category :code_testing, only: [:metrics_reports]
|
||||
feature_category :source_code_management, except: [:test_reports, :coverage_reports]
|
||||
class DashboardController < ApplicationController
|
||||
feature_category :issue_tracking, [:issues, :issues_calendar]
|
||||
feature_category :code_review, [:merge_requests]
|
||||
end
|
||||
```
|
||||
|
||||
`except` and `only` arguments can not be combined.
|
||||
|
||||
When specifying `except` all other actions will get the specified
|
||||
category assigned.
|
||||
|
||||
The assignment can also be scoped using `if` and `unless` procs:
|
||||
|
||||
```ruby
|
||||
class Projects::MergeRequestsController < ApplicationController
|
||||
feature_category :source_code_management,
|
||||
unless: -> (action) { action.include?("reports") }
|
||||
if: -> (action) { action.include?("widget") }
|
||||
end
|
||||
```
|
||||
|
||||
In this case, both procs need to be satisfied for the action to get
|
||||
the category assigned.
|
||||
These forms cannot be mixed: if a controller has more than one category,
|
||||
every single action must be listed.
|
||||
|
||||
### Excluding controller actions from feature categorization
|
||||
|
||||
|
@ -125,6 +110,5 @@ The `spec/controllers/every_controller_spec.rb` will iterate over all
|
|||
defined routes, and check the controller to see if a category is
|
||||
assigned to all actions.
|
||||
|
||||
The spec also validates if the used feature categories are known. And
|
||||
if the actions used in `only` and `except` configuration still exist
|
||||
as routes.
|
||||
The spec also validates if the used feature categories are known. And if
|
||||
the actions used in configuration still exist as routes.
|
||||
|
|
BIN
doc/user/project/merge_requests/img/commit_nav_v13_4.png
Normal file
BIN
doc/user/project/merge_requests/img/commit_nav_v13_4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -122,6 +122,8 @@ the commits to open the single-commit view. From there, you can navigate among t
|
|||
by clicking the **Prev** and **Next** buttons on the top-right of the page or by using the
|
||||
<kbd>X</kbd> and <kbd>C</kbd> keyboard shortcuts.
|
||||
|
||||
![Merge requests commit navigation](img/commit_nav_v13_4.png)
|
||||
|
||||
### Incrementally expand merge request diffs
|
||||
|
||||
By default, the diff shows only the parts of a file which are changed.
|
||||
|
|
|
@ -141,6 +141,7 @@ module Gitlab
|
|||
projects_creating_incidents: distinct_count(Issue.incident, :project_id),
|
||||
projects_imported_from_github: count(Project.where(import_type: 'github')),
|
||||
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
|
||||
projects_with_tracing_enabled: count(ProjectTracingSetting),
|
||||
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
|
||||
projects_with_alerts_service_enabled: count(AlertsService.active),
|
||||
projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),
|
||||
|
@ -573,7 +574,8 @@ module Gitlab
|
|||
clusters_applications_prometheus: cluster_applications_user_distinct_count(::Clusters::Applications::Prometheus, time_period),
|
||||
operations_dashboard_default_dashboard: count(::User.active.with_dashboard('operations').where(time_period),
|
||||
start: user_minimum_id,
|
||||
finish: user_maximum_id)
|
||||
finish: user_maximum_id),
|
||||
projects_with_tracing_enabled: distinct_count(::Project.with_tracing_enabled.where(time_period), :creator_id)
|
||||
}
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -265,6 +265,11 @@ msgid_plural "%d open issues"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d pending comment"
|
||||
msgid_plural "%d pending comments"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d personal project will be removed and cannot be restored."
|
||||
msgid_plural "%d personal projects will be removed and cannot be restored."
|
||||
msgstr[0] ""
|
||||
|
@ -424,11 +429,6 @@ msgid_plural "%{count} participants"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} pending comment"
|
||||
msgid_plural "%{count} pending comments"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} related %{pluralized_subject}: %{links}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3476,12 +3476,6 @@ msgstr ""
|
|||
msgid "Artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser."
|
||||
msgstr ""
|
||||
|
||||
msgid "As WebAuthn devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a WebAuthn device. That way you'll always be able to log in - even when you're using an unsupported browser."
|
||||
msgstr ""
|
||||
|
||||
msgid "As we continue to build more features for SAST, we'd love your feedback on the SAST configuration feature in %{linkStart}this issue%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -3987,15 +3981,6 @@ msgstr ""
|
|||
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
|
||||
msgstr ""
|
||||
|
||||
msgid "BatchComments|Delete all pending comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "BatchComments|Discard review?"
|
||||
msgstr ""
|
||||
|
||||
msgid "BatchComments|You're about to discard your review which will delete all of your pending comments. The deleted comments %{strong_start}cannot%{strong_end} be restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "Be careful. Changing the project's namespace can have unintended side effects."
|
||||
msgstr ""
|
||||
|
||||
|
@ -6989,6 +6974,9 @@ msgstr[1] ""
|
|||
msgid "ContainerRegistry|Set cleanup policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Some tags were not deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Something went wrong while fetching the cleanup policy."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7028,6 +7016,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -9069,9 +9060,6 @@ msgstr ""
|
|||
msgid "Discard draft"
|
||||
msgstr ""
|
||||
|
||||
msgid "Discard review"
|
||||
msgstr ""
|
||||
|
||||
msgid "DiscordService|Discord Notifications"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11323,9 +11311,6 @@ msgstr ""
|
|||
msgid "Finish editing this message first!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Finish review"
|
||||
msgstr ""
|
||||
|
||||
msgid "Finish setting up your dedicated account for %{group_name}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -17544,6 +17529,12 @@ msgstr ""
|
|||
msgid "Not Implemented"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not all browsers support U2F devices. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even when you're using an unsupported browser."
|
||||
msgstr ""
|
||||
|
||||
msgid "Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even from an unsupported browser."
|
||||
msgstr ""
|
||||
|
||||
msgid "Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited."
|
||||
msgstr ""
|
||||
|
||||
|
@ -18578,6 +18569,9 @@ msgstr ""
|
|||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pending comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "People without permission will never get a notification and won't be able to comment."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21827,6 +21821,9 @@ msgstr ""
|
|||
msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The allowlist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com."
|
||||
msgstr ""
|
||||
|
||||
msgid "Require admin approval for new sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Require all users in this group to setup Two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23465,6 +23462,9 @@ msgstr ""
|
|||
msgid "Set up a %{type} Runner manually"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set up a hardware device as a second factor to sign in."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27920,9 +27920,6 @@ msgstr ""
|
|||
msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use a hardware device to add the second factor of authentication."
|
||||
msgstr ""
|
||||
|
||||
msgid "Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
|
||||
msgstr ""
|
||||
|
||||
|
@ -28950,6 +28947,9 @@ msgstr ""
|
|||
msgid "When a runner is locked, it cannot be assigned to other projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by the admin before they can login. This setting is effective only if sign-ups are enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "When enabled, any user visiting %{host} will be able to create an account."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -75,8 +75,6 @@ module QA
|
|||
|
||||
view 'app/assets/javascripts/batch_comments/components/review_bar.vue' do
|
||||
element :review_bar
|
||||
element :discard_review
|
||||
element :modal_delete_pending_comments
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/notes/components/note_form.vue' do
|
||||
|
|
|
@ -5,27 +5,39 @@ require 'spec_helper'
|
|||
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do
|
||||
let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
|
||||
|
||||
before do
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||
context 'when session cookie is set' do
|
||||
before do
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||
end
|
||||
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||
end
|
||||
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||
end
|
||||
context 'when user is logged in' do
|
||||
let(:user) { create(:user) }
|
||||
let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } }
|
||||
|
||||
context 'when user is logged in' do
|
||||
let(:user) { create(:user) }
|
||||
let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } }
|
||||
it 'sets current_user' do
|
||||
connect
|
||||
|
||||
it 'sets current_user' do
|
||||
connect
|
||||
expect(connection.current_user).to eq(user)
|
||||
end
|
||||
|
||||
expect(connection.current_user).to eq(user)
|
||||
context 'with a stale password' do
|
||||
let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] }
|
||||
let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } }
|
||||
|
||||
it 'sets current_user to nil' do
|
||||
connect
|
||||
|
||||
expect(connection.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a stale password' do
|
||||
let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] }
|
||||
let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } }
|
||||
context 'when user is not logged in' do
|
||||
let(:session_hash) { {} }
|
||||
|
||||
it 'sets current_user to nil' do
|
||||
connect
|
||||
|
@ -35,13 +47,21 @@ RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
let(:session_hash) { {} }
|
||||
|
||||
context 'when session cookie is not set' do
|
||||
it 'sets current_user to nil' do
|
||||
connect
|
||||
|
||||
expect(connection.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when session cookie is an empty string' do
|
||||
it 'sets current_user to nil' do
|
||||
cookies[Gitlab::Application.config.session_options[:key]] = ''
|
||||
|
||||
connect
|
||||
|
||||
expect(connection.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,6 +87,38 @@ RSpec.describe Admin::ApplicationSettingsController do
|
|||
sign_in(admin)
|
||||
end
|
||||
|
||||
context 'require_admin_approval_after_user_signup setting' do
|
||||
subject do
|
||||
put :update, params: { application_setting: { require_admin_approval_after_user_signup: true } }
|
||||
end
|
||||
|
||||
context 'when feature is enabled' do
|
||||
before do
|
||||
stub_feature_flags(admin_approval_for_new_user_signups: true)
|
||||
end
|
||||
|
||||
it 'updates the require_admin_approval_after_user_signup setting' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(general_admin_application_settings_path)
|
||||
expect(ApplicationSetting.current.require_admin_approval_after_user_signup).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(admin_approval_for_new_user_signups: false)
|
||||
end
|
||||
|
||||
it 'does not update the require_admin_approval_after_user_signup setting' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(general_admin_application_settings_path)
|
||||
expect(ApplicationSetting.current.require_admin_approval_after_user_signup).not_to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the password_authentication_enabled_for_git setting' do
|
||||
put :update, params: { application_setting: { password_authentication_enabled_for_git: "0" } }
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
|
|||
# triggering the auth form will request admin mode
|
||||
get :new
|
||||
|
||||
Timecop.freeze(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do
|
||||
travel_to(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do
|
||||
post :create, params: { user: { password: user.password } }
|
||||
|
||||
expect(response).to redirect_to(new_admin_session_path)
|
||||
|
|
|
@ -416,13 +416,13 @@ RSpec.describe ApplicationController do
|
|||
end
|
||||
|
||||
it 'returns false if the grace period has expired' do
|
||||
Timecop.freeze(3.hours.from_now) do
|
||||
travel_to(3.hours.from_now) do
|
||||
expect(subject).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns true if the grace period is still active' do
|
||||
Timecop.freeze(1.hour.from_now) do
|
||||
travel_to(1.hour.from_now) do
|
||||
expect(subject).to be_truthy
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "fast_spec_helper"
|
||||
require "rspec-parameterized"
|
||||
require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config"
|
||||
|
||||
RSpec.describe ControllerWithFeatureCategory::Config do
|
||||
describe "#matches?" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do
|
||||
nil | nil | nil | nil | "action" | true
|
||||
[:included] | nil | nil | nil | "action" | false
|
||||
[:included] | nil | nil | nil | "included" | true
|
||||
nil | [:excluded] | nil | nil | "excluded" | false
|
||||
nil | nil | true | nil | "action" | true
|
||||
[:included] | nil | true | nil | "action" | false
|
||||
[:included] | nil | true | nil | "included" | true
|
||||
nil | [:excluded] | true | nil | "excluded" | false
|
||||
nil | nil | false | nil | "action" | false
|
||||
[:included] | nil | false | nil | "action" | false
|
||||
[:included] | nil | false | nil | "included" | false
|
||||
nil | [:excluded] | false | nil | "excluded" | false
|
||||
nil | nil | nil | true | "action" | false
|
||||
[:included] | nil | nil | true | "action" | false
|
||||
[:included] | nil | nil | true | "included" | false
|
||||
nil | [:excluded] | nil | true | "excluded" | false
|
||||
nil | nil | nil | false | "action" | true
|
||||
[:included] | nil | nil | false | "action" | false
|
||||
[:included] | nil | nil | false | "included" | true
|
||||
nil | [:excluded] | nil | false | "excluded" | false
|
||||
nil | nil | true | false | "action" | true
|
||||
[:included] | nil | true | false | "action" | false
|
||||
[:included] | nil | true | false | "included" | true
|
||||
nil | [:excluded] | true | false | "excluded" | false
|
||||
nil | nil | false | true | "action" | false
|
||||
[:included] | nil | false | true | "action" | false
|
||||
[:included] | nil | false | true | "included" | false
|
||||
nil | [:excluded] | false | true | "excluded" | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:config) do
|
||||
if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc }
|
||||
unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc }
|
||||
|
||||
described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc)
|
||||
end
|
||||
|
||||
specify { expect(config.matches?(test_action)).to be(expected) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require_relative "../../../app/controllers/concerns/controller_with_feature_category"
|
||||
require_relative "../../../app/controllers/concerns/controller_with_feature_category/config"
|
||||
|
||||
RSpec.describe ControllerWithFeatureCategory do
|
||||
describe ".feature_category_for_action" do
|
||||
|
@ -14,17 +13,15 @@ RSpec.describe ControllerWithFeatureCategory do
|
|||
|
||||
let(:controller) do
|
||||
Class.new(base_controller) do
|
||||
feature_category :baz
|
||||
feature_category :foo, except: %w(update edit)
|
||||
feature_category :bar, only: %w(index show)
|
||||
feature_category :quux, only: %w(destroy)
|
||||
feature_category :quuz, only: %w(destroy)
|
||||
feature_category :foo, %w(update edit)
|
||||
feature_category :bar, %w(index show)
|
||||
feature_category :quux, %w(destroy)
|
||||
end
|
||||
end
|
||||
|
||||
let(:subclass) do
|
||||
Class.new(controller) do
|
||||
feature_category :qux, only: %w(index)
|
||||
feature_category :baz, %w(subclass_index)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -33,34 +30,31 @@ RSpec.describe ControllerWithFeatureCategory do
|
|||
end
|
||||
|
||||
it "returns the expected category", :aggregate_failures do
|
||||
expect(controller.feature_category_for_action("update")).to eq(:baz)
|
||||
expect(controller.feature_category_for_action("hello")).to eq(:foo)
|
||||
expect(controller.feature_category_for_action("update")).to eq(:foo)
|
||||
expect(controller.feature_category_for_action("index")).to eq(:bar)
|
||||
expect(controller.feature_category_for_action("destroy")).to eq(:quux)
|
||||
end
|
||||
|
||||
it "returns the closest match for categories defined in subclasses" do
|
||||
expect(subclass.feature_category_for_action("index")).to eq(:qux)
|
||||
expect(subclass.feature_category_for_action("show")).to eq(:bar)
|
||||
it "returns the expected category for categories defined in subclasses" do
|
||||
expect(subclass.feature_category_for_action("subclass_index")).to eq(:baz)
|
||||
end
|
||||
|
||||
it "returns the last defined feature category when multiple match" do
|
||||
expect(controller.feature_category_for_action("destroy")).to eq(:quuz)
|
||||
end
|
||||
|
||||
it "raises an error when using including and excluding the same action" do
|
||||
it "raises an error when defining for the controller and for individual actions" do
|
||||
expect do
|
||||
Class.new(base_controller) do
|
||||
feature_category :hello, only: [:world], except: [:world]
|
||||
feature_category :hello
|
||||
feature_category :goodbye, [:world]
|
||||
end
|
||||
end.to raise_error(%r(cannot configure both `only` and `except`))
|
||||
end.to raise_error(ArgumentError, "hello is defined for all actions, but other categories are set")
|
||||
end
|
||||
|
||||
it "raises an error when using unknown arguments" do
|
||||
it "raises an error when multiple calls define the same action" do
|
||||
expect do
|
||||
Class.new(base_controller) do
|
||||
feature_category :hello, hello: :world
|
||||
feature_category :hello, [:world]
|
||||
feature_category :goodbye, ["world"]
|
||||
end
|
||||
end.to raise_error(%r(unknown arguments))
|
||||
end.to raise_error(ArgumentError, "Actions have multiple feature categories: world")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,20 +17,27 @@ RSpec.describe "Every controller" do
|
|||
.compact
|
||||
.select { |route| route[:controller].present? && route[:action].present? }
|
||||
.map { |route| [constantize_controller(route[:controller]), route[:action]] }
|
||||
.reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) }
|
||||
.select { |(controller, action)| controller&.include?(ControllerWithFeatureCategory) }
|
||||
.reject { |(controller, action)| controller == Devise::UnlocksController }
|
||||
end
|
||||
|
||||
let_it_be(:routes_without_category) do
|
||||
controller_actions.map do |controller, action|
|
||||
"#{controller}##{action}" unless controller.feature_category_for_action(action)
|
||||
next if controller.feature_category_for_action(action)
|
||||
next unless controller.to_s.start_with?('B', 'C', 'D', 'E', 'F', 'Projects::MergeRequestsController')
|
||||
|
||||
"#{controller}##{action}"
|
||||
end.compact
|
||||
end
|
||||
|
||||
it "has feature categories" do
|
||||
pending("We'll work on defining categories for all controllers: "\
|
||||
"https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463")
|
||||
routes_without_category.map { |x| x.split('#') }.group_by(&:first).each do |controller, actions|
|
||||
puts controller
|
||||
puts actions.map { |x| ":#{x.last}" }.sort.join(', ')
|
||||
puts ''
|
||||
end
|
||||
|
||||
expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category"
|
||||
expect(routes_without_category).to be_empty, "#{routes_without_category} did not have a category"
|
||||
end
|
||||
|
||||
it "completed controllers don't get new routes without categories" do
|
||||
|
@ -74,9 +81,9 @@ RSpec.describe "Every controller" do
|
|||
end
|
||||
|
||||
def actions_defined_in_feature_category_config(controller)
|
||||
feature_category_configs = controller.send(:class_attributes)[:feature_category_config]
|
||||
feature_category_configs.map do |config|
|
||||
Array(config.send(:only)) + Array(config.send(:except))
|
||||
end.flatten.uniq.map(&:to_s)
|
||||
controller.send(:class_attributes)[:feature_category_config]
|
||||
.values
|
||||
.flatten
|
||||
.map(&:to_s)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -113,18 +113,6 @@ RSpec.describe Projects::Releases::EvidencesController do
|
|||
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
|
||||
context 'when the issue is confidential' do
|
||||
let(:issue) { create(:issue, :confidential, project: project) }
|
||||
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
end
|
||||
|
||||
context 'when the user is the author of the confidential issue' do
|
||||
let(:issue) { create(:issue, :confidential, project: project, author: user) }
|
||||
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
end
|
||||
|
||||
context 'when project is private' do
|
||||
let(:project) { create(:project, :repository, :private) }
|
||||
|
||||
|
@ -143,32 +131,16 @@ RSpec.describe Projects::Releases::EvidencesController do
|
|||
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
|
||||
context 'when the issue is confidential' do
|
||||
let(:issue) { create(:issue, :confidential, project: project) }
|
||||
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
end
|
||||
|
||||
context 'when the user is the author of the confidential issue' do
|
||||
let(:issue) { create(:issue, :confidential, project: project, author: user) }
|
||||
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
end
|
||||
|
||||
context 'when project is private' do
|
||||
let(:project) { create(:project, :repository, :private) }
|
||||
|
||||
it 'returns evidence ' do
|
||||
subject
|
||||
|
||||
expect(json_response).to eq(evidence.summary)
|
||||
end
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
end
|
||||
|
||||
context 'when project restricts the visibility of issues to project members only' do
|
||||
let(:project) { create(:project, :repository, :issues_private) }
|
||||
|
||||
it_behaves_like 'evidence not found'
|
||||
it_behaves_like 'does not show the issue in evidence'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -131,4 +131,25 @@ RSpec.describe Projects::TagsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let(:tag) { project.repository.add_tag(user, 'fake-tag', 'master') }
|
||||
let(:request) do
|
||||
delete(:destroy, params: { id: tag.name, namespace_id: project.namespace.to_param, project_id: project })
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'deletes tag' do
|
||||
request
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to include("Tag was removed")
|
||||
|
||||
expect(project.repository.find_tag(tag.name)).not_to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -78,6 +78,9 @@ RSpec.describe SessionsController do
|
|||
end
|
||||
|
||||
context 'when using standard authentications' do
|
||||
let(:user) { create(:user) }
|
||||
let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) }
|
||||
|
||||
context 'invalid password' do
|
||||
it 'does not authenticate user' do
|
||||
post(:create, params: { user: { login: 'invalid', password: 'invalid' } })
|
||||
|
@ -87,6 +90,26 @@ RSpec.describe SessionsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'a blocked user' do
|
||||
it 'does not authenticate the user' do
|
||||
user.block!
|
||||
post_action
|
||||
|
||||
expect(@request.env['warden']).not_to be_authenticated
|
||||
expect(flash[:alert]).to include('Your account has been blocked')
|
||||
end
|
||||
end
|
||||
|
||||
context 'an internal user' do
|
||||
it 'does not authenticate the user' do
|
||||
user.ghost!
|
||||
post_action
|
||||
|
||||
expect(@request.env['warden']).not_to be_authenticated
|
||||
expect(flash[:alert]).to include('Your account does not have the required permission to login')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using valid password', :clean_gitlab_redis_shared_state do
|
||||
let(:user) { create(:user) }
|
||||
let(:user_params) { { login: user.username, password: user.password } }
|
||||
|
|
8
spec/factories/issue_email_participants.rb
Normal file
8
spec/factories/issue_email_participants.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :issue_email_participant do
|
||||
issue
|
||||
email { generate(:email) }
|
||||
end
|
||||
end
|
8
spec/factories/project_tracing_settings.rb
Normal file
8
spec/factories/project_tracing_settings.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :project_tracing_setting do
|
||||
project
|
||||
external_url { 'https://example.com' }
|
||||
end
|
||||
end
|
|
@ -50,6 +50,9 @@ FactoryBot.define do
|
|||
create(:protected_branch, project: projects[0])
|
||||
create(:protected_branch, name: 'main', project: projects[0])
|
||||
|
||||
# Tracing
|
||||
create(:project_tracing_setting, project: projects[0])
|
||||
|
||||
# Incident Labeled Issues
|
||||
incident_label = create(:label, :incident, project: projects[0])
|
||||
create(:labeled_issue, project: projects[0], labels: [incident_label])
|
||||
|
|
|
@ -130,6 +130,38 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n
|
|||
expect(user_internal_regex['placeholder']).to eq 'Regex pattern'
|
||||
end
|
||||
|
||||
context 'Change Sign-up restrictions' do
|
||||
context 'Require Admin approval for new signup setting' do
|
||||
context 'when feature is enabled' do
|
||||
before do
|
||||
stub_feature_flags(admin_approval_for_new_user_signups: true)
|
||||
end
|
||||
|
||||
it 'changes the setting' do
|
||||
page.within('.as-signup') do
|
||||
check 'Require admin approval for new sign-ups'
|
||||
click_button 'Save changes'
|
||||
end
|
||||
|
||||
expect(current_settings.require_admin_approval_after_user_signup).to be_truthy
|
||||
expect(page).to have_content "Application settings saved successfully"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(admin_approval_for_new_user_signups: false)
|
||||
end
|
||||
|
||||
it 'does not show the the setting' do
|
||||
page.within('.as-signup') do
|
||||
expect(page).not_to have_selector('.application_setting_require_admin_approval_after_user_signup')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'Change Sign-in restrictions' do
|
||||
page.within('.as-signin') do
|
||||
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
|
||||
|
|
|
@ -180,7 +180,7 @@ RSpec.describe 'Contributions Calendar', :js do
|
|||
before do
|
||||
push_code_contribution
|
||||
|
||||
Timecop.freeze(Date.yesterday) do
|
||||
travel_to(Date.yesterday) do
|
||||
Issues::CreateService.new(contributed_project, user, issue_params).execute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,6 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
write_comment
|
||||
|
||||
page.within('.review-bar-content') do
|
||||
click_button 'Finish review'
|
||||
click_button 'Submit review'
|
||||
end
|
||||
|
||||
|
@ -64,18 +63,6 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong')
|
||||
end
|
||||
|
||||
it 'discards review' do
|
||||
write_comment
|
||||
|
||||
click_button 'Discard review'
|
||||
|
||||
click_button 'Delete all pending comments'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_selector('.draft-note-component')
|
||||
end
|
||||
|
||||
it 'deletes draft note' do
|
||||
write_comment
|
||||
|
||||
|
@ -149,7 +136,6 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
write_reply_to_discussion(resolve: true)
|
||||
|
||||
page.within('.review-bar-content') do
|
||||
click_button 'Finish review'
|
||||
click_button 'Submit review'
|
||||
end
|
||||
|
||||
|
@ -192,7 +178,6 @@ RSpec.describe 'Merge request > Batch comments', :js do
|
|||
write_reply_to_discussion(button_text: 'Start a review', unresolve: true)
|
||||
|
||||
page.within('.review-bar-content') do
|
||||
click_button 'Finish review'
|
||||
click_button 'Submit review'
|
||||
end
|
||||
|
||||
|
|
|
@ -21,11 +21,11 @@ RSpec.describe 'Branches' do
|
|||
before do
|
||||
# Add 4 stale branches
|
||||
(1..4).reverse_each do |i|
|
||||
Timecop.freeze((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") }
|
||||
travel_to((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") }
|
||||
end
|
||||
# Add 6 active branches
|
||||
(1..6).each do |i|
|
||||
Timecop.freeze((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") }
|
||||
travel_to((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue