Commit 6b541601 authored by Alexander Turinske's avatar Alexander Turinske Committed by Savas Vedova

Add informative banner to cluster security tab

- alert users to the fact that this tab only represents
  a sub-set of the vulnerabilities for the given project
  and direct them to the main vulnerabilities page
- hide alert based on cookie
- abstract constants out to a constants file
- add modal to verify user actions
- add snowplow event for tracking the closing of the alert
- update tests
parent e514baf2
<script>
import { PortalTarget } from 'portal-vue';
import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import VulnerabilityListGraphql from '../shared/vulnerability_report/vulnerability_list_graphql.vue';
import VulnerabilityFilters from '../shared/vulnerability_report/vulnerability_filters.vue';
......@@ -11,11 +16,24 @@ import {
} from '../shared/vulnerability_report/constants';
import projectVulnerabilitiesQuery from '../../graphql/queries/project_vulnerabilities.query.graphql';
import { DASHBOARD_TYPES } from '../../store/constants';
const PORTAL_NAME = 'vulnerability-report-sticky';
import {
ALERT_COOKIE_KEY,
ALERT_MESSAGE,
ALERT_TITLE,
MODAL_ACTIONS,
MODAL_MESSAGE,
MODAL_TITLE,
PORTAL_NAME,
trackAgentSecurityTabAlert,
VULNERABILITY_REPORT_OPERATIONAL_TAB_LINK_LOCATION,
} from './constants';
export default {
components: {
GlAlert,
GlLink,
GlModal,
GlSprintf,
PortalTarget,
VulnerabilityFilters,
VulnerabilityListGraphql,
......@@ -36,7 +54,7 @@ export default {
hasJiraVulnerabilitiesIntegrationEnabled: false,
};
},
inject: ['projectPath'],
inject: ['agentName', 'projectPath'],
props: {
clusterAgentId: {
type: String,
......@@ -45,10 +63,33 @@ export default {
},
data() {
return {
alertDismissed: parseBoolean(getCookie(ALERT_COOKIE_KEY)),
graphqlFilters: { clusterAgentId: [this.clusterAgentId] },
};
},
computed: {
alertTitleComputed() {
return sprintf(ALERT_TITLE, { agent: this.agentName });
},
operationalVulnerabilitiesHref() {
return joinPaths(
gon.relative_url_root || '/',
this.projectPath,
VULNERABILITY_REPORT_OPERATIONAL_TAB_LINK_LOCATION,
);
},
},
methods: {
handleAlertDismiss() {
this.$refs.modal.show();
},
handleModalPrimary() {
setCookie(ALERT_COOKIE_KEY, 'true');
this.alertDismissed = true;
const { category, action } = trackAgentSecurityTabAlert;
Tracking.event(category, action);
},
updateGraphqlFilters(graphqlFilters) {
this.graphqlFilters = graphqlFilters;
this.graphqlFilters.reportType = REPORT_TYPE_PRESETS.OPERATIONAL;
......@@ -57,6 +98,8 @@ export default {
},
fieldsToShow: FIELD_PRESETS[REPORT_TAB.OPERATIONAL],
filtersToShow: FILTER_PRESETS[REPORT_TAB.OPERATIONAL],
i18n: { ALERT_MESSAGE, MODAL_MESSAGE, MODAL_TITLE },
MODAL_ACTIONS,
REPORT_TAB,
PORTAL_NAME,
projectVulnerabilitiesQuery,
......@@ -65,7 +108,22 @@ export default {
<template>
<div>
<div class="security-dashboard-filters gl-mt-7">
<gl-alert
v-if="!alertDismissed"
variant="info"
:title="alertTitleComputed"
@dismiss="handleAlertDismiss"
>
<gl-sprintf :message="$options.i18n.ALERT_MESSAGE">
<template #agent>
<p class="gl-display-inline">{{ agentName }}</p>
</template>
<template #link="{ content }">
<gl-link :href="operationalVulnerabilitiesHref">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<div class="security-dashboard-filters" :class="{ 'gl-mt-3': !alertDismissed }">
<vulnerability-filters
:filters="$options.filtersToShow"
@filters-changed="updateGraphqlFilters"
......@@ -79,5 +137,17 @@ export default {
:filters="graphqlFilters"
:portal-name="$options.PORTAL_NAME"
/>
<gl-modal
ref="modal"
modal-id="agent-vulnerabilities-modal"
size="sm"
:action-primary="$options.MODAL_ACTIONS.primary"
:action-secondary="$options.MODAL_ACTIONS.secondary"
:title="$options.i18n.MODAL_TITLE"
@primary="handleModalPrimary"
>
{{ $options.i18n.MODAL_MESSAGE }}
</gl-modal>
</div>
</template>
import { __, s__ } from '~/locale';
export const ALERT_COOKIE_KEY = 'agent-vulnerabilities-info-alert-dismissed';
export const ALERT_MESSAGE = s__(
'SecurityOrchestration|This view only shows scan results for the agent %{agent}. You can view scan results for all agents in the %{linkStart}Operational Vulnerabilities tab of the vulnerability report%{linkEnd}.',
);
export const ALERT_TITLE = s__('SecurityOrchestration|Latest scan run against %{agent}');
export const MODAL_ACTIONS = {
primary: { text: __('Dismiss Alert') },
secondary: { text: __('Cancel') },
};
export const MODAL_MESSAGE = s__(
'SecurityOrchestration|After dismissing the alert, the information will never be shown again.',
);
export const MODAL_TITLE = s__("SecurityOrchestration|Don't show the alert anymore");
export const PORTAL_NAME = 'vulnerability-report-sticky';
export const VULNERABILITY_REPORT_OPERATIONAL_TAB_LINK_LOCATION =
'/-/security/vulnerability_report/?tab=OPERATIONAL';
/**
* Tracks snowplow event when user views report details
*/
export const trackAgentSecurityTabAlert = {
category: 'Vulnerability_Management',
action: 'agent_security_tab_alert',
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Agent vulnerability report component renders 1`] = `
exports[`Agent vulnerability report component default renders 1`] = `
<div>
<gl-alert-stub
dismissible="true"
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
title="Latest scan run against primary-agent"
variant="info"
>
<gl-sprintf-stub
message="This view only shows scan results for the agent %{agent}. You can view scan results for all agents in the %{linkStart}Operational Vulnerabilities tab of the vulnerability report%{linkEnd}."
/>
</gl-alert-stub>
<div
class="security-dashboard-filters gl-mt-7"
class="security-dashboard-filters gl-mt-3"
>
<vulnerability-filters-stub
filters="[object Object],[object Object],[object Object]"
......@@ -22,5 +37,11 @@ exports[`Agent vulnerability report component renders 1`] = `
portalname="vulnerability-report-sticky"
query="[object Object]"
/>
<div>
After dismissing the alert, the information will never be shown again.
</div>
</div>
`;
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlModal } from '@gitlab/ui';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import { stubComponent } from 'helpers/stub_component';
import AgentVulnerabilityReport from 'ee/security_dashboard/components/agent/agent_vulnerability_report.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { trackAgentSecurityTabAlert } from 'ee/security_dashboard/components/agent/constants';
jest.mock('~/lib/utils/common_utils', () => ({
parseBoolean: jest.requireActual('~/lib/utils/common_utils').parseBoolean,
getCookie: jest.fn().mockReturnValue(false),
setCookie: jest.fn(),
}));
describe('Agent vulnerability report component', () => {
let wrapper;
......@@ -7,17 +19,71 @@ describe('Agent vulnerability report component', () => {
const propsData = { clusterAgentId: 'gid://gitlab/Clusters::Agent/1' };
const provide = { agentName: 'primary-agent', projectPath: '/path/to/project/' };
const createWrapper = () => {
wrapper = shallowMount(AgentVulnerabilityReport, { propsData, provide });
const findAlert = () => wrapper.findComponent(GlAlert);
const findModal = () => wrapper.findComponent(GlModal);
const createWrapper = ({ stubs = {} } = {}) => {
wrapper = shallowMount(AgentVulnerabilityReport, { propsData, provide, stubs });
};
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
createWrapper();
describe('default', () => {
beforeEach(() => {
createWrapper({ stubs: { GlModal: stubComponent(GlModal) } });
jest.spyOn(findModal().vm, 'show').mockReturnValue();
});
it('renders', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('shows the vulnerability alert if there is no cookie', () => {
expect(findAlert().exists()).toBe(true);
});
it('opens the modal when a user dismisses the alert', async () => {
const modal = findModal();
expect(modal.vm.show).not.toHaveBeenCalled();
findAlert().vm.$emit('dismiss');
await nextTick();
expect(modal.vm.show).toHaveBeenCalled();
});
it('sets the cookie when a user verifies they do not want to view the alert', async () => {
expect(setCookie).not.toHaveBeenCalled();
const modal = findModal();
findAlert().vm.$emit('dismiss');
await nextTick();
modal.vm.$emit('primary');
await nextTick();
expect(setCookie).toHaveBeenCalled();
});
describe('snowplow tracking', () => {
afterEach(() => {
unmockTracking();
});
it('tracks the Snowplow event when a user verifies they do not want to view the alert', async () => {
const { category, action } = trackAgentSecurityTabAlert;
const trackingSpy = mockTracking(category, wrapper.vm.$el, jest.spyOn);
expect(trackingSpy).not.toHaveBeenCalled();
findAlert().vm.$emit('dismiss');
await nextTick();
findModal().vm.$emit('primary');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(category, action);
});
});
});
it('hides the vulnerability alert if there is a cookie', async () => {
getCookie.mockImplementationOnce(() => true);
createWrapper();
await nextTick();
expect(findAlert().exists()).toBe(false);
});
});
......@@ -12740,6 +12740,9 @@ msgid_plural "Dismiss %d selected vulnerabilities as"
msgstr[0] ""
msgstr[1] ""
msgid "Dismiss Alert"
msgstr ""
msgid "Dismiss merge request promotion"
msgstr ""
......@@ -31976,6 +31979,9 @@ msgstr ""
msgid "SecurityOrchestration|Add rule"
msgstr ""
msgid "SecurityOrchestration|After dismissing the alert, the information will never be shown again."
msgstr ""
msgid "SecurityOrchestration|All policies"
msgstr ""
......@@ -31991,6 +31997,9 @@ msgstr ""
msgid "SecurityOrchestration|Description"
msgstr ""
msgid "SecurityOrchestration|Don't show the alert anymore"
msgstr ""
msgid "SecurityOrchestration|Edit policy"
msgstr ""
......@@ -32015,6 +32024,9 @@ msgstr ""
msgid "SecurityOrchestration|Latest scan"
msgstr ""
msgid "SecurityOrchestration|Latest scan run against %{agent}"
msgstr ""
msgid "SecurityOrchestration|Network"
msgstr ""
......@@ -32120,6 +32132,9 @@ msgstr ""
msgid "SecurityOrchestration|This project does not contain any security policies."
msgstr ""
msgid "SecurityOrchestration|This view only shows scan results for the agent %{agent}. You can view scan results for all agents in the %{linkStart}Operational Vulnerabilities tab of the vulnerability report%{linkEnd}."
msgstr ""
msgid "SecurityOrchestration|To widen your search, change filters above or select a different security policy project."
msgstr ""
......
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