Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-03 18:08:28 +00:00
parent 3dcdea9502
commit 8b0ef13236
382 changed files with 2076 additions and 1003 deletions

View File

@ -1,5 +1,9 @@
Please view this file on the master branch, on stable branches it's out of date.
## 13.0.4 (2020-06-03)
- No changes.
## 13.0.3 (2020-05-29)
- No changes.

View File

@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.0.4 (2020-06-03)
### Security (1 change)
- Prevent fetching repository code with unauthorized ci token.
## 13.0.3 (2020-05-29)
### Fixed (8 changes, 1 of them is from the community)

View File

@ -177,7 +177,11 @@ export default {
{{ createIssueError }}
</gl-alert>
<div v-if="loading"><gl-loading-icon size="lg" class="gl-mt-5" /></div>
<div v-if="alert" class="alert-management-details gl-relative">
<div
v-if="alert"
class="alert-management-details gl-relative"
:class="{ 'pr-8': sidebarCollapsed }"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row"
>

View File

@ -13,7 +13,7 @@ export default {
required: true,
},
label: {
type: String,
type: Number,
required: false,
default: null,
},

View File

@ -1,14 +1,19 @@
<script>
import { ApolloMutation } from 'vue-apollo';
import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/createNote.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
export default {
components: {
@ -16,6 +21,14 @@ export default {
DesignNote,
ReplyPlaceholder,
DesignReplyForm,
GlIcon,
GlLoadingIcon,
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [allVersionsMixin],
props: {
@ -31,15 +44,15 @@ export default {
type: String,
required: true,
},
discussionIndex: {
type: Number,
required: true,
},
markdownPreviewPath: {
type: String,
required: false,
default: '',
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
},
apollo: {
activeDiscussion: {
@ -49,6 +62,7 @@ export default {
// We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
// We don't want scrollIntoView to be triggered from the discussion click itself
if (
this.resolvedDiscussionsExpanded &&
discussionId &&
data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
discussionId === this.discussion.notes[0].id
@ -66,6 +80,9 @@ export default {
discussionComment: '',
isFormRendered: false,
activeDiscussion: {},
isResolving: false,
shouldChangeResolvedStatus: false,
areRepliesCollapsed: this.discussion.resolved,
};
},
computed: {
@ -87,6 +104,29 @@ export default {
isDiscussionHighlighted() {
return this.discussion.notes[0].id === this.activeDiscussion.id;
},
resolveCheckboxText() {
return this.discussion.resolved
? s__('DesignManagement|Unresolve thread')
: s__('DesignManagement|Resolve thread');
},
firstNote() {
return this.discussion.notes[0];
},
discussionReplies() {
return this.discussion.notes.slice(1);
},
areRepliesShown() {
return !this.discussion.resolved || !this.areRepliesCollapsed;
},
resolveIconName() {
return this.discussion.resolved ? 'check-circle-filled' : 'check-circle';
},
isRepliesWidgetVisible() {
return this.discussion.resolved && this.discussionReplies.length > 0;
},
isReplyPlaceholderVisible() {
return this.areRepliesShown || !this.discussionReplies.length;
},
},
methods: {
addDiscussionComment(
@ -106,9 +146,12 @@ export default {
onDone() {
this.discussionComment = '';
this.hideForm();
if (this.shouldChangeResolvedStatus) {
this.toggleResolvedStatus();
}
},
onError(err) {
this.$emit('error', err);
onCreateNoteError(err) {
this.$emit('createNoteError', err);
},
hideForm() {
this.isFormRendered = false;
@ -123,6 +166,25 @@ export default {
}
this.isFormRendered = false;
},
toggleResolvedStatus() {
this.isResolving = true;
this.$apollo
.mutate({
mutation: toggleResolveDiscussionMutation,
variables: { id: this.discussion.id, resolve: !this.discussion.resolved },
})
.then(({ data }) => {
if (data.errors?.length > 0) {
this.$emit('resolveDiscussionError', data.errors[0]);
}
})
.catch(err => {
this.$emit('resolveDiscussionError', err);
})
.finally(() => {
this.isResolving = false;
});
},
},
createNoteMutation,
};
@ -130,20 +192,69 @@ export default {
<template>
<div class="design-discussion-wrapper">
<div class="badge badge-pill" type="button">{{ discussionIndex }}</div>
<div
class="design-discussion bordered-box position-relative"
class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
:class="{ resolved: discussion.resolved }"
type="button"
>
{{ discussion.index }}
</div>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
>
<design-note
v-for="note in discussion.notes"
:note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
@error="$emit('updateNoteError', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
v-gl-tooltip
:class="{ 'is-active': discussion.resolved }"
:title="resolveCheckboxText"
:aria-label="resolveCheckboxText"
type="button"
class="line-resolve-btn note-action-button gl-mr-3"
data-testid="resolve-button"
@click.stop="toggleResolvedStatus"
>
<gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" />
<gl-loading-icon v-else inline />
</button>
</template>
<template v-if="discussion.resolved" #resolvedStatus>
<p class="gl-text-gray-700 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
{{ __('Resolved by') }}
<gl-link
class="gl-text-gray-700 gl-text-decoration-none gl-font-sm link-inherit-color"
:href="discussion.resolvedBy.webUrl"
target="_blank"
>{{ discussion.resolvedBy.name }}</gl-link
>
<time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" />
</p>
</template>
</design-note>
<toggle-replies-widget
v-if="isRepliesWidgetVisible"
:collapsed="areRepliesCollapsed"
:replies="discussionReplies"
@toggle="areRepliesCollapsed = !areRepliesCollapsed"
/>
<design-note
v-for="note in discussionReplies"
v-show="areRepliesShown"
:key="note.id"
:note="note"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
@error="$emit('updateNoteError', $event)"
/>
<div class="reply-wrapper">
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder
v-if="!isFormRendered"
class="qa-discussion-reply"
@ -159,7 +270,7 @@ export default {
}"
:update="addDiscussionComment"
@done="onDone"
@error="onError"
@error="onCreateNoteError"
>
<design-reply-form
v-model="discussionComment"
@ -168,9 +279,16 @@ export default {
@submitForm="mutate"
@cancelForm="hideForm"
@onBlur="handleReplyFormBlur"
/>
>
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
<input v-model="shouldChangeResolvedStatus" type="checkbox" />
{{ resolveCheckboxText }}
</label>
</template>
</design-reply-form>
</apollo-mutation>
</div>
</div>
</li>
</ul>
</div>
</template>

View File

@ -54,6 +54,9 @@ export default {
body: this.noteText,
};
},
isEditButtonVisible() {
return !this.isEditing && this.note.userPermissions.adminNote;
},
},
mounted() {
if (this.isNoteLinked) {
@ -107,23 +110,28 @@ export default {
</template>
</span>
</div>
<button
v-if="!isEditing && note.userPermissions.adminNote"
v-gl-tooltip
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
@click="isEditing = true"
>
<gl-icon name="pencil" class="link-highlight" />
</button>
<div class="gl-display-flex">
<slot name="resolveDiscussion"></slot>
<button
v-if="isEditButtonVisible"
v-gl-tooltip
type="button"
:title="__('Edit comment')"
class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
@click="isEditing = true"
>
<gl-icon name="pencil" class="link-highlight" />
</button>
</div>
</div>
<div
v-if="!isEditing"
class="note-text js-note-text md"
data-qa-selector="note_content"
v-html="note.bodyHtml"
></div>
<template v-if="!isEditing">
<div
class="note-text js-note-text md"
data-qa-selector="note_content"
v-html="note.bodyHtml"
></div>
<slot name="resolvedStatus"></slot>
</template>
<apollo-mutation
v-else
#default="{ mutate, loading }"

View File

@ -108,7 +108,8 @@ export default {
</textarea>
</template>
</markdown-field>
<div class="note-form-actions d-flex justify-content-between">
<slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
<gl-deprecated-button
ref="submitButton"
:disabled="!hasValue || isSaving"

View File

@ -0,0 +1,70 @@
<script>
import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
import { __, n__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'ToggleNotesWidget',
components: {
GlIcon,
GlButton,
GlLink,
TimeAgoTooltip,
},
props: {
collapsed: {
type: Boolean,
required: true,
},
replies: {
type: Array,
required: true,
},
},
computed: {
lastReply() {
return this.replies[this.replies.length - 1];
},
iconName() {
return this.collapsed ? 'chevron-right' : 'chevron-down';
},
toggleText() {
return this.collapsed
? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}`
: __('Collapse replies');
},
},
};
</script>
<template>
<li
class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3"
:class="{ expanded: !collapsed }"
data-testid="toggle-comments-wrapper"
>
<gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" />
<gl-button
variant="link"
class="toggle-comments-button gl-ml-2 gl-mr-2"
@click.stop="$emit('toggle')"
>
{{ toggleText }}
</gl-button>
<template v-if="collapsed">
<span class="gl-text-gray-700">{{ __('Last reply by') }}</span>
<gl-link
:href="lastReply.author.webUrl"
target="_blank"
class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
>
{{ lastReply.author.name }}
</gl-link>
<time-ago-tooltip
:time="lastReply.createdAt"
tooltip-placement="bottom"
class="gl-text-gray-700"
/>
</template>
</li>
</template>

View File

@ -33,6 +33,10 @@ export default {
required: false,
default: false,
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
},
apollo: {
activeDiscussion: {
@ -236,6 +240,9 @@ export default {
isNoteInactive(note) {
return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
},
designPinClass(note) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
},
},
};
</script>
@ -254,20 +261,23 @@ export default {
data-qa-selector="design_image_button"
@mouseup="onAddCommentMouseup"
></button>
<design-note-pin
v-for="(note, index) in notes"
:key="note.id"
:label="`${index + 1}`"
:repositioning="isMovingNote(note.id)"
:position="
isMovingNote(note.id) && movingNoteNewPosition
? getNotePositionStyle(movingNoteNewPosition)
: getNotePositionStyle(note.position)
"
:class="{ inactive: isNoteInactive(note) }"
@mousedown.stop="onNoteMousedown($event, note)"
@mouseup.stop="onNoteMouseup(note)"
/>
<template v-for="note in notes">
<design-note-pin
v-if="resolvedDiscussionsExpanded || !note.resolved"
:key="note.id"
:label="note.index"
:repositioning="isMovingNote(note.id)"
:position="
isMovingNote(note.id) && movingNoteNewPosition
? getNotePositionStyle(movingNoteNewPosition)
: getNotePositionStyle(note.position)
"
:class="designPinClass(note)"
@mousedown.stop="onNoteMousedown($event, note)"
@mouseup.stop="onNoteMouseup(note)"
/>
</template>
<design-note-pin
v-if="currentCommentForm"
:position="currentCommentPositionStyle"

View File

@ -35,6 +35,10 @@ export default {
required: false,
default: 1,
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
},
data() {
return {
@ -54,7 +58,10 @@ export default {
},
computed: {
discussionStartingNotes() {
return this.discussions.map(discussion => discussion.notes[0]);
return this.discussions.map(discussion => ({
...discussion.notes[0],
index: discussion.index,
}));
},
currentCommentForm() {
return (this.isAnnotating && this.currentAnnotationPosition) || null;
@ -305,6 +312,7 @@ export default {
:notes="discussionStartingNotes"
:current-comment-form="currentCommentForm"
:disable-commenting="isDraggingDesign"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"
@moveNote="moveNote"

View File

@ -0,0 +1,170 @@
<script>
import { s__ } from '~/locale';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
export default {
components: {
DesignDiscussion,
Participants,
GlCollapse,
GlButton,
GlPopover,
},
props: {
design: {
type: Object,
required: true,
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
},
data() {
return {
isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)),
};
},
computed: {
discussions() {
return extractDiscussions(this.design.discussions);
},
issue() {
return {
...this.design.issue,
webPath: this.design.issue.webPath.substr(1),
};
},
discussionParticipants() {
return extractParticipants(this.issue.participants);
},
resolvedDiscussions() {
return this.discussions.filter(discussion => discussion.resolved);
},
unresolvedDiscussions() {
return this.discussions.filter(discussion => !discussion.resolved);
},
resolvedCommentsToggleIcon() {
return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
},
},
methods: {
handleSidebarClick() {
this.isResolvedCommentsPopoverHidden = true;
Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 });
this.updateActiveDiscussion();
},
updateActiveDiscussion(id) {
this.$apollo.mutate({
mutation: updateActiveDiscussionMutation,
variables: {
id,
source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
},
});
},
closeCommentForm() {
this.comment = '';
this.$emit('closeCommentForm');
},
},
resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'),
cookieKey: 'hide_design_resolved_comments_popover',
};
</script>
<template>
<div class="image-notes" @click="handleSidebarClick">
<h2 class="gl-font-weight-bold gl-mt-0">
{{ issue.title }}
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
:href="issue.webUrl"
>{{ issue.webPath }}</a
>
<participants
:participants="discussionParticipants"
:show-participant-label="false"
class="gl-mb-4"
/>
<h2
v-if="unresolvedDiscussions.length === 0"
class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
data-testid="new-discussion-disclaimer"
>
{{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
</h2>
<design-discussion
v-for="discussion in unresolvedDiscussions"
:key="discussion.id"
:discussion="discussion"
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
data-testid="unresolved-discussion"
@createNoteError="$emit('onDesignDiscussionError', $event)"
@updateNoteError="$emit('updateNoteError', $event)"
@resolveDiscussionError="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
id="resolved-comments"
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
@click="$emit('toggleResolvedComments')"
>{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
</gl-button>
<gl-popover
v-if="!isResolvedCommentsPopoverHidden"
:show="!isResolvedCommentsPopoverHidden"
target="resolved-comments"
container="popovercontainer"
placement="top"
:title="s__('DesignManagement|Resolved Comments')"
>
<p>
{{
s__(
'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below',
)
}}
</p>
<a href="#" rel="noopener noreferrer" target="_blank">{{
s__('DesignManagement|Learn more about resolving comments')
}}</a>
</gl-popover>
<gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
<design-discussion
v-for="discussion in resolvedDiscussions"
:key="discussion.id"
:discussion="discussion"
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
data-testid="resolved-discussion"
@error="$emit('onDesignDiscussionError', $event)"
@updateNoteError="$emit('updateNoteError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
</gl-collapse>
</template>
<slot name="replyForm"></slot>
</div>
</template>

View File

@ -1,6 +1,7 @@
#import "./designNote.fragment.graphql"
#import "./designList.fragment.graphql"
#import "./diffRefs.fragment.graphql"
#import "./discussion_resolved_status.fragment.graphql"
fragment DesignItem on Design {
...DesignListItem
@ -12,6 +13,7 @@ fragment DesignItem on Design {
nodes {
id
replyId
...ResolvedStatus
notes {
nodes {
...DesignNote

View File

@ -10,6 +10,7 @@ fragment DesignNote on Note {
body
bodyHtml
createdAt
resolved
position {
diffRefs {
...DesignDiffRefs

View File

@ -0,0 +1,9 @@
fragment ResolvedStatus on Discussion {
resolvable
resolved
resolvedAt
resolvedBy {
name
webUrl
}
}

View File

@ -0,0 +1,17 @@
#import "../fragments/designNote.fragment.graphql"
#import "../fragments/discussion_resolved_status.fragment.graphql"
mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion {
id
...ResolvedStatus
notes {
nodes {
...DesignNote
}
}
}
errors
}
}

View File

@ -1,17 +1,16 @@
<script>
import { ApolloMutation } from 'vue-apollo';
import Mousetrap from 'mousetrap';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue';
import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignScaler from '../../components/design_scaler.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import appDataQuery from '../../graphql/queries/appData.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
@ -20,7 +19,6 @@ import updateActiveDiscussionMutation from '../../graphql/mutations/update_activ
import {
extractDiscussions,
extractDesign,
extractParticipants,
updateImageDiffNoteOptimisticResponse,
} from '../../utils/design_management_utils';
import {
@ -43,15 +41,14 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
export default {
components: {
ApolloMutation,
DesignReplyForm,
DesignPresentation,
DesignDiscussion,
DesignScaler,
DesignDestroyer,
Toolbar,
DesignReplyForm,
GlLoadingIcon,
GlAlert,
Participants,
DesignSidebar,
},
mixins: [allVersionsMixin],
props: {
@ -69,6 +66,7 @@ export default {
errorMessage: '',
issueIid: '',
scale: 1,
resolvedDiscussionsExpanded: false,
};
},
apollo: {
@ -103,20 +101,17 @@ export default {
return this.$apollo.queries.design.loading && !this.design.filename;
},
discussions() {
if (!this.design.discussions) {
return [];
}
return extractDiscussions(this.design.discussions);
},
discussionParticipants() {
return extractParticipants(this.design.issue.participants);
},
markdownPreviewPath() {
return `/${this.projectPath}/preview_markdown?target_type=Issue`;
},
isSubmitButtonDisabled() {
return this.comment.trim().length === 0;
},
renderDiscussions() {
return this.discussions.length || this.annotationCoordinates;
},
designVariables() {
return {
fullPath: this.projectPath,
@ -144,15 +139,19 @@ export default {
},
};
},
issue() {
return {
...this.design.issue,
webPath: this.design.issue.webPath.substr(1),
};
},
isAnnotating() {
return Boolean(this.annotationCoordinates);
},
resolvedDiscussions() {
return this.discussions.filter(discussion => discussion.resolved);
},
},
watch: {
resolvedDiscussions(val) {
if (!val.length) {
this.resolvedDiscussionsExpanded = false;
}
},
},
mounted() {
Mousetrap.bind('esc', this.closeDesign);
@ -250,6 +249,9 @@ export default {
onDesignDeleteError(e) {
this.onError(designDeletionError({ singular: true }), e);
},
onResolveDiscussionError(e) {
this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
},
openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates;
},
@ -281,6 +283,9 @@ export default {
},
});
},
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
},
createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME,
@ -323,6 +328,7 @@ export default {
:discussions="discussions"
:is-annotating="isAnnotating"
:scale="scale"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"
@moveNote="onMoveNote"
@ -332,33 +338,19 @@ export default {
<design-scaler @scale="scale = $event" />
</div>
</div>
<div class="image-notes" @click="updateActiveDiscussion()">
<h2 class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0">
{{ issue.title }}
</h2>
<a class="text-tertiary text-decoration-none mb-3 d-block" :href="issue.webUrl">{{
issue.webPath
}}</a>
<participants
:participants="discussionParticipants"
:show-participant-label="false"
class="mb-4"
/>
<template v-if="renderDiscussions">
<design-discussion
v-for="(discussion, index) in discussions"
:key="discussion.id"
:discussion="discussion"
:design-id="id"
:noteable-id="design.id"
:discussion-index="index + 1"
:markdown-preview-path="markdownPreviewPath"
@error="onDesignDiscussionError"
@updateNoteError="onUpdateNoteError"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
<design-sidebar
:design="design"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:markdown-preview-path="markdownPreviewPath"
@onDesignDiscussionError="onDesignDiscussionError"
@onCreateImageDiffNoteError="onCreateImageDiffNoteError"
@updateNoteError="onUpdateNoteError"
@resolveDiscussionError="onResolveDiscussionError"
@toggleResolvedComments="toggleResolvedComments"
>
<template #replyForm>
<apollo-mutation
v-if="annotationCoordinates"
v-if="isAnnotating"
#default="{ mutate, loading }"
:mutation="$options.createImageDiffNoteMutation"
:variables="{
@ -374,13 +366,9 @@ export default {
:markdown-preview-path="markdownPreviewPath"
@submitForm="mutate"
@cancelForm="closeCommentForm"
/>
</apollo-mutation>
</template>
<h2 v-else class="new-discussion-disclaimer gl-font-base m-0">
{{ __("Click the image where you'd like to start a new discussion") }}
</h2>
</div>
/> </apollo-mutation
></template>
</design-sidebar>
</template>
</div>
</template>

View File

@ -95,6 +95,10 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
__typename: 'Discussion',
id: createImageDiffNote.note.discussion.id,
replyId: createImageDiffNote.note.discussion.replyId,
resolvable: true,
resolved: false,
resolvedAt: null,
resolvedBy: null,
notes: {
__typename: 'NoteConnection',
nodes: [createImageDiffNote.note],

View File

@ -21,8 +21,9 @@ export const extractNodes = elements => elements.edges.map(({ node }) => node);
*/
export const extractDiscussions = discussions =>
discussions.nodes.map(discussion => ({
discussions.nodes.map((discussion, index) => ({
...discussion,
index: index + 1,
notes: discussion.notes.nodes,
}));

View File

@ -11,7 +11,7 @@ import {
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
} from '../constants';
} from '../constants/index';
export default {
components: {

View File

@ -8,7 +8,7 @@ import {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
} from '../constants';
} from '../constants/index';
export default {
name: 'ImageListrow',

View File

@ -3,7 +3,12 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { COPY_LOGIN_TITLE, COPY_BUILD_TITLE, COPY_PUSH_TITLE, QUICK_START } from '../constants';
import {
COPY_LOGIN_TITLE,
COPY_BUILD_TITLE,
COPY_PUSH_TITLE,
QUICK_START,
} from '../constants/index';
export default {
name: 'ProjectEmptyState',

View File

@ -9,7 +9,7 @@ import {
EXPIRATION_POLICY_WILL_RUN_IN,
EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
} from '../constants';
} from '../constants/index';
export default {
components: {

View File

@ -1,131 +0,0 @@
import { s__ } from '~/locale';
// List page
export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry');
export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error');
export const CONNECTION_ERROR_MESSAGE = s__(
`ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}`,
);
export const LIST_INTRO_TEXT = s__(
`ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
);
export const LIST_DELETE_BUTTON_DISABLED = s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
);
export const REMOVE_REPOSITORY_LABEL = s__('ContainerRegistry|Remove repository');
export const REMOVE_REPOSITORY_MODAL_TEXT = s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
);
export const ROW_SCHEDULED_FOR_DELETION = s__(
`ContainerRegistry|This image repository is scheduled for deletion`,
);
export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the repository list.',
);
export const FETCH_TAGS_LIST_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the tags list.',
);
export const DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again.',
);
export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
`ContainerRegistry|There was an error during the deletion of this image repository, please try again.`,
);
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
);
// Image details page
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
export const DELETE_TAG_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while marking the tag for deletion.',
);
export const DELETE_TAG_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tag successfully marked for deletion.',
);
export const DELETE_TAGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while marking the tags for deletion.',
);
export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
);
export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10;
export const GROUP_PAGE_TYPE = 'groups';
export const LIST_KEY_TAG = 'name';
export const LIST_KEY_IMAGE_ID = 'short_revision';
export const LIST_KEY_SIZE = 'total_size';
export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox';
export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
// Expiration policies
export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
'ContainerRegistry|Expiration policy will run in %{time}',
);
export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
'ContainerRegistry|Expiration policy is disabled',
);
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
);
// Quick Start
export const QUICK_START = s__('ContainerRegistry|CLI Commands');
export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');
export const COPY_BUILD_TITLE = s__('ContainerRegistry|Copy build command');
export const PUSH_COMMAND_LABEL = s__('ContainerRegistry|Push an image');
export const COPY_PUSH_TITLE = s__('ContainerRegistry|Copy push command');
// Image state
export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';

View File

@ -0,0 +1,49 @@
import { s__ } from '~/locale';
// Translations strings
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
export const DELETE_TAG_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while marking the tag for deletion.',
);
export const DELETE_TAG_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tag successfully marked for deletion.',
);
export const DELETE_TAGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while marking the tags for deletion.',
);
export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
);
export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
// Parameters
export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10;
export const GROUP_PAGE_TYPE = 'groups';
export const LIST_KEY_TAG = 'name';
export const LIST_KEY_IMAGE_ID = 'short_revision';
export const LIST_KEY_SIZE = 'total_size';
export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox';

