Commit ebb87c96 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '213598-multi-dismiss-frontend-vulnerability-list' into 'master'

Allow vulnerability selection in project dashboard

See merge request gitlab-org/gitlab!29293
parents 24b51f5c e4a602ee
......@@ -83,6 +83,9 @@ export default {
});
}
},
refetchVulnerabilities() {
this.$apollo.queries.vulnerabilities.refetch();
},
},
emptyStateDescription: s__(
`While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.`,
......@@ -104,7 +107,10 @@ export default {
:is-loading="isLoadingFirstVulnerabilities"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:filters="filters"
should-show-selection
:vulnerabilities="vulnerabilities"
@refetch-vulnerabilities="refetchVulnerabilities"
>
<template #emptyState>
<gl-empty-state
......
<script>
import { s__, __ } from '~/locale';
import { GlEmptyState, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import { GlEmptyState, GlFormCheckbox, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import RemediatedBadge from './remediated_badge.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import { VULNERABILITIES_PER_PAGE } from '../constants';
export default {
name: 'VulnerabilityList',
components: {
GlEmptyState,
GlFormCheckbox,
GlLink,
GlSkeletonLoading,
GlTable,
RemediatedBadge,
SelectionSummary,
SeverityBadge,
},
props: {
......@@ -24,6 +27,16 @@ export default {
type: String,
required: true,
},
filters: {
type: Object,
required: false,
default: null,
},
shouldShowSelection: {
type: Boolean,
required: false,
default: false,
},
vulnerabilities: {
type: Array,
required: true,
......@@ -34,70 +47,149 @@ export default {
default: false,
},
},
fields: [
{
key: 'state',
label: s__('Vulnerability|Status'),
thClass: 'gl-w-64',
},
{
key: 'severity',
label: s__('Vulnerability|Severity'),
thClass: 'gl-w-64',
},
{
key: 'title',
label: __('Description'),
},
],
data() {
return {
selectedVulnerabilities: {},
};
},
computed: {
hasSelectedAllVulnerabilities() {
return this.numOfSelectedVulnerabilities === this.vulnerabilities.length;
},
numOfSelectedVulnerabilities() {
return Object.keys(this.selectedVulnerabilities).length;
},
shouldShowSelectionSummary() {
return this.shouldShowSelection && Boolean(this.numOfSelectedVulnerabilities);
},
checkboxClass() {
return this.shouldShowSelection ? '' : 'd-none';
},
fields() {
return [
{
key: 'checkbox',
class: this.checkboxClass,
},
{
key: 'state',
label: s__('Vulnerability|Status'),
thClass: 'gl-w-64',
},
{
key: 'severity',
label: s__('Vulnerability|Severity'),
thClass: 'gl-w-64',
},
{
key: 'title',
label: __('Description'),
},
];
},
},
watch: {
filters() {
this.selectedVulnerabilities = {};
},
},
methods: {
deselectAllVulnerabilities() {
this.selectedVulnerabilities = {};
},
isSelected(vulnerability = {}) {
return Boolean(this.selectedVulnerabilities[vulnerability.id]);
},
selectAllVulnerabilities() {
this.selectedVulnerabilities = this.vulnerabilities.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
},
toggleAllVulnerabilities() {
if (this.hasSelectedAllVulnerabilities) {
this.deselectAllVulnerabilities();
} else {
this.selectAllVulnerabilities();
}
},
toggleVulnerability(vulnerability) {
if (!vulnerability) return;
if (this.selectedVulnerabilities[vulnerability.id]) {
this.$delete(this.selectedVulnerabilities, `${vulnerability.id}`);
} else {
this.$set(this.selectedVulnerabilities, `${vulnerability.id}`, vulnerability);
}
},
},
VULNERABILITIES_PER_PAGE,
};
</script>
<template>
<gl-table
:busy="isLoading"
:fields="$options.fields"
:items="vulnerabilities"
stacked="sm"
show-empty
responsive
>
<template #cell(state)="{ item }">
<span class="text-capitalize js-status">{{ item.state.toLowerCase() }}</span>
</template>
<div>
<selection-summary
v-if="shouldShowSelectionSummary"
:selected-vulnerabilities="Object.values(selectedVulnerabilities)"
@deselect-all-vulnerabilities="deselectAllVulnerabilities"
@refetch-vulnerabilities="$emit('refetch-vulnerabilities')"
/>
<gl-table
:busy="isLoading"
:fields="fields"
:items="vulnerabilities"
stacked="sm"
show-empty
responsive
>
<template #head(checkbox)>
<gl-form-checkbox
:checked="hasSelectedAllVulnerabilities"
@change="toggleAllVulnerabilities"
/>
</template>
<template #cell(checkbox)="{ item }">
<gl-form-checkbox :checked="isSelected(item)" @change="toggleVulnerability(item)" />
</template>
<template #cell(severity)="{ item }">
<severity-badge class="js-severity" :severity="item.severity" />
</template>
<template #cell(state)="{ item }">
<span class="text-capitalize js-status">{{ item.state.toLowerCase() }}</span>
</template>
<template #cell(title)="{ item }">
<gl-link class="text-body js-description" :href="item.vulnerabilityPath">
{{ item.title }}
</gl-link>
<remediated-badge v-if="item.resolved_on_default_branch" class="ml-2" />
</template>
<template #cell(severity)="{ item }">
<severity-badge class="js-severity" :severity="item.severity" />
</template>
<template #table-busy>
<gl-skeleton-loading
v-for="n in $options.VULNERABILITIES_PER_PAGE"
:key="n"
class="m-2 js-skeleton-loader"
:lines="2"
/>
</template>
<template #cell(title)="{ item }">
<gl-link class="text-body js-description" :href="item.vulnerabilityPath">
{{ item.title }}
</gl-link>
<remediated-badge v-if="item.resolved_on_default_branch" class="ml-2" />
</template>
<template #empty>
<slot name="emptyState">
<gl-empty-state
:title="s__(`We've found no vulnerabilities`)"
:description="
s__(
`While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly.`,
)
"
<template #table-busy>
<gl-skeleton-loading
v-for="n in $options.VULNERABILITIES_PER_PAGE"
:key="n"
class="m-2 js-skeleton-loader"
:lines="2"
/>
</slot>
</template>
</gl-table>
</template>
<template #empty>
<slot name="emptyState">
<gl-empty-state
:title="s__(`We've found no vulnerabilities`)"
:description="
__(
`While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly.`,
)
"
/>
</slot>
</template>
</gl-table>
</div>
</template>
......@@ -50,7 +50,9 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
isLoading: true,
shouldShowSelection: false,
vulnerabilities: [],
});
});
......@@ -138,7 +140,9 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
isLoading: false,
shouldShowSelection: false,
vulnerabilities,
});
});
......
......@@ -48,7 +48,9 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
isLoading: true,
shouldShowSelection: false,
vulnerabilities: [],
});
});
......@@ -127,7 +129,9 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
filters: null,
isLoading: false,
shouldShowSelection: false,
vulnerabilities,
});
});
......
......@@ -102,7 +102,6 @@ describe('Selection Summary component', () => {
it('should show toast with the right message if all calls were successful', () => {
dismissButton().trigger('submit');
setImmediate(() => {
// return wrapper.vm.$nextTick().then(() => {
expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
});
});
......
import { mount } from '@vue/test-utils';
import { GlEmptyState, GlSkeletonLoading } from '@gitlab/ui';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import VulnerabilityList from 'ee/vulnerabilities/components/vulnerability_list.vue';
import { generateVulnerabilities } from './mock_data';
import { generateVulnerabilities, vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => {
let wrapper;
const createWrapper = props => {
const defaultData = {
selectedVulnerabilities: {},
};
const createWrapper = ({ props = {}, data = defaultData }) => {
return mount(VulnerabilityList, {
propsData: {
dashboardDocumentation: '#',
......@@ -18,52 +23,84 @@ describe('Vulnerability list component', () => {
stubs: {
GlPopover: true,
},
data: () => data,
attachToDocument: true,
});
};
const findCell = label => wrapper.find(`.js-${label}`);
const findRow = (index = 0) => wrapper.findAll('tbody tr').at(index);
const findSelectionSummary = () => wrapper.find(SelectionSummary);
const findCheckAllCheckboxCell = () => wrapper.find('thead tr th');
const findFirstCheckboxCell = () => wrapper.find('tbody tr td');
afterEach(() => wrapper.destroy());
describe('with vulnerabilities', () => {
let vulnerabilities;
let newVulnerabilities;
beforeEach(() => {
vulnerabilities = generateVulnerabilities();
wrapper = createWrapper({ vulnerabilities });
newVulnerabilities = generateVulnerabilities();
wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
});
it('should render a list of vulnerabilities', () => {
expect(wrapper.findAll('.js-status')).toHaveLength(vulnerabilities.length);
expect(wrapper.findAll('.js-status')).toHaveLength(newVulnerabilities.length);
});
it('should correctly render the status', () => {
const cell = findCell('status');
expect(cell.text()).toEqual(vulnerabilities[0].state);
expect(cell.text()).toBe(newVulnerabilities[0].state);
});
it('should correctly render the severity', () => {
const cell = findCell('severity');
expect(cell.text().toLowerCase()).toEqual(vulnerabilities[0].severity);
expect(cell.text().toLowerCase()).toBe(newVulnerabilities[0].severity);
});
it('should correctly render the description', () => {
const cell = findCell('description');
expect(cell.text()).toEqual(vulnerabilities[0].title);
expect(cell.text()).toBe(newVulnerabilities[0].title);
});
it('should not show the selection summary if no vulnerabilities are selected', () => {
expect(findSelectionSummary().exists()).toBe(false);
});
it('should not show the checkboxes if shouldShowSelection is passed in', () => {
expect(findCheckAllCheckboxCell().classes()).toContain('d-none');
expect(findFirstCheckboxCell().classes()).toContain('d-none');
});
it('should show the selection summary when a checkbox is selected', () => {
wrapper = createWrapper({
props: { vulnerabilities, shouldShowSelection: true },
});
const checkbox = findFirstCheckboxCell().find('input');
checkbox.setChecked();
return wrapper.vm.$nextTick().then(() => {
expect(findSelectionSummary().exists()).toBe(true);
});
});
it('should show the checkboxes if shouldShowSelection is passed in', () => {
wrapper = createWrapper({
props: { vulnerabilities, shouldShowSelection: true },
});
expect(findCheckAllCheckboxCell().classes()).not.toContain('d-none');
expect(findFirstCheckboxCell().classes()).not.toContain('d-none');
});
});
describe('when a vulnerability is resolved on the default branch', () => {
let vulnerabilities;
let newVulnerabilities;
beforeEach(() => {
vulnerabilities = generateVulnerabilities();
vulnerabilities[0].resolved_on_default_branch = true;
wrapper = createWrapper({ vulnerabilities });
newVulnerabilities = generateVulnerabilities();
newVulnerabilities[0].resolved_on_default_branch = true;
wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
});
it('should render the remediated info badge on the first vulnerability', () => {
......@@ -83,7 +120,7 @@ describe('Vulnerability list component', () => {
describe('when loading', () => {
beforeEach(() => {
wrapper = createWrapper({ isLoading: true });
wrapper = createWrapper({ props: { isLoading: true } });
});
it('should show the loading state', () => {
......@@ -94,7 +131,7 @@ describe('Vulnerability list component', () => {
describe('with no vulnerabilities', () => {
beforeEach(() => {
wrapper = createWrapper();
wrapper = createWrapper({});
});
it('should show the empty state', () => {
......
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