summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/reports/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/reports/components')
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue8
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js9
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue57
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue85
-rw-r--r--app/assets/javascripts/reports/components/modal_open_name.vue33
-rw-r--r--app/assets/javascripts/reports/components/report_issues.vue59
-rw-r--r--app/assets/javascripts/reports/components/report_link.vue29
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue181
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue71
9 files changed, 528 insertions, 4 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 140475b4dfa..7b37f4e9a97 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,10 +1,10 @@
<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 { componentNames } from './issue_body';
+ import ReportSection from './report_section.vue';
+ import SummaryRow from './summary_row.vue';
+ import IssuesList from './issues_list.vue';
import Modal from './modal.vue';
import createStore from '../store';
import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
new file mode 100644
index 00000000000..8b5af263d50
--- /dev/null
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -0,0 +1,9 @@
+import TestIssueBody from './test_issue_body.vue';
+
+export const components = {
+ TestIssueBody,
+};
+
+export const componentNames = {
+ TestIssueBody: TestIssueBody.name,
+};
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
new file mode 100644
index 00000000000..85811698a37
--- /dev/null
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -0,0 +1,57 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import {
+ STATUS_FAILED,
+ STATUS_NEUTRAL,
+ STATUS_SUCCESS,
+} from '../constants';
+
+export default {
+ name: 'IssueStatusIcon',
+ components: {
+ Icon,
+ },
+ props: {
+ // failed || success
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ if (this.isStatusFailed) {
+ return 'status_failed_borderless';
+ } else if (this.isStatusSuccess) {
+ return 'status_success_borderless';
+ }
+
+ return 'status_created_borderless';
+ },
+ isStatusFailed() {
+ return this.status === STATUS_FAILED;
+ },
+ isStatusSuccess() {
+ return this.status === STATUS_SUCCESS;
+ },
+ isStatusNeutral() {
+ return this.status === STATUS_NEUTRAL;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="{
+ failed: isStatusFailed,
+ success: isStatusSuccess,
+ neutral: isStatusNeutral,
+ }"
+ class="report-block-list-icon"
+ >
+ <icon
+ :name="iconName"
+ :size="32"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
new file mode 100644
index 00000000000..dbb8848d1fa
--- /dev/null
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -0,0 +1,85 @@
+<script>
+import IssuesBlock from './report_issues.vue';
+import {
+ STATUS_SUCCESS,
+ STATUS_FAILED,
+ STATUS_NEUTRAL,
+} from '../constants';
+
+/**
+ * Renders block of issues
+ */
+
+export default {
+ components: {
+ IssuesBlock,
+ },
+ success: STATUS_SUCCESS,
+ failed: STATUS_FAILED,
+ neutral: STATUS_NEUTRAL,
+ props: {
+ newIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ unresolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ resolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ neutralIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ component: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <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"
+ :status="$options.failed"
+ class="js-mr-code-new-issues"
+ />
+
+ <issues-block
+ v-if="neutralIssues.length"
+ :component="component"
+ :issues="neutralIssues"
+ :status="$options.neutral"
+ class="js-mr-code-non-issues"
+ />
+
+ <issues-block
+ v-if="resolvedIssues.length"
+ :component="component"
+ :issues="resolvedIssues"
+ :status="$options.success"
+ class="js-mr-code-resolved-issues"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue
new file mode 100644
index 00000000000..4f81cee2a38
--- /dev/null
+++ b/app/assets/javascripts/reports/components/modal_open_name.vue
@@ -0,0 +1,33 @@
+<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>
+ <button
+ type="button"
+ class="btn-link btn-blank text-left break-link vulnerability-name-button"
+ @click="handleIssueClick()"
+ >
+ {{ issue.title }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_issues.vue
new file mode 100644
index 00000000000..884f55c8dec
--- /dev/null
+++ b/app/assets/javascripts/reports/components/report_issues.vue
@@ -0,0 +1,59 @@
+<script>
+import IssueStatusIcon from './issue_status_icon.vue';
+import { components, componentNames } from './issue_body';
+
+export default {
+ name: 'ReportIssues',
+ components: {
+ IssueStatusIcon,
+ ...components,
+ },
+ props: {
+ issues: {
+ type: Array,
+ required: true,
+ },
+ component: {
+ type: String,
+ required: false,
+ default: '',
+ validator: value => value === '' || Object.values(componentNames).includes(value),
+ },
+ // failed || success
+ status: {
+ type: String,
+ required: true,
+ },
+ isNew: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <ul class="report-block-list">
+ <li
+ v-for="(issue, index) in issues"
+ :class="{ 'is-dismissed': issue.isDismissed }"
+ :key="index"
+ class="report-block-list-issue"
+ >
+ <issue-status-icon
+ :status="issue.status || status"
+ class="append-right-5"
+ />
+
+ <component
+ v-if="component"
+ :is="component"
+ :issue="issue"
+ :status="issue.status || status"
+ :is-new="isNew"
+ />
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/components/report_link.vue b/app/assets/javascripts/reports/components/report_link.vue
new file mode 100644
index 00000000000..74d68f9f439
--- /dev/null
+++ b/app/assets/javascripts/reports/components/report_link.vue
@@ -0,0 +1,29 @@
+<script>
+export default {
+ name: 'ReportIssueLink',
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="report-block-list-issue-description-link">
+ in
+
+ <a
+ v-if="issue.urlPath"
+ :href="issue.urlPath"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="break-link"
+ >
+ {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
+ </a>
+ <template v-else>
+ {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
new file mode 100644
index 00000000000..dc609d6f90e
--- /dev/null
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -0,0 +1,181 @@
+<script>
+import { __ } from '~/locale';
+import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import Popover from '~/vue_shared/components/help_popover.vue';
+import IssuesList from './issues_list.vue';
+
+const LOADING = 'LOADING';
+const ERROR = 'ERROR';
+const SUCCESS = 'SUCCESS';
+
+export default {
+ name: 'ReportSection',
+ components: {
+ IssuesList,
+ StatusIcon,
+ Popover,
+ },
+ props: {
+ alwaysOpen: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ component: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ loadingText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ errorText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ successText: {
+ type: String,
+ required: true,
+ },
+ unresolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ resolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ neutralIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ infoText: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ },
+ hasIssues: {
+ type: Boolean,
+ required: true,
+ },
+ popoverOptions: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ isCollapsed: true,
+ };
+ },
+
+ computed: {
+ collapseText() {
+ return this.isCollapsed ? __('Expand') : __('Collapse');
+ },
+ isLoading() {
+ return this.status === LOADING;
+ },
+ loadingFailed() {
+ return this.status === ERROR;
+ },
+ isSuccess() {
+ return this.status === SUCCESS;
+ },
+ isCollapsible() {
+ return !this.alwaysOpen && this.hasIssues;
+ },
+ isExpanded() {
+ return this.alwaysOpen || !this.isCollapsed;
+ },
+ statusIconName() {
+ if (this.isLoading) {
+ return 'loading';
+ }
+ if (this.loadingFailed || this.unresolvedIssues.length || this.neutralIssues.length) {
+ return 'warning';
+ }
+ return 'success';
+ },
+ headerText() {
+ if (this.isLoading) {
+ return this.loadingText;
+ }
+
+ if (this.isSuccess) {
+ return this.successText;
+ }
+
+ if (this.loadingFailed) {
+ return this.errorText;
+ }
+
+ return '';
+ },
+ hasPopover() {
+ return Object.keys(this.popoverOptions).length > 0;
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.isCollapsed = !this.isCollapsed;
+ },
+ },
+};
+</script>
+<template>
+ <section class="media-section">
+ <div class="media">
+ <status-icon :status="statusIconName" />
+ <div class="media-body space-children d-flex flex-align-self-center">
+ <span class="js-code-text code-text">
+ {{ headerText }}
+
+ <popover
+ v-if="hasPopover"
+ :options="popoverOptions"
+ class="prepend-left-5"
+ />
+ </span>
+
+ <slot name="actionButtons"></slot>
+
+ <button
+ v-if="isCollapsible"
+ type="button"
+ class="js-collapse-btn btn float-right btn-sm"
+ @click="toggleCollapsed"
+ >
+ {{ collapseText }}
+ </button>
+ </div>
+ </div>
+
+ <div
+ v-if="hasIssues"
+ v-show="isExpanded"
+ class="js-report-section-container"
+ >
+ <slot name="body">
+ <issues-list
+ :unresolved-issues="unresolvedIssues"
+ :resolved-issues="resolvedIssues"
+ :neutral-issues="neutralIssues"
+ :component="component"
+ />
+ </slot>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
new file mode 100644
index 00000000000..4456d84c968
--- /dev/null
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -0,0 +1,71 @@
+<script>
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import Popover from '~/vue_shared/components/help_popover.vue';
+
+/**
+ * Renders the summary row for each report
+ *
+ * Used both in MR widget and Pipeline's view for:
+ * - Unit tests reports
+ * - Security reports
+ */
+
+export default {
+ name: 'ReportSummaryRow',
+ components: {
+ CiIcon,
+ LoadingIcon,
+ Popover,
+ },
+ props: {
+ summary: {
+ type: String,
+ required: true,
+ },
+ statusIcon: {
+ type: String,
+ required: true,
+ },
+ popoverOptions: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ iconStatus() {
+ return {
+ group: this.statusIcon,
+ icon: `status_${this.statusIcon}`,
+ };
+ },
+ },
+};
+</script>
+<template>
+ <div class="report-block-list-issue report-block-list-issue-parent">
+ <div class="report-block-list-icon append-right-10 prepend-left-5">
+ <loading-icon
+ v-if="statusIcon === 'loading'"
+ css-class="report-block-list-loading-icon"
+ />
+ <ci-icon
+ v-else
+ :status="iconStatus"
+ />
+ </div>
+
+ <div class="report-block-list-issue-description">
+ <div class="report-block-list-issue-description-text">
+ {{ summary }}
+ </div>
+
+ <popover
+ v-if="popoverOptions"
+ :options="popoverOptions"
+ />
+
+ </div>
+ </div>
+</template>