Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/reports')
-rw-r--r--app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue30
-rw-r--r--app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue64
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js79
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js48
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/index.js16
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutations.js20
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/state.js28
-rw-r--r--app/assets/javascripts/reports/components/grouped_issues_list.vue93
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue15
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue3
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue2
-rw-r--r--app/assets/javascripts/reports/constants.js3
-rw-r--r--app/assets/javascripts/reports/store/mutations.js3
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;