Commit 8226586f authored by Savas Vedova's avatar Savas Vedova

Load vulnerabilities for instance dashboard

- Enable the feature flag for instance level dashboard
- Display the vulnerability list on the dashboard
- Add related tests
parent 6c39d8d1
import initInstanceSecurityDashboard from 'ee/security_dashboard/instance_init'; import initInstanceSecurityDashboard from 'ee/security_dashboard/instance_init';
import initFirstClassSecurityDashboard from 'ee/security_dashboard/first_class_init';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
if (gon.features?.instanceSecurityDashboard) { document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', initInstanceSecurityDashboard); if (gon.features?.firstClassVulnerabilities) {
} initFirstClassSecurityDashboard(
document.getElementById('js-security'),
DASHBOARD_TYPES.INSTANCE,
);
} else if (gon.features?.instanceSecurityDashboard) {
initInstanceSecurityDashboard();
}
});
<script>
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import InstanceSecurityVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue';
export default {
components: {
SecurityDashboardLayout,
InstanceSecurityVulnerabilities,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<security-dashboard-layout>
<instance-security-vulnerabilities
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
/>
</security-dashboard-layout>
</template>
<script>
import { GlAlert, GlNewButton, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
import { s__ } from '~/locale';
import VulnerabilityList from 'ee/vulnerabilities/components/vulnerability_list.vue';
import vulnerabilitiesQuery from '../graphql/instance_vulnerabilities.graphql';
import { VULNERABILITIES_PER_PAGE } from 'ee/vulnerabilities/constants';
export default {
components: {
GlAlert,
GlNewButton,
GlEmptyState,
GlIntersectionObserver,
VulnerabilityList,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
},
data() {
return {
pageInfo: {},
vulnerabilities: [],
errorLoadingVulnerabilities: false,
};
},
apollo: {
vulnerabilities: {
query: vulnerabilitiesQuery,
variables() {
return {
first: VULNERABILITIES_PER_PAGE,
};
},
update: ({ vulnerabilities }) => vulnerabilities.nodes,
result({ data }) {
this.pageInfo = data.vulnerabilities.pageInfo;
},
error() {
this.errorLoadingVulnerabilities = true;
},
},
},
computed: {
isLoadingQuery() {
return this.$apollo.queries.vulnerabilities.loading;
},
isLoadingFirstResult() {
return this.isLoadingQuery && this.vulnerabilities.length === 0;
},
},
methods: {
onErrorDismiss() {
this.errorLoadingVulnerabilities = false;
},
fetchNextPage() {
if (this.pageInfo.hasNextPage) {
this.$apollo.queries.vulnerabilities.fetchMore({
variables: { after: this.pageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
fetchMoreResult.vulnerabilities.nodes.unshift(...previousResult.vulnerabilities.nodes);
return fetchMoreResult;
},
});
}
},
},
emptyStateDescription: 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.`,
),
};
</script>
<template>
<div>
<gl-alert
v-if="errorLoadingVulnerabilities"
class="mb-4"
variant="danger"
@dismiss="onErrorDismiss"
>
{{
s__(
'Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again.',
)
}}
</gl-alert>
<vulnerability-list
v-else
:is-loading="isLoadingFirstResult"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities="vulnerabilities"
>
<template #emptyState>
<gl-empty-state
:title="s__(`SecurityDashboard|No vulnerabilities found for dashboard`)"
:svg-path="emptyStateSvgPath"
:description="$options.emptyStateDescription"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')"
/>
</template>
</vulnerability-list>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
class="text-center"
@appear="fetchNextPage"
>
<gl-new-button :loading="isLoadingQuery" :disabled="isLoadingQuery" @click="fetchNextPage">{{
__('Load more vulnerabilities')
}}</gl-new-button>
</gl-intersection-observer>
</div>
</template>
...@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue'; import FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue';
import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue'; import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue';
import FirstClassInstanceSecurityDashboard from './components/first_class_instance_security_dashboard.vue';
import createStore from './store'; import createStore from './store';
import createRouter from './store/router'; import createRouter from './store/router';
import projectsPlugin from './store/plugins/projects'; import projectsPlugin from './store/plugins/projects';
...@@ -46,6 +47,8 @@ export default ( ...@@ -46,6 +47,8 @@ export default (
component = FirstClassGroupSecurityDashboard; component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath; props.groupFullPath = el.dataset.groupFullPath;
props.vulnerableProjectsEndpoint = el.dataset.vulnerableProjectsEndpoint; props.vulnerableProjectsEndpoint = el.dataset.vulnerableProjectsEndpoint;
} else if (dashboardType === DASHBOARD_TYPES.INSTANCE) {
component = FirstClassInstanceSecurityDashboard;
} }
const router = createRouter(); const router = createRouter();
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "ee/vulnerabilities/graphql/vulnerability.fragment.graphql"
query instance($after: String, $first: Int) {
vulnerabilities(after:$after, first:$first) {
nodes {
...Vulnerability
}
pageInfo {
...PageInfo
}
}
}
...@@ -2,5 +2,8 @@ ...@@ -2,5 +2,8 @@
module Security module Security
class DashboardController < ::Security::ApplicationController class DashboardController < ::Security::ApplicationController
before_action only: [:show] do
push_frontend_feature_flag(:first_class_vulnerabilities)
end
end end
end end
import { shallowMount } from '@vue/test-utils';
import FirstClassInstanceDashboard from 'ee/security_dashboard/components/first_class_instance_security_dashboard.vue';
import FirstClassInstanceVulnerabilities from 'ee/security_dashboard/components/first_class_instance_security_dashboard_vulnerabilities.vue';
describe('First Class Group Dashboard Component', () => {
let wrapper;
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const findGroupVulnerabilities = () => wrapper.find(FirstClassInstanceVulnerabilities);
const createWrapper = () => {
return shallowMount(FirstClassInstanceDashboard, {
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
},
});
};
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('should render correctly', () => {
expect(findGroupVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlTable, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
import FirstClassInstanceVulnerabilities from 'ee/security_dashboard/components/first_class_instance_security_dashboard_vulnerabilities.vue';
import VulnerabilityList from 'ee/vulnerabilities/components/vulnerability_list.vue';
import { generateVulnerabilities } from '../../vulnerabilities/mock_data';
describe('First Class Group Dashboard Vulnerabilities Component', () => {
let wrapper;
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const emptyStateDescription =
"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.";
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findVulnerabilities = () => wrapper.find(VulnerabilityList);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findAlert = () => wrapper.find(GlAlert);
const createWrapper = ({ stubs, loading = false } = {}) => {
return shallowMount(FirstClassInstanceVulnerabilities, {
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
},
stubs,
mocks: {
$apollo: {
queries: { vulnerabilities: { loading } },
},
fetchNextPage: () => {},
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when the query is loading', () => {
beforeEach(() => {
wrapper = createWrapper({
loading: true,
});
});
it('passes down isLoading correctly', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
isLoading: true,
vulnerabilities: [],
});
});
});
describe('when the query returned an error status', () => {
beforeEach(() => {
wrapper = createWrapper({
stubs: {
GlAlert,
},
});
wrapper.setData({
errorLoadingVulnerabilities: true,
});
});
it('displays the alert', () => {
expect(findAlert().text()).toBe(
'Error fetching the vulnerability list. Please check your network connection and try again.',
);
});
it('should have an alert that is dismissable', () => {
const alert = findAlert();
alert.find('button').trigger('click');
return wrapper.vm.$nextTick(() => {
expect(alert.exists()).toBe(false);
});
});
it('does not display the vulnerabilities', () => {
expect(findVulnerabilities().exists()).toBe(false);
});
});
describe('when the query returned an empty vulnerability list', () => {
beforeEach(() => {
wrapper = createWrapper({
stubs: {
VulnerabilityList,
GlTable,
GlEmptyState,
},
});
});
it('displays the empty state', () => {
expect(findEmptyState().text()).toContain(emptyStateDescription);
});
});
describe('when the query is loaded and we have results', () => {
const vulnerabilities = generateVulnerabilities();
beforeEach(() => {
wrapper = createWrapper({
stubs: {
VulnerabilityList,
GlTable,
GlEmptyState,
},
});
wrapper.setData({
vulnerabilities,
});
});
it('does not have an empty state', () => {
expect(wrapper.html()).not.toContain(emptyStateDescription);
});
it('passes down properties correctly', () => {
expect(findVulnerabilities().props()).toEqual({
dashboardDocumentation,
emptyStateSvgPath,
isLoading: false,
vulnerabilities,
});
});
});
describe('when there is more than a page of vulnerabilities', () => {
const vulnerabilities = generateVulnerabilities();
beforeEach(() => {
wrapper = createWrapper();
wrapper.setData({
vulnerabilities,
pageInfo: {
hasNextPage: true,
},
});
});
it('should render the observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
});
});
...@@ -17941,6 +17941,9 @@ msgstr "" ...@@ -17941,6 +17941,9 @@ msgstr ""
msgid "SecurityDashboard|More information" msgid "SecurityDashboard|More information"
msgstr "" msgstr ""
msgid "SecurityDashboard|No vulnerabilities found for dashboard"
msgstr ""
msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}" msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
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