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
path: root/app
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-08-03 16:24:48 +0300
committerPhil Hughes <me@iamphill.com>2018-08-03 16:24:48 +0300
commit8a9421429d15a805ef07b8200bcba551fe7314ff (patch)
tree175bdc21d5f7d0a4f029df3997a4830f8931c338 /app
parentbeda5ca507ba2c5ce1617be4c21d3a3076f25d3e (diff)
parent94981308a028a1e6c8996701f324d0e0c0339e73 (diff)
Merge branch '45318-junit-FE' into 'master'
Frontend code for "JUnit XML Test Summary In MR widget" See merge request gitlab-org/gitlab-ce!20936
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue116
-rw-r--r--app/assets/javascripts/reports/components/modal.vue73
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue44
-rw-r--r--app/assets/javascripts/reports/constants.js16
-rw-r--r--app/assets/javascripts/reports/store/actions.js27
-rw-r--r--app/assets/javascripts/reports/store/getters.js16
-rw-r--r--app/assets/javascripts/reports/store/index.js2
-rw-r--r--app/assets/javascripts/reports/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/reports/store/mutations.js21
-rw-r--r--app/assets/javascripts/reports/store/state.js33
-rw-r--r--app/assets/javascripts/reports/store/utils.js59
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issue_body.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issues_list.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/report_issues.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/summary_row.vue9
-rw-r--r--app/assets/stylesheets/framework/blocks.scss15
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss41
21 files changed, 522 insertions, 7 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..0fc84b4552a
--- /dev/null
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -0,0 +1,116 @@
+<script>
+ import { mapActions, mapGetters, mapState } from 'vuex';
+ import { s__ } from '~/locale';
+ import { componentNames } from '~/vue_shared/components/reports/issue_body';
+ 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 { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
+
+ export default {
+ name: 'GroupedTestReportsApp',
+ store: createStore(),
+ components: {
+ ReportSection,
+ SummaryRow,
+ IssuesList,
+ Modal,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ componentNames,
+ computed: {
+ ...mapState([
+ 'reports',
+ 'isLoading',
+ 'hasError',
+ 'summary',
+ ]),
+ ...mapState({
+ modalTitle: state => state.modal.title || '',
+ modalData: state => state.modal.data || {},
+ }),
+ ...mapGetters([
+ 'summaryStatus',
+ ]),
+ groupedSummaryText() {
+ if (this.isLoading) {
+ return s__('Reports|Test summary results are being parsed');
+ }
+
+ if (this.hasError) {
+ return s__('Reports|Test summary failed loading results');
+ }
+
+ return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
+ },
+ },
+ created() {
+ this.setEndpoint(this.endpoint);
+
+ this.fetchReports();
+ },
+ methods: {
+ ...mapActions(['setEndpoint', 'fetchReports']),
+ reportText(report) {
+ const summary = report.summary || {};
+ return reportTextBuilder(report.name, summary);
+ },
+ getReportIcon(report) {
+ return statusIcon(report.status);
+ },
+ shouldRenderIssuesList(report) {
+ return (
+ report.existing_failures.length > 0 ||
+ report.new_failures.length > 0 ||
+ report.resolved_failures > 0
+ );
+ },
+ },
+ };
+</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
+ v-if="shouldRenderIssuesList(report)"
+ :unresolved-issues="report.existing_failures"
+ :new-issues="report.new_failures"
+ :resolved-issues="report.resolved_failures"
+ :key="`issues-list-${i}`"
+ :component="$options.componentNames.TestIssueBody"
+ 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
new file mode 100644
index 00000000000..b2133714858
--- /dev/null
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -0,0 +1,73 @@
+<script>
+ import Modal from '~/vue_shared/components/gl_modal.vue';
+ import LoadingButton from '~/vue_shared/components/loading_button.vue';
+ import CodeBlock from '~/vue_shared/components/code_block.vue';
+ import { fieldTypes } from '../constants';
+
+ export default {
+ components: {
+ Modal,
+ LoadingButton,
+ CodeBlock,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ modalData: {
+ type: Object,
+ required: true,
+ },
+ },
+ fieldTypes,
+ };
+</script>
+<template>
+ <modal
+ id="modal-mrwidget-reports"
+ :header-title-text="title"
+ class="modal-security-report-dast modal-hide-footer"
+ >
+ <slot>
+ <div
+ v-for="(field, key, index) in modalData"
+ v-if="field.value"
+ :key="index"
+ class="row prepend-top-10 append-bottom-10"
+ >
+ <strong class="col-sm-2 text-right">
+ {{ field.text }}:
+ </strong>
+
+ <div class="col-sm-10 text-secondary">
+ <code-block
+ v-if="field.type === $options.fieldTypes.codeBock"
+ :code="field.value"
+ />
+
+ <template v-else-if="field.type === $options.fieldTypes.link">
+ <a
+ :href="field.value"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="js-modal-link"
+ >
+ {{ 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">
+ </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..cd443a49b52
--- /dev/null
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -0,0 +1,44 @@
+<script>
+ import { mapActions } from 'vuex';
+
+ export default {
+ name: 'TestIssueBody',
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ // failed || success
+ status: {
+ type: String,
+ required: true,
+ },
+ isNew: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ ...mapActions(['openModal']),
+ },
+ };
+</script>
+<template>
+ <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
+ <div class="report-block-list-issue-description-text">
+ <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>{{ 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..807ecb1039e
--- /dev/null
+++ b/app/assets/javascripts/reports/constants.js
@@ -0,0 +1,16 @@
+export const fieldTypes = {
+ codeBock: 'codeBlock',
+ link: 'link',
+ miliseconds: 'miliseconds',
+ text: 'text',
+};
+
+export const LOADING = 'LOADING';
+export const ERROR = 'ERROR';
+export const SUCCESS = 'SUCCESS';
+
+export const STATUS_FAILED = 'failed';
+export const STATUS_SUCCESS = 'success';
+export const ICON_WARNING = 'warning';
+export const ICON_SUCCESS = 'success';
+export const ICON_NOTFOUND = 'notfound';
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index 15c077b0fd8..acabcc1d193 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -1,7 +1,9 @@
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';
+import httpStatusCodes from '../../lib/utils/http_status';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
@@ -41,12 +43,19 @@ export const fetchReports = ({ state, dispatch }) => {
},
data: state.endpoint,
method: 'getReports',
- successCallback: ({ data }) => dispatch('receiveReportsSuccess', data),
+ successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', {
+ data, status,
+ }),
errorCallback: () => dispatch('receiveReportsError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
+ } else {
+ axios
+ .get(state.endpoint)
+ .then(({ data, status }) => dispatch('receiveReportsSuccess', { data, status }))
+ .catch(() => dispatch('receiveReportsError'));
}
Visibility.change(() => {
@@ -58,10 +67,22 @@ export const fetchReports = ({ state, dispatch }) => {
});
};
-export const receiveReportsSuccess = ({ commit }, response) =>
- commit(types.RECEIVE_REPORTS_SUCCESS, response);
+export const receiveReportsSuccess = ({ commit }, response) => {
+ // With 204 we keep polling and don't update the state
+ if (response.status === httpStatusCodes.OK) {
+ commit(types.RECEIVE_REPORTS_SUCCESS, response.data);
+ }
+};
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..95266194acb
--- /dev/null
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -0,0 +1,16 @@
+import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants';
+
+export const summaryStatus = state => {
+ if (state.isLoading) {
+ return LOADING;
+ }
+
+ if (state.hasError || state.status === STATUS_FAILED) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index af4f9688fb4..9d8f7dc3b74 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -1,6 +1,7 @@
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 +10,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..82bda31df5d 100644
--- a/app/assets/javascripts/reports/store/mutation_types.js
+++ b/app/assets/javascripts/reports/store/mutation_types.js
@@ -3,3 +3,5 @@ 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 d9d301826cf..e806d120b51 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -16,11 +16,32 @@ export default {
state.summary.resolved = response.summary.resolved;
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;
+
+ state.reports = [];
+ state.summary = {
+ total: 0,
+ resolved: 0,
+ failed: 0,
+ };
+ state.status = null;
+ },
+ [types.SET_ISSUE_MODAL_DATA](state, payload) {
+ state.modal.title = payload.issue.name;
+
+ Object.keys(payload.issue).forEach((key) => {
+ if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
+ state.modal.data[key] = {
+ ...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..4cab2e27a16 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,32 @@ 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..35632218269
--- /dev/null
+++ b/app/assets/javascripts/reports/store/utils.js
@@ -0,0 +1,59 @@
+import { sprintf, n__, s__ } from '~/locale';
+import {
+ STATUS_FAILED,
+ STATUS_SUCCESS,
+ ICON_WARNING,
+ ICON_SUCCESS,
+ ICON_NOTFOUND,
+} from '../constants';
+
+const textBuilder = results => {
+ const { failed, resolved, total } = results;
+
+ const failedString = failed
+ ? n__('%d failed test result', '%d failed test results', failed)
+ : null;
+ const resolvedString = resolved
+ ? n__('%d fixed test result', '%d fixed test results', resolved)
+ : null;
+ const totalString = total ? n__('out of %d total test', 'out of %d total tests', total) : null;
+
+ let resultsString = s__('Reports|no changed test results');
+
+ if (failed) {
+ if (resolved) {
+ resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), {
+ failedString,
+ resolvedString,
+ });
+ } else {
+ resultsString = failedString;
+ }
+ } else if (resolved) {
+ resultsString = resolvedString;
+ }
+
+ return `${resultsString} ${totalString}`;
+};
+
+export const summaryTextBuilder = (name = '', results = {}) => {
+ const resultsString = textBuilder(results);
+ return `${name} contained ${resultsString}`;
+};
+
+export const reportTextBuilder = (name = '', results = {}) => {
+ const resultsString = textBuilder(results);
+ return `${name} found ${resultsString}`;
+};
+
+export const statusIcon = status => {
+ if (status === STATUS_FAILED) {
+ return ICON_WARNING;
+ }
+
+ if (status === STATUS_SUCCESS) {
+ return ICON_SUCCESS;
+ }
+
+ return ICON_NOTFOUND;
+};
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..80593d1f34a 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: {
@@ -260,6 +262,10 @@ export default {
: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..672e5280b5e 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_reports_path;
+
this.setState(data);
}
diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue
new file mode 100644
index 00000000000..3cca7a86bef
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ name: 'CodeBlock',
+ props: {
+ code: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <pre class="code-block rounded">
+ <code class="d-block">{{ code }}</code>
+ </pre>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/vue_shared/components/reports/issue_body.js
index f2141e519da..54dfb7b16bf 100644
--- a/app/assets/javascripts/vue_shared/components/reports/issue_body.js
+++ b/app/assets/javascripts/vue_shared/components/reports/issue_body.js
@@ -1,3 +1,9 @@
-export const components = {};
+import TestIssueBody from '~/reports/components/test_issue_body.vue';
-export const componentNames = {};
+export const components = {
+ TestIssueBody,
+};
+
+export const componentNames = {
+ TestIssueBody: TestIssueBody.name,
+};
diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
index c01f77c2509..2545e84f932 100644
--- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
@@ -18,6 +18,11 @@ export default {
failed: STATUS_FAILED,
neutral: STATUS_NEUTRAL,
props: {
+ newIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
unresolvedIssues: {
type: Array,
required: false,
@@ -45,6 +50,15 @@ export default {
<div class="report-block-container">
<issues-block
+ v-if="newIssues.length"
+ :component="component"
+ :issues="newIssues"
+ class="js-mr-code-new-issues"
+ status="failed"
+ is-new
+ />
+
+ <issues-block
v-if="unresolvedIssues.length"
:component="component"
:issues="unresolvedIssues"
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 2d1f3d82234..1f13e555b31 100644
--- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
+ isNew: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
};
</script>
@@ -46,6 +51,7 @@ export default {
:is="component"
:issue="issue"
:status="issue.status || status"
+ :is-new="isNew"
/>
</li>
</ul>
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..28156d7c983 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,11 @@ export default {
{{ summary }}
</div>
- <popover :options="popoverOptions" />
+ <popover
+ v-if="popoverOptions"
+ :options="popoverOptions"
+ />
+
</div>
</div>
</template>
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 340fddd398b..7145a76db6d 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -353,3 +353,18 @@
.flex-right {
margin-left: auto;
}
+
+.code-block {
+ background: $black;
+ color: $gray-darkest;
+ white-space: pre;
+ overflow-x: auto;
+ font-size: 12px;
+ border: 0;
+ padding: $grid-size;
+
+ code {
+ background-color: inherit;
+ padding: inherit;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2d6dba52801..c9865610b78 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -469,3 +469,4 @@ img.emoji {
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
+.flex-align-self-center { align-self: center; }
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7bd0f0bf1e0..a355ceea7a0 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -15,6 +15,39 @@
}
}
+.mr-widget-border-top {
+ border-top: 1px solid $border-color;
+}
+
+.media-section {
+ @include media-breakpoint-down(md) {
+ align-items: flex-start;
+
+ .media-body {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ }
+
+ .code-text {
+ @include media-breakpoint-up(lg) {
+ align-self: center;
+ flex: 1;
+ }
+ }
+}
+
+.mr-widget-section {
+ .media {
+ align-items: center;
+ }
+
+ .code-text {
+ flex: 1;
+ }
+}
+
+
.mr-widget-heading {
position: relative;
border: 1px solid $border-color;
@@ -54,6 +87,14 @@
padding: 0;
}
+ .grouped-security-reports {
+ padding: 0;
+
+ > .media {
+ padding: $gl-padding;
+ }
+ }
+
form {
margin-bottom: 0;