Commit b0c3c70b authored by Sam Beckham's avatar Sam Beckham Committed by Mark Florian

Makes a vulnerability list vue component

- Adds the dumb component for the vulnerability table
- Adds a smart wrapper around the above component
- Removes the haml table and replaces it with a loading state
- Adds pagination to the vue table
- Adds the alert component error state
- Hooks up the HAML data to the app
parent e1587c3e
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 = {}) => {
dispatch('receiveVulnerabilitiesSuccess', { headers, data });
})
.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 @@
.issue-details.issuable-details
%h2.title= _("Vulnerabilities")
%table.table.b-table.b-table-stacked-sm.gl-table.vulnerabilities-list{ "aria-colcount" => "3", :role => "table" }
- if @vulnerabilities.blank?
%tbody{ :role => "rowgroup" }
%tr.b-table-empty-row{ :role => "row" }
%td{ :role => "cell" }
.empty-state
.svg-250.svg-content
= image_tag 'illustrations/security-dashboard-empty-state.svg', alt: _("No vulnerabilities found for this project")
.text-content
%h4.center= _("No vulnerabilities found for this project")
%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.")
- else
%thead{ :role => "rowgroup" }
%tr{ :role => "row" }
%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"
#app{ data: { empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
vulnerabilities_endpoint: expose_path(api_v4_projects_vulnerabilities_path(id: @project.id)),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index') } }
-# Display table loading animation while Vue app loads
%table.table.gl-table
%thead
%tr
%th.animation-container
.skeleton-line-1
%tbody
- 10.times do |n|
%tr
%td.animation-container
.skeleton-line-1
.skeleton-line-2
---
title: Create a vulnerability-list component
merge_request: 25927
author:
type: added
......@@ -18,7 +18,6 @@ describe Projects::Security::VulnerabilitiesController do
before do
group.add_developer(user)
stub_licensed_features(security_dashboard: true)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
describe 'GET #index' do
......@@ -40,28 +39,6 @@ describe Projects::Security::VulnerabilitiesController do
expect(response).to have_gitlab_http_status(:ok)
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
......
// 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