Commit 7511a956 authored by Mark Florian's avatar Mark Florian

Merge branch '207373-vulnerability-list-component' into 'master'

Create a vulnerability-list component

See merge request gitlab-org/gitlab!25927
parents e97e979e b0c3c70b
import Vue from 'vue';
import VulnerabilitiesApp from 'ee/vulnerabilities/components/vulnerabilities_app.vue';
import createStore from 'ee/security_dashboard/store';
function render() {
const el = document.getElementById('app');
if (!el) {
return false;
}
const { dashboardDocumentation, emptyStateSvgPath, vulnerabilitiesEndpoint } = el.dataset;
return new Vue({
el,
store: createStore(),
render(createElement) {
return createElement(VulnerabilitiesApp, {
props: {
emptyStateSvgPath,
dashboardDocumentation,
vulnerabilitiesEndpoint,
},
});
},
});
}
window.addEventListener('DOMContentLoaded', () => {
render();
});
...@@ -83,7 +83,7 @@ export const fetchVulnerabilities = ({ state, dispatch }, params = {}) => { ...@@ -83,7 +83,7 @@ export const fetchVulnerabilities = ({ state, dispatch }, params = {}) => {
dispatch('receiveVulnerabilitiesSuccess', { headers, data }); dispatch('receiveVulnerabilitiesSuccess', { headers, data });
}) })
.catch(error => { .catch(error => {
dispatch('receiveVulnerabilitiesError', error.response.status); dispatch('receiveVulnerabilitiesError', error?.response?.status);
}); });
}; };
......
<script>
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import VulnerabilityList from 'ee/vulnerabilities/components/vulnerability_list.vue';
export default {
name: 'VulnerabilitiesApp',
components: {
GlAlert,
GlEmptyState,
PaginationLinks,
VulnerabilityList,
},
props: {
vulnerabilitiesEndpoint: {
type: String,
required: true,
},
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState('vulnerabilities', [
'errorLoadingVulnerabilities',
'isLoadingVulnerabilities',
'pageInfo',
'vulnerabilities',
]),
...mapState('filters', ['activeFilters']),
},
created() {
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.fetchVulnerabilities();
},
methods: {
...mapActions('vulnerabilities', ['setVulnerabilitiesEndpoint', 'fetchVulnerabilities']),
fetchPage(page) {
this.fetchVulnerabilities({ ...this.activeFilters, page });
},
},
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.`,
),
};
</script>
<template>
<div>
<gl-alert v-if="errorLoadingVulnerabilities" :dismissible="false" variant="danger">
{{
s__(
'Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again.',
)
}}
</gl-alert>
<vulnerability-list
v-else
:is-loading="isLoadingVulnerabilities"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities="vulnerabilities"
>
<template #emptyState>
<gl-empty-state
:title="s__(`No vulnerabilities found for this project`)"
:svg-path="emptyStateSvgPath"
:description="$options.emptyStateDecription"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')"
/>
</template>
</vulnerability-list>
<pagination-links
v-if="pageInfo.total > 1"
class="justify-content-center prepend-top-default"
:page-info="pageInfo"
:change="fetchPage"
/>
</div>
</template>
<script>
import { s__, __ } from '~/locale';
import { GlEmptyState, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
export default {
name: 'VulnerabilityList',
components: {
GlEmptyState,
GlLink,
GlSkeletonLoading,
GlTable,
SeverityBadge,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
vulnerabilities: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
vulnerabilityPath({ id, path }) {
// Since there's no way to get this from the current data, I'll have to make it up.
// It will be available shortly though, so there's a fall-forward to use it once it's available
// We should remove this method once it's fully available.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25740
return path || `vulnerabilities/${id}`;
},
},
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'),
},
],
};
</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 }}</span>
</template>
<template #cell(severity)="{ item }">
<severity-badge class="js-severity" :severity="item.severity" />
</template>
<template #cell(title)="{ item }">
<gl-link class="text-body js-description" :href="vulnerabilityPath(item)">{{
item.title
}}</gl-link>
</template>
<template #table-busy>
<gl-skeleton-loading v-for="n in 10" :key="n" class="m-2 js-skeleton-loader" :lines="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.`,
)
"
/>
</slot>
</template>
</gl-table>
</template>
...@@ -5,49 +5,18 @@ ...@@ -5,49 +5,18 @@
.issue-details.issuable-details .issue-details.issuable-details
%h2.title= _("Vulnerabilities") %h2.title= _("Vulnerabilities")
%table.table.b-table.b-table-stacked-sm.gl-table.vulnerabilities-list{ "aria-colcount" => "3", :role => "table" } #app{ data: { empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
- if @vulnerabilities.blank? vulnerabilities_endpoint: expose_path(api_v4_projects_vulnerabilities_path(id: @project.id)),
%tbody{ :role => "rowgroup" } dashboard_documentation: help_page_path('user/application_security/security_dashboard/index') } }
%tr.b-table-empty-row{ :role => "row" } -# Display table loading animation while Vue app loads
%td{ :role => "cell" } %table.table.gl-table
.empty-state %thead
.svg-250.svg-content %tr
= image_tag 'illustrations/security-dashboard-empty-state.svg', alt: _("No vulnerabilities found for this project") %th.animation-container
.text-content .skeleton-line-1
%h4.center= _("No vulnerabilities found for this project") %tbody
%p= _("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.") - 10.times do |n|
%tr
- else %td.animation-container
%thead{ :role => "rowgroup" } .skeleton-line-1
%tr{ :role => "row" } .skeleton-line-2
%th.gl-w-64{ "aria-colindex" => "1", :role => "columnheader", :scope => "col" }= s_("Vulnerability|Status")
%th.gl-w-64{ "aria-colindex" => "2", :role => "columnheader", :scope => "col" }= s_("Vulnerability|Severity")
%th{ "aria-colindex" => "3", :role => "columnheader", :scope => "col" }= _("Description")
%tbody{ :role => "rowgroup" }
- @vulnerabilities.each do |vulnerability|
%tr.js-vulnerability{ :role => "row" }
%td{ "aria-colindex" => "1", :role => "cell", "data-label" => s_("Vulnerability|Status") }
.text-capitalize= vulnerability.state
%td{ "aria-colindex" => "2", :role => "cell", "data-label" => s_("Vulnerability|Severity") }
.text-nowrap
- if vulnerability.severity === 'critical'
= sprite_icon("severity-critical", size: 12, css_class: "align-middle text-danger-800")
- elsif vulnerability.severity === 'high'
= sprite_icon("severity-high", size: 12, css_class: "align-middle text-danger-600")
- elsif vulnerability.severity === 'medium'
= sprite_icon("severity-medium", size: 12, css_class: "align-middle text-warning-400")
- elsif vulnerability.severity === 'low'
= sprite_icon("severity-low", size: 12, css_class: "align-middle text-warning-300")
- elsif vulnerability.severity === 'info'
= sprite_icon("severity-info", size: 12, css_class: "align-middle text-primary-400")
- else
= sprite_icon("severity-unknown", size: 12, css_class: "align-middle text-secondary-400")
%span.align-middle.ml-1.text-capitalize= vulnerability.severity
%td{ "aria-colindex" => "2", :role => "cell", "data-label" => _("Description") }
%div
= link_to vulnerability.title, vulnerability_path(vulnerability), :class => "text-body"
- if vulnerability.finding
%br
%span.text-muted.small= vulnerability.finding.location["file"]
= paginate @vulnerabilities, theme: "gitlab"
---
title: Create a vulnerability-list component
merge_request: 25927
author:
type: added
...@@ -18,7 +18,6 @@ describe Projects::Security::VulnerabilitiesController do ...@@ -18,7 +18,6 @@ describe Projects::Security::VulnerabilitiesController do
before do before do
group.add_developer(user) group.add_developer(user)
stub_licensed_features(security_dashboard: true) stub_licensed_features(security_dashboard: true)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end end
describe 'GET #index' do describe 'GET #index' do
...@@ -40,28 +39,6 @@ describe Projects::Security::VulnerabilitiesController do ...@@ -40,28 +39,6 @@ describe Projects::Security::VulnerabilitiesController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index) expect(response).to render_template(:index)
expect(response.body).to have_css(".vulnerabilities-list")
end
it 'renders the first vulnerability' do
show_vulnerability_list
expect(response.body).to have_css(".js-vulnerability", count: 1)
end
it 'renders the pagination' do
show_vulnerability_list
expect(response.body).to have_css(".gl-pagination")
end
end
context "when we have no vulnerabilities" do
it 'renders the empty state' do
show_vulnerability_list
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to have_css('.empty-state')
end end
end end
......
// eslint-disable-next-line import/prefer-default-export
export const vulnerabilities = [
{
id: 'id_0',
title: 'Vuln1',
severity: 'critical',
state: 'dismissed',
},
{
id: 'id_1',
title: 'Vuln2',
severity: 'high',
state: 'opened',
},
];
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { waitForMutation } from 'helpers/vue_test_utils_helper';
import createStore from 'ee/security_dashboard/store';
import VulnerabilitiesApp from 'ee/vulnerabilities/components/vulnerabilities_app.vue';
import VulnerabilityList from 'ee/vulnerabilities/components/vulnerability_list.vue';
import { vulnerabilities } from './mock_data';
describe('Vulnerabilities app component', () => {
let store;
let wrapper;
const WORKING_ENDPOINT = 'WORKING_ENDPOINT';
const mock = new MockAdapter(axios);
const createWrapper = props => {
store = createStore();
return shallowMount(VulnerabilitiesApp, {
propsData: {
dashboardDocumentation: '#',
emptyStateSvgPath: '#',
vulnerabilitiesEndpoint: '',
...props,
},
store,
});
};
beforeEach(() => {
mock.onGet(WORKING_ENDPOINT).replyOnce(200, vulnerabilities);
wrapper = createWrapper({ vulnerabilitiesEndpoint: WORKING_ENDPOINT });
return waitForMutation(wrapper.vm.$store, `vulnerabilities/RECEIVE_VULNERABILITIES_SUCCESS`);
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
it('should pass the vulnerabilties to the vulnerabilites list', () => {
const vulnerabilityList = wrapper.find(VulnerabilityList);
expect(vulnerabilityList.props().vulnerabilities).toEqual(vulnerabilities);
});
});
import { mount } from '@vue/test-utils';
import { GlEmptyState, GlSkeletonLoading } from '@gitlab/ui';
import VulnerabilityList from 'ee/vulnerabilities/components/vulnerability_list.vue';
import { vulnerabilities } from './mock_data';
describe('Vulnerability list component', () => {
let wrapper;
const createWrapper = props => {
return mount(VulnerabilityList, {
propsData: {
dashboardDocumentation: '#',
emptyStateSvgPath: '#',
vulnerabilities: [],
...props,
},
});
};
const findCell = label => wrapper.find(`.js-${label}`);
afterEach(() => wrapper.destroy());
describe('with vulnerabilities', () => {
beforeEach(() => {
wrapper = createWrapper({ vulnerabilities });
});
it('should render a list of vulnerabilities', () => {
expect(wrapper.findAll('.js-status')).toHaveLength(vulnerabilities.length);
});
it('should correctly render the status', () => {
const cell = findCell('status');
expect(cell.text()).toEqual(vulnerabilities[0].state);
});
it('should correctly render the severity', () => {
const cell = findCell('severity');
expect(cell.text().toLowerCase()).toEqual(vulnerabilities[0].severity);
});
it('should correctly render the description', () => {
const cell = findCell('description');
expect(cell.text()).toEqual(vulnerabilities[0].title);
});
});
describe('when loading', () => {
beforeEach(() => {
wrapper = createWrapper({ isLoading: true });
});
it('should show the loading state', () => {
expect(findCell('status').exists()).toEqual(false);
expect(wrapper.find(GlSkeletonLoading).exists()).toEqual(true);
});
});
describe('with no vulnerabilities', () => {
beforeEach(() => {
wrapper = createWrapper();
});
it('should show the empty state', () => {
expect(findCell('status').exists()).toEqual(false);
expect(wrapper.find(GlEmptyState).exists()).toEqual(true);
});
});
});
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