Commit 7f7f1aae authored by Savas Vedova's avatar Savas Vedova

Persist banner dismissal

- Use user callouts endpoint to persist dismissal
- Remove setting the cookie but still provide
  backward compatibility by reading its value
parent 46e975af
......@@ -2,6 +2,7 @@
import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
......@@ -50,11 +51,23 @@ export default {
required: false,
default: '',
},
showIntroductionBanner: {
type: Boolean,
required: true,
},
userCalloutId: {
type: String,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
},
data() {
return {
filters: {},
isBannerVisible: !parseBoolean(Cookies.get(BANNER_COOKIE_KEY)),
isBannerVisible: this.showIntroductionBanner && !parseBoolean(Cookies.get(BANNER_COOKIE_KEY)), // The and statement is for backward compatibility. See https://gitlab.com/gitlab-org/gitlab/-/issues/213671 for more information.
};
},
methods: {
......@@ -62,8 +75,11 @@ export default {
this.filters = filters;
},
handleBannerClose() {
Cookies.set(BANNER_COOKIE_KEY, 'true', { expires: 365 * 10 });
this.isBannerVisible = false;
axios.post(this.userCalloutsPath, {
feature_name: this.userCalloutId,
});
},
},
};
......
import Vue from 'vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue';
import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue';
import FirstClassInstanceSecurityDashboard from './components/first_class_instance_security_dashboard.vue';
......@@ -50,6 +51,9 @@ export default (
component = FirstClassProjectSecurityDashboard;
props.projectFullPath = el.dataset.projectFullPath;
props.vulnerabilitiesExportEndpoint = el.dataset.vulnerabilitiesExportEndpoint;
props.userCalloutId = el.dataset.userCalloutId;
props.userCalloutsPath = el.dataset.userCalloutsPath;
props.showIntroductionBanner = parseBoolean(el.dataset.showIntroductionBanner);
} else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath;
......
......@@ -220,7 +220,10 @@ module EE
ref_path: project_commits_url(project, pipeline.ref),
pipeline_path: pipeline_url(pipeline),
pipeline_created: pipeline.created_at.to_s(:iso8601),
has_pipeline_data: "true"
has_pipeline_data: "true",
user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::STANDALONE_VULNERABILITIES_INTRODUCTION_BANNER,
show_introduction_banner: show_standalone_vulnerabilities_introduction_banner?.to_s
}.merge(project_vulnerabilities_config(project))
end
end
......
......@@ -12,6 +12,7 @@ module EE
THREAT_MONITORING_INFO = 'threat_monitoring_info'
ACCOUNT_RECOVERY_REGULAR_CHECK = 'account_recovery_regular_check'
USERS_OVER_LICENSE_BANNER = 'users_over_license_banner'
STANDALONE_VULNERABILITIES_INTRODUCTION_BANNER = 'standalone_vulnerabilities_introduction_banner'
def show_canary_deployment_callout?(project)
!user_dismissed?(CANARY_DEPLOYMENT) &&
......@@ -81,6 +82,10 @@ module EE
!user_dismissed?(THREAT_MONITORING_INFO)
end
def show_standalone_vulnerabilities_introduction_banner?
!user_dismissed?(STANDALONE_VULNERABILITIES_INTRODUCTION_BANNER)
end
private
def hashed_storage_enabled?
......
......@@ -17,7 +17,8 @@ module EE
gold_trial_billings: 8,
threat_monitoring_info: 11,
account_recovery_regular_check: 12,
users_over_license_banner: 16
users_over_license_banner: 16,
standalone_vulnerabilities_introduction_banner: 17
)
end
end
......
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import FirstClassProjectSecurityDashboard, {
BANNER_COOKIE_KEY,
} from 'ee/security_dashboard/components/first_class_project_security_dashboard.vue';
......@@ -16,6 +20,9 @@ const props = {
projectFullPath: '/group/project',
securityDashboardHelpPath: '/security/dashboard/help-path',
vulnerabilitiesExportEndpoint: '/vulnerabilities/exports',
userCalloutId: 'standalone_vulnerabilities_introduction_banner',
userCalloutsPath: `${TEST_HOST}/user_callouts`,
showIntroductionBanner: false,
};
const filters = { foo: 'bar' };
......@@ -41,7 +48,7 @@ describe('First class Project Security Dashboard component', () => {
afterEach(() => {
wrapper.destroy();
Cookies.remove(BANNER_COOKIE_KEY);
wrapper = null;
});
describe('on render when pipeline has data', () => {
......@@ -77,8 +84,16 @@ describe('First class Project Security Dashboard component', () => {
});
describe('when user visits for the first time', () => {
let mockAxios;
beforeEach(() => {
createComponent({ props: { hasPipelineData: true } });
mockAxios = new MockAdapter(axios);
mockAxios.onPost(props.userCalloutsPath, { feature_name: props.userCalloutId }).reply(200);
createComponent({ props: { hasPipelineData: true, showIntroductionBanner: true } });
});
afterEach(() => {
mockAxios.restore();
});
it('displays a banner which the title highlights the new functionality', () => {
......@@ -100,14 +115,25 @@ describe('First class Project Security Dashboard component', () => {
.find('button.close')
.trigger('click');
return wrapper.vm.$nextTick(() => {
return waitForPromises().then(() => {
expect(findIntroductionBanner().exists()).toBe(false);
expect(mockAxios.history.post).toHaveLength(1);
});
});
});
// Also the newly created component should not display the banner
// because we're setting the cookie.
createComponent({ props: { hasPipelineData: true } });
expect(findIntroductionBanner().exists()).toBe(false);
describe('when user already dismissed the banner in the past', () => {
beforeEach(() => {
Cookies.set(BANNER_COOKIE_KEY, 'true');
createComponent({ props: { hasPipelineData: true, showIntroductionBanner: true } });
});
afterEach(() => {
Cookies.remove(BANNER_COOKIE_KEY);
});
it('does not display the banner despite showIntroductionBanner is true', () => {
expect(findIntroductionBanner().exists()).toBe(false);
});
});
......
......@@ -347,4 +347,26 @@ describe EE::UserCalloutsHelper do
it { is_expected.to be_falsy }
end
end
describe '.show_standalone_vulnerabilities_introduction_banner?' do
subject { helper.show_standalone_vulnerabilities_introduction_banner? }
let(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
context 'when the introduction banner has not been dismissed' do
it { is_expected.to be_truthy }
end
context 'when the introduction banner was dismissed' do
before do
create(:user_callout, user: user, feature_name: described_class::STANDALONE_VULNERABILITIES_INTRODUCTION_BANNER)
end
it { is_expected.to be_falsy }
end
end
end
......@@ -104,6 +104,10 @@ describe ProjectsHelper do
describe '#project_security_dashboard_config' do
include_context 'project with owner and pipeline'
before do
allow(helper).to receive(:current_user).and_return(user)
end
let(:project) { create(:project, :repository, group: group) }
context 'project without pipeline' do
......
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