Commit c507de52 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '213671-persist-storage-to-db' into 'master'

Persist banner dismissal

See merge request gitlab-org/gitlab!31894
parents aeb27a75 7f7f1aae
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlBanner } from '@gitlab/ui'; import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils'; 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 ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
import ReportsNotConfigured from 'ee/security_dashboard/components/empty_states/reports_not_configured.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'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
...@@ -50,11 +51,23 @@ export default { ...@@ -50,11 +51,23 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
showIntroductionBanner: {
type: Boolean,
required: true,
},
userCalloutId: {
type: String,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
filters: {}, 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: { methods: {
...@@ -62,8 +75,11 @@ export default { ...@@ -62,8 +75,11 @@ export default {
this.filters = filters; this.filters = filters;
}, },
handleBannerClose() { handleBannerClose() {
Cookies.set(BANNER_COOKIE_KEY, 'true', { expires: 365 * 10 });
this.isBannerVisible = false; this.isBannerVisible = false;
axios.post(this.userCalloutsPath, {
feature_name: this.userCalloutId,
});
}, },
}, },
}; };
......
import Vue from 'vue'; import Vue from 'vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; 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 FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue';
import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue'; import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue';
import FirstClassInstanceSecurityDashboard from './components/first_class_instance_security_dashboard.vue'; import FirstClassInstanceSecurityDashboard from './components/first_class_instance_security_dashboard.vue';
...@@ -50,6 +51,9 @@ export default ( ...@@ -50,6 +51,9 @@ export default (
component = FirstClassProjectSecurityDashboard; component = FirstClassProjectSecurityDashboard;
props.projectFullPath = el.dataset.projectFullPath; props.projectFullPath = el.dataset.projectFullPath;
props.vulnerabilitiesExportEndpoint = el.dataset.vulnerabilitiesExportEndpoint; 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) { } else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard; component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath; props.groupFullPath = el.dataset.groupFullPath;
......
...@@ -220,7 +220,10 @@ module EE ...@@ -220,7 +220,10 @@ module EE
ref_path: project_commits_url(project, pipeline.ref), ref_path: project_commits_url(project, pipeline.ref),
pipeline_path: pipeline_url(pipeline), pipeline_path: pipeline_url(pipeline),
pipeline_created: pipeline.created_at.to_s(:iso8601), 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)) }.merge(project_vulnerabilities_config(project))
end end
end end
......
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
THREAT_MONITORING_INFO = 'threat_monitoring_info' THREAT_MONITORING_INFO = 'threat_monitoring_info'
ACCOUNT_RECOVERY_REGULAR_CHECK = 'account_recovery_regular_check' ACCOUNT_RECOVERY_REGULAR_CHECK = 'account_recovery_regular_check'
USERS_OVER_LICENSE_BANNER = 'users_over_license_banner' USERS_OVER_LICENSE_BANNER = 'users_over_license_banner'
STANDALONE_VULNERABILITIES_INTRODUCTION_BANNER = 'standalone_vulnerabilities_introduction_banner'
def show_canary_deployment_callout?(project) def show_canary_deployment_callout?(project)
!user_dismissed?(CANARY_DEPLOYMENT) && !user_dismissed?(CANARY_DEPLOYMENT) &&
...@@ -81,6 +82,10 @@ module EE ...@@ -81,6 +82,10 @@ module EE
!user_dismissed?(THREAT_MONITORING_INFO) !user_dismissed?(THREAT_MONITORING_INFO)
end end
def show_standalone_vulnerabilities_introduction_banner?
!user_dismissed?(STANDALONE_VULNERABILITIES_INTRODUCTION_BANNER)
end
private private
def hashed_storage_enabled? def hashed_storage_enabled?
......
...@@ -17,7 +17,8 @@ module EE ...@@ -17,7 +17,8 @@ module EE
gold_trial_billings: 8, gold_trial_billings: 8,
threat_monitoring_info: 11, threat_monitoring_info: 11,
account_recovery_regular_check: 12, account_recovery_regular_check: 12,
users_over_license_banner: 16 users_over_license_banner: 16,
standalone_vulnerabilities_introduction_banner: 17
) )
end end
end end
......
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 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, { import FirstClassProjectSecurityDashboard, {
BANNER_COOKIE_KEY, BANNER_COOKIE_KEY,
} from 'ee/security_dashboard/components/first_class_project_security_dashboard.vue'; } from 'ee/security_dashboard/components/first_class_project_security_dashboard.vue';
...@@ -16,6 +20,9 @@ const props = { ...@@ -16,6 +20,9 @@ const props = {
projectFullPath: '/group/project', projectFullPath: '/group/project',
securityDashboardHelpPath: '/security/dashboard/help-path', securityDashboardHelpPath: '/security/dashboard/help-path',
vulnerabilitiesExportEndpoint: '/vulnerabilities/exports', vulnerabilitiesExportEndpoint: '/vulnerabilities/exports',
userCalloutId: 'standalone_vulnerabilities_introduction_banner',
userCalloutsPath: `${TEST_HOST}/user_callouts`,
showIntroductionBanner: false,
}; };
const filters = { foo: 'bar' }; const filters = { foo: 'bar' };
...@@ -41,7 +48,7 @@ describe('First class Project Security Dashboard component', () => { ...@@ -41,7 +48,7 @@ describe('First class Project Security Dashboard component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
Cookies.remove(BANNER_COOKIE_KEY); wrapper = null;
}); });
describe('on render when pipeline has data', () => { describe('on render when pipeline has data', () => {
...@@ -77,8 +84,16 @@ describe('First class Project Security Dashboard component', () => { ...@@ -77,8 +84,16 @@ describe('First class Project Security Dashboard component', () => {
}); });
describe('when user visits for the first time', () => { describe('when user visits for the first time', () => {
let mockAxios;
beforeEach(() => { 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', () => { it('displays a banner which the title highlights the new functionality', () => {
...@@ -100,17 +115,28 @@ describe('First class Project Security Dashboard component', () => { ...@@ -100,17 +115,28 @@ describe('First class Project Security Dashboard component', () => {
.find('button.close') .find('button.close')
.trigger('click'); .trigger('click');
return wrapper.vm.$nextTick(() => { return waitForPromises().then(() => {
expect(findIntroductionBanner().exists()).toBe(false);
// 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); expect(findIntroductionBanner().exists()).toBe(false);
expect(mockAxios.history.post).toHaveLength(1);
}); });
}); });
}); });
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);
});
});
describe('with filter data', () => { describe('with filter data', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
......
...@@ -347,4 +347,26 @@ describe EE::UserCalloutsHelper do ...@@ -347,4 +347,26 @@ describe EE::UserCalloutsHelper do
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
end end
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 end
...@@ -104,6 +104,10 @@ describe ProjectsHelper do ...@@ -104,6 +104,10 @@ describe ProjectsHelper do
describe '#project_security_dashboard_config' do describe '#project_security_dashboard_config' do
include_context 'project with owner and pipeline' 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) } let(:project) { create(:project, :repository, group: group) }
context 'project without pipeline' do 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