diff options
Diffstat (limited to 'app/assets/javascripts/reports/codequality_report')
10 files changed, 342 insertions, 0 deletions
diff --git a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue b/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue new file mode 100644 index 00000000000..0c758ee2b5c --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue @@ -0,0 +1,42 @@ +<script> +/** + * Renders Code quality body text + * Fixed: [name] in [link]:[line] + */ +import ReportLink from '~/reports/components/report_link.vue'; +import { STATUS_SUCCESS } from '~/reports/constants'; + +export default { + name: 'CodequalityIssueBody', + + components: { + ReportLink, + }, + props: { + status: { + type: String, + required: true, + }, + issue: { + type: Object, + required: true, + }, + }, + computed: { + isStatusSuccess() { + return this.status === STATUS_SUCCESS; + }, + }, +}; +</script> +<template> + <div class="report-block-list-issue-description gl-mt-2 gl-mb-2"> + <div class="report-block-list-issue-description-text"> + <template v-if="isStatusSuccess">{{ s__('ciReport|Fixed:') }}</template> + + {{ issue.name }} + </div> + + <report-link v-if="issue.path" :issue="issue" /> + </div> +</template> diff --git a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue new file mode 100644 index 00000000000..f3d5b1a80f8 --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue @@ -0,0 +1,83 @@ +<script> +import { mapState, mapActions, mapGetters } from 'vuex'; +import { componentNames } from '~/reports/components/issue_body'; +import { s__, sprintf } from '~/locale'; +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']), + }, + loadingText: sprintf(s__('ciReport|Loading %{reportName} report'), { + reportName: 'codeclimate', + }), + errorText: sprintf(s__('ciReport|Failed to load %{reportName} report'), { + reportName: 'codeclimate', + }), +}; +</script> +<template> + <report-section + :status="codequalityStatus" + :loading-text="$options.loadingText" + :error-text="$options.errorText" + :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> diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js new file mode 100644 index 00000000000..bf84d27b5ea --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/actions.js @@ -0,0 +1,30 @@ +import axios from '~/lib/utils/axios_utils'; +import * as types from './mutation_types'; +import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequality_comparison'; + +export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths); + +export const fetchReports = ({ state, dispatch, commit }) => { + commit(types.REQUEST_REPORTS); + + if (!state.basePath) { + return dispatch('receiveReportsError'); + } + return Promise.all([axios.get(state.headPath), axios.get(state.basePath)]) + .then(results => + doCodeClimateComparison( + parseCodeclimateMetrics(results[0].data, state.headBlobPath), + parseCodeclimateMetrics(results[1].data, state.baseBlobPath), + ), + ) + .then(data => dispatch('receiveReportsSuccess', data)) + .catch(() => dispatch('receiveReportsError')); +}; + +export const receiveReportsSuccess = ({ commit }, data) => { + commit(types.RECEIVE_REPORTS_SUCCESS, data); +}; + +export const receiveReportsError = ({ commit }) => { + commit(types.RECEIVE_REPORTS_ERROR); +}; diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js new file mode 100644 index 00000000000..5df58c7f85f --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/getters.js @@ -0,0 +1,58 @@ +import { LOADING, ERROR, SUCCESS } from '../../constants'; +import { sprintf, __, s__, n__ } from '~/locale'; + +export const hasCodequalityIssues = state => + Boolean(state.newIssues?.length || state.resolvedIssues?.length); + +export const codequalityStatus = state => { + if (state.isLoading) { + return LOADING; + } + if (state.hasError) { + return ERROR; + } + + return SUCCESS; +}; + +export const codequalityText = state => { + const { newIssues, resolvedIssues } = state; + const text = []; + + if (!newIssues.length && !resolvedIssues.length) { + text.push(s__('ciReport|No changes to code quality')); + } else { + 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 && resolvedIssues.length) { + text.push(__(' and')); + } + + if (newIssues.length) { + text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length)); + } + } + + return text.join(''); +}; + +export const codequalityPopover = state => { + if (state.headPath && !state.basePath) { + return { + title: s__('ciReport|Base pipeline codequality artifact not found'), + content: sprintf( + s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'), + { + linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`, + linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>', + }, + false, + ), + }; + } + return {}; +}; diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js new file mode 100644 index 00000000000..047964260ad --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export default initialState => + new Vuex.Store({ + actions, + getters, + mutations, + state: state(initialState), + }); diff --git a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/reports/codequality_report/store/mutation_types.js new file mode 100644 index 00000000000..c362c973ae1 --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_PATHS = 'SET_PATHS'; + +export const REQUEST_REPORTS = 'REQUEST_REPORTS'; +export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; +export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js new file mode 100644 index 00000000000..7ef4f3ce2db --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js @@ -0,0 +1,24 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_PATHS](state, paths) { + state.basePath = paths.basePath; + state.headPath = paths.headPath; + state.baseBlobPath = paths.baseBlobPath; + state.headBlobPath = paths.headBlobPath; + state.helpPath = paths.helpPath; + }, + [types.REQUEST_REPORTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_REPORTS_SUCCESS](state, data) { + state.hasError = false; + state.isLoading = false; + state.newIssues = data.newIssues; + state.resolvedIssues = data.resolvedIssues; + }, + [types.RECEIVE_REPORTS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, +}; diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/reports/codequality_report/store/state.js new file mode 100644 index 00000000000..38ab53b432e --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/state.js @@ -0,0 +1,15 @@ +export default () => ({ + basePath: null, + headPath: null, + + baseBlobPath: null, + headBlobPath: null, + + isLoading: false, + hasError: false, + + newIssues: [], + resolvedIssues: [], + + helpPath: null, +}); diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js new file mode 100644 index 00000000000..eba9e340c4e --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js @@ -0,0 +1,41 @@ +import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker'; + +export const parseCodeclimateMetrics = (issues = [], path = '') => { + return issues.map(issue => { + const parsedIssue = { + ...issue, + name: issue.description, + }; + + if (issue?.location?.path) { + let parseCodeQualityUrl = `${path}/${issue.location.path}`; + parsedIssue.path = issue.location.path; + + if (issue?.location?.lines?.begin) { + parsedIssue.line = issue.location.lines.begin; + parseCodeQualityUrl += `#L${issue.location.lines.begin}`; + } else if (issue?.location?.positions?.begin?.line) { + parsedIssue.line = issue.location.positions.begin.line; + parseCodeQualityUrl += `#L${issue.location.positions.begin.line}`; + } + + parsedIssue.urlPath = parseCodeQualityUrl; + } + + return parsedIssue; + }); +}; + +export const 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, + }); + }); +}; diff --git a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js new file mode 100644 index 00000000000..fc55602f95c --- /dev/null +++ b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js @@ -0,0 +1,28 @@ +import { differenceBy } from 'lodash'; + +const KEY_TO_FILTER_BY = '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: differenceBy(headIssues, baseIssues, KEY_TO_FILTER_BY), + resolvedIssues: differenceBy(baseIssues, headIssues, KEY_TO_FILTER_BY), + }); + + // eslint-disable-next-line no-restricted-globals + return self.close(); +}); |