summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2018-07-20 18:54:06 +0100
committerFilipa Lacerda <filipa@gitlab.com>2018-07-20 18:54:06 +0100
commit5cfe99006ed539fbc9e0a184b09af4be63e6d91b (patch)
treec48db7c2e0d2f6d1e168a3ddf17963fe8ae713f1
parent8e74f14c26b9df6a4fdc4fb79322b13b06c3c509 (diff)
downloadgitlab-ce-5cfe99006ed539fbc9e0a184b09af4be63e6d91b.tar.gz
Render grouped test report in MR widget
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue120
-rw-r--r--app/assets/javascripts/reports/components/modal.vue27
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue40
-rw-r--r--app/assets/javascripts/reports/constants.js56
-rw-r--r--app/assets/javascripts/reports/store/actions.js9
-rw-r--r--app/assets/javascripts/reports/store/getters.js20
-rw-r--r--app/assets/javascripts/reports/store/index.js3
-rw-r--r--app/assets/javascripts/reports/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/reports/store/mutations.js10
-rw-r--r--app/assets/javascripts/reports/store/state.js31
-rw-r--r--app/assets/javascripts/reports/store/utils.js44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/report_issues.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/summary_row.vue8
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss12
16 files changed, 292 insertions, 111 deletions
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 016806b84c3..c3f775013eb 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,40 +1,77 @@
<script>
-import ReportSection from '~/vue_shared/components/reports/report_section.vue';
-import SummaryRow from '~/vue_shared/components/reports/summary_row.vue';
-import IssuesList from '~/vue_shared/components/reports/issues_list.vue';
-import { mapActions } from 'vuex';
-
-export default {
- name: 'GroupedTestReportsApp',
- // store: createStore(),
- components: {
- ReportSection,
- SummaryRow,
- IssuesList,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
+ import { mapActions, mapGetters } from 'vuex';
+ import { s__ } from '~/locale';
+ import ReportSection from '~/vue_shared/components/reports/report_section.vue';
+ import SummaryRow from '~/vue_shared/components/reports/summary_row.vue';
+ import IssuesList from '~/vue_shared/components/reports/issues_list.vue';
+ import Modal from './modal.vue';
+ import createStore from '../store';
+ import { textBuilder, statusIcon } from '../store/utils';
+
+ export default {
+ name: 'GroupedTestReportsApp',
+ store: createStore(),
+ components: {
+ ReportSection,
+ SummaryRow,
+ IssuesList,
+ Modal,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters([
+ 'reports',
+ 'summaryStatus',
+ 'isLoading',
+ 'hasError',
+ 'summaryCounts',
+ 'modalTitle',
+ 'modalData',
+ ]),
+
+ groupedSummaryText() {
+ if (this.isLoading) {
+ return s__('Reports|Test summary results are being parsed');
+ }
+
+ if (this) {
+ return s__('Reports|Test summary failed loading results');
+ }
+
+ const { failed, total, resolved } = this.summaryCounts;
+
+ return textBuilder(s__('Reports|Test summary'), failed, resolved, total);
+ },
+ },
+ created() {
+ this.setEndpoint(this.endpoint);
+
+ this.fetchReports();
},
- },
- created() {
- this.setEndpoint(this.endpoint);
-
- this.fetchReports();
- },
- methods: {
- ...mapActions(['setEndpoint', 'fetchReports'])
- },
-}
+ methods: {
+ ...mapActions(['setEndpoint', 'fetchReports']),
+ reportText(report) {
+ const summary = report.summary || {}; // TODO_SAM: Check with backend if summary is always present. if it is we may be able to remove this check.
+ return textBuilder(report.name, summary.failed, summary.resolved, summary.total);
+ },
+ getReportIcon(report) {
+ return statusIcon(report.status);
+ },
+ },
+ };
</script>
<template>
<report-section
- :status=""
- :success-text=""
- :loading-text=""
- :error-text=""
- :has-issues="true"
+ :status="summaryStatus"
+ :success-text="groupedSummaryText"
+ :loading-text="groupedSummaryText"
+ :error-text="groupedSummaryText"
+ :has-issues="reports.length > 0"
class="mr-widget-border-top grouped-security-reports"
>
<div
@@ -43,20 +80,27 @@ export default {
>
<template
v-for="(report, i) in reports"
- :key="i"
>
<summary-row
- :summary=""
- :status-icon=""
+ :summary="reportText(report)"
+ :status-icon="getReportIcon(report)"
+ :key="`summary-row-${i}`"
+
/>
<issues-list
- :unresolved-issues="sast.newIssues"
- :resolved-issues="sast.resolvedIssues"
- :all-issues="sast.allIssues"
+ :unresolved-issues="report.new_failures"
+ :resolved-issues="report.resolved_failures"
+ :all-issues="report.existing_failures"
+ :key="`issues-list-${i}`"
type="test"
class="report-block-group-list"
/>
</template>
+
+ <modal
+ :title="modalTitle"
+ :modal-data="modalData"
+ />
</div>
</report-section>
</template>
diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue
index 61004399aa4..3eb7094f166 100644
--- a/app/assets/javascripts/reports/components/modal.vue
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -20,24 +20,15 @@
type: Object,
required: true,
},
- actions: {
- type: Array,
- required: true,
- },
- },
-
- computed: {
- shouldRenderFooterSection() {
- return this.actions.length > 0;
- },
},
+ fieldTypes,
};
</script>
<template>
<modal
- id="modal-mrwidget-security-issue"
+ id="modal-mrwidget-reports"
:header-title-text="title"
- :class="{ 'modal-hide-footer': !shouldRenderFooterSection }"
+ :class="{ 'modal-hide-footer': false }"
class="modal-security-report-dast"
>
<slot>
@@ -52,25 +43,25 @@
</label>
<div class="col-sm-10 text-secondary">
- <template v-if="field.type === fieldTypes.codeBlock">
- <code>{{field.value}}</code>
+ <template v-if="field.type === $options.fieldTypes.codeBock">
+ <pre> {{ field.value }} </pre>
</template>
- <template v-else-if="field.type === fieldTypes.link">
+ <template v-else-if="field.type === $options.fieldTypes.link">
<a
:href="field.value"
target="_blank"
rel="noopener noreferrer"
>
- {{ field.value}}
+ {{ field.value }}
</a>
</template>
- <template v-else-if="field.type === fieldTypes.miliseconds">
+ <template v-else-if="field.type === $options.fieldTypes.miliseconds">
{{ field.value }} ms
</template>
- <template v-else-if="field.type === fieldTypes.text">
+ <template v-else-if="field.type === $options.fieldTypes.text">
{{ field.value }}
</template>
</div>
diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue
new file mode 100644
index 00000000000..21dd1840807
--- /dev/null
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -0,0 +1,40 @@
+<script>
+ import { mapActions } from 'vuex';
+
+ export default {
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ // failed || success
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['openModal']),
+ handleIssueClick() {
+ const { issue, status, openModal } = this;
+ openModal({ issue, status });
+ },
+ },
+ };
+</script>
+<template>
+ <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
+ <div class="report-block-list-issue-description-text">
+ <!-- TODO_SAM: I've duplicated this from modal_open_name, because it's expecting a title and we have name
+ Not sure if it's worth refactring now, or if it's ok to leave it duplciated and fix it when we move the components around
+ -->
+ <button
+ type="button"
+ class="btn-link btn-blank text-left break-link vulnerability-name-button"
+ @click="handleIssueClick()"
+ >
+ {{ issue.name }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index 36306f33153..882617c20f1 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -1,5 +1,3 @@
-import { s__ } from '~/locale';
-
export const fieldTypes = {
codeBock: 'codeBlock',
link: 'link',
@@ -7,54 +5,6 @@ export const fieldTypes = {
text: 'text',
};
-/**
- * Map of data show in modalbox
- *
- * Each key will only be rendered if `value` is present.
- *
- * Keys in this map have the same format as the backend json response.
- *
- * When the user clicks on the issue name, in modal_open_name,
- * the store will set the modal data based on this map
- * and the keys available in the store.
- */
-export const modalData = {
- class: {
- value: null,
- text: s__('Reports|Class'),
- type: fieldTypes.link,
- },
- execution_time: {
- value: null,
- text: s__('Reports|Execution time'),
- type: fieldTypes.miliseconds,
- },
- failure: {
- value: null,
- text: s__('Reports|Failure'),
- type: fieldTypes.codeBock,
- },
- system_output: {
- value: null,
- text: s__('Reports|System output'),
- type: fieldTypes.codeBock,
- },
-};
-
-export const modalActions = [
- {
- title: s__('Reports|Cancel'),
- attributes: {
- type: 'button',
- class: 'btn btn-default',
- 'data-dismiss': 'modal',
- },
- },
- {
- title: s__('Reports|Create Issue'),
- attributes: {
- type: 'button',
- class: 'btn btn-success btn-inverted',
- },
- }
-];
+export const LOADING = 'LOADING';
+export const ERROR = 'ERROR';
+export const SUCCESS = 'SUCCESS';
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index 6ce0a00a03e..66d579b7556 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -1,4 +1,5 @@
import Visibility from 'visibilityjs';
+import $ from 'jquery';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
@@ -63,5 +64,13 @@ export const receiveReportsSuccess = ({ commit }, response) =>
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
+export const openModal = ({ dispatch }, payload) => {
+ dispatch('setModalData', payload);
+
+ $('#modal-mrwidget-reports').modal('show');
+};
+
+export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js
new file mode 100644
index 00000000000..8d3efa0b2bf
--- /dev/null
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -0,0 +1,20 @@
+import { LOADING, ERROR, SUCCESS } from '../constants';
+
+export const reports = state => state.reports;
+export const summaryCounts = state => state.summary;
+export const isLoading = state => state.isLoading;
+export const hasError = state => state.hasError;
+export const modalTitle = state => state.modal.title || '';
+export const modalData = state => state.modal.data || {};
+
+export const summaryStatus = state => {
+ if (state.isLoading) {
+ return LOADING;
+ }
+
+ if (state.hasError) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index af4f9688fb4..1eb2960910b 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -1,6 +1,8 @@
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';
@@ -9,5 +11,6 @@ Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
+ getters,
state: state(),
});
diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js
index 77722974c45..599d4862dfe 100644
--- a/app/assets/javascripts/reports/store/mutation_types.js
+++ b/app/assets/javascripts/reports/store/mutation_types.js
@@ -3,3 +3,4 @@ export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
+export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index 3851f2f5799..4ff9b10f28c 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -17,6 +17,7 @@ export default {
Vue.set(state.summary, 'resolved', response.summary.resolved);
Vue.set(state.summary, 'failed', response.summary.failed);
+ state.status = response.status;
state.reports = response.suites;
},
@@ -24,4 +25,13 @@ export default {
state.isLoading = false;
state.hasError = true;
},
+ [types.SET_ISSUE_MODAL_DATA](state, payload) {
+ Vue.set(state.modal, 'title', payload.issue.name);
+
+ Object.keys(payload.issue).forEach((key) => {
+ if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
+ Vue.set(state.modal.data[key], 'value', payload.issue[key]);
+ }
+ });
+ },
};
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
index 97f9d0a6859..acb937351e4 100644
--- a/app/assets/javascripts/reports/store/state.js
+++ b/app/assets/javascripts/reports/store/state.js
@@ -1,9 +1,14 @@
+import { s__ } from '~/locale';
+import { fieldTypes } from '../constants';
+
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
+ status: null,
+
summary: {
total: 0,
resolved: 0,
@@ -25,4 +30,30 @@ export default () => ({
* }
*/
reports: [],
+
+ modal: {
+ title: null,
+ data: {
+ class: {
+ value: null,
+ text: s__('Reports|Class'),
+ type: fieldTypes.link,
+ },
+ execution_time: {
+ value: null,
+ text: s__('Reports|Execution time'),
+ type: fieldTypes.miliseconds,
+ },
+ failure: {
+ value: null,
+ text: s__('Reports|Failure'),
+ type: fieldTypes.codeBock,
+ },
+ system_output: {
+ value: null,
+ text: s__('Reports|System output'),
+ type: fieldTypes.codeBock,
+ },
+ },
+ },
});
diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js
new file mode 100644
index 00000000000..f921bb8ad2d
--- /dev/null
+++ b/app/assets/javascripts/reports/store/utils.js
@@ -0,0 +1,44 @@
+import { sprintf, n__, s__ } from '~/locale';
+
+export const textBuilder = (name = '', failed = 0, resolved = 0, total = 0) => {
+ if (failed === 0 && resolved === 0) {
+ // TODO: Check with dimitrie
+ return sprintf(s__('Reports|%{name} found no tests'), { name });
+ }
+
+ const text = [name];
+
+ if (failed > 0 && resolved === 0) {
+ text.push(n__('found %d failed test', 'found %d failed tests', failed));
+ }
+
+ if (failed > 0 && resolved > 0) {
+ text.push(
+ `${n__('found %d failed test', 'found %d failed tests', failed)} ${n__(
+ 'and %d fixed test',
+ 'and %d fixed tests',
+ resolved,
+ )}`,
+ );
+ }
+
+ if (failed === 0 && resolved > 0) {
+ text.push(n__('found %d fixed test', 'found %d fixed tests', resolved));
+ }
+
+ if (failed === 0 && resolved === 0) {
+ text.push(s__('found no tests'));
+ }
+
+ text.push(sprintf('out of %{total} total', { total }));
+
+ return text.join(' ');
+};
+
+export const statusIcon = (status) => {
+ if (status === 'failed') {
+ return 'warning';
+ }
+
+ return 'success';
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index b5de3dd6d73..f356236fd11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -36,6 +36,7 @@ import {
notify,
SourceBranchRemovalStatus,
} from './dependencies';
+import GroupedTestReportsApp from '~/reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
export default {
@@ -68,6 +69,7 @@ export default {
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus,
+ GroupedTestReportsApp,
},
props: {
mrData: {
@@ -259,6 +261,12 @@ export default {
:key="deployment.id"
:deployment="deployment"
/>
+
+ <grouped-test-reports-app
+ v-if="mr.testResultsPath"
+ :endpoint="mr.testResultsPath"
+ />
+
<div class="mr-section-container">
<div class="mr-widget-section">
<component
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e84c436905d..d96f96c5df6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -108,6 +108,8 @@ export default class MergeRequestStore {
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
+ this.testResultsPath = data.test_results_path || 'foo';
+
this.setState(data);
}
diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
index ecffb02a3a0..5ce3ece4cc5 100644
--- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
@@ -1,10 +1,12 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
+import Issue from '~/reports/components/test_issue_body.vue';
export default {
name: 'ReportIssues',
components: {
Icon,
+ Issue,
},
props: {
issues: {
@@ -40,6 +42,10 @@ export default {
isStatusNeutral() {
return this.status === 'neutral';
},
+ isTypeTest() {
+ // TODO: Remove this. It's needed because of the EE port of this. Ideally there would be no type here.
+ return this.type === 'test';
+ },
},
};
</script>
@@ -66,6 +72,12 @@ export default {
/>
</div>
+ <issue
+ v-if="isTypeTest"
+ :issue="issue"
+ :status="status"
+ />
+
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue
index 997bad960e2..b85103d95f2 100644
--- a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue
@@ -29,7 +29,8 @@ export default {
},
popoverOptions: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
},
computed: {
@@ -60,7 +61,10 @@ export default {
{{ summary }}
</div>
- <popover :options="popoverOptions" />
+ <popover
+ v-if="popoverOptions"
+ :options="popoverOptions"
+ />
</div>
</div>
</template>
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index c8349a4ef79..d61c7eb6a0e 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -15,6 +15,10 @@
}
}
+.mr-widget-border-top {
+ border-top: 1px solid $border-color;
+}
+
.mr-widget-heading {
position: relative;
border: 1px solid $border-color;
@@ -54,6 +58,14 @@
padding: 0;
}
+ .grouped-security-reports {
+ padding: 0;
+
+ > .media {
+ padding: $gl-padding;
+ }
+ }
+
form {
margin-bottom: 0;