Commit c05cf762 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '14707-persist-threat-monitoring-alert-dismissal-ee' into 'master'

Persist Threat Monitoring alert dismissal

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