Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-10 12:08:57 +00:00
parent 01c201bc6a
commit a08f8baa63
162 changed files with 2634 additions and 1244 deletions

View File

@ -1 +1 @@
fa974a4ab21aa6acc4c3a00456265248a4d70703
9cde939eef5182b062f9aa04a3a3f78d8264af4b

View File

@ -17,6 +17,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
import service from '../services';
import {
integrationTypesNew,
JSON_VALIDATE_DELAY,
@ -89,7 +90,7 @@ export default {
MappingBuilder,
},
directives: {
'gl-modal': GlModalDirective,
GlModal: GlModalDirective,
},
inject: {
generic: {
@ -150,6 +151,13 @@ export default {
apiUrl: this.currentIntegration?.apiUrl || '',
};
},
testAlertPayload() {
return {
data: this.integrationTestPayload.json,
endpoint: this.integrationForm.url,
token: this.integrationForm.token,
};
},
},
watch: {
currentIntegration(val) {
@ -170,8 +178,14 @@ export default {
}
},
submitWithTestPayload() {
// TODO: Test payload before saving via GraphQL
this.submit();
return service
.updateTestAlert(this.testAlertPayload)
.then(() => {
this.submit();
})
.catch(() => {
this.$emit('test-payload-failure');
});
},
submit() {
const { name, apiUrl } = this.integrationForm;
@ -359,7 +373,6 @@ export default {
</div>
</gl-form-group>
<gl-form-group
id="test-integration"
:label="$options.i18n.integrationFormSteps.step4.label"
label-for="test-integration"
:invalid-feedback="integrationTestPayload.error"
@ -395,6 +408,8 @@ export default {
<div class="gl-display-flex gl-justify-content-end">
<gl-button type="reset" class="gl-mr-3 js-no-auto-disable">{{ __('Cancel') }}</gl-button>
<gl-button
data-testid="integration-test-and-submit"
:disabled="Boolean(integrationTestPayload.error)"
category="secondary"
variant="success"
class="gl-mr-1 js-no-auto-disable"

View File

@ -24,6 +24,7 @@ import {
ADD_INTEGRATION_ERROR,
RESET_INTEGRATION_TOKEN_ERROR,
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
} from '../utils/error_messages';
export default {
@ -244,6 +245,9 @@ export default {
clearCurrentIntegration() {
this.currentIntegration = null;
},
testPayloadFailure() {
createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
},
},
};
</script>
@ -266,6 +270,7 @@ export default {
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
@test-payload-failure="testPayloadFailure"
/>
<settings-form-old v-else />
</div>

View File

@ -2,6 +2,7 @@
import axios from '~/lib/utils/axios_utils';
export default {
// TODO: All this code save updateTestAlert will be deleted as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/255501
updateGenericKey({ endpoint, params }) {
return axios.put(endpoint, params);
},
@ -25,11 +26,11 @@ export default {
},
});
},
updateTestAlert({ endpoint, data, authKey }) {
updateTestAlert({ endpoint, data, token }) {
return axios.post(endpoint, data, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authKey}`,
Authorization: `Bearer ${token}`,
},
});
},

View File

@ -15,3 +15,7 @@ export const UPDATE_INTEGRATION_ERROR = s__(
export const RESET_INTEGRATION_TOKEN_ERROR = s__(
'AlertsIntegrations|The integration token could not be reset. Please try again.',
);
export const INTEGRATION_PAYLOAD_TEST_ERROR = s__(
'AlertsIntegrations|Integration payload is invalid. You can still save your changes.',
);

View File

@ -1,32 +0,0 @@
<script>
import DraftNote from './draft_note.vue';
export default {
components: {
DraftNote,
},
props: {
draft: {
type: Object,
required: true,
},
diffFile: {
type: Object,
required: true,
},
line: {
type: Object,
required: false,
default: null,
},
},
};
</script>
<template>
<tr class="notes_holder js-temp-notes-holder">
<td class="notes-content" colspan="4">
<div class="content"><draft-note :draft="draft" :diff-file="diffFile" :line="line" /></div>
</td>
</tr>
</template>

View File

@ -1,49 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import DraftNote from './draft_note.vue';
export default {
components: {
DraftNote,
},
props: {
line: {
type: Object,
required: true,
},
diffFileContentSha: {
type: String,
required: true,
},
},
computed: {
...mapGetters('batchComments', ['draftForLine']),
className() {
return this.leftDraft > 0 || this.rightDraft > 0 ? '' : 'js-temp-notes-holder';
},
leftDraft() {
return this.draftForLine(this.diffFileContentSha, this.line, 'left');
},
rightDraft() {
return this.draftForLine(this.diffFileContentSha, this.line, 'right');
},
},
};
</script>
<template>
<tr :class="className" class="notes_holder">
<td class="notes_line old"></td>
<td class="notes-content parallel old" colspan="2">
<div v-if="leftDraft.isDraft" class="content">
<draft-note :draft="leftDraft" :line="line.left" />
</div>
</td>
<td class="notes_line new"></td>
<td class="notes-content parallel new" colspan="2">
<div v-if="rightDraft.isDraft" class="content">
<draft-note :draft="rightDraft" :line="line.right" />
</div>
</td>
</tr>
</template>

View File

@ -4,7 +4,7 @@ import { GlDropdownItem, GlDropdownDivider, GlAvatarLabeled, GlAvatarLink } from
import { __, n__ } from '~/locale';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import AssigneesDropdown from '~/vue_shared/components/sidebar/assignees_dropdown.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
export default {
@ -17,7 +17,7 @@ export default {
components: {
BoardEditableItem,
IssuableAssignees,
AssigneesDropdown,
MultiSelectDropdown,
GlDropdownItem,
GlDropdownDivider,
GlAvatarLabeled,
@ -92,7 +92,7 @@ export default {
</template>
<template #default>
<assignees-dropdown
<multi-select-dropdown
class="w-100"
:text="$options.i18n.assignees"
:header-text="$options.i18n.assignTo"
@ -138,7 +138,7 @@ export default {
</gl-avatar-link>
</gl-dropdown-item>
</template>
</assignees-dropdown>
</multi-select-dropdown>
</template>
</board-editable-item>
</template>

View File

@ -112,9 +112,9 @@ export default {
},
canMoveNote(note) {
const { userPermissions } = note;
const { adminNote } = userPermissions || {};
const { repositionNote } = userPermissions || {};
return Boolean(adminNote);
return Boolean(repositionNote);
},
isPositionInOverlay(position) {
const { top, left } = this.getNoteRelativePosition(position);

View File

@ -1,3 +1,4 @@
fragment DesignNotePermissions on NotePermissions {
adminNote
repositionNote
}

View File

@ -0,0 +1,10 @@
#import "../fragments/design_note.fragment.graphql"
mutation repositionImageDiffNote($input: RepositionImageDiffNoteInput!) {
repositionImageDiffNote(input: $input) {
errors
note {
...DesignNote
}
}
}

View File

@ -1,10 +0,0 @@
#import "../fragments/design_note.fragment.graphql"
mutation updateImageDiffNote($input: UpdateImageDiffNoteInput!) {
updateImageDiffNote(input: $input) {
errors
note {
...DesignNote
}
}
}

View File

@ -13,19 +13,19 @@ import DesignReplyForm from '../../components/design_notes/design_reply_form.vue
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
import repositionImageDiffNoteMutation from '../../graphql/mutations/reposition_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
import {
extractDiscussions,
extractDesign,
updateImageDiffNoteOptimisticResponse,
repositionImageDiffNoteOptimisticResponse,
toDiffNoteGid,
extractDesignNoteId,
getPageLayoutElement,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
updateStoreAfterUpdateImageDiffNote,
updateStoreAfterRepositionImageDiffNote,
} from '../../utils/cache_update';
import {
ADD_DISCUSSION_COMMENT_ERROR,
@ -182,12 +182,12 @@ export default {
updateImageDiffNoteInStore(
store,
{
data: { updateImageDiffNote },
data: { repositionImageDiffNote },
},
) {
return updateStoreAfterUpdateImageDiffNote(
return updateStoreAfterRepositionImageDiffNote(
store,
updateImageDiffNote,
repositionImageDiffNote,
getDesignQuery,
this.designVariables,
);
@ -199,7 +199,7 @@ export default {
);
const mutationPayload = {
optimisticResponse: updateImageDiffNoteOptimisticResponse(note, {
optimisticResponse: repositionImageDiffNoteOptimisticResponse(note, {
position,
}),
variables: {
@ -208,7 +208,7 @@ export default {
position,
},
},
mutation: updateImageDiffNoteMutation,
mutation: repositionImageDiffNoteMutation,
update: this.updateImageDiffNoteInStore,
};

View File

@ -101,7 +101,7 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
});
};
const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
const updateImageDiffNoteInStore = (store, repositionImageDiffNote, query, variables) => {
const sourceData = store.readQuery({
query,
variables,
@ -111,12 +111,12 @@ const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables
const design = extractDesign(draftData);
const discussion = extractCurrentDiscussion(
design.discussions,
updateImageDiffNote.note.discussion.id,
repositionImageDiffNote.note.discussion.id,
);
discussion.notes = {
...discussion.notes,
nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
nodes: [repositionImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
};
});
@ -268,7 +268,7 @@ export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariab
}
};
export const updateStoreAfterUpdateImageDiffNote = (store, data, query, queryVariables) => {
export const updateStoreAfterRepositionImageDiffNote = (store, data, query, queryVariables) => {
if (hasErrors(data)) {
onError(data, UPDATE_IMAGE_DIFF_NOTE_ERROR);
} else {

View File

@ -107,12 +107,12 @@ export const designUploadOptimisticResponse = files => {
* @param {Object} note
* @param {Object} position
*/
export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
export const repositionImageDiffNoteOptimisticResponse = (note, { position }) => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
updateImageDiffNote: {
__typename: 'UpdateImageDiffNotePayload',
repositionImageDiffNote: {
__typename: 'RepositionImageDiffNotePayload',
note: {
...note,
position: {

View File

@ -0,0 +1,69 @@
<script>
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
type: Object,
required: true,
},
diffFileHash: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
hasDraft: {
type: Boolean,
required: false,
default: false,
},
linePosition: {
type: String,
required: false,
default: '',
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
},
};
</script>
<template>
<div class="content">
<diff-discussions
v-if="line.renderDiscussion"
:line="line"
:discussions="line.discussions"
:help-page-path="helpPagePath"
/>
<diff-discussion-reply
v-if="!hasDraft"
:has-form="line.hasCommentForm"
:render-reply-placeholder="Boolean(line.discussions.length)"
@showNewDiscussionForm="showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
:line-position="linePosition"
/>
</template>
</diff-discussion-reply>
</div>
</template>

View File

@ -10,6 +10,7 @@ import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_prev
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
import DiffView from './diff_view.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import NoteForm from '../../notes/components/note_form.vue';
import ImageDiffOverlay from './image_diff_overlay.vue';
@ -18,12 +19,14 @@ import eventHub from '../../notes/event_hub';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils';
import { diffViewerModes } from '~/ide/constants';
import { mapInline, mapParallel } from './diff_row_utils';
export default {
components: {
GlLoadingIcon,
InlineDiffView,
ParallelDiffView,
DiffView,
DiffViewer,
NoteForm,
DiffDiscussions,
@ -83,6 +86,19 @@ export default {
author() {
return this.getUserData;
},
mappedLines() {
if (this.glFeatures.unifiedDiffLines && this.glFeatures.unifiedDiffComponents) {
return this.diffLines(this.diffFile, true).map(mapParallel(this)) || [];
}
// TODO: Everything below this line can be deleted when unifiedDiffComponents FF is removed
if (this.isInlineView) {
return this.diffFile.highlighted_diff_lines.map(mapInline(this));
}
return this.glFeatures.unifiedDiffLines
? this.diffLines(this.diffFile).map(mapParallel(this))
: this.diffFile.parallel_diff_lines.map(mapParallel(this)) || [];
},
},
updated() {
this.$nextTick(() => {
@ -113,19 +129,28 @@ export default {
<template>
<div class="diff-content">
<div class="diff-viewer">
<template v-if="isTextFile">
<template
v-if="isTextFile && glFeatures.unifiedDiffLines && glFeatures.unifiedDiffComponents"
>
<diff-view
:diff-file="diffFile"
:diff-lines="mappedLines"
:help-page-path="helpPagePath"
:inline="isInlineView"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
</template>
<template v-else-if="isTextFile">
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlighted_diff_lines"
:diff-lines="mappedLines"
:help-page-path="helpPagePath"
/>
<parallel-diff-view
v-else-if="isParallelView"
:diff-file="diffFile"
:diff-lines="
glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || []
"
:diff-lines="mappedLines"
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />

View File

@ -54,11 +54,6 @@ export default {
required: false,
default: false,
},
colspan: {
type: Number,
required: false,
default: 4,
},
},
computed: {
...mapState({
@ -231,28 +226,26 @@ export default {
</script>
<template>
<td :colspan="colspan" class="text-center gl-font-regular">
<div class="content js-line-expansion-content">
<a
v-if="canExpandDown"
class="gl-mx-2 gl-cursor-pointer js-unfold-down gl-display-inline-block gl-py-4"
@click="handleExpandLines(EXPAND_DOWN)"
>
<gl-icon :size="12" name="expand-down" aria-hidden="true" />
<span>{{ $options.i18n.showMore }}</span>
</a>
<a class="gl-mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
<gl-icon :size="12" name="expand" aria-hidden="true" />
<span>{{ $options.i18n.showAll }}</span>
</a>
<a
v-if="canExpandUp"
class="gl-mx-2 gl-cursor-pointer js-unfold gl-display-inline-block gl-py-4"
@click="handleExpandLines(EXPAND_UP)"
>
<gl-icon :size="12" name="expand-up" aria-hidden="true" />
<span>{{ $options.i18n.showMore }}</span>
</a>
</div>
</td>
<div class="content js-line-expansion-content">
<a
v-if="canExpandDown"
class="gl-mx-2 gl-cursor-pointer js-unfold-down gl-display-inline-block gl-py-4"
@click="handleExpandLines(EXPAND_DOWN)"
>
<gl-icon :size="12" name="expand-down" aria-hidden="true" />
<span>{{ $options.i18n.showMore }}</span>
</a>
<a class="gl-mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
<gl-icon :size="12" name="expand" aria-hidden="true" />
<span>{{ $options.i18n.showAll }}</span>
</a>
<a
v-if="canExpandUp"
class="gl-mx-2 gl-cursor-pointer js-unfold gl-display-inline-block gl-py-4"
@click="handleExpandLines(EXPAND_UP)"
>
<gl-icon :size="12" name="expand-up" aria-hidden="true" />
<span>{{ $options.i18n.showMore }}</span>
</a>
</div>
</template>

View File

@ -0,0 +1,271 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
components: {
GlIcon,
DiffGutterAvatars,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
props: {
fileHash: {
type: String,
required: true,
},
filePath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isCommented: {
type: Boolean,
required: false,
default: false,
},
inline: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
...mapGetters(['isLoggedIn']),
...mapState({
isHighlighted(state) {
const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
},
}),
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn);
},
classNameMapCellRight() {
return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn);
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);
},
addCommentTooltipRight() {
return utils.addCommentTooltip(this.line.right);
},
shouldRenderCommentButton() {
return (
this.isLoggedIn &&
!this.line.isContextLineLeft &&
!this.line.isMetaLineLeft &&
!this.line.hasDiscussionsLeft &&
!this.line.hasDiscussionsRight
);
},
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
},
methods: {
...mapActions('diffs', [
'scrollToLineIfNeededParallel',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
]),
// Prevent text selecting on both sides of parallel diff view
// Backport of the same code from legacy diff notes.
handleParallelLineMouseDown(e) {
const line = e.currentTarget;
const table = line.closest('.diff-table');
table.classList.remove('left-side-selected', 'right-side-selected');
const [lineClass] = ['left-side', 'right-side'].filter(name => line.classList.contains(name));
if (lineClass) {
table.classList.add(`${lineClass}-selected`);
}
},
handleCommentButton(line) {
this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
},
},
};
</script>
<template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
<div class="diff-grid-left left-side">
<template v-if="line.left">
<div
:class="classNameMapCellLeft"
data-testid="leftLineNumber"
class="diff-td diff-line-num old_line"
>
<span
v-if="shouldRenderCommentButton"
v-gl-tooltip
data-testid="leftCommentButton"
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipLeft"
>
<button
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:disabled="line.left.commentsDisabled"
@click="handleCommentButton(line.left)"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.left.old_line"
:data-linenumber="line.left.old_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded"
data-testid="leftDiscussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.left.line_code,
fileHash,
expanded: !line.left.discussionsExpanded,
})
"
/>
</div>
<div :class="classNameMapCellLeft" class="diff-td diff-line-num old_line">
<a
v-if="line.left.old_line"
:data-linenumber="line.left.old_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</div>
<div :class="parallelViewLeftLineType" class="diff-td line-coverage left-side"></div>
<div
:id="line.left.line_code"
:key="line.left.line_code"
v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType"
class="diff-td line_content with-coverage parallel left-side"
data-testid="leftContent"
@mousedown="handleParallelLineMouseDown"
></div>
</template>
<template v-else>
<div data-testid="leftEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td line-coverage left-side empty-cell"></div>
<div class="diff-td line_content with-coverage parallel left-side empty-cell"></div>
</template>
</div>
<div
v-if="!inline || (line.right && Boolean(line.right.type))"
class="diff-grid-right right-side"
>
<template v-if="line.right">
<div
:class="classNameMapCellRight"
data-testid="rightLineNumber"
class="diff-td diff-line-num new_line"
>
<span
v-if="shouldRenderCommentButton"
v-gl-tooltip
data-testid="rightCommentButton"
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipRight"
>
<button
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:disabled="line.right.commentsDisabled"
@click="handleCommentButton(line.right)"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.right.new_line"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded"
data-testid="rightDiscussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.right.line_code,
fileHash,
expanded: !line.right.discussionsExpanded,
})
"
/>
</div>
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<a
v-if="line.right.new_line"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</div>
<div
v-gl-tooltip.hover
:title="coverageState.text"
:class="[line.right.type, coverageState.class, { hll: isHighlighted }]"
class="diff-td line-coverage right-side"
></div>
<div
:id="line.right.line_code"
:key="line.right.rich_text"
v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
hll: isHighlighted,
},
]"
class="diff-td line_content with-coverage parallel right-side"
@mousedown="handleParallelLineMouseDown"
></div>
</template>
<template v-else>
<div data-testid="rightEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td line-coverage right-side empty-cell"></div>
<div class="diff-td line_content with-coverage parallel right-side empty-cell"></div>
</template>
</div>
</div>
</template>

View File

@ -83,3 +83,76 @@ export const parallelViewLeftLineType = (line, hll) => {
export const shouldShowCommentButton = (hover, context, meta, discussions) => {
return hover && !context && !meta && !discussions;
};
export const mapParallel = content => line => {
let { left, right } = line;
// Dicussions/Comments
const hasExpandedDiscussionOnLeft =
left?.discussions?.length > 0 ? left?.discussionsExpanded : false;
const hasExpandedDiscussionOnRight =
right?.discussions?.length > 0 ? right?.discussionsExpanded : false;
const renderCommentRow =
hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight || left?.hasForm || right?.hasForm;
if (left) {
left = {
...left,
renderDiscussion: hasExpandedDiscussionOnLeft,
hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'),
hasCommentForm: left.hasForm,
};
}
if (right) {
right = {
...right,
renderDiscussion: Boolean(hasExpandedDiscussionOnRight && right.type),
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
hasCommentForm: Boolean(right.hasForm && right.type),
};
}
return {
...line,
left,
right,
isMatchLineLeft: isMatchLine(left?.type),
isMatchLineRight: isMatchLine(right?.type),
isContextLineLeft: isContextLine(left?.type),
isContextLineRight: isContextLine(right?.type),
hasDiscussionsLeft: hasDiscussions(left),
hasDiscussionsRight: hasDiscussions(right),
lineHrefOld: lineHref(left),
lineHrefNew: lineHref(right),
lineCode: lineCode(line),
isMetaLineLeft: isMetaLine(left?.type),
isMetaLineRight: isMetaLine(right?.type),
draftRowClasses: left?.lineDraft > 0 || right?.lineDraft > 0 ? '' : 'js-temp-notes-holder',
renderCommentRow,
commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder',
};
};
// TODO: Delete this function when unifiedDiffComponents FF is removed
export const mapInline = content => line => {
// Discussions/Comments
const renderCommentRow = line.hasForm || (line.discussions?.length && line.discussionsExpanded);
return {
...line,
renderDiscussion: Boolean(line.discussions?.length),
isMatchLine: isMatchLine(line.type),
commentRowClasses: line.discussions?.length ? '' : 'js-temp-notes-holder',
renderCommentRow,
hasDraft: content.shouldRenderDraftRow(content.diffFile.file_hash, line),
hasCommentForm: line.hasForm,
isMetaLine: isMetaLine(line.type),
isContextLine: isContextLine(line.type),
hasDiscussions: hasDiscussions(line),
lineHref: lineHref(line),
lineCode: lineCode(line),
};
};

View File

@ -0,0 +1,149 @@
<script>
import { mapGetters, mapState } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import DiffRow from './diff_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
DiffExpansionCell,
DiffRow,
DiffCommentCell,
DraftNote,
},
mixins: [draftCommentsMixin],
props: {
diffFile: {
type: Object,
required: true,
},
diffLines: {
type: Array,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
inline: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
}),
diffLinesLength() {
return this.diffLines.length;
},
commentedLines() {
return getCommentedLines(
this.selectedCommentPosition || this.selectedCommentPositionHover,
this.diffLines,
);
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div
:class="[$options.userColorScheme, { inline }]"
:data-commit-id="commitId"
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
>
<template v-for="(line, index) in diffLines">
<div
v-if="line.isMatchLineLeft || line.isMatchLineRight"
:key="`expand-${index}`"
class="diff-tr line_expansion match"
>
<div class="diff-td text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line.left"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</div>
</div>
<diff-row
v-if="!line.isMatchLineLeft && !line.isMatchLineRight"
:key="line.line_code"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline"
/>
<div
v-if="line.renderCommentRow"
:key="`dcr-${line.line_code || index}`"
:class="line.commentRowClasses"
class="diff-grid-comments diff-tr notes_holder"
>
<div
v-if="!inline || (line.left && line.left.discussions.length)"
class="diff-td notes-content parallel old"
>
<diff-comment-cell
v-if="line.left"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
:has-draft="line.left.hasDraft"
line-position="left"
/>
</div>
<div
v-if="!inline || (line.right && line.right.discussions.length)"
class="diff-td notes-content parallel new"
>
<diff-comment-cell
v-if="line.right"
:line="line.right"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
:has-draft="line.right.hasDraft"
line-position="right"
/>
</div>
</div>
<div
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
:key="`drafts-${index}`"
:class="line.draftRowClasses"
class="diff-grid-drafts diff-tr notes_holder"
>
<div
v-if="!inline || (line.left && line.left.lineDraft.isDraft)"
class="diff-td notes-content parallel old"
>
<div v-if="line.left && line.left.lineDraft.isDraft" class="content">
<draft-note :draft="line.left.lineDraft" :line="line.left" />
</div>
</div>
<div
v-if="!inline || (line.right && line.right.lineDraft.isDraft)"
class="diff-td notes-content parallel new"
>
<div v-if="line.right && line.right.lineDraft.isDraft" class="content">
<draft-note :draft="line.right.lineDraft" :line="line.right" />
</div>
</div>
</div>
</template>
</div>
</template>

View File

@ -1,82 +0,0 @@
<script>
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
type: Object,
required: true,
},
diffFileHash: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
hasDraft: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
className() {
return this.line.discussions.length ? '' : 'js-temp-notes-holder';
},
shouldRender() {
if (this.line.hasForm) return true;
if (!this.line.discussions || !this.line.discussions.length) {
return false;
}
return this.line.discussionsExpanded;
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
},
};
</script>
<template>
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes-content" colspan="4">
<div class="content">
<diff-discussions
v-if="line.discussions.length"
:line="line"
:discussions="line.discussions"
:help-page-path="helpPagePath"
/>
<diff-discussion-reply
v-if="!hasDraft"
:has-form="line.hasForm"
:render-reply-placeholder="Boolean(line.discussions.length)"
@showNewDiscussionForm="
showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
</template>
</diff-discussion-reply>
</div>
</td>
</tr>
</template>

View File

@ -1,51 +0,0 @@
<script>
import DiffExpansionCell from './diff_expansion_cell.vue';
import { MATCH_LINE_TYPE } from '../constants';
export default {
components: {
DiffExpansionCell,
},
props: {
fileHash: {
type: String,
required: true,
},
contextLinesPath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isTop: {
type: Boolean,
required: false,
default: false,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isMatchLine() {
return this.line.type === MATCH_LINE_TYPE;
},
},
};
</script>
<template>
<tr v-if="isMatchLine" class="line_expansion match">
<diff-expansion-cell
:file-hash="fileHash"
:context-lines-path="contextLinesPath"
:line="line"
:is-top="isTop"
:is-bottom="isBottom"
/>
</tr>
</template>

View File

@ -3,7 +3,13 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { CONTEXT_LINE_CLASS_NAME } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
import {
isHighlighted,
shouldShowCommentButton,
shouldRenderCommentButton,
classNameMapCell,
addCommentTooltip,
} from './diff_row_utils';
export default {
components: {
@ -48,60 +54,42 @@ export default {
...mapGetters('diffs', ['fileLineCoverage']),
...mapState({
isHighlighted(state) {
return utils.isHighlighted(state, this.line, this.isCommented);
return isHighlighted(state, this.line, this.isCommented);
},
}),
isContextLine() {
return utils.isContextLine(this.line.type);
},
classNameMap() {
return [
this.line.type,
{
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLine,
},
];
},
inlineRowId() {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
},
isMatchLine() {
return utils.isMatchLine(this.line.type);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
isMetaLine() {
return utils.isMetaLine(this.line.type);
},
classNameMapCell() {
return utils.classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
},
addCommentTooltip() {
return utils.addCommentTooltip(this.line);
return addCommentTooltip(this.line);
},
shouldRenderCommentButton() {
return utils.shouldRenderCommentButton(this.isLoggedIn, true);
return shouldRenderCommentButton(this.isLoggedIn, true);
},
shouldShowCommentButton() {
return utils.shouldShowCommentButton(
return shouldShowCommentButton(
this.isHover,
this.isContextLine,
this.isMetaLine,
this.hasDiscussions,
this.line.isContextLine,
this.line.isMetaLine,
this.line.hasDiscussions,
);
},
hasDiscussions() {
return utils.hasDiscussions(this.line);
},
lineHref() {
return utils.lineHref(this.line);
},
lineCode() {
return utils.lineCode(this.line);
},
shouldShowAvatarsOnGutter() {
return this.hasDiscussions;
return this.line.hasDiscussions;
},
},
mounted() {
@ -128,7 +116,6 @@ export default {
<template>
<tr
v-if="!isMatchLine"
:id="inlineRowId"
:class="classNameMap"
class="line_holder"
@ -158,8 +145,8 @@ export default {
v-if="line.old_line"
ref="lineNumberRefOld"
:data-linenumber="line.old_line"
:href="lineHref"
@click="setHighlightedRow(lineCode)"
:href="line.lineHref"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
@ -167,7 +154,11 @@ export default {
:discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
toggleLineDiscussions({
lineCode: line.lineCode,
fileHash,
expanded: !line.discussionsExpanded,
})
"
/>
</td>
@ -176,8 +167,8 @@ export default {
v-if="line.new_line"
ref="lineNumberRefNew"
:data-linenumber="line.new_line"
:href="lineHref"
@click="setHighlightedRow(lineCode)"
:href="line.lineHref"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</td>

View File

@ -2,18 +2,18 @@
import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import InlineDraftCommentRow from '~/batch_comments/components/inline_draft_comment_row.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
import inlineDiffExpansionRow from './inline_diff_expansion_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
inlineDiffCommentRow,
DiffCommentCell,
inlineDiffTableRow,
InlineDraftCommentRow,
inlineDiffExpansionRow,
DraftNote,
DiffExpansionCell,
},
mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
props: {
@ -65,15 +65,19 @@ export default {
</colgroup>
<tbody>
<template v-for="(line, index) in diffLines">
<inline-diff-expansion-row
:key="`expand-${index}`"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
<tr v-if="line.isMatchLine" :key="`expand-${index}`" class="line_expansion match">
<td colspan="4" class="text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</td>
</tr>
<inline-diff-table-row
v-if="!line.isMatchLine"
:key="`${line.line_code || index}`"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
@ -81,20 +85,32 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<inline-diff-comment-row
<tr
v-if="line.renderCommentRow"
:key="`icr-${line.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
:help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false"
/>
<inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
:key="`draft_${index}`"
:draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile"
:line="line"
/>
:class="line.commentRowClasses"
class="notes_holder"
>
<td class="notes-content" colspan="4">
<diff-comment-cell
:diff-file-hash="diffFile.file_hash"
:line="line"
:help-page-path="helpPagePath"
:has-draft="line.hasDraft"
/>
</td>
</tr>
<tr v-if="line.hasDraft" :key="`draft_${index}`" class="notes_holder js-temp-notes-holder">
<td class="notes-content" colspan="4">
<div class="content">
<draft-note
:draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile"
:line="line"
/>
</div>
</td>
</tr>
</template>
</tbody>
</table>

View File

@ -1,175 +0,0 @@
<script>
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
type: Object,
required: true,
},
diffFileHash: {
type: String,
required: true,
},
lineIndex: {
type: Number,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
hasDraftLeft: {
type: Boolean,
required: false,
default: false,
},
hasDraftRight: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length
? this.line.left.discussionsExpanded
: false;
},
hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length
? this.line.right.discussionsExpanded
: false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
return (
this.line.left &&
this.line.left.discussions &&
this.line.left.discussions.length &&
this.hasExpandedDiscussionOnLeft
);
},
shouldRenderDiscussionsOnRight() {
return (
this.line.right &&
this.line.right.discussions &&
this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
},
showRightSideCommentForm() {
return this.line.right && this.line.right.type && this.line.right.hasForm;
},
showLeftSideCommentForm() {
return this.line.left && this.line.left.hasForm;
},
className() {
return (this.left && this.line.left.discussions.length > 0) ||
(this.right && this.line.right.discussions.length > 0)
? ''
: 'js-temp-notes-holder';
},
shouldRender() {
const { line } = this;
const hasDiscussion =
(line.left && line.left.discussions && line.left.discussions.length) ||
(line.right && line.right.discussions && line.right.discussions.length);
if (
hasDiscussion &&
(this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight)
) {
return true;
}
const hasCommentFormOnLeft = line.left && line.left.hasForm;
const hasCommentFormOnRight = line.right && line.right.hasForm;
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
shouldRenderReplyPlaceholderOnLeft() {
return Boolean(
this.line.left && this.line.left.discussions && this.line.left.discussions.length,
);
},
shouldRenderReplyPlaceholderOnRight() {
return Boolean(
this.line.right && this.line.right.discussions && this.line.right.discussions.length,
);
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
showNewDiscussionForm(lineCode) {
this.showCommentForm({ lineCode, fileHash: this.diffFileHash });
},
},
};
</script>
<template>
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes-content parallel old" colspan="3">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
:discussions="line.left.discussions"
:line="line.left"
:help-page-path="helpPagePath"
/>
</div>
<diff-discussion-reply
v-if="!hasDraftLeft"
:has-form="showLeftSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
@showNewDiscussionForm="showNewDiscussionForm(line.left.line_code)"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
</template>
</diff-discussion-reply>
</td>
<td class="notes-content parallel new" colspan="3">
<div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions
:discussions="line.right.discussions"
:line="line.right"
:help-page-path="helpPagePath"
/>
</div>
<diff-discussion-reply
v-if="!hasDraftRight"
:has-form="showRightSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
@showNewDiscussionForm="showNewDiscussionForm(line.right.line_code)"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
</template>
</diff-discussion-reply>
</td>
</tr>
</template>

View File

@ -1,56 +0,0 @@
<script>
import { MATCH_LINE_TYPE } from '../constants';
import DiffExpansionCell from './diff_expansion_cell.vue';
export default {
components: {
DiffExpansionCell,
},
props: {
fileHash: {
type: String,
required: true,
},
contextLinesPath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isTop: {
type: Boolean,
required: false,
default: false,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isMatchLineLeft() {
return this.line.left && this.line.left.type === MATCH_LINE_TYPE;
},
isMatchLineRight() {
return this.line.right && this.line.right.type === MATCH_LINE_TYPE;
},
},
};
</script>
<template>
<tr class="line_expansion match">
<template v-if="isMatchLineLeft || isMatchLineRight">
<diff-expansion-cell
:file-hash="fileHash"
:context-lines-path="contextLinesPath"
:line="line.left"
:is-top="isTop"
:is-bottom="isBottom"
:colspan="6"
/>
</template>
</tr>
</template>

View File

@ -55,27 +55,15 @@ export default {
return utils.isHighlighted(state, line, this.isCommented);
},
}),
isContextLineLeft() {
return utils.isContextLine(this.line.left?.type);
},
isContextLineRight() {
return utils.isContextLine(this.line.right?.type);
},
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.isContextLineLeft,
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
},
isMatchLineLeft() {
return utils.isMatchLine(this.line.left?.type);
},
isMatchLineRight() {
return utils.isMatchLine(this.line.right?.type);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
@ -107,40 +95,19 @@ export default {
shouldShowCommentButtonLeft() {
return utils.shouldShowCommentButton(
this.isLeftHover,
this.isContextLineLeft,
this.isMetaLineLeft,
this.hasDiscussionsLeft,
this.line.isContextLineLeft,
this.line.isMetaLineLeft,
this.line.hasDiscussionsLeft,
);
},
shouldShowCommentButtonRight() {
return utils.shouldShowCommentButton(
this.isRightHover,
this.isContextLineRight,
this.isMetaLineRight,
this.hasDiscussionsRight,
this.line.isContextLineRight,
this.line.isMetaLineRight,
this.line.hasDiscussionsRight,
);
},
hasDiscussionsLeft() {
return utils.hasDiscussions(this.line.left);
},
hasDiscussionsRight() {
return utils.hasDiscussions(this.line.right);
},
lineHrefOld() {
return utils.lineHref(this.line.left);
},
lineHrefNew() {
return utils.lineHref(this.line.right);
},
lineCode() {
return utils.lineCode(this.line);
},
isMetaLineLeft() {
return utils.isMetaLine(this.line.left?.type);
},
isMetaLineRight() {
return utils.isMetaLine(this.line.right?.type);
},
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
@ -203,7 +170,7 @@ export default {
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>
<template v-if="line.left && !isMatchLineLeft">
<template v-if="line.left && !line.isMatchLineLeft">
<td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line">
<span
v-if="shouldRenderCommentButton"
@ -227,12 +194,12 @@ export default {
v-if="line.left.old_line"
ref="lineNumberRefOld"
:data-linenumber="line.left.old_line"
:href="lineHrefOld"
@click="setHighlightedRow(lineCode)"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="hasDiscussionsLeft"
v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded"
@toggleLineDiscussions="
@ -259,7 +226,7 @@ export default {
<td class="line-coverage left-side empty-cell"></td>
<td class="line_content with-coverage parallel left-side empty-cell"></td>
</template>
<template v-if="line.right && !isMatchLineRight">
<template v-if="line.right && !line.isMatchLineRight">
<td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line">
<span
v-if="shouldRenderCommentButton"
@ -283,12 +250,12 @@ export default {
v-if="line.right.new_line"
ref="lineNumberRefNew"
:data-linenumber="line.right.new_line"
:href="lineHrefNew"
@click="setHighlightedRow(lineCode)"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="hasDiscussionsRight"
v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded"
@toggleLineDiscussions="

View File

@ -1,18 +1,18 @@
<script>
import { mapGetters, mapState } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import ParallelDraftCommentRow from '~/batch_comments/components/parallel_draft_comment_row.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
import parallelDiffExpansionRow from './parallel_diff_expansion_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
parallelDiffExpansionRow,
DiffExpansionCell,
parallelDiffTableRow,
parallelDiffCommentRow,
ParallelDraftCommentRow,
DiffCommentCell,
DraftNote,
},
mixins: [draftCommentsMixin],
props: {
@ -66,14 +66,21 @@ export default {
</colgroup>
<tbody>
<template v-for="(line, index) in diffLines">
<parallel-diff-expansion-row
<tr
v-if="line.isMatchLineLeft || line.isMatchLineRight"
:key="`expand-${index}`"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
class="line_expansion match"
>
<td colspan="6" class="text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line.left"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</td>
</tr>
<parallel-diff-table-row
:key="line.line_code"
:file-hash="diffFile.file_hash"
@ -82,21 +89,53 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<parallel-diff-comment-row
<tr
v-if="line.renderCommentRow"
:key="`dcr-${line.line_code || index}`"
:line="line"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
:has-draft-left="hasParallelDraftLeft(diffFile.file_hash, line) || false"
:has-draft-right="hasParallelDraftRight(diffFile.file_hash, line) || false"
/>
<parallel-draft-comment-row
:class="line.commentRowClasses"
class="notes_holder"
>
<td class="notes-content parallel old" colspan="3">
<diff-comment-cell
v-if="line.left"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
:has-draft="line.left.hasDraft"
line-position="left"
/>
</td>
<td class="notes-content parallel new" colspan="3">
<diff-comment-cell
v-if="line.right"
:line="line.right"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
:has-draft="line.right.hasDraft"
line-position="right"
/>
</td>
</tr>
<tr
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
:key="`drafts-${index}`"
:line="line"
:diff-file-content-sha="diffFile.file_hash"
/>
:class="line.draftRowClasses"
class="notes_holder"
>
<td class="notes_line old"></td>
<td class="notes-content parallel old" colspan="2">
<div v-if="line.left && line.left.lineDraft.isDraft" class="content">
<draft-note :draft="line.left.lineDraft" :line="line.left" />
</div>
</td>
<td class="notes_line new"></td>
<td class="notes-content parallel new" colspan="2">
<div v-if="line.right && line.right.lineDraft.isDraft" class="content">
<draft-note :draft="line.right.lineDraft" :line="line.right" />
</div>
</td>
</tr>
</template>
</tbody>
</table>

View File

@ -165,8 +165,8 @@ export const fileLineCoverage = state => (file, line) => {
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
export const diffLines = state => file => {
if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
export const diffLines = state => (file, unifiedDiffComponents) => {
if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
return null;
}

View File

@ -84,13 +84,7 @@ export default class Issue {
projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
this.createMergeRequestDropdown.unavailable();
this.createMergeRequestDropdown.disable();
} else {
// We should check in case a branch was created in another tab
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
} else {
flash(issueFailMessage);

View File

@ -0,0 +1,66 @@
<script>
import { GlLink, GlModal } from '@gitlab/ui';
import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '../constants';
export default {
name: 'JobRetryForwardDeploymentModal',
components: {
GlLink,
GlModal,
},
i18n: {
...JOB_RETRY_FORWARD_DEPLOYMENT_MODAL,
},
props: {
modalId: {
type: String,
required: true,
},
href: {
type: String,
required: true,
},
},
inject: {
retryOutdatedJobDocsUrl: {
default: '',
},
},
data() {
return {
primaryProps: {
text: this.$options.i18n.primaryText,
attributes: [
{
'data-method': 'post',
'data-testid': 'retry-button-modal',
href: this.href,
variant: 'danger',
},
],
},
cancelProps: {
text: this.$options.i18n.cancel,
attributes: [{ category: 'secondary', variant: 'default' }],
},
};
},
};
</script>
<template>
<gl-modal
:action-cancel="cancelProps"
:action-primary="primaryProps"
:modal-id="modalId"
:title="$options.i18n.title"
>
<p>
{{ $options.i18n.info }}
<gl-link v-if="retryOutdatedJobDocsUrl" :href="retryOutdatedJobDocsUrl" target="_blank">
{{ $options.i18n.moreInfo }}
</gl-link>
</p>
<p>{{ $options.i18n.areYouSure }}</p>
</gl-modal>
</template>

View File

@ -0,0 +1,45 @@
<script>
import { GlButton, GlLink, GlModalDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { JOB_SIDEBAR } from '../constants';
export default {
name: 'JobSidebarRetryButton',
i18n: {
retryLabel: JOB_SIDEBAR.retry,
},
components: {
GlButton,
GlLink,
},
directives: {
GlModal: GlModalDirective,
},
props: {
modalId: {
type: String,
required: true,
},
href: {
type: String,
required: true,
},
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
},
};
</script>
<template>
<gl-button
v-if="hasForwardDeploymentFailure"
v-gl-modal="modalId"
:aria-label="$options.i18n.retryLabel"
category="primary"
variant="info"
>{{ $options.i18n.retryLabel }}</gl-button
>
<gl-link v-else :href="href" data-method="post" rel="nofollow"
>{{ $options.i18n.retryLabel }}
</gl-link>
</template>

View File

@ -24,7 +24,7 @@ export default {
};
</script>
<template>
<div class="js-jobs-container builds-container">
<div class="builds-container">
<job-container-item
v-for="job in jobs"
:key="job.id"

View File

@ -1,28 +1,39 @@
<script>
import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import ArtifactsBlock from './artifacts_block.vue';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
import SidebarJobDetailsContainer from './sidebar_job_details_container.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
import { JOB_SIDEBAR } from '../constants';
export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
export default {
name: 'JobSidebar',
i18n: {
...JOB_SIDEBAR,
},
forwardDeploymentFailureModalId,
components: {
ArtifactsBlock,
CommitBlock,
GlIcon,
TriggerBlock,
StagesDropdown,
JobsContainer,
GlLink,
GlButton,
SidebarJobDetailsContainer,
GlLink,
GlIcon,
JobsContainer,
JobSidebarRetryButton,
JobRetryForwardDeploymentModal,
JobSidebarDetailsContainer,
StagesDropdown,
TooltipOnTruncate,
TriggerBlock,
},
props: {
artifactHelpUrl: {
@ -37,9 +48,10 @@ export default {
},
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
...mapState(['job', 'stages', 'jobs', 'selectedStage']),
retryButtonClass() {
let className = 'js-retry-button btn btn-retry';
let className = 'btn btn-retry';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
@ -56,6 +68,9 @@ export default {
commit() {
return this.job?.pipeline?.commit || {};
},
shouldShowJobRetryForwardDeploymentModal() {
return this.job.retry_path && this.hasForwardDeploymentFailure;
},
},
methods: {
...mapActions(['fetchJobsForStage', 'toggleSidebar']),
@ -73,27 +88,27 @@ export default {
</h4>
</tooltip-on-truncate>
<div class="flex-grow-1 flex-shrink-0 text-right">
<gl-link
<job-sidebar-retry-button
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
:modal-id="$options.forwardDeploymentFailureModalId"
data-qa-selector="retry_button"
rel="nofollow"
>{{ __('Retry') }}
</gl-link>
data-testid="retry-button"
/>
<gl-link
v-if="job.cancel_path"
:href="job.cancel_path"
class="js-cancel-job btn btn-default"
class="btn btn-default"
data-method="post"
data-testid="cancel-button"
rel="nofollow"
>{{ __('Cancel') }}
>{{ $options.i18n.cancel }}
</gl-link>
</div>
<gl-button
:aria-label="__('Toggle Sidebar')"
:aria-label="$options.i18n.toggleSidebar"
category="tertiary"
class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle"
icon="chevron-double-lg-right"
@ -107,19 +122,20 @@ export default {
:href="job.new_issue_path"
class="btn btn-success btn-inverted float-left mr-2"
data-testid="job-new-issue"
>{{ __('New issue') }}
>{{ $options.i18n.newIssue }}
</gl-link>
<gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
class="btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
target="_blank"
data-testid="terminal-link"
>
{{ __('Debug') }}
{{ $options.i18n.debug }}
<gl-icon :size="14" name="external-link" />
</gl-link>
</div>
<sidebar-job-details-container :runner-help-url="runnerHelpUrl" />
<job-sidebar-details-container :runner-help-url="runnerHelpUrl" />
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
@ -139,5 +155,10 @@ export default {
<jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
</div>
<job-retry-forward-deployment-modal
v-if="shouldShowJobRetryForwardDeploymentModal"
:modal-id="$options.forwardDeploymentFailureModalId"
:href="job.retry_path"
/>
</aside>
</template>

View File

@ -6,7 +6,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
export default {
name: 'SidebarJobDetailsContainer',
name: 'JobSidebarDetailsContainer',
components: {
DetailRow,
},

View File

@ -1,11 +1,13 @@
<script>
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import { GlLink, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
components: {
CiIcon,
GlDropdown,
GlDropdownItem,
GlLink,
},
props: {
@ -78,20 +80,15 @@ export default {
</template>
</div>
<button
type="button"
data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle gl-mt-3"
>
{{ selectedStage }} <i class="fa fa-chevron-down"></i>
</button>
<ul class="dropdown-menu">
<li v-for="stage in stages" :key="stage.name">
<button type="button" class="js-stage-item stage-item" @click="onStageClick(stage)">
{{ stage.name }}
</button>
</li>
</ul>
<gl-dropdown :text="selectedStage" class="js-selected-stage gl-w-full gl-mt-3">
<gl-dropdown-item
v-for="stage in stages"
:key="stage.name"
class="js-stage-item stage-item"
@click="onStageClick(stage)"
>
{{ stage.name }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>

View File

@ -0,0 +1,24 @@
import { __, s__ } from '~/locale';
const cancel = __('Cancel');
const moreInfo = __('More information');
export const JOB_SIDEBAR = {
cancel,
debug: __('Debug'),
newIssue: __('New issue'),
retry: __('Retry'),
toggleSidebar: __('Toggle Sidebar'),
};
export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
cancel,
info: s__(
`Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment.
Retrying this job could result in overwriting the environment with the older source code.`,
),
areYouSure: s__('Jobs|Are you sure you want to proceed?'),
moreInfo,
primaryText: __('Retry job'),
title: s__('Jobs|Are you sure you want to retry this job?'),
};

View File

@ -10,27 +10,31 @@ export default () => {
// Let's start initializing the store (i.e. fetching data) right away
store.dispatch('init', element.dataset);
const {
artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
logState,
buildStatus,
projectPath,
retryOutdatedJobDocsUrl,
} = element.dataset;
return new Vue({
el: element,
store,
components: {
JobApp,
},
provide: {
retryOutdatedJobDocsUrl,
},
render(createElement) {
const {
artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
logState,
buildStatus,
projectPath,
} = element.dataset;
return createElement('job-app', {
props: {
artifactHelpUrl,

View File

@ -3,6 +3,9 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
export const hasForwardDeploymentFailure = state =>
state?.job?.failure_reason === 'forward_deployment_failure';
export const hasUnmetPrerequisitesFailure = state =>
state?.job?.failure_reason === 'unmet_prerequisites';

View File

@ -740,6 +740,24 @@ export const roundOffFloat = (number, precision = 0) => {
return Math.round(number * multiplier) / multiplier;
};
/**
* Method to round down values with decimal places
* with provided precision.
*
* Eg; roundDownFloat(3.141592, 3) = 3.141
*
* Refer to spec/javascripts/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
* @param {Number} precision
*/
export const roundDownFloat = (number, precision = 0) => {
// eslint-disable-next-line no-restricted-properties
const multiplier = Math.pow(10, precision);
return Math.floor(number * multiplier) / multiplier;
};
/**
* Represents navigation type constants of the Performance Navigation API.
* Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation.

View File

@ -37,15 +37,6 @@ export default {
ROW_SCHEDULED_FOR_DELETION,
},
computed: {
encodedItem() {
const params = JSON.stringify({
name: this.item.path,
tags_path: this.item.tags_path,
id: this.item.id,
cleanup_policy_started_at: this.item.cleanup_policy_started_at,
});
return window.btoa(params);
},
disabledDelete() {
return !this.item.destroy_path || this.item.deleting;
},
@ -82,7 +73,7 @@ export default {
<router-link
class="gl-text-body gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
:to="{ name: 'details', params: { id: item.id } }"
>
{{ item.path }}
</router-link>

View File

@ -30,7 +30,7 @@ export default {
return {
tagName,
className,
text: this.$route.meta.nameGenerator(this.$route),
text: this.$route.meta.nameGenerator(this.$store.state),
path: { to: this.$route.name },
};
},
@ -48,7 +48,7 @@ export default {
></li>
<li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path">
{{ rootRoute.meta.nameGenerator(rootRoute) }}
{{ rootRoute.meta.nameGenerator($store.state) }}
</router-link>
<component :is="divider.tagName" v-safe-html="divider.innerHTML" :class="divider.classList" />
</li>

View File

@ -11,7 +11,6 @@ import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
import { decodeAndParse } from '../utils';
import {
ALERT_SUCCESS_TAG,
ALERT_DANGER_TAG,
@ -43,12 +42,9 @@ export default {
};
},
computed: {
...mapState(['tagsPagination', 'isLoading', 'config', 'tags']),
queryParameters() {
return decodeAndParse(this.$route.params.id);
},
...mapState(['tagsPagination', 'isLoading', 'config', 'tags', 'imageDetails']),
showPartialCleanupWarning() {
return this.queryParameters.cleanup_policy_started_at && !this.dismissPartialCleanupWarning;
return this.imageDetails?.cleanup_policy_started_at && !this.dismissPartialCleanupWarning;
},
tracking() {
return {
@ -61,15 +57,20 @@ export default {
return this.tagsPagination.page;
},
set(page) {
this.requestTagsList({ pagination: { page }, params: this.$route.params.id });
this.requestTagsList({ page });
},
},
},
mounted() {
this.requestTagsList({ params: this.$route.params.id });
this.requestImageDetailsAndTagsList(this.$route.params.id);
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
...mapActions([
'requestTagsList',
'requestDeleteTag',
'requestDeleteTags',
'requestImageDetailsAndTagsList',
]),
deleteTags(toBeDeleted) {
this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]);
this.track('click_button');
@ -78,7 +79,7 @@ export default {
handleSingleDelete() {
const [itemToDelete] = this.itemsToBeDeleted;
this.itemsToBeDeleted = [];
return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
return this.requestDeleteTag({ tag: itemToDelete })
.then(() => {
this.deleteAlertType = ALERT_SUCCESS_TAG;
})
@ -92,7 +93,6 @@ export default {
return this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => x.name),
params: this.$route.params.id,
})
.then(() => {
this.deleteAlertType = ALERT_SUCCESS_TAGS;
@ -132,7 +132,7 @@ export default {
@dismiss="dismissPartialCleanupWarning = true"
/>
<details-header :image-name="queryParameters.name" />
<details-header :image-name="imageDetails.name" />
<tags-loader v-if="isLoading" />
<template v-else>

View File

@ -2,7 +2,6 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
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);
@ -26,7 +25,7 @@ export default function createRouter(base) {
path: '/:id',
component: Details,
meta: {
nameGenerator: route => decodeAndParse(route.params.id).name,
nameGenerator: ({ imageDetails }) => imageDetails?.name,
},
},
],

View File

@ -9,7 +9,7 @@ import {
FETCH_TAGS_LIST_ERROR_MESSAGE,
FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
} from '../constants/index';
import { decodeAndParse } from '../utils';
import { pathGenerator } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setShowGarbageCollectionTip = ({ commit }, data) =>
@ -45,13 +45,13 @@ export const requestImagesList = (
});
};
export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params }) => {
export const requestTagsList = ({ commit, dispatch, state: { imageDetails } }, pagination = {}) => {
commit(types.SET_MAIN_LOADING, true);
const { tags_path } = decodeAndParse(params);
const tagsPath = pathGenerator(imageDetails);
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios
.get(tags_path, { params: { page, per_page: perPage } })
.get(tagsPath, { params: { page, per_page: perPage } })
.then(({ data, headers }) => {
dispatch('receiveTagsListSuccess', { data, headers });
})
@ -76,30 +76,30 @@ export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => {
});
};
export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => {
export const requestDeleteTag = ({ commit, dispatch, state }, { tag }) => {
commit(types.SET_MAIN_LOADING, true);
return axios
.delete(tag.destroy_path)
.then(() => {
dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
return dispatch('requestTagsList', state.tagsPagination);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => {
export const requestDeleteTags = ({ commit, dispatch, state }, { ids }) => {
commit(types.SET_MAIN_LOADING, true);
const { tags_path } = decodeAndParse(params);
const url = tags_path.replace('?format=json', '/bulk_destroy');
const tagsPath = pathGenerator(state.imageDetails, '/bulk_destroy');
return axios
.delete(url, { params: { ids } })
.delete(tagsPath, { params: { ids } })
.then(() => {
dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
return dispatch('requestTagsList', state.tagsPagination);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false);

View File

@ -1,9 +1,6 @@
export const decodeAndParse = param => JSON.parse(window.atob(param));
// eslint-disable-next-line @gitlab/require-i18n-strings
export const pathGenerator = (imageDetails, ending = 'tags?format=json') => {
export const pathGenerator = (imageDetails, ending = '?format=json') => {
// this method is a temporary workaround, to be removed with graphql implementation
// https://gitlab.com/gitlab-org/gitlab/-/issues/276432
const basePath = imageDetails.path.replace(`/${imageDetails.name}`, '');
return `/${basePath}/registry/repository/${imageDetails.id}/${ending}`;
return `/${basePath}/registry/repository/${imageDetails.id}/tags${ending}`;
};

View File

@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { roundDownFloat } from '~/lib/utils/common_utils';
export default {
directives: {
@ -89,7 +89,7 @@ export default {
return 0;
}
const percent = roundOffFloat((count / this.totalCount) * 100, 1);
const percent = roundDownFloat((count / this.totalCount) * 100, 1);
if (percent > 0 && percent < 1) {
return '< 1';
}

View File

@ -449,6 +449,7 @@
}
}
.diff-table.code,
table.code {
width: 100%;
font-family: $monospace-font;
@ -459,10 +460,12 @@ table.code {
table-layout: fixed;
border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-tr:first-of-type.line_expansion > .diff-td,
tr:first-of-type.line_expansion > td {
border-top: 0;
}
.diff-tr:nth-last-of-type(2).line_expansion > .diff-td,
tr:nth-last-of-type(2).line_expansion,
tr:last-of-type.line_expansion {
> td {
@ -470,6 +473,7 @@ table.code {
}
}
.diff-tr.line_holder .diff-td,
tr.line_holder td {
line-height: $code-line-height;
font-size: $code-font-size;
@ -565,24 +569,95 @@ table.code {
}
.line_holder:last-of-type {
.diff-td:first-child,
td:first-child {
border-bottom-left-radius: $border-radius-default;
}
}
&.left-side-selected {
.diff-td.line_content.parallel.right-side,
td.line_content.parallel.right-side {
user-select: none;
}
}
&.right-side-selected {
.diff-td.line_content.parallel.left-side,
td.line_content.parallel.left-side {
user-select: none;
}
}
}
// Merge request diff grid layout
.diff-grid {
.diff-grid-row {
display: grid;
grid-template-columns: 1fr 1fr;
}
.diff-grid-left,
.diff-grid-right {
display: grid;
grid-template-columns: 50px 8px 1fr;
.diff-td:nth-child(2) {
display: none;
}
}
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr 1fr;
}
.diff-grid-drafts {
display: grid;
grid-template-columns: 1fr 1fr;
}
&.inline {
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr;
}
.diff-grid-drafts {
display: grid;
grid-template-columns: 1fr;
}
.diff-grid-row {
grid-template-columns: 1fr;
}
.diff-grid-left,
.diff-grid-right {
grid-template-columns: 50px 50px 8px 1fr;
.diff-td:nth-child(2) {
display: block;
}
}
.diff-grid-left .old:nth-child(2) [data-linenumber],
.diff-grid-right .new:nth-child(2) [data-linenumber] {
display: inline;
}
.diff-grid-left .old:nth-child(3) [data-linenumber],
.diff-grid-right .new:nth-child(1) [data-linenumber] {
display: none;
}
}
}
// Merge request diff grid layout overrides
.diff-table.code .diff-tr.line_holder .diff-td.line_content.parallel {
width: unset;
}
.diff-stats {
align-items: center;
padding: 0 1rem;

View File

@ -20,6 +20,7 @@
@mixin diff-expansion($background, $border, $link) {
background-color: $background;
.diff-td,
td {
border-top: 1px solid $border;
border-bottom: 1px solid $border;
@ -41,3 +42,12 @@
border-left: 3px solid $no-coverage;
}
}
@mixin line-number-hover($color) {
background-color: $color;
border-color: darken($color, 5%);
a {
color: darken($color, 15%);
}
}

View File

@ -125,6 +125,9 @@ $dark-il: #de935f;
@include dark-diff-match-line;
}
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
@ -158,15 +161,17 @@ $dark-il: #de935f;
}
}
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($dark-over-bg);
}
}
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $dark-over-bg;
border-color: darken($dark-over-bg, 5%);
a {
color: darken($dark-over-bg, 15%);
}
@include line-number-hover($dark-over-bg);
}
}

View File

@ -125,6 +125,9 @@ $monokai-gi: #a6e22e;
@include dark-diff-match-line;
}
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
@ -158,15 +161,17 @@ $monokai-gi: #a6e22e;
}
}
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($monokai-over-bg);
}
}
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $monokai-over-bg;
border-color: darken($monokai-over-bg, 5%);
a {
color: darken($monokai-over-bg, 15%);
}
@include line-number-hover($monokai-over-bg);
}
}

View File

@ -59,6 +59,13 @@
}
}
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($none-over-bg);
}
}
.diff-line-num {
&.old {
a {
@ -74,12 +81,7 @@
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $none-over-bg;
border-color: darken($none-over-bg, 5%);
a {
color: darken($none-over-bg, 15%);
}
@include line-number-hover($none-over-bg);
}
&.hll:not(.empty-cell) {

View File

@ -129,6 +129,9 @@ $solarized-dark-il: #2aa198;
@include dark-diff-match-line;
}
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
@ -140,6 +143,13 @@ $solarized-dark-il: #2aa198;
@include line-coverage-border-color($solarized-dark-coverage, $solarized-dark-no-coverage);
}
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($solarized-dark-over-bg);
}
}
.diff-line-num.new,
.line-coverage.new,
.line_content.new {
@ -165,12 +175,7 @@ $solarized-dark-il: #2aa198;
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-dark-over-bg;
border-color: darken($solarized-dark-over-bg, 5%);
a {
color: darken($solarized-dark-over-bg, 15%);
}
@include line-number-hover($solarized-dark-over-bg);
}
}

View File

@ -136,6 +136,9 @@ $solarized-light-il: #2aa198;
@include match-line;
}
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
@ -159,6 +162,13 @@ $solarized-light-il: #2aa198;
}
}
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($solarized-light-over-bg);
}
}
.diff-line-num.old,
.line-coverage.old,
.line_content.old {
@ -173,12 +183,7 @@ $solarized-light-il: #2aa198;
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-light-over-bg;
border-color: darken($solarized-light-over-bg, 5%);
a {
color: darken($solarized-light-over-bg, 15%);
}
@include line-number-hover($solarized-light-over-bg);
}
}

View File

@ -113,6 +113,13 @@ pre.code,
@include match-line;
}
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($white-over-bg);
}
}
.diff-line-num {
&.old {
background-color: $line-number-old;
@ -134,12 +141,7 @@ pre.code,
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
@include line-number-hover($white-over-bg);
}
&.hll:not(.empty-cell) {

View File

@ -2,6 +2,7 @@
* Note Form
*/
.diff-file .diff-content {
.diff-tr.line_holder:hover > .diff-td .line_note_link,
tr.line_holder:hover > td .line_note_link {
opacity: 1;
filter: alpha(opacity = 100);

View File

@ -453,6 +453,8 @@ $note-form-margin-left: 72px;
}
.diff-file {
.diff-grid-left:hover,
.diff-grid-right:hover,
.is-over {
.add-diff-note {
display: inline-flex;
@ -490,6 +492,7 @@ $note-form-margin-left: 72px;
.notes_holder {
font-family: $regular-font;
.diff-td,
td {
border: 1px solid $border-color;
border-left: 0;
@ -805,6 +808,8 @@ $note-form-margin-left: 72px;
* Line note button on the side of diffs
*/
.diff-grid-left:hover,
.diff-grid-right:hover,
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;

View File

@ -218,8 +218,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# TODO Remove domain_denylist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-foss/issues/67204)
params.delete(:domain_denylist_raw) if params[:domain_denylist_file]
params.delete(:domain_denylist_raw) if params[:domain_blacklist]
params.delete(:domain_allowlist_raw) if params[:domain_whitelist]
params.delete(:domain_denylist_raw) if params[:domain_denylist]
params.delete(:domain_allowlist_raw) if params[:domain_allowlist]
params.require(:application_setting).permit(
visible_application_setting_attributes

View File

@ -37,6 +37,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:unified_diff_lines, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project)
push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)

View File

@ -33,9 +33,9 @@ module Mutations
super
end
def resolve(args)
def resolve(queue_name:, **args)
{
result: Gitlab::SidekiqQueue.new(args[:queue_name]).drop_jobs!(args, timeout: 30),
result: Gitlab::SidekiqQueue.new(queue_name).drop_jobs!(args, timeout: 30),
errors: []
}
rescue Gitlab::SidekiqQueue::NoMetadataError
@ -44,7 +44,7 @@ module Mutations
errors: ['No metadata provided']
}
rescue Gitlab::SidekiqQueue::InvalidQueueError
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Queue #{args[:queue_name]} not found"
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Queue #{queue_name} not found"
end
end
end

View File

@ -4,7 +4,6 @@ module Mutations
module AlertManagement
class Base < BaseMutation
include Gitlab::Utils::UsageData
include ResolvesProject
argument :project_path, GraphQL::ID_TYPE,
required: true,
@ -33,13 +32,12 @@ module Mutations
private
def find_object(project_path:, iid:)
project = resolve_project(full_path: project_path)
def find_object(project_path:, **args)
project = Project.find_by_full_path(project_path)
return unless project
resolver = Resolvers::AlertManagement::AlertResolver.single.new(object: project, context: context, field: nil)
resolver.resolve(iid: iid)
::AlertManagement::AlertsFinder.new(current_user, project, args).execute.first
end
end
end

View File

@ -9,9 +9,9 @@ module Mutations
required: true,
description: 'The status to set the alert'
def resolve(args)
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = update_status(alert, args[:status])
def resolve(project_path:, iid:, status:)
alert = authorized_find!(project_path: project_path, iid: iid)
result = update_status(alert, status)
track_usage_event(:incident_management_alert_status_changed, current_user.id)

View File

@ -11,16 +11,6 @@ module Mutations
id = ::Types::GlobalIDType[::Todo].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def map_to_global_ids(ids)
return [] if ids.blank?
ids.map { |id| to_global_id(id) }
end
def to_global_id(id)
Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s
end
end
end
end

View File

@ -8,7 +8,7 @@ module Mutations
authorize :update_user
field :updated_ids,
[GraphQL::ID_TYPE],
[::Types::GlobalIDType[::Todo]],
null: false,
deprecated: { reason: 'Use todos', milestone: '13.2' },
description: 'Ids of the updated todos'
@ -23,7 +23,7 @@ module Mutations
updated_ids = mark_all_todos_done
{
updated_ids: map_to_global_ids(updated_ids),
updated_ids: updated_ids,
todos: Todo.id_in(updated_ids),
errors: []
}

View File

@ -12,7 +12,7 @@ module Mutations
required: true,
description: 'The global ids of the todos to restore (a maximum of 50 is supported at once)'
field :updated_ids, [GraphQL::ID_TYPE],
field :updated_ids, [::Types::GlobalIDType[Todo]],
null: false,
description: 'The ids of the updated todo items',
deprecated: { reason: 'Use todos', milestone: '13.2' }
@ -28,7 +28,7 @@ module Mutations
updated_ids = restore(todos)
{
updated_ids: gids_of(updated_ids),
updated_ids: updated_ids,
todos: Todo.id_in(updated_ids),
errors: errors_on_objects(todos)
}
@ -36,10 +36,6 @@ module Mutations
private
def gids_of(ids)
ids.map { |id| Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s }
end
def model_ids_of(ids)
ids.map do |gid|
# TODO: remove this line when the compatibility layer is removed

View File

@ -199,11 +199,11 @@ module ApplicationSettingsHelper
:default_projects_limit,
:default_snippet_visibility,
:disabled_oauth_sign_in_sources,
:domain_blacklist,
:domain_blacklist_enabled,
:domain_denylist,
:domain_denylist_enabled,
# TODO Remove domain_denylist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-foss/issues/67204)
:domain_denylist_raw,
:domain_whitelist,
:domain_allowlist,
# TODO Remove domain_allowlist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-foss/issues/67204)
:domain_allowlist_raw,
:outbound_local_requests_allowlist_raw,

View File

@ -15,7 +15,8 @@ module Ci
"build_status" => @build.status,
"build_stage" => @build.stage,
"log_state" => '',
"build_options" => javascript_build_options
"build_options" => javascript_build_options,
"retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs')
}
end
end

View File

@ -40,8 +40,8 @@ class ApplicationSetting < ApplicationRecord
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
@ -184,9 +184,9 @@ class ApplicationSetting < ApplicationRecord
validates :enabled_git_access_protocol,
inclusion: { in: %w(ssh http), allow_blank: true }
validates :domain_blacklist,
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
validates :domain_denylist,
presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' },
if: :domain_denylist_enabled?
validates :housekeeping_incremental_repack_period,
presence: true,

View File

@ -60,7 +60,7 @@ module ApplicationSettingImplementation
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
domain_whitelist: Settings.gitlab['domain_whitelist'],
domain_allowlist: Settings.gitlab['domain_allowlist'],
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
@ -203,19 +203,19 @@ module ApplicationSettingImplementation
end
def domain_allowlist_raw
array_to_string(self.domain_whitelist)
array_to_string(self.domain_allowlist)
end
def domain_denylist_raw
array_to_string(self.domain_blacklist)
array_to_string(self.domain_denylist)
end
def domain_allowlist_raw=(values)
self.domain_whitelist = strings_to_array(values)
self.domain_allowlist = strings_to_array(values)
end
def domain_denylist_raw=(values)
self.domain_blacklist = strings_to_array(values)
self.domain_denylist = strings_to_array(values)
end
def domain_denylist_file=(file)
@ -242,7 +242,7 @@ module ApplicationSettingImplementation
end
# This method separates out the strings stored in the
# application_setting.outbound_local_requests_allowlist array into 2 arrays;
# application_setting.outbound_local_requests_whitelist array into 2 arrays;
# an array of IPAddr objects (`[IPAddr.new('127.0.0.1')]`), and an array of
# domain strings (`['www.example.com']`).
def outbound_local_requests_allowlist_arrays

View File

@ -1843,15 +1843,15 @@ class User < ApplicationRecord
valid = true
error = nil
if Gitlab::CurrentSettings.domain_blacklist_enabled?
blocked_domains = Gitlab::CurrentSettings.domain_blacklist
if Gitlab::CurrentSettings.domain_denylist_enabled?
blocked_domains = Gitlab::CurrentSettings.domain_denylist
if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
end
allowed_domains = Gitlab::CurrentSettings.domain_whitelist
allowed_domains = Gitlab::CurrentSettings.domain_allowlist
unless allowed_domains.blank?
if domain_matches?(allowed_domains, email)
valid = true

View File

@ -31,14 +31,14 @@
.form-text.text-muted
= _("See GitLab's %{password_policy_guidelines}").html_safe % { password_policy_guidelines: password_policy_guidelines_link }
.form-group
= f.label :domain_whitelist, _('Allowed domains for sign-ups'), class: 'label-bold'
= f.label :domain_allowlist, _('Allowed domains for sign-ups'), class: 'label-bold'
= f.text_area :domain_allowlist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
.form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :domain_blacklist_enabled, _('Domain denylist'), class: 'label-bold'
= f.label :domain_denylist_enabled, _('Domain denylist'), class: 'label-bold'
.form-check
= f.check_box :domain_blacklist_enabled, class: 'form-check-input'
= f.label :domain_blacklist_enabled, class: 'form-check-label' do
= f.check_box :domain_denylist_enabled, class: 'form-check-input'
= f.label :domain_denylist_enabled, class: 'form-check-label' do
Enable domain denylist for sign ups
.form-group
.form-check
@ -47,7 +47,7 @@
.option-title
Upload denylist file
.form-check
= radio_button_tag :denylist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input'
= radio_button_tag :denylist_type, :raw, @application_setting.domain_denylist.present? || @application_setting.domain_denylist.blank?, class: 'form-check-input'
= label_tag :denylist_type_raw, class: 'form-check-label' do
.option-title
Enter denylist manually
@ -56,7 +56,7 @@
= f.file_field :domain_denylist_file, class: 'form-control', accept: '.txt,.conf'
.form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
.form-group.js-denylist-raw
= f.label :domain_blacklist, _('Denied domains for sign-ups'), class: 'label-bold'
= f.label :domain_denylist, _('Denied domains for sign-ups'), class: 'label-bold'
= f.text_area :domain_denylist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
.form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group

View File

@ -16,15 +16,16 @@ module ContainerExpirationPolicies
return unless throttling_enabled?
return unless container_repository
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
unless allowed_to_run?(container_repository)
container_repository.cleanup_unscheduled!
log_info(container_repository_id: container_repository.id, cleanup_status: :skipped)
log_extra_metadata_on_done(:cleanup_status, :skipped)
return
end
result = ContainerExpirationPolicies::CleanupService.new(container_repository)
.execute
log_extra_metadata_on_done(:container_repository_id, result.payload[:container_repository_id])
log_extra_metadata_on_done(:cleanup_status, result.payload[:cleanup_status])
end

View File

@ -0,0 +1,5 @@
---
title: Change the mutation and permissions for image note reposition
merge_request: 47161
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Forward deployment, add modal to warn users on Retry action
merge_request: 46416
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update container_scanning to version 3 to support FIPS
merge_request: 47099
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Migrate-Bootstrap-dropdown-to-GitLab-UI-GlDropdown-in-app/assets/javascripts/jobs/components/stages_dropdown.vue
merge_request: 41452
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Use allowlist/denylist in application settings backend
merge_request: 46170
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Use new image details API in container registry details
merge_request: 47054
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fixed create branch button not hiding when issue is closed
merge_request: 47187
author:
type: fixed

View File

@ -0,0 +1,7 @@
---
name: unified_diff_components
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44974
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/268039
type: development
group: group::source code
default_enabled: false

View File

@ -198,7 +198,7 @@ Settings.gitlab.default_projects_features['snippets'] = true if Settin
Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.__send__(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['domain_allowlist'] ||= []
Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['content_security_policy'] ||= Gitlab::ContentSecurityPolicy::ConfigLoader.default_settings_hash

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RenameApplicationSettingsToAllowDenyNames < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
rename_column_concurrently :application_settings, :domain_blacklist_enabled, :domain_denylist_enabled
rename_column_concurrently :application_settings, :domain_blacklist, :domain_denylist
rename_column_concurrently :application_settings, :domain_whitelist, :domain_allowlist
end
def down
undo_rename_column_concurrently :application_settings, :domain_blacklist_enabled, :domain_denylist_enabled
undo_rename_column_concurrently :application_settings, :domain_blacklist, :domain_denylist
undo_rename_column_concurrently :application_settings, :domain_whitelist, :domain_allowlist
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class CleanupApplicationSettingsToAllowDenyRename < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :application_settings, :domain_blacklist_enabled, :domain_denylist_enabled
cleanup_concurrent_column_rename :application_settings, :domain_blacklist, :domain_denylist
cleanup_concurrent_column_rename :application_settings, :domain_whitelist, :domain_allowlist
end
def down
undo_cleanup_concurrent_column_rename :application_settings, :domain_blacklist_enabled, :domain_denylist_enabled
undo_cleanup_concurrent_column_rename :application_settings, :domain_blacklist, :domain_denylist
undo_cleanup_concurrent_column_rename :application_settings, :domain_whitelist, :domain_allowlist
end
end

View File

@ -0,0 +1 @@
c718bc731f7dc3e1f0104dfdb79a3dc46c46849153ec9b228600eeb5a92465e7

View File

@ -0,0 +1 @@
a61310c95a1302871ea18881d45bc0c7357baa8f24daa31b7e2174318dab5707

View File

@ -9083,7 +9083,6 @@ CREATE TABLE application_settings (
max_attachment_size integer DEFAULT 10 NOT NULL,
default_project_visibility integer DEFAULT 0 NOT NULL,
default_snippet_visibility integer DEFAULT 0 NOT NULL,
domain_whitelist text,
user_oauth_applications boolean DEFAULT true,
after_sign_out_path character varying,
session_expire_delay integer DEFAULT 10080 NOT NULL,
@ -9119,8 +9118,6 @@ CREATE TABLE application_settings (
elasticsearch_search boolean DEFAULT false NOT NULL,
repository_storages character varying DEFAULT 'default'::character varying,
enabled_git_access_protocol character varying,
domain_blacklist_enabled boolean DEFAULT false,
domain_blacklist text,
usage_ping_enabled boolean DEFAULT true NOT NULL,
sign_in_text_html text,
help_page_text_html text,
@ -9341,6 +9338,9 @@ CREATE TABLE application_settings (
secret_detection_token_revocation_url text,
encrypted_secret_detection_token_revocation_token text,
encrypted_secret_detection_token_revocation_token_iv text,
domain_denylist_enabled boolean DEFAULT false,
domain_denylist text,
domain_allowlist text,
new_user_signups_cap integer,
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),

View File

@ -20841,7 +20841,7 @@ type TodoRestoreManyPayload {
"""
The ids of the updated todo items. Deprecated in 13.2: Use todos
"""
updatedIds: [ID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
updatedIds: [TodoID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
}
"""
@ -20938,7 +20938,7 @@ type TodosMarkAllDonePayload {
"""
Ids of the updated todos. Deprecated in 13.2: Use todos
"""
updatedIds: [ID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
updatedIds: [TodoID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
}
"""

View File

@ -60657,7 +60657,7 @@
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"name": "TodoID",
"ofType": null
}
}
@ -60934,7 +60934,7 @@
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"name": "TodoID",
"ofType": null
}
}

View File

@ -3077,7 +3077,7 @@ Autogenerated return type of TodoRestoreMany.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `todos` | Todo! => Array | Updated todos |
| `updatedIds` **{warning-solid}** | ID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
| `updatedIds` **{warning-solid}** | TodoID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
### TodoRestorePayload
@ -3098,7 +3098,7 @@ Autogenerated return type of TodosMarkAllDone.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `todos` | Todo! => Array | Updated todos |
| `updatedIds` **{warning-solid}** | ID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
| `updatedIds` **{warning-solid}** | TodoID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
### ToggleAwardEmojiPayload

View File

@ -95,7 +95,7 @@ GET /groups?statistics=true
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z",
"statistics": {
"storage_size" : 212,
"storage_size" : 363,
"repository_size" : 33,
"wiki_size" : 100,
"lfs_objects_size" : 123,

View File

@ -43,9 +43,9 @@ Example response:
"home_page_url" : null,
"default_snippet_visibility" : "private",
"outbound_local_requests_whitelist": [],
"domain_whitelist" : [],
"domain_blacklist_enabled" : false,
"domain_blacklist" : [],
"domain_allowlist" : [],
"domain_denylist_enabled" : false,
"domain_denylist" : [],
"created_at" : "2016-01-04T15:44:55.176Z",
"default_ci_config_path" : null,
"default_project_visibility" : "private",
@ -134,9 +134,9 @@ Example response:
"default_snippet_visibility": "private",
"default_group_visibility": "private",
"outbound_local_requests_whitelist": [],
"domain_whitelist": [],
"domain_blacklist_enabled" : false,
"domain_blacklist" : [],
"domain_allowlist": [],
"domain_denylist_enabled" : false,
"domain_denylist" : [],
"external_authorization_service_enabled": true,
"external_authorization_service_url": "https://authorize.me",
"external_authorization_service_default_label": "default",
@ -233,9 +233,9 @@ listed in the descriptions of the relevant settings.
| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). |
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |
| `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. |
| `domain_blacklist` | array of strings | no | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
| `domain_denylist_enabled` | boolean | no | (**If enabled, requires:** `domain_denylist`) Allows blocking sign-ups from emails from specific domains. |
| `domain_denylist` | array of strings | no | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
| `domain_allowlist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |

View File

@ -231,6 +231,16 @@ When enabled, any older deployments job are skipped when a new deployment starts
For more information, see [Deployment safety](../environments/deployment_safety.md).
## Retry outdated jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211339) in GitLab 13.6.
A deployment job can fail because a newer one has run. If you retry the failed deployment job, the
environment could be overwritten with older source code. If you click **Retry**, a modal warns you
about this and asks for confirmation.
For more information, see [Deployment safety](../environments/deployment_safety.md).
## Pipeline Badges
In the pipelines settings page you can find pipeline status and test coverage

View File

@ -201,8 +201,8 @@ estimated to keep migration timing to a minimum.
NOTE: **Note:**
Keep in mind that all runtimes should be measured against GitLab.com.
| Migration Type | Execution Time Recommended | Notes |
| Migration Type | Execution Time Recommended | Notes |
|----|----|---|
| Regular migrations on `db/migrate` | `3 minutes` | A valid exception are index creation as this can take a long time. |
| Post migrations on `db/post_migrate` | `10 minutes` | |
| Background migrations | --- | Since these are suitable for larger tables, it's not possible to set a precise timing guideline, however, any single query must stay below `1 second` execution time with cold caches. |
| Post migrations on `db/post_migrate` | `10 minutes` | |
| Background migrations | --- | Since these are suitable for larger tables, it's not possible to set a precise timing guideline, however, any single query must stay below `1 second` execution time with cold caches. |

View File

@ -1210,10 +1210,10 @@ When you take screenshots:
or concept in the image. If the image is of the GitLab interface, append the
GitLab version to the file name, based on the following format:
`image_name_vX_Y.png`. For example, for a screenshot taken from the pipelines
page of GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're adding an
page of GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're adding an
illustration that doesn't include parts of the user interface, add the release
number corresponding to the release the image was added to; for an MR added to
11.1's milestone, a valid name for an illustration is `devops_diagram_v11_1.png`.
11.1's milestone, a valid name for an illustration is `devops_diagram_v11_1.png`.
- Place images in a separate directory named `img/` in the same directory where
the `.md` document that you're working on is located.
- Consider using PNG images instead of JPEG.

View File

@ -162,7 +162,7 @@ repository and a pool.
### Pool existence
If GitLab thinks a pool repository exists (i.e. it exists according to
If GitLab thinks a pool repository exists (i.e. it exists according to
SQL), but it does not on the Gitaly server, then it will be created on
the fly by Gitaly.

View File

@ -100,7 +100,7 @@ To propose additions to the glossary please
### Inclusive language in French
<!-- vale gitlab.Spelling = NO -->
In French, the "écriture inclusive" is now over (see on [Legifrance](https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000036068906&categorieLien=id)).
In French, the "écriture inclusive" is now over (see on [Legifrance](https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000036068906&categorieLien=id)).
So, to include both genders, write “Utilisateurs et utilisatrices” instead of “Utilisateur·rice·s”.
When space is missing, the male gender should be used alone.
<!-- vale gitlab.Spelling = YES -->

View File

@ -360,28 +360,28 @@ The NDJSON tree will look like this:
```shell
tree
├── project
   ├── auto_devops.ndjson
   ├── boards.ndjson
   ├── ci_cd_settings.ndjson
   ├── ci_pipelines.ndjson
   ├── container_expiration_policy.ndjson
   ├── custom_attributes.ndjson
   ├── error_tracking_setting.ndjson
   ├── external_pull_requests.ndjson
   ├── issues.ndjson
   ├── labels.ndjson
   ├── merge_requests.ndjson
   ├── milestones.ndjson
   ├── pipeline_schedules.ndjson
   ├── project_badges.ndjson
   ├── project_feature.ndjson
   ├── project_members.ndjson
   ├── protected_branches.ndjson
   ├── protected_tags.ndjson
   ├── releases.ndjson
   ├── services.ndjson
   ├── snippets.ndjson
   └── triggers.ndjson
├── auto_devops.ndjson
├── boards.ndjson
├── ci_cd_settings.ndjson
├── ci_pipelines.ndjson
├── container_expiration_policy.ndjson
├── custom_attributes.ndjson
├── error_tracking_setting.ndjson
├── external_pull_requests.ndjson
├── issues.ndjson
├── labels.ndjson
├── merge_requests.ndjson
├── milestones.ndjson
├── pipeline_schedules.ndjson
├── project_badges.ndjson
├── project_feature.ndjson
├── project_members.ndjson
├── protected_branches.ndjson
├── protected_tags.ndjson
├── releases.ndjson
├── services.ndjson
├── snippets.ndjson
└── triggers.ndjson
└── project.json
```
@ -395,19 +395,19 @@ The NDJSON tree will look like this:
tree
└── groups
├── 4351
   ├── badges.ndjson
   ├── boards.ndjson
   ├── epics.ndjson
   ├── labels.ndjson
   ├── members.ndjson
   └── milestones.ndjson
├── badges.ndjson
├── boards.ndjson
├── epics.ndjson
├── labels.ndjson
├── members.ndjson
└── milestones.ndjson
├── 4352
   ├── badges.ndjson
   ├── boards.ndjson
   ├── epics.ndjson
   ├── labels.ndjson
   ├── members.ndjson
   └── milestones.ndjson
├── badges.ndjson
├── boards.ndjson
├── epics.ndjson
├── labels.ndjson
├── members.ndjson
└── milestones.ndjson
├── _all.ndjson
├── 4351.json
└── 4352.json

View File

@ -256,7 +256,7 @@ to `info`.
When executing command lines, scanners should use the `debug` level to log the command line and its output.
For instance, the [bundler-audit](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit) scanner
uses the `debug` level to log the command line `bundle audit check --quiet`,
uses the `debug` level to log the command line `bundle audit check --quiet`,
and what `bundle audit` writes to the standard output.
#### common logutil package
@ -298,7 +298,7 @@ The `vulnerabilities` field of the report is an array of vulnerability objects.
#### ID
The `id` field is the unique identifier of the vulnerability.
The `id` field is the unique identifier of the vulnerability.
It is used to reference a fixed vulnerability from a [remediation objects](#remediations).
We recommend that you generate a UUID and use it as the `id` field's value.

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