Commit 4ce0bee9 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/gitlab@master

parent 02ab65d4
Pipeline #109228920 passed with stages
in 93 minutes and 54 seconds
......@@ -6,6 +6,7 @@ import { __ } from '~/locale';
import createFlash from '~/flash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
......@@ -145,6 +146,9 @@ export default {
},
watch: {
diffViewType() {
if (this.needsReload() || this.needsFirstLoad()) {
this.refetchDiffData();
}
this.adjustView();
},
shouldShow() {
......@@ -224,6 +228,16 @@ export default {
{ 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) {
if (this.glFeatures.diffsBatchLoad) {
this.fetchDiffFilesMeta()
......@@ -237,6 +251,13 @@ export default {
});
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())
.catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
......@@ -250,6 +271,7 @@ export default {
requestIdleCallback(
() => {
this.setDiscussions();
this.startRenderDiffsQueue();
},
{ timeout: 1000 },
......
......@@ -4,6 +4,7 @@ import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
......@@ -55,12 +56,7 @@ export default {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
hasDiff() {
return (
(this.file.highlighted_diff_lines &&
this.file.parallel_diff_lines &&
this.file.parallel_diff_lines.length > 0) ||
!this.file.blob.readable_text
);
return hasDiff(this.file);
},
isFileTooLarge() {
return this.file.viewer.error === diffViewerErrors.too_large;
......
......@@ -65,6 +65,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
w: state.showWhitespace ? '0' : '1',
};
if (state.useSingleDiffStyle) {
urlParams.view = state.diffViewType;
}
commit(types.SET_LOADING, true);
worker.addEventListener('message', ({ data }) => {
......@@ -90,13 +94,22 @@ export const fetchDiffFiles = ({ state, commit }) => {
};
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_RETRIEVING_BATCHES, true);
const getBatch = page =>
axios
.get(state.endpointBatch, {
params: { page, per_page: DIFFS_PER_PAGE, w: state.showWhitespace ? '0' : '1' },
params: { ...urlParams, page },
})
.then(({ data: { pagination, diff_files } }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
......@@ -150,7 +163,10 @@ export const assignDiscussionsToDiff = (
{ commit, state, rootState },
discussions = rootState.notes.discussions,
) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const diffPositionByLineCode = getDiffPositionByLineCode(
state.diffFiles,
state.useSingleDiffStyle,
);
const hash = getLocationHash();
discussions
......@@ -339,24 +355,23 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
let linesWithDiscussions;
if (diff.highlighted_diff_lines) {
linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
}
if (diff.parallel_diff_lines) {
linesWithDiscussions = diff.parallel_diff_lines.filter(
line =>
(line.left && line.left.discussions.length) ||
(line.right && line.right.discussions.length),
);
}
if (linesWithDiscussions.length) {
linesWithDiscussions.forEach(line => {
const lineCodesWithDiscussions = new Set();
const { parallel_diff_lines: parallelLines, highlighted_diff_lines: inlineLines } = diff;
const allLines = inlineLines.concat(
parallelLines.map(line => line.left),
parallelLines.map(line => line.right),
);
const lineHasDiscussion = line => Boolean(line?.discussions.length);
const registerDiscussionLine = line => lineCodesWithDiscussions.add(line.line_code);
allLines.filter(lineHasDiscussion).forEach(registerDiscussionLine);
if (lineCodesWithDiscussions.size) {
Array.from(lineCodesWithDiscussions).forEach(lineCode => {
commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash,
lineCode: line.line_code,
expanded: !discussionWrappersExpanded,
lineCode,
});
});
}
......
......@@ -45,26 +45,28 @@ export default {
},
[types.SET_DIFF_DATA](state, data) {
let files = state.diffFiles;
if (
!(
gon &&
gon.features &&
gon.features.diffsBatchLoad &&
window.location.search.indexOf('diff_id') === -1
)
!(gon?.features?.diffsBatchLoad && window.location.search.indexOf('diff_id') === -1) &&
data.diff_files
) {
prepareDiffData(data);
files = prepareDiffData(data, files);
}
Object.assign(state, {
...convertObjectPropsToCamelCase(data),
diffFiles: files,
});
},
[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) {
......@@ -88,11 +90,11 @@ export default {
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;
}
if (diffFile.parallel_diff_lines) {
if (diffFile.parallel_diff_lines.length) {
const line = diffFile.parallel_diff_lines.find(l => {
const { left, right } = l;
......@@ -153,13 +155,13 @@ export default {
},
[types.EXPAND_ALL_FILES](state) {
state.diffFiles = state.diffFiles.map(file => ({
...file,
viewer: {
...file.viewer,
collapsed: false,
},
}));
state.diffFiles.forEach(file => {
Object.assign(file, {
viewer: Object.assign(file.viewer, {
collapsed: false,
}),
});
});
},
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
......@@ -197,29 +199,29 @@ export default {
};
};
state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
);
state.diffFiles.forEach(file => {
if (file.file_hash === fileHash) {
if (file.highlighted_diff_lines.length) {
file.highlighted_diff_lines.forEach(line => {
Object.assign(
line,
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
);
});
}
if (file.parallel_diff_lines) {
file.parallel_diff_lines = file.parallel_diff_lines.map(line => {
if (file.parallel_diff_lines.length) {
file.parallel_diff_lines.forEach(line => {
const left = line.left && lineCheck(line.left);
const right = line.right && lineCheck(line.right);
if (left || right) {
return {
...line,
Object.assign(line, {
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null,
};
});
}
return line;
......@@ -227,15 +229,15 @@ export default {
}
if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
file.discussions = (file.discussions || [])
const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id)
.concat(discussion);
}
return file;
Object.assign(file, {
discussions: newDiscussions,
});
}
}
return diffFile;
});
},
......@@ -259,9 +261,9 @@ export default {
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, { discussionsExpanded: expanded }),
);
updateLineInFile(selectedFile, lineCode, line => {
Object.assign(line, { discussionsExpanded: expanded });
});
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
......
This diff is collapsed.
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
import createFlash from '~/flash';
import { GlFormInput, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
......@@ -11,6 +12,8 @@ import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils';
import query from '../queries/details.query.graphql';
export default {
components: {
LoadingButton,
......@@ -27,6 +30,14 @@ export default {
},
mixins: [timeagoMixin],
props: {
issueId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
issueDetailsPath: {
type: String,
required: true,
......@@ -44,8 +55,28 @@ export default {
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() {
return {
GQLerror: null,
issueCreationInProgress: false,
};
},
......@@ -56,26 +87,28 @@ export default {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
reportedBy: `<strong>${this.GQLerror.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
},
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() {
return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`;
return `${this.error.external_base_url}releases/${this.GQLerror.lastReleaseShortVersion}`;
},
showDetails() {
return Boolean(!this.loading && this.error && this.error.id);
return Boolean(
!this.loading && !this.$apollo.queries.GQLerror.loading && this.error && this.GQLerror,
);
},
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
issueTitle() {
return this.error.title;
return this.GQLerror.title;
},
issueDescription() {
return sprintf(
......@@ -84,13 +117,13 @@ export default {
),
{
description: '# Error Details:\n',
errorUrl: `${this.error.external_url}\n`,
firstSeen: `\n${this.error.first_seen}\n`,
lastSeen: `${this.error.last_seen}\n`,
countLabel: n__('- Event', '- Events', this.error.count),
count: `${this.error.count}\n`,
userCountLabel: n__('- User', '- Users', this.error.user_count),
userCount: `${this.error.user_count}\n`,
errorUrl: `${this.GQLerror.externalUrl}\n`,
firstSeen: `\n${this.GQLerror.firstSeen}\n`,
lastSeen: `${this.GQLerror.lastSeen}\n`,
countLabel: n__('- Event', '- Events', this.GQLerror.count),
count: `${this.GQLerror.count}\n`,
userCountLabel: n__('- User', '- Users', this.GQLerror.userCount),
userCount: `${this.GQLerror.userCount}\n`,
},
false,
);
......@@ -119,7 +152,7 @@ export default {
<template>
<div>
<div v-if="loading" class="py-3">
<div v-if="$apollo.queries.GQLerror.loading || loading" class="py-3">
<gl-loading-icon :size="3" />
</div>
<div v-else-if="showDetails" class="error-details">
......@@ -129,7 +162,7 @@ export default {
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input
:value="error.id"
:value="GQLerror.id"
class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/>
......@@ -145,16 +178,16 @@ export default {
</form>
</div>
<div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ error.title }}</h2>
<tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ GQLerror.title }}</h2>
</tooltip-on-truncate>
<template v-if="error.tags">
<gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2">{{
errorLevel
}}</gl-badge>
<gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill">{{
error.tags.logger
}}</gl-badge>
<gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2"
>{{ errorLevel }}
</gl-badge>
<gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill"
>{{ error.tags.logger }}
</gl-badge>
</template>
<h3>{{ __('Error details') }}</h3>
......@@ -168,35 +201,35 @@ export default {
<li>
<span class="bold">{{ __('Sentry event') }}:</span>
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)"
:href="error.external_url"
v-track-event="trackClickErrorLinkToSentryOptions(GQLerror.externalUrl)"
:href="GQLerror.externalUrl"
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" />
</gl-link>
</li>
<li v-if="error.first_release_short_version">
<li v-if="GQLerror.firstReleaseShortVersion">
<span class="bold">{{ __('First seen') }}:</span>
{{ formatDate(error.first_seen) }}
{{ formatDate(GQLerror.firstSeen) }}
<gl-link :href="firstReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.first_release_short_version }}</span>
<span>{{ __('Release') }}: {{ GQLerror.firstReleaseShortVersion }}</span>
</gl-link>
</li>
<li v-if="error.last_release_short_version">
<li v-if="GQLerror.lastReleaseShortVersion">
<span class="bold">{{ __('Last seen') }}:</span>
{{ formatDate(error.last_seen) }}
{{ formatDate(GQLerror.lastSeen) }}
<gl-link :href="lastReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.last_release_short_version }}</span>
<span>{{ __('Release') }}: {{ GQLerror.lastReleaseShortVersion }}</span>
</gl-link>
</li>
<li>
<span class="bold">{{ __('Events') }}:</span>
<span>{{ error.count }}</span>
<span>{{ GQLerror.count }}</span>
</li>
<li>
<span class="bold">{{ __('Users') }}:</span>
<span>{{ error.user_count }}</span>
<span>{{ GQLerror.userCount }}</span>
</li>
</ul>
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import store from './store';
import ErrorDetails from './components/error_details.vue';
import csrf from '~/lib/utils/csrf';
Vue.use(VueApollo);
export default () => {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_details',
apolloProvider,
components: {
ErrorDetails,
},
store,
render(createElement) {
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', {
props: {
issueId,
projectPath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
......
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 */
);
}
......@@ -39,7 +39,7 @@ export const requestMetricsDashboard = ({ commit }) => {
};
export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
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);
};
export const receiveMetricsDashboardFailure = ({ commit }, error) => {
......
......@@ -84,23 +84,26 @@ export default {
state.emptyState = 'loading';
state.showEmptyState = true;
},
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
state.dashboard.panel_groups = groupData.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
let { panels = [] } = group;
// each panel has metric information that needs to be normalized
panels = panels.map(panel => ({
...panel,
metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
}));
return {
...group,
panels,
key,
};
});
[types.RECEIVE_METRICS_DATA_SUCCESS](state, dashboard) {
state.dashboard = {
...dashboard,
panel_groups: dashboard.panel_groups.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
let { panels = [] } = group;
// each panel has metric information that needs to be normalized
panels = panels.map(panel => ({
...panel,
metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
}));
return {
...group,
panels,