Commit f6f4d8b4 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Nicolò Maria Mezzopera

Create auto-fix user callout

Created the auto-fix user callout that shows up in the project security
dashboard (unless previously dismissed).
parent a4f227d0
<script>
import { GlBanner } from '@gitlab/ui';
export default {
name: 'AutoFixUserCallout',
components: {
GlBanner,
},
props: {
helpPagePath: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-banner
:title="s__('AutoRemediation|Introducing GitLab auto-fix')"
:button-text="__('Learn more')"
:button-link="helpPagePath"
variant="introduction"
v-on="$listeners"
>
<p>
{{
s__(
"AutoRemediation|If you're using dependency and/or container scanning, and auto-fix is enabled, auto-fix automatically creates merge requests with fixes to vulnerabilities.",
)
}}
</p>
</gl-banner>
</template>
<script> <script>
import Cookies from 'js-cookie';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AutoFixUserCallout from './auto_fix_user_callout.vue';
import ProjectVulnerabilitiesApp from './project_vulnerabilities.vue'; import ProjectVulnerabilitiesApp from './project_vulnerabilities.vue';
import ReportsNotConfigured from './empty_states/reports_not_configured.vue'; import ReportsNotConfigured from './empty_states/reports_not_configured.vue';
import SecurityDashboardLayout from './security_dashboard_layout.vue'; import SecurityDashboardLayout from './security_dashboard_layout.vue';
...@@ -10,6 +13,7 @@ export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner'; ...@@ -10,6 +13,7 @@ export const BANNER_COOKIE_KEY = 'hide_vulnerabilities_introduction_banner';
export default { export default {
components: { components: {
AutoFixUserCallout,
ProjectVulnerabilitiesApp, ProjectVulnerabilitiesApp,
ReportsNotConfigured, ReportsNotConfigured,
SecurityDashboardLayout, SecurityDashboardLayout,
...@@ -17,6 +21,7 @@ export default { ...@@ -17,6 +21,7 @@ export default {
CsvExportButton, CsvExportButton,
Filters, Filters,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
securityDashboardHelpPath: { securityDashboardHelpPath: {
type: String, type: String,
...@@ -39,15 +44,22 @@ export default { ...@@ -39,15 +44,22 @@ export default {
}, },
}, },
data() { data() {
const shoudShowAutoFixUserCallout =
this.glFeatures.securityAutoFix && !Cookies.get('auto_fix_user_callout_dismissed');
return { return {
filters: {}, filters: {},
shoudShowAutoFixUserCallout,
}; };
}, },
inject: ['dashboardDocumentation'], inject: ['dashboardDocumentation', 'autoFixDocumentation'],
methods: { methods: {
handleFilterChange(filters) { handleFilterChange(filters) {
this.filters = filters; this.filters = filters;
}, },
handleAutoFixUserCalloutClose() {
Cookies.set('auto_fix_user_callout_dismissed', 'true');
this.shoudShowAutoFixUserCallout = false;
},
}, },
}; };
</script> </script>
...@@ -55,6 +67,11 @@ export default { ...@@ -55,6 +67,11 @@ export default {
<template> <template>
<div> <div>
<template v-if="hasVulnerabilities"> <template v-if="hasVulnerabilities">
<auto-fix-user-callout
v-if="shoudShowAutoFixUserCallout"
:help-page-path="autoFixDocumentation"
@close="handleAutoFixUserCalloutClose"
/>
<security-dashboard-layout> <security-dashboard-layout>
<template #header> <template #header>
<div class="mt-4 d-flex"> <div class="mt-4 d-flex">
......
...@@ -42,6 +42,7 @@ export default (el, dashboardType) => { ...@@ -42,6 +42,7 @@ export default (el, dashboardType) => {
if (dashboardType === DASHBOARD_TYPES.PROJECT) { if (dashboardType === DASHBOARD_TYPES.PROJECT) {
component = FirstClassProjectSecurityDashboard; component = FirstClassProjectSecurityDashboard;
props.projectFullPath = el.dataset.projectFullPath; props.projectFullPath = el.dataset.projectFullPath;
provide.autoFixDocumentation = el.dataset.autoFixDocumentation;
} else if (dashboardType === DASHBOARD_TYPES.GROUP) { } else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard; component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath; props.groupFullPath = el.dataset.groupFullPath;
......
...@@ -6,6 +6,10 @@ module Projects ...@@ -6,6 +6,10 @@ module Projects
include SecurityDashboardsPermissions include SecurityDashboardsPermissions
alias_method :vulnerable, :project alias_method :vulnerable, :project
before_action only: [:index] do
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
end
end end
end end
end end
...@@ -231,7 +231,8 @@ module EE ...@@ -231,7 +231,8 @@ module EE
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'), dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'), not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'),
no_pipeline_run_scanners_help_path: new_project_pipeline_path(project), no_pipeline_run_scanners_help_path: new_project_pipeline_path(project),
security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index') security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'),
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests')
}.merge!(security_dashboard_pipeline_data(project)) }.merge!(security_dashboard_pipeline_data(project))
end end
end end
......
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import AutoFixUserCallout from 'ee/security_dashboard/components/auto_fix_user_callout.vue';
describe('AutoFixUserCallout', () => {
let wrapper;
const helpPagePath = '/help/page/path';
const createWrapper = () => {
wrapper = shallowMount(AutoFixUserCallout, {
propsData: {
helpPagePath,
},
});
};
it('renders properly', () => {
createWrapper();
expect(wrapper.find(GlBanner).exists()).toBe(true);
expect(wrapper.find(GlBanner).props()).toMatchObject({
title: 'Introducing GitLab auto-fix',
buttonText: 'Learn more',
buttonLink: helpPagePath,
});
expect(wrapper.text()).toContain(
"If you're using dependency and/or container scanning, and auto-fix is enabled, auto-fix automatically creates merge requests with fixes to vulnerabilities.",
);
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui'; import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import FirstClassProjectSecurityDashboard from 'ee/security_dashboard/components/first_class_project_security_dashboard.vue'; import FirstClassProjectSecurityDashboard from 'ee/security_dashboard/components/first_class_project_security_dashboard.vue';
import AutoFixUserCallout from 'ee/security_dashboard/components/auto_fix_user_callout.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue'; import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue'; import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue';
...@@ -18,7 +20,11 @@ const props = { ...@@ -18,7 +20,11 @@ const props = {
const provide = { const provide = {
dashboardDocumentation: '/help/docs', dashboardDocumentation: '/help/docs',
autoFixDocumentation: '/auto/fix/documentation',
emptyStateSvgPath: '/svgs/empty/svg', emptyStateSvgPath: '/svgs/empty/svg',
glFeatures: {
securityAutoFix: true,
},
}; };
const filters = { foo: 'bar' }; const filters = { foo: 'bar' };
...@@ -31,6 +37,7 @@ describe('First class Project Security Dashboard component', () => { ...@@ -31,6 +37,7 @@ describe('First class Project Security Dashboard component', () => {
const findVulnerabilityCountList = () => wrapper.find(VulnerabilityCountList); const findVulnerabilityCountList = () => wrapper.find(VulnerabilityCountList);
const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured); const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured);
const findCsvExportButton = () => wrapper.find(CsvExportButton); const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findAutoFixUserCallout = () => wrapper.find(AutoFixUserCallout);
const createComponent = options => { const createComponent = options => {
wrapper = shallowMount(FirstClassProjectSecurityDashboard, { wrapper = shallowMount(FirstClassProjectSecurityDashboard, {
...@@ -90,6 +97,61 @@ describe('First class Project Security Dashboard component', () => { ...@@ -90,6 +97,61 @@ describe('First class Project Security Dashboard component', () => {
}); });
}); });
describe('auto-fix user callout', () => {
describe('feature flag disabled', () => {
beforeEach(() => {
createComponent({
props: { hasVulnerabilities: true },
provide: {
...provide,
glFeatures: {
securityAutoFix: false,
},
},
});
});
it('does not show user callout', () => {
expect(findAutoFixUserCallout().exists()).toBe(false);
});
});
describe('cookie not set', () => {
beforeEach(() => {
jest.spyOn(Cookies, 'set');
createComponent({
props: { hasVulnerabilities: true },
});
});
it('shows user callout by default', () => {
expect(findAutoFixUserCallout().exists()).toBe(true);
});
it('when dismissed, hides the user callout and sets the cookie', async () => {
await findAutoFixUserCallout().vm.$emit('close');
expect(findAutoFixUserCallout().exists()).toBe(false);
expect(Cookies.set).toHaveBeenCalledWith('auto_fix_user_callout_dismissed', 'true');
});
});
describe('cookie set', () => {
beforeEach(() => {
jest.doMock('js-cookie', () => ({
get: jest.fn().mockReturnValue(true),
}));
createComponent({
props: { hasVulnerabilities: true },
});
});
it('does not show user callout', () => {
expect(findAutoFixUserCallout().exists()).toBe(false);
});
});
});
describe('with filter data', () => { describe('with filter data', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
......
...@@ -154,7 +154,8 @@ RSpec.describe ProjectsHelper do ...@@ -154,7 +154,8 @@ RSpec.describe ProjectsHelper do
dashboard_documentation: '/help/user/application_security/security_dashboard/index', dashboard_documentation: '/help/user/application_security/security_dashboard/index',
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index', security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'), not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'),
no_pipeline_run_scanners_help_path: "/#{project.full_path}/-/pipelines/new" no_pipeline_run_scanners_help_path: "/#{project.full_path}/-/pipelines/new",
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests')
} }
end end
......
...@@ -3813,6 +3813,12 @@ msgstr "" ...@@ -3813,6 +3813,12 @@ msgstr ""
msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}" msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}"
msgstr "" msgstr ""
msgid "AutoRemediation|If you're using dependency and/or container scanning, and auto-fix is enabled, auto-fix automatically creates merge requests with fixes to vulnerabilities."
msgstr ""
msgid "AutoRemediation|Introducing GitLab auto-fix"
msgstr ""
msgid "Autocomplete" msgid "Autocomplete"
msgstr "" 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