diff options
Diffstat (limited to 'app/assets/javascripts/reports')
14 files changed, 387 insertions, 22 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 6aae9195be1..653dcced98b 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 @@ -26,18 +26,11 @@ export default { * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation. * Here we simply split the string on `.` and get the code in the 5th position */ - if (this.issue.code === undefined) { - return null; - } - - return this.issue.code.split('.')[4] || null; + return this.issue.code?.split('.')[4]; }, learnMoreUrl() { - if (this.parsedTECHSCode === null) { - return 'https://www.w3.org/TR/WCAG20-TECHS/Overview.html'; - } - - return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode}.html`; + // eslint-disable-next-line @gitlab/require-i18n-strings + return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode || 'Overview'}.html`; }, }, }; @@ -52,10 +45,19 @@ export default { > {{ s__('AccessibilityReport|New') }} </div> - {{ issue.name }} - <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{ - s__('AccessibilityReport|Learn More') - }}</gl-link> + <div> + {{ + sprintf( + s__( + 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}', + ), + { code: issue.code }, + ) + }} + <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{ + s__('AccessibilityReport|Learn More') + }}</gl-link> + </div> {{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }} </div> </div> diff --git a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue new file mode 100644 index 00000000000..6f8ddd01df8 --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue @@ -0,0 +1,64 @@ +<script> +import { mapActions, mapGetters } from 'vuex'; +import { componentNames } from '~/reports/components/issue_body'; +import ReportSection from '~/reports/components/report_section.vue'; +import IssuesList from '~/reports/components/issues_list.vue'; +import createStore from './store'; + +export default { + name: 'GroupedAccessibilityReportsApp', + store: createStore(), + components: { + ReportSection, + IssuesList, + }, + props: { + endpoint: { + type: String, + required: true, + }, + }, + componentNames, + computed: { + ...mapGetters([ + 'summaryStatus', + 'groupedSummaryText', + 'shouldRenderIssuesList', + 'unresolvedIssues', + 'resolvedIssues', + 'newIssues', + ]), + }, + created() { + this.setEndpoint(this.endpoint); + + this.fetchReport(); + }, + methods: { + ...mapActions(['fetchReport', 'setEndpoint']), + }, +}; +</script> +<template> + <report-section + :status="summaryStatus" + :success-text="groupedSummaryText" + :loading-text="groupedSummaryText" + :error-text="groupedSummaryText" + :has-issues="shouldRenderIssuesList" + class="mr-widget-section grouped-security-reports mr-report" + > + <template #body> + <div class="mr-widget-grouped-section report-block"> + <issues-list + v-if="shouldRenderIssuesList" + :unresolved-issues="unresolvedIssues" + :new-issues="newIssues" + :resolved-issues="resolvedIssues" + :component="$options.componentNames.AccessibilityIssueBody" + class="report-block-group-list" + /> + </div> + </template> + </report-section> +</template> diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js new file mode 100644 index 00000000000..446cfd79984 --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/store/actions.js @@ -0,0 +1,79 @@ +import Visibility from 'visibilityjs'; +import Poll from '~/lib/utils/poll'; +import httpStatusCodes from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; +import * as types from './mutation_types'; + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); + +/** + * We need to poll the report endpoint while they are being parsed in the Backend. + * This can take up to one minute. + * + * Poll.js will handle etag response. + * While http status code is 204, it means it's parsing, and we'll keep polling + * When http status code is 200, it means parsing is done, we can show the results & stop polling + * When http status code is 500, it means parsing went wrong and we stop polling + */ +export const fetchReport = ({ state, dispatch, commit }) => { + commit(types.REQUEST_REPORT); + + eTagPoll = new Poll({ + resource: { + getReport(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.endpoint, + method: 'getReport', + successCallback: ({ status, data }) => dispatch('receiveReportSuccess', { status, data }), + errorCallback: () => dispatch('receiveReportError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + axios + .get(state.endpoint) + .then(({ status, data }) => dispatch('receiveReportSuccess', { status, data })) + .catch(() => dispatch('receiveReportError')); + } + + Visibility.change(() => { + if (!Visibility.hidden() && state.isLoading) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveReportSuccess = ({ commit, dispatch }, { status, data }) => { + if (status === httpStatusCodes.OK) { + commit(types.RECEIVE_REPORT_SUCCESS, data); + // Stop polling since we have the information already parsed and it won't be changing + dispatch('stopPolling'); + } +}; + +export const receiveReportError = ({ commit, dispatch }) => { + commit(types.RECEIVE_REPORT_ERROR); + dispatch('stopPolling'); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js new file mode 100644 index 00000000000..9aff427e644 --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js @@ -0,0 +1,48 @@ +import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants'; +import { s__, n__ } from '~/locale'; + +export const groupedSummaryText = state => { + if (state.isLoading) { + return s__('Reports|Accessibility scanning results are being parsed'); + } + + if (state.hasError) { + return s__('Reports|Accessibility scanning failed loading results'); + } + + const numberOfResults = state.report?.summary?.errored || 0; + if (numberOfResults === 0) { + return s__('Reports|Accessibility scanning detected no issues for the source branch only'); + } + + return n__( + 'Reports|Accessibility scanning detected %d issue for the source branch only', + 'Reports|Accessibility scanning detected %d issues for the source branch only', + numberOfResults, + ); +}; + +export const summaryStatus = state => { + if (state.isLoading) { + return LOADING; + } + + if (state.hasError || state.status === STATUS_FAILED) { + return ERROR; + } + + return SUCCESS; +}; + +export const shouldRenderIssuesList = state => + Object.values(state.report).some(x => Array.isArray(x) && x.length > 0); + +// We could just map state, but we're going to iterate in the future +// to add notes and warnings to these issue lists, so I'm going to +// keep these as getters +export const unresolvedIssues = state => state.report.existing_errors; +export const resolvedIssues = state => state.report.resolved_errors; +export const newIssues = state => state.report.new_errors; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js new file mode 100644 index 00000000000..047964260ad --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_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/accessibility_report/store/mutation_types.js b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js new file mode 100644 index 00000000000..22e2330e1ea --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; + +export const REQUEST_REPORT = 'REQUEST_REPORT'; +export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS'; +export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR'; diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutations.js b/app/assets/javascripts/reports/accessibility_report/store/mutations.js new file mode 100644 index 00000000000..20d3e5be9a3 --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/store/mutations.js @@ -0,0 +1,20 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.REQUEST_REPORT](state) { + state.isLoading = true; + }, + [types.RECEIVE_REPORT_SUCCESS](state, report) { + state.hasError = false; + state.isLoading = false; + state.report = report; + }, + [types.RECEIVE_REPORT_ERROR](state) { + state.isLoading = false; + state.hasError = true; + state.report = {}; + }, +}; diff --git a/app/assets/javascripts/reports/accessibility_report/store/state.js b/app/assets/javascripts/reports/accessibility_report/store/state.js new file mode 100644 index 00000000000..2a4cefea5e6 --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/store/state.js @@ -0,0 +1,28 @@ +export default (initialState = {}) => ({ + endpoint: initialState.endpoint || '', + + isLoading: initialState.isLoading || false, + hasError: initialState.hasError || false, + + /** + * Report will have the following format: + * { + * status: {String}, + * summary: { + * total: {Number}, + * resolved: {Number}, + * errored: {Number}, + * }, + * existing_errors: {Array.<Object>}, + * existing_notes: {Array.<Object>}, + * existing_warnings: {Array.<Object>}, + * new_errors: {Array.<Object>}, + * new_notes: {Array.<Object>}, + * new_warnings: {Array.<Object>}, + * resolved_errors: {Array.<Object>}, + * resolved_notes: {Array.<Object>}, + * resolved_warnings: {Array.<Object>}, + * } + */ + report: initialState.report || {}, +}); diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/reports/components/grouped_issues_list.vue new file mode 100644 index 00000000000..97587636644 --- /dev/null +++ b/app/assets/javascripts/reports/components/grouped_issues_list.vue @@ -0,0 +1,93 @@ +<script> +import { s__ } from '~/locale'; +import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; +import ReportItem from '~/reports/components/report_item.vue'; + +export default { + components: { + ReportItem, + SmartVirtualList, + }, + props: { + component: { + type: String, + required: false, + default: '', + }, + resolvedIssues: { + type: Array, + required: false, + default: () => [], + }, + unresolvedIssues: { + type: Array, + required: false, + default: () => [], + }, + resolvedHeading: { + type: String, + required: false, + default: s__('ciReport|Fixed'), + }, + unresolvedHeading: { + type: String, + required: false, + default: s__('ciReport|New'), + }, + }, + groups: ['unresolved', 'resolved'], + typicalReportItemHeight: 32, + maxShownReportItems: 20, + computed: { + groups() { + return this.$options.groups + .map(group => ({ + name: group, + issues: this[`${group}Issues`], + heading: this[`${group}Heading`], + })) + .filter(({ issues }) => issues.length > 0); + }, + listLength() { + // every group has a header which is rendered as a list item + const groupsCount = this.groups.length; + const issuesCount = this.groups.reduce( + (totalIssues, { issues }) => totalIssues + issues.length, + 0, + ); + + return groupsCount + issuesCount; + }, + }, +}; +</script> + +<template> + <smart-virtual-list + :length="listLength" + :remain="$options.maxShownReportItems" + :size="$options.typicalReportItemHeight" + class="report-block-container" + wtag="ul" + wclass="report-block-list" + > + <template v-for="(group, groupIndex) in groups"> + <h2 + :key="group.name" + :data-testid="`${group.name}Heading`" + :class="[groupIndex > 0 ? 'mt-2' : 'mt-0']" + class="h5 mb-1" + > + {{ group.heading }} + </h2> + <report-item + v-for="(issue, issueIndex) in group.issues" + :key="`${group.name}-${issue.name}-${group.name}-${issueIndex}`" + :issue="issue" + :show-report-section-status-icon="false" + :component="component" + status="none" + /> + </template> + </smart-virtual-list> +</template> 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 88d174f96ed..0f7a0e60dc0 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { s__ } from '~/locale'; +import { sprintf, s__ } from '~/locale'; import { componentNames } from './issue_body'; import ReportSection from './report_section.vue'; import SummaryRow from './summary_row.vue'; @@ -52,8 +52,17 @@ export default { methods: { ...mapActions(['setEndpoint', 'fetchReports']), reportText(report) { - const summary = report.summary || {}; - return reportTextBuilder(report.name, summary); + const { name, summary } = report || {}; + + if (report.status === 'error') { + return sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }); + } + + if (!report.name) { + return s__('Reports|An error occured while loading report'); + } + + return reportTextBuilder(name, summary); }, getReportIcon(report) { return statusIcon(report.status); diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 62a9338b864..d79e3ddd798 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -8,7 +8,6 @@ export default { Icon, }, props: { - // failed || success status: { type: String, required: true, @@ -27,7 +26,7 @@ export default { return 'status_success_borderless'; } - return 'status_created_borderless'; + return 'dash'; }, isStatusFailed() { return this.status === STATUS_FAILED; diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index 20b0c52dbda..68956fc6d2b 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -167,7 +167,7 @@ export default { <div class="media"> <status-icon :status="statusIconName" :size="24" class="align-self-center" /> <div class="media-body d-flex flex-align-self-center align-items-center"> - <div class="js-code-text code-text"> + <div data-testid="report-section-code-text" class="js-code-text code-text"> <div> {{ headerText }} <slot :name="slotName"></slot> diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js index 1845b51e6b2..b3905cbfcfb 100644 --- a/app/assets/javascripts/reports/constants.js +++ b/app/assets/javascripts/reports/constants.js @@ -22,3 +22,6 @@ export const status = { ERROR: 'ERROR', SUCCESS: 'SUCCESS', }; + +export const ACCESSIBILITY_ISSUE_ERROR = 'error'; +export const ACCESSIBILITY_ISSUE_WARNING = 'warning'; diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index 68f6de3a7ee..35ab72bf694 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -8,8 +8,7 @@ export default { state.isLoading = true; }, [types.RECEIVE_REPORTS_SUCCESS](state, response) { - // Make sure to clean previous state in case it was an error - state.hasError = false; + state.hasError = response.suites.some(suite => suite.status === 'error'); state.isLoading = false; |