Commit 9f17a8b9 authored by Daniel Tian's avatar Daniel Tian Committed by Martin Wortschack

Improve bulk dismissal on security dashboard

Hide vulnerability after dismissal if not in filter list and process
each dismissal instead of stopping after the first failure
parent c4372c85
...@@ -23,6 +23,9 @@ export default { ...@@ -23,6 +23,9 @@ export default {
popoverTitle() { popoverTitle() {
return n__('1 Issue', '%d Issues', this.numberOfIssues); return n__('1 Issue', '%d Issues', this.numberOfIssues);
}, },
issueBadgeEl() {
return () => this.$refs.issueBadge?.$el;
},
}, },
}; };
</script> </script>
...@@ -33,7 +36,7 @@ export default { ...@@ -33,7 +36,7 @@ export default {
<gl-icon name="issues" class="gl-mr-2" /> <gl-icon name="issues" class="gl-mr-2" />
{{ numberOfIssues }} {{ numberOfIssues }}
</gl-badge> </gl-badge>
<gl-popover ref="popover" :target="() => $refs.issueBadge.$el" triggers="hover" placement="top"> <gl-popover ref="popover" :target="issueBadgeEl" triggers="hover" placement="top">
<template #title> <template #title>
{{ popoverTitle }} {{ popoverTitle }}
</template> </template>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlButton, GlFormSelect } from '@gitlab/ui'; import { GlButton, GlFormSelect } from '@gitlab/ui';
import { s__, n__ } from '~/locale'; import { s__, n__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast'; import toast from '~/vue_shared/plugins/global_toast';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import createFlash from '~/flash';
import vulnerabilityDismiss from '../graphql/vulnerability_dismiss.mutation.graphql'; import vulnerabilityDismiss from '../graphql/vulnerability_dismiss.mutation.graphql';
const REASON_NONE = s__('SecurityReports|[No reason]'); const REASON_NONE = s__('SecurityReports|[No reason]');
...@@ -48,30 +48,46 @@ export default { ...@@ -48,30 +48,46 @@ export default {
this.dismissSelectedVulnerabilities(); this.dismissSelectedVulnerabilities();
}, },
dismissSelectedVulnerabilities() { dismissSelectedVulnerabilities() {
let fulfilledCount = 0;
let rejectedCount = 0;
const promises = this.selectedVulnerabilities.map(vulnerability => const promises = this.selectedVulnerabilities.map(vulnerability =>
this.$apollo.mutate({ this.$apollo
mutation: vulnerabilityDismiss, .mutate({
variables: { id: vulnerability.id, comment: this.dismissalReason }, mutation: vulnerabilityDismiss,
}), variables: { id: vulnerability.id, comment: this.dismissalReason },
})
.then(() => {
fulfilledCount += 1;
this.$emit('vulnerability-updated', vulnerability.id);
})
.catch(() => {
rejectedCount += 1;
}),
); );
Promise.all(promises) Promise.all(promises)
.then(() => { .then(() => {
toast( if (fulfilledCount > 0) {
n__( toast(
'%d vulnerability dismissed', n__('%d vulnerability dismissed', '%d vulnerabilities dismissed', fulfilledCount),
'%d vulnerabilities dismissed', );
this.selectedVulnerabilities.length, }
),
);
this.$emit('deselect-all-vulnerabilities'); if (rejectedCount > 0) {
createFlash({
message: n__(
'SecurityReports|There was an error dismissing %d vulnerability. Please try again later.',
'SecurityReports|There was an error dismissing %d vulnerabilities. Please try again later.',
rejectedCount,
),
});
}
}) })
.catch(() => { .catch(() => {
createFlash( createFlash({
s__('SecurityReports|There was an error dismissing the vulnerabilities.'), message: s__('SecurityReports|There was an error dismissing the vulnerabilities.'),
'alert', });
);
}); });
}, },
}, },
......
...@@ -15,6 +15,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba ...@@ -15,6 +15,7 @@ import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_ba
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue'; import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type'; import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier'; import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import SecurityScannerAlert from './security_scanner_alert.vue'; import SecurityScannerAlert from './security_scanner_alert.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
...@@ -87,11 +88,19 @@ export default { ...@@ -87,11 +88,19 @@ export default {
}; };
}, },
computed: { computed: {
// This is a workaround to remove vulnerabilities from the list when their state has changed
// through the bulk update feature, but no longer matches the filters. For more details:
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43468#note_420050017
filteredVulnerabilities() {
return this.vulnerabilities.filter(x =>
this.filters.state?.length ? this.filters.state.includes(x.state) : true,
);
},
isSortable() { isSortable() {
return Boolean(this.$listeners['sort-changed']); return Boolean(this.$listeners['sort-changed']);
}, },
hasAnyScannersOtherThanGitLab() { hasAnyScannersOtherThanGitLab() {
return this.vulnerabilities.some(v => v.scanner?.vendor !== 'GitLab'); return this.filteredVulnerabilities.some(v => v.scanner?.vendor !== 'GitLab');
}, },
notEnabledSecurityScanners() { notEnabledSecurityScanners() {
const { available = [], enabled = [] } = this.securityScanners; const { available = [], enabled = [] } = this.securityScanners;
...@@ -112,16 +121,16 @@ export default { ...@@ -112,16 +121,16 @@ export default {
return Object.keys(this.filters).length > 0; return Object.keys(this.filters).length > 0;
}, },
hasSelectedAllVulnerabilities() { hasSelectedAllVulnerabilities() {
if (!this.vulnerabilities.length) { if (!this.filteredVulnerabilities.length) {
return false; return false;
} }
return this.numOfSelectedVulnerabilities === this.vulnerabilities.length; return this.numOfSelectedVulnerabilities === this.filteredVulnerabilities.length;
}, },
numOfSelectedVulnerabilities() { numOfSelectedVulnerabilities() {
return Object.keys(this.selectedVulnerabilities).length; return Object.keys(this.selectedVulnerabilities).length;
}, },
shouldShowSelectionSummary() { shouldShowSelectionSummary() {
return this.shouldShowSelection && Boolean(this.numOfSelectedVulnerabilities); return this.shouldShowSelection && this.numOfSelectedVulnerabilities > 0;
}, },
theadClass() { theadClass() {
return this.shouldShowSelectionSummary ? 'below-selection-summary' : ''; return this.shouldShowSelectionSummary ? 'below-selection-summary' : '';
...@@ -195,8 +204,8 @@ export default { ...@@ -195,8 +204,8 @@ export default {
filters() { filters() {
this.selectedVulnerabilities = {}; this.selectedVulnerabilities = {};
}, },
vulnerabilities(vulnerabilities) { filteredVulnerabilities() {
const ids = new Set(vulnerabilities.map(v => v.id)); const ids = new Set(this.filteredVulnerabilities.map(v => v.id));
Object.keys(this.selectedVulnerabilities).forEach(vulnerabilityId => { Object.keys(this.selectedVulnerabilities).forEach(vulnerabilityId => {
if (!ids.has(vulnerabilityId)) { if (!ids.has(vulnerabilityId)) {
...@@ -219,6 +228,9 @@ export default { ...@@ -219,6 +228,9 @@ export default {
return file; return file;
}, },
deselectVulnerability(vulnerabilityId) {
this.$delete(this.selectedVulnerabilities, vulnerabilityId);
},
deselectAllVulnerabilities() { deselectAllVulnerabilities() {
this.selectedVulnerabilities = {}; this.selectedVulnerabilities = {};
}, },
...@@ -282,6 +294,11 @@ export default { ...@@ -282,6 +294,11 @@ export default {
this.$emit('sort-changed', { ...args, sortBy: convertToSnakeCase(args.sortBy) }); this.$emit('sort-changed', { ...args, sortBy: convertToSnakeCase(args.sortBy) });
} }
}, },
getVulnerabilityState(state = '') {
const stateName = state.toLowerCase();
// Use the raw state name if we don't have a localization for it.
return VULNERABILITY_STATES[stateName] || stateName;
},
}, },
VULNERABILITIES_PER_PAGE, VULNERABILITIES_PER_PAGE,
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY, SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
...@@ -298,12 +315,12 @@ export default { ...@@ -298,12 +315,12 @@ export default {
<selection-summary <selection-summary
v-if="shouldShowSelectionSummary" v-if="shouldShowSelectionSummary"
:selected-vulnerabilities="Object.values(selectedVulnerabilities)" :selected-vulnerabilities="Object.values(selectedVulnerabilities)"
@deselect-all-vulnerabilities="deselectAllVulnerabilities" @vulnerability-updated="deselectVulnerability"
/> />
<gl-table <gl-table
:busy="isLoading" :busy="isLoading"
:fields="fields" :fields="fields"
:items="vulnerabilities" :items="filteredVulnerabilities"
:thead-class="theadClass" :thead-class="theadClass"
:sort-desc="sortDesc" :sort-desc="sortDesc"
:sort-by="sortBy" :sort-by="sortBy"
...@@ -313,6 +330,7 @@ export default { ...@@ -313,6 +330,7 @@ export default {
class="vulnerability-list" class="vulnerability-list"
show-empty show-empty
responsive responsive
primary-key="id"
@sort-changed="handleSortChange" @sort-changed="handleSortChange"
> >
<template #head(checkbox)> <template #head(checkbox)>
...@@ -350,7 +368,7 @@ export default { ...@@ -350,7 +368,7 @@ export default {
</template> </template>
<template #cell(state)="{ item }"> <template #cell(state)="{ item }">
<span class="text-capitalize js-status">{{ item.state.toLowerCase() }}</span> <span class="text-capitalize js-status">{{ getVulnerabilityState(item.state) }}</span>
</template> </template>
<template #cell(severity)="{ item }"> <template #cell(severity)="{ item }">
......
---
title: Remove item when dismissed on security dashboard if it no longer matches filter
merge_request: 43468
author:
type: changed
...@@ -14,7 +14,7 @@ export const generateVulnerabilities = () => [ ...@@ -14,7 +14,7 @@ export const generateVulnerabilities = () => [
], ],
title: 'Vulnerability 0', title: 'Vulnerability 0',
severity: 'critical', severity: 'critical',
state: 'dismissed', state: 'DISMISSED',
reportType: 'SAST', reportType: 'SAST',
resolvedOnDefaultBranch: true, resolvedOnDefaultBranch: true,
location: { location: {
...@@ -39,7 +39,7 @@ export const generateVulnerabilities = () => [ ...@@ -39,7 +39,7 @@ export const generateVulnerabilities = () => [
], ],
title: 'Vulnerability 1', title: 'Vulnerability 1',
severity: 'high', severity: 'high',
state: 'opened', state: 'DETECTED',
reportType: 'DEPENDENCY_SCANNING', reportType: 'DEPENDENCY_SCANNING',
location: { location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java', file: 'src/main/java/com/gitlab/security_products/tests/App.java',
...@@ -58,7 +58,7 @@ export const generateVulnerabilities = () => [ ...@@ -58,7 +58,7 @@ export const generateVulnerabilities = () => [
identifiers: [], identifiers: [],
title: 'Vulnerability 2', title: 'Vulnerability 2',
severity: 'high', severity: 'high',
state: 'opened', state: 'DETECTED',
reportType: 'CUSTOM_SCANNER_WITHOUT_TRANSLATION', reportType: 'CUSTOM_SCANNER_WITHOUT_TRANSLATION',
location: { location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java', file: 'src/main/java/com/gitlab/security_products/tests/App.java',
...@@ -74,7 +74,7 @@ export const generateVulnerabilities = () => [ ...@@ -74,7 +74,7 @@ export const generateVulnerabilities = () => [
id: 'id_3', id: 'id_3',
title: 'Vulnerability 3', title: 'Vulnerability 3',
severity: 'high', severity: 'high',
state: 'opened', state: 'DETECTED',
location: { location: {
file: 'yarn.lock', file: 'yarn.lock',
}, },
...@@ -87,7 +87,7 @@ export const generateVulnerabilities = () => [ ...@@ -87,7 +87,7 @@ export const generateVulnerabilities = () => [
id: 'id_4', id: 'id_4',
title: 'Vulnerability 4', title: 'Vulnerability 4',
severity: 'critical', severity: 'critical',
state: 'dismissed', state: 'DISMISSED',
location: {}, location: {},
project: { project: {
nameWithNamespace: 'Administrator / Security reports', nameWithNamespace: 'Administrator / Security reports',
......
...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue'; import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import { GlFormSelect, GlButton } from '@gitlab/ui'; import { GlFormSelect, GlButton } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import createFlash from '~/flash';
import toast from '~/vue_shared/plugins/global_toast'; import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -25,11 +25,7 @@ describe('Selection Summary component', () => { ...@@ -25,11 +25,7 @@ describe('Selection Summary component', () => {
const dismissButton = () => wrapper.find(GlButton); const dismissButton = () => wrapper.find(GlButton);
const dismissMessage = () => wrapper.find({ ref: 'dismiss-message' }); const dismissMessage = () => wrapper.find({ ref: 'dismiss-message' });
const formSelect = () => wrapper.find(GlFormSelect); const formSelect = () => wrapper.find(GlFormSelect);
const createComponent = ({ props = {}, data = defaultData, mocks = defaultMocks }) => { const createComponent = ({ props = {}, data = defaultData, mocks = defaultMocks } = {}) => {
if (wrapper) {
throw new Error('Please avoid recreating components in the same spec');
}
spyMutate = mocks.$apollo.mutate; spyMutate = mocks.$apollo.mutate;
wrapper = mount(SelectionSummary, { wrapper = mount(SelectionSummary, {
mocks: { mocks: {
...@@ -63,7 +59,7 @@ describe('Selection Summary component', () => { ...@@ -63,7 +59,7 @@ describe('Selection Summary component', () => {
expect(dismissButton().attributes('disabled')).toBe('disabled'); expect(dismissButton().attributes('disabled')).toBe('disabled');
}); });
it('should have the button enabled if a vulnerability is selected and an option is selected', () => { it('should have the button enabled if a vulnerability is selected and an option is selected', async () => {
expect(wrapper.vm.dismissalReason).toBe(null); expect(wrapper.vm.dismissalReason).toBe(null);
expect(wrapper.findAll('option')).toHaveLength(4); expect(wrapper.findAll('option')).toHaveLength(4);
formSelect() formSelect()
...@@ -71,15 +67,15 @@ describe('Selection Summary component', () => { ...@@ -71,15 +67,15 @@ describe('Selection Summary component', () => {
.at(1) .at(1)
.setSelected(); .setSelected();
formSelect().trigger('change'); formSelect().trigger('change');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.vm.dismissalReason).toEqual(expect.any(String));
expect(dismissButton().attributes('disabled')).toBe(undefined); expect(wrapper.vm.dismissalReason).toEqual(expect.any(String));
}); expect(dismissButton().attributes('disabled')).toBe(undefined);
}); });
}); });
}); });
describe('with 1 vulnerabilities selected', () => { describe('with multiple vulnerabilities selected', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } }); createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } });
}); });
...@@ -93,10 +89,12 @@ describe('Selection Summary component', () => { ...@@ -93,10 +89,12 @@ describe('Selection Summary component', () => {
let mutateMock; let mutateMock;
beforeEach(() => { beforeEach(() => {
mutateMock = jest.fn().mockResolvedValue(); mutateMock = jest.fn(data =>
data.variables.id % 2 === 0 ? Promise.resolve() : Promise.reject(),
);
createComponent({ createComponent({
props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] }, props: { selectedVulnerabilities: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }] },
data: { dismissalReason: 'Will Not Fix' }, data: { dismissalReason: 'Will Not Fix' },
mocks: { $apollo: { mutate: mutateMock } }, mocks: { $apollo: { mutate: mutateMock } },
}); });
...@@ -104,31 +102,29 @@ describe('Selection Summary component', () => { ...@@ -104,31 +102,29 @@ describe('Selection Summary component', () => {
it('should make an API request for each vulnerability', () => { it('should make an API request for each vulnerability', () => {
dismissButton().trigger('submit'); dismissButton().trigger('submit');
expect(spyMutate).toHaveBeenCalledTimes(2); expect(spyMutate).toHaveBeenCalledTimes(5);
}); });
it('should show toast with the right message if all calls were successful', () => { it('should show toast with the right message for the successful calls', async () => {
dismissButton().trigger('submit'); dismissButton().trigger('submit');
return waitForPromises().then(() => { await waitForPromises();
expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
}); expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
}); });
it('should show flash with the right message if some calls failed', () => { it('should show flash with the right message for the failed calls', async () => {
mutateMock.mockRejectedValue();
dismissButton().trigger('submit'); dismissButton().trigger('submit');
return waitForPromises().then(() => { await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
'There was an error dismissing the vulnerabilities.', expect(createFlash).toHaveBeenCalledWith({
'alert', message: 'There was an error dismissing 3 vulnerabilities. Please try again later.',
);
}); });
}); });
}); });
describe('when vulnerabilities are not selected', () => { describe('when vulnerabilities are not selected', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}); createComponent();
}); });
it('should have the button disabled', () => { it('should have the button disabled', () => {
......
...@@ -12,6 +12,7 @@ import VulnerabilityList, { ...@@ -12,6 +12,7 @@ import VulnerabilityList, {
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue'; import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue'; import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { capitalize } from 'lodash';
import { generateVulnerabilities, vulnerabilities } from './mock_data'; import { generateVulnerabilities, vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => { describe('Vulnerability list component', () => {
...@@ -19,7 +20,7 @@ describe('Vulnerability list component', () => { ...@@ -19,7 +20,7 @@ describe('Vulnerability list component', () => {
let wrapper; let wrapper;
const createWrapper = ({ props = {}, listeners }) => { const createWrapper = ({ props = {}, listeners } = {}) => {
return mount(VulnerabilityList, { return mount(VulnerabilityList, {
propsData: { propsData: {
vulnerabilities: [], vulnerabilities: [],
...@@ -42,7 +43,9 @@ describe('Vulnerability list component', () => { ...@@ -42,7 +43,9 @@ describe('Vulnerability list component', () => {
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.find(GlTable);
const findSortableColumn = () => wrapper.find('[aria-sort="descending"]'); const findSortableColumn = () => wrapper.find('[aria-sort="descending"]');
const findCell = label => wrapper.find(`.js-${label}`); const findCell = label => wrapper.find(`.js-${label}`);
const findRow = (index = 0) => wrapper.findAll('tbody tr').at(index); const findRows = () => wrapper.findAll('tbody tr');
const findRow = (index = 0) => findRows().at(index);
const findRowById = id => wrapper.find(`tbody tr[data-pk="${id}"`);
const findIssuesBadge = () => wrapper.find(IssuesBadge); const findIssuesBadge = () => wrapper.find(IssuesBadge);
const findRemediatedBadge = () => wrapper.find(RemediatedBadge); const findRemediatedBadge = () => wrapper.find(RemediatedBadge);
const findSecurityScannerAlert = () => wrapper.find(SecurityScannerAlert); const findSecurityScannerAlert = () => wrapper.find(SecurityScannerAlert);
...@@ -76,7 +79,7 @@ describe('Vulnerability list component', () => { ...@@ -76,7 +79,7 @@ describe('Vulnerability list component', () => {
it('should correctly render the status', () => { it('should correctly render the status', () => {
const cell = findCell('status'); const cell = findCell('status');
expect(cell.text()).toBe(newVulnerabilities[0].state); expect(cell.text()).toBe(capitalize(newVulnerabilities[0].state));
}); });
it('should correctly render the severity', () => { it('should correctly render the severity', () => {
...@@ -133,12 +136,11 @@ describe('Vulnerability list component', () => { ...@@ -133,12 +136,11 @@ describe('Vulnerability list component', () => {
expect(findSelectionSummary().exists()).toBe(false); expect(findSelectionSummary().exists()).toBe(false);
}); });
it('should show the selection summary when a checkbox is selected', () => { it('should show the selection summary when a checkbox is selected', async () => {
findDataCell('vulnerability-checkbox').setChecked(true); findDataCell('vulnerability-checkbox').setChecked(true);
await wrapper.vm.$nextTick();
return wrapper.vm.$nextTick().then(() => { expect(findSelectionSummary().exists()).toBe(true);
expect(findSelectionSummary().exists()).toBe(true);
});
}); });
it('should sync selected vulnerabilities when the vulnerability list is updated', async () => { it('should sync selected vulnerabilities when the vulnerability list is updated', async () => {
...@@ -154,6 +156,18 @@ describe('Vulnerability list component', () => { ...@@ -154,6 +156,18 @@ describe('Vulnerability list component', () => {
expect(findSelectionSummary().exists()).toBe(false); expect(findSelectionSummary().exists()).toBe(false);
}); });
it('should uncheck a selected vulnerability after the vulnerability is updated', async () => {
const checkbox = () => findDataCell('vulnerability-checkbox');
checkbox().setChecked(true);
expect(checkbox().element.checked).toBe(true);
await wrapper.vm.$nextTick();
findSelectionSummary().vm.$emit('vulnerability-updated', newVulnerabilities[0].id);
await wrapper.vm.$nextTick();
expect(checkbox().element.checked).toBe(false);
});
}); });
describe('when vulnerability selection is disabled', () => { describe('when vulnerability selection is disabled', () => {
...@@ -371,7 +385,7 @@ describe('Vulnerability list component', () => { ...@@ -371,7 +385,7 @@ describe('Vulnerability list component', () => {
describe('with no vulnerabilities when there are no filters', () => { describe('with no vulnerabilities when there are no filters', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({}); wrapper = createWrapper();
}); });
it('should show the empty state', () => { it('should show the empty state', () => {
...@@ -398,6 +412,26 @@ describe('Vulnerability list component', () => { ...@@ -398,6 +412,26 @@ describe('Vulnerability list component', () => {
}); });
}); });
describe('with vulnerabilities when there are filters', () => {
it.each`
state
${['DETECTED']}
${['DISMISSED']}
${[]}
${['DETECTED', 'DISMISSED']}
`('should only show vulnerabilities that match filter $state', state => {
wrapper = createWrapper({ props: { vulnerabilities, filters: { state } } });
const filteredVulnerabilities = vulnerabilities.filter(x =>
state.length ? state.includes(x.state) : true,
);
expect(findRows().length).toBe(filteredVulnerabilities.length);
filteredVulnerabilities.forEach(vulnerability => {
expect(findRowById(vulnerability.id).exists()).toBe(true);
});
});
});
describe('security scanner alerts', () => { describe('security scanner alerts', () => {
describe.each` describe.each`
available | enabled | pipelineRun | expectAlertShown available | enabled | pipelineRun | expectAlertShown
...@@ -492,7 +526,7 @@ describe('Vulnerability list component', () => { ...@@ -492,7 +526,7 @@ describe('Vulnerability list component', () => {
describe('when does not have a sort-changed listener defined', () => { describe('when does not have a sort-changed listener defined', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({}); wrapper = createWrapper();
}); });
it('is not sortable', () => { it('is not sortable', () => {
......
...@@ -23027,6 +23027,11 @@ msgstr "" ...@@ -23027,6 +23027,11 @@ msgstr ""
msgid "SecurityReports|There was an error deleting the comment." msgid "SecurityReports|There was an error deleting the comment."
msgstr "" msgstr ""
msgid "SecurityReports|There was an error dismissing %d vulnerability. Please try again later."
msgid_plural "SecurityReports|There was an error dismissing %d vulnerabilities. Please try again later."
msgstr[0] ""
msgstr[1] ""
msgid "SecurityReports|There was an error dismissing the vulnerabilities." msgid "SecurityReports|There was an error dismissing the vulnerabilities."
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