Commit b2bb5691 authored by Mark Florian's avatar Mark Florian Committed by David O'Regan

Implement security reports upgrade popover

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/273425.
parent 39847335
<script>
import { GlButton, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
GlIcon,
GlLink,
GlPopover,
},
props: {
helpPath: {
type: String,
required: true,
},
discoverProjectSecurityPath: {
type: String,
required: false,
default: '',
},
},
i18n: {
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
upgradeToManageVulnerabilities: s__('SecurityReports|Upgrade to manage vulnerabilities'),
upgradeToInteract: s__(
'SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI.',
),
},
};
</script>
<template>
<span v-if="discoverProjectSecurityPath">
<gl-button
ref="discoverProjectSecurity"
icon="information-o"
category="tertiary"
:aria-label="$options.i18n.upgradeToManageVulnerabilities"
/>
<gl-popover
:target="() => $refs.discoverProjectSecurity.$el"
:title="$options.i18n.upgradeToManageVulnerabilities"
placement="top"
triggers="click blur"
>
{{ $options.i18n.upgradeToInteract }}
<gl-link :href="discoverProjectSecurityPath" target="_blank" class="gl-font-sm">{{
__('Learn more')
}}</gl-link>
</gl-popover>
</span>
<gl-link v-else target="_blank" :href="helpPath" :aria-label="$options.i18n.securityReportsHelp">
<gl-icon name="question" />
</gl-link>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { GlLink, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
......@@ -8,6 +8,7 @@ import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import Api from '~/api';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
import store from './store';
......@@ -23,10 +24,10 @@ import { extractSecurityReportArtifacts } from './utils';
export default {
store,
components: {
GlIcon,
GlLink,
GlSprintf,
ReportSection,
HelpIcon,
SecurityReportDownloadDropdown,
SecuritySummary,
},
......@@ -44,6 +45,11 @@ export default {
type: String,
required: true,
},
discoverProjectSecurityPath: {
type: String,
required: false,
default: '',
},
sastComparisonPath: {
type: String,
required: false,
......@@ -64,6 +70,11 @@ export default {
required: false,
default: 0,
},
canDiscoverProjectSecurity: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -231,7 +242,6 @@ export default {
downloadFromPipelineTab: s__(
'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
......@@ -248,14 +258,10 @@ export default {
<span :key="slot">
<security-summary :message="groupedSummaryText" />
<gl-link
target="_blank"
data-testid="help"
:href="securityReportsDocsPath"
:aria-label="$options.i18n.securityReportsHelp"
>
<gl-icon name="question" />
</gl-link>
<help-icon
:help-path="securityReportsDocsPath"
:discover-project-security-path="discoverProjectSecurityPath"
/>
</span>
</template>
......@@ -300,14 +306,10 @@ export default {
</template>
</gl-sprintf>
<gl-link
target="_blank"
data-testid="help"
:href="securityReportsDocsPath"
:aria-label="$options.i18n.securityReportsHelp"
>
<gl-icon name="question" />
</gl-link>
<help-icon
:help-path="securityReportsDocsPath"
:discover-project-security-path="discoverProjectSecurityPath"
/>
</template>
<template v-if="canShowDownloads" #action-buttons>
......
---
title: Show upgrade popover in security widget in merge requests when the user is able to upgrade
merge_request: 49613
author:
type: added
......@@ -318,6 +318,7 @@ export default {
:secret-scanning-comparison-path="mr.secretScanningComparisonPath"
:target-project-full-path="mr.targetProjectFullPath"
:mr-iid="mr.iid"
:discover-project-security-path="mr.discoverProjectSecurityPath"
/>
<grouped-security-reports-app
v-else-if="shouldRenderExtendedSecurityReport"
......
......@@ -54,6 +54,8 @@ export default class MergeRequestStore extends CEMergeRequestStore {
// Paths are set on the first load of the page and not auto-refreshed
super.setPaths(data);
this.discoverProjectSecurityPath = data.discover_project_security_path;
// Security scan diff paths
this.containerScanningComparisonPath = data.container_scanning_comparison_path;
this.coverageFuzzingComparisonPath = data.coverage_fuzzing_comparison_path;
......
......@@ -52,6 +52,10 @@ module EE
merge_request.missing_security_scan_types if expose_missing_security_scan_types?
end
def discover_project_security_path
project_security_discover_path(project) if show_discover_project_security?(project)
end
private
def expose_mr_approval_path?
......
......@@ -89,6 +89,10 @@ module EE
presenter(merge_request).create_vulnerability_feedback_dismissal_path(merge_request.project)
end
expose :discover_project_security_path do |merge_request|
presenter(merge_request).discover_project_security_path
end
expose :has_approvals_available do |merge_request|
merge_request.approval_feature_available?
end
......
......@@ -11,6 +11,7 @@ export default {
license_management: false,
secret_detection: false,
},
discover_project_security_path: '/discover_project_security',
container_scanning_comparison_path: '/container_scanning_comparison_path',
dependency_scanning_comparison_path: '/dependency_scanning_comparison_path',
dast_comparison_path: '/dast_comparison_path',
......
......@@ -70,6 +70,7 @@ describe('MergeRequestStore', () => {
describe('setPaths', () => {
it.each([
'discover_project_security_path',
'container_scanning_comparison_path',
'dependency_scanning_comparison_path',
'sast_comparison_path',
......
......@@ -131,4 +131,29 @@ RSpec.describe MergeRequestPresenter do
it { is_expected.to eq(attribute_value) }
end
end
describe '#discover_project_security_path' do
let(:presenter) { described_class.new(merge_request, current_user: user) }
let(:can_discover_project_security) { true }
subject { presenter.discover_project_security_path }
before do
allow(presenter).to receive(:show_discover_project_security?) { can_discover_project_security }
end
context 'when project security is discoverable' do
it 'returns path' do
is_expected.to eq(presenter.project_security_discover_path(project))
end
end
context 'when project security is not discoverable' do
let(:can_discover_project_security) { false }
it 'returns nil' do
is_expected.to be_nil
end
end
end
end
......@@ -294,6 +294,10 @@ RSpec.describe MergeRequestWidgetEntity do
expect(subject.as_json).to include(:can_read_vulnerability_feedback)
end
it 'has discover project security path' do
expect(subject.as_json).to include(:discover_project_security_path)
end
it 'has pipeline id' do
allow(merge_request).to receive(:head_pipeline).and_return(pipeline)
......
......@@ -24607,6 +24607,12 @@ msgstr ""
msgid "SecurityReports|Undo dismiss"
msgstr ""
msgid "SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI."
msgstr ""
msgid "SecurityReports|Upgrade to manage vulnerabilities"
msgstr ""
msgid "SecurityReports|Vulnerability Report"
msgstr ""
......
import { GlLink, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
const helpPath = '/docs';
const discoverProjectSecurityPath = '/discoverProjectSecurityPath';
describe('HelpIcon component', () => {
let wrapper;
const createWrapper = props => {
wrapper = shallowMount(HelpIcon, {
propsData: {
helpPath,
...props,
},
});
};
const findLink = () => wrapper.find(GlLink);
const findPopover = () => wrapper.find(GlPopover);
const findPopoverTarget = () => wrapper.find({ ref: 'discoverProjectSecurity' });
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('given a help path only', () => {
beforeEach(() => {
createWrapper();
});
it('does not render a popover', () => {
expect(findPopover().exists()).toBe(false);
});
it('renders a help link', () => {
expect(findLink().attributes()).toMatchObject({
href: helpPath,
target: '_blank',
});
});
});
describe('given a help path and discover project security path', () => {
beforeEach(() => {
createWrapper({ discoverProjectSecurityPath });
});
it('renders a popover', () => {
const popover = findPopover();
expect(popover.props('target')()).toBe(findPopoverTarget().element);
expect(popover.attributes()).toMatchObject({
title: HelpIcon.i18n.upgradeToManageVulnerabilities,
triggers: 'click blur',
});
expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract);
});
it('renders a link to the discover path', () => {
expect(findLink().attributes()).toMatchObject({
href: discoverProjectSecurityPath,
target: '_blank',
});
});
});
});
......@@ -19,6 +19,7 @@ import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
......@@ -38,6 +39,7 @@ describe('Security reports app', () => {
pipelineId: 123,
projectId: 456,
securityReportsDocsPath: '/docs',
discoverProjectSecurityPath: '/discoverProjectSecurityPath',
};
const createComponent = options => {
......@@ -47,6 +49,9 @@ describe('Security reports app', () => {
{
localVue,
propsData: { ...props },
stubs: {
HelpIcon: true,
},
},
options,
),
......@@ -68,7 +73,7 @@ describe('Security reports app', () => {
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]');
const findHelpIconComponent = () => wrapper.find(HelpIcon);
const setupMockJobArtifact = reportType => {
jest
.spyOn(Api, 'pipelineJobs')
......@@ -133,8 +138,9 @@ describe('Security reports app', () => {
});
it('renders a help link', () => {
expect(findHelpLink().attributes()).toMatchObject({
href: props.securityReportsDocsPath,
expect(findHelpIconComponent().props()).toEqual({
helpPath: props.securityReportsDocsPath,
discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment