Skip to content
Snippets Groups Projects
Commit 4ce0bee9 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 02ab65d4
No related branches found
No related tags found
No related merge requests found
Showing
with 439 additions and 244 deletions
Loading
@@ -6,6 +6,7 @@ import { __ } from '~/locale';
Loading
@@ -6,6 +6,7 @@ import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue'; import DiffFile from './diff_file.vue';
Loading
@@ -145,6 +146,9 @@ export default {
Loading
@@ -145,6 +146,9 @@ export default {
}, },
watch: { watch: {
diffViewType() { diffViewType() {
if (this.needsReload() || this.needsFirstLoad()) {
this.refetchDiffData();
}
this.adjustView(); this.adjustView();
}, },
shouldShow() { shouldShow() {
Loading
@@ -224,6 +228,16 @@ export default {
Loading
@@ -224,6 +228,16 @@ export default {
{ timeout: 1000 }, { timeout: 1000 },
); );
}, },
needsReload() {
return (
this.glFeatures.singleMrDiffView &&
this.diffFiles.length &&
isSingleViewStyle(this.diffFiles[0])
);
},
needsFirstLoad() {
return this.glFeatures.singleMrDiffView && !this.diffFiles.length;
},
fetchData(toggleTree = true) { fetchData(toggleTree = true) {
if (this.glFeatures.diffsBatchLoad) { if (this.glFeatures.diffsBatchLoad) {
this.fetchDiffFilesMeta() this.fetchDiffFilesMeta()
Loading
@@ -237,6 +251,13 @@ export default {
Loading
@@ -237,6 +251,13 @@ export default {
}); });
   
this.fetchDiffFilesBatch() this.fetchDiffFilesBatch()
.then(() => {
// Guarantee the discussions are assigned after the batch finishes.
// Just watching the length of the discussions or the diff files
// isn't enough, because with split diff loading, neither will
// change when loading the other half of the diff files.
this.setDiscussions();
})
.then(() => this.startDiffRendering()) .then(() => this.startDiffRendering())
.catch(() => { .catch(() => {
createFlash(__('Something went wrong on our end. Please try again!')); createFlash(__('Something went wrong on our end. Please try again!'));
Loading
@@ -250,6 +271,7 @@ export default {
Loading
@@ -250,6 +271,7 @@ export default {
   
requestIdleCallback( requestIdleCallback(
() => { () => {
this.setDiscussions();
this.startRenderDiffsQueue(); this.startRenderDiffsQueue();
}, },
{ timeout: 1000 }, { timeout: 1000 },
Loading
Loading
Loading
@@ -4,6 +4,7 @@ import _ from 'underscore';
Loading
@@ -4,6 +4,7 @@ import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue'; import DiffContent from './diff_content.vue';
Loading
@@ -55,12 +56,7 @@ export default {
Loading
@@ -55,12 +56,7 @@ export default {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
}, },
hasDiff() { hasDiff() {
return ( return hasDiff(this.file);
(this.file.highlighted_diff_lines &&
this.file.parallel_diff_lines &&
this.file.parallel_diff_lines.length > 0) ||
!this.file.blob.readable_text
);
}, },
isFileTooLarge() { isFileTooLarge() {
return this.file.viewer.error === diffViewerErrors.too_large; return this.file.viewer.error === diffViewerErrors.too_large;
Loading
Loading
Loading
@@ -65,6 +65,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
Loading
@@ -65,6 +65,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
w: state.showWhitespace ? '0' : '1', w: state.showWhitespace ? '0' : '1',
}; };
   
if (state.useSingleDiffStyle) {
urlParams.view = state.diffViewType;
}
commit(types.SET_LOADING, true); commit(types.SET_LOADING, true);
   
worker.addEventListener('message', ({ data }) => { worker.addEventListener('message', ({ data }) => {
Loading
@@ -90,13 +94,22 @@ export const fetchDiffFiles = ({ state, commit }) => {
Loading
@@ -90,13 +94,22 @@ export const fetchDiffFiles = ({ state, commit }) => {
}; };
   
export const fetchDiffFilesBatch = ({ commit, state }) => { export const fetchDiffFilesBatch = ({ commit, state }) => {
const urlParams = {
per_page: DIFFS_PER_PAGE,
w: state.showWhitespace ? '0' : '1',
};
if (state.useSingleDiffStyle) {
urlParams.view = state.diffViewType;
}
commit(types.SET_BATCH_LOADING, true); commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true); commit(types.SET_RETRIEVING_BATCHES, true);
   
const getBatch = page => const getBatch = page =>
axios axios
.get(state.endpointBatch, { .get(state.endpointBatch, {
params: { page, per_page: DIFFS_PER_PAGE, w: state.showWhitespace ? '0' : '1' }, params: { ...urlParams, page },
}) })
.then(({ data: { pagination, diff_files } }) => { .then(({ data: { pagination, diff_files } }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files }); commit(types.SET_DIFF_DATA_BATCH, { diff_files });
Loading
@@ -150,7 +163,10 @@ export const assignDiscussionsToDiff = (
Loading
@@ -150,7 +163,10 @@ export const assignDiscussionsToDiff = (
{ commit, state, rootState }, { commit, state, rootState },
discussions = rootState.notes.discussions, discussions = rootState.notes.discussions,
) => { ) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); const diffPositionByLineCode = getDiffPositionByLineCode(
state.diffFiles,
state.useSingleDiffStyle,
);
const hash = getLocationHash(); const hash = getLocationHash();
   
discussions discussions
Loading
@@ -339,24 +355,23 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
Loading
@@ -339,24 +355,23 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
   
export const toggleFileDiscussionWrappers = ({ commit }, diff) => { export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff); const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
let linesWithDiscussions; const lineCodesWithDiscussions = new Set();
if (diff.highlighted_diff_lines) { const { parallel_diff_lines: parallelLines, highlighted_diff_lines: inlineLines } = diff;
linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length); const allLines = inlineLines.concat(
} parallelLines.map(line => line.left),
if (diff.parallel_diff_lines) { parallelLines.map(line => line.right),
linesWithDiscussions = diff.parallel_diff_lines.filter( );
line => const lineHasDiscussion = line => Boolean(line?.discussions.length);
(line.left && line.left.discussions.length) || const registerDiscussionLine = line => lineCodesWithDiscussions.add(line.line_code);
(line.right && line.right.discussions.length),
); allLines.filter(lineHasDiscussion).forEach(registerDiscussionLine);
}
if (lineCodesWithDiscussions.size) {
if (linesWithDiscussions.length) { Array.from(lineCodesWithDiscussions).forEach(lineCode => {
linesWithDiscussions.forEach(line => {
commit(types.TOGGLE_LINE_DISCUSSIONS, { commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash, fileHash: diff.file_hash,
lineCode: line.line_code,
expanded: !discussionWrappersExpanded, expanded: !discussionWrappersExpanded,
lineCode,
}); });
}); });
} }
Loading
Loading
Loading
@@ -45,26 +45,28 @@ export default {
Loading
@@ -45,26 +45,28 @@ export default {
}, },
   
[types.SET_DIFF_DATA](state, data) { [types.SET_DIFF_DATA](state, data) {
let files = state.diffFiles;
if ( if (
!( !(gon?.features?.diffsBatchLoad && window.location.search.indexOf('diff_id') === -1) &&
gon && data.diff_files
gon.features &&
gon.features.diffsBatchLoad &&
window.location.search.indexOf('diff_id') === -1
)
) { ) {
prepareDiffData(data); files = prepareDiffData(data, files);
} }
   
Object.assign(state, { Object.assign(state, {
...convertObjectPropsToCamelCase(data), ...convertObjectPropsToCamelCase(data),
diffFiles: files,
}); });
}, },
   
[types.SET_DIFF_DATA_BATCH](state, data) { [types.SET_DIFF_DATA_BATCH](state, data) {
prepareDiffData(data); const files = prepareDiffData(data, state.diffFiles);
   
state.diffFiles.push(...data.diff_files); Object.assign(state, {
...convertObjectPropsToCamelCase(data),
diffFiles: files,
});
}, },
   
[types.RENDER_FILE](state, file) { [types.RENDER_FILE](state, file) {
Loading
@@ -88,11 +90,11 @@ export default {
Loading
@@ -88,11 +90,11 @@ export default {
   
if (!diffFile) return; if (!diffFile) return;
   
if (diffFile.highlighted_diff_lines) { if (diffFile.highlighted_diff_lines.length) {
diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm; diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm;
} }
   
if (diffFile.parallel_diff_lines) { if (diffFile.parallel_diff_lines.length) {
const line = diffFile.parallel_diff_lines.find(l => { const line = diffFile.parallel_diff_lines.find(l => {
const { left, right } = l; const { left, right } = l;
   
Loading
@@ -153,13 +155,13 @@ export default {
Loading
@@ -153,13 +155,13 @@ export default {
}, },
   
[types.EXPAND_ALL_FILES](state) { [types.EXPAND_ALL_FILES](state) {
state.diffFiles = state.diffFiles.map(file => ({ state.diffFiles.forEach(file => {
...file, Object.assign(file, {
viewer: { viewer: Object.assign(file.viewer, {
...file.viewer, collapsed: false,
collapsed: false, }),
}, });
})); });
}, },
   
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) { [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
Loading
@@ -197,29 +199,29 @@ export default {
Loading
@@ -197,29 +199,29 @@ export default {
}; };
}; };
   
state.diffFiles = state.diffFiles.map(diffFile => { state.diffFiles.forEach(file => {
if (diffFile.file_hash === fileHash) { if (file.file_hash === fileHash) {
const file = { ...diffFile }; if (file.highlighted_diff_lines.length) {
file.highlighted_diff_lines.forEach(line => {
if (file.highlighted_diff_lines) { Object.assign(
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => line,
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line), setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
); );
});
} }
   
if (file.parallel_diff_lines) { if (file.parallel_diff_lines.length) {
file.parallel_diff_lines = file.parallel_diff_lines.map(line => { file.parallel_diff_lines.forEach(line => {
const left = line.left && lineCheck(line.left); const left = line.left && lineCheck(line.left);
const right = line.right && lineCheck(line.right); const right = line.right && lineCheck(line.right);
   
if (left || right) { if (left || right) {
return { Object.assign(line, {
...line,
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null, left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left)) ? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null, : null,
}; });
} }
   
return line; return line;
Loading
@@ -227,15 +229,15 @@ export default {
Loading
@@ -227,15 +229,15 @@ export default {
} }
   
if (!file.parallel_diff_lines || !file.highlighted_diff_lines) { if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
file.discussions = (file.discussions || []) const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id) .filter(d => d.id !== discussion.id)
.concat(discussion); .concat(discussion);
}
   
return file; Object.assign(file, {
discussions: newDiscussions,
});
}
} }
return diffFile;
}); });
}, },
   
Loading
@@ -259,9 +261,9 @@ export default {
Loading
@@ -259,9 +261,9 @@ export default {
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) { [types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
   
updateLineInFile(selectedFile, lineCode, line => updateLineInFile(selectedFile, lineCode, line => {
Object.assign(line, { discussionsExpanded: expanded }), Object.assign(line, { discussionsExpanded: expanded });
); });
}, },
   
[types.TOGGLE_FOLDER_OPEN](state, path) { [types.TOGGLE_FOLDER_OPEN](state, path) {
Loading
Loading
Loading
@@ -185,6 +185,7 @@ export function addContextLines(options) {
Loading
@@ -185,6 +185,7 @@ export function addContextLines(options) {
* Trims the first char of the `richText` property when it's either a space or a diff symbol. * Trims the first char of the `richText` property when it's either a space or a diff symbol.
* @param {Object} line * @param {Object} line
* @returns {Object} * @returns {Object}
* @deprecated
*/ */
export function trimFirstCharOfLineContent(line = {}) { export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
Loading
@@ -212,79 +213,171 @@ function getLineCode({ left, right }, index) {
Loading
@@ -212,79 +213,171 @@ function getLineCode({ left, right }, index) {
return index; return index;
} }
   
// This prepares and optimizes the incoming diff data from the server function diffFileUniqueId(file) {
// by setting up incremental rendering and removing unneeded data return `${file.content_sha}-${file.file_hash}`;
export function prepareDiffData(diffData) { }
const filesLength = diffData.diff_files.length;
let showingLines = 0; function combineDiffFilesWithPriorFiles(files, prior = []) {
for (let i = 0; i < filesLength; i += 1) { files.forEach(file => {
const file = diffData.diff_files[i]; const id = diffFileUniqueId(file);
const oldMatch = prior.find(oldFile => diffFileUniqueId(oldFile) === id);
if (file.parallel_diff_lines) {
const linesLength = file.parallel_diff_lines.length; if (oldMatch) {
for (let u = 0; u < linesLength; u += 1) { const missingInline = !file.highlighted_diff_lines;
const line = file.parallel_diff_lines[u]; const missingParallel = !file.parallel_diff_lines;
line.line_code = getLineCode(line, u); if (missingInline) {
if (line.left) { Object.assign(file, {
line.left = trimFirstCharOfLineContent(line.left); highlighted_diff_lines: oldMatch.highlighted_diff_lines,
line.left.discussions = []; });
line.left.hasForm = false;
}
if (line.right) {
line.right = trimFirstCharOfLineContent(line.right);
line.right.discussions = [];
line.right.hasForm = false;
}
} }
}
   
if (file.highlighted_diff_lines) { if (missingParallel) {
const linesLength = file.highlighted_diff_lines.length; Object.assign(file, {
for (let u = 0; u < linesLength; u += 1) { parallel_diff_lines: oldMatch.parallel_diff_lines,
const line = file.highlighted_diff_lines[u];
Object.assign(line, {
...trimFirstCharOfLineContent(line),
discussions: [],
hasForm: false,
}); });
} }
showingLines += file.parallel_diff_lines.length; }
});
return files;
}
function ensureBasicDiffFileLines(file) {
const missingInline = !file.highlighted_diff_lines;
const missingParallel = !file.parallel_diff_lines;
Object.assign(file, {
highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
parallel_diff_lines: missingParallel ? [] : file.parallel_diff_lines,
});
return file;
}
function cleanRichText(text) {
return text ? text.replace(/^[+ -]/, '') : undefined;
}
function prepareLine(line) {
return Object.assign(line, {
rich_text: cleanRichText(line.rich_text),
discussionsExpanded: true,
discussions: [],
hasForm: false,
text: undefined,
});
}
function prepareDiffFileLines(file) {
const inlineLines = file.highlighted_diff_lines;
const parallelLines = file.parallel_diff_lines;
let parallelLinesCount = 0;
inlineLines.forEach(prepareLine);
parallelLines.forEach((line, index) => {
Object.assign(line, { line_code: getLineCode(line, index) });
if (line.left) {
parallelLinesCount += 1;
prepareLine(line.left);
} }
   
const name = (file.viewer && file.viewer.name) || diffViewerModes.text; if (line.right) {
parallelLinesCount += 1;
prepareLine(line.right);
}
   
Object.assign(file, { Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, inlineLinesCount: inlineLines.length,
collapsed: name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED, parallelLinesCount,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
renderingLines: false,
}); });
} });
return file;
} }
   
export function getDiffPositionByLineCode(diffFiles) { function getVisibleDiffLines(file) {
return diffFiles.reduce((acc, diffFile) => { return Math.max(file.inlineLinesCount, file.parallelLinesCount);
// We can only use highlightedDiffLines to create the map of diff lines because }
// highlightedDiffLines will also include every parallel diff line in it.
if (diffFile.highlighted_diff_lines) { function finalizeDiffFile(file) {
const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
const lines = getVisibleDiffLines(file);
Object.assign(file, {
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
renderingLines: false,
});
return file;
}
export function prepareDiffData(diffData, priorFiles) {
return combineDiffFilesWithPriorFiles(diffData.diff_files, priorFiles)
.map(ensureBasicDiffFileLines)
.map(prepareDiffFileLines)
.map(finalizeDiffFile);
}
export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
let lines = [];
const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0);
if (!useSingleDiffStyle || hasInlineDiffs) {
// In either of these cases, we can use `highlighted_diff_lines` because
// that will include all of the parallel diff lines, too
lines = diffFiles.reduce((acc, diffFile) => {
diffFile.highlighted_diff_lines.forEach(line => { diffFile.highlighted_diff_lines.forEach(line => {
if (line.line_code) { acc.push({ file: diffFile, line });
acc[line.line_code] = { });
base_sha: diffFile.diff_refs.base_sha,
head_sha: diffFile.diff_refs.head_sha, return acc;
start_sha: diffFile.diff_refs.start_sha, }, []);
new_path: diffFile.new_path, } else {
old_path: diffFile.old_path, // If we're in single diff view mode and the inline lines haven't been
old_line: line.old_line, // loaded yet, we need to parse the parallel lines
new_line: line.new_line,
line_code: line.line_code, lines = diffFiles.reduce((acc, diffFile) => {
position_type: 'text', diffFile.parallel_diff_lines.forEach(pair => {
}; // It's possible for a parallel line to have an opposite line that doesn't exist
// For example: *deleted* lines will have `null` right lines, while
// *added* lines will have `null` left lines.
// So we have to check each line before we push it onto the array so we're not
// pushing null line diffs
if (pair.left) {
acc.push({ file: diffFile, line: pair.left });
}
if (pair.right) {
acc.push({ file: diffFile, line: pair.right });
} }
}); });
return acc;
}, []);
}
return lines.reduce((acc, { file, line }) => {
if (line.line_code) {
acc[line.line_code] = {
base_sha: file.diff_refs.base_sha,
head_sha: file.diff_refs.head_sha,
start_sha: file.diff_refs.start_sha,
new_path: file.new_path,
old_path: file.old_path,
old_line: line.old_line,
new_line: line.new_line,
line_code: line.line_code,
position_type: 'text',
};
} }
   
return acc; return acc;
Loading
@@ -462,47 +555,47 @@ export const convertExpandLines = ({
Loading
@@ -462,47 +555,47 @@ export const convertExpandLines = ({
   
export const idleCallback = cb => requestIdleCallback(cb); export const idleCallback = cb => requestIdleCallback(cb);
   
export const updateLineInFile = (selectedFile, lineCode, updateFn) => { function getLinesFromFileByLineCode(file, lineCode) {
if (selectedFile.parallel_diff_lines) { const parallelLines = file.parallel_diff_lines;
const targetLine = selectedFile.parallel_diff_lines.find( const inlineLines = file.highlighted_diff_lines;
line => const matchesCode = line => line.line_code === lineCode;
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
   
updateFn(targetLine[side]); return [
} ...parallelLines.reduce((acc, line) => {
} if (line.left) {
if (selectedFile.highlighted_diff_lines) { acc.push(line.left);
const targetInlineLine = selectedFile.highlighted_diff_lines.find( }
line => line.line_code === lineCode,
);
   
if (targetInlineLine) { if (line.right) {
updateFn(targetInlineLine); acc.push(line.right);
} }
}
return acc;
}, []),
...inlineLines,
].filter(matchesCode);
}
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
getLinesFromFileByLineCode(selectedFile, lineCode).forEach(updateFn);
}; };
   
export const allDiscussionWrappersExpanded = diff => { export const allDiscussionWrappersExpanded = diff => {
const discussionsExpandedArray = []; let discussionsExpanded = true;
if (diff.parallel_diff_lines) { const changeExpandedResult = line => {
diff.parallel_diff_lines.forEach(line => { if (line && line.discussions.length) {
if (line.left && line.left.discussions.length) { discussionsExpanded = discussionsExpanded && line.discussionsExpanded;
discussionsExpandedArray.push(line.left.discussionsExpanded); }
} };
if (line.right && line.right.discussions.length) {
discussionsExpandedArray.push(line.right.discussionsExpanded); diff.parallel_diff_lines.forEach(line => {
} changeExpandedResult(line.left);
}); changeExpandedResult(line.right);
} else if (diff.highlighted_diff_lines) { });
diff.highlighted_diff_lines.forEach(line => {
if (line.discussions.length) { diff.highlighted_diff_lines.forEach(line => {
discussionsExpandedArray.push(line.discussionsExpanded); changeExpandedResult(line);
} });
});
} return discussionsExpanded;
return discussionsExpandedArray.every(el => el);
}; };
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import createFlash from '~/flash';
import { GlFormInput, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; import { GlFormInput, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { __, sprintf, n__ } from '~/locale'; import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
Loading
@@ -11,6 +12,8 @@ import TrackEventDirective from '~/vue_shared/directives/track_event';
Loading
@@ -11,6 +12,8 @@ import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils'; import { trackClickErrorLinkToSentryOptions } from '../utils';
   
import query from '../queries/details.query.graphql';
export default { export default {
components: { components: {
LoadingButton, LoadingButton,
Loading
@@ -27,6 +30,14 @@ export default {
Loading
@@ -27,6 +30,14 @@ export default {
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
issueId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
issueDetailsPath: { issueDetailsPath: {
type: String, type: String,
required: true, required: true,
Loading
@@ -44,8 +55,28 @@ export default {
Loading
@@ -44,8 +55,28 @@ export default {
required: true, required: true,
}, },
}, },
apollo: {
GQLerror: {
query,
variables() {
return {
fullPath: this.projectPath,
errorId: `gid://gitlab/Gitlab::ErrorTracking::DetailedError/${this.issueId}`,
};
},
pollInterval: 2000,
update: data => data.project.sentryDetailedError,
error: () => createFlash(__('Failed to load error details from Sentry.')),
result(res) {
if (res.data.project?.sentryDetailedError) {
this.$apollo.queries.GQLerror.stopPolling();
}
},
},
},
data() { data() {
return { return {
GQLerror: null,
issueCreationInProgress: false, issueCreationInProgress: false,
}; };
}, },
Loading
@@ -56,26 +87,28 @@ export default {
Loading
@@ -56,26 +87,28 @@ export default {
return sprintf( return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'), __('Reported %{timeAgo} by %{reportedBy}'),
{ {
reportedBy: `<strong>${this.error.culprit}</strong>`, reportedBy: `<strong>${this.GQLerror.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received), timeAgo: this.timeFormatted(this.stacktraceData.date_received),
}, },
false, false,
); );
}, },
firstReleaseLink() { firstReleaseLink() {
return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`; return `${this.error.external_base_url}/releases/${this.GQLerror.firstReleaseShortVersion}`;
}, },
lastReleaseLink() { lastReleaseLink() {
return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`; return `${this.error.external_base_url}releases/${this.GQLerror.lastReleaseShortVersion}`;
}, },
showDetails() { showDetails() {
return Boolean(!this.loading && this.error && this.error.id); return Boolean(
!this.loading && !this.$apollo.queries.GQLerror.loading && this.error && this.GQLerror,
);
}, },
showStacktrace() { showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length); return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
}, },
issueTitle() { issueTitle() {
return this.error.title; return this.GQLerror.title;
}, },
issueDescription() { issueDescription() {
return sprintf( return sprintf(
Loading
@@ -84,13 +117,13 @@ export default {
Loading
@@ -84,13 +117,13 @@ export default {
), ),
{ {
description: '# Error Details:\n', description: '# Error Details:\n',
errorUrl: `${this.error.external_url}\n`, errorUrl: `${this.GQLerror.externalUrl}\n`,
firstSeen: `\n${this.error.first_seen}\n`, firstSeen: `\n${this.GQLerror.firstSeen}\n`,
lastSeen: `${this.error.last_seen}\n`, lastSeen: `${this.GQLerror.lastSeen}\n`,
countLabel: n__('- Event', '- Events', this.error.count), countLabel: n__('- Event', '- Events', this.GQLerror.count),
count: `${this.error.count}\n`, count: `${this.GQLerror.count}\n`,
userCountLabel: n__('- User', '- Users', this.error.user_count), userCountLabel: n__('- User', '- Users', this.GQLerror.userCount),
userCount: `${this.error.user_count}\n`, userCount: `${this.GQLerror.userCount}\n`,
}, },
false, false,
); );
Loading
@@ -119,7 +152,7 @@ export default {
Loading
@@ -119,7 +152,7 @@ export default {
   
<template> <template>
<div> <div>
<div v-if="loading" class="py-3"> <div v-if="$apollo.queries.GQLerror.loading || loading" class="py-3">
<gl-loading-icon :size="3" /> <gl-loading-icon :size="3" />
</div> </div>
<div v-else-if="showDetails" class="error-details"> <div v-else-if="showDetails" class="error-details">
Loading
@@ -129,7 +162,7 @@ export default {
Loading
@@ -129,7 +162,7 @@ export default {
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" /> <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" /> <input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input <gl-form-input
:value="error.id" :value="GQLerror.id"
class="hidden" class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]" name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/> />
Loading
@@ -145,16 +178,16 @@ export default {
Loading
@@ -145,16 +178,16 @@ export default {
</form> </form>
</div> </div>
<div> <div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top"> <tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ error.title }}</h2> <h2 class="text-truncate">{{ GQLerror.title }}</h2>
</tooltip-on-truncate> </tooltip-on-truncate>
<template v-if="error.tags"> <template v-if="error.tags">
<gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2">{{ <gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2"
errorLevel >{{ errorLevel }}
}}</gl-badge> </gl-badge>
<gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill">{{ <gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill"
error.tags.logger >{{ error.tags.logger }}
}}</gl-badge> </gl-badge>
</template> </template>
   
<h3>{{ __('Error details') }}</h3> <h3>{{ __('Error details') }}</h3>
Loading
@@ -168,35 +201,35 @@ export default {
Loading
@@ -168,35 +201,35 @@ export default {
<li> <li>
<span class="bold">{{ __('Sentry event') }}:</span> <span class="bold">{{ __('Sentry event') }}:</span>
<gl-link <gl-link
v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)" v-track-event="trackClickErrorLinkToSentryOptions(GQLerror.externalUrl)"
:href="error.external_url" :href="GQLerror.externalUrl"
target="_blank" target="_blank"
> >
<span class="text-truncate">{{ error.external_url }}</span> <span class="text-truncate">{{ GQLerror.externalUrl }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" /> <icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link> </gl-link>
</li> </li>
<li v-if="error.first_release_short_version"> <li v-if="GQLerror.firstReleaseShortVersion">
<span class="bold">{{ __('First seen') }}:</span> <span class="bold">{{ __('First seen') }}:</span>
{{ formatDate(error.first_seen) }} {{ formatDate(GQLerror.firstSeen) }}
<gl-link :href="firstReleaseLink" target="_blank"> <gl-link :href="firstReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.first_release_short_version }}</span> <span>{{ __('Release') }}: {{ GQLerror.firstReleaseShortVersion }}</span>
</gl-link> </gl-link>
</li> </li>
<li v-if="error.last_release_short_version"> <li v-if="GQLerror.lastReleaseShortVersion">
<span class="bold">{{ __('Last seen') }}:</span> <span class="bold">{{ __('Last seen') }}:</span>
{{ formatDate(error.last_seen) }} {{ formatDate(GQLerror.lastSeen) }}
<gl-link :href="lastReleaseLink" target="_blank"> <gl-link :href="lastReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.last_release_short_version }}</span> <span>{{ __('Release') }}: {{ GQLerror.lastReleaseShortVersion }}</span>
</gl-link> </gl-link>
</li> </li>
<li> <li>
<span class="bold">{{ __('Events') }}:</span> <span class="bold">{{ __('Events') }}:</span>
<span>{{ error.count }}</span> <span>{{ GQLerror.count }}</span>
</li> </li>
<li> <li>
<span class="bold">{{ __('Users') }}:</span> <span class="bold">{{ __('Users') }}:</span>
<span>{{ error.user_count }}</span> <span>{{ GQLerror.userCount }}</span>
</li> </li>
</ul> </ul>
   
Loading
Loading
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import store from './store'; import store from './store';
import ErrorDetails from './components/error_details.vue'; import ErrorDetails from './components/error_details.vue';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
   
Vue.use(VueApollo);
export default () => { export default () => {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: '#js-error_details', el: '#js-error_details',
apolloProvider,
components: { components: {
ErrorDetails, ErrorDetails,
}, },
store, store,
render(createElement) { render(createElement) {
const domEl = document.querySelector(this.$options.el); const domEl = document.querySelector(this.$options.el);
const { issueDetailsPath, issueStackTracePath, projectIssuesPath } = domEl.dataset; const {
issueId,
projectPath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
} = domEl.dataset;
   
return createElement('error-details', { return createElement('error-details', {
props: { props: {
issueId,
projectPath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
projectIssuesPath, projectIssuesPath,
Loading
Loading
query errorDetails($fullPath: ID!, $errorId: ID!) {
project(fullPath: $fullPath) {
sentryDetailedError(id: $errorId) {
id
sentryId
title
userCount
count
firstSeen
lastSeen
message
culprit
externalUrl
firstReleaseShortVersion
lastReleaseShortVersion
}
}
}
export function hasInlineLines(diffFile) {
return diffFile?.highlighted_diff_lines?.length > 0; /* eslint-disable-line camelcase */
}
export function hasParallelLines(diffFile) {
return diffFile?.parallel_diff_lines?.length > 0; /* eslint-disable-line camelcase */
}
export function isSingleViewStyle(diffFile) {
return !hasParallelLines(diffFile) || !hasInlineLines(diffFile);
}
export function hasDiff(diffFile) {
return (
hasInlineLines(diffFile) ||
hasParallelLines(diffFile) ||
!diffFile?.blob?.readable_text /* eslint-disable-line camelcase */
);
}
Loading
@@ -39,7 +39,7 @@ export const requestMetricsDashboard = ({ commit }) => {
Loading
@@ -39,7 +39,7 @@ export const requestMetricsDashboard = ({ commit }) => {
}; };
export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => { export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
commit(types.SET_ALL_DASHBOARDS, response.all_dashboards); commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups); commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard);
return dispatch('fetchPrometheusMetrics', params); return dispatch('fetchPrometheusMetrics', params);
}; };
export const receiveMetricsDashboardFailure = ({ commit }, error) => { export const receiveMetricsDashboardFailure = ({ commit }, error) => {
Loading
Loading
Loading
@@ -84,23 +84,26 @@ export default {
Loading
@@ -84,23 +84,26 @@ export default {
state.emptyState = 'loading'; state.emptyState = 'loading';
state.showEmptyState = true; state.showEmptyState = true;
}, },
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) { [types.RECEIVE_METRICS_DATA_SUCCESS](state, dashboard) {
state.dashboard.panel_groups = groupData.map((group, i) => { state.dashboard = {
const key = `${slugify(group.group || 'default')}-${i}`; ...dashboard,
let { panels = [] } = group; panel_groups: dashboard.panel_groups.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
// each panel has metric information that needs to be normalized let { panels = [] } = group;
panels = panels.map(panel => ({
...panel, // each panel has metric information that needs to be normalized
metrics: normalizePanelMetrics(panel.metrics, panel.y_label), panels = panels.map(panel => ({
})); ...panel,
metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
return { }));
...group,
panels, return {
key, ...group,
}; panels,
}); key,
};
}),
};
   
if (!state.dashboard.panel_groups.length) { if (!state.dashboard.panel_groups.length) {
state.emptyState = 'noData'; state.emptyState = 'noData';
Loading
Loading
Loading
@@ -18,9 +18,11 @@ module Projects::ErrorTrackingHelper
Loading
@@ -18,9 +18,11 @@ module Projects::ErrorTrackingHelper
opts = [project, issue_id, { format: :json }] opts = [project, issue_id, { format: :json }]
   
{ {
'project-issues-path' => project_issues_path(project), 'issue-id' => issue_id,
'project-path' => project.full_path,
'issue-details-path' => details_project_error_tracking_index_path(*opts), 'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-update-path' => update_project_error_tracking_index_path(*opts), 'issue-update-path' => update_project_error_tracking_index_path(*opts),
'project-issues-path' => project_issues_path(project),
'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts) 'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts)
} }
end end
Loading
Loading
Loading
@@ -11,7 +11,7 @@ module Ci
Loading
@@ -11,7 +11,7 @@ module Ci
has_many :trigger_requests has_many :trigger_requests
   
validates :token, presence: true, uniqueness: true validates :token, presence: true, uniqueness: true
validates :owner, presence: true, unless: :supports_legacy_tokens? validates :owner, presence: true
   
before_validation :set_default_values before_validation :set_default_values
   
Loading
@@ -31,17 +31,8 @@ module Ci
Loading
@@ -31,17 +31,8 @@ module Ci
token[0...4] if token.present? token[0...4] if token.present?
end end
   
def legacy?
self.owner_id.blank?
end
def supports_legacy_tokens?
Feature.enabled?(:use_legacy_pipeline_triggers, project)
end
def can_access_project? def can_access_project?
supports_legacy_tokens? && legacy? || Ability.allowed?(self.owner, :create_build, project)
Ability.allowed?(self.owner, :create_build, project)
end end
end end
end end
Loading
Loading
Loading
@@ -5,13 +5,12 @@ module Ci
Loading
@@ -5,13 +5,12 @@ module Ci
delegate { @subject.project } delegate { @subject.project }
   
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
condition(:legacy) { @subject.supports_legacy_tokens? && @subject.legacy? }
   
with_score 0 with_score 0
condition(:is_owner) { @user && @subject.owner_id == @user.id } condition(:is_owner) { @user && @subject.owner_id == @user.id }
   
rule { ~can?(:admin_build) }.prevent :admin_trigger rule { ~can?(:admin_build) }.prevent :admin_trigger
rule { legacy | is_owner }.enable :admin_trigger rule { is_owner }.enable :admin_trigger
   
rule { can?(:admin_build) }.enable :manage_trigger rule { can?(:admin_build) }.enable :manage_trigger
end end
Loading
Loading
Loading
@@ -4,7 +4,7 @@
Loading
@@ -4,7 +4,7 @@
= link_to s_('Nav|Home'), root_path = link_to s_('Nav|Home'), root_path
%li %li
- if current_user - if current_user
= link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path = link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path, method: :post
- else - else
= link_to s_('Nav|Sign In / Register'), new_session_path(:user, redirect_to_referer: 'yes') = link_to s_('Nav|Sign In / Register'), new_session_path(:user, redirect_to_referer: 'yes')
%li %li
Loading
Loading
Loading
@@ -47,4 +47,4 @@
Loading
@@ -47,4 +47,4 @@
- if current_user_menu?(:sign_out) - if current_user_menu?(:sign_out)
%li.divider %li.divider
%li %li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link", data: { qa_selector: 'sign_out_link' } = link_to _("Sign out"), destroy_user_session_path, method: :post, class: "sign-out-link", data: { qa_selector: 'sign_out_link' }
Loading
@@ -55,7 +55,7 @@
Loading
@@ -55,7 +55,7 @@
- if Feature.enabled?(:user_mode_in_session) - if Feature.enabled?(:user_mode_in_session)
- if header_link?(:admin_mode) - if header_link?(:admin_mode)
= nav_link(controller: 'admin/sessions') do = nav_link(controller: 'admin/sessions') do
= link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do = link_to destroy_admin_session_path, method: :post, class: 'd-lg-none lock-open-icon' do
= _('Leave Admin Mode') = _('Leave Admin Mode')
- elsif current_user.admin? - elsif current_user.admin?
= nav_link(controller: 'admin/sessions') do = nav_link(controller: 'admin/sessions') do
Loading
Loading
- if Feature.enabled?(:use_legacy_pipeline_triggers, @project)
%p.append-bottom-default
Triggers with the
%span.badge.badge-primary legacy
label do not have an associated user and only have access to the current project.
%br
= succeed '.' do
Learn more in the
= link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
.row.prepend-top-default.append-bottom-default.triggers-container .row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-12 .col-lg-12
= render "projects/triggers/content"
.card .card
.card-header .card-header
Manage your project's triggers Manage your project's triggers
Loading
Loading
Loading
@@ -7,12 +7,7 @@
Loading
@@ -7,12 +7,7 @@
%span= trigger.short_token %span= trigger.short_token
   
.label-container .label-container
- if trigger.legacy? - unless trigger.can_access_project?
- if trigger.supports_legacy_tokens?
%span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
- else
%span.badge.badge-danger.has-tooltip{ title: "Trigger is invalid due to being a legacy trigger. We recommend replacing it with a new trigger" } invalid
- elsif !trigger.can_access_project?
%span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid %span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
   
%td %td
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment