summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2018-07-23 14:23:29 +0900
committerShinya Maeda <shinya@gitlab.com>2018-07-23 14:23:29 +0900
commit41a439c75983a8bd200e0728b231487ff6e1699c (patch)
treeb25df6509d4bb5ac2496eb132379109b38ddb43d
parentbc959fffb1a1e8937078a1399dad2f698534ac84 (diff)
parent7105b37a558acf22a23125cddfefc517c040a0fb (diff)
downloadgitlab-ce-41a439c75983a8bd200e0728b231487ff6e1699c.tar.gz
Merge branch 'artifact-format-v2-with-parser' of gitlab.com:gitlab-org/gitlab-ce into artifact-format-v2-with-parser
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue115
-rw-r--r--app/assets/javascripts/reports/components/modal.vue98
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue42
-rw-r--r--app/assets/javascripts/reports/constants.js10
-rw-r--r--app/assets/javascripts/reports/store/actions.js92
-rw-r--r--app/assets/javascripts/reports/store/getters.js21
-rw-r--r--app/assets/javascripts/reports/store/index.js16
-rw-r--r--app/assets/javascripts/reports/store/mutation_types.js9
-rw-r--r--app/assets/javascripts/reports/store/mutations.js47
-rw-r--r--app/assets/javascripts/reports/store/state.js65
-rw-r--r--app/assets/javascripts/reports/store/utils.js44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue7
-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.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/summary_row.vue8
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss12
-rw-r--r--changelogs/unreleased/45318-vuex-store.yml5
-rw-r--r--spec/javascripts/reports/store/actions_spec.js130
-rw-r--r--spec/javascripts/reports/store/mutations_spec.js101
19 files changed, 835 insertions, 2 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
new file mode 100644
index 00000000000..175424e9d73
--- /dev/null
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -0,0 +1,115 @@
+<script>
+ 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',
+ 'isCreatingNewIssue',
+ ]),
+
+ 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();
+ },
+ methods: {
+ ...mapActions(['setEndpoint', 'fetchReports']),
+ reportText(report) {
+ // TODO_SAM: Check with backend if summary is always present.
+ // if it is we may be able to remove this check.
+ const summary = report.summary || {};
+ return textBuilder(report.name, summary.failed, summary.resolved, summary.total);
+ },
+ getReportIcon(report) {
+ return statusIcon(report.status);
+ },
+ createIssue() {
+ // TODO_SAM
+ },
+ },
+ };
+</script>
+<template>
+ <report-section
+ :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
+ slot="body"
+ class="mr-widget-grouped-section report-block"
+ >
+ <template
+ v-for="(report, i) in reports"
+ >
+ <summary-row
+ :summary="reportText(report)"
+ :status-icon="getReportIcon(report)"
+ :key="`summary-row-${i}`"
+
+ />
+ <issues-list
+ :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"
+ :is-creating-new-issue="isCreatingNewIssue"
+ @createIssue="createIssue"
+ />
+ </div>
+ </report-section>
+</template>
+
diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue
new file mode 100644
index 00000000000..83edb6455a9
--- /dev/null
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -0,0 +1,98 @@
+<script>
+ import Modal from '~/vue_shared/components/gl_modal.vue';
+ import LoadingButton from '~/vue_shared/components/loading_button.vue';
+ import { fieldTypes } from '../constants';
+
+ export default {
+ components: {
+ Modal,
+ LoadingButton,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ modalData: {
+ type: Object,
+ required: true,
+ },
+ isCreatingNewIssue: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ fieldTypes,
+ methods: {
+ handleCreateIssueClick() {
+ // TODO_SAM: Check with Shynia how to handle this.
+ // I believe we've agreed we'd do this in a new iteration
+ // we may need to hide the footer always. - We probably need to provide some params
+ this.$emit('createIssue');
+ },
+ },
+ };
+</script>
+<template>
+ <modal
+ id="modal-mrwidget-reports"
+ :header-title-text="title"
+ :class="{ 'modal-hide-footer': false }"
+ class="modal-security-report-dast"
+ >
+ <slot>
+ <div
+ v-for="(field, key, index) in modalData"
+ v-if="field.value"
+ :key="index"
+ class="row prepend-top-10 append-bottom-10"
+ >
+ <label class="col-sm-2 text-right font-weight-bold">
+ {{ field.text }}:
+ </label>
+
+ <div class="col-sm-10 text-secondary">
+ <template v-if="field.type === $options.fieldTypes.codeBock">
+ <pre> {{ field.value }} </pre>
+ </template>
+
+ <template v-else-if="field.type === $options.fieldTypes.link">
+ <a
+ :href="field.value"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ field.value }}
+ </a>
+ </template>
+
+ <template v-else-if="field.type === $options.fieldTypes.miliseconds">
+ {{ field.value }} ms
+ </template>
+
+ <template v-else-if="field.type === $options.fieldTypes.text">
+ {{ field.value }}
+ </template>
+ </div>
+ </div>
+ </slot>
+ <div slot="footer">
+ <button
+ type="button"
+ class="btn btn-default"
+ data-dismiss="modal"
+ >
+ {{ __('Cancel' ) }}
+ </button>
+
+ <loading-button
+ v-if="canCreateIssuePermission"
+ :loading="isCreatingNewIssue"
+ :disabled="isCreatingNewIssue"
+ :label="__('Create issue')"
+ container-class="js-create-issue-btn btn btn-success btn-inverted"
+ @click="handleCreateIssueClick"
+ />
+ </div>
+ </modal>
+</template>
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..0b01bee4557
--- /dev/null
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -0,0 +1,42 @@
+<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
new file mode 100644
index 00000000000..882617c20f1
--- /dev/null
+++ b/app/assets/javascripts/reports/constants.js
@@ -0,0 +1,10 @@
+export const fieldTypes = {
+ codeBock: 'codeBlock',
+ link: 'link',
+ miliseconds: 'miliseconds',
+ text: 'text',
+};
+
+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
new file mode 100644
index 00000000000..c18885f23a4
--- /dev/null
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -0,0 +1,92 @@
+import Visibility from 'visibilityjs';
+import $ from 'jquery';
+import axios from '../../lib/utils/axios_utils';
+import Poll from '../../lib/utils/poll';
+import flash from '../../flash';
+import { s__ } from '../../locale';
+import * as types from './mutation_types';
+
+export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
+
+export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
+
+let eTagPoll;
+
+export const clearEtagPoll = () => {
+ eTagPoll = null;
+};
+
+export const stopPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+
+export const restartPolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+
+/**
+ * We need to poll the reports 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 parsins went wrong and we stop polling
+ */
+export const fetchReports = ({ state, dispatch }) => {
+ dispatch('requestReports');
+
+ eTagPoll = new Poll({
+ resource: {
+ getReports(endpoint) {
+ return axios.get(endpoint);
+ },
+ },
+ data: state.endpoint,
+ method: 'getReports',
+ successCallback: ({ data }) => dispatch('receiveReportsSuccess', data),
+ errorCallback: () => dispatch('receiveReportsError'),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ dispatch('restartPolling');
+ } else {
+ dispatch('stopPolling');
+ }
+ });
+};
+
+export const receiveReportsSuccess = ({ commit }, response) =>
+ commit(types.RECEIVE_REPORTS_SUCCESS, 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);
+
+export const createNewIssue = ({ state, dispatch }) => {
+ dispatch('requestCreateIssue');
+ return axios.post(state.modal.endpoint)
+ .then(() => dispatch('receiveCreateIssueSuccess'))
+ .catch(() => dispatch('receiveCreateIssueError'));
+};
+
+export const requestCreateIssue = ({ commit }) => commit(types.REQUEST_CREATE_ISSUE);
+export const receiveCreateIssueSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_ISSUE_SUCCESS);
+export const receiveCreateIssueError = ({ commit }) => {
+ flash(s__('Report|There was an error creating the issue. Please try again.'));
+ commit(types.RECEIVE_CREATE_ISSUE_ERROR);
+};
+
+// 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..ddc4b3d3e76
--- /dev/null
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -0,0 +1,21 @@
+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 isCreatingNewIssue = state => state.modal.isLoading;
+
+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
new file mode 100644
index 00000000000..1eb2960910b
--- /dev/null
+++ b/app/assets/javascripts/reports/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 () => 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
new file mode 100644
index 00000000000..6242c31b4e2
--- /dev/null
+++ b/app/assets/javascripts/reports/store/mutation_types.js
@@ -0,0 +1,9 @@
+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';
+export const REQUEST_CREATE_ISSUE = 'REQUEST_CREATE_ISSUE';
+export const RECEIVE_CREATE_ISSUE_SUCCESS = 'RECEIVE_CREATE_ISSUE_SUCCESS';
+export const RECEIVE_CREATE_ISSUE_ERROR = 'RECEIVE_CREATE_ISSUE_ERROR';
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
new file mode 100644
index 00000000000..120ec177666
--- /dev/null
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -0,0 +1,47 @@
+/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_ENDPOINT](state, endpoint) {
+ state.endpoint = endpoint;
+ },
+ [types.REQUEST_REPORTS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_REPORTS_SUCCESS](state, response) {
+
+ state.isLoading = false;
+
+ Vue.set(state.summary, 'total', response.summary.total);
+ Vue.set(state.summary, 'resolved', response.summary.resolved);
+ Vue.set(state.summary, 'failed', response.summary.failed);
+
+ state.status = response.status;
+ state.reports = response.suites;
+
+ },
+ [types.RECEIVE_REPORTS_ERROR](state) {
+ state.isLoading = false;
+ state.hasError = true;
+ },
+ [types.SET_ISSUE_MODAL_DATA](state, payload) {
+ Vue.set(state.modal, 'title', payload.issue.name);
+ Vue.set(state.modal, 'status', payload.status);
+
+ 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]);
+ }
+ });
+ },
+ [types.REQUEST_CREATE_ISSUE](state) {
+ Vue.set(state.modal, 'isLoading', true);
+ },
+ [types.RECEIVE_CREATE_ISSUE_SUCCESS](state) {
+ Vue.set(state.modal, 'isLoading', false);
+ },
+ [types.RECEIVE_CREATE_ISSUE_ERROR](state) {
+ Vue.set(state.modal, 'isLoading', false);
+ },
+};
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
new file mode 100644
index 00000000000..4254f839fd7
--- /dev/null
+++ b/app/assets/javascripts/reports/store/state.js
@@ -0,0 +1,65 @@
+import { s__ } from '~/locale';
+import { fieldTypes } from '../constants';
+
+export default () => ({
+ endpoint: null,
+
+ isLoading: false,
+ hasError: false,
+
+ status: null,
+
+ summary: {
+ total: 0,
+ resolved: 0,
+ failed: 0,
+ },
+
+ /**
+ * Each report will have the following format:
+ * {
+ * name: {String},
+ * summary: {
+ * total: {Number},
+ * resolved: {Number},
+ * failed: {Number},
+ * },
+ * new_failures: {Array.<Object>},
+ * resolved_failures: {Array.<Object>},
+ * existing_failures: {Array.<Object>},
+ * }
+ */
+ reports: [],
+
+ modal: {
+ title: null,
+
+ status: null,
+
+ isCreatingNewIssue: false,
+ hasError: false,
+
+ 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..23162c595b7
--- /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_SAM: 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..77b40076fd9 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,7 +261,12 @@ export default {
:key="deployment.id"
:deployment="deployment"
/>
+
<div class="mr-section-container">
+ <grouped-test-reports-app
+ v-if="mr.testResultsPath"
+ :endpoint="mr.testResultsPath"
+ />
<div class="mr-widget-section">
<component
:is="componentName"
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..bb78f3aed26 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;
+
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..47b2de8fcef 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,11 @@ 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 +73,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;
diff --git a/changelogs/unreleased/45318-vuex-store.yml b/changelogs/unreleased/45318-vuex-store.yml
new file mode 100644
index 00000000000..5ea89034bce
--- /dev/null
+++ b/changelogs/unreleased/45318-vuex-store.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Vuex store for reports section in MR widget
+merge_request: 20709
+author:
+type: added
diff --git a/spec/javascripts/reports/store/actions_spec.js b/spec/javascripts/reports/store/actions_spec.js
new file mode 100644
index 00000000000..fc2973de5bd
--- /dev/null
+++ b/spec/javascripts/reports/store/actions_spec.js
@@ -0,0 +1,130 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setEndpoint,
+ requestReports,
+ fetchReports,
+ stopPolling,
+ clearEtagPoll,
+ receiveReportsSuccess,
+ receiveReportsError,
+} from '~/reports/store/actions';
+import state from '~/reports/store/state';
+import * as types from '~/reports/store/mutation_types';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+
+describe('Reports Store Actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('setEndpoint', () => {
+ it('should commit SET_ENDPOINT mutation', done => {
+ testAction(
+ setEndpoint,
+ 'endpoint.json',
+ mockedState,
+ [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestReports', () => {
+ it('should commit REQUEST_REPORTS mutation', done => {
+ testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], [], done);
+ });
+ });
+
+ describe('fetchReports', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestReports and receiveReportsSuccess ', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, { summary: {}, suites: [{ name: 'rspec' }] });
+
+ testAction(
+ fetchReports,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReports',
+ },
+ {
+ payload: { summary: {}, suites: [{ name: 'rspec' }] },
+ type: 'receiveReportsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ });
+
+ it('dispatches requestReports and receiveReportsError ', done => {
+ testAction(
+ fetchReports,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReports',
+ },
+ {
+ type: 'receiveReportsError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveReportsSuccess', () => {
+ it('should commit RECEIVE_REPORTS_SUCCESS mutation', done => {
+ testAction(
+ receiveReportsSuccess,
+ { summary: {} },
+ mockedState,
+ [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReportsError', () => {
+ it('should commit RECEIVE_REPORTS_ERROR mutation', done => {
+ testAction(
+ receiveReportsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_REPORTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/reports/store/mutations_spec.js b/spec/javascripts/reports/store/mutations_spec.js
new file mode 100644
index 00000000000..3e0b15438c3
--- /dev/null
+++ b/spec/javascripts/reports/store/mutations_spec.js
@@ -0,0 +1,101 @@
+import state from '~/reports/store/state';
+import mutations from '~/reports/store/mutations';
+import * as types from '~/reports/store/mutation_types';
+
+describe('Reports Store Mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('SET_ENDPOINT', () => {
+ it('should set endpoint', () => {
+ mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json');
+ expect(stateCopy.endpoint).toEqual('endpoint.json');
+ });
+ });
+
+ describe('REQUEST_REPORTS', () => {
+ it('should set isLoading to true', () => {
+ mutations[types.REQUEST_REPORTS](stateCopy);
+ expect(stateCopy.isLoading).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_REPORTS_SUCCESS', () => {
+ const mockedResponse = {
+ summary: {
+ total: 14,
+ resolved: 0,
+ failed: 7,
+ },
+ suites: [
+ {
+ name: 'build:linux',
+ summary: {
+ total: 2,
+ resolved: 0,
+ failed: 1,
+ },
+ new_failures: [
+ {
+ name: 'StringHelper#concatenate when a is git and b is lab returns summary',
+ execution_time: 0.0092435,
+ system_output:
+ 'Failure/Error: is_expected.to eq(\'gitlab\')',
+ },
+ ],
+ resolved_failures: [
+ {
+ name: 'StringHelper#concatenate when a is git and b is lab returns summary',
+ execution_time: 0.009235,
+ system_output:
+ 'Failure/Error: is_expected.to eq(\'gitlab\')',
+ },
+ ],
+ existing_failures: [
+ {
+ name: 'StringHelper#concatenate when a is git and b is lab returns summary',
+ execution_time: 1232.08,
+ system_output:
+ 'Failure/Error: is_expected.to eq(\'gitlab\')',
+ },
+ ],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ mutations[types.RECEIVE_REPORTS_SUCCESS](stateCopy, mockedResponse);
+ });
+
+ it('should reset isLoading', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('should set summary counts', () => {
+ expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total);
+ expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved);
+ expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed);
+ });
+
+ it('should set reports', () => {
+ expect(stateCopy.reports).toEqual(mockedResponse.suites);
+ });
+ });
+
+ describe('RECEIVE_REPORTS_ERROR', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
+ });
+ it('should reset isLoading', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('should set hasError to true', () => {
+ expect(stateCopy.hasError).toEqual(true);
+ });
+
+ });
+});