diff options
Diffstat (limited to 'app/assets/javascripts/reports')
17 files changed, 390 insertions, 15 deletions
diff --git a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue index 653dcced98b..ed4f3c4e0fe 100644 --- a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue +++ b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue @@ -36,13 +36,9 @@ export default { }; </script> <template> - <div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> + <div class="report-block-list-issue-description gl-mt-2 gl-mb-2"> <div ref="accessibility-issue-description" class="report-block-list-issue-description-text"> - <div - v-if="isNew" - ref="accessibility-issue-is-new-badge" - class="badge badge-danger append-right-5" - > + <div v-if="isNew" ref="accessibility-issue-is-new-badge" class="badge badge-danger gl-mr-2"> {{ s__('AccessibilityReport|New') }} </div> <div> @@ -55,7 +51,7 @@ export default { ) }} <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{ - s__('AccessibilityReport|Learn More') + s__('AccessibilityReport|Learn more') }}</gl-link> </div> {{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }} 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(); +}); diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index a670cad5f9f..b8a8cb940e7 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -6,7 +6,9 @@ import ReportSection from './report_section.vue'; import SummaryRow from './summary_row.vue'; import IssuesList from './issues_list.vue'; import Modal from './modal.vue'; +import { GlButton } from '@gitlab/ui'; import createStore from '../store'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; export default { @@ -17,12 +19,19 @@ export default { SummaryRow, IssuesList, Modal, + GlButton, }, + mixins: [glFeatureFlagsMixin()], props: { endpoint: { type: String, required: true, }, + pipelinePath: { + type: String, + required: false, + default: '', + }, }, componentNames, computed: { @@ -43,6 +52,12 @@ export default { return summaryTextBuilder(s__('Reports|Test summary'), this.summary); }, + testTabURL() { + return `${this.pipelinePath}/test_report`; + }, + showViewFullReport() { + return Boolean(this.glFeatures.junitPipelineView) && this.pipelinePath.length; + }, }, created() { this.setEndpoint(this.endpoint); @@ -98,6 +113,16 @@ export default { :has-issues="reports.length > 0" class="mr-widget-section grouped-security-reports mr-report" > + <template v-if="showViewFullReport" #actionButtons> + <gl-button + :href="testTabURL" + icon="external-link" + data-testid="group-test-reports-full-link" + class="gl-mr-3" + > + {{ s__('ciReport|View full report') }} + </gl-button> + </template> <template #body> <div class="mr-widget-grouped-section report-block"> <template v-for="(report, i) in reports"> diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js index e106e60951b..1e6dc4f8b78 100644 --- a/app/assets/javascripts/reports/components/issue_body.js +++ b/app/assets/javascripts/reports/components/issue_body.js @@ -1,12 +1,15 @@ import TestIssueBody from './test_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 = { AccessibilityIssueBody, + CodequalityIssueBody, TestIssueBody, }; export const componentNames = { AccessibilityIssueBody: AccessibilityIssueBody.name, + CodequalityIssueBody: CodequalityIssueBody.name, TestIssueBody: TestIssueBody.name, }; diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue index 51062cd7928..1b47d03aa01 100644 --- a/app/assets/javascripts/reports/components/report_item.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -52,7 +52,7 @@ export default { v-if="showReportSectionStatusIcon" :status="status" :status-icon-size="statusIconSize" - class="append-right-default" + class="gl-mr-3" /> <component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" /> diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index 68956fc6d2b..63af8a5a9ac 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -91,6 +91,11 @@ export default { required: false, default: undefined, }, + shouldEmitToggleEvent: { + type: Boolean, + required: false, + default: false, + }, }, data() { @@ -157,6 +162,9 @@ export default { }, methods: { toggleCollapsed() { + if (this.shouldEmitToggleEvent) { + this.$emit('toggleEvent'); + } this.isCollapsed = !this.isCollapsed; }, }, @@ -171,7 +179,7 @@ export default { <div> {{ headerText }} <slot :name="slotName"></slot> - <popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" /> + <popover v-if="hasPopover" :options="popoverOptions" class="gl-ml-2" /> </div> <slot name="subHeading"></slot> </div> diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index b9fc902cd3a..3232c0edf96 100644 --- a/app/assets/javascripts/reports/components/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -21,7 +21,8 @@ export default { props: { summary: { type: String, - required: true, + required: false, + default: '', }, statusIcon: { type: String, @@ -45,7 +46,7 @@ export default { </script> <template> <div class="report-block-list-issue report-block-list-issue-parent align-items-center"> - <div class="report-block-list-icon append-right-default"> + <div class="report-block-list-icon gl-mr-3"> <gl-loading-icon v-if="statusIcon === 'loading'" css-class="report-block-list-loading-icon" @@ -58,8 +59,8 @@ export default { class="report-block-list-issue-description-text" data-testid="test-summary-row-description" > - {{ summary - }}<span v-if="popoverOptions" class="text-nowrap" + <slot name="summary">{{ summary }}</slot + ><span v-if="popoverOptions" class="text-nowrap" > <popover v-if="popoverOptions" :options="popoverOptions" class="align-top" /> </span> </div> diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index c41238070b1..4e0631740d8 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -25,14 +25,14 @@ export default { }; </script> <template> - <div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> + <div class="report-block-list-issue-description gl-mt-2 gl-mb-2"> <div class="report-block-list-issue-description-text" data-testid="test-issue-body-description"> <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" @click="openModal({ issue })" > - <div v-if="isNew" class="badge badge-danger append-right-5">{{ s__('New') }}</div> + <div v-if="isNew" class="badge badge-danger gl-mr-2">{{ s__('New') }}</div> {{ issue.name }} </button> </div> |