diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
16 files changed, 311 insertions, 123 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index e8cc9b2eb2a..7cfc9431c2a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -69,7 +69,7 @@ export default { isCollapsible() { if (!this.isLoadingSummary && this.loadingState !== LOADING_STATES.collapsedError) { if (this.shouldCollapse) { - return this.shouldCollapse(); + return this.shouldCollapse(this.collapsedData); } return true; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue index 6475def461a..e435dc56503 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue @@ -1,12 +1,10 @@ <script> -import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; export default { components: { GlButton, - GlLoadingIcon, - GlIcon, }, props: { title: { @@ -32,7 +30,7 @@ export default { computed: { arrowIconName() { - return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down'; + return this.isCollapsed ? 'chevron-right' : 'chevron-down'; }, ariaLabel() { return this.isCollapsed ? __('Expand') : __('Collapse'); @@ -47,7 +45,7 @@ export default { </script> <template> <div class="mr-widget-extension"> - <div class="d-flex align-items-center pl-3"> + <div class="d-flex align-items-center pl-3 gl-py-3"> <div v-if="hasError" class="ci-widget media"> <div class="media-body"> <span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state"> @@ -57,16 +55,15 @@ export default { </div> <template v-else> - <button - class="btn-blank btn s32 square" - type="button" + <gl-button + class="gl-mr-3" + size="small" :aria-label="ariaLabel" - :disabled="isLoading" + :loading="isLoading" + :icon="arrowIconName" + category="tertiary" @click="toggleCollapsed" - > - <gl-loading-icon v-if="isLoading" size="sm" /> - <gl-icon v-else :name="arrowIconName" class="js-icon" /> - </button> + /> <template v-if="isCollapsed"> <slot name="header"></slot> <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue index c2a3ae361ca..20284c4a3d8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue @@ -19,20 +19,18 @@ export default { }, step3: { label: __('Step 3.'), - help: __( - 'Merge the feature branch into the target branch and fix any conflicts. %{linkStart}How do I fix them?%{linkEnd}', - ), + help: __('Resolve any conflicts. %{linkStart}How do I fix them?%{linkEnd}'), }, step4: { label: __('Step 4.'), - help: __('Push the target branch up to GitLab.'), + help: __('Push the source branch up to GitLab.'), }, }, copyCommands: __('Copy commands'), tip: __( - '%{strongStart}Tip:%{strongEnd} You can also check out merge requests locally. %{linkStart}Learn more.%{linkEnd}', + '%{strongStart}Tip:%{strongEnd} You can also %{linkStart}check out with merge request ID%{linkEnd}.', ), - title: __('Check out, review, and merge locally'), + title: __('Check out, review, and resolve locally'), }, components: { GlModal, @@ -93,21 +91,11 @@ export default { : `git fetch origin\ngit checkout -b ${this.escapedSourceBranch} ${escapedOriginBranch}`; }, mergeInfo2() { - return this.isFork - ? `git fetch origin\ngit checkout ${this.escapedTargetBranch}\ngit merge --no-ff ${this.escapedForkBranch}` - : `git fetch origin\ngit checkout ${this.escapedTargetBranch}\ngit merge --no-ff ${this.escapedSourceBranch}`; - }, - mergeInfo3() { - return this.canMerge - ? `git push origin ${this.escapedTargetBranch}` - : __('Note that pushing to GitLab requires write access to this repository.'); + return `git push origin ${this.escapedSourceBranch}`; }, escapedForkBranch() { return escapeShellString(`${this.sourceProjectPath}-${this.sourceBranch}`); }, - escapedTargetBranch() { - return escapeShellString(this.targetBranch); - }, escapedSourceBranch() { return escapeShellString(this.sourceBranch); }, @@ -145,6 +133,18 @@ export default { class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0" /> </div> + <p v-if="reviewingDocsPath"> + <gl-sprintf data-testid="docs-tip" :message="$options.i18n.tip"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + <template #link="{ content }"> + <gl-link class="gl-display-inline-block" :href="reviewingDocsPath" target="_blank">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </p> <p> <strong> @@ -164,14 +164,6 @@ export default { </template> </gl-sprintf> </p> - <div class="gl-display-flex"> - <pre class="gl-w-full" data-testid="how-to-merge-instructions">{{ mergeInfo2 }}</pre> - <clipboard-button - :text="mergeInfo2" - :title="$options.i18n.copyCommands" - class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0" - /> - </div> <p> <strong> {{ $options.i18n.steps.step4.label }} @@ -179,24 +171,12 @@ export default { {{ $options.i18n.steps.step4.help }} </p> <div class="gl-display-flex"> - <pre class="gl-w-full" data-testid="how-to-merge-instructions">{{ mergeInfo3 }}</pre> + <pre class="gl-w-full" data-testid="how-to-merge-instructions">{{ mergeInfo2 }}</pre> <clipboard-button - :text="mergeInfo3" + :text="mergeInfo2" :title="$options.i18n.copyCommands" class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0" /> </div> - <p v-if="reviewingDocsPath"> - <gl-sprintf data-testid="docs-tip" :message="$options.i18n.tip"> - <template #strong="{ content }"> - <strong>{{ content }}</strong> - </template> - <template #link="{ content }"> - <gl-link class="gl-display-inline-block" :href="reviewingDocsPath" target="_blank">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </p> </gl-modal> </template> 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/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue index 9a3555d3e11..f7d6f7b4345 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue @@ -76,17 +76,17 @@ export default { <div :class="{ 'gl-display-flex gl-align-items-center': actions.length, - 'gl-md-display-flex gl-align-items-center': !actions.length, + 'gl-md-display-flex gl-align-items-center gl-flex-wrap gl-gap-3': !actions.length, }" - class="media-body" + class="media-body gl-line-height-24" > <slot></slot> <div :class="{ - 'state-container-action-buttons gl-flex-direction-column gl-flex-wrap gl-justify-content-end': !actions.length, + 'state-container-action-buttons gl-flex-wrap gl-lg-justify-content-end': !actions.length, 'gl-md-pt-0 gl-pt-3': hasActionsSlot, }" - class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3" + class="gl-display-flex gl-font-size-0 gl-gap-3" > <slot name="actions"> <actions v-if="actions.length" :tertiary-buttons="actions" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 8e1b18c63a4..a5d982fe221 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -88,25 +88,24 @@ export default { </template> <template v-if="!isLoading && !state.shouldBeRebased" #actions> <gl-button - v-if="userPermissions.canMerge" + v-if="showResolveButton" + :href="mr.conflictResolutionPath" size="small" variant="confirm" - category="secondary" - data-testid="merge-locally-button" - class="js-check-out-modal-trigger gl-align-self-start" - :class="{ 'gl-mr-2': showResolveButton }" + class="gl-align-self-start" + data-testid="resolve-conflicts-button" > - {{ s__('mrWidget|Resolve locally') }} + {{ s__('mrWidget|Resolve conflicts') }} </gl-button> <gl-button - v-if="showResolveButton" - :href="mr.conflictResolutionPath" + v-if="userPermissions.canMerge" size="small" variant="confirm" - class="gl-mb-2 gl-md-mb-0 gl-align-self-start" - data-testid="resolve-conflicts-button" + category="secondary" + data-testid="merge-locally-button" + class="js-check-out-modal-trigger gl-align-self-start" > - {{ s__('mrWidget|Resolve conflicts') }} + {{ s__('mrWidget|Resolve locally') }} </gl-button> </template> </state-container> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 4ae4edf02c3..d687f0346c7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -179,27 +179,27 @@ export default { </template> <template v-if="!isLoading" #actions> <gl-button - v-if="showRebaseWithoutPipeline" :loading="isMakingRequest" variant="confirm" size="small" - category="secondary" - data-testid="rebase-without-ci-button" - class="gl-align-self-start gl-mr-2" - @click="rebaseWithoutCi" + data-qa-selector="mr_rebase_button" + data-testid="standard-rebase-button" + class="gl-align-self-start" + @click="rebase" > - {{ __('Rebase without pipeline') }} + {{ __('Rebase') }} </gl-button> <gl-button + v-if="showRebaseWithoutPipeline" :loading="isMakingRequest" variant="confirm" size="small" - data-qa-selector="mr_rebase_button" - data-testid="standard-rebase-button" - class="gl-mb-2 gl-md-mb-0 gl-align-self-start" - @click="rebase" + category="secondary" + data-testid="rebase-without-ci-button" + class="gl-align-self-start gl-mr-2" + @click="rebaseWithoutCi" > - {{ __('Rebase') }} + {{ __('Rebase without pipeline') }} </gl-button> </template> </state-container> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 01f9b4757a0..211fbba305f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -1,7 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; import { produce } from 'immer'; -import $ from 'jquery'; import { createAlert } from '~/flash'; import toast from '~/vue_shared/plugins/global_toast'; import { __ } from '~/locale'; @@ -111,7 +110,9 @@ export default { }, }) => { toast(__('Marked as ready. Merging is now allowed.')); - $('.merge-request .detail-page-description .title').text(title); + document.querySelector( + '.merge-request .detail-page-description .title', + ).textContent = title; if (!window.gon?.features?.realtimeMrStatusChange) { eventHub.$emit('MRWidgetUpdateRequested'); 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/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue index 543136dc659..b64f9c148d1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue @@ -79,8 +79,11 @@ export default { </script> <template> <div - class="gl-w-full gl-display-flex gl-align-items-baseline" - :class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }" + class="gl-w-full gl-display-flex" + :class="{ + 'gl-border-t gl-py-3 gl-pl-7 gl-align-items-baseline': level === 2, + 'gl-align-items-center': level === 3, + }" > <status-icon v-if="statusIconName && !header" 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..26c986884d3 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 %{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 394f8979a53..4f9bba1e0cb 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', @@ -12,28 +12,36 @@ export default { props: ['codeQuality', 'blobPath'], i18n, computed: { + shouldCollapse(data) { + const { newErrors, resolvedErrors, parsingInProgress } = data; + if (parsingInProgress || (newErrors.length === 0 && resolvedErrors.length === 0)) { + return false; + } + return true; + }, summary(data) { - const { newErrors, resolvedErrors, errorSummary, parsingInProgress } = data; - + const { newErrors, resolvedErrors, parsingInProgress } = data; if (parsingInProgress) { return i18n.loading; - } else if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) { + } 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 (errorSummary.resolved >= 1) { - return i18n.improvedCopy(i18n.singularReport(resolvedErrors)); - } else if (errorSummary.errored >= 1) { - return i18n.degradedCopy(i18n.singularReport(newErrors)); + } else if (resolvedErrors.length >= 1) { + return i18n.singularCopy(i18n.findings(resolvedErrors, codeQualityPrefixes.fixed)); + } else if (newErrors.length >= 1) { + return i18n.singularCopy(i18n.findings(newErrors, codeQualityPrefixes.new)); } return i18n.noChanges; }, statusIcon() { - if (this.collapsedData.errorSummary?.errored >= 1) { + if (this.collapsedData.newErrors.length >= 1) { return EXTENSION_ICONS.warning; + } else if (this.collapsedData.resolvedErrors.length >= 1) { + return EXTENSION_ICONS.success; } - return EXTENSION_ICONS.success; + return EXTENSION_ICONS.neutral; }, }, methods: { @@ -46,8 +54,6 @@ export default { parsingInProgress: status === HTTP_STATUS_NO_CONTENT, resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path), newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path), - existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path), - errorSummary: data.summary, }, }; }); 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> diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js index 5fd5950859b..c8d969e3adf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js @@ -1,6 +1,6 @@ import Visibility from 'visibilityjs'; import axios from '~/lib/utils/axios_utils'; -import httpStatusCodes from '~/lib/utils/http_status'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import Poll from '~/lib/utils/poll'; import * as types from './mutation_types'; @@ -63,7 +63,7 @@ export const fetchArtifacts = ({ state, dispatch }) => { export const receiveArtifactsSuccess = ({ commit }, response) => { // With 204 we keep polling and don't update the state - if (response.status === httpStatusCodes.OK) { + if (response.status === HTTP_STATUS_OK) { commit(types.RECEIVE_ARTIFACTS_SUCCESS, response.data); } }; |