Commit 9b23dc51 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '321891-change-alert-status' into 'master'

Create status filter on policy alerts

See merge request gitlab-org/gitlab!57538
parents 05ef4806 9adb7fe9
...@@ -221,7 +221,7 @@ to set the status for each alert: ...@@ -221,7 +221,7 @@ to set the status for each alert:
By default, the list doesn't display resolved or dismissed alerts. To show these alerts, clear the By default, the list doesn't display resolved or dismissed alerts. To show these alerts, clear the
checkbox **Hide dismissed alerts**. checkbox **Hide dismissed alerts**.
![Policy Alert List](img/threat_monitoring_policy_alert_list_v13_9.png) ![Policy Alert List](img/threat_monitoring_policy_alert_list_v13_11.png)
Clicking an alert's name takes the user to the [alert details page](../../../operations/incident_management/alerts.md#alert-details-page). Clicking an alert's name takes the user to the [alert details page](../../../operations/incident_management/alerts.md#alert-details-page).
......
<script> <script>
import { GlFormCheckbox, GlFormGroup, GlSearchBoxByType } from '@gitlab/ui'; import {
import { s__ } from '~/locale'; GlDropdown,
import { DEBOUNCE, DEFAULT_FILTERS } from './constants'; GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlIcon,
GlSearchBoxByType,
GlTruncate,
} from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import { ALL, DEBOUNCE, STATUSES } from './constants';
export default { export default {
ALL,
DEBOUNCE, DEBOUNCE,
DEFAULT_DISMISSED_FILTER: true, DEFAULT_DISMISSED_FILTER: true,
components: { GlFormCheckbox, GlFormGroup, GlSearchBoxByType }, components: {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlIcon,
GlSearchBoxByType,
GlTruncate,
},
props: { props: {
filters: { filters: {
type: Object, type: Object,
required: false, required: false,
default: () => {}, default: () => ({}),
}, },
}, },
data() { data() {
...@@ -20,46 +37,107 @@ export default { ...@@ -20,46 +37,107 @@ export default {
}; };
}, },
i18n: { i18n: {
STATUSES,
HIDE_DISMISSED_TITLE: s__('ThreatMonitoring|Hide dismissed alerts'), HIDE_DISMISSED_TITLE: s__('ThreatMonitoring|Hide dismissed alerts'),
POLICY_NAME_FILTER_PLACEHOLDER: s__('NetworkPolicy|Search by policy name'), POLICY_NAME_FILTER_PLACEHOLDER: s__('NetworkPolicy|Search by policy name'),
POLICY_NAME_FILTER_TITLE: s__('NetworkPolicy|Policy'), POLICY_NAME_FILTER_TITLE: s__('NetworkPolicy|Policy'),
POLICY_STATUS_FILTER_TITLE: s__('NetworkPolicy|Status'),
},
computed: {
extraOptionCount() {
const numOfStatuses = this.filters.statuses?.length || 0;
return numOfStatuses > 0 ? numOfStatuses - 1 : 0;
},
firstSelectedOption() {
const firstOption = this.filters.statuses?.length ? this.filters.statuses[0] : undefined;
return this.$options.i18n.STATUSES[firstOption] || this.$options.ALL.value;
},
extraOptionText() {
return sprintf(__('+%{extra} more'), { extra: this.extraOptionCount });
},
}, },
methods: { methods: {
changeDismissedFilter(filtered) { handleFilterChange(newFilters) {
const newFilters = filtered ? DEFAULT_FILTERS : { statuses: [] }; this.$emit('filter-change', { ...this.filters, ...newFilters });
this.handleFilterChange(newFilters);
}, },
handleSearch(searchTerm) { handleNameFilter(searchTerm) {
const newFilters = { searchTerm }; const newFilters = { searchTerm };
this.handleFilterChange(newFilters); this.handleFilterChange(newFilters);
}, },
handleFilterChange(newFilters) { handleStatusFilter(status) {
this.$emit('filter-change', { ...this.filters, ...newFilters }); let newFilters;
if (status === this.$options.ALL.key) {
newFilters = { statuses: [] };
} else {
newFilters = this.isChecked(status)
? { statuses: [...this.filters.statuses.filter((s) => s !== status)] }
: { statuses: [...this.filters.statuses, status] };
}
// If all statuses are selected, select the 'All' option
if (newFilters.statuses.length === Object.entries(STATUSES).length) {
newFilters = { statuses: [] };
}
this.handleFilterChange(newFilters);
},
isChecked(status) {
if (status === this.$options.ALL.key) {
return !this.filters.statuses?.length;
}
return this.filters.statuses?.includes(status);
}, },
}, },
}; };
</script> </script>
<template> <template>
<div <div class="gl-p-4 gl-bg-gray-10 gl-display-flex gl-align-items-center">
class="gl-p-4 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between gl-align-items-center" <gl-form-group :label="$options.i18n.POLICY_NAME_FILTER_TITLE" label-size="sm" class="gl-mb-0">
>
<div>
<h5 class="gl-mt-0">{{ $options.i18n.POLICY_NAME_FILTER_TITLE }}</h5>
<gl-search-box-by-type <gl-search-box-by-type
:debounce="$options.DEBOUNCE" :debounce="$options.DEBOUNCE"
:placeholder="$options.i18n.POLICY_NAME_FILTER_PLACEHOLDER" :placeholder="$options.i18n.POLICY_NAME_FILTER_PLACEHOLDER"
@input="handleSearch" @input="handleNameFilter"
/> />
</div> </gl-form-group>
<gl-form-group label-size="sm" class="gl-mb-0"> <gl-form-group
<gl-form-checkbox :label="$options.i18n.POLICY_STATUS_FILTER_TITLE"
class="gl-mt-3" label-size="sm"
:checked="$options.DEFAULT_DISMISSED_FILTER" class="gl-mb-0 col-sm-6 col-md-4 col-lg-2"
@change="changeDismissedFilter" data-testid="policy-alert-status-filter"
>
<gl-dropdown toggle-class="gl-inset-border-1-gray-400!" class="gl-w-full">
<template #button-content>
<gl-truncate :text="firstSelectedOption" class="gl-min-w-0 gl-mr-2" />
<span v-if="extraOptionCount > 0" class="gl-mr-2">
{{ extraOptionText }}
</span>
<gl-icon name="chevron-down" class="gl-flex-shrink-0 gl-ml-auto" />
</template>
<gl-dropdown-item
key="All"
data-testid="ALL"
:is-checked="isChecked($options.ALL.key)"
is-check-item
@click="handleStatusFilter($options.ALL.key)"
>
{{ $options.ALL.value }}
</gl-dropdown-item>
<gl-dropdown-divider />
<template v-for="[status, translated] in Object.entries($options.i18n.STATUSES)">
<gl-dropdown-item
:key="status"
:data-testid="status"
:is-checked="isChecked(status)"
is-check-item
@click="handleStatusFilter(status)"
> >
{{ $options.i18n.HIDE_DISMISSED_TITLE }} {{ translated }}
</gl-form-checkbox> </gl-dropdown-item>
</template>
</gl-dropdown>
</gl-form-group> </gl-form-group>
</div> </div>
</template> </template>
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
export const MESSAGES = { export const MESSAGES = {
CONFIGURE: s__( CONFIGURE: s__(
...@@ -58,3 +58,5 @@ export const DEFAULT_FILTERS = { statuses: ['TRIGGERED', 'ACKNOWLEDGED'] }; ...@@ -58,3 +58,5 @@ export const DEFAULT_FILTERS = { statuses: ['TRIGGERED', 'ACKNOWLEDGED'] };
export const DOMAIN = 'threat_monitoring'; export const DOMAIN = 'threat_monitoring';
export const DEBOUNCE = 250; export const DEBOUNCE = 250;
export const ALL = { key: 'ALL', value: __('All') };
---
title: Create status filter on policy alerts
merge_request: 57538
author:
type: changed
import { GlFormCheckbox, GlSearchBoxByType } from '@gitlab/ui'; import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import AlertFilters from 'ee/threat_monitoring/components/alerts/alert_filters.vue'; import AlertFilters from 'ee/threat_monitoring/components/alerts/alert_filters.vue';
import { DEFAULT_FILTERS } from 'ee/threat_monitoring/components/alerts/constants'; import { ALL, DEFAULT_FILTERS, STATUSES } from 'ee/threat_monitoring/components/alerts/constants';
import { trimText } from 'helpers/text_helper';
describe('AlertFilters component', () => { describe('AlertFilters component', () => {
let wrapper; let wrapper;
const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox); const findDropdownItemAtIndex = (index) => wrapper.findAll(GlDropdownItem).at(index);
const findGlSearch = () => wrapper.find(GlSearchBoxByType); const clickDropdownItemAtIndex = (index) => findDropdownItemAtIndex(index).vm.$emit('click');
const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdownMessage = () =>
wrapper.find('[data-testid="policy-alert-status-filter"] .dropdown button').text();
const createWrapper = (filters = DEFAULT_FILTERS) => { const createWrapper = ({ filters = DEFAULT_FILTERS, method = shallowMount } = {}) => {
wrapper = shallowMount(AlertFilters, { propsData: { filters } }); wrapper = method(AlertFilters, { propsData: { filters } });
}; };
afterEach(() => { afterEach(() => {
...@@ -20,19 +24,19 @@ describe('AlertFilters component', () => { ...@@ -20,19 +24,19 @@ describe('AlertFilters component', () => {
describe('Policy Name Filter', () => { describe('Policy Name Filter', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper({});
}); });
describe('default state', () => { describe('default state', () => {
it('shows policy name search box', () => { it('shows policy name search box', () => {
const search = findGlSearch(); const search = findSearch();
expect(search.exists()).toBe(true); expect(search.exists()).toBe(true);
expect(search.attributes('value')).toBe(''); expect(search.attributes('value')).toBe('');
}); });
it('does emit an event with a user-defined string', async () => { it('does emit an event with a user-defined string', async () => {
const searchTerm = 'abc'; const searchTerm = 'abc';
const search = findGlSearch(); const search = findSearch();
search.vm.$emit('input', searchTerm); search.vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.emitted('filter-change')).toStrictEqual([ expect(wrapper.emitted('filter-change')).toStrictEqual([
...@@ -42,32 +46,58 @@ describe('AlertFilters component', () => { ...@@ -42,32 +46,58 @@ describe('AlertFilters component', () => {
}); });
}); });
describe('Hide Dismissed Filter', () => { describe('Status Filter', () => {
describe('default state', () => { it('Displays the "All" status if no statuses are selected', () => {
it('"hide dismissed checkbox" is checked', () => { createWrapper({ method: mount, filters: { statuses: [] } });
createWrapper(); expect(findDropdownMessage()).toBe(ALL.value);
const checkbox = findGlFormCheckbox();
expect(checkbox.exists()).toBe(true);
expect(checkbox.attributes('checked')).toBeTruthy();
}); });
it('Displays the status if only one status is selected', () => {
const status = 'TRIGGERED';
const translated = STATUSES[status];
createWrapper({ method: mount, filters: { statuses: [status] } });
expect(findDropdownMessage()).toBe(translated);
}); });
describe('dismissed alerts filter', () => { it('Displays the additional text if more than one status is selected', () => {
it('emits an event with no filters on filter deselect', async () => { const status = 'TRIGGERED';
createWrapper(); const translated = STATUSES[status];
const checkbox = findGlFormCheckbox(); createWrapper({ method: mount });
checkbox.vm.$emit('change', false); expect(trimText(findDropdownMessage())).toBe(`${translated} +1 more`);
await wrapper.vm.$nextTick();
expect(wrapper.emitted('filter-change')).toStrictEqual([[{ statuses: [] }]]);
}); });
it('emits an event with the default filters on filter select', async () => { it('Emits an event with the new filters on deselect', async () => {
createWrapper({}); createWrapper({});
const checkbox = findGlFormCheckbox(); clickDropdownItemAtIndex(2);
checkbox.vm.$emit('change', true); expect(wrapper.emitted('filter-change')).toHaveLength(1);
await wrapper.vm.$nextTick(); expect(wrapper.emitted('filter-change')[0][0]).toStrictEqual({ statuses: ['TRIGGERED'] });
expect(wrapper.emitted('filter-change')).toEqual([[DEFAULT_FILTERS]]); });
it('Emits an event with the new filters on a select', () => {
createWrapper({});
clickDropdownItemAtIndex(4);
expect(wrapper.emitted('filter-change')).toHaveLength(1);
expect(wrapper.emitted('filter-change')[0][0]).toStrictEqual({
statuses: ['TRIGGERED', 'ACKNOWLEDGED', 'IGNORED'],
}); });
}); });
it('Emits an event with no filters on a select of all the filters', () => {
const MOST_STATUSES = [...Object.keys(STATUSES)].slice(1);
createWrapper({ filters: { statuses: MOST_STATUSES } });
clickDropdownItemAtIndex(1);
expect(wrapper.emitted('filter-change')).toHaveLength(1);
expect(wrapper.emitted('filter-change')[0][0]).toStrictEqual({ statuses: [] });
});
it('Checks "All" filter if no statuses are selected', () => {
createWrapper({ filters: { statuses: [] } });
expect(findDropdownItemAtIndex(0).props('isChecked')).toBe(true);
});
it('Unchecks "All" filter if a status is selected', () => {
createWrapper({});
expect(findDropdownItemAtIndex(0).props('isChecked')).toBe(false);
});
}); });
}); });
...@@ -1074,6 +1074,9 @@ msgstr[1] "" ...@@ -1074,6 +1074,9 @@ msgstr[1] ""
msgid "+%{approvers} more approvers" msgid "+%{approvers} more approvers"
msgstr "" msgstr ""
msgid "+%{extra} more"
msgstr ""
msgid "+%{more_assignees_count}" msgid "+%{more_assignees_count}"
msgstr "" msgstr ""
...@@ -20546,6 +20549,9 @@ msgstr "" ...@@ -20546,6 +20549,9 @@ msgstr ""
msgid "NetworkPolicy|Search by policy name" msgid "NetworkPolicy|Search by policy name"
msgstr "" msgstr ""
msgid "NetworkPolicy|Status"
msgstr ""
msgid "Never" msgid "Never"
msgstr "" 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