Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7a8d983c19
commit
6f15c2c272
|
|
@ -64,6 +64,7 @@ const Api = {
|
|||
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
|
||||
tagsPath: '/api/:version/projects/:id/repository/tags',
|
||||
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
|
||||
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
|
||||
|
||||
group(groupId, callback = () => {}) {
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
|
|
@ -686,6 +687,19 @@ const Api = {
|
|||
return axios.post(url, freezePeriod);
|
||||
},
|
||||
|
||||
trackRedisHllUserEvent(event) {
|
||||
if (!gon.features?.usageDataApi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = Api.buildUrl(this.usageDataIncrementUniqueUsersPath);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
return axios.post(url, { event }, { headers });
|
||||
},
|
||||
|
||||
buildUrl(url) {
|
||||
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,32 +2,36 @@
|
|||
import { escape } from 'lodash';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import {
|
||||
GlDeprecatedButton,
|
||||
GlTooltipDirective,
|
||||
GlSafeHtmlDirective,
|
||||
GlLoadingIcon,
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
} from '@gitlab/ui';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { diffViewerModes } from '~/ide/constants';
|
||||
import EditButton from './edit_button.vue';
|
||||
import DiffStats from './diff_stats.vue';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlDeprecatedButton,
|
||||
ClipboardButton,
|
||||
EditButton,
|
||||
GlIcon,
|
||||
FileIcon,
|
||||
DiffStats,
|
||||
GlButton,
|
||||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -70,7 +74,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
hasDropdownOpen: false,
|
||||
moreActionsShown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -155,6 +159,13 @@ export default {
|
|||
}
|
||||
return s__('MRDiff|Show full file');
|
||||
},
|
||||
showEditButton() {
|
||||
return (
|
||||
this.diffFile.blob?.readable_text &&
|
||||
!this.diffFile.deleted_file &&
|
||||
(this.diffFile.edit_path || this.diffFile.ide_edit_path)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', [
|
||||
|
|
@ -166,8 +177,11 @@ export default {
|
|||
handleToggleFile() {
|
||||
this.$emit('toggleFile');
|
||||
},
|
||||
showForkMessage() {
|
||||
this.$emit('showForkMessage');
|
||||
showForkMessage(e) {
|
||||
if (this.canCurrentUserFork && !this.diffFile.can_modify_blob) {
|
||||
e.preventDefault();
|
||||
this.$emit('showForkMessage');
|
||||
}
|
||||
},
|
||||
handleFileNameClick(e) {
|
||||
const isLinkToOtherPage =
|
||||
|
|
@ -183,8 +197,8 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
setDropdownOpen(val) {
|
||||
this.hasDropdownOpen = val;
|
||||
setMoreActionsShown(val) {
|
||||
this.moreActionsShown = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -193,8 +207,8 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
ref="header"
|
||||
:class="{ 'gl-z-dropdown-menu!': moreActionsShown }"
|
||||
class="js-file-title file-title file-title-flex-parent"
|
||||
:class="{ 'gl-z-dropdown-menu!': hasDropdownOpen }"
|
||||
@click.self="handleToggleFile"
|
||||
>
|
||||
<div class="file-header-content">
|
||||
|
|
@ -255,96 +269,95 @@ export default {
|
|||
|
||||
<div
|
||||
v-if="!diffFile.submodule && addMergeRequestButtons"
|
||||
class="file-actions d-none d-sm-flex align-items-center flex-wrap"
|
||||
class="file-actions d-flex align-items-center flex-wrap"
|
||||
>
|
||||
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
|
||||
<div class="btn-group" role="group">
|
||||
<template v-if="diffFile.blob && diffFile.blob.readable_text">
|
||||
<span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')">
|
||||
<gl-deprecated-button
|
||||
ref="toggleDiscussionsButton"
|
||||
:disabled="!diffHasDiscussions(diffFile)"
|
||||
:class="{ active: diffHasExpandedDiscussions(diffFile) }"
|
||||
class="js-btn-vue-toggle-comments btn"
|
||||
data-qa-selector="toggle_comments_button"
|
||||
data-track-event="click_toggle_comments_button"
|
||||
data-track-label="diff_toggle_comments_button"
|
||||
data-track-property="diff_toggle_comments"
|
||||
type="button"
|
||||
@click="toggleFileDiscussionWrappers(diffFile)"
|
||||
>
|
||||
<gl-icon name="comment" />
|
||||
</gl-deprecated-button>
|
||||
</span>
|
||||
|
||||
<edit-button
|
||||
v-if="!diffFile.deleted_file"
|
||||
:can-current-user-fork="canCurrentUserFork"
|
||||
:edit-path="diffFile.edit_path"
|
||||
:ide-edit-path="diffFile.ide_edit_path"
|
||||
:can-modify-blob="diffFile.can_modify_blob"
|
||||
data-track-event="click_toggle_edit_button"
|
||||
data-track-label="diff_toggle_edit_button"
|
||||
data-track-property="diff_toggle_edit"
|
||||
@showForkMessage="showForkMessage"
|
||||
@open="setDropdownOpen(true)"
|
||||
@close="setDropdownOpen(false)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<a
|
||||
v-if="diffFile.replaced_view_path"
|
||||
ref="replacedFileButton"
|
||||
v-safe-html="viewReplacedFileButtonText"
|
||||
:href="diffFile.replaced_view_path"
|
||||
class="btn view-file"
|
||||
>
|
||||
</a>
|
||||
<gl-deprecated-button
|
||||
v-if="!diffFile.is_fully_expanded"
|
||||
ref="expandDiffToFullFileButton"
|
||||
v-gl-tooltip.hover
|
||||
:title="expandDiffToFullFileTitle"
|
||||
class="expand-file"
|
||||
data-track-event="click_toggle_view_full_button"
|
||||
data-track-label="diff_toggle_view_full_button"
|
||||
data-track-property="diff_toggle_view_full"
|
||||
@click="toggleFullDiff(diffFile.file_path)"
|
||||
>
|
||||
<gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
|
||||
<gl-icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
|
||||
<gl-icon v-else name="doc-expand" />
|
||||
</gl-deprecated-button>
|
||||
<gl-deprecated-button
|
||||
ref="viewButton"
|
||||
v-gl-tooltip.hover
|
||||
:href="diffFile.view_path"
|
||||
target="_blank"
|
||||
class="view-file"
|
||||
data-track-event="click_toggle_view_sha_button"
|
||||
data-track-label="diff_toggle_view_sha_button"
|
||||
data-track-property="diff_toggle_view_sha"
|
||||
:title="viewFileButtonText"
|
||||
>
|
||||
<gl-icon name="doc-text" />
|
||||
</gl-deprecated-button>
|
||||
|
||||
<a
|
||||
<gl-button-group class="gl-pt-0!">
|
||||
<gl-button
|
||||
v-if="diffFile.external_url"
|
||||
ref="externalLink"
|
||||
v-gl-tooltip.hover
|
||||
:href="diffFile.external_url"
|
||||
:title="`View on ${diffFile.formatted_external_url}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-track-event="click_toggle_external_button"
|
||||
data-track-label="diff_toggle_external_button"
|
||||
data-track-property="diff_toggle_external"
|
||||
class="btn btn-file-option"
|
||||
icon="external-link"
|
||||
/>
|
||||
<gl-dropdown
|
||||
v-gl-tooltip.hover.focus="__('More actions')"
|
||||
right
|
||||
toggle-class="btn-icon js-diff-more-actions"
|
||||
class="gl-pt-0!"
|
||||
@show="setMoreActionsShown(true)"
|
||||
@hidden="setMoreActionsShown(false)"
|
||||
>
|
||||
<gl-icon name="external-link" />
|
||||
</a>
|
||||
</div>
|
||||
<template #button-content>
|
||||
<gl-icon name="ellipsis_v" class="mr-0" />
|
||||
<span class="sr-only">{{ __('More actions') }}</span>
|
||||
</template>
|
||||
<gl-dropdown-section-header>
|
||||
{{ __('More actions') }}
|
||||
</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-if="diffFile.replaced_view_path"
|
||||
ref="replacedFileButton"
|
||||
v-safe-html="viewReplacedFileButtonText"
|
||||
:href="diffFile.replaced_view_path"
|
||||
target="_blank"
|
||||
/>
|
||||
<gl-dropdown-item ref="viewButton" :href="diffFile.view_path" target="_blank">
|
||||
{{ viewFileButtonText }}
|
||||
</gl-dropdown-item>
|
||||
<template v-if="showEditButton">
|
||||
<gl-dropdown-item
|
||||
v-if="diffFile.edit_path"
|
||||
ref="editButton"
|
||||
:href="diffFile.edit_path"
|
||||
class="js-edit-blob"
|
||||
@click="showForkMessage"
|
||||
>
|
||||
{{ __('Edit in single-file editor') }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item
|
||||
v-if="diffFile.edit_path"
|
||||
ref="ideEditButton"
|
||||
:href="diffFile.ide_edit_path"
|
||||
class="js-ide-edit-blob"
|
||||
>
|
||||
{{ __('Edit in Web IDE') }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
||||
<template v-if="!diffFile.viewer.collapsed">
|
||||
<gl-dropdown-divider
|
||||
v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
|
||||
/>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-if="diffHasDiscussions(diffFile)"
|
||||
ref="toggleDiscussionsButton"
|
||||
data-qa-selector="toggle_comments_button"
|
||||
@click="toggleFileDiscussionWrappers(diffFile)"
|
||||
>
|
||||
<template v-if="diffHasExpandedDiscussions(diffFile)">
|
||||
{{ __('Hide comments on this file') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ __('Show comments on this file') }}
|
||||
</template>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item
|
||||
v-if="!diffFile.is_fully_expanded"
|
||||
ref="expandDiffToFullFileButton"
|
||||
@click="toggleFullDiff(diffFile.file_path)"
|
||||
>
|
||||
{{ expandDiffToFullFileTitle }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</gl-button-group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default {
|
|||
class="diff-stats"
|
||||
:class="{
|
||||
'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
|
||||
'd-inline-flex': !isCompareVersionsHeader,
|
||||
'd-none d-sm-inline-flex': !isCompareVersionsHeader,
|
||||
}"
|
||||
>
|
||||
<div v-if="hasDiffFiles" class="diff-stats-group">
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
<script>
|
||||
import { uniqueId } from 'lodash';
|
||||
import {
|
||||
GlTooltipDirective,
|
||||
GlIcon,
|
||||
GlDeprecatedDropdown as GlDropdown,
|
||||
GlDeprecatedDropdownItem as GlDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
editPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
ideEditPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canCurrentUserFork: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canModifyBlob: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { tooltipId: uniqueId('edit_button_tooltip_') };
|
||||
},
|
||||
computed: {
|
||||
tooltipTitle() {
|
||||
if (this.isDisabled) {
|
||||
return __("Can't edit as source branch was deleted");
|
||||
}
|
||||
|
||||
return __('Edit file in...');
|
||||
},
|
||||
isDisabled() {
|
||||
return !this.editPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleShow(evt) {
|
||||
// We must hide the tooltip because it is redundant and doesn't close itself
|
||||
// when dropdown opens because we are still "focused".
|
||||
this.$root.$emit('bv::hide::tooltip', this.tooltipId);
|
||||
|
||||
if (this.canCurrentUserFork && !this.canModifyBlob) {
|
||||
evt.preventDefault();
|
||||
this.$emit('showForkMessage');
|
||||
} else {
|
||||
this.$emit('open');
|
||||
}
|
||||
},
|
||||
handleHide() {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-gl-tooltip.top="{ title: tooltipTitle, id: tooltipId }" class="gl-display-flex">
|
||||
<gl-dropdown
|
||||
toggle-class="rounded-0"
|
||||
:disabled="isDisabled"
|
||||
:class="{ 'cursor-not-allowed': isDisabled }"
|
||||
right
|
||||
data-testid="edit_file"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
>
|
||||
<template #button-content>
|
||||
<span class="gl-dropdown-toggle-text"><gl-icon name="pencil"/></span>
|
||||
<gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
|
||||
</template>
|
||||
<gl-dropdown-item v-if="editPath" :href="editPath">{{
|
||||
__('Edit in single-file editor')
|
||||
}}</gl-dropdown-item>
|
||||
<gl-dropdown-item v-if="ideEditPath" :href="ideEditPath">{{
|
||||
__('Edit in Web IDE')
|
||||
}}</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -46,15 +46,24 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
|
|||
* @param {Object} diff
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const diffHasExpandedDiscussions = (state, getters) => diff => {
|
||||
const discussions = getters.getDiffFileDiscussions(diff);
|
||||
export const diffHasExpandedDiscussions = state => diff => {
|
||||
const lines = {
|
||||
[INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
|
||||
[PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
|
||||
if (line.left) {
|
||||
acc.push(line.left);
|
||||
}
|
||||
|
||||
return (
|
||||
(discussions &&
|
||||
discussions.length &&
|
||||
discussions.find(discussion => discussion.expanded) !== undefined) ||
|
||||
false
|
||||
);
|
||||
if (line.right) {
|
||||
acc.push(line.right);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
};
|
||||
return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType]
|
||||
.filter(l => l.discussions.length >= 1)
|
||||
.some(l => l.discussionsExpanded);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -62,8 +71,25 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
|
|||
* @param {Boolean} diff
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const diffHasDiscussions = (state, getters) => diff =>
|
||||
getters.getDiffFileDiscussions(diff).length > 0;
|
||||
export const diffHasDiscussions = state => diff => {
|
||||
const lines = {
|
||||
[INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
|
||||
[PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
|
||||
if (line.left) {
|
||||
acc.push(line.left);
|
||||
}
|
||||
|
||||
if (line.right) {
|
||||
acc.push(line.right);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
};
|
||||
return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType].some(
|
||||
l => l.discussions.length >= 1,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array with the discussions of the given diff
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
<script>
|
||||
import { GlLink, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -26,16 +25,12 @@ export default {
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
|
||||
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
|
||||
>
|
||||
<div class="gl-pr-3">
|
||||
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
|
||||
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
|
||||
<gl-sprintf :message="__('Alert #%{alertId}')">
|
||||
<template #alertId>
|
||||
<span>{{ alert.iid }}</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
#{{ alert.iid }}
|
||||
</gl-link>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { GlTooltipDirective, GlIcon, GlButton, GlButtonGroup } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import discussionNavigation from '../mixins/discussion_navigation';
|
||||
|
||||
export default {
|
||||
|
|
@ -9,6 +10,8 @@ export default {
|
|||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlButtonGroup,
|
||||
},
|
||||
mixins: [discussionNavigation],
|
||||
computed: {
|
||||
|
|
@ -34,6 +37,12 @@ export default {
|
|||
allExpanded() {
|
||||
return this.toggeableDiscussions.every(discussion => discussion.expanded);
|
||||
},
|
||||
lineResolveClass() {
|
||||
return this.allResolved ? 'line-resolve-btn is-active' : 'line-resolve-text';
|
||||
},
|
||||
toggleThreadsLabel() {
|
||||
return this.allExpanded ? __('Collapse all threads') : __('Expand all threads');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setExpandDiscussions']),
|
||||
|
|
@ -51,59 +60,49 @@ export default {
|
|||
<div
|
||||
v-if="resolvableDiscussionsCount > 0"
|
||||
ref="discussionCounter"
|
||||
class="line-resolve-all-container full-width-mobile"
|
||||
class="line-resolve-all-container full-width-mobile gl-display-flex d-sm-flex"
|
||||
>
|
||||
<div class="full-width-mobile d-flex d-sm-flex">
|
||||
<div class="line-resolve-all">
|
||||
<span
|
||||
:class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
|
||||
>
|
||||
<template v-if="allResolved">
|
||||
<gl-icon name="check-circle-filled" />
|
||||
{{ __('All threads resolved') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="resolveAllDiscussionsIssuePath && !allResolved"
|
||||
class="btn-group btn-group-sm"
|
||||
role="group"
|
||||
>
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:href="resolveAllDiscussionsIssuePath"
|
||||
:title="s__('Resolve all threads in new issue')"
|
||||
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
|
||||
>
|
||||
<gl-icon name="issue-new" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
v-gl-tooltip
|
||||
:title="__('Jump to next unresolved thread')"
|
||||
class="btn btn-default discussion-next-btn"
|
||||
data-track-event="click_button"
|
||||
data-track-label="mr_next_unresolved_thread"
|
||||
data-track-property="click_next_unresolved_thread_top"
|
||||
@click="jumpToNextDiscussion"
|
||||
>
|
||||
<gl-icon name="comment-next" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
v-gl-tooltip
|
||||
:title="__('Toggle all threads')"
|
||||
class="btn btn-default toggle-all-discussions-btn"
|
||||
@click="handleExpandDiscussions"
|
||||
>
|
||||
<gl-icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="line-resolve-all">
|
||||
<span :class="lineResolveClass">
|
||||
<template v-if="allResolved">
|
||||
<gl-icon name="check-circle-filled" />
|
||||
{{ __('All threads resolved') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<gl-button-group>
|
||||
<gl-button
|
||||
v-if="resolveAllDiscussionsIssuePath && !allResolved"
|
||||
v-gl-tooltip
|
||||
:href="resolveAllDiscussionsIssuePath"
|
||||
:title="s__('Resolve all threads in new issue')"
|
||||
:aria-label="s__('Resolve all threads in new issue')"
|
||||
class="new-issue-for-discussion discussion-create-issue-btn"
|
||||
icon="issue-new"
|
||||
/>
|
||||
<gl-button
|
||||
v-if="isLoggedIn && !allResolved"
|
||||
v-gl-tooltip
|
||||
:title="__('Jump to next unresolved thread')"
|
||||
:aria-label="__('Jump to next unresolved thread')"
|
||||
class="discussion-next-btn"
|
||||
data-track-event="click_button"
|
||||
data-track-label="mr_next_unresolved_thread"
|
||||
data-track-property="click_next_unresolved_thread_top"
|
||||
icon="comment-next"
|
||||
@click="jumpToNextDiscussion"
|
||||
/>
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
:title="toggleThreadsLabel"
|
||||
:aria-label="toggleThreadsLabel"
|
||||
class="toggle-all-discussions-btn"
|
||||
:icon="allExpanded ? 'angle-up' : 'angle-down'"
|
||||
@click="handleExpandDiscussions"
|
||||
/>
|
||||
</gl-button-group>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export default {
|
|||
});
|
||||
},
|
||||
displayMemberBadgeText() {
|
||||
return sprintf(__('This user is a %{access} of the %{name} project.'), {
|
||||
return sprintf(__('This user has the %{access} role in the %{name} project.'), {
|
||||
access: this.accessLevel.toLowerCase(),
|
||||
name: this.projectName,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export default {
|
|||
this.newSnippet = false;
|
||||
},
|
||||
onSnippetFetch(snippetRes) {
|
||||
if (snippetRes.data.snippets.edges.length === 0) {
|
||||
if (snippetRes.data.snippets.nodes.length === 0) {
|
||||
this.onNewSnippetFetched();
|
||||
} else {
|
||||
this.onExistingSnippetFetched();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default {
|
|||
return {
|
||||
ids: this.snippet.id,
|
||||
rich: this.activeViewerType === RICH_BLOB_VIEWER,
|
||||
paths: [this.blob.path],
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
|
|
@ -79,8 +80,10 @@ export default {
|
|||
},
|
||||
onContentUpdate(data) {
|
||||
const { path: blobPath } = this.blob;
|
||||
const { blobs } = data.snippets.edges[0].node;
|
||||
const updatedBlobData = blobs.find(blob => blob.path === blobPath);
|
||||
const {
|
||||
blobs: { nodes: dataBlobs },
|
||||
} = data.snippets.nodes[0];
|
||||
const updatedBlobData = dataBlobs.find(blob => blob.path === blobPath);
|
||||
return updatedBlobData.richData || updatedBlobData.plainData;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
|
|||
import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
|
||||
import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -37,6 +38,7 @@ export default {
|
|||
},
|
||||
apollo: {
|
||||
canCreateSnippet: {
|
||||
fetchPolicy: fetchPolicies.NO_CACHE,
|
||||
query() {
|
||||
return this.snippet.project ? CanCreateProjectSnippet : CanCreatePersonalSnippet;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ fragment SnippetBase on Snippet {
|
|||
httpUrlToRepo
|
||||
sshUrlToRepo
|
||||
blobs {
|
||||
binary
|
||||
name
|
||||
path
|
||||
rawPath
|
||||
size
|
||||
externalStorage
|
||||
renderedAsText
|
||||
simpleViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
richViewer {
|
||||
...BlobViewer
|
||||
nodes {
|
||||
binary
|
||||
name
|
||||
path
|
||||
rawPath
|
||||
size
|
||||
externalStorage
|
||||
renderedAsText
|
||||
simpleViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
richViewer {
|
||||
...BlobViewer
|
||||
}
|
||||
}
|
||||
}
|
||||
userPermissions {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function appFactory(el, Component) {
|
|||
}
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
defaultClient: createDefaultClient({}, { batchMax: 1 }),
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,16 @@ export const getSnippetMixin = {
|
|||
ids: this.snippetGid,
|
||||
};
|
||||
},
|
||||
update: data => data.snippets.edges[0]?.node,
|
||||
update: data => {
|
||||
const res = data.snippets.nodes[0];
|
||||
if (res) {
|
||||
res.blobs = res.blobs.nodes;
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
result(res) {
|
||||
this.blobs = res.data.snippets.edges[0]?.node?.blobs || blobsDefault;
|
||||
this.blobs = res.data.snippets.nodes[0]?.blobs || blobsDefault;
|
||||
if (this.onSnippetFetch) {
|
||||
this.onSnippetFetch(res);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
|
||||
query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
|
||||
snippets(ids: $ids) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
blobs {
|
||||
nodes {
|
||||
id
|
||||
blobs(paths: $paths) {
|
||||
nodes {
|
||||
path
|
||||
richData @include(if: $rich)
|
||||
plainData @skip(if: $rich)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@
|
|||
|
||||
query GetSnippetQuery($ids: [ID!]) {
|
||||
snippets(ids: $ids) {
|
||||
edges {
|
||||
node {
|
||||
...SnippetBase
|
||||
...SnippetProject
|
||||
author {
|
||||
...Author
|
||||
}
|
||||
nodes {
|
||||
...SnippetBase
|
||||
...SnippetProject
|
||||
author {
|
||||
...Author
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { escape } from 'lodash';
|
||||
import { escape, last } from 'lodash';
|
||||
import Tribute from 'tributejs';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { spriteIcon } from '~/lib/utils/common_utils';
|
||||
|
|
@ -12,6 +12,8 @@ const AutoComplete = {
|
|||
MergeRequests: 'mergeRequests',
|
||||
};
|
||||
|
||||
const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
|
||||
|
||||
function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
|
||||
const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
|
||||
const currentLine = fullText.split('\n')[currentLineNumber - 1];
|
||||
|
|
@ -79,30 +81,40 @@ const autoCompleteMap = {
|
|||
return this.members;
|
||||
},
|
||||
menuItemTemplate({ original }) {
|
||||
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
|
||||
const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
|
||||
const noAvatarClasses = `${commonClasses} gl-rounded-small
|
||||
gl-display-flex gl-align-items-center gl-justify-content-center`;
|
||||
|
||||
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
|
||||
gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
|
||||
const avatar = original.avatar_url
|
||||
? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
|
||||
: `<div class="${noAvatarClasses}" aria-hidden="true">
|
||||
${original.username.charAt(0).toUpperCase()}</div>`;
|
||||
|
||||
const avatarTag = original.avatar_url
|
||||
? `<img
|
||||
src="${original.avatar_url}"
|
||||
alt="${original.username}'s avatar"
|
||||
class="${avatarClasses}"/>`
|
||||
: `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
|
||||
let displayName = original.name;
|
||||
let parentGroupOrUsername = `@${original.username}`;
|
||||
|
||||
const name = escape(original.name);
|
||||
if (original.type === groupType) {
|
||||
const splitName = original.name.split(' / ');
|
||||
displayName = splitName.pop();
|
||||
parentGroupOrUsername = splitName.pop();
|
||||
}
|
||||
|
||||
const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
|
||||
|
||||
const icon = original.mentionsDisabled
|
||||
? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
|
||||
const disabledMentionsIcon = original.mentionsDisabled
|
||||
? spriteIcon('notifications-off', 's16 gl-ml-3')
|
||||
: '';
|
||||
|
||||
return `${avatarTag}
|
||||
${original.username}
|
||||
<small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
|
||||
${icon}`;
|
||||
return `
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
${avatar}
|
||||
<div class="gl-font-sm gl-line-height-normal gl-ml-3">
|
||||
<div>${escape(displayName)}${count}</div>
|
||||
<div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
|
||||
</div>
|
||||
${disabledMentionsIcon}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
[AutoComplete.MergeRequests]: {
|
||||
|
|
@ -139,7 +151,8 @@ export default {
|
|||
{
|
||||
trigger: '@',
|
||||
fillAttr: 'username',
|
||||
lookup: value => value.name + value.username,
|
||||
lookup: value =>
|
||||
value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
|
||||
menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
|
||||
values: this.getValues(AutoComplete.Members),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
right: 15px;
|
||||
margin-left: auto;
|
||||
|
||||
.btn {
|
||||
.btn:not(.btn-icon) {
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: 28px;
|
||||
|
|
@ -372,7 +372,7 @@ span.idiff {
|
|||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.file-actions .btn {
|
||||
.file-actions .btn:not(.btn-icon) {
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: 28px;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,21 @@
|
|||
box-shadow: 0 -2px 0 0 var(--white);
|
||||
cursor: pointer;
|
||||
|
||||
.dropdown-menu {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, sm)-1) {
|
||||
.file-header-content {
|
||||
width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
margin-left: $gl-spacing-scale-2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, md)) {
|
||||
// The `+11` is to ensure the file header border shows when scrolled -
|
||||
// the bottom of the compare-versions header and the top of the file header
|
||||
|
|
@ -420,6 +435,10 @@
|
|||
margin-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.file-actions .dropdown {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
table.code {
|
||||
|
|
|
|||
|
|
@ -858,68 +858,28 @@ $note-form-margin-left: 72px;
|
|||
}
|
||||
|
||||
.line-resolve-all-container {
|
||||
margin: $gl-padding-4;
|
||||
|
||||
> div {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.discussion-next-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.toggle-all-discussions-btn {
|
||||
.btn-group .btn:first-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
line-height: $gl-line-height;
|
||||
|
||||
svg {
|
||||
fill: $gray-500;
|
||||
}
|
||||
|
||||
&.discussion-create-issue-btn {
|
||||
border-radius: 0;
|
||||
border-right: 0;
|
||||
|
||||
a {
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.discussion-next-btn {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-resolve-all {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: $gl-padding-4 10px;
|
||||
padding: $gl-padding-8 $gl-padding-12;
|
||||
background-color: $gray-light;
|
||||
border: 1px solid $border-color;
|
||||
border-right: 0;
|
||||
border-radius: $border-radius-default;
|
||||
font-size: $gl-btn-small-font-size;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: 0;
|
||||
|
||||
.line-resolve-btn {
|
||||
color: $gray-500;
|
||||
|
||||
svg {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
||||
font-size: $gl-font-size;
|
||||
line-height: 1rem;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
flex: 1;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ module Boards
|
|||
list = board.lists.destroyable.find(params[:id])
|
||||
service = Boards::Lists::DestroyService.new(board_parent, current_user)
|
||||
|
||||
if service.execute(list)
|
||||
if service.execute(list).success?
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ module WikiActions
|
|||
wiki.list_pages(sort: params[:sort], direction: params[:direction])
|
||||
).page(params[:page])
|
||||
|
||||
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
|
||||
@wiki_entries = WikiDirectory.group_pages(@wiki_pages)
|
||||
|
||||
render 'shared/wikis/pages'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Boards
|
||||
module Lists
|
||||
class Destroy < ::Mutations::BaseMutation
|
||||
graphql_name 'DestroyBoardList'
|
||||
|
||||
field :list,
|
||||
Types::BoardListType,
|
||||
null: true,
|
||||
description: 'The list after mutation.'
|
||||
|
||||
argument :list_id, ::Types::GlobalIDType[::List],
|
||||
required: true,
|
||||
loads: Types::BoardListType,
|
||||
description: 'Global ID of the list to destroy. Only label lists are accepted.'
|
||||
|
||||
def resolve(list:)
|
||||
raise_resource_not_available_error! unless can_admin_list?(list)
|
||||
|
||||
response = ::Boards::Lists::DestroyService.new(list.board.resource_parent, current_user)
|
||||
.execute(list)
|
||||
|
||||
{
|
||||
list: response.success? ? nil : list,
|
||||
errors: response.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_admin_list?(list)
|
||||
return false unless list.present?
|
||||
|
||||
Ability.allowed?(current_user, :admin_list, list.board)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Snippets
|
||||
class BlobsResolver < BaseResolver
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
|
||||
alias_method :snippet, :object
|
||||
|
||||
argument :paths, [GraphQL::STRING_TYPE],
|
||||
required: false,
|
||||
description: 'Paths of the blobs'
|
||||
|
||||
def resolve(**args)
|
||||
authorize!(snippet)
|
||||
|
||||
return [snippet.blob] if snippet.empty_repo?
|
||||
|
||||
paths = Array(args.fetch(:paths, []))
|
||||
|
||||
if paths.empty?
|
||||
snippet.blobs
|
||||
else
|
||||
snippet.repository.blobs_at(transformed_blob_paths(paths))
|
||||
end
|
||||
end
|
||||
|
||||
def authorized_resource?(snippet)
|
||||
Ability.allowed?(context[:current_user], :read_snippet, snippet)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transformed_blob_paths(paths)
|
||||
ref = snippet.default_branch
|
||||
paths.map { |path| [ref, path] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,7 @@ module Types
|
|||
mount_mutation Mutations::Boards::Issues::IssueMoveList
|
||||
mount_mutation Mutations::Boards::Lists::Create
|
||||
mount_mutation Mutations::Boards::Lists::Update
|
||||
mount_mutation Mutations::Boards::Lists::Destroy
|
||||
mount_mutation Mutations::Branches::Create, calls_gitaly: true
|
||||
mount_mutation Mutations::Commits::Create, calls_gitaly: true
|
||||
mount_mutation Mutations::Discussions::ToggleResolve
|
||||
|
|
|
|||
|
|
@ -69,10 +69,11 @@ module Types
|
|||
null: false,
|
||||
deprecated: { reason: 'Use `blobs`', milestone: '13.3' }
|
||||
|
||||
field :blobs, type: [Types::Snippets::BlobType],
|
||||
field :blobs, type: Types::Snippets::BlobType.connection_type,
|
||||
description: 'Snippet blobs',
|
||||
calls_gitaly: true,
|
||||
null: false
|
||||
null: true,
|
||||
resolver: Resolvers::Snippets::BlobsResolver
|
||||
|
||||
field :ssh_url_to_repo, type: GraphQL::STRING_TYPE,
|
||||
description: 'SSH URL to the snippet repository',
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
if access = project.team.human_max_access(issuable.author_id)
|
||||
output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: project.name })
|
||||
output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: project.name })
|
||||
elsif project.team.contributor?(issuable.author_id)
|
||||
output << content_tag(:span, _("Contributor"), class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3", title: _("This user has previously committed to the %{name} project.") % { name: project.name })
|
||||
end
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class Wiki
|
|||
limited = pages.size > limit
|
||||
pages = pages.first(limit) if limited
|
||||
|
||||
[WikiPage.group_by_directory(pages), limited]
|
||||
[WikiDirectory.group_pages(pages), limited]
|
||||
end
|
||||
|
||||
# Finds a page within the repository based on a tile
|
||||
|
|
|
|||
|
|
@ -3,13 +3,46 @@
|
|||
class WikiDirectory
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :slug, :pages
|
||||
attr_accessor :slug, :entries
|
||||
|
||||
validates :slug, presence: true
|
||||
|
||||
def initialize(slug, pages = [])
|
||||
# Groups a list of wiki pages into a nested collection of WikiPage and WikiDirectory objects,
|
||||
# preserving the order of the passed pages.
|
||||
#
|
||||
# Returns an array with all entries for the toplevel directory.
|
||||
#
|
||||
# @param [Array<WikiPage>] pages
|
||||
# @return [Array<WikiPage, WikiDirectory>]
|
||||
#
|
||||
def self.group_pages(pages)
|
||||
# Build a hash to map paths to created WikiDirectory objects,
|
||||
# and recursively create them for each level of the path.
|
||||
# For the toplevel directory we use '' as path, as that's what WikiPage#directory returns.
|
||||
directories = Hash.new do |_, path|
|
||||
directories[path] = new(path).tap do |directory|
|
||||
if path.present?
|
||||
parent = File.dirname(path)
|
||||
parent = '' if parent == '.'
|
||||
directories[parent].entries << directory
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
pages.each do |page|
|
||||
directories[page.directory].entries << page
|
||||
end
|
||||
|
||||
directories[''].entries
|
||||
end
|
||||
|
||||
def initialize(slug, entries = [])
|
||||
@slug = slug
|
||||
@pages = pages
|
||||
@entries = entries
|
||||
end
|
||||
|
||||
def title
|
||||
WikiPage.unhyphenize(File.basename(slug))
|
||||
end
|
||||
|
||||
# Relative path to the partial to be used when rendering collections
|
||||
|
|
|
|||
|
|
@ -31,29 +31,6 @@ class WikiPage
|
|||
|
||||
alias_method :==, :eql?
|
||||
|
||||
# Sorts and groups pages by directory.
|
||||
#
|
||||
# pages - an array of WikiPage objects.
|
||||
#
|
||||
# Returns an array of WikiPage and WikiDirectory objects. The entries are
|
||||
# sorted by alphabetical order (directories and pages inside each directory).
|
||||
# Pages at the root level come before everything.
|
||||
def self.group_by_directory(pages)
|
||||
return [] if pages.blank?
|
||||
|
||||
pages.each_with_object([]) do |page, grouped_pages|
|
||||
next grouped_pages << page unless page.directory.present?
|
||||
|
||||
directory = grouped_pages.find do |obj|
|
||||
obj.is_a?(WikiDirectory) && obj.slug == page.directory
|
||||
end
|
||||
|
||||
next directory.pages << page if directory
|
||||
|
||||
grouped_pages << WikiDirectory.new(page.directory, [page])
|
||||
end
|
||||
end
|
||||
|
||||
def self.unhyphenize(name)
|
||||
name.gsub(/-+/, ' ')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,15 +32,9 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
def blob
|
||||
blobs.first
|
||||
end
|
||||
return snippet.blob if snippet.empty_repo?
|
||||
|
||||
def blobs
|
||||
if snippet.empty_repo?
|
||||
[snippet.blob]
|
||||
else
|
||||
snippet.blobs
|
||||
end
|
||||
blobs.first
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ module Boards
|
|||
module Lists
|
||||
class DestroyService < Boards::BaseService
|
||||
def execute(list)
|
||||
return false unless list.destroyable?
|
||||
unless list.destroyable?
|
||||
return ServiceResponse.error(message: "The list cannot be destroyed. Only label lists can be destroyed.")
|
||||
end
|
||||
|
||||
@board = list.board
|
||||
|
||||
|
|
@ -12,6 +14,8 @@ module Boards
|
|||
decrement_higher_lists(list)
|
||||
remove_list(list)
|
||||
end
|
||||
|
||||
ServiceResponse.success
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -26,7 +30,7 @@ module Boards
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def remove_list(list)
|
||||
list.destroy
|
||||
list.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module Spam
|
|||
attr_reader :user, :context
|
||||
|
||||
def allowlisted?(user)
|
||||
user.respond_to?(:gitlab_employee) && user.gitlab_employee?
|
||||
user.try(:gitlab_employee?) || user.try(:gitlab_bot?)
|
||||
end
|
||||
|
||||
def perform_spam_service_check(api)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
- if note.noteable_author?(@noteable)
|
||||
%span{ class: 'note-role user-access-role has-tooltip d-none d-md-inline-block', title: _("This user is the author of this %{noteable}.") % { noteable: @noteable.human_class_name } }= _("Author")
|
||||
- if access
|
||||
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
|
||||
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
|
||||
- elsif note.contributor?
|
||||
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
%li
|
||||
= link_to wiki_page.title, wiki_page_path(@wiki, wiki_page), data: { qa_selector: 'wiki_page_link', qa_page_name: wiki_page.slug }
|
||||
= link_to wiki_page.human_title, wiki_page_path(@wiki, wiki_page), data: { qa_selector: 'wiki_page_link', qa_page_name: wiki_page.slug }
|
||||
%small (#{wiki_page.format})
|
||||
.float-right
|
||||
- if wiki_page.last_version
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
%li{ data: { qa_selector: 'wiki_directory_content' } }
|
||||
= wiki_directory.slug
|
||||
= wiki_directory.title
|
||||
%ul
|
||||
= render wiki_directory.pages, context: context
|
||||
= render wiki_directory.entries, context: context
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show wiki tree structure in sidebar and pages overview
|
||||
merge_request: 42867
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Destroy issue board list via GraphQL
|
||||
merge_request: 43081
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Surface Alert number GFM reference in highlight bar
|
||||
merge_request: 42832
|
||||
title: Update alert GFM reference in highlight bar
|
||||
merge_request: 43104
|
||||
author:
|
||||
type: changed
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: JS client for increment_unique_users API
|
||||
merge_request: 43084
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Measure npm request forwarding usage
|
||||
merge_request: 40174
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move diff header actions into dropdown menu
|
||||
merge_request:
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Revise tooltip text of note role badge
|
||||
merge_request: 42771
|
||||
author: Mycroft Kang @TaehyeokKang
|
||||
type: Bug fix
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: default_merge_ref_for_diffs
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34472
|
||||
rollout_issue_url:
|
||||
type: development
|
||||
group:
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: design_management_allow_dangerous_images
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16160
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/34279
|
||||
group: group::knowledge
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: export_lfs_objects_projects
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: merge_request_widget_graphql
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38311
|
||||
rollout_issue_url:
|
||||
group: group::source code
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: product_analytics
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36443
|
||||
rollout_issue_url:
|
||||
group: group::product_analytics
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_alert_assigned
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_alert_status_changed
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_alert_todo
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_assigned
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_change_confidential
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_closed
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_comment
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_created
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_relate
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_reopened
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_todo
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_unrelate
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: usage_data_incident_management_incident_zoom_meeting
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40475
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: view_diffs_file_by_file
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35223
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229848
|
||||
group: group::source code
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: web_ide_primary_edit
|
||||
introduced_by_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35957
|
||||
rollout_issue_url:
|
||||
group:
|
||||
group: group::editor
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: widget_visibility_polling
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29318
|
||||
rollout_issue_url:
|
||||
group: group::source code
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -25,10 +25,12 @@ if app.config.public_file_server.enabled
|
|||
}
|
||||
|
||||
if Rails.env.development?
|
||||
# /assets are proxied through a Rails middlware to the Webpack
|
||||
# server, so we have to use the local Rails settings.
|
||||
settings.merge!(
|
||||
host: Gitlab.config.gitlab.host,
|
||||
port: Gitlab.config.gitlab.port,
|
||||
https: false
|
||||
https: Gitlab.config.gitlab.https
|
||||
)
|
||||
app.config.middleware.insert_before(
|
||||
Gitlab::Middleware::Static,
|
||||
|
|
|
|||
|
|
@ -17,21 +17,21 @@ full list of reference architectures, see
|
|||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|--------------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
|
||||
| External load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Consul | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis - Cache | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
|
||||
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
|
||||
| Gitaly | 2 (minimum) | 16 vCPU, 60GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
|
||||
| Sidekiq | 4 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 3 | 32 vCPU, 28.8GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
|
||||
| Monitoring node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Object Storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS Server | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
|
||||
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
|
||||
| Gitaly | 2 (minimum) | 16 vCPU, 60 GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
|
||||
| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 3 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
|
||||
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
@ -40,11 +40,10 @@ or higher, are required for your CPU or node counts. For more information, see
|
|||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
For data objects (such as LFS, Uploads, or Artifacts), an
|
||||
[object storage service](#configure-the-object-storage) is recommended instead
|
||||
of NFS where possible, due to better performance and availability. Since this
|
||||
doesn't require a node to be set up, *Object Storage* is noted as not
|
||||
applicable (n/a) in the previous table.
|
||||
Due to better performance and availability, for data objects (such as LFS,
|
||||
uploads, or artifacts), using an [object storage service](#configure-the-object-storage)
|
||||
is recommended instead of using NFS. Using an object storage service also
|
||||
doesn't require you to provision and maintain a node.
|
||||
|
||||
## Setup components
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ many organizations .
|
|||
|
||||
| Users | Configuration | GCP | AWS | Azure |
|
||||
|--------------|-------------------------|----------------|-----------------|----------------|
|
||||
| Up to 500 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Up to 1,000 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Up to 500 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Up to 1,000 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
@ -30,9 +30,9 @@ or higher, are required for your CPU or node counts. For more information, see
|
|||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
In addition to the stated configurations, we recommend having at least 2GB of
|
||||
In addition to the stated configurations, we recommend having at least 2 GB of
|
||||
swap on your server, even if you currently have enough available memory. Having
|
||||
swap will help reduce the chance of errors occurring if your available memory
|
||||
swap helps to reduce the chance of errors occurring if your available memory
|
||||
changes. We also recommend configuring the kernel's swappiness setting to a
|
||||
lower value (such as `10`) to make the most of your memory, while still having
|
||||
the swap available when needed.
|
||||
|
|
|
|||
|
|
@ -17,21 +17,21 @@ full list of reference architectures, see
|
|||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
|
||||
| External load balancing node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Consul | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 8 vCPU, 30GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis - Cache | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
|
||||
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
|
||||
| Gitaly | 2 (minimum) | 32 vCPU, 120GB memory | n1-standard-32 | m5.8xlarge | D32s v3 |
|
||||
| Sidekiq | 4 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 5 | 32 vCPU, 28.8GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
|
||||
| Monitoring node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Object Storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS Server | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| External load balancing node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
|
||||
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
|
||||
| Gitaly | 2 (minimum) | 32 vCPU, 120 GB memory | n1-standard-32 | m5.8xlarge | D32s v3 |
|
||||
| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 5 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
|
||||
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
@ -40,11 +40,10 @@ or higher, are required for your CPU or node counts. For more information, see
|
|||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
For data objects (such as LFS, Uploads, or Artifacts), an
|
||||
[object storage service](#configure-the-object-storage) is recommended instead
|
||||
of NFS where possible, due to better performance and availability. Since this
|
||||
doesn't require a node to be set up, *Object Storage* is noted as not
|
||||
applicable (n/a) in the previous table.
|
||||
Due to better performance and availability, for data objects (such as LFS,
|
||||
uploads, or artifacts), using an [object storage service](#configure-the-object-storage)
|
||||
is recommended instead of using NFS. Using an object storage service also
|
||||
doesn't require you to provision and maintain a node.
|
||||
|
||||
## Setup components
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ For a full list of reference architectures, see
|
|||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|------------------------------------------|--------|-------------------------|----------------|--------------|---------|
|
||||
| Load balancer | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 1 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Redis | 1 | 1 vCPU, 3.75GB memory | n1-standard-1 | m5.large | D2s v3 |
|
||||
| Gitaly | 1 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 2 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Load balancer | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 1 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Redis | 1 | 1 vCPU, 3.75 GB memory | n1-standard-1 | m5.large | D2s v3 |
|
||||
| Gitaly | 1 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 2 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
|
|||
|
|
@ -24,18 +24,18 @@ costly-to-operate environment by using the
|
|||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|--------------------------------------------|-------------|-----------------------|----------------|-------------|---------|
|
||||
| External load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis | 3 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Consul + Sentinel | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Gitaly | 2 (minimum) | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Sidekiq | 4 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| GitLab Rails | 3 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Object Storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS Server (optional, not recommended) | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Consul + Sentinel | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Gitaly | 2 (minimum) | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Sidekiq | 4 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| GitLab Rails | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
@ -44,11 +44,10 @@ or higher, are required for your CPU or node counts. For more information, see
|
|||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
For data objects (such as LFS, Uploads, or Artifacts), an
|
||||
[object storage service](#configure-the-object-storage) is recommended instead
|
||||
of NFS where possible, due to better performance and availability. Since this
|
||||
doesn't require a node to be set up, *Object Storage* is noted as not
|
||||
applicable (n/a) in the previous table.
|
||||
Due to better performance and availability, for data objects (such as LFS,
|
||||
uploads, or artifacts), using an [object storage service](#configure-the-object-storage)
|
||||
is recommended instead of using NFS. Using an object storage service also
|
||||
doesn't require you to provision and maintain a node.
|
||||
|
||||
## Setup components
|
||||
|
||||
|
|
|
|||
|
|
@ -17,21 +17,21 @@ full list of reference architectures, see
|
|||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
|
||||
| External load balancing node | 1 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Consul | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 16 vCPU, 60GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Redis - Cache | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
|
||||
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
|
||||
| Gitaly | 2 (minimum) | 64 vCPU, 240GB memory | n1-standard-64 | m5.16xlarge | D64s v3 |
|
||||
| Sidekiq | 4 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 12 | 32 vCPU, 28.8GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
|
||||
| Monitoring node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Object Storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS Server | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| External load balancing node | 1 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 16 vCPU, 60 GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
|
||||
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
|
||||
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
|
||||
| Gitaly | 2 (minimum) | 64 vCPU, 240 GB memory | n1-standard-64 | m5.16xlarge | D64s v3 |
|
||||
| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| GitLab Rails | 12 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
|
||||
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
@ -40,11 +40,10 @@ or higher, are required for your CPU or node counts. For more information, see
|
|||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
For data objects (such as LFS, Uploads, or Artifacts), an
|
||||
[object storage service](#configure-the-object-storage) is recommended instead
|
||||
of NFS where possible, due to better performance and availability. Since this
|
||||
doesn't require a node to be set up, *Object Storage* is noted as not
|
||||
applicable (n/a) in the previous table.
|
||||
Due to better performance and availability, for data objects (such as LFS,
|
||||
uploads, or artifacts), using an [object storage service](#configure-the-object-storage)
|
||||
is recommended instead of using NFS. Using an object storage service also
|
||||
doesn't require you to provision and maintain a node.
|
||||
|
||||
## Setup components
|
||||
|
||||
|
|
|
|||
|
|
@ -24,18 +24,18 @@ costly-to-operate environment by using the
|
|||
|
||||
| Service | Nodes | Configuration | GCP | AWS | Azure |
|
||||
|--------------------------------------------|-------------|-------------------------|----------------|-------------|----------|
|
||||
| External load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis | 3 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Consul + Sentinel | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Gitaly | 2 (minimum) | 8 vCPU, 30GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
|
||||
| Sidekiq | 4 | 2 vCPU, 7.5GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| GitLab Rails | 3 | 16 vCPU, 14.4GB memory | n1-highcpu-16 | c5.4xlarge | F16s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Object Storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS Server (optional, not recommended) | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Redis | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| Consul + Sentinel | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| PostgreSQL | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Gitaly | 2 (minimum) | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
|
||||
| Sidekiq | 4 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| GitLab Rails | 3 | 16 vCPU, 14.4 GB memory | n1-highcpu-16 | c5.4xlarge | F16s v2 |
|
||||
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
|
||||
| Object storage | n/a | n/a | n/a | n/a | n/a |
|
||||
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
|
||||
|
||||
The Google Cloud Platform (GCP) architectures were built and tested using the
|
||||
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
|
|
@ -44,11 +44,10 @@ or higher, are required for your CPU or node counts. For more information, see
|
|||
our [Sysbench](https://github.com/akopytov/sysbench)-based
|
||||
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
For data objects (such as LFS, Uploads, or Artifacts), an
|
||||
[object storage service](#configure-the-object-storage) is recommended instead
|
||||
of NFS where possible, due to better performance and availability. Since this
|
||||
doesn't require a node to be set up, *Object Storage* is noted as not
|
||||
applicable (n/a) in the previous table.
|
||||
Due to better performance and availability, for data objects (such as LFS,
|
||||
uploads, or artifacts), using an [object storage service](#configure-the-object-storage)
|
||||
is recommended instead of using NFS. Using an object storage service also
|
||||
doesn't require you to provision and maintain a node.
|
||||
|
||||
## Setup components
|
||||
|
||||
|
|
|
|||
|
|
@ -4495,6 +4495,41 @@ input DestroyBoardInput {
|
|||
id: BoardID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of DestroyBoardList
|
||||
"""
|
||||
input DestroyBoardListInput {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Global ID of the list to destroy. Only label lists are accepted.
|
||||
"""
|
||||
listId: ListID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of DestroyBoardList
|
||||
"""
|
||||
type DestroyBoardListPayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]!
|
||||
|
||||
"""
|
||||
The list after mutation.
|
||||
"""
|
||||
list: BoardList
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of DestroyBoard
|
||||
"""
|
||||
|
|
@ -9280,6 +9315,11 @@ Identifier of Label
|
|||
"""
|
||||
scalar LabelID
|
||||
|
||||
"""
|
||||
Identifier of List
|
||||
"""
|
||||
scalar ListID
|
||||
|
||||
"""
|
||||
List limit metric setting
|
||||
"""
|
||||
|
|
@ -10835,6 +10875,7 @@ type Mutation {
|
|||
designManagementMove(input: DesignManagementMoveInput!): DesignManagementMovePayload
|
||||
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
|
||||
destroyBoard(input: DestroyBoardInput!): DestroyBoardPayload
|
||||
destroyBoardList(input: DestroyBoardListInput!): DestroyBoardListPayload
|
||||
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
|
||||
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
|
||||
|
||||
|
|
@ -16250,7 +16291,32 @@ type Snippet implements Noteable {
|
|||
"""
|
||||
Snippet blobs
|
||||
"""
|
||||
blobs: [SnippetBlob!]!
|
||||
blobs(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
Paths of the blobs
|
||||
"""
|
||||
paths: [String!]
|
||||
): SnippetBlobConnection
|
||||
|
||||
"""
|
||||
Timestamp this snippet was created
|
||||
|
|
@ -16473,6 +16539,41 @@ input SnippetBlobActionInputType {
|
|||
previousPath: String
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for SnippetBlob.
|
||||
"""
|
||||
type SnippetBlobConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [SnippetBlobEdge]
|
||||
|
||||
"""
|
||||
A list of nodes.
|
||||
"""
|
||||
nodes: [SnippetBlob]
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type SnippetBlobEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
cursor: String!
|
||||
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: SnippetBlob
|
||||
}
|
||||
|
||||
"""
|
||||
Represents how the blob content should be displayed
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -12400,6 +12400,108 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "DestroyBoardListInput",
|
||||
"description": "Autogenerated input type of DestroyBoardList",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "listId",
|
||||
"description": "Global ID of the list to destroy. Only label lists are accepted.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ListID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "DestroyBoardListPayload",
|
||||
"description": "Autogenerated return type of DestroyBoardList",
|
||||
"fields": [
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "errors",
|
||||
"description": "Errors encountered during execution of the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "list",
|
||||
"description": "The list after mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "BoardList",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "DestroyBoardPayload",
|
||||
|
|
@ -25724,6 +25826,16 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "ListID",
|
||||
"description": "Identifier of List",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "ListLimitMetric",
|
||||
|
|
@ -31083,6 +31195,33 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "destroyBoardList",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "DestroyBoardListInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "DestroyBoardListPayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "destroyNote",
|
||||
"description": null,
|
||||
|
|
@ -47549,24 +47688,69 @@
|
|||
"name": "blobs",
|
||||
"description": "Snippet blobs",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
{
|
||||
"name": "paths",
|
||||
"description": "Paths of the blobs",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlob",
|
||||
"ofType": null
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlobConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
|
|
@ -48220,6 +48404,118 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlobConnection",
|
||||
"description": "The connection type for SnippetBlob.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlobEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlob",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlobEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlob",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SnippetBlobViewer",
|
||||
|
|
|
|||
|
|
@ -740,6 +740,16 @@ A specific version in which designs were added, modified or deleted.
|
|||
| `id` | ID! | ID of the design version |
|
||||
| `sha` | ID! | SHA of the design version |
|
||||
|
||||
### DestroyBoardListPayload
|
||||
|
||||
Autogenerated return type of DestroyBoardList.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `list` | BoardList | The list after mutation. |
|
||||
|
||||
### DestroyBoardPayload
|
||||
|
||||
Autogenerated return type of DestroyBoard.
|
||||
|
|
@ -2303,7 +2313,6 @@ Represents a snippet entry.
|
|||
| ----- | ---- | ----------- |
|
||||
| `author` | User | The owner of the snippet |
|
||||
| `blob` **{warning-solid}** | SnippetBlob! | **Deprecated:** Use `blobs`. Deprecated in 13.3 |
|
||||
| `blobs` | SnippetBlob! => Array | Snippet blobs |
|
||||
| `createdAt` | Time! | Timestamp this snippet was created |
|
||||
| `description` | String | Description of the snippet |
|
||||
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
|
||||
|
|
|
|||
|
|
@ -220,3 +220,53 @@ Example response:
|
|||
"closed_at": null
|
||||
}
|
||||
```
|
||||
|
||||
## Revert vulnerability to detected state
|
||||
|
||||
Reverts a given vulnerability to detected state. Returns status code `304` if the vulnerability is already in detected state.
|
||||
|
||||
If an authenticated user does not have permission to
|
||||
[revert vulnerability to detected state](../user/permissions.md#project-members-permissions),
|
||||
this request will result in a `403` status code.
|
||||
|
||||
```plaintext
|
||||
POST /vulnerabilities/:id/revert
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | The ID of a vulnerability to revert to detected state |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/vulnerabilities/5/dismiss"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Predictable pseudorandom number generator",
|
||||
"description": null,
|
||||
"state": "detected",
|
||||
"severity": "medium",
|
||||
"confidence": "medium",
|
||||
"report_type": "sast",
|
||||
"project": {
|
||||
"id": 32,
|
||||
"name": "security-reports",
|
||||
"full_path": "/gitlab-examples/security/security-reports",
|
||||
"full_name": "gitlab-examples / security / security-reports"
|
||||
},
|
||||
"author_id": 1,
|
||||
"updated_by_id": null,
|
||||
"last_edited_by_id": null,
|
||||
"closed_by_id": null,
|
||||
"start_date": null,
|
||||
"due_date": null,
|
||||
"created_at": "2019-10-13T15:08:40.219Z",
|
||||
"updated_at": "2019-10-13T15:09:40.382Z",
|
||||
"last_edited_at": null,
|
||||
"closed_at": null
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
|||
| `event` | string | yes | The event name it should be tracked |
|
||||
|
||||
Response
|
||||
|
||||
w
|
||||
Return 200 if tracking failed for any reason.
|
||||
|
||||
- `200` if event was tracked or any errors
|
||||
|
|
@ -368,6 +368,18 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
|||
- `401 Unauthorized` if user is not authenticated
|
||||
- `403 Forbidden` for invalid CSRF token provided
|
||||
|
||||
1. Track events using JavaScript/Vue API helper which calls the API above
|
||||
|
||||
Example usage for an existing event already defined in [known events](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events.yml):
|
||||
|
||||
Note that `usage_data_api` and `usage_data_#{event_name}` should be enabled in order to rack events using API.
|
||||
|
||||
```javascript
|
||||
import api from '~/api';
|
||||
|
||||
api.trackRedisHllUserEvent('my_already_defined_event_name'),
|
||||
```
|
||||
|
||||
1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
|
||||
|
||||
Arguments:
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ The following table depicts the various user permission levels in a project.
|
|||
| Create vulnerability from vulnerability finding **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| Resolve vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| Dismiss vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| Revert vulnerability to detected state **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| Apply code change suggestions | | | ✓ | ✓ | ✓ |
|
||||
| Create and edit wiki pages | | | ✓ | ✓ | ✓ |
|
||||
| Rewrite/remove Git tags | | | ✓ | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module API
|
|||
def destroy_list(list)
|
||||
destroy_conditionally!(list) do |list|
|
||||
service = ::Boards::Lists::DestroyService.new(board_parent, current_user)
|
||||
unless service.execute(list)
|
||||
if service.execute(list).error?
|
||||
render_api_error!({ error: 'List could not be deleted!' }, 400)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ module API
|
|||
|
||||
def redirect_registry_request(forward_to_registry, package_type, options)
|
||||
if forward_to_registry && redirect_registry_request_available?
|
||||
track_event("#{package_type}_request_forward")
|
||||
redirect(registry_url(package_type, options))
|
||||
else
|
||||
yield
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ module Gitlab
|
|||
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: true)
|
||||
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
|
||||
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
|
||||
push_frontend_feature_flag(:usage_data_api, default_enabled: false)
|
||||
|
||||
# Startup CSS feature is a special one as it can be enabled by means of cookies and params
|
||||
gon.push({ features: { 'startupCss' => use_startup_css? } }, true)
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ module Gitlab
|
|||
save_lfs_object(lfs_object)
|
||||
end
|
||||
|
||||
append_lfs_json_for_batch(batch) if write_lfs_json_enabled?
|
||||
append_lfs_json_for_batch(batch)
|
||||
end
|
||||
|
||||
write_lfs_json if write_lfs_json_enabled?
|
||||
write_lfs_json
|
||||
|
||||
true
|
||||
rescue => e
|
||||
|
|
@ -35,10 +35,6 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def write_lfs_json_enabled?
|
||||
::Feature.enabled?(:export_lfs_objects_projects, default_enabled: true)
|
||||
end
|
||||
|
||||
def save_lfs_object(lfs_object)
|
||||
if lfs_object.local_store?
|
||||
copy_file_for_lfs_object(lfs_object)
|
||||
|
|
|
|||
|
|
@ -2192,9 +2192,6 @@ msgid_plural "Alerts"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Alert #%{alertId}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Acknowledged"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4486,9 +4483,6 @@ msgstr ""
|
|||
msgid "Can't create snippet: %{err}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Can't edit as source branch was deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Can't fetch content for the blob: %{err}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6385,6 +6379,9 @@ msgstr ""
|
|||
msgid "Collapse"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collapse all threads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collapse approvers"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9321,9 +9318,6 @@ msgstr ""
|
|||
msgid "Edit environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit file in..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit files in the editor and commit changes here"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10461,6 +10455,9 @@ msgstr ""
|
|||
msgid "Expand all files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand all threads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand approvers"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12994,6 +12991,9 @@ msgid_plural "Hide charts"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Hide comments on this file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15838,9 +15838,6 @@ msgstr ""
|
|||
msgid "MergeRequests|Thread will be unresolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequests|Toggle comments for this file"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequests|View file @ %{commitId}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23417,6 +23414,9 @@ msgstr ""
|
|||
msgid "Show comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show comments on this file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show comments only"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26296,7 +26296,7 @@ msgstr ""
|
|||
msgid "This user has previously committed to the %{name} project."
|
||||
msgstr ""
|
||||
|
||||
msgid "This user is a %{access} of the %{name} project."
|
||||
msgid "This user has the %{access} role in the %{name} project."
|
||||
msgstr ""
|
||||
|
||||
msgid "This user is the author of this %{noteable}."
|
||||
|
|
@ -26810,9 +26810,6 @@ msgstr ""
|
|||
msgid "Toggle Sidebar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle all threads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle backtrace"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28362,6 +28359,12 @@ msgstr ""
|
|||
msgid "VulnerabilityManagement|Could not process %{issueReference}: %{errorMessage}."
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Detected"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Needs triage"
|
||||
msgstr ""
|
||||
|
||||
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module QA
|
|||
RSpec.describe 'Create' do
|
||||
context 'Wiki' do
|
||||
let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! }
|
||||
let(:new_path) { "a/new/path" }
|
||||
let(:new_path) { "a/new/path-with-spaces" }
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
|
|
@ -23,7 +23,9 @@ module QA
|
|||
Page::Project::Wiki::Edit.perform(&:click_save_changes)
|
||||
|
||||
Page::Project::Wiki::Show.perform do |wiki|
|
||||
expect(wiki).to have_directory(new_path)
|
||||
expect(wiki).to have_directory('a')
|
||||
expect(wiki).to have_directory('new')
|
||||
expect(wiki).to have_directory('path with spaces')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -260,6 +260,17 @@ RSpec.describe Boards::ListsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with an error service response' do
|
||||
it 'returns an unprocessable entity response' do
|
||||
allow(Boards::Lists::DestroyService).to receive(:new)
|
||||
.and_return(double(execute: ServiceResponse.error(message: 'error')))
|
||||
|
||||
remove_board_list user: user, board: board, list: planning
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_board_list(user:, board:, list:)
|
||||
sign_in(user)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GroupsController do
|
||||
RSpec.describe GroupsController, factory_default: :keep do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:admin) { create(:admin) }
|
||||
let(:group) { create(:group, :public) }
|
||||
let(:project) { create(:project, namespace: group) }
|
||||
let!(:group_member) { create(:group_member, group: group, user: user) }
|
||||
let!(:owner) { group.add_owner(create(:user)).user }
|
||||
let!(:maintainer) { group.add_maintainer(create(:user)).user }
|
||||
let!(:developer) { group.add_developer(create(:user)).user }
|
||||
let!(:guest) { group.add_guest(create(:user)).user }
|
||||
let_it_be_with_refind(:group) { create_default(:group, :public) }
|
||||
let_it_be_with_refind(:project) { create(:project, namespace: group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:group_member) { create(:group_member, group: group, user: user) }
|
||||
let_it_be(:owner) { group.add_owner(create(:user)).user }
|
||||
let_it_be(:maintainer) { group.add_maintainer(create(:user)).user }
|
||||
let_it_be(:developer) { group.add_developer(create(:user)).user }
|
||||
let_it_be(:guest) { group.add_guest(create(:user)).user }
|
||||
|
||||
shared_examples 'member with ability to create subgroups' do
|
||||
it 'renders the new page' do
|
||||
|
|
@ -57,7 +57,6 @@ RSpec.describe GroupsController do
|
|||
describe 'GET #show' do
|
||||
before do
|
||||
sign_in(user)
|
||||
project
|
||||
end
|
||||
|
||||
let(:format) { :html }
|
||||
|
|
@ -82,7 +81,6 @@ RSpec.describe GroupsController do
|
|||
describe 'GET #details' do
|
||||
before do
|
||||
sign_in(user)
|
||||
project
|
||||
end
|
||||
|
||||
let(:format) { :html }
|
||||
|
|
@ -131,12 +129,9 @@ RSpec.describe GroupsController do
|
|||
end
|
||||
|
||||
describe 'GET #activity' do
|
||||
render_views
|
||||
|
||||
context 'as json' do
|
||||
before do
|
||||
sign_in(user)
|
||||
project
|
||||
end
|
||||
|
||||
it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do
|
||||
|
|
@ -157,10 +152,6 @@ RSpec.describe GroupsController do
|
|||
end
|
||||
|
||||
context 'when user has no permission to see the event' do
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
|
||||
let(:project_with_restricted_access) do
|
||||
create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group)
|
||||
end
|
||||
|
|
@ -552,8 +543,6 @@ RSpec.describe GroupsController do
|
|||
end
|
||||
|
||||
context 'when there is a conflicting group path' do
|
||||
render_views
|
||||
|
||||
let!(:conflict_group) { create(:group, path: SecureRandom.hex(12) ) }
|
||||
let!(:old_name) { group.name }
|
||||
|
||||
|
|
@ -794,6 +783,7 @@ RSpec.describe GroupsController do
|
|||
|
||||
context 'when transferring to a subgroup goes right' do
|
||||
let(:new_parent_group) { create(:group, :public) }
|
||||
let(:group) { create(:group, :public) }
|
||||
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
|
||||
let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
|
||||
|
||||
|
|
@ -805,11 +795,8 @@ RSpec.describe GroupsController do
|
|||
}
|
||||
end
|
||||
|
||||
it 'returns a notice' do
|
||||
it 'returns a notice and redirects to the new path' do
|
||||
expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
|
||||
end
|
||||
|
||||
it 'redirects to the new path' do
|
||||
expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}")
|
||||
end
|
||||
end
|
||||
|
|
@ -826,17 +813,15 @@ RSpec.describe GroupsController do
|
|||
}
|
||||
end
|
||||
|
||||
it 'returns a notice' do
|
||||
it 'returns a notice and redirects to the new path' do
|
||||
expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
|
||||
end
|
||||
|
||||
it 'redirects to the new path' do
|
||||
expect(response).to redirect_to("/#{group.path}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'When the transfer goes wrong' do
|
||||
let(:new_parent_group) { create(:group, :public) }
|
||||
let(:group) { create(:group, :public) }
|
||||
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
|
||||
let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
|
||||
|
||||
|
|
@ -850,17 +835,15 @@ RSpec.describe GroupsController do
|
|||
}
|
||||
end
|
||||
|
||||
it 'returns an alert' do
|
||||
it 'returns an alert and redirects to the current path' do
|
||||
expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved"
|
||||
end
|
||||
|
||||
it 'redirects to the current path' do
|
||||
expect(response).to redirect_to(edit_group_path(group))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is not allowed to transfer the group' do
|
||||
let(:new_parent_group) { create(:group, :public) }
|
||||
let(:group) { create(:group, :public) }
|
||||
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
|
||||
let!(:new_parent_group_member) { create(:group_member, :guest, group: new_parent_group, user: user) }
|
||||
|
||||
|
|
@ -879,6 +862,7 @@ RSpec.describe GroupsController do
|
|||
|
||||
context 'transferring when a project has container images' do
|
||||
let(:group) { create(:group, :public, :nested) }
|
||||
let(:project) { create(:project, namespace: group) }
|
||||
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
|
||||
|
||||
before do
|
||||
|
|
@ -979,6 +963,8 @@ RSpec.describe GroupsController do
|
|||
end
|
||||
|
||||
context 'when there is no file available to download' do
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
|
@ -1149,9 +1135,7 @@ RSpec.describe GroupsController do
|
|||
|
||||
describe "GET #activity as JSON" do
|
||||
include DesignManagementTestHelpers
|
||||
render_views
|
||||
|
||||
let(:project) { create(:project, :public, group: group) }
|
||||
let(:other_project) { create(:project, :public, group: group) }
|
||||
|
||||
def get_activity
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ FactoryBot.define do
|
|||
content { 'Content for wiki page' }
|
||||
format { :markdown }
|
||||
message { nil }
|
||||
project { association(:project, :wiki_repo) }
|
||||
project { association(:project) }
|
||||
container { project }
|
||||
wiki { association(:wiki, container: container) }
|
||||
page { OpenStruct.new(url_path: title) }
|
||||
|
|
@ -18,6 +18,7 @@ FactoryBot.define do
|
|||
initialize_with do
|
||||
new(wiki, page).tap do |page|
|
||||
page.attributes = {
|
||||
slug: title&.tr(' ', '-'),
|
||||
title: title,
|
||||
content: content,
|
||||
format: format
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
FactoryBot.define do
|
||||
factory :wiki do
|
||||
transient do
|
||||
container { association(:project, :wiki_repo) }
|
||||
container { association(:project) }
|
||||
user { association(:user) }
|
||||
end
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ FactoryBot.define do
|
|||
|
||||
factory :project_wiki do
|
||||
transient do
|
||||
project { association(:project, :wiki_repo) }
|
||||
project { association(:project) }
|
||||
end
|
||||
|
||||
container { project }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
let_it_be(:user_xss_title) { 'eve <img src=x onerror=alert(2)<img src=x onerror=alert(1)>' }
|
||||
let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
|
||||
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:group) { create(:group, name: 'Ancestor') }
|
||||
let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
|
||||
let_it_be(:project) { create(:project, group: child_group) }
|
||||
let_it_be(:label) { create(:label, project: project, title: 'special+') }
|
||||
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
|
@ -535,7 +537,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content(user_xss.username)
|
||||
expect(find('.tribute-container ul', visible: true)).to have_text(user_xss.username)
|
||||
end
|
||||
|
||||
it 'selects the first item for assignee dropdowns' do
|
||||
|
|
@ -563,6 +565,24 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
expect(find('.tribute-container ul', visible: true)).to have_content(user.name)
|
||||
end
|
||||
|
||||
context 'when autocompleting for groups' do
|
||||
it 'shows the group when searching for the name of the group' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys('@mygroup')
|
||||
end
|
||||
|
||||
expect(find('.tribute-container ul', visible: true)).to have_text('My group')
|
||||
end
|
||||
|
||||
it 'does not show the group when searching for the name of the parent of the group' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys('@ancestor')
|
||||
end
|
||||
|
||||
expect(find('.tribute-container ul', visible: true)).not_to have_text('My group')
|
||||
end
|
||||
end
|
||||
|
||||
context 'if a selected value has special characters' do
|
||||
it 'wraps the result in double quotes' do
|
||||
note = find('#note-body')
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
|
|||
visit project_merge_request_path(target_project, merge_request)
|
||||
click_link 'Changes'
|
||||
wait_for_requests
|
||||
within first('.js-file-title') do
|
||||
find('[data-testid="edit_file"]').click
|
||||
click_link 'Edit in single-file editor'
|
||||
|
||||
page.within(first('.js-file-title')) do
|
||||
find('.js-diff-more-actions').click
|
||||
find('.js-edit-blob').click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
page.within('.diff-files-holder > div:nth-child(3)') do
|
||||
expect(page).to have_content('Line is wrong')
|
||||
|
||||
find('.js-btn-vue-toggle-comments').click
|
||||
find('.js-diff-more-actions').click
|
||||
click_button 'Hide comments on this file'
|
||||
|
||||
expect(page).not_to have_content('Line is wrong')
|
||||
end
|
||||
|
|
@ -67,7 +68,8 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
# Hide the comment.
|
||||
page.within('.diff-files-holder > div:nth-child(3)') do
|
||||
find('.js-btn-vue-toggle-comments').click
|
||||
find('.js-diff-more-actions').click
|
||||
click_button 'Hide comments on this file'
|
||||
|
||||
expect(page).not_to have_content('Line is wrong')
|
||||
end
|
||||
|
|
@ -80,7 +82,8 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
# Show the comment.
|
||||
page.within('.diff-files-holder > div:nth-child(3)') do
|
||||
find('.js-btn-vue-toggle-comments').click
|
||||
find('.js-diff-more-actions').click
|
||||
click_button 'Show comments on this file'
|
||||
end
|
||||
|
||||
# Now both the comments should be shown.
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
|
||||
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
|
||||
expect(page).to have_selector("[id=\"#{changelog_id}\"] [data-testid='edit_file']")
|
||||
expect(page).to have_selector("[id=\"#{changelog_id}\"] .js-edit-blob", visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -73,7 +73,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
|
|||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
|
||||
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
|
||||
find("[id=\"#{changelog_id}\"] [data-testid=\"edit_file\"").click
|
||||
find("[id=\"#{changelog_id}\"] .js-diff-more-actions").click
|
||||
find("[id=\"#{changelog_id}\"] .js-edit-blob").click
|
||||
|
||||
expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
|
||||
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,8 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
it 'can add and remove suggestions from a batch' do
|
||||
files.each_with_index do |file, index|
|
||||
page.within("[id='#{file[:hash]}']") do
|
||||
find("button[title='Show full file']").click
|
||||
find('.js-diff-more-actions').click
|
||||
click_button 'Show full file'
|
||||
wait_for_requests
|
||||
|
||||
click_diff_line(find("[id='#{file[:line_code]}']"))
|
||||
|
|
|
|||
|
|
@ -20,22 +20,14 @@ RSpec.describe 'Editing file blob', :js do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
def edit_and_commit(commit_changes: true)
|
||||
def edit_and_commit(commit_changes: true, is_diff: false)
|
||||
wait_for_requests
|
||||
find('.js-edit-blob').click
|
||||
|
||||
fill_and_commit(commit_changes)
|
||||
end
|
||||
if is_diff
|
||||
first('.js-diff-more-actions').click
|
||||
end
|
||||
|
||||
def mr_edit_and_commit(commit_changes: true)
|
||||
wait_for_requests
|
||||
find('[data-testid="edit_file"]').click
|
||||
click_link 'Edit in single-file editor'
|
||||
|
||||
fill_and_commit(commit_changes)
|
||||
end
|
||||
|
||||
def fill_and_commit(commit_changes)
|
||||
first('.js-edit-blob').click
|
||||
fill_editor(content: 'class NextFeature\\nend\\n')
|
||||
|
||||
if commit_changes
|
||||
|
|
@ -51,7 +43,7 @@ RSpec.describe 'Editing file blob', :js do
|
|||
context 'from MR diff' do
|
||||
before do
|
||||
visit diffs_project_merge_request_path(project, merge_request)
|
||||
mr_edit_and_commit
|
||||
edit_and_commit(is_diff: true)
|
||||
end
|
||||
|
||||
it 'returns me to the mr' do
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1152,4 +1152,44 @@ describe('Api', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackRedisHllUserEvent', () => {
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/usage_data/increment_unique_users`;
|
||||
|
||||
const event = 'dummy_event';
|
||||
const postData = { event };
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
describe('when usage data increment unique users is called with feature flag disabled', () => {
|
||||
beforeEach(() => {
|
||||
gon.features = { ...gon.features, usageDataApi: false };
|
||||
});
|
||||
|
||||
it('returns null', () => {
|
||||
jest.spyOn(axios, 'post');
|
||||
mock.onPost(expectedUrl).replyOnce(httpStatus.OK, true);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledTimes(0);
|
||||
expect(Api.trackRedisHllUserEvent(event)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when usage data increment unique users is called', () => {
|
||||
beforeEach(() => {
|
||||
gon.features = { ...gon.features, usageDataApi: true };
|
||||
});
|
||||
|
||||
it('resolves the Promise', () => {
|
||||
jest.spyOn(axios, 'post');
|
||||
mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true);
|
||||
|
||||
return Api.trackRedisHllUserEvent(event).then(({ data }) => {
|
||||
expect(data).toEqual(true);
|
||||
expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, { headers });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
import EditButton from '~/diffs/components/edit_button.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import diffDiscussionsMockData from '../mock_data/diff_discussions';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
|
|
@ -76,16 +74,7 @@ describe('DiffFileHeader component', () => {
|
|||
const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' });
|
||||
const findViewFileButton = () => wrapper.find({ ref: 'viewButton' });
|
||||
const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
|
||||
const hasZDropdownMenuClass = () => wrapper.classes('gl-z-dropdown-menu!');
|
||||
|
||||
const findIconByName = iconName => {
|
||||
const icons = wrapper.findAll(GlIcon).filter(w => w.props('name') === iconName);
|
||||
if (icons.length === 0) return icons;
|
||||
if (icons.length > 1) {
|
||||
throw new Error(`Multiple icons found for ${iconName}`);
|
||||
}
|
||||
return icons.at(0);
|
||||
};
|
||||
const findEditButton = () => wrapper.find({ ref: 'editButton' });
|
||||
|
||||
const createComponent = props => {
|
||||
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
|
||||
|
|
@ -152,10 +141,6 @@ describe('DiffFileHeader component', () => {
|
|||
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not have z dropdown menu class', () => {
|
||||
expect(hasZDropdownMenuClass()).toBe(false);
|
||||
});
|
||||
|
||||
describe('for submodule', () => {
|
||||
const submoduleDiffFile = {
|
||||
...diffFile,
|
||||
|
|
@ -208,16 +193,6 @@ describe('DiffFileHeader component', () => {
|
|||
describe('for any file', () => {
|
||||
const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed');
|
||||
|
||||
it('when edit button emits showForkMessage event it is re-emitted', () => {
|
||||
createComponent({
|
||||
addMergeRequestButtons: true,
|
||||
});
|
||||
wrapper.find(EditButton).vm.$emit('showForkMessage');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.emitted().showForkMessage).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('for mode_changed file mode displays mode changes', () => {
|
||||
createComponent({
|
||||
diffFile: {
|
||||
|
|
@ -276,16 +251,16 @@ describe('DiffFileHeader component', () => {
|
|||
});
|
||||
it('should not render edit button', () => {
|
||||
createComponent({ addMergeRequestButtons: false });
|
||||
expect(wrapper.find(EditButton).exists()).toBe(false);
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when addMergeRequestButtons is true', () => {
|
||||
describe('without discussions', () => {
|
||||
it('renders a disabled toggle discussions button', () => {
|
||||
it('does not render a toggle discussions button', () => {
|
||||
diffHasDiscussionsResultMock.mockReturnValue(false);
|
||||
createComponent({ addMergeRequestButtons: true });
|
||||
expect(findToggleDiscussionsButton().attributes('disabled')).toBe('true');
|
||||
expect(findToggleDiscussionsButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -293,7 +268,7 @@ describe('DiffFileHeader component', () => {
|
|||
it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
|
||||
diffHasDiscussionsResultMock.mockReturnValue(true);
|
||||
createComponent({ addMergeRequestButtons: true });
|
||||
expect(findToggleDiscussionsButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findToggleDiscussionsButton().exists()).toBe(true);
|
||||
findToggleDiscussionsButton().vm.$emit('click');
|
||||
expect(
|
||||
mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers,
|
||||
|
|
@ -305,28 +280,7 @@ describe('DiffFileHeader component', () => {
|
|||
createComponent({
|
||||
addMergeRequestButtons: true,
|
||||
});
|
||||
expect(wrapper.find(EditButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when edit button opens', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ addMergeRequestButtons: true });
|
||||
wrapper.find(EditButton).vm.$emit('open');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('should add z dropdown menu class when edit button opens', async () => {
|
||||
expect(hasZDropdownMenuClass()).toBe(true);
|
||||
});
|
||||
|
||||
it('when closes again, should remove class', async () => {
|
||||
wrapper.find(EditButton).vm.$emit('close');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(hasZDropdownMenuClass()).toBe(false);
|
||||
});
|
||||
expect(findEditButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('view on environment button', () => {
|
||||
|
|
@ -360,7 +314,7 @@ describe('DiffFileHeader component', () => {
|
|||
});
|
||||
|
||||
it('should not render edit button', () => {
|
||||
expect(wrapper.find(EditButton).exists()).toBe(false);
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('with file blob', () => {
|
||||
|
|
@ -371,7 +325,7 @@ describe('DiffFileHeader component', () => {
|
|||
addMergeRequestButtons: true,
|
||||
});
|
||||
expect(findViewFileButton().attributes('href')).toBe(viewPath);
|
||||
expect(findViewFileButton().attributes('title')).toEqual(
|
||||
expect(findViewFileButton().text()).toEqual(
|
||||
`View file @ ${diffFile.content_sha.substr(0, 8)}`,
|
||||
);
|
||||
});
|
||||
|
|
@ -401,21 +355,6 @@ describe('DiffFileHeader component', () => {
|
|||
addMergeRequestButtons: true,
|
||||
};
|
||||
|
||||
it.each`
|
||||
iconName | isShowingFullFile
|
||||
${'doc-expand'} | ${false}
|
||||
${'doc-changes'} | ${true}
|
||||
`(
|
||||
'shows $iconName when isShowingFullFile set to $isShowingFullFile',
|
||||
({ iconName, isShowingFullFile }) => {
|
||||
createComponent({
|
||||
...fullyNotExpandedFileProps,
|
||||
diffFile: { ...fullyNotExpandedFileProps.diffFile, isShowingFullFile },
|
||||
});
|
||||
expect(findIconByName(iconName).exists()).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it('renders expand to full file button if not showing full file already', () => {
|
||||
createComponent(fullyNotExpandedFileProps);
|
||||
expect(findExpandButton().exists()).toBe(true);
|
||||
|
|
@ -481,7 +420,7 @@ describe('DiffFileHeader component', () => {
|
|||
|
||||
it('does not show edit button', () => {
|
||||
createComponent({ diffFile: { ...diffFile, deleted_file: true } });
|
||||
expect(wrapper.find(EditButton).exists()).toBe(false);
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import EditButton from '~/diffs/components/edit_button.vue';
|
||||
|
||||
jest.mock('lodash/uniqueId', () => (str = '') => `${str}fake`);
|
||||
|
||||
const TOOLTIP_ID = 'edit_button_tooltip_fake';
|
||||
const EDIT_ITEM = {
|
||||
href: 'test-path',
|
||||
text: 'Edit in single-file editor',
|
||||
};
|
||||
const IDE_EDIT_ITEM = {
|
||||
href: 'ide-test-path',
|
||||
text: 'Edit in Web IDE',
|
||||
};
|
||||
|
||||
describe('EditButton', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}, mountFn = shallowMount) => {
|
||||
wrapper = mountFn(EditButton, {
|
||||
propsData: {
|
||||
editPath: EDIT_ITEM.href,
|
||||
ideEditPath: IDE_EDIT_ITEM.href,
|
||||
canCurrentUserFork: false,
|
||||
...props,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
|
||||
const findDropdown = () => wrapper.find(GlDeprecatedDropdown);
|
||||
const parseDropdownItems = () =>
|
||||
wrapper.findAll(GlDeprecatedDropdownItem).wrappers.map(x => ({
|
||||
text: x.text(),
|
||||
href: x.attributes('href'),
|
||||
}));
|
||||
const triggerShow = () => {
|
||||
const event = new Event('');
|
||||
jest.spyOn(event, 'preventDefault');
|
||||
|
||||
findDropdown().vm.$emit('show', event);
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
it.each`
|
||||
props | expectedItems
|
||||
${{}} | ${[EDIT_ITEM, IDE_EDIT_ITEM]}
|
||||
${{ editPath: '' }} | ${[IDE_EDIT_ITEM]}
|
||||
${{ ideEditPath: '' }} | ${[EDIT_ITEM]}
|
||||
`('should render items with=$props', ({ props, expectedItems }) => {
|
||||
createComponent(props);
|
||||
|
||||
expect(parseDropdownItems()).toEqual(expectedItems);
|
||||
});
|
||||
|
||||
describe('with default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, mount);
|
||||
});
|
||||
|
||||
it('does not have tooltip', () => {
|
||||
expect(getTooltip()).toEqual({ id: TOOLTIP_ID, title: 'Edit file in...' });
|
||||
});
|
||||
|
||||
it('shows pencil dropdown', () => {
|
||||
expect(wrapper.find(GlIcon).props('name')).toBe('pencil');
|
||||
expect(wrapper.find('.gl-dropdown-caret').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
event | expectedEmit | expectedRootEmit
|
||||
${'show'} | ${'open'} | ${[['bv::hide::tooltip', TOOLTIP_ID]]}
|
||||
${'hide'} | ${'close'} | ${[]}
|
||||
`('when dropdown emits $event', ({ event, expectedEmit, expectedRootEmit }) => {
|
||||
let rootEmitSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
rootEmitSpy = jest.spyOn(wrapper.vm.$root, '$emit');
|
||||
|
||||
findDropdown().vm.$emit(event);
|
||||
});
|
||||
|
||||
it(`emits ${expectedEmit}`, () => {
|
||||
expect(wrapper.emitted(expectedEmit)).toEqual([[]]);
|
||||
});
|
||||
|
||||
it(`emits root = ${JSON.stringify(expectedRootEmit)}`, () => {
|
||||
expect(rootEmitSpy.mock.calls).toEqual(expectedRootEmit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with cant modify blob and can fork', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
canModifyBlob: false,
|
||||
canCurrentUserFork: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('when try to open, emits showForkMessage', () => {
|
||||
expect(wrapper.emitted('showForkMessage')).toBeUndefined();
|
||||
|
||||
const event = triggerShow();
|
||||
|
||||
expect(wrapper.emitted('showForkMessage')).toEqual([[]]);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(wrapper.emitted('open')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with editPath is falsey', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
editPath: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable dropdown', () => {
|
||||
expect(findDropdown().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('should have tooltip', () => {
|
||||
expect(getTooltip()).toEqual({
|
||||
id: TOOLTIP_ID,
|
||||
title: "Can't edit as source branch was deleted",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -139,50 +139,74 @@ describe('Diffs Module Getters', () => {
|
|||
|
||||
describe('diffHasExpandedDiscussions', () => {
|
||||
it('returns true when one of the discussions is expanded', () => {
|
||||
discussionMock1.expanded = false;
|
||||
const diffFile = {
|
||||
parallel_diff_lines: [],
|
||||
highlighted_diff_lines: [
|
||||
{
|
||||
discussions: [discussionMock, discussionMock],
|
||||
discussionsExpanded: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
getters.diffHasExpandedDiscussions(localState, {
|
||||
getDiffFileDiscussions: () => [discussionMock, discussionMock],
|
||||
})(diffFileMock),
|
||||
).toEqual(true);
|
||||
expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when there are no discussions', () => {
|
||||
expect(
|
||||
getters.diffHasExpandedDiscussions(localState, { getDiffFileDiscussions: () => [] })(
|
||||
diffFileMock,
|
||||
),
|
||||
).toEqual(false);
|
||||
const diffFile = {
|
||||
parallel_diff_lines: [],
|
||||
highlighted_diff_lines: [
|
||||
{
|
||||
discussions: [],
|
||||
discussionsExpanded: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when no discussion is expanded', () => {
|
||||
discussionMock.expanded = false;
|
||||
discussionMock1.expanded = false;
|
||||
const diffFile = {
|
||||
parallel_diff_lines: [],
|
||||
highlighted_diff_lines: [
|
||||
{
|
||||
discussions: [discussionMock, discussionMock],
|
||||
discussionsExpanded: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
getters.diffHasExpandedDiscussions(localState, {
|
||||
getDiffFileDiscussions: () => [discussionMock, discussionMock1],
|
||||
})(diffFileMock),
|
||||
).toEqual(false);
|
||||
expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('diffHasDiscussions', () => {
|
||||
it('returns true when getDiffFileDiscussions returns discussions', () => {
|
||||
expect(
|
||||
getters.diffHasDiscussions(localState, {
|
||||
getDiffFileDiscussions: () => [discussionMock],
|
||||
})(diffFileMock),
|
||||
).toEqual(true);
|
||||
const diffFile = {
|
||||
parallel_diff_lines: [],
|
||||
highlighted_diff_lines: [
|
||||
{
|
||||
discussions: [discussionMock, discussionMock],
|
||||
discussionsExpanded: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getters.diffHasDiscussions(localState)(diffFile)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when getDiffFileDiscussions returns no discussions', () => {
|
||||
expect(
|
||||
getters.diffHasDiscussions(localState, {
|
||||
getDiffFileDiscussions: () => [],
|
||||
})(diffFileMock),
|
||||
).toEqual(false);
|
||||
const diffFile = {
|
||||
parallel_diff_lines: [],
|
||||
highlighted_diff_lines: [
|
||||
{
|
||||
discussions: [],
|
||||
discussionsExpanded: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getters.diffHasDiscussions(localState)(diffFile)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue