diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports.vue b/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports.vue index 728b4a7de2c47e61d7a6c17452d94c27645595d1..9c63081cd0b25af5ea42b9b39aa525473cd2535c 100644 --- a/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports.vue +++ b/ee/app/assets/javascripts/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports.vue @@ -1,5 +1,5 @@ <script> -import { GlBadge, GlButton } from '@gitlab/ui'; +import { GlBadge, GlButton, GlIcon } from '@gitlab/ui'; import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue'; import MrWidgetRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue'; @@ -7,6 +7,7 @@ import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; import SummaryHighlights from 'ee/vue_shared/security_reports/components/summary_highlights.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin'; import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants'; import { capitalizeFirstCharacter, convertToCamelCase } from '~/lib/utils/text_utility'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -28,10 +29,11 @@ export default { SecurityTrainingPromoWidget, GlBadge, GlButton, + GlIcon, DynamicScroller, DynamicScrollerItem, }, - mixins: [glFeatureFlagMixin()], + mixins: [glAbilitiesMixin(), glFeatureFlagMixin()], i18n, props: { mr: { @@ -317,6 +319,14 @@ export default { clearModalData() { this.modalData = null; }, + + isAiResolvable(vuln) { + return ( + vuln.ai_resolution_enabled && + this.glFeatures.resolveVulnerabilityInMr && + this.glAbilities.resolveVulnerabilityWithAi + ); + }, }, SEVERITY_LEVELS, widgetHelpPopover: { @@ -452,6 +462,14 @@ export default { <gl-badge v-if="isDismissed(vuln)" class="gl-ml-3">{{ $options.i18n.dismissed }}</gl-badge> + <gl-badge + v-if="isAiResolvable(vuln)" + variant="info" + class="gl-ml-3" + data-testid="ai-resolvable-badge" + > + <gl-icon :size="12" name="tanuki-ai" /> + </gl-badge> </template> </mr-widget-row> </dynamic-scroller-item> diff --git a/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports_spec.js b/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports_spec.js index eae879cc42cac4df5065e0a53dd20899294eb97d..f6cc44140cdb770709e43082bc27a524edb6b3d1 100644 --- a/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports_spec.js +++ b/ee/spec/frontend/vue_merge_request_widget/widgets/security_reports/mr_widget_security_reports_spec.js @@ -275,6 +275,84 @@ describe('MR Widget Security Reports', () => { expect(findDismissedBadge().text()).toBe('Dismissed'); }); + it.each` + resolveVulnerabilityWithAi | aiResolutionEnabled | shouldShowAiBadge + ${true} | ${true} | ${true} + ${false} | ${true} | ${false} + ${true} | ${false} | ${false} + `( + 'with the resolveVulnerabilityWithAi ability set to "$resolveVulnerabilityWithAi" and the vulnerability has ai_resolution_enabled set to "$aiResolutionEnabled" it should show the AI-Badge: "$shouldShowAiBadge"', + async ({ resolveVulnerabilityWithAi, aiResolutionEnabled, shouldShowAiBadge }) => { + await createComponentAndExpandWidget({ + mockDataFn: () => + mockWithData({ + findings: { + sast: { + added: [ + { + uuid: '1', + severity: 'critical', + name: 'Password leak', + state: 'dismissed', + ai_resolution_enabled: aiResolutionEnabled, + }, + ], + }, + }, + }), + provide: { + glAbilities: { + resolveVulnerabilityWithAi, + }, + glFeatures: { + resolveVulnerabilityInMr: true, + }, + }, + }); + + expect(wrapper.findByTestId('ai-resolvable-badge').exists()).toBe(shouldShowAiBadge); + }, + ); + + it.each` + resolveVulnerabilityWithAi | aiResolutionEnabled + ${true} | ${true} + ${false} | ${true} + ${true} | ${false} + `( + 'with the "resolveVulnerabilityInMr" feature flag set to "false" it should never show the AI-Badge', + async ({ resolveVulnerabilityWithAi, aiResolutionEnabled }) => { + await createComponentAndExpandWidget({ + mockDataFn: () => + mockWithData({ + findings: { + sast: { + added: [ + { + uuid: '1', + severity: 'critical', + name: 'Password leak', + state: 'dismissed', + ai_resolution_enabled: aiResolutionEnabled, + }, + ], + }, + }, + }), + provide: { + glAbilities: { + resolveVulnerabilityWithAi, + }, + glFeatures: { + resolveVulnerabilityInMr: false, + }, + }, + }); + + expect(wrapper.findByTestId('ai-resolvable-badge').exists()).toBe(false); + }, + ); + it('should mount the widget component', async () => { await createComponentWithData();