diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-10 15:09:05 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-10 15:09:05 +0300 |
commit | fbf183eebe154eea4734f80975dd403f08173398 (patch) | |
tree | 4a91625645e09eea05e38dba28b1e849bde59ec2 /app/assets/javascripts/vue_merge_request_widget | |
parent | 14b71b2795e7765989101241ee89d7bfa55bd838 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
7 files changed, 231 insertions, 29 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue index 2683956e603..ecf08f78f57 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue @@ -6,7 +6,15 @@ export default { }; }, updated() { - this.hasChildren = this.$scopedSlots.default?.()?.some((c) => c.tag); + this.hasChildren = this.checkSlots(); + }, + mounted() { + this.hasChildren = this.checkSlots(); + }, + methods: { + checkSlots() { + return this.$scopedSlots.default?.()?.some((c) => c.tag); + }, }, }; </script> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue index 18aa85484ea..5db5f1f8dcf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue @@ -1,5 +1,11 @@ <script> export default { + components: { + MrSecurityWidget: () => + import( + '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue' + ), + }, props: { mr: { type: Object, @@ -8,7 +14,9 @@ export default { }, computed: { widgets() { - return [].filter((w) => w); + return [window.gon?.features?.refactorSecurityExtension && 'MrSecurityWidget'].filter( + (w) => w, + ); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index cdf35033021..7343c98938c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -42,7 +42,8 @@ export default { */ value: { type: Object, - required: true, + required: false, + default: () => ({}), }, loadingText: { type: String, @@ -56,7 +57,8 @@ export default { }, fetchCollapsedData: { type: Function, - required: true, + required: false, + default: undefined, }, fetchExpandedData: { type: Function, @@ -119,6 +121,12 @@ export default { required: false, default: null, }, + // When this is provided, the widget will display an error message in the summary section. + hasError: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -138,8 +146,17 @@ export default { summaryStatusIcon() { return this.summaryError ? this.$options.failedStatusIcon : this.statusIconName; }, + hasActionButtons() { + return this.actionButtons.length > 0 || Boolean(this.$scopedSlots['action-buttons']); + }, }, watch: { + hasError: { + handler(newValue) { + this.summaryError = newValue ? this.errorText : null; + }, + immediate: true, + }, isLoading(newValue) { this.$emit('is-loading', newValue); }, @@ -154,7 +171,9 @@ export default { this.telemetryHub?.viewed(); try { - await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED); + if (this.fetchCollapsedData) { + await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED); + } } catch { this.summaryError = this.errorText; } @@ -258,7 +277,7 @@ export default { v-if="helpPopover" icon="information-o" :options="helpPopover.options" - :class="{ 'gl-mr-3': actionButtons.length > 0 }" + :class="{ 'gl-mr-3': hasActionButtons }" > <template v-if="helpPopover.content"> <p @@ -275,12 +294,14 @@ export default { > </template> </help-popover> - <action-buttons - v-if="actionButtons.length > 0" - :widget="widgetName" - :tertiary-buttons="actionButtons" - @clickedAction="onActionClick" - /> + <slot name="action-buttons"> + <action-buttons + v-if="actionButtons.length > 0" + :widget="widgetName" + :tertiary-buttons="actionButtons" + @clickedAction="onActionClick" + /> + </slot> </div> <div v-if="isCollapsible" diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js index 03af21a5019..363fa1b6a5d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js @@ -1,4 +1,9 @@ -import { n__, s__, sprintf } from '~/locale'; +import { n__, s__, __, sprintf } from '~/locale'; + +export const codeQualityPrefixes = { + fixed: 'fixed', + new: 'new', +}; export const i18n = { label: s__('ciReport|Code Quality'), @@ -7,25 +12,23 @@ export const i18n = { noChanges: s__(`ciReport|Code Quality hasn't changed.`), prependText: s__(`ciReport|in`), fixed: s__(`ciReport|Fixed`), - pluralReport: (errors) => + findings: (errors, prefix) => sprintf( n__( - '%{strong_start}%{errors}%{strong_end} point', - '%{strong_start}%{errors}%{strong_end} points', + '%{strong_start}%{errors}%{strong_end} %{prefix} finding', + '%{strong_start}%{errors}%{strong_end} %{prefix} findings', errors.length, ), { errors: errors.length, + prefix, }, false, ), - singularReport: (errors) => n__('%d point', '%d points', errors.length), improvementAndDegradationCopy: (improvement, degradation) => - sprintf( - s__(`ciReport|Code Quality improved on ${improvement} and degraded on ${degradation}.`), - ), - improvedCopy: (improvements) => - sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)), - degradedCopy: (degradations) => - sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)), + sprintf(__('Code Quality scans found %{degradation} and fixed %{improvement}.'), { + improvement, + degradation, + }), + singularCopy: (findings) => sprintf(__('Code Quality scans found %{findings}.'), { findings }), }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js index 11bc2983f0f..23f83b8d6cc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js @@ -4,7 +4,7 @@ import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/consta import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status'; import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; -import { i18n } from './constants'; +import { i18n, codeQualityPrefixes } from './constants'; export default { name: 'WidgetCodeQuality', @@ -25,13 +25,13 @@ export default { return i18n.loading; } else if (newErrors.length >= 1 && resolvedErrors.length >= 1) { return i18n.improvementAndDegradationCopy( - i18n.pluralReport(resolvedErrors), - i18n.pluralReport(newErrors), + i18n.findings(resolvedErrors, codeQualityPrefixes.fixed), + i18n.findings(newErrors, codeQualityPrefixes.new), ); } else if (resolvedErrors.length >= 1) { - return i18n.improvedCopy(i18n.singularReport(resolvedErrors)); + return i18n.singularCopy(i18n.findings(resolvedErrors, codeQualityPrefixes.fixed)); } else if (newErrors.length >= 1) { - return i18n.degradedCopy(i18n.singularReport(newErrors)); + return i18n.singularCopy(i18n.findings(newErrors, codeQualityPrefixes.new)); } return i18n.noChanges; }, diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql new file mode 100644 index 00000000000..c12e4d1febb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql @@ -0,0 +1,28 @@ +query securityReportsDownloadPaths( + $projectPath: ID! + $iid: String! + $reportTypes: [SecurityReportTypeEnum!] +) { + project(fullPath: $projectPath) { + id + mergeRequest(iid: $iid) { + id + headPipeline { + id + jobs(securityReportTypes: $reportTypes) { + nodes { + id + name + artifacts { + # eslint-disable-next-line @graphql-eslint/require-id-when-available + nodes { + downloadPath + fileType + } + } + } + } + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue new file mode 100644 index 00000000000..f0b20adc5cf --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue @@ -0,0 +1,134 @@ +<script> +import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; +import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { s__, sprintf } from '~/locale'; +import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants'; +import securityReportMergeRequestDownloadPathsQuery from './graphql/security_report_merge_request_download_paths.query.graphql'; + +export default { + name: 'WidgetSecurityReportsCE', + components: { + MrWidget, + GlDropdown, + GlDropdownItem, + }, + directives: { + GlTooltip, + }, + i18n: { + apiError: s__( + 'SecurityReports|Failed to get security report information. Please reload the page or try again later.', + ), + scansHaveRun: s__('SecurityReports|Security scans have run'), + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + data() { + return { + hasError: false, + }; + }, + reportTypes: ['sast', 'secret_detection'], + apollo: { + reportArtifacts: { + query: securityReportMergeRequestDownloadPathsQuery, + variables() { + return { + projectPath: this.mr.targetProjectFullPath, + iid: String(this.mr.iid), + reportTypes: this.$options.reportTypes.map((r) => r.toUpperCase()), + }; + }, + update(data) { + const artifacts = []; + + (data?.project?.mergeRequest?.headPipeline?.jobs?.nodes || []).forEach((reportType) => { + reportType.artifacts?.nodes.forEach((artifact) => { + if (artifact.fileType !== 'TRACE') { + artifacts.push({ + name: reportType.name, + id: reportType.id, + path: artifact.downloadPath, + }); + } + }); + }); + + return artifacts; + }, + error() { + this.hasError = true; + }, + }, + }, + computed: { + artifacts() { + return this.reportArtifacts || []; + }, + }, + methods: { + handleIsLoading(value) { + this.isLoading = value; + }, + + artifactText({ name }) { + return sprintf(s__('SecurityReports|Download %{artifactName}'), { + artifactName: name, + }); + }, + }, + widgetHelpPopover: { + options: { title: s__('ciReport|Security scan results') }, + content: { + text: s__( + 'ciReport|New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch.', + ), + learnMorePath: helpPagePath('user/application_security/index', { + anchor: 'view-security-scan-information-in-merge-requests', + }), + }, + }, + icons: EXTENSION_ICONS, +}; +</script> + +<template> + <mr-widget + :has-error="hasError" + :error-text="$options.i18n.apiError" + :status-icon-name="$options.icons.warning" + :widget-name="$options.name" + :is-collapsible="false" + :help-popover="$options.widgetHelpPopover" + :summary="$options.i18n.scansHaveRun" + @is-loading="handleIsLoading" + > + <template v-if="artifacts.length > 0" #action-buttons> + <div class="gl-ml-3"> + <gl-dropdown + v-gl-tooltip + icon="download" + size="small" + category="tertiary" + variant="confirm" + right + > + <gl-dropdown-item + v-for="artifact in artifacts" + :key="artifact.path" + :href="artifact.path" + :data-testid="`download-${artifact.name}`" + download + > + {{ artifactText(artifact) }} + </gl-dropdown-item> + </gl-dropdown> + </div> + </template> + </mr-widget> +</template> |