Commit 9a7159e5 authored by Sam Beckham's avatar Sam Beckham Committed by Fatih Acet

Makes the sec dashboard empty state a scoped slot

- Defaults to a generic empty state message.
- Set explicit messages for:
- - Group Dashboard
- - Project Dashboard
- - Pipeline Dashboard
parent 1c10562d
---
title: Use better context-specific empty state screens for the Security Dashboards
merge_request: 18382
author:
type: changed
import Vue from 'vue';
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import Translate from '~/vue_shared/translate';
import createDashboardStore from 'ee/security_dashboard/store';
import SecurityDashboardApp from 'ee/security_dashboard/components/app.vue';
......@@ -24,8 +26,6 @@ const initSecurityDashboardApp = el => {
render(createElement) {
return createElement(SecurityDashboardApp, {
props: {
dashboardDocumentation,
emptyStateSvgPath,
lockToProject: {
id: parseInt(projectId, 10),
},
......@@ -38,6 +38,22 @@ const initSecurityDashboardApp = el => {
updateBadgeCount('.js-security-counter', count);
},
},
scopedSlots: {
emptyState: () =>
createElement(GlEmptyState, {
props: {
title: s__(`No vulnerabilities found for this pipeline`),
svgPath: emptyStateSvgPath,
description: s__(
`While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully.`,
),
primaryButtonLink: dashboardDocumentation,
primaryButtonText: s__(
'Security Reports|Learn more about setting up your dashboard',
),
},
}),
},
});
},
});
......
......@@ -17,14 +17,6 @@ export default {
VulnerabilityCountList,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
vulnerabilitiesEndpoint: {
type: String,
required: true,
......@@ -146,10 +138,11 @@ export default {
<div class="row mt-4">
<article class="col" :class="{ 'col-xl-7': !isLockedToProject }">
<security-dashboard-table
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
/>
<security-dashboard-table>
<template #emptyState>
<slot name="emptyState"></slot>
</template>
</security-dashboard-table>
</article>
<aside v-if="shouldShowChart" class="col-xl-5">
......
<script>
import { mapActions } from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import SecurityDashboard from './app.vue';
export default {
name: 'GroupSecurityDashboard',
components: {
GlEmptyState,
SecurityDashboard,
},
props: {
......@@ -49,11 +51,23 @@ export default {
<template>
<security-dashboard
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesCountEndpoint"
:vulnerabilities-history-endpoint="vulnerabilitiesHistoryEndpoint"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
/>
>
<template #emptyState>
<gl-empty-state
:title="s__(`No vulnerabilities found for this group`)"
:svg-path="emptyStateSvgPath"
:description="
s__(
`While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly.`,
)
"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')"
/>
</template>
</security-dashboard>
</template>
......@@ -139,8 +139,6 @@ export default {
<security-dashboard
v-else
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyDashboardStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesCountEndpoint"
:vulnerabilities-history-endpoint="vulnerabilitiesHistoryEndpoint"
......
......@@ -11,16 +11,6 @@ export default {
Pagination,
SecurityDashboardTableRow,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState('vulnerabilities', [
'errorLoadingVulnerabilities',
......@@ -92,18 +82,16 @@ export default {
@openModal="openModal({ vulnerability })"
/>
<gl-empty-state
v-if="showEmptyState"
:title="s__(`Security Reports|We've found no vulnerabilities for your group`)"
:svg-path="emptyStateSvgPath"
:description="
s__(
`Security Reports|While it's rare to have no vulnerabilities for your group, 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.`,
)
"
:primary-button-link="dashboardDocumentation"
:primary-button-text="s__('Security Reports|Learn more about setting up your dashboard')"
/>
<slot v-if="showEmptyState" 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>
<pagination
v-if="showPagination"
......
......@@ -135,12 +135,26 @@ export default {
<h4 class="mt-4 mb-3">{{ __('Vulnerabilities') }}</h4>
<security-dashboard-app
:lock-to-project="project"
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesSummaryEndpoint"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
/>
>
<template #emptyState>
<gl-empty-state
:title="s__(`No vulnerabilities found for this project`)"
:svg-path="emptyStateSvgPath"
:description="
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.`,
)
"
:primary-button-link="dashboardDocumentation"
:primary-button-text="
s__('Security Reports|Learn more about setting up your dashboard')
"
/>
</template>
</security-dashboard-app>
</template>
<gl-empty-state
v-else
......
......@@ -48,7 +48,6 @@ describe('Security Dashboard app', () => {
},
propsData: {
dashboardDocumentation: '',
emptyStateSvgPath: '',
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
......
import Vuex from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import GroupSecurityDashboard from 'ee/security_dashboard/components/group_security_dashboard.vue';
import SecurityDashboard from 'ee/security_dashboard/components/app.vue';
......@@ -18,7 +19,7 @@ describe('Group Security Dashboard component', () => {
let store;
let wrapper;
const factory = () => {
const factory = options => {
store = new Vuex.Store({
modules: {
projects: {
......@@ -45,6 +46,7 @@ describe('Group Security Dashboard component', () => {
vulnerabilitiesHistoryEndpoint,
vulnerabilityFeedbackHelpPath,
},
...options,
});
};
......@@ -69,8 +71,6 @@ describe('Group Security Dashboard component', () => {
expect(dashboard.exists()).toBe(true);
expect(dashboard.props()).toEqual(
expect.objectContaining({
dashboardDocumentation,
emptyStateSvgPath,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
......@@ -79,4 +79,20 @@ describe('Group Security Dashboard component', () => {
);
});
});
describe('with a stubbed dashboard for slot testing', () => {
beforeEach(() => {
factory({
stubs: {
'security-dashboard': { template: '<div><slot name="emptyState"></slot></div>' },
},
});
});
it('renders empty state component with correct props', () => {
const emptyState = wrapper.find(GlEmptyState);
expect(emptyState.attributes('title')).toBe('No vulnerabilities found for this group');
});
});
});
......@@ -161,8 +161,6 @@ describe('Instance Security Dashboard component', () => {
expect(wrapper.find(ProjectManager).exists()).toBe(false);
expectComponentWithProps(SecurityDashboard, {
dashboardDocumentation,
emptyStateSvgPath: emptyDashboardStateSvgPath,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
......
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import component from 'ee/security_dashboard/components/security_dashboard_table.vue';
import SecurityDashboardTable from 'ee/security_dashboard/components/security_dashboard_table.vue';
import SecurityDashboardTableRow from 'ee/security_dashboard/components/security_dashboard_table_row.vue';
import createStore from 'ee/security_dashboard/store';
import { TEST_HOST } from 'spec/test_constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import {
RECEIVE_VULNERABILITIES_ERROR,
......@@ -11,37 +12,37 @@ import {
REQUEST_VULNERABILITIES,
} from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Security Dashboard Table', () => {
const Component = Vue.extend(component);
const vulnerabilitiesEndpoint = '/vulnerabilitiesEndpoint.json';
const props = {
dashboardDocumentation: TEST_HOST,
emptyStateSvgPath: TEST_HOST,
};
let store;
let vm;
let wrapper;
beforeEach(() => {
store = createStore();
wrapper = shallowMount(SecurityDashboardTable, {
localVue,
store,
sync: false,
});
store.state.vulnerabilities.vulnerabilitiesEndpoint = vulnerabilitiesEndpoint;
});
afterEach(() => {
resetStore(store);
vm.$destroy();
wrapper.destroy();
});
describe('while loading', () => {
beforeEach(() => {
store.commit(`vulnerabilities/${REQUEST_VULNERABILITIES}`);
vm = mountComponentWithStore(Component, { store, props });
});
it('should render 10 skeleton rows in the table', () => {
expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength(10);
expect(wrapper.findAll(SecurityDashboardTableRow).length).toEqual(10);
});
});
......@@ -51,11 +52,10 @@ describe('Security Dashboard Table', () => {
vulnerabilities: mockDataVulnerabilities,
pageInfo: {},
});
vm = mountComponentWithStore(Component, { store, props });
});
it('should render a row for each vulnerability', () => {
expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength(
expect(wrapper.findAll(SecurityDashboardTableRow).length).toEqual(
mockDataVulnerabilities.length,
);
});
......@@ -67,26 +67,46 @@ describe('Security Dashboard Table', () => {
vulnerabilities: [],
pageInfo: {},
});
vm = mountComponentWithStore(Component, { store, props });
});
it('should render the empty state', () => {
expect(vm.$el.querySelector('.empty-state')).not.toBeNull();
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
});
});
describe('on error', () => {
beforeEach(() => {
store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_ERROR}`);
vm = mountComponentWithStore(Component, { store, props });
});
it('should not render the empty state', () => {
expect(vm.$el.querySelector('.empty-state')).toBeNull();
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
});
it('should render the error alert', () => {
expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
expect(wrapper.find('.flash-alert').exists()).toBe(true);
});
});
describe('with a custom empty state', () => {
beforeEach(() => {
wrapper = shallowMount(SecurityDashboardTable, {
localVue,
store,
sync: false,
slots: {
emptyState: '<div class="customEmptyState">Hello World</div>',
},
});
store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`, {
vulnerabilities: [],
pageInfo: {},
});
});
it('should render the custom empty state', () => {
expect(wrapper.find('.customEmptyState').exists()).toBe(true);
});
});
});
{
"critical": 2,
"high": 4,
"low": 7,
"medium": 8,
"unknown": 0
}
\ No newline at end of file
......@@ -53,8 +53,6 @@ describe('Card security reports app', () => {
id: 123,
name: 'my-project',
},
dashboardDocumentation: `${TEST_HOST}/dashboard_documentation`,
emptyStateSvgPath: `/empty_state.svg`,
vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerability_feedback_help`,
vulnerabilitiesEndpoint,
vulnerabilitiesSummaryEndpoint,
......
......@@ -11619,6 +11619,15 @@ msgstr ""
msgid "No value set by top-level parent group."
msgstr ""
msgid "No vulnerabilities found for this group"
msgstr ""
msgid "No vulnerabilities found for this pipeline"
msgstr ""
msgid "No vulnerabilities found for this project"
msgstr ""
msgid "No, directly import the existing email addresses and usernames."
msgstr ""
......@@ -15358,12 +15367,6 @@ msgstr ""
msgid "Security Reports|Undo dismiss"
msgstr ""
msgid "Security Reports|We've found no vulnerabilities for your group"
msgstr ""
msgid "Security Reports|While it's rare to have no vulnerabilities for your group, 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."
msgstr ""
msgid "Security configuration help link"
msgstr ""
......@@ -19610,6 +19613,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
msgid "We've found no vulnerabilities"
msgstr ""
msgid "Web IDE"
msgstr ""
......@@ -19678,6 +19684,18 @@ msgstr ""
msgid "When:"
msgstr ""
msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
msgid "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."
msgstr ""
msgid "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."
msgstr ""
msgid "White helpers give contextual information."
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