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