diff --git a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue new file mode 100644 index 0000000000000000000000000000000000000000..ec4326634da6f98ad5c77cae062d92dd57269c0a --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue @@ -0,0 +1,148 @@ + + + diff --git a/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a41e58ff063499114591c8b7f83de51bc6339e85 --- /dev/null +++ b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js @@ -0,0 +1,168 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; +import InstanceSecurityDashboard from 'ee/security_dashboard/components/instance_security_dashboard.vue'; +import SecurityDashboard from 'ee/security_dashboard/components/app.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +const dashboardDocumentation = '/help/docs'; +const emptyStateSvgPath = '/svgs/empty.svg'; +const emptyDashboardStateSvgPath = '/svgs/empty-dash.svg'; +const projectsEndpoint = '/projects'; +const vulnerabilitiesEndpoint = '/vulnerabilities'; +const vulnerabilitiesCountEndpoint = '/vulnerabilities_summary'; +const vulnerabilitiesHistoryEndpoint = '/vulnerabilities_history'; +const vulnerabilityFeedbackHelpPath = '/vulnerabilities_feedback_help'; + +describe('Instance Security Dashboard component', () => { + let store; + let wrapper; + let actionResolvers; + + const factory = ({ projects = [] } = {}) => { + store = new Vuex.Store({ + modules: { + projects: { + namespaced: true, + actions: { + fetchProjects() {}, + setProjectsEndpoint() {}, + }, + state: { + projects, + }, + }, + }, + }); + + actionResolvers = []; + jest.spyOn(store, 'dispatch').mockImplementation( + () => + new Promise(resolve => { + actionResolvers.push(resolve); + }), + ); + + wrapper = shallowMount(InstanceSecurityDashboard, { + localVue, + store, + sync: false, + propsData: { + dashboardDocumentation, + emptyStateSvgPath, + emptyDashboardStateSvgPath, + projectsEndpoint, + vulnerabilitiesEndpoint, + vulnerabilitiesCountEndpoint, + vulnerabilitiesHistoryEndpoint, + vulnerabilityFeedbackHelpPath, + }, + }); + }; + + const resolveActions = () => { + actionResolvers.forEach(resolve => resolve()); + }; + + const findProjectSelectorToggleButton = () => wrapper.find('.js-project-selector-toggle'); + + const clickProjectSelectorToggleButton = () => { + findProjectSelectorToggleButton().vm.$emit('click'); + + return wrapper.vm.$nextTick(); + }; + + const expectComponentWithProps = (Component, props) => { + const componentWrapper = wrapper.find(Component); + expect(componentWrapper.exists()).toBe(true); + expect(componentWrapper.props()).toEqual(expect.objectContaining(props)); + }; + + const expectProjectSelectorState = () => { + expect(findProjectSelectorToggleButton().exists()).toBe(true); + expect(wrapper.find(GlEmptyState).exists()).toBe(false); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(SecurityDashboard).exists()).toBe(false); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('on creation', () => { + beforeEach(() => { + factory(); + }); + + it('dispatches the expected actions', () => { + expect(store.dispatch.mock.calls).toEqual([ + ['projects/setProjectsEndpoint', projectsEndpoint], + ['projects/fetchProjects', undefined], + ]); + }); + + it('displays the initial loading state', () => { + expect(findProjectSelectorToggleButton().exists()).toBe(false); + expect(wrapper.find(GlEmptyState).exists()).toBe(false); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.find(SecurityDashboard).exists()).toBe(false); + }); + }); + + describe('given there are no projects', () => { + beforeEach(() => { + factory(); + resolveActions(); + }); + + it('renders the empty state', () => { + expect(findProjectSelectorToggleButton().exists()).toBe(true); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(SecurityDashboard).exists()).toBe(false); + + expectComponentWithProps(GlEmptyState, { + svgPath: emptyStateSvgPath, + }); + }); + + describe('after clicking the project selector toggle button', () => { + beforeEach(clickProjectSelectorToggleButton); + + it('renders the project selector state', () => { + expectProjectSelectorState(); + }); + }); + }); + + describe('given there are projects', () => { + beforeEach(() => { + factory({ projects: [{ name: 'foo', id: 1 }] }); + resolveActions(); + }); + + it('renders the security dashboard state', () => { + expect(findProjectSelectorToggleButton().exists()).toBe(true); + expect(wrapper.find(GlEmptyState).exists()).toBe(false); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + + expectComponentWithProps(SecurityDashboard, { + dashboardDocumentation, + emptyStateSvgPath: emptyDashboardStateSvgPath, + vulnerabilitiesEndpoint, + vulnerabilitiesCountEndpoint, + vulnerabilitiesHistoryEndpoint, + vulnerabilityFeedbackHelpPath, + }); + }); + + describe('after clicking the project selector toggle button', () => { + beforeEach(clickProjectSelectorToggleButton); + + it('renders the project selector state', () => { + expectProjectSelectorState(); + }); + }); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2b4f75d0a58e1c4fd14f33ee07e407fccbfadf48..c3937a3890595be88628a348e88e55976b31a6df 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14060,15 +14060,30 @@ msgstr "" msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}" msgstr "" +msgid "SecurityDashboard|Add a project to your dashboard" +msgstr "" + +msgid "SecurityDashboard|Add or remove projects from your dashboard" +msgstr "" + +msgid "SecurityDashboard|Add projects" +msgstr "" + msgid "SecurityDashboard|Confidence" msgstr "" +msgid "SecurityDashboard|Edit dashboard" +msgstr "" + msgid "SecurityDashboard|Hide dismissed" msgstr "" msgid "SecurityDashboard|Monitor vulnerabilities in your code" msgstr "" +msgid "SecurityDashboard|More information" +msgstr "" + msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered" msgstr "" @@ -14078,9 +14093,18 @@ msgstr "" msgid "SecurityDashboard|Report type" msgstr "" +msgid "SecurityDashboard|Return to dashboard" +msgstr "" + +msgid "SecurityDashboard|Security Dashboard" +msgstr "" + msgid "SecurityDashboard|Severity" msgstr "" +msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects." +msgstr "" + msgid "SecurityDashboard|Unable to add %{invalidProjects}" msgstr ""