diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-03 03:09:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-03 03:09:02 +0300 |
commit | 4d6582cc54cc317bc31cc8dc2339668bb7550ced (patch) | |
tree | a5840938cd13e64c0db27792faecb3fd7f6fa782 /app | |
parent | 65be6f9dd4d92590294f18c6212a075585467f6d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
19 files changed, 172 insertions, 29 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index cdea55f0885..a1358cdc40e 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -84,6 +84,11 @@ export default { required: false, default: '', }, + endpointCodequality: { + type: String, + required: false, + default: '', + }, projectPath: { type: String, required: true, @@ -160,6 +165,7 @@ export default { plainDiffPath: (state) => state.diffs.plainDiffPath, emailPatchPath: (state) => state.diffs.emailPatchPath, retrievingBatches: (state) => state.diffs.retrievingBatches, + codequalityDiff: (state) => state.diffs.codequalityDiff, }), ...mapState('diffs', [ 'showTreeList', @@ -173,7 +179,12 @@ export default { 'viewDiffsFileByFile', 'mrReviews', ]), - ...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']), + ...mapGetters('diffs', [ + 'whichCollapsedTypes', + 'isParallelView', + 'currentDiffIndex', + 'fileCodequalityDiff', + ]), ...mapGetters(['isNotesFetched', 'getNoteableData']), diffs() { if (!this.viewDiffsFileByFile) { @@ -271,6 +282,7 @@ export default { endpointMetadata: this.endpointMetadata, endpointBatch: this.endpointBatch, endpointCoverage: this.endpointCoverage, + endpointCodequality: this.endpointCodequality, projectPath: this.projectPath, dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, @@ -326,6 +338,7 @@ export default { 'fetchDiffFilesMeta', 'fetchDiffFilesBatch', 'fetchCoverageFiles', + 'fetchCodequality', 'startRenderDiffsQueue', 'assignDiscussionsToDiff', 'setHighlightedRow', @@ -382,6 +395,10 @@ export default { this.fetchCoverageFiles(); } + if (this.endpointCodequality) { + this.fetchCodequality(); + } + if (!this.isNotesFetched) { notesEventHub.$emit('fetchNotesData'); } @@ -509,6 +526,7 @@ export default { :help-page-path="helpPagePath" :can-current-user-fork="canCurrentUserFork" :view-diffs-file-by-file="viewDiffsFileByFile" + :codequality-diff="fileCodequalityDiff(file.file_path)" /> <div v-if="showFileByFileNavigation" diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index ca4543f7002..93855db52b6 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -67,6 +67,11 @@ export default { type: Boolean, required: true, }, + codequalityDiff: { + type: Array, + required: false, + default: () => [], + }, }, data() { return { @@ -148,6 +153,9 @@ export default { return loggedIn && featureOn; }, + hasCodequalityChanges() { + return this.codequalityDiff.length > 0; + }, }, watch: { 'file.id': { @@ -294,6 +302,7 @@ export default { :add-merge-request-buttons="true" :view-diffs-file-by-file="viewDiffsFileByFile" :show-local-file-reviews="showLocalFileReviews" + :has-codequality-changes="hasCodequalityChanges" class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100" :class="hasBodyClasses.header" @toggleFile="handleToggle" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 27d9343ebf6..3b4e21ab61b 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -41,6 +41,7 @@ export default { GlDropdownDivider, GlFormCheckbox, GlLoadingIcon, + CodeQualityBadge: () => import('ee_component/diffs/components/code_quality_badge.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -95,6 +96,11 @@ export default { required: false, default: false, }, + hasCodequalityChanges: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -327,6 +333,8 @@ export default { data-track-property="diff_copy_file" /> + <code-quality-badge v-if="hasCodequalityChanges" class="gl-mr-2" /> + <small v-if="isModeChanged" ref="fileMode" class="mr-1"> {{ diffFile.a_mode }} → {{ diffFile.b_mode }} </small> diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 87e9af174e5..c278baf2d2c 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -73,6 +73,7 @@ export default function initDiffsApp(store) { endpointMetadata: dataset.endpointMetadata || '', endpointBatch: dataset.endpointBatch || '', endpointCoverage: dataset.endpointCoverage || '', + endpointCodequality: dataset.endpointCodequality || '', projectPath: dataset.projectPath, helpPagePath: dataset.helpPagePath, currentUser: JSON.parse(dataset.currentUserData) || {}, @@ -114,6 +115,7 @@ export default function initDiffsApp(store) { endpointMetadata: this.endpointMetadata, endpointBatch: this.endpointBatch, endpointCoverage: this.endpointCoverage, + endpointCodequality: this.endpointCodequality, currentUser: this.currentUser, projectPath: this.projectPath, helpPagePath: this.helpPagePath, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 70f7b24cd08..8e036dc32d0 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -1,4 +1,5 @@ import Cookies from 'js-cookie'; +import Visibility from 'visibilityjs'; import Vue from 'vue'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { diffViewerModes } from '~/ide/constants'; @@ -52,12 +53,15 @@ import { prepareLineForRenamedFile, } from './utils'; +let eTagPoll; + export const setBaseConfig = ({ commit }, options) => { const { endpoint, endpointMetadata, endpointBatch, endpointCoverage, + endpointCodequality, projectPath, dismissEndpoint, showSuggestPopover, @@ -70,6 +74,7 @@ export const setBaseConfig = ({ commit }, options) => { endpointMetadata, endpointBatch, endpointCoverage, + endpointCodequality, projectPath, dismissEndpoint, showSuggestPopover, @@ -231,6 +236,48 @@ export const fetchCoverageFiles = ({ commit, state }) => { coveragePoll.makeRequest(); }; +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopCodequalityPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartCodequalityPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const fetchCodequality = ({ commit, state, dispatch }) => { + eTagPoll = new Poll({ + resource: { + getCodequalityDiffReports: (endpoint) => axios.get(endpoint), + }, + data: state.endpointCodequality, + method: 'getCodequalityDiffReports', + successCallback: ({ status, data }) => { + if (status === httpStatusCodes.OK) { + commit(types.SET_CODEQUALITY_DATA, data); + + eTagPoll.stop(); + } + }, + errorCallback: () => createFlash(__('Something went wrong on our end. Please try again!')), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartCodequalityPolling'); + } else { + dispatch('stopCodequalityPolling'); + } + }); +}; + export const setHighlightedRow = ({ commit }, lineCode) => { const fileHash = lineCode.split('_')[0]; commit(types.SET_HIGHLIGHTED_ROW, lineCode); diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index dec3f87b03e..b06faa2284b 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -136,6 +136,16 @@ export const fileLineCoverage = (state) => (file, line) => { }; /** + * Returns the codequality diff data for a given file + * @param {string} filePath + * @returns {Array} + */ +export const fileCodequalityDiff = (state) => (filePath) => { + if (!state.codequalityDiff.files || !state.codequalityDiff.files[filePath]) return []; + return state.codequalityDiff.files[filePath]; +}; + +/** * Returns index of a currently selected diff in diffFiles * @returns {number} */ diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index f93435363ec..b673a5387ca 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -28,6 +28,7 @@ export default () => ({ startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff diffFiles: [], coverageFiles: {}, + codequalityDiff: {}, mergeRequestDiffs: [], mergeRequestDiff: null, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 4641731c4b6..b0f396f905a 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -11,6 +11,7 @@ export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS'; export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE'; export const SET_COVERAGE_DATA = 'SET_COVERAGE_DATA'; +export const SET_CODEQUALITY_DATA = 'SET_CODEQUALITY_DATA'; export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS'; export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index a93cacc49d8..a676b1bdb37 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -33,6 +33,7 @@ export default { endpointMetadata, endpointBatch, endpointCoverage, + endpointCodequality, projectPath, dismissEndpoint, showSuggestPopover, @@ -45,6 +46,7 @@ export default { endpointMetadata, endpointBatch, endpointCoverage, + endpointCodequality, projectPath, dismissEndpoint, showSuggestPopover, @@ -87,6 +89,10 @@ export default { Object.assign(state, { coverageFiles }); }, + [types.SET_CODEQUALITY_DATA](state, codequalityDiffData) { + Object.assign(state, { codequalityDiff: codequalityDiffData }); + }, + [types.RENDER_FILE](state, file) { renderFile(file); }, diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index c5dd3e1df35..d1c32350b1d 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -16,7 +16,8 @@ class Groups::LabelsController < Groups::ApplicationController format.html do # at group level we do not want to list project labels, # we only want `only_group_labels = false` when pulling labels for label filter dropdowns, fetched through json - @labels = available_labels(params.merge(only_group_labels: true)).page(params[:page]) + @labels = available_labels(params.merge(only_group_labels: true)).includes(:group).page(params[:page]) # rubocop: disable CodeReuse/ActiveRecord + @labels.each { |label| label.lazy_subscription(current_user) } # preload subscriptions end format.json do render json: LabelSerializer.new.represent_appearance(available_labels) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index cfc4075100b..6cc9959dcb9 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -164,8 +164,8 @@ module LabelsHelper end def label_subscription_status(label, project) - return 'group-level' if label.subscribed?(current_user) - return 'project-level' if label.subscribed?(current_user, project) + return 'group-level' if label.lazy_subscribed?(current_user) + return 'project-level' if label.lazy_subscribed?(current_user, project) 'unsubscribed' end @@ -181,7 +181,7 @@ module LabelsHelper end def label_subscription_toggle_button_text(label, project = nil) - label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe' + label.lazy_subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe' end def create_label_title(subject) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 376dde9d612..ecb5a7a9071 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -185,6 +185,27 @@ module MergeRequestsHelper end end + def diffs_tab_pane_data(project, merge_request, params) + { + "is-locked": merge_request.discussion_locked?, + endpoint: diffs_project_merge_request_path(project, merge_request, 'json', params), + endpoint_metadata: @endpoint_metadata_url, + endpoint_batch: diffs_batch_project_json_merge_request_path(project, merge_request, 'json', params), + endpoint_coverage: @coverage_path, + help_page_path: help_page_path('user/discussions/index.md', anchor: 'suggest-changes'), + current_user_data: @current_user_data, + update_current_user_path: @update_current_user_path, + project_path: project_path(merge_request.project), + changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), + is_fluid_layout: fluid_layout.to_s, + dismiss_endpoint: user_callouts_path, + show_suggest_popover: show_suggest_popover?.to_s, + show_whitespace_default: @show_whitespace_default.to_s, + file_by_file_default: @file_by_file_default.to_s, + default_suggestion_commit_message: default_suggestion_commit_message + } + end + private def review_requested_merge_requests_count diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index 33e9e0e38fb..10bfea0965e 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -17,13 +17,47 @@ module Subscribable def subscribed?(user, project = nil) return false unless user - if subscription = subscriptions.find_by(user: user, project: project) + if (subscription = subscriptions.find_by(user: user, project: project)) subscription.subscribed else subscribed_without_subscriptions?(user, project) end end + def lazy_subscribed?(user, project = nil) + return false unless user + + if (subscription = lazy_subscription(user, project)&.itself) + subscription.subscribed + else + subscribed_without_subscriptions?(user, project) + end + end + + def lazy_subscription(user, project = nil) + return unless user + + # handle project and group labels as well as issuable subscriptions + subscribable_type = self.class.ancestors.include?(Label) ? 'Label' : self.class.name + BatchLoader.for(id: id, subscribable_type: subscribable_type, project_id: project&.id).batch do |items, loader| + values = items.each_with_object({ ids: Set.new, subscribable_types: Set.new, project_ids: Set.new }) do |item, result| + result[:ids] << item[:id] + result[:subscribable_types] << item[:subscribable_type] + result[:project_ids] << item[:project_id] + end + + subscriptions = Subscription.where(subscribable_id: values[:ids], subscribable_type: values[:subscribable_types], project_id: values[:project_ids], user: user) + + subscriptions.each do |subscription| + loader.call({ + id: subscription.subscribable_id, + subscribable_type: subscription.subscribable_type, + project_id: subscription.project_id + }, subscription) + end + end + end + # Override this method to define custom logic to consider a subscribable as # subscribed without an explicit subscription record. def subscribed_without_subscriptions?(user, project) diff --git a/app/views/admin/application_settings/ci/_header.html.haml b/app/views/admin/application_settings/ci/_header.html.haml index aa4cebdb603..919f501d2ee 100644 --- a/app/views/admin/application_settings/ci/_header.html.haml +++ b/app/views/admin/application_settings/ci/_header.html.haml @@ -8,7 +8,7 @@ %p = _('Variables store information, like passwords and secret keys, that you can use in job scripts. All projects on the instance can use these variables.') - = link_to s_('Learn more.'), help_page_path('ci/variables/README', anchor: 'instance-level-cicd-variables'), target: '_blank', rel: 'noopener noreferrer' + = link_to s_('Learn more.'), help_page_path('ci/variables/README', anchor: 'instance-cicd-variables'), target: '_blank', rel: 'noopener noreferrer' %p = _('Variables can be:') %ul @@ -16,4 +16,4 @@ = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } %li = html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } - = link_to _('Learn more.'), help_page_path('ci/variables/README', anchor: 'masked-variable-requirements'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('ci/variables/README', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml index 28572fccb9d..d38b4cba40a 100644 --- a/app/views/admin/application_settings/ci_cd.html.haml +++ b/app/views/admin/application_settings/ci_cd.html.haml @@ -8,7 +8,7 @@ .settings-content - if ci_variable_protected_by_default? %p.settings-message.text-center - - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') } + - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-cicd-variable') } = s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } #js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} } diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml index fd4b546e150..5eded970bf0 100644 --- a/app/views/ci/variables/_content.html.haml +++ b/app/views/ci/variables/_content.html.haml @@ -7,4 +7,4 @@ = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } %li = html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } - = link_to _('Learn more.'), help_page_path('ci/variables/README', anchor: 'masked-variable-requirements'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('ci/variables/README', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index fc0e3488e57..cb6c82e6d77 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -2,7 +2,7 @@ - if ci_variable_protected_by_default? %p.settings-message.text-center - - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') } + - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-cicd-variable') } = s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } - is_group = !@group.nil? @@ -16,7 +16,7 @@ aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-the-aws-elastic-container-service-ecs'), aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'run-aws-commands-from-gitlab-cicd'), aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'aws'), - protected_environment_variables_link: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable'), + protected_environment_variables_link: help_page_path('ci/variables/README', anchor: 'protect-a-cicd-variable'), masked_environment_variables_link: help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'), } } diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index 193ec8abf04..b0c38f72cf1 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -39,7 +39,7 @@ = value %p.masking-validation-error.gl-field-error.hide = s_("CiVariables|Cannot use Masked Variable with current value") - = link_to sprite_icon('question-o'), help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'), target: '_blank', rel: 'noopener noreferrer' + = link_to sprite_icon('question-o'), help_page_path('ci/variables/README', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer' - unless only_key_value .ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0 .gl-mr-3 diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index e266c79df9c..2c4bd2e3256 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -81,22 +81,7 @@ - params = request.query_parameters - if Feature.enabled?(:default_merge_ref_for_diffs, @project, default_enabled: :yaml) - params = params.merge(diff_head: true) - = render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: { "is-locked": @merge_request.discussion_locked?, - endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', params), - endpoint_metadata: @endpoint_metadata_url, - endpoint_batch: diffs_batch_project_json_merge_request_path(@project, @merge_request, 'json', params), - endpoint_coverage: @coverage_path, - help_page_path: suggest_changes_help_path, - current_user_data: @current_user_data, - update_current_user_path: @update_current_user_path, - project_path: project_path(@merge_request.project), - changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), - is_fluid_layout: fluid_layout.to_s, - dismiss_endpoint: user_callouts_path, - show_suggest_popover: show_suggest_popover?.to_s, - show_whitespace_default: @show_whitespace_default.to_s, - file_by_file_default: @file_by_file_default.to_s, - default_suggestion_commit_message: default_suggestion_commit_message } + = render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: diffs_tab_pane_data(@project, @merge_request, params) .mr-loading-status .loading.hide |