Skip to content
Snippets Groups Projects
Commit 75b8a41b authored by Mark Florian's avatar Mark Florian
Browse files

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]: #14707
parent e17a3d85
No related branches found
No related tags found
No related merge requests found
Pipeline #106599474 failed
......@@ -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 UserCalloutsHelper
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) &&
......@@ -113,5 +114,9 @@ def has_no_trial_or_gold_plan?(user)
def has_some_namespaces_with_no_trials?(user)
user&.any_namespace_without_trial?
end
def show_threat_monitoring_info?
!user_dismissed?(THREAT_MONITORING_INFO)
end
end
end
......@@ -14,7 +14,8 @@ def feature_names
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 MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
......@@ -14,6 +16,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;
......@@ -30,7 +34,15 @@ describe('ThreatMonitoringApp component', () => {
wrapper = shallowMount(ThreatMonitoringApp, {
localVue,
propsData,
propsData: {
defaultEnvironmentId,
emptyStateSvgPath,
documentationPath,
showUserCallout: true,
userCalloutId,
userCalloutsPath,
...propsData,
},
store,
sync: false,
});
......@@ -51,8 +63,6 @@ describe('ThreatMonitoringApp component', () => {
beforeEach(() => {
factory({
defaultEnvironmentId: invalidEnvironmentId,
emptyStateSvgPath,
documentationPath,
});
});
......@@ -73,11 +83,7 @@ describe('ThreatMonitoringApp component', () => {
describe('given there is a default environment', () => {
beforeEach(() => {
factory({
defaultEnvironmentId,
emptyStateSvgPath,
documentationPath,
});
factory();
});
it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => {
......@@ -105,13 +111,26 @@ 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');
});
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 the statistics are loading', () => {
......@@ -129,4 +148,16 @@ describe('ThreatMonitoringApp component', () => {
});
});
});
describe('given showUserCallout is false', () => {
beforeEach(() => {
factory({
showUserCallout: false,
});
});
it('does not render the alert', () => {
expect(findAlert().exists()).toBe(false);
});
});
});
......@@ -268,4 +268,27 @@
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
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment