Commit 3431b247 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '332691-security-configuration-upgrade-banner' into 'master'

Add upgrade banner to Security Configuration page

See merge request gitlab-org/gitlab!63448
parents b27cf9d5 85827e9e
<script> <script>
import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui'; import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import FeatureCard from './feature_card.vue'; import FeatureCard from './feature_card.vue';
import SectionLayout from './section_layout.vue'; import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
export const i18n = { export const i18n = {
compliance: s__('SecurityConfiguration|Compliance'), compliance: s__('SecurityConfiguration|Compliance'),
...@@ -25,6 +27,8 @@ export default { ...@@ -25,6 +27,8 @@ export default {
GlSprintf, GlSprintf,
FeatureCard, FeatureCard,
SectionLayout, SectionLayout,
UpgradeBanner,
UserCalloutDismisser,
}, },
props: { props: {
augmentedSecurityFeatures: { augmentedSecurityFeatures: {
...@@ -52,6 +56,11 @@ export default { ...@@ -52,6 +56,11 @@ export default {
}, },
}, },
computed: { computed: {
canUpgrade() {
return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some(
({ available }) => !available,
);
},
canViewCiHistory() { canViewCiHistory() {
return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath); return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath);
}, },
...@@ -65,6 +74,12 @@ export default { ...@@ -65,6 +74,12 @@ export default {
<h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1> <h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1>
</header> </header>
<user-callout-dismisser v-if="canUpgrade" feature-name="security_configuration_upgrade_banner">
<template #default="{ dismiss, shouldShowCallout }">
<upgrade-banner v-if="shouldShowCallout" @close="dismiss" />
</template>
</user-callout-dismisser>
<gl-tabs content-class="gl-pt-6"> <gl-tabs content-class="gl-pt-6">
<gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting"> <gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
<section-layout :heading="$options.i18n.securityTesting"> <section-layout :heading="$options.i18n.securityTesting">
......
<script>
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlBanner,
},
inject: ['upgradePath'],
i18n: {
title: s__('SecurityConfiguration|Secure your project with Ultimate'),
bodyStart: s__(
`SecurityConfiguration|GitLab Ultimate checks your application for security vulnerabilities
that may lead to unauthorized access, data leaks, and denial of service
attacks. Its features include:`,
),
bodyListItems: [
s__('SecurityConfiguration|Vulnerability details and statistics in the merge request.'),
s__('SecurityConfiguration|High-level vulnerability statistics across projects and groups.'),
s__('SecurityConfiguration|Runtime security metrics for application environments.'),
],
bodyEnd: s__(
'SecurityConfiguration|With the information provided, you can immediately begin risk analysis and remediation within GitLab.',
),
buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'),
},
};
</script>
<template>
<gl-banner
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
:button-link="upgradePath"
v-on="$listeners"
>
<p>{{ $options.i18n.bodyStart }}</p>
<ul>
<li v-for="bodyListItem in $options.i18n.bodyListItems" :key="bodyListItem">
{{ bodyListItem }}
</li>
</ul>
<p>{{ $options.i18n.bodyEnd }}</p>
</gl-banner>
</template>
...@@ -30,7 +30,8 @@ class UserCallout < ApplicationRecord ...@@ -30,7 +30,8 @@ class UserCallout < ApplicationRecord
eoa_bronze_plan_banner: 28, # EE-only eoa_bronze_plan_banner: 28, # EE-only
pipeline_needs_banner: 29, pipeline_needs_banner: 29,
pipeline_needs_hover_tip: 30, pipeline_needs_hover_tip: 30,
web_ide_ci_environments_guidance: 31 web_ide_ci_environments_guidance: 31,
security_configuration_upgrade_banner: 32
} }
validates :user, presence: true validates :user, presence: true
......
...@@ -14824,6 +14824,7 @@ Name of the feature that the callout is for. ...@@ -14824,6 +14824,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumpipeline_needs_banner"></a>`PIPELINE_NEEDS_BANNER` | Callout feature name for pipeline_needs_banner. | | <a id="usercalloutfeaturenameenumpipeline_needs_banner"></a>`PIPELINE_NEEDS_BANNER` | Callout feature name for pipeline_needs_banner. |
| <a id="usercalloutfeaturenameenumpipeline_needs_hover_tip"></a>`PIPELINE_NEEDS_HOVER_TIP` | Callout feature name for pipeline_needs_hover_tip. | | <a id="usercalloutfeaturenameenumpipeline_needs_hover_tip"></a>`PIPELINE_NEEDS_HOVER_TIP` | Callout feature name for pipeline_needs_hover_tip. |
| <a id="usercalloutfeaturenameenumregistration_enabled_callout"></a>`REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. | | <a id="usercalloutfeaturenameenumregistration_enabled_callout"></a>`REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. |
| <a id="usercalloutfeaturenameenumsecurity_configuration_upgrade_banner"></a>`SECURITY_CONFIGURATION_UPGRADE_BANNER` | Callout feature name for security_configuration_upgrade_banner. |
| <a id="usercalloutfeaturenameenumservice_templates_deprecated_callout"></a>`SERVICE_TEMPLATES_DEPRECATED_CALLOUT` | Callout feature name for service_templates_deprecated_callout. | | <a id="usercalloutfeaturenameenumservice_templates_deprecated_callout"></a>`SERVICE_TEMPLATES_DEPRECATED_CALLOUT` | Callout feature name for service_templates_deprecated_callout. |
| <a id="usercalloutfeaturenameenumsuggest_pipeline"></a>`SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. | | <a id="usercalloutfeaturenameenumsuggest_pipeline"></a>`SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. |
| <a id="usercalloutfeaturenameenumsuggest_popover_dismissed"></a>`SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. | | <a id="usercalloutfeaturenameenumsuggest_popover_dismissed"></a>`SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. |
......
...@@ -28844,6 +28844,12 @@ msgstr "" ...@@ -28844,6 +28844,12 @@ msgstr ""
msgid "SecurityConfiguration|Feature documentation for %{featureName}" msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr "" msgstr ""
msgid "SecurityConfiguration|GitLab Ultimate checks your application for security vulnerabilities that may lead to unauthorized access, data leaks, and denial of service attacks. Its features include:"
msgstr ""
msgid "SecurityConfiguration|High-level vulnerability statistics across projects and groups."
msgstr ""
msgid "SecurityConfiguration|Manage" msgid "SecurityConfiguration|Manage"
msgstr "" msgstr ""
...@@ -28856,12 +28862,18 @@ msgstr "" ...@@ -28856,12 +28862,18 @@ msgstr ""
msgid "SecurityConfiguration|Not enabled" msgid "SecurityConfiguration|Not enabled"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Runtime security metrics for application environments."
msgstr ""
msgid "SecurityConfiguration|SAST Analyzers" msgid "SecurityConfiguration|SAST Analyzers"
msgstr "" msgstr ""
msgid "SecurityConfiguration|SAST Configuration" msgid "SecurityConfiguration|SAST Configuration"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Secure your project with Ultimate"
msgstr ""
msgid "SecurityConfiguration|Security Control" msgid "SecurityConfiguration|Security Control"
msgstr "" msgstr ""
...@@ -28877,12 +28889,21 @@ msgstr "" ...@@ -28877,12 +28889,21 @@ msgstr ""
msgid "SecurityConfiguration|The status of the tools only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan." msgid "SecurityConfiguration|The status of the tools only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
msgstr "" msgstr ""
msgid "SecurityConfiguration|Upgrade or start a free trial"
msgstr ""
msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}" msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}"
msgstr "" msgstr ""
msgid "SecurityConfiguration|View history" msgid "SecurityConfiguration|View history"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request."
msgstr ""
msgid "SecurityConfiguration|With the information provided, you can immediately begin risk analysis and remediation within GitLab."
msgstr ""
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}." msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
msgstr "" msgstr ""
......
/**
* Mock factory for the UserCalloutDismisser component.
* @param {slotProps} The slot props to pass to the default slot content.
* @returns {VueComponent}
*/
export const makeMockUserCalloutDismisser = ({
dismiss = () => {},
shouldShowCallout = true,
} = {}) => ({
render() {
return this.$scopedSlots.default({
dismiss,
shouldShowCallout,
});
},
});
import { GlTab } from '@gitlab/ui'; import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { import {
SAST_NAME, SAST_NAME,
...@@ -15,18 +16,33 @@ import FeatureCard from '~/security_configuration/components/feature_card.vue'; ...@@ -15,18 +16,33 @@ import FeatureCard from '~/security_configuration/components/feature_card.vue';
import RedesignedSecurityConfigurationApp, { import RedesignedSecurityConfigurationApp, {
i18n, i18n,
} from '~/security_configuration/components/redesigned_app.vue'; } from '~/security_configuration/components/redesigned_app.vue';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import { import {
REPORT_TYPE_LICENSE_COMPLIANCE, REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST, REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants'; } from '~/vue_shared/security_reports/constants';
const upgradePath = '/upgrade';
describe('redesigned App component', () => { describe('redesigned App component', () => {
let wrapper; let wrapper;
let userCalloutDismissSpy;
const createComponent = ({ shouldShowCallout = true, ...propsData }) => {
userCalloutDismissSpy = jest.fn();
const createComponent = (propsData) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(RedesignedSecurityConfigurationApp, { mount(RedesignedSecurityConfigurationApp, {
propsData, propsData,
provide: {
upgradePath,
},
stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
},
}), }),
); );
}; };
...@@ -38,6 +54,7 @@ describe('redesigned App component', () => { ...@@ -38,6 +54,7 @@ describe('redesigned App component', () => {
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard); const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const findComplianceViewHistoryLink = () => findByTestId('compliance-view-history-link'); const findComplianceViewHistoryLink = () => findByTestId('compliance-view-history-link');
const findSecurityViewHistoryLink = () => findByTestId('security-view-history-link'); const findSecurityViewHistoryLink = () => findByTestId('security-view-history-link');
const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
const securityFeaturesMock = [ const securityFeaturesMock = [
{ {
...@@ -112,6 +129,58 @@ describe('redesigned App component', () => { ...@@ -112,6 +129,58 @@ describe('redesigned App component', () => {
}); });
}); });
describe('upgrade banner', () => {
const makeAvailable = (available) => (feature) => ({ ...feature, available });
describe('given at least one unavailable feature', () => {
beforeEach(() => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(false)),
});
});
it('renders the banner', () => {
expect(findUpgradeBanner().exists()).toBe(true);
});
it('calls the dismiss callback when closing the banner', () => {
expect(userCalloutDismissSpy).not.toHaveBeenCalled();
findUpgradeBanner().vm.$emit('close');
expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
});
});
describe('given at least one unavailable feature, but banner is already dismissed', () => {
beforeEach(() => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(false)),
shouldShowCallout: false,
});
});
it('does not render the banner', () => {
expect(findUpgradeBanner().exists()).toBe(false);
});
});
describe('given all features are available', () => {
beforeEach(() => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock.map(makeAvailable(true)),
augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(true)),
});
});
it('does not render the banner', () => {
expect(findUpgradeBanner().exists()).toBe(false);
});
});
});
describe('when given latestPipelinePath props', () => { describe('when given latestPipelinePath props', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
......
import { GlBanner } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
const upgradePath = '/upgrade';
describe('UpgradeBanner component', () => {
let wrapper;
let closeSpy;
const createComponent = (propsData) => {
closeSpy = jest.fn();
wrapper = shallowMountExtended(UpgradeBanner, {
provide: {
upgradePath,
},
propsData,
listeners: {
close: closeSpy,
},
});
};
const findGlBanner = () => wrapper.findComponent(GlBanner);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('passes the expected props to GlBanner', () => {
expect(findGlBanner().props()).toMatchObject({
title: UpgradeBanner.i18n.title,
buttonText: UpgradeBanner.i18n.buttonText,
buttonLink: upgradePath,
});
});
it('renders the list of benefits', () => {
const wrapperText = wrapper.text();
expect(wrapperText).toContain('GitLab Ultimate checks your application');
expect(wrapperText).toContain('statistics in the merge request');
expect(wrapperText).toContain('statistics across projects');
expect(wrapperText).toContain('Runtime security metrics');
expect(wrapperText).toContain('risk analysis and remediation');
});
it(`re-emits GlBanner's close event`, () => {
expect(closeSpy).not.toHaveBeenCalled();
wrapper.findComponent(GlBanner).vm.$emit('close');
expect(closeSpy).toHaveBeenCalledTimes(1);
});
});
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