View File

@ -0,0 +1,11 @@
import { s__ } from '~/locale';
export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
'ContainerRegistry|Expiration policy will run in %{time}',
);
export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
'ContainerRegistry|Expiration policy is disabled',
);
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
);

View File

@ -0,0 +1,4 @@
export * from './expiration_policies';
export * from './quick_start';
export * from './list';
export * from './details';

View File

@ -0,0 +1,48 @@
import { s__ } from '~/locale';
// Translations strings
export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry');
export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error');
export const CONNECTION_ERROR_MESSAGE = s__(
`ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}`,
);
export const LIST_INTRO_TEXT = s__(
`ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
);
export const LIST_DELETE_BUTTON_DISABLED = s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
);
export const REMOVE_REPOSITORY_LABEL = s__('ContainerRegistry|Remove repository');
export const REMOVE_REPOSITORY_MODAL_TEXT = s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
);
export const ROW_SCHEDULED_FOR_DELETION = s__(
`ContainerRegistry|This image repository is scheduled for deletion`,
);
export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the repository list.',
);
export const FETCH_TAGS_LIST_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the tags list.',
);
export const DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again.',
);
export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
`ContainerRegistry|There was an error during the deletion of this image repository, please try again.`,
);
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
);
// Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';

View File

@ -0,0 +1,9 @@
import { s__ } from '~/locale';
export const QUICK_START = s__('ContainerRegistry|CLI Commands');
export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');
export const COPY_BUILD_TITLE = s__('ContainerRegistry|Copy build command');
export const PUSH_COMMAND_LABEL = s__('ContainerRegistry|Push an image');
export const COPY_PUSH_TITLE = s__('ContainerRegistry|Copy push command');

View File

@ -45,7 +45,7 @@ import {
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '../constants';
} from '../constants/index';
export default {
components: {

View File

@ -29,7 +29,7 @@ import {
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
} from '../constants';
} from '../constants/index';
export default {
name: 'RegistryListApp',

View File

@ -1,9 +1,9 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import { s__ } from '~/locale';
import List from './pages/list.vue';
import Details from './pages/details.vue';
import { decodeAndParse } from './utils';
import { CONTAINER_REGISTRY_TITLE } from './constants/index';
Vue.use(VueRouter);
@ -17,7 +17,7 @@ export default function createRouter(base) {
path: '/',
component: List,
meta: {
nameGenerator: () => s__('ContainerRegistry|Container Registry'),
nameGenerator: () => CONTAINER_REGISTRY_TITLE,
root: true,
},
},

View File

@ -6,7 +6,7 @@ import {
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE,
} from '../constants';
} from '../constants/index';
import { decodeAndParse } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);

View File

@ -1,6 +1,6 @@
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_FAILED_DELETED_STATUS } from '../constants';
import { IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_FAILED_DELETED_STATUS } from '../constants/index';
export default {
[types.SET_INITIAL_STATE](state, config) {

View File

@ -20,6 +20,20 @@
}
}
}
.badge.badge-pill {
display: flex;
height: 28px;
width: 28px;
background-color: $blue-400;
color: $white;
border: $white 1px solid;
border-radius: 50%;
&.resolved {
background-color: $gray-700;
}
}
}
.design-presentation-wrapper {
@ -52,14 +66,31 @@
min-width: 400px;
flex-basis: 28%;
.link-inherit-color {
&:hover,
&:active,
&:focus {
color: inherit;
text-decoration: none;
}
}
.toggle-comments {
line-height: 20px;
border-top: 1px solid $border-color;
&.expanded {
border-bottom: 1px solid $border-color;
}
.toggle-comments-button:focus {
text-decoration: none;
color: $blue-600;
}
}
.badge.badge-pill {
margin-left: $gl-padding;
background-color: $blue-400;
color: $white;
border: $white 1px solid;
min-height: 28px;
padding: 7px 10px;
border-radius: $gl-padding;
}
.design-discussion {

View File

@ -9,40 +9,6 @@ module IssuesHelper
classes.join(' ')
end
def url_for_issue(issue_iid, project = @project, options = {})
return '' if project.nil?
url =
if options[:internal]
url_for_internal_issue(issue_iid, project, options)
else
url_for_tracker_issue(issue_iid, project, options)
end
# Ensure we return a valid URL to prevent possible XSS.
URI.parse(url).to_s
rescue URI::InvalidURIError
''
end
def url_for_tracker_issue(issue_iid, project, options)
if options[:only_path]
project.issues_tracker.issue_path(issue_iid)
else
project.issues_tracker.issue_url(issue_iid)
end
end
def url_for_internal_issue(issue_iid, project = @project, options = {})
helpers = Gitlab::Routing.url_helpers
if options[:only_path]
helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue_iid)
else
helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue_iid)
end
end
def status_box_class(item)
if item.try(:expired?)
'status-box-expired'
@ -168,11 +134,6 @@ module IssuesHelper
def show_moved_service_desk_issue_warning?(issue)
false
end
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
module_function :url_for_internal_issue
module_function :url_for_tracker_issue
end
IssuesHelper.prepend_if_ee('EE::IssuesHelper')

View File

@ -450,6 +450,7 @@ class ProjectPolicy < BasePolicy
rule { repository_disabled }.policy do
prevent :push_code
prevent :download_code
prevent :build_download_code
prevent :fork_project
prevent :read_commit_status
prevent :read_pipeline

View File

@ -0,0 +1,5 @@
---
title: "[Frontend] Resolvable design discussions"
merge_request: 32399
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix sidebar spacing for alert details
merge_request: 33630
author:
type: fixed

View File

@ -255,7 +255,7 @@ module.exports = {
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
maxInitialRequests: 4,
maxInitialRequests: 20,
cacheGroups: {
default: false,
common: () => ({

View File

@ -16,6 +16,7 @@ exceptions:
- AWS
- CNAME
- CSS
- CSV
- DNS
- GET
- GNU

View File

@ -1,30 +0,0 @@
---
extends: existence
message: "Remove profanity: '%s'"
ignorecase: true
level: error
tokens:
- 'arse(hole)?'
- 'ass(hole)?'
- 'bastard'
- 'bitch'
- 'bloody'
- 'bollocks'
- 'bugger'
- 'cocksucker'
- 'crap'
- 'cunt'
- 'damn'
- 'eff(ing)?'
- 'fart'
- 'fuck(er|ing)?'
- 'goddamn(it?|ed)'
- 'hell'
- 'horseshit'
- 'motherfuck(ers?|ing)'
- 'piss(ing)?'
- 'shit'
- 'tits'
- 'turd'
- 'twat'
- 'wank(er|ing)?'

View File

@ -8,8 +8,8 @@ For a full list of reference architectures, see
> - **High Availability:** True
> - **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|------------------------|
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|----------------|
| GitLab Rails ([1](#footnotes)) | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge | F32s 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 |
@ -74,6 +74,3 @@ For a full list of reference architectures, see
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -7,7 +7,7 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 1,000
> - **High Availability:** False
| Users | Configuration([8](#footnotes)) | GCP | AWS([9](#footnotes)) | Azure([9](#footnotes)) |
| Users | Configuration([8](#footnotes)) | GCP | AWS | Azure |
|-------|--------------------------------|---------------|----------------------|------------------------|
| 100 | 2 vCPU, 7.2GB Memory | n1-standard-2 | m5.large | D2s v3 |
| 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
@ -77,6 +77,3 @@ added performance and reliability at a reduced complexity cost.
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -8,8 +8,8 @@ For a full list of reference architectures, see
> - **High Availability:** True
> - **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|------------------------|
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|----------------|
| GitLab Rails ([1](#footnotes)) | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge | F32s 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 |
@ -74,6 +74,3 @@ For a full list of reference architectures, see
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -8,7 +8,7 @@ For a full list of reference architectures, see
> - **High Availability:** False
> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|----------------|
| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
| Object Storage ([4](#footnotes)) | - | - | - | - | - |
@ -85,6 +85,3 @@ For a full list of reference architectures, see
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -14,8 +14,8 @@ following the [2,000-user reference architecture](2k_users.md).
> - **High Availability:** True
> - **Test RPS rates:** API: 60 RPS, Web: 6 RPS, Git: 6 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|------------------------|
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|----------------|
| GitLab Rails ([1](#footnotes)) | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | F8s 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 |
@ -77,6 +77,3 @@ following the [2,000-user reference architecture](2k_users.md).
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -8,8 +8,8 @@ For a full list of reference architectures, see
> - **High Availability:** True
> - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|------------------------|
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|----------------|
| GitLab Rails ([1](#footnotes)) | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge | F32s 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 |
@ -74,6 +74,3 @@ For a full list of reference architectures, see
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -8,8 +8,8 @@ For a full list of reference architectures, see
> - **High Availability:** True
> - **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|------------------------|
| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|----------------|
| GitLab Rails ([1](#footnotes)) | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge | F16s 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 |
@ -71,6 +71,3 @@ For a full list of reference architectures, see
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.

View File

@ -163,7 +163,7 @@ column.
| Repmgr | PostgreSQL cluster management and failover | [PostgreSQL and Repmgr configuration](../high_availability/database.md) | Yes |
| [Redis](../../development/architecture.md#redis) ([3](#footnotes)) | Key/value store for fast data lookup and caching | [Redis configuration](../high_availability/redis.md) | Yes |
| Redis Sentinel | Redis | [Redis Sentinel configuration](../high_availability/redis.md) | Yes |
| [Gitaly](../../development/architecture.md#gitaly) ([2](#footnotes)) ([7](#footnotes)) ([10](#footnotes)) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#running-gitaly-on-its-own-server) | Yes |
| [Gitaly](../../development/architecture.md#gitaly) ([2](#footnotes)) ([7](#footnotes)) ([9](#footnotes)) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#running-gitaly-on-its-own-server) | Yes |
| [Sidekiq](../../development/architecture.md#sidekiq) | Asynchronous/background jobs | [Sidekiq configuration](../high_availability/sidekiq.md) | Yes |
| [GitLab application services](../../development/architecture.md#unicorn)([1](#footnotes)) | Puma/Unicorn, Workhorse, GitLab Shell - serves front-end requests (UI, API, Git over HTTP/SSH) | [GitLab app scaling configuration](../high_availability/gitlab.md) | Yes |
| [Prometheus](../../development/architecture.md#prometheus) and [Grafana](../../development/architecture.md#grafana) | GitLab environment monitoring | [Monitoring node for scaling](../high_availability/monitoring_node.md) | Yes |
@ -217,9 +217,6 @@ column.
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
and may change in the future. They have not yet been tested and validated.
1. From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab
14.0, support for NFS for Git repositories is scheduled to be removed.
Upgrade to [Gitaly Cluster](../gitaly/praefect.md) as soon as possible.

View File

@ -121,13 +121,15 @@ Example response:
"id":2,
"name":"awesome-v0.2.msi",
"url":"http://192.168.10.15:3000/msi",
"external":true
"external":true,
"link_type":"other"
},
{
"id":1,
"name":"awesome-v0.2.dmg",
"url":"http://192.168.10.15:3000",
"external":true
"external":true,
"link_type":"other"
}
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json"
@ -323,7 +325,8 @@ Example response:
"id":3,
"name":"hoge",
"url":"https://gitlab.example.com/root/awesome-app/-/tags/v0.11.1/binaries/linux-amd64",
"external":true
"external":true,
"link_type":"other"
}
]
},
@ -357,13 +360,14 @@ POST /projects/:id/releases
| `assets:links:name`| string | required by: `assets:links` | The name of the link. |
| `assets:links:url` | string | required by: `assets:links` | The URL of the link. |
| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases.md).
| `assets:links:link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`.
| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
Example request:
```shell
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \
--data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "milestones": ["v1.0", "v1.0-rc"], "assets": { "links": [{ "name": "hoge", "url": "https://google.com", "filepath": "/binaries/linux-amd64" }] } }' \
--data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "milestones": ["v1.0", "v1.0-rc"], "assets": { "links": [{ "name": "hoge", "url": "https://google.com", "filepath": "/binaries/linux-amd64", "link_type":"other" }] } }' \
--request POST https://gitlab.example.com/api/v4/projects/24/releases
```
@ -465,7 +469,8 @@ Example response:
"id":3,
"name":"hoge",
"url":"https://gitlab.example.com/root/awesome-app/-/tags/v0.11.1/binaries/linux-amd64",
"external":true
"external":true,
"link_type":"other"
}
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.3/evidence.json"

View File

@ -32,13 +32,15 @@ Example response:
"id":2,
"name":"awesome-v0.2.msi",
"url":"http://192.168.10.15:3000/msi",
"external":true
"external":true,
"link_type":"other"
},
{
"id":1,
"name":"awesome-v0.2.dmg",
"url":"http://192.168.10.15:3000",
"external":true
"external":true,
"link_type":"other"
}
]
```
@ -70,7 +72,8 @@ Example response:
"id":1,
"name":"awesome-v0.2.dmg",
"url":"http://192.168.10.15:3000",
"external":true
"external":true,
"link_type":"other"
}
```
@ -87,7 +90,8 @@ POST /projects/:id/releases/:tag_name/assets/links
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag associated with the Release. |
| `name` | string | yes | The name of the link. |
| `url` | string | yes | The URL of the link. |
| `url` | string | yes | The URL of the link. |
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
Example request:
@ -106,7 +110,8 @@ Example response:
"id":1,
"name":"awesome-v0.2.dmg",
"url":"http://192.168.10.15:3000",
"external":true
"external":true,
"link_type":"other"
}
```
@ -122,9 +127,10 @@ PUT /projects/:id/releases/:tag_name/assets/links/:link_id
| ------------- | -------------- | -------- | --------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag associated with the Release. |
| `link_id` | integer | yes | The ID of the link. |
| `link_id` | integer | yes | The ID of the link. |
| `name` | string | no | The name of the link. |
| `url` | string | no | The URL of the link. |
| `url` | string | no | The URL of the link. |
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
NOTE: **NOTE**
You have to specify at least one of `name` or `url`
@ -132,7 +138,7 @@ You have to specify at least one of `name` or `url`
Example request:
```shell
curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
curl --request PUT --data name="new name" --data link_type="runbook" --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
@ -142,7 +148,8 @@ Example response:
"id":1,
"name":"new name",
"url":"http://192.168.10.15:3000",
"external":true
"external":true,
"link_type":"runbook"
}
```
@ -173,6 +180,7 @@ Example response:
"id":1,
"name":"new name",
"url":"http://192.168.10.15:3000",
"external":true
"external":true,
"link_type":"other"
}
```

View File

@ -63,7 +63,7 @@ Complementary reads:
- [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags/index.md)
- [Licensed feature availability](licensed_feature_availability.md)
- [View sent emails or preview mailers](emails.md)
- [Dealing with email/mailers](emails.md)
- [Shell commands](shell_commands.md) in the GitLab codebase
- [`Gemfile` guidelines](gemfile.md)
- [Pry debugging](pry_debugging.md)

View File

@ -1368,7 +1368,35 @@ versions back, you can consider removing the text if it's irrelevant or confusin
For example, if the current major version is 12.x, version text referencing versions of GitLab 8.x
and older are candidates for removal if necessary for clearer or cleaner docs.
## Product badges
## Products and features
Refer to the information in this section when describing products and features
within the GitLab product documentation.
### Avoid line breaks in names
When entering a product or feature name that includes a space (such as
GitLab Community Edition) or even other companies' products (such as
Amazon Web Services), be sure to not split the product or feature name across
lines with an inserted line break. Splitting product or feature names across
lines makes searching for these items more difficult, and can cause problems if
names change.
For example, the followng Markdown content is *not* formatted correctly:
```markdown
When entering a product or feature name that includes a space (such as GitLab
Community Edition), don't split the product or feature name across lines.
```
Instead, it should appear similar to the following:
```markdown
When entering a product or feature name that includes a space (such as
GitLab Community Edition), don't split the product or feature name across lines.
```
### Product badges
When a feature is available in EE-only tiers, add the corresponding tier according to the
feature availability:
@ -1408,7 +1436,7 @@ For example:
The absence of tiers' mentions mean that the feature is available in GitLab Core,
GitLab.com Free, and all higher tiers.
### How it works
#### How it works
Introduced by [!244](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/244),
the special markup `**(STARTER)**` will generate a `span` element to trigger the

View File

@ -1,5 +1,20 @@
# Dealing with email in development
## Ensuring compatibility with mailer Sidekiq jobs
A Sidekiq job is enqueued whenever `deliver_later` is called on an `ActionMailer`.
If a mailer argument needs to be added or removed, it is important to ensure
both backward and forward compatibility. Adhere to the Sidekiq Style Guide steps for
[changing the arguments for a worker](sidekiq_style_guide.md#changing-the-arguments-for-a-worker).
In the following example from [`NotificationService`](https://gitlab.com/gitlab-org/gitlab/-/blob/33ccb22e4fc271dbaac94b003a7a1a2915a13441/app/services/notification_service.rb#L74)
adding or removing an argument in this mailer's definition may cause problems
during deployment before all Rails and Sidekiq nodes have the updated code.
```ruby
mailer.unknown_sign_in_email(user, ip, time).deliver_later
```
## Sent emails
To view rendered emails "sent" in your development instance, visit

View File

@ -537,18 +537,74 @@ possible situations:
### Changing the arguments for a worker
Jobs need to be backwards- and forwards-compatible between consecutive versions
of the application.
Jobs need to be backward and forward compatible between consecutive versions
of the application. Adding or removing an argument may cause problems
during deployment before all Rails and Sidekiq nodes have the updated code.
This can be done by following this process:
#### Remove an argument
1. **Do not remove arguments from the `perform` function.**. Instead, use the
following approach
1. Provide a default value (usually `nil`) and use a comment to mark the
argument as deprecated
1. Stop using the argument in `perform_async`.
1. Ignore the value in the worker class, but do not remove it until the next
major release.
**Do not remove arguments from the `perform` function.**. Instead, use the
following approach:
1. Provide a default value (usually `nil`) and use a comment to mark the
argument as deprecated
1. Stop using the argument in `perform_async`.
1. Ignore the value in the worker class, but do not remove it until the next
major release.
In the following example, if you want to remove `arg2`, first set a `nil` default value,
and then update locations where `ExampleWorker.perform_async` is called.
```ruby
class ExampleWorker
def perform(object_id, arg1, arg2 = nil)
# ...
end
end
```
#### Add an argument
There are two options for safely adding new arguments to Sidekiq workers:
1. Set up a [multi-step deployment](#multi-step-deployment) in which the new argument is first added to the worker
1. Use a [parameter hash](#parameter-hash) for additional arguments. This is perhaps the most flexible option.
1. Use a parameter hash for additional arguments. This is perhaps the most flexible option.
##### Multi-step deployment
This approach requires multiple merge requests and for the first merge request
to be merged and deployed before additional changes are merged.
1. In an initial merge request, add the argument to the worker with a default
value:
```ruby
class ExampleWorker
def perform(object_id, new_arg = nil)
# ...
end
end
```
1. Merge and deploy the worker with the new argument.
1. In a further merge request, update `ExampleWorker.perform_async` calls to
use the new argument.
##### Parameter hash
This approach will not require multiple deployments if an existing worker already
utilizes a parameter hash.
1. Use a parameter hash in the worker to allow for future flexibility:
```ruby
class ExampleWorker
def perform(object_id, params = {})
# ...
end
end
```
### Removing workers

View File

@ -7,47 +7,49 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Incident Management
GitLab offers solutions for handling incidents in your applications and services,
from setting up an alert with Prometheus, to receiving a notification through a
monitoring tool like Slack, and [setting up Zoom calls](#zoom-integration-in-issues) with your
support team. Incidents can display [metrics](#embed-metrics-in-incidents-and-issues)
and [logs](#view-logs-from-metrics-panel).
such as setting up Prometheus alerts, displaying metrics, and sending notifications.
## Configure incidents **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4925) in GitLab Ultimate 11.11.
You can enable or disable Incident Management features in your project's
**{settings}** **Settings > Operations > Incidents**. Issues can be created for
each alert triggered, and separate email notifications can be sent to users with
[Developer permissions](../permissions.md). Appropriately configured alerts include an
You can enable or disable Incident Management features in the GitLab user interface
to create issues when alerts are triggered:
1. Navigate to **{settings}** **Settings > Operations > Incidents** and expand
**Incidents**:
![Incident Management Settings](img/incident_management_settings.png)
1. For GitLab versions 11.11 and greater, you can select the **Create an issue**
checkbox to create an issue based on your own
[issue templates](../project/description_templates.md#creating-issue-templates).
For more information, see
[Taking Action on Incidents](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate) **(ULTIMATE)**.
1. To create issues from alerts, select the template in the **Issue Template**
select box.
1. To send [separate email notifications](#notify-developers-of-alerts) to users
with [Developer permissions](../permissions.md), select
**Send a separate email notification to Developers**.
1. Click **Save changes**.
Appropriately configured alerts include an
[embedded chart](../project/integrations/prometheus.md#embedding-metrics-based-on-alerts-in-incident-issues)
for the query corresponding to the alert. You can also configure GitLab to
[close issues](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate)
when you receive notification that the alert is resolved.
![Incident Management Settings](img/incident_management_settings.png)
### Create issues from alerts
You can create GitLab issues from an alert notification. These issues contain
information about the alerts to help you diagnose the source of the alerts.
1. Visit your project's **{settings}** **Settings > Operations > Incidents**.
1. Select the **Create an issue** checkbox for GitLab to create an issue from
the incident.
1. Select the template from the **Issue Template** dropdown.
You can create your own [issue templates](../project/description_templates.md#creating-issue-templates)
to [use within Incident Management](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate).
1. Click **Save changes**.
## Notify developers of alerts
### Notify developers of alerts
GitLab can react to the alerts triggered from your applications and services
by creating issues and alerting developers through email. GitLab sends these emails
to [owners and maintainers](../permissions.md) of the project. They contain details
of the alert, and a link for more information.
by creating issues and alerting developers through email. By default, GitLab
sends these emails to [owners and maintainers](../permissions.md) of the project.
These emails contain details of the alert, and a link for more information.
### Configure Prometheus alerts
To send separate email notifications to users with
[Developer permissions](../permissions.md), see [Configure incidents](#configure-incidents-ultimate).
## Configure Prometheus alerts
You can set up Prometheus alerts in:
@ -57,7 +59,7 @@ You can set up Prometheus alerts in:
Prometheus alerts are created by the special Alert Bot user. You can't remove this
user, but it does not count toward your license limit.
### Configure external generic alerts
## Configure external generic alerts
GitLab can accept alerts from any source through a generic webhook receiver. When
[configuring the generic alerts integration](../project/integrations/generic_alerts.md),
@ -65,7 +67,7 @@ GitLab creates a unique endpoint which receives a JSON-formatted, customizable p
## Embed metrics in incidents and issues
You can embed metrics anywhere GitLab Markdown is used, such as descriptions,
You can embed metrics anywhere [GitLab Markdown](../markdown.md) is used, such as descriptions,
comments on issues, and merge requests. Embedding metrics helps you share them
when discussing incidents or performance issues. You can output the dashboard directly
into any issue, merge request, epic, or any other Markdown text field in GitLab
@ -78,10 +80,9 @@ in incidents and issue templates.
### Context menu
From each of the embedded metrics panels, you can access more details
about the data you're viewing from a context menu. You can access the context menu
by clicking the **{ellipsis_v}** **More actions** dropdown box above the
upper right corner of the panel. The options are:
You can view more details about an embedded metrics panel from the context menu.
To access the context menu, click the **{ellipsis_v}** **More actions** dropdown box
above the upper right corner of the panel. The options are:
- [View logs](#view-logs-from-metrics-panel).
- **Download CSV** - Data from embedded charts can be
@ -97,16 +98,16 @@ incident and need to [explore logs](../project/integrations/prometheus.md#view-l
from across your application. These logs help you understand what is affecting
your application's performance and resolve any problems.
## Slack integration
## Integrate incidents with Slack
Slack slash commands allow you to control GitLab and view GitLab content without leaving Slack.
Learn how to [set up Slack slash commands](../project/integrations/slack_slash_commands.md)
and how to [use the available slash commands](../../integration/slash_commands.md).
## Zoom integration in issues
## Integrate issues with Zoom
GitLab enables you to [associate a Zoom meeting with an issue](../project/issues/associate_zoom_meeting.md)
for synchronous communication during incident management. After starting a Zoom
call for an incident, you can associate the conference call with an issue, so your
team members can join without requesting a link.
call for an incident, you can associate the conference call with an issue. Your
team members can join the Zoom call without requesting a link.

View File

@ -177,6 +177,22 @@ Different discussions have different pin numbers:
From GitLab 12.5 on, new discussions will be outputted to the issue activity,
so that everyone involved can participate in the discussion.
## Resolve Design threads
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13049) in GitLab 13.1.
Discussion threads can be resolved on Designs. You can mark a thread as resolved
or unresolved by clicking the **Resolve thread** icon at the first comment of the
discussion.
![Resolve thread icon](img/resolve_design-discussion_icon_v13_1.png)
Pinned comments can also be resolved or unresolved in their threads.
When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve
the thread once published.
![Resolve checkbox](img/resolve_design-discussion_checkbox_v13_1.png)
## Referring to designs in Markdown
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217160) in **GitLab 13.1**.

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -68,7 +68,8 @@ If you have the correct access and a Premium license, you have the option to set
Follow these steps to do so:
1. [Set up incoming email](../../administration/incoming_email.md#set-it-up) for the GitLab instance.
This must support [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing).
- We recommend using [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing),
but in GitLab 11.7 and later you can also use [catch-all mailboxes](../../administration/incoming_email.md#catch-all-mailbox).
1. Navigate to your project's **Settings > General** and locate the **Service Desk** section.
1. Enable the **Activate Service Desk** toggle. This reveals a unique email address to email issues
to the project. These issues will be [confidential](issues/confidential_issues.md), so they will

View File

@ -66,7 +66,7 @@ module Banzai
# links have `gfm` and `gfm-issue` class names attached for styling.
def issue_link_filter(text, link_content: nil)
self.class.references_in(text, issue_reference_pattern) do |match, id|
url = url_for_issue(id, project, only_path: context[:only_path])
url = url_for_issue(id)
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
content = link_content || match
@ -77,8 +77,19 @@ module Banzai
end
end
def url_for_issue(*args)
IssuesHelper.url_for_issue(*args)
def url_for_issue(issue_id)
return '' if project.nil?
url = if only_path?
project.external_issue_tracker.issue_path(issue_id)
else
project.external_issue_tracker.issue_url(issue_id)
end
# Ensure we return a valid URL to prevent possible XSS.
URI.parse(url).to_s
rescue URI::InvalidURIError
''
end
def default_issues_tracker?

View File

@ -18,7 +18,9 @@ module Banzai
end
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true)
return issue_path(issue, project) if only_path?
issue_url(issue, project)
end
def projects_relation_for_paths(paths)
@ -35,6 +37,14 @@ module Banzai
private
def issue_path(issue, project)
Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid)
end
def issue_url(issue, project)
Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid)
end
def design_link_extras(issue, path)
if path == '/designs' && read_designs?(issue)
['designs']

View File

@ -142,6 +142,12 @@ module Banzai
def element_node?(node)
node.is_a?(Nokogiri::XML::Element)
end
private
def only_path?
context[:only_path]
end
end
end
end

View File

@ -4451,9 +4451,6 @@ msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
msgid "Click the image where you'd like to start a new discussion"
msgstr ""
msgid "Click to expand it."
msgstr ""
@ -5528,6 +5525,9 @@ msgstr ""
msgid "Collapse child epics"
msgstr ""
msgid "Collapse replies"
msgstr ""
msgid "Collapse sidebar"
msgstr ""
@ -7519,9 +7519,15 @@ msgstr ""
msgid "DesignManagement|Cancel comment update confirmation"
msgstr ""
msgid "DesignManagement|Click the image where you'd like to start a new discussion"
msgstr ""
msgid "DesignManagement|Comment"
msgstr ""
msgid "DesignManagement|Comments you resolve can be viewed and unresolved by going to the \"Resolved Comments\" section below"
msgstr ""
msgid "DesignManagement|Could not add a new comment. Please try again."
msgstr ""
@ -7567,9 +7573,18 @@ msgstr ""
msgid "DesignManagement|Keep comment"
msgstr ""
msgid "DesignManagement|Learn more about resolving comments"
msgstr ""
msgid "DesignManagement|Requested design version does not exist. Showing latest version instead"
msgstr ""
msgid "DesignManagement|Resolve thread"
msgstr ""
msgid "DesignManagement|Resolved Comments"
msgstr ""
msgid "DesignManagement|Save comment"
msgstr ""
@ -7582,6 +7597,9 @@ msgstr ""
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
msgstr ""
msgid "DesignManagement|Unresolve thread"
msgstr ""
msgid "DesignManagement|Upload designs"
msgstr ""
@ -18657,6 +18675,9 @@ msgstr ""
msgid "Resolved all discussions."
msgstr ""
msgid "Resolved by"
msgstr ""
msgid "Resolved by %{name}"
msgstr ""
@ -19423,6 +19444,9 @@ msgstr ""
msgid "SecurityReports|Status"
msgstr ""
msgid "SecurityReports|The rating \"unknown\" indicates that the underlying scanner doesnt contain or provide a severity rating."
msgstr ""
msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""

View File

@ -3,6 +3,8 @@
module QA
module Resource
class ProjectMilestone < Base
attr_writer :start_date, :due_date
attribute :id
attribute :title
@ -27,7 +29,10 @@ module QA
def api_post_body
{
title: title
}
}.tap do |hash|
hash[:start_date] = @start_date if @start_date
hash[:due_date] = @due_date if @due_date
end
end
end
end

View File

@ -2,6 +2,6 @@
require 'spec_helper'
describe InstanceStatistics::DevOpsScoreController do
RSpec.describe InstanceStatistics::DevOpsScoreController do
it_behaves_like 'instance statistics availability'
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe InvitesController do
RSpec.describe InvitesController do
let(:token) { '123456' }
let(:user) { create(:user) }
let(:member) { create(:project_member, invite_token: token, invite_email: 'test@abc.com', user: user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Ldap::OmniauthCallbacksController do
RSpec.describe Ldap::OmniauthCallbacksController do
include_context 'Ldap::OmniauthCallbacksController'
it 'allows sign in' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe MetricsController, :request_store do
RSpec.describe MetricsController, :request_store do
include StubENV
let(:metrics_multiproc_dir) { @metrics_multiproc_dir }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe NotificationSettingsController do
RSpec.describe NotificationSettingsController do
let(:project) { create(:project) }
let(:group) { create(:group, :internal) }
let(:user) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Oauth::ApplicationsController do
RSpec.describe Oauth::ApplicationsController do
let(:user) { create(:user) }
let(:application) { create(:oauth_application, owner: user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Oauth::AuthorizationsController do
RSpec.describe Oauth::AuthorizationsController do
let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
let(:params) do
{

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Oauth::AuthorizedApplicationsController do
RSpec.describe Oauth::AuthorizedApplicationsController do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:application) { create(:oauth_application, owner: guest) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do
RSpec.describe OmniauthCallbacksController, type: :controller do
include LoginHelpers
describe 'omniauth' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe PasswordsController do
RSpec.describe PasswordsController do
describe '#check_password_authentication_available' do
before do
@request.env["devise.mapping"] = Devise.mappings[:user]

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::AccountsController do
RSpec.describe Profiles::AccountsController do
describe 'DELETE unlink' do
let(:user) { create(:omniauth_user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::AvatarsController do
RSpec.describe Profiles::AvatarsController do
let(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png")) }
before do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::EmailsController do
RSpec.describe Profiles::EmailsController do
let(:user) { create(:user) }
before do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::KeysController do
RSpec.describe Profiles::KeysController do
let(:user) { create(:user) }
describe 'POST #create' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::NotificationsController do
RSpec.describe Profiles::NotificationsController do
let(:user) do
create(:user) do |user|
user.emails.create(email: 'original@example.com', confirmed_at: Time.current)

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
RSpec.describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
let(:token_attributes) { attributes_for(:personal_access_token) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::PreferencesController do
RSpec.describe Profiles::PreferencesController do
let(:user) { create(:user) }
before do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Profiles::TwoFactorAuthsController do
RSpec.describe Profiles::TwoFactorAuthsController do
before do
# `user` should be defined within the action-specific describe blocks
sign_in(user)

View File

@ -2,7 +2,7 @@
require('spec_helper')
describe ProfilesController, :request_store do
RSpec.describe ProfilesController, :request_store do
let(:user) { create(:user) }
describe 'POST update' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::AlertManagementController do
RSpec.describe Projects::AlertManagementController do
let_it_be(:project) { create(:project) }
let_it_be(:role) { :developer }
let_it_be(:user) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::Alerting::NotificationsController do
RSpec.describe Projects::Alerting::NotificationsController do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::ArtifactsController do
RSpec.describe Projects::ArtifactsController do
include RepoHelpers
let(:user) { project.owner }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::AutocompleteSourcesController do
RSpec.describe Projects::AutocompleteSourcesController do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:issue) { create(:issue, project: project) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::AvatarsController do
RSpec.describe Projects::AvatarsController do
let_it_be(:project) { create(:project, :repository) }
before do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::BadgesController do
RSpec.describe Projects::BadgesController do
let(:project) { pipeline.project }
let!(:pipeline) { create(:ci_empty_pipeline) }
let(:user) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::BlameController do
RSpec.describe Projects::BlameController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::BlobController do
RSpec.describe Projects::BlobController do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::BoardsController do
RSpec.describe Projects::BoardsController do
let(:project) { create(:project) }
let(:user) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::BranchesController do
RSpec.describe Projects::BranchesController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:developer) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::Ci::DailyBuildGroupReportResultsController do
RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
describe 'GET index' do
let(:project) { create(:project, :public, :repository) }
let(:ref_path) { 'refs/heads/master' }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::Ci::LintsController do
RSpec.describe Projects::Ci::LintsController do
include StubRequests
let(:project) { create(:project, :repository) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::Clusters::ApplicationsController do
RSpec.describe Projects::Clusters::ApplicationsController do
include AccessMatchersForController
def current_application

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::ClustersController do
RSpec.describe Projects::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::CommitController do
RSpec.describe Projects::CommitController do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe Projects::CommitsController do
RSpec.describe Projects::CommitsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }

Some files were not shown because too many files have changed in this diff Show More