Commit 0a05443e authored by Mark Florian's avatar Mark Florian

Add upgrade banner to Security Configuration page

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/332691.
parent 2b84f0ef
<script>
import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import FeatureCard from './feature_card.vue';
import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
export const i18n = {
compliance: s__('SecurityConfiguration|Compliance'),
......@@ -25,6 +27,8 @@ export default {
GlSprintf,
FeatureCard,
SectionLayout,
UpgradeBanner,
UserCalloutDismisser,
},
props: {
augmentedSecurityFeatures: {
......@@ -52,6 +56,11 @@ export default {
},
},
computed: {
canUpgrade() {
return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some(
({ available }) => !available,
);
},
canViewCiHistory() {
return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath);
},
......@@ -65,6 +74,12 @@ export default {
<h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1>
</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-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
<section-layout :heading="$options.i18n.securityTesting">
......
<script>
import { GlBanner, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlBanner,
GlSprintf,
},
inject: ['upgradePath'],
i18n: {
title: s__('SecurityConfiguration|Secure your project with Ultimate'),
listItems: [
s__(
'SecurityConfiguration|Check your application %{strongStart}before%{strongEnd} deployment for security vulnerabilities including unauthorized access, data leaks, denial of service attacks.',
),
s__('SecurityConfiguration|Manage and report on vulnerabilities detected.'),
s__(
'SecurityConfiguration|Implement controls to prevent known vulnerabilities being added to your application.',
),
],
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"
>
<ul>
<li v-for="listItem in $options.i18n.listItems" :key="listItem">
<gl-sprintf :message="listItem">
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
</gl-sprintf>
</li>
</ul>
</gl-banner>
</template>
......@@ -30,7 +30,8 @@ class UserCallout < ApplicationRecord
eoa_bronze_plan_banner: 28, # EE-only
pipeline_needs_banner: 29,
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
......
......@@ -14801,6 +14801,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_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="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="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. |
......
......@@ -28790,6 +28790,9 @@ msgstr ""
msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the Merge Request."
msgstr ""
msgid "SecurityConfiguration|Check your application %{strongStart}before%{strongEnd} deployment for security vulnerabilities including unauthorized access, data leaks, denial of service attacks."
msgstr ""
msgid "SecurityConfiguration|Code snippet for the %{scanType} configuration"
msgstr ""
......@@ -28838,9 +28841,15 @@ msgstr ""
msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
msgid "SecurityConfiguration|Implement controls to prevent known vulnerabilities being added to your application."
msgstr ""
msgid "SecurityConfiguration|Manage"
msgstr ""
msgid "SecurityConfiguration|Manage and report on vulnerabilities detected."
msgstr ""
msgid "SecurityConfiguration|Manage scans"
msgstr ""
......@@ -28856,6 +28865,9 @@ msgstr ""
msgid "SecurityConfiguration|SAST Configuration"
msgstr ""
msgid "SecurityConfiguration|Secure your project with Ultimate"
msgstr ""
msgid "SecurityConfiguration|Security Control"
msgstr ""
......@@ -28871,6 +28883,9 @@ 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."
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}"
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 { mount } from '@vue/test-utils';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
SAST_NAME,
......@@ -15,18 +16,33 @@ import FeatureCard from '~/security_configuration/components/feature_card.vue';
import RedesignedSecurityConfigurationApp, {
i18n,
} from '~/security_configuration/components/redesigned_app.vue';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
const upgradePath = '/upgrade';
describe('redesigned App component', () => {
let wrapper;
let userCalloutDismissSpy;
const createComponent = ({ shouldShowCallout = true, ...propsData }) => {
userCalloutDismissSpy = jest.fn();
const createComponent = (propsData) => {
wrapper = extendedWrapper(
mount(RedesignedSecurityConfigurationApp, {
propsData,
provide: {
upgradePath,
},
stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
},
}),
);
};
......@@ -38,6 +54,7 @@ describe('redesigned App component', () => {
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const findComplianceViewHistoryLink = () => findByTestId('compliance-view-history-link');
const findSecurityViewHistoryLink = () => findByTestId('security-view-history-link');
const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
const securityFeaturesMock = [
{
......@@ -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', () => {
beforeEach(() => {
createComponent({
......
import { GlBanner, GlSprintf } 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,
},
stubs: {
GlSprintf,
},
});
};
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('Check your application');
expect(wrapperText).toContain('Manage and report');
expect(wrapperText).toContain('Implement controls');
});
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