Commit bf617edd authored by Mark Florian's avatar Mark Florian

Persist Threat Monitoring alert dismissal

This ensures that once a user dismisses the info alert on the Threat
Monitoring page, it doesn't appear again on subsequent visits.

Part of [WAF statistics reporting][1].

[1]: https://gitlab.com/gitlab-org/gitlab/issues/14707
parent 8d9171f1
......@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { GlAlert, GlEmptyState, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import ThreatMonitoringFilters from './threat_monitoring_filters.vue';
import WafLoadingSkeleton from './waf_loading_skeleton.vue';
import WafStatisticsSummary from './waf_statistics_summary.vue';
......@@ -33,10 +34,22 @@ export default {
type: String,
required: true,
},
showUserCallout: {
type: Boolean,
required: true,
},
userCalloutId: {
type: String,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
},
data() {
return {
showAlert: true,
showAlert: this.showUserCallout,
// WAF requires the project to have at least one available environment.
// An invalid default environment id means there there are no available
......@@ -61,6 +74,10 @@ export default {
},
dismissAlert() {
this.showAlert = false;
axios.post(this.userCalloutsPath, {
feature_name: this.userCalloutId,
});
},
},
emptyStateDescription: s__(
......
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ThreatMonitoringApp from './components/app.vue';
import createStore from './store';
......@@ -10,6 +11,9 @@ export default () => {
emptyStateSvgPath,
documentationPath,
defaultEnvironmentId,
showUserCallout,
userCalloutId,
userCalloutsPath,
} = el.dataset;
const store = createStore();
......@@ -27,6 +31,9 @@ export default () => {
emptyStateSvgPath,
documentationPath,
defaultEnvironmentId: parseInt(defaultEnvironmentId, 10),
showUserCallout: parseBoolean(showUserCallout),
userCalloutId,
userCalloutsPath,
},
});
},
......
......@@ -9,6 +9,7 @@ module EE
CANARY_DEPLOYMENT = 'canary_deployment'
GOLD_TRIAL = 'gold_trial'
GOLD_TRIAL_BILLINGS = 'gold_trial_billings'
THREAT_MONITORING_INFO = 'threat_monitoring_info'
def show_canary_deployment_callout?(project)
!user_dismissed?(CANARY_DEPLOYMENT) &&
......@@ -64,6 +65,10 @@ module EE
render 'shared/gold_trial_callout_content', is_dismissable: !namespace.free_plan?, callout: GOLD_TRIAL_BILLINGS
end
def show_threat_monitoring_info?
!user_dismissed?(THREAT_MONITORING_INFO)
end
private
def hashed_storage_enabled?
......
......@@ -14,7 +14,8 @@ module EE
geo_enable_hashed_storage: 5,
geo_migrate_hashed_storage: 6,
canary_deployment: 7,
gold_trial_billings: 8
gold_trial_billings: 8,
threat_monitoring_info: 11
)
end
end
......
......@@ -8,4 +8,7 @@
waf_statistics_endpoint: 'dummy',
environments_endpoint: project_environments_path(@project),
default_environment_id: default_environment_id,
user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::THREAT_MONITORING_INFO,
show_user_callout: show_threat_monitoring_info?.to_s,
} }
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import createStore from 'ee/threat_monitoring/store';
......@@ -13,6 +15,8 @@ const documentationPath = '/docs';
const emptyStateSvgPath = '/svgs';
const environmentsEndpoint = `${TEST_HOST}/environments`;
const wafStatisticsEndpoint = `${TEST_HOST}/waf`;
const userCalloutId = 'threat_monitoring_info';
const userCalloutsPath = `${TEST_HOST}/user_callouts`;
describe('ThreatMonitoringApp component', () => {
let store;
......@@ -28,7 +32,15 @@ describe('ThreatMonitoringApp component', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(ThreatMonitoringApp, {
propsData,
propsData: {
defaultEnvironmentId,
emptyStateSvgPath,
documentationPath,
showUserCallout: true,
userCalloutId,
userCalloutsPath,
...propsData,
},
store,
sync: false,
});
......@@ -49,8 +61,6 @@ describe('ThreatMonitoringApp component', () => {
beforeEach(() => {
factory({
defaultEnvironmentId: invalidEnvironmentId,
emptyStateSvgPath,
documentationPath,
});
});
......@@ -71,11 +81,7 @@ describe('ThreatMonitoringApp component', () => {
describe('given there is a default environment', () => {
beforeEach(() => {
factory({
defaultEnvironmentId,
emptyStateSvgPath,
documentationPath,
});
factory();
});
it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => {
......@@ -103,14 +109,39 @@ describe('ThreatMonitoringApp component', () => {
});
describe('dismissing the alert', () => {
let mockAxios;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onPost(userCalloutsPath, { feature_name: userCalloutId }).reply(200);
findAlert().vm.$emit('dismiss');
return wrapper.vm.$nextTick();
});
afterEach(() => {
mockAxios.restore();
});
it('hides the alert', () => {
expect(findAlert().exists()).toBe(false);
});
it('posts the dismissal to the user callouts endpoint', () => {
expect(mockAxios.history.post).toHaveLength(1);
});
});
});
describe('given showUserCallout is false', () => {
beforeEach(() => {
factory({
showUserCallout: false,
});
});
it('does not render the alert', () => {
expect(findAlert().exists()).toBe(false);
});
describe('given the statistics are loading', () => {
......
......@@ -268,4 +268,26 @@ describe EE::UserCalloutsHelper do
end
end
end
describe '.show_threat_monitoring_info?' do
subject { helper.show_threat_monitoring_info? }
let(:user) { create(:user) }
before do
expect(helper).to receive(:current_user).and_return(user)
end
context 'when the threat monitoring info has not been dismissed' do
it { is_expected.to be_truthy }
end
context 'when the threat monitoring info was dismissed' do
before do
create(:user_callout, user: user, feature_name: described_class::THREAT_MONITORING_INFO)
end
it { is_expected.to be_falsy }
end
end
end
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