Commit 5b389170 authored by Dave Pisek's avatar Dave Pisek

Add security training promo to security dashboard

This commit adds a new Vue component that displays a promotion callout
for the new, security training feature to the security dashboard.
parent 8a746770
......@@ -2,7 +2,9 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import projectsHistoryQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities_by_day_and_count.query.graphql';
import SecurityTrainingPromo from 'ee/security_dashboard/components/shared/security_training_promo.vue';
import { PROJECT_LOADING_ERROR_MESSAGE } from 'ee/security_dashboard/helpers';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createFlash from '~/flash';
import { formatDate, getDateInPast } from '~/lib/utils/datetime_utility';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
......@@ -26,9 +28,11 @@ export default {
components: {
ReportNotConfigured,
SecurityDashboardLayout,
SecurityTrainingPromo,
GlLoadingIcon,
GlLineChart,
},
mixins: [glFeatureFlagsMixin()],
props: {
projectFullPath: {
type: String,
......@@ -172,6 +176,7 @@ export default {
<report-not-configured />
</template>
<template v-else-if="shouldShowCharts" #default>
<security-training-promo v-if="glFeatures.secureVulnerabilityTraining" />
<gl-line-chart
class="gl-mt-6"
:width="chartWidth"
......
<script>
import { GlBanner } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlBanner,
},
inject: ['securityConfigurationPath'],
i18n: {
title: __('Reduce risk and triage fewer vulnerabilities with security training'),
buttonText: __('Enable security training'),
content: __(
'Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability.',
),
},
computed: {
buttonLink() {
return `${this.securityConfigurationPath}?tab=vulnerability-management`;
},
},
};
</script>
<template>
<gl-banner
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
:button-link="buttonLink"
variant="introduction"
>
<p>{{ $options.i18n.content }}</p>
</gl-banner>
</template>
......@@ -12,6 +12,7 @@ module Projects
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:sbom_survey, @user, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, @project, default_enabled: :yaml)
end
feature_category :vulnerability_management
......
......@@ -204,7 +204,8 @@ module EE
scanners: VulnerabilityScanners::ListService.new(project).execute.to_json,
can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: can_view_false_positive?
can_view_false_positive: can_view_false_positive?,
security_configuration_path: project_security_configuration_path(@project)
}.merge!(security_dashboard_pipeline_data(project))
end
end
......
......@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import ProjectSecurityDashboard from 'ee/security_dashboard/components/project/project_security_dashboard.vue';
import ReportNotConfigured from 'ee/security_dashboard/components/shared/empty_states/report_not_configured_project.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/shared/security_dashboard_layout.vue';
import SecurityTrainingPromo from 'ee/security_dashboard/components/shared/security_training_promo.vue';
import projectsHistoryQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities_by_day_and_count.query.graphql';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -26,15 +27,21 @@ describe('Project Security Dashboard component', () => {
const projectFullPath = 'project/path';
const helpPath = 'docs/security/dashboard';
const findLineChart = () => wrapper.find(GlLineChart);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(ReportNotConfigured);
const findLineChart = () => wrapper.findComponent(GlLineChart);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(ReportNotConfigured);
const findSecurityTrainingPromo = () => wrapper.findComponent(SecurityTrainingPromo);
const createApolloProvider = (...queries) => {
return createMockApollo([...queries]);
};
const createComponent = ({ query, propsData, chartWidth = 1024 }) => {
const createComponent = ({
query,
propsData,
chartWidth = 1024,
secureVulnerabilityTrainingEnabled = true,
}) => {
const component = shallowMount(ProjectSecurityDashboard, {
localVue,
apolloProvider: createApolloProvider([
......@@ -49,6 +56,9 @@ describe('Project Security Dashboard component', () => {
provide: {
// To be consumed by SecurityDashboardLayout
sbomSurveySvgPath: '/',
glFeatures: {
secureVulnerabilityTraining: secureVulnerabilityTrainingEnabled,
},
},
stubs: {
SecurityDashboardLayout,
......@@ -125,6 +135,10 @@ describe('Project Security Dashboard component', () => {
startValue: '2021-03-12',
});
});
it('contains a promotion for the security training feature', () => {
expect(findSecurityTrainingPromo().exists()).toBe(true);
});
});
describe('when there is no history data', () => {
......@@ -169,4 +183,18 @@ describe('Project Security Dashboard component', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('with the "secureVulnerabilityTraining" feature flag disabled', () => {
it('does not contain a promotion for the security training feature', async () => {
wrapper = createComponent({
query: mockProjectSecurityChartsWithData(),
propsData: { hasVulnerabilities: true },
secureVulnerabilityTrainingEnabled: false,
});
await wrapper.vm.$nextTick();
expect(findSecurityTrainingPromo().exists()).toBe(false);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import SecurityTrainingPromo from 'ee/security_dashboard/components/shared/security_training_promo.vue';
const SECURITY_CONFIGURATION_PATH = 'foo/bar';
const VULNERABILITY_MANAGEMENT_TAB_NAME = 'vulnerability-management';
describe('ee/security_dashboard/components/shared/security_training_promo.vue', () => {
let wrapper;
const createWrapper = () =>
shallowMount(SecurityTrainingPromo, {
provide: {
securityConfigurationPath: SECURITY_CONFIGURATION_PATH,
},
});
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
const findBanner = () => wrapper.findComponent(GlBanner);
describe('banner', () => {
it('should be an introduction that announces the security training feature', () => {
expect(findBanner().props()).toMatchObject({
variant: 'introduction',
title: SecurityTrainingPromo.i18n.title,
buttonText: SecurityTrainingPromo.i18n.buttonText,
});
expect(findBanner().text()).toBe(SecurityTrainingPromo.i18n.content);
});
it(`should link to the security configuration's vulnerability management tab`, () => {
expect(findBanner().props('buttonLink')).toBe(
`${SECURITY_CONFIGURATION_PATH}?tab=${VULNERABILITY_MANAGEMENT_TAB_NAME}`,
);
});
});
});
......@@ -13261,6 +13261,12 @@ msgstr ""
msgid "Enable repository checks"
msgstr ""
msgid "Enable security training"
msgstr ""
msgid "Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability."
msgstr ""
msgid "Enable shared runners for all projects and subgroups in this group."
msgstr ""
......@@ -29320,6 +29326,9 @@ msgstr ""
msgid "Reduce project visibility"
msgstr ""
msgid "Reduce risk and triage fewer vulnerabilities with security training"
msgstr ""
msgid "Reduce this project’s visibility?"
msgstr ""
......
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