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:
By default, the list doesn't display resolved or dismissed alerts. To show these alerts, clear the
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).
......
<script>
import { GlFormCheckbox, GlFormGroup, GlSearchBoxByType } from '@gitlab/ui';
import { s__ } from '~/locale';
import { DEBOUNCE, DEFAULT_FILTERS } from './constants';
import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlIcon,
GlSearchBoxByType,
GlTruncate,
} from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import { ALL, DEBOUNCE, STATUSES } from './constants';
export default {
ALL,
DEBOUNCE,
DEFAULT_DISMISSED_FILTER: true,
components: { GlFormCheckbox, GlFormGroup, GlSearchBoxByType },
components: {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlIcon,
GlSearchBoxByType,
GlTruncate,
},
props: {
filters: {
type: Object,
required: false,
default: () => {},
default: () => ({}),
},
},
data() {
......@@ -20,46 +37,107 @@ export default {
};
},
i18n: {
STATUSES,
HIDE_DISMISSED_TITLE: s__('ThreatMonitoring|Hide dismissed alerts'),
POLICY_NAME_FILTER_PLACEHOLDER: s__('NetworkPolicy|Search by policy name'),
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: {
changeDismissedFilter(filtered) {
const newFilters = filtered ? DEFAULT_FILTERS : { statuses: [] };
this.handleFilterChange(newFilters);
handleFilterChange(newFilters) {
this.$emit('filter-change', { ...this.filters, ...newFilters });
},
handleSearch(searchTerm) {
handleNameFilter(searchTerm) {
const newFilters = { searchTerm };
this.handleFilterChange(newFilters);
},
handleFilterChange(newFilters) {
this.$emit('filter-change', { ...this.filters, ...newFilters });
handleStatusFilter(status) {
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>
<template>
<div
class="gl-p-4 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<div>
<h5 class="gl-mt-0">{{ $options.i18n.POLICY_NAME_FILTER_TITLE }}</h5>
<div class="gl-p-4 gl-bg-gray-10 gl-display-flex gl-align-items-center">
<gl-form-group :label="$options.i18n.POLICY_NAME_FILTER_TITLE" label-size="sm" class="gl-mb-0">
<gl-search-box-by-type
:debounce="$options.DEBOUNCE"
:placeholder="$options.i18n.POLICY_NAME_FILTER_PLACEHOLDER"
@input="handleSearch"
@input="handleNameFilter"
/>
</div>
<gl-form-group label-size="sm" class="gl-mb-0">
<gl-form-checkbox
class="gl-mt-3"
:checked="$options.DEFAULT_DISMISSED_FILTER"
@change="changeDismissedFilter"
>
{{ $options.i18n.HIDE_DISMISSED_TITLE }}
</gl-form-checkbox>
</gl-form-group>
<gl-form-group
:label="$options.i18n.POLICY_STATUS_FILTER_TITLE"
label-size="sm"
class="gl-mb-0 col-sm-6 col-md-4 col-lg-2"
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)"
>
{{ translated }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</gl-form-group>
</div>
</template>
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
export const MESSAGES = {
CONFIGURE: s__(
......@@ -58,3 +58,5 @@ export const DEFAULT_FILTERS = { statuses: ['TRIGGERED', 'ACKNOWLEDGED'] };
export const DOMAIN = 'threat_monitoring';
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 { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
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', () => {
let wrapper;
const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox);
const findGlSearch = () => wrapper.find(GlSearchBoxByType);
const findDropdownItemAtIndex = (index) => wrapper.findAll(GlDropdownItem).at(index);
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) => {
wrapper = shallowMount(AlertFilters, { propsData: { filters } });
const createWrapper = ({ filters = DEFAULT_FILTERS, method = shallowMount } = {}) => {
wrapper = method(AlertFilters, { propsData: { filters } });
};
afterEach(() => {
......@@ -20,19 +24,19 @@ describe('AlertFilters component', () => {
describe('Policy Name Filter', () => {
beforeEach(() => {
createWrapper();
createWrapper({});
});
describe('default state', () => {
it('shows policy name search box', () => {
const search = findGlSearch();
const search = findSearch();
expect(search.exists()).toBe(true);
expect(search.attributes('value')).toBe('');
});
it('does emit an event with a user-defined string', async () => {
const searchTerm = 'abc';
const search = findGlSearch();
const search = findSearch();
search.vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
expect(wrapper.emitted('filter-change')).toStrictEqual([
......@@ -42,32 +46,58 @@ describe('AlertFilters component', () => {
});
});
describe('Hide Dismissed Filter', () => {
describe('default state', () => {
it('"hide dismissed checkbox" is checked', () => {
createWrapper();
const checkbox = findGlFormCheckbox();
expect(checkbox.exists()).toBe(true);
expect(checkbox.attributes('checked')).toBeTruthy();
});
describe('Status Filter', () => {
it('Displays the "All" status if no statuses are selected', () => {
createWrapper({ method: mount, filters: { statuses: [] } });
expect(findDropdownMessage()).toBe(ALL.value);
});
describe('dismissed alerts filter', () => {
it('emits an event with no filters on filter deselect', async () => {
createWrapper();
const checkbox = findGlFormCheckbox();
checkbox.vm.$emit('change', false);
await wrapper.vm.$nextTick();
expect(wrapper.emitted('filter-change')).toStrictEqual([[{ statuses: [] }]]);
});
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);
});
it('emits an event with the default filters on filter select', async () => {
createWrapper({});
const checkbox = findGlFormCheckbox();
checkbox.vm.$emit('change', true);
await wrapper.vm.$nextTick();
expect(wrapper.emitted('filter-change')).toEqual([[DEFAULT_FILTERS]]);
it('Displays the additional text if more than one status is selected', () => {
const status = 'TRIGGERED';
const translated = STATUSES[status];
createWrapper({ method: mount });
expect(trimText(findDropdownMessage())).toBe(`${translated} +1 more`);
});
it('Emits an event with the new filters on deselect', async () => {
createWrapper({});
clickDropdownItemAtIndex(2);
expect(wrapper.emitted('filter-change')).toHaveLength(1);
expect(wrapper.emitted('filter-change')[0][0]).toStrictEqual({ statuses: ['TRIGGERED'] });
});
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] ""
msgid "+%{approvers} more approvers"
msgstr ""
msgid "+%{extra} more"
msgstr ""
msgid "+%{more_assignees_count}"
msgstr ""
......@@ -20546,6 +20549,9 @@ msgstr ""
msgid "NetworkPolicy|Search by policy name"
msgstr ""
msgid "NetworkPolicy|Status"
msgstr ""
msgid "Never"
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