Commit a39c97e3 authored by mfluharty's avatar mfluharty

Use new vuex store for code quality MR widget

Make GroupedCodequalityReportsApp to replace report-section
Hook it up to the vuex store
Fix import for parseCodeclimateMetrics in full code quality report

Move out of ee directory:
- endpoints passed in via haml
- endpoints passed through the mr widget store
- issue body component
- codeclimate mock data for the mr widget
parent c795c32e
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { componentNames } from '~/reports/components/issue_body';
import ReportSection from '~/reports/components/report_section.vue';
import createStore from './store';
export default {
name: 'GroupedCodequalityReportsApp',
store: createStore(),
components: {
ReportSection,
},
props: {
headPath: {
type: String,
required: true,
},
headBlobPath: {
type: String,
required: true,
},
basePath: {
type: String,
required: false,
default: null,
},
baseBlobPath: {
type: String,
required: false,
default: null,
},
codequalityHelpPath: {
type: String,
required: true,
},
},
componentNames,
computed: {
...mapState(['newIssues', 'resolvedIssues']),
...mapGetters([
'hasCodequalityIssues',
'codequalityStatus',
'codequalityText',
'codequalityPopover',
]),
},
created() {
this.setPaths({
basePath: this.basePath,
headPath: this.headPath,
baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath,
helpPath: this.codequalityHelpPath,
});
this.fetchReports();
},
methods: {
...mapActions(['fetchReports', 'setPaths']),
},
};
</script>
<template>
<report-section
:status="codequalityStatus"
:loading-text="
sprintf(s__('ciReport|Loading %{reportName} report'), {
reportName: 'codeclimate',
})
"
:error-text="
sprintf(s__('ciReport|Failed to load %{reportName} report'), {
reportName: 'codeclimate',
})
"
:success-text="codequalityText"
:unresolved-issues="newIssues"
:resolved-issues="resolvedIssues"
:has-issues="hasCodequalityIssues"
:component="$options.componentNames.CodequalityIssueBody"
:popover-options="codequalityPopover"
class="js-codequality-widget mr-widget-border-top mr-report"
/>
</template>
import TestIssueBody from './test_issue_body.vue'; import TestIssueBody from './test_issue_body.vue';
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue'; import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
export const components = { export const components = {
AccessibilityIssueBody, AccessibilityIssueBody,
CodequalityIssueBody,
TestIssueBody, TestIssueBody,
}; };
export const componentNames = { export const componentNames = {
AccessibilityIssueBody: AccessibilityIssueBody.name, AccessibilityIssueBody: AccessibilityIssueBody.name,
CodequalityIssueBody: CodequalityIssueBody.name,
TestIssueBody: TestIssueBody.name, TestIssueBody: TestIssueBody.name,
}; };
...@@ -37,6 +37,7 @@ import eventHub from './event_hub'; ...@@ -37,6 +37,7 @@ import eventHub from './event_hub';
import notify from '~/lib/utils/notify'; import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue'; import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils'; import { setFaviconOverlay } from '../lib/utils/common_utils';
import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue'; import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
...@@ -75,6 +76,7 @@ export default { ...@@ -75,6 +76,7 @@ export default {
'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState, 'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus, SourceBranchRemovalStatus,
GroupedCodequalityReportsApp,
GroupedTestReportsApp, GroupedTestReportsApp,
TerraformPlan, TerraformPlan,
GroupedAccessibilityReportsApp, GroupedAccessibilityReportsApp,
...@@ -111,6 +113,9 @@ export default { ...@@ -111,6 +113,9 @@ export default {
shouldSuggestPipelines() { shouldSuggestPipelines() {
return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath; return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
}, },
shouldRenderCodeQuality() {
return this.mr?.codeclimate?.head_path;
},
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState; return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
}, },
...@@ -380,6 +385,15 @@ export default { ...@@ -380,6 +385,15 @@ export default {
:mr="mr" :mr="mr"
/> />
<div class="mr-section-container mr-widget-workflow"> <div class="mr-section-container mr-widget-workflow">
<grouped-codequality-reports-app
v-if="shouldRenderCodeQuality"
:base-path="mr.codeclimate.base_path"
:head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:codequality-help-path="mr.codequalityHelpPath"
/>
<grouped-test-reports-app <grouped-test-reports-app
v-if="mr.testResultsPath" v-if="mr.testResultsPath"
class="js-reports-container" class="js-reports-container"
......
...@@ -185,6 +185,13 @@ export default class MergeRequestStore { ...@@ -185,6 +185,13 @@ export default class MergeRequestStore {
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path; this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access; this.humanAccess = data.human_access;
this.newPipelinePath = data.new_project_pipeline_path; this.newPipelinePath = data.new_project_pipeline_path;
// codeclimate
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
} }
get isNothingToMergeState() { get isNothingToMergeState() {
......
...@@ -13,5 +13,6 @@ ...@@ -13,5 +13,6 @@
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}'; window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}'; window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}'; window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
...@@ -3,13 +3,13 @@ import * as types from './mutation_types'; ...@@ -3,13 +3,13 @@ import * as types from './mutation_types';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store'; import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_comparison';
export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page); export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page);
export const requestReport = ({ commit }) => commit(types.REQUEST_REPORT); export const requestReport = ({ commit }) => commit(types.REQUEST_REPORT);
export const receiveReportSuccess = ({ state, commit }, data) => { export const receiveReportSuccess = ({ state, commit }, data) => {
const parsedIssues = MergeRequestStore.parseCodeclimateMetrics(data, state.blobPath); const parsedIssues = parseCodeclimateMetrics(data, state.blobPath);
commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues); commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues);
}; };
export const receiveReportError = ({ commit }, error) => commit(types.RECEIVE_REPORT_ERROR, error); export const receiveReportError = ({ commit }, error) => commit(types.RECEIVE_REPORT_ERROR, error);
......
import PerformanceIssueBody from 'ee/vue_merge_request_widget/components/performance_issue_body.vue'; import PerformanceIssueBody from 'ee/vue_merge_request_widget/components/performance_issue_body.vue';
import CodequalityIssueBody from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import BlockingMergeRequestsBody from 'ee/vue_merge_request_widget/components/blocking_merge_requests/blocking_merge_request_body.vue'; import BlockingMergeRequestsBody from 'ee/vue_merge_request_widget/components/blocking_merge_requests/blocking_merge_request_body.vue';
import LicenseIssueBody from 'ee/vue_shared/license_compliance/components/license_issue_body.vue'; import LicenseIssueBody from 'ee/vue_shared/license_compliance/components/license_issue_body.vue';
import SecurityIssueBody from 'ee/vue_shared/security_reports/components/security_issue_body.vue'; import SecurityIssueBody from 'ee/vue_shared/security_reports/components/security_issue_body.vue';
...@@ -12,7 +11,6 @@ import { ...@@ -12,7 +11,6 @@ import {
export const components = { export const components = {
...componentsCE, ...componentsCE,
PerformanceIssueBody, PerformanceIssueBody,
CodequalityIssueBody,
LicenseIssueBody, LicenseIssueBody,
SecurityIssueBody, SecurityIssueBody,
MetricsReportsIssueBody, MetricsReportsIssueBody,
...@@ -22,7 +20,6 @@ export const components = { ...@@ -22,7 +20,6 @@ export const components = {
export const componentNames = { export const componentNames = {
...componentNamesCE, ...componentNamesCE,
PerformanceIssueBody: PerformanceIssueBody.name, PerformanceIssueBody: PerformanceIssueBody.name,
CodequalityIssueBody: CodequalityIssueBody.name,
LicenseIssueBody: LicenseIssueBody.name, LicenseIssueBody: LicenseIssueBody.name,
SecurityIssueBody: SecurityIssueBody.name, SecurityIssueBody: SecurityIssueBody.name,
MetricsReportsIssueBody: MetricsReportsIssueBody.name, MetricsReportsIssueBody: MetricsReportsIssueBody.name,
......
...@@ -8,7 +8,7 @@ import MrWidgetLicenses from 'ee/vue_shared/license_compliance/mr_widget_license ...@@ -8,7 +8,7 @@ import MrWidgetLicenses from 'ee/vue_shared/license_compliance/mr_widget_license
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import BlockingMergeRequestsReport from './components/blocking_merge_requests/blocking_merge_requests_report.vue'; import BlockingMergeRequestsReport from './components/blocking_merge_requests/blocking_merge_requests_report.vue';
import { n__, s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import MrWidgetApprovals from './components/approvals/approvals.vue'; import MrWidgetApprovals from './components/approvals/approvals.vue';
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue'; import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
...@@ -33,10 +33,8 @@ export default { ...@@ -33,10 +33,8 @@ export default {
componentNames, componentNames,
data() { data() {
return { return {
isLoadingCodequality: false,
isLoadingBrowserPerformance: false, isLoadingBrowserPerformance: false,
isLoadingLoadPerformance: false, isLoadingLoadPerformance: false,
loadingCodequalityFailed: false,
loadingBrowserPerformanceFailed: false, loadingBrowserPerformanceFailed: false,
loadingLoadPerformanceFailed: false, loadingLoadPerformanceFailed: false,
loadingLicenseReportFailed: false, loadingLicenseReportFailed: false,
...@@ -46,22 +44,9 @@ export default { ...@@ -46,22 +44,9 @@ export default {
shouldRenderApprovals() { shouldRenderApprovals() {
return this.mr.hasApprovalsAvailable && this.mr.state !== 'nothingToMerge'; return this.mr.hasApprovalsAvailable && this.mr.state !== 'nothingToMerge';
}, },
shouldRenderCodeQuality() {
const { codeclimate } = this.mr || {};
return codeclimate && codeclimate.head_path;
},
shouldRenderLicenseReport() { shouldRenderLicenseReport() {
return this.mr.enabledReports?.licenseScanning; return this.mr.enabledReports?.licenseScanning;
}, },
hasCodequalityIssues() {
return (
this.mr.codeclimateMetrics &&
((this.mr.codeclimateMetrics.newIssues &&
this.mr.codeclimateMetrics.newIssues.length > 0) ||
(this.mr.codeclimateMetrics.resolvedIssues &&
this.mr.codeclimateMetrics.resolvedIssues.length > 0))
);
},
hasBrowserPerformanceMetrics() { hasBrowserPerformanceMetrics() {
return ( return (
this.mr.browserPerformanceMetrics?.degraded?.length > 0 || this.mr.browserPerformanceMetrics?.degraded?.length > 0 ||
...@@ -112,47 +97,6 @@ export default { ...@@ -112,47 +97,6 @@ export default {
this.$options.securityReportTypes.some(reportType => enabledReports[reportType]) this.$options.securityReportTypes.some(reportType => enabledReports[reportType])
); );
}, },
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
const text = [];
if (!newIssues.length && !resolvedIssues.length) {
text.push(s__('ciReport|No changes to code quality'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) {
text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
text.push(__(' and'));
}
if (newIssues.length) {
text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
}
}
return text.join('');
},
codequalityPopover() {
const { codeclimate } = this.mr || {};
if (codeclimate && !codeclimate.base_path) {
return {
title: s__('ciReport|Base pipeline codequality artifact not found'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
{
linkStartTag: `<a href="${this.mr.codequalityHelpPath}" target="_blank" rel="noopener noreferrer">`,
linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>',
},
false,
),
};
}
return {};
},
browserPerformanceText() { browserPerformanceText() {
const { improved, degraded, same } = this.mr.browserPerformanceMetrics; const { improved, degraded, same } = this.mr.browserPerformanceMetrics;
...@@ -204,10 +148,6 @@ export default { ...@@ -204,10 +148,6 @@ export default {
return [...text, ...reportNumbers.join(', ')].join(''); return [...text, ...reportNumbers.join(', ')].join('');
}, },
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
browserPerformanceStatus() { browserPerformanceStatus() {
return this.checkReportStatus( return this.checkReportStatus(
this.isLoadingBrowserPerformance, this.isLoadingBrowserPerformance,
...@@ -236,11 +176,6 @@ export default { ...@@ -236,11 +176,6 @@ export default {
}, },
}, },
watch: { watch: {
shouldRenderCodeQuality(newVal) {
if (newVal) {
this.fetchCodeQuality();
}
},
hasBrowserPerformancePaths(newVal) { hasBrowserPerformancePaths(newVal) {
if (newVal) { if (newVal) {
this.fetchBrowserPerformance(); this.fetchBrowserPerformance();
...@@ -264,37 +199,6 @@ export default { ...@@ -264,37 +199,6 @@ export default {
apiUnapprovePath: store.apiUnapprovePath, apiUnapprovePath: store.apiUnapprovePath,
}; };
}, },
fetchCodeQuality() {
const { codeclimate } = this.mr || {};
if (!codeclimate.base_path) {
this.isLoadingCodequality = false;
this.loadingCodequalityFailed = true;
return;
}
this.isLoadingCodequality = true;
Promise.all([
this.service.fetchReport(codeclimate.head_path),
this.service.fetchReport(codeclimate.base_path),
])
.then(values =>
this.mr.compareCodeclimateMetrics(
values[0],
values[1],
this.mr.headBlobPath,
this.mr.baseBlobPath,
),
)
.then(() => {
this.isLoadingCodequality = false;
})
.catch(() => {
this.isLoadingCodequality = false;
this.loadingCodequalityFailed = true;
});
},
fetchBrowserPerformance() { fetchBrowserPerformance() {
const { head_path, base_path } = this.mr.browserPerformance; const { head_path, base_path } = this.mr.browserPerformance;
...@@ -367,18 +271,13 @@ export default { ...@@ -367,18 +271,13 @@ export default {
/> />
<div class="mr-section-container mr-widget-workflow"> <div class="mr-section-container mr-widget-workflow">
<blocking-merge-requests-report :mr="mr" /> <blocking-merge-requests-report :mr="mr" />
<report-section <grouped-codequality-reports-app
v-if="shouldRenderCodeQuality" v-if="shouldRenderCodeQuality"
:status="codequalityStatus" :base-path="mr.codeclimate.base_path"
:loading-text="translateText('codeclimate').loading" :head-path="mr.codeclimate.head_path"
:error-text="translateText('codeclimate').error" :head-blob-path="mr.headBlobPath"
:success-text="codequalityText" :base-blob-path="mr.baseBlobPath"
:unresolved-issues="mr.codeclimateMetrics.newIssues" :codequality-help-path="mr.codequalityHelpPath"
:resolved-issues="mr.codeclimateMetrics.resolvedIssues"
:has-issues="hasCodequalityIssues"
:component="$options.componentNames.CodequalityIssueBody"
:popover-options="codequalityPopover"
class="js-codequality-widget mr-widget-border-top mr-report"
/> />
<report-section <report-section
v-if="shouldRenderBrowserPerformance" v-if="shouldRenderBrowserPerformance"
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mapApprovalsResponse, mapApprovalRulesResponse } from '../mappers'; import { mapApprovalsResponse, mapApprovalRulesResponse } from '../mappers';
import CodeQualityComparisonWorker from '../workers/code_quality_comparison_worker';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export default class MergeRequestStore extends CEMergeRequestStore { export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) { constructor(data) {
super(data); super(data);
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
this.sastHelp = data.sast_help_path; this.sastHelp = data.sast_help_path;
this.containerScanningHelp = data.container_scanning_help_path; this.containerScanningHelp = data.container_scanning_help_path;
this.dastHelp = data.dast_help_path; this.dastHelp = data.dast_help_path;
...@@ -20,7 +16,6 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -20,7 +16,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.canReadVulnerabilityFeedback = data.can_read_vulnerability_feedback; this.canReadVulnerabilityFeedback = data.can_read_vulnerability_feedback;
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path; this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path;
this.approvalsHelpPath = data.approvals_help_path; this.approvalsHelpPath = data.approvals_help_path;
this.codequalityHelpPath = data.codequality_help_path;
this.securityReportsPipelineId = data.pipeline_id; this.securityReportsPipelineId = data.pipeline_id;
this.securityReportsPipelineIid = data.pipeline_iid; this.securityReportsPipelineIid = data.pipeline_iid;
this.createVulnerabilityFeedbackIssuePath = data.create_vulnerability_feedback_issue_path; this.createVulnerabilityFeedbackIssuePath = data.create_vulnerability_feedback_issue_path;
...@@ -31,7 +26,6 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -31,7 +26,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.visualReviewAppAvailable = Boolean(data.visual_review_app_available); this.visualReviewAppAvailable = Boolean(data.visual_review_app_available);
this.appUrl = gon && gon.gitlab_url; this.appUrl = gon && gon.gitlab_url;
this.initCodeclimate(data);
this.initBrowserPerformanceReport(data); this.initBrowserPerformanceReport(data);
this.initLoadPerformanceReport(data); this.initLoadPerformanceReport(data);
this.licenseScanning = data.license_scanning; this.licenseScanning = data.license_scanning;
...@@ -81,14 +75,6 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -81,14 +75,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.approvalRules = mapApprovalRulesResponse(data.rules, this.approvals); this.approvalRules = mapApprovalRulesResponse(data.rules, this.approvals);
} }
initCodeclimate(data) {
this.codeclimate = data.codeclimate;
this.codeclimateMetrics = {
newIssues: [],
resolvedIssues: [],
};
}
initBrowserPerformanceReport(data) { initBrowserPerformanceReport(data) {
this.browserPerformance = data.browser_performance; this.browserPerformance = data.browser_performance;
this.browserPerformanceMetrics = { this.browserPerformanceMetrics = {
...@@ -107,32 +93,6 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -107,32 +93,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
}; };
} }
static doCodeClimateComparison(headIssues, baseIssues) {
// Do these comparisons in worker threads to avoid blocking the main thread
return new Promise((resolve, reject) => {
const worker = new CodeQualityComparisonWorker();
worker.addEventListener('message', ({ data }) =>
data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
);
worker.postMessage({
headIssues,
baseIssues,
});
});
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseCodeclimateMetrics(baseIssues, baseBlobPath);
return MergeRequestStore.doCodeClimateComparison(parsedHeadIssues, parsedBaseIssues).then(
response => {
this.codeclimateMetrics.newIssues = response.newIssues;
this.codeclimateMetrics.resolvedIssues = response.resolvedIssues;
},
);
}
compareBrowserPerformanceMetrics(headMetrics, baseMetrics) { compareBrowserPerformanceMetrics(headMetrics, baseMetrics) {
const headMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(headMetrics); const headMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(headMetrics);
const baseMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(baseMetrics); const baseMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(baseMetrics);
...@@ -254,38 +214,4 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -254,38 +214,4 @@ export default class MergeRequestStore extends CEMergeRequestStore {
return indexedMetrics; return indexedMetrics;
} }
static parseCodeclimateMetrics(issues = [], path = '') {
return issues.map(issue => {
const parsedIssue = {
...issue,
name: issue.description,
};
if (issue.location) {
let parseCodeQualityUrl;
if (issue.location.path) {
parseCodeQualityUrl = `${path}/${issue.location.path}`;
parsedIssue.path = issue.location.path;
if (issue.location.lines && issue.location.lines.begin) {
parsedIssue.line = issue.location.lines.begin;
parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
} else if (
issue.location.positions &&
issue.location.positions.begin &&
issue.location.positions.begin.line
) {
parsedIssue.line = issue.location.positions.begin.line;
parseCodeQualityUrl += `#L${issue.location.positions.begin.line}`;
}
parsedIssue.urlPath = parseCodeQualityUrl;
}
}
return parsedIssue;
});
}
} }
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
const keyToFilterBy = 'fingerprint';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data } = e;
if (data === undefined) {
return null;
}
const { headIssues, baseIssues } = data;
if (!headIssues || !baseIssues) {
// eslint-disable-next-line no-restricted-globals
return self.postMessage({});
}
// eslint-disable-next-line no-restricted-globals
self.postMessage({
newIssues: filterByKey(headIssues, baseIssues, keyToFilterBy),
resolvedIssues: filterByKey(baseIssues, headIssues, keyToFilterBy),
});
// eslint-disable-next-line no-restricted-globals
return self.close();
});
/**
* Compares two arrays by the given key and returns the difference
*
* @param {Array} firstArray
* @param {Array} secondArray
* @param {String} key
* @returns {Array}
*/
const filterByKey = (firstArray = [], secondArray = [], key = '') =>
firstArray.filter(item => !secondArray.find(el => el[key] === item[key]));
export default filterByKey;
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,6 @@
window.gl.mrWidgetData.secret_scanning_help_path = '#{help_page_path('user/application_security/sast/index', anchor: 'secret-detection')}'; window.gl.mrWidgetData.secret_scanning_help_path = '#{help_page_path('user/application_security/sast/index', anchor: 'secret-detection')}';
window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}'; window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}'; window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true'; window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true';
window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}' window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}' window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}'
......
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import mrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue'; import mrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue';
import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import mountComponent from 'helpers/vue_mount_component_helper'; import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import mockData, { import mockData, {
baseIssues,
headIssues,
baseBrowserPerformance, baseBrowserPerformance,
headBrowserPerformance, headBrowserPerformance,
baseLoadPerformance, baseLoadPerformance,
headLoadPerformance, headLoadPerformance,
parsedBaseIssues,
parsedHeadIssues,
} from './mock_data'; } from './mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
...@@ -303,204 +297,6 @@ describe('ee merge request widget options', () => { ...@@ -303,204 +297,6 @@ describe('ee merge request widget options', () => {
}); });
}); });
describe('code quality', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
codeclimate: {},
};
});
describe('when it is loading', () => {
it('should render loading indicator', done => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
vm.mr.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.$nextTick(() => {
expect(trimText(vm.$el.querySelector('.js-codequality-widget').textContent)).toContain(
'Loading codeclimate report',
);
done();
});
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
// mock worker response
jest.spyOn(MRWidgetStore, 'doCodeClimateComparison').mockResolvedValue({
newIssues: filterByKey(parsedHeadIssues, parsedBaseIssues, 'fingerprint'),
resolvedIssues: filterByKey(parsedBaseIssues, parsedHeadIssues, 'fingerprint'),
});
});
it('should render provided data', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Code quality improved on 1 point and degraded on 1 point');
done();
});
});
describe('text connector', () => {
it('should only render information about fixed issues', done => {
setImmediate(() => {
vm.mr.codeclimateMetrics.newIssues = [];
Vue.nextTick(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Code quality improved on 1 point');
done();
});
});
});
it('should only render information about added issues', done => {
setImmediate(() => {
vm.mr.codeclimateMetrics.resolvedIssues = [];
Vue.nextTick(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Code quality degraded on 1 point');
done();
});
});
});
});
});
describe('with empty successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, []);
mock.onGet('base.json').reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
// mock worker response
jest.spyOn(MRWidgetStore, 'doCodeClimateComparison').mockResolvedValue({
newIssues: filterByKey([], [], 'fingerprint'),
resolvedIssues: filterByKey([], [], 'fingerprint'),
});
});
afterEach(() => {
mock.restore();
});
it('should render provided data', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('No changes to code quality');
done();
});
});
});
describe('with a head_path but no base_path', () => {
beforeEach(() => {
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: null,
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toContain('Failed to load codeclimate report');
done();
});
});
it('should render a help icon with more information', done => {
setImmediate(() => {
expect(vm.$el.querySelector('.js-codequality-widget .btn-help')).not.toBeNull();
expect(vm.codequalityPopover.title).toBe('Base pipeline codequality artifact not found');
done();
});
});
});
describe('with codeclimate comparison worker rejection', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
// mock worker rejection
jest.spyOn(MRWidgetStore, 'doCodeClimateComparison').mockRejectedValue();
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toEqual('Failed to load codeclimate report');
done();
});
});
});
describe('with failed request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(500, []);
mock.onGet('base.json').reply(500, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
gl.mrWidgetData.codeclimate = {
head_path: 'head.json',
base_path: 'base.json',
};
vm.mr.codeclimate = gl.mrWidgetData.codeclimate;
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
trimText(vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent),
).toContain('Failed to load codeclimate report');
done();
});
});
});
});
describe('browser_performance', () => { describe('browser_performance', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = { gl.mrWidgetData = {
......
...@@ -2,14 +2,6 @@ import mockData, { mockStore } from 'jest/vue_mr_widget/mock_data'; ...@@ -2,14 +2,6 @@ import mockData, { mockStore } from 'jest/vue_mr_widget/mock_data';
export default { export default {
...mockData, ...mockData,
codeclimate: {
head_path: 'head.json',
base_path: 'base.json',
},
blob_path: {
base_path: 'blob_path',
head_path: 'blob_path',
},
vulnerability_feedback_help_path: '/help/user/application_security/index', vulnerability_feedback_help_path: '/help/user/application_security/index',
enabled_reports: { enabled_reports: {
sast: false, sast: false,
...@@ -21,84 +13,6 @@ export default { ...@@ -21,84 +13,6 @@ export default {
}, },
}; };
// Codeclimate
export const headIssues = [
{
check_name: 'Rubocop/Lint/UselessAssignment',
description: 'Insecure Dependency',
location: {
path: 'lib/six.rb',
lines: {
begin: 6,
end: 7,
},
},
fingerprint: 'e879dd9bbc0953cad5037cde7ff0f627',
},
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 22,
end: 22,
},
},
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
},
];
// Codeclimate
export const parsedHeadIssues = [
{
...headIssues[1],
name: 'Insecure Dependency',
path: 'lib/six.rb',
urlPath: 'headPath/lib/six.rb#L6',
line: 6,
},
];
export const baseIssues = [
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 22,
end: 22,
},
},
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
},
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 21,
end: 21,
},
},
fingerprint: 'ca2354534dee94ae60ba2f54e3857c50e5',
},
];
export const parsedBaseIssues = [
{
...baseIssues[1],
name: 'Insecure Dependency',
path: 'Gemfile.lock',
line: 21,
urlPath: 'basePath/Gemfile.lock#L21',
},
];
// Browser Performance Testing // Browser Performance Testing
export const headBrowserPerformance = [ export const headBrowserPerformance = [
{ {
......
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store'; import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key'; import mockData from 'ee_jest/vue_mr_widget/mock_data';
import mockData, {
headIssues,
baseIssues,
parsedBaseIssues,
parsedHeadIssues,
} from 'ee_jest/vue_mr_widget/mock_data';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
describe('MergeRequestStore', () => { describe('MergeRequestStore', () => {
...@@ -15,38 +9,6 @@ describe('MergeRequestStore', () => { ...@@ -15,38 +9,6 @@ describe('MergeRequestStore', () => {
store = new MergeRequestStore(mockData); store = new MergeRequestStore(mockData);
}); });
describe('compareCodeclimateMetrics', () => {
beforeEach(() => {
// mock worker response
jest.spyOn(MergeRequestStore, 'doCodeClimateComparison').mockImplementation(() =>
Promise.resolve({
newIssues: filterByKey(parsedHeadIssues, parsedBaseIssues, 'fingerprint'),
resolvedIssues: filterByKey(parsedBaseIssues, parsedHeadIssues, 'fingerprint'),
}),
);
return store.compareCodeclimateMetrics(headIssues, baseIssues, 'headPath', 'basePath');
});
it('should return the new issues', () => {
expect(store.codeclimateMetrics.newIssues[0]).toEqual(parsedHeadIssues[0]);
});
it('should return the resolved issues', () => {
expect(store.codeclimateMetrics.resolvedIssues[0]).toEqual(parsedBaseIssues[0]);
});
});
describe('parseCodeclimateMetrics', () => {
it('should parse the received issues', () => {
const codequality = MergeRequestStore.parseCodeclimateMetrics(baseIssues, 'path')[0];
expect(codequality.name).toEqual(baseIssues[0].check_name);
expect(codequality.path).toEqual(baseIssues[0].location.path);
expect(codequality.line).toEqual(baseIssues[0].location.lines.begin);
});
});
describe('isNothingToMergeState', () => { describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => { it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge; store.state = stateKey.nothingToMerge;
......
...@@ -6,7 +6,6 @@ import { ...@@ -6,7 +6,6 @@ import {
groupedReportText, groupedReportText,
} from 'ee/vue_shared/security_reports/store/utils'; } from 'ee/vue_shared/security_reports/store/utils';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type'; import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import getFileLocation from 'ee/vue_shared/security_reports/store/utils/get_file_location'; import getFileLocation from 'ee/vue_shared/security_reports/store/utils/get_file_location';
import { import {
CRITICAL, CRITICAL,
...@@ -63,15 +62,6 @@ describe('security reports utils', () => { ...@@ -63,15 +62,6 @@ describe('security reports utils', () => {
); );
}); });
describe('filterByKey', () => {
it('filters the array with the provided key', () => {
const array1 = [{ id: '1234' }, { id: 'abg543' }, { id: '214swfA' }];
const array2 = [{ id: '1234' }, { id: 'abg543' }, { id: '453OJKs' }];
expect(filterByKey(array1, array2, 'id')).toEqual([{ id: '214swfA' }]);
});
});
describe('getFileLocation', () => { describe('getFileLocation', () => {
const hostname = 'https://hostna.me'; const hostname = 'https://hostna.me';
const path = '/deeply/nested/route'; const path = '/deeply/nested/route';
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import component from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue'; import component from '~/reports/codequality_report/components/codequality_issue_body.vue';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
describe('code quality issue body issue body', () => { describe('code quality issue body issue body', () => {
......
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
import store from '~/reports/codequality_report/store';
import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Grouped code quality reports app', () => {
const Component = localVue.extend(GroupedCodequalityReportsApp);
let wrapper;
let mockStore;
const mountComponent = (props = {}) => {
wrapper = mount(Component, {
store: mockStore,
localVue,
propsData: {
basePath: 'base.json',
headPath: 'head.json',
baseBlobPath: 'base/blob/path/',
headBlobPath: 'head/blob/path/',
codequalityHelpPath: 'codequality_help.html',
...props,
},
methods: {
fetchReports: () => {},
},
});
};
const findWidget = () => wrapper.find('.js-codequality-widget');
const findIssueBody = () => wrapper.find(CodequalityIssueBody);
beforeEach(() => {
mockStore = store();
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('when it is loading reports', () => {
beforeEach(() => {
mockStore.state.isLoading = true;
});
it('should render loading text', () => {
expect(findWidget().text()).toEqual('Loading codeclimate report');
});
});
describe('when base and head reports are loaded and compared', () => {
describe('with no issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [];
mockStore.state.resolvedIssues = [];
});
it('renders no changes text', () => {
expect(findWidget().text()).toEqual('No changes to code quality');
});
});
describe('with issues', () => {
describe('with new issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
mockStore.state.resolvedIssues = [];
});
it('renders summary text', () => {
expect(findWidget().text()).toContain('Code quality degraded on 1 point');
});
it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
});
});
describe('with resolved issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [];
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
});
it('renders summary text', () => {
expect(findWidget().text()).toContain('Code quality improved on 1 point');
});
it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedBaseIssues[0]);
});
});
describe('with new and resolved issues', () => {
beforeEach(() => {
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
});
it('renders summary text', () => {
expect(findWidget().text()).toContain(
'Code quality improved on 1 point and degraded on 1 point',
);
});
it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
});
});
});
});
describe('when there is a head report but no base report', () => {
beforeEach(() => {
mockStore.state.basePath = null;
mockStore.state.hasError = true;
});
it('renders error text', () => {
expect(findWidget().text()).toEqual('Failed to load codeclimate report');
});
it('renders a help icon with more information', () => {
expect(findWidget().html()).toContain('ic-question');
});
});
describe('on error', () => {
beforeEach(() => {
mockStore.state.hasError = true;
});
it('renders error text', () => {
expect(findWidget().text()).toContain('Failed to load codeclimate report');
});
it('does not render a help icon', () => {
expect(findWidget().html()).not.toContain('ic-question');
});
});
});
...@@ -211,6 +211,15 @@ export default { ...@@ -211,6 +211,15 @@ export default {
can_revert_on_current_merge_request: true, can_revert_on_current_merge_request: true,
can_cherry_pick_on_current_merge_request: true, can_cherry_pick_on_current_merge_request: true,
}, },
codeclimate: {
head_path: 'head.json',
base_path: 'base.json',
},
blob_path: {
base_path: 'blob_path',
head_path: 'blob_path',
},
codequality_help_path: 'code_quality.html',
target_branch_path: '/root/acets-app/branches/master', target_branch_path: '/root/acets-app/branches/master',
source_branch_path: '/root/acets-app/branches/daaaa', source_branch_path: '/root/acets-app/branches/daaaa',
conflict_resolution_ui_path: '/root/acets-app/-/merge_requests/22/conflicts', conflict_resolution_ui_path: '/root/acets-app/-/merge_requests/22/conflicts',
......
...@@ -609,6 +609,12 @@ describe('mrWidgetOptions', () => { ...@@ -609,6 +609,12 @@ describe('mrWidgetOptions', () => {
}); });
}); });
describe('code quality widget', () => {
it('renders the component', () => {
expect(vm.$el.querySelector('.js-codequality-widget')).toExist();
});
});
describe('pipeline for target branch after merge', () => { describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => { describe('with information for target branch pipeline', () => {
beforeEach(done => { beforeEach(done => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment