From b3d3fa1a830cab5ba1b3a6d2bcb9664febad20b3 Mon Sep 17 00:00:00 2001 From: Mark Florian Date: Wed, 4 Sep 2019 14:11:29 +0100 Subject: [PATCH 01/10] Add Instance Security Dashboard component This implements the basic scaffolding of the root application component that wraps the security dashboard, and leaves a placeholder for the project selector. Another MR will implement the project selector, and likely a third will integrate the two into the store. Part of the [Instance Security Dashboard MVC][1]. [1]: https://gitlab.com/gitlab-org/gitlab-ee/issues/6953 --- .../javascripts/pages/security/index.js | 56 +++++--- .../instance_security_dashboard_app.vue | 131 ++++++++++++++++++ .../store/modules/projects/getters.js | 3 + .../store/modules/projects/index.js | 2 + ee/app/policies/ee/global_policy.rb | 6 + ee/app/views/security/index.html.haml | 9 +- 6 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue create mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js diff --git a/ee/app/assets/javascripts/pages/security/index.js b/ee/app/assets/javascripts/pages/security/index.js index db010145eec250..02dfc06b470863 100644 --- a/ee/app/assets/javascripts/pages/security/index.js +++ b/ee/app/assets/javascripts/pages/security/index.js @@ -1,24 +1,44 @@ import Vue from 'vue'; import createStore from 'ee/security_dashboard/store'; import router from 'ee/security_dashboard/store/router'; -import DashboardComponent from 'ee/security_dashboard/components/app.vue'; +import InstanceSecurityDashboardApp from 'ee/security_dashboard/components/instance_security_dashboard_app.vue'; if (gon.features && gon.features.securityDashboard) { - document.addEventListener( - 'DOMContentLoaded', - () => - new Vue({ - el: '#js-security', - store: createStore(), - router, - components: { - DashboardComponent, - }, - render(createElement) { - return createElement(DashboardComponent, { - props: {}, - }); - }, - }), - ); + document.addEventListener('DOMContentLoaded', () => { + const el = document.querySelector('#js-security'); + const { + dashboardDocumentation, + emptyStateSvgPath, + emptyDashboardStateSvgPath, + projectsEndpoint, + vulnerabilitiesCountEndpoint, + vulnerabilitiesEndpoint, + vulnerabilitiesHistoryEndpoint, + vulnerabilityFeedbackHelpPath, + } = el.dataset; + const store = createStore(); + + return new Vue({ + el, + store, + router, + components: { + InstanceSecurityDashboardApp, + }, + render(createElement) { + return createElement(InstanceSecurityDashboardApp, { + props: { + dashboardDocumentation, + emptyStateSvgPath, + emptyDashboardStateSvgPath, + projectsEndpoint, + vulnerabilitiesCountEndpoint, + vulnerabilitiesEndpoint, + vulnerabilitiesHistoryEndpoint, + vulnerabilityFeedbackHelpPath, + }, + }); + }, + }); + }); } diff --git a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue new file mode 100644 index 00000000000000..fda63c35fb74a1 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue @@ -0,0 +1,131 @@ + + + diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js new file mode 100644 index 00000000000000..149dbec3c9084e --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js @@ -0,0 +1,3 @@ +import { sprintf, __ } from '~/locale'; + +export const hasProjects = ({ projects }) => projects.length > 0; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js index 68c81bb45096f1..5cec73bde2ebf4 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js @@ -1,10 +1,12 @@ import state from './state'; import mutations from './mutations'; import * as actions from './actions'; +import * as getters from './getters'; export default { namespaced: true, state, mutations, actions, + getters, }; diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index 116478f4b29580..b7a3026551f4ce 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -9,6 +9,12 @@ module GlobalPolicy License.feature_available?(:operations_dashboard) end + condition(:instance_security_dashboard_available) do + true + end + + rule { instance_security_dashboard_available }.enable :read_security_dashboard + rule { operations_dashboard_available }.enable :read_operations_dashboard rule { admin }.policy do enable :read_licenses diff --git a/ee/app/views/security/index.html.haml b/ee/app/views/security/index.html.haml index 48a2c63137292d..602f93e9cafc69 100644 --- a/ee/app/views/security/index.html.haml +++ b/ee/app/views/security/index.html.haml @@ -1,4 +1,11 @@ - page_title _('Security Dashboard') - @hide_breadcrumbs = true -#js-security +#js-security{ data: { vulnerabilities_endpoint: '/groups/gitlab-org/-/security/vulnerabilities', + vulnerabilities_count_endpoint: '/groups/gitlab-org/-/security/vulnerabilities/summary', + vulnerabilities_history_endpoint: '/groups/gitlab-org/-/security/vulnerabilities/history', + projects_endpoint: '/api/v4/groups/2/projects', + vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'), + empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'), + empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), + dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard') } } -- GitLab From e12051c9ff6584705368068c494019cf9de1c784 Mon Sep 17 00:00:00 2001 From: Mark Florian Date: Wed, 25 Sep 2019 17:48:58 +0100 Subject: [PATCH 02/10] Flesh out component and add tests This commit also removes extraneous changes, such as the entry point and HAML template, since the component isn't yet executable outside of a test environment. --- .../javascripts/pages/security/index.js | 56 ++---- .../instance_security_dashboard.vue | 140 +++++++++++++++ .../instance_security_dashboard_app.vue | 131 -------------- .../store/modules/projects/getters.js | 3 - .../store/modules/projects/index.js | 2 - ee/app/policies/ee/global_policy.rb | 6 - ee/app/views/security/index.html.haml | 9 +- .../instance_security_dashboard_spec.js | 165 ++++++++++++++++++ 8 files changed, 324 insertions(+), 188 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue delete mode 100644 ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue delete mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js create mode 100644 ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js diff --git a/ee/app/assets/javascripts/pages/security/index.js b/ee/app/assets/javascripts/pages/security/index.js index 02dfc06b470863..db010145eec250 100644 --- a/ee/app/assets/javascripts/pages/security/index.js +++ b/ee/app/assets/javascripts/pages/security/index.js @@ -1,44 +1,24 @@ import Vue from 'vue'; import createStore from 'ee/security_dashboard/store'; import router from 'ee/security_dashboard/store/router'; -import InstanceSecurityDashboardApp from 'ee/security_dashboard/components/instance_security_dashboard_app.vue'; +import DashboardComponent from 'ee/security_dashboard/components/app.vue'; if (gon.features && gon.features.securityDashboard) { - document.addEventListener('DOMContentLoaded', () => { - const el = document.querySelector('#js-security'); - const { - dashboardDocumentation, - emptyStateSvgPath, - emptyDashboardStateSvgPath, - projectsEndpoint, - vulnerabilitiesCountEndpoint, - vulnerabilitiesEndpoint, - vulnerabilitiesHistoryEndpoint, - vulnerabilityFeedbackHelpPath, - } = el.dataset; - const store = createStore(); - - return new Vue({ - el, - store, - router, - components: { - InstanceSecurityDashboardApp, - }, - render(createElement) { - return createElement(InstanceSecurityDashboardApp, { - props: { - dashboardDocumentation, - emptyStateSvgPath, - emptyDashboardStateSvgPath, - projectsEndpoint, - vulnerabilitiesCountEndpoint, - vulnerabilitiesEndpoint, - vulnerabilitiesHistoryEndpoint, - vulnerabilityFeedbackHelpPath, - }, - }); - }, - }); - }); + document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#js-security', + store: createStore(), + router, + components: { + DashboardComponent, + }, + render(createElement) { + return createElement(DashboardComponent, { + props: {}, + }); + }, + }), + ); } 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 00000000000000..f020659a0905d2 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue @@ -0,0 +1,140 @@ + + + diff --git a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue deleted file mode 100644 index fda63c35fb74a1..00000000000000 --- a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard_app.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js deleted file mode 100644 index 149dbec3c9084e..00000000000000 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/getters.js +++ /dev/null @@ -1,3 +0,0 @@ -import { sprintf, __ } from '~/locale'; - -export const hasProjects = ({ projects }) => projects.length > 0; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js index 5cec73bde2ebf4..68c81bb45096f1 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js @@ -1,12 +1,10 @@ import state from './state'; import mutations from './mutations'; import * as actions from './actions'; -import * as getters from './getters'; export default { namespaced: true, state, mutations, actions, - getters, }; diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index b7a3026551f4ce..116478f4b29580 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -9,12 +9,6 @@ module GlobalPolicy License.feature_available?(:operations_dashboard) end - condition(:instance_security_dashboard_available) do - true - end - - rule { instance_security_dashboard_available }.enable :read_security_dashboard - rule { operations_dashboard_available }.enable :read_operations_dashboard rule { admin }.policy do enable :read_licenses diff --git a/ee/app/views/security/index.html.haml b/ee/app/views/security/index.html.haml index 602f93e9cafc69..48a2c63137292d 100644 --- a/ee/app/views/security/index.html.haml +++ b/ee/app/views/security/index.html.haml @@ -1,11 +1,4 @@ - page_title _('Security Dashboard') - @hide_breadcrumbs = true -#js-security{ data: { vulnerabilities_endpoint: '/groups/gitlab-org/-/security/vulnerabilities', - vulnerabilities_count_endpoint: '/groups/gitlab-org/-/security/vulnerabilities/summary', - vulnerabilities_history_endpoint: '/groups/gitlab-org/-/security/vulnerabilities/history', - projects_endpoint: '/api/v4/groups/2/projects', - vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'), - empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'), - empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), - dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard') } } +#js-security 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 00000000000000..1d4816368dbda5 --- /dev/null +++ b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js @@ -0,0 +1,165 @@ +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; + + const factory = ({ projects = [], isInitialized = false } = {}) => { + store = new Vuex.Store({ + modules: { + projects: { + namespaced: true, + actions: { + fetchProjects() {}, + setProjectsEndpoint() {}, + }, + state: { + isInitialized, + projects, + }, + }, + }, + }); + jest.spyOn(store, 'dispatch').mockImplementation(); + + wrapper = shallowMount(InstanceSecurityDashboard, { + localVue, + store, + sync: false, + propsData: { + dashboardDocumentation, + emptyStateSvgPath, + emptyDashboardStateSvgPath, + projectsEndpoint, + vulnerabilitiesEndpoint, + vulnerabilitiesCountEndpoint, + vulnerabilitiesHistoryEndpoint, + vulnerabilityFeedbackHelpPath, + }, + }); + }; + + const findProjectSelectorToggleButton = () => { + return wrapper.find('.js-project-selector-toggle'); + }; + + const clickProjectSelectorToggleButton = () => { + findProjectSelectorToggleButton().vm.$emit('click'); + + return wrapper.vm.$nextTick(); + }; + + const expectLoadingState = () => { + 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); + }; + + const expectEmptyState = () => { + expect(findProjectSelectorToggleButton().exists()).toBe(true); + expect(wrapper.find(GlEmptyState).exists()).toBe(true); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(SecurityDashboard).exists()).toBe(false); + }; + + const expectDashboardState = () => { + expect(findProjectSelectorToggleButton().exists()).toBe(true); + expect(wrapper.find(GlEmptyState).exists()).toBe(false); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + + const dashboard = wrapper.find(SecurityDashboard); + expect(dashboard.exists()).toBe(true); + expect(dashboard.props()).toEqual( + expect.objectContaining({ + dashboardDocumentation, + emptyStateSvgPath: emptyDashboardStateSvgPath, + projectsEndpoint, + vulnerabilitiesEndpoint, + vulnerabilitiesCountEndpoint, + vulnerabilitiesHistoryEndpoint, + vulnerabilityFeedbackHelpPath, + }), + ); + }; + + 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 loading spinner', () => { + expectLoadingState(); + }); + }); + + describe('given there are no projects', () => { + beforeEach(() => { + factory({ isInitialized: true }); + }); + + it('renders the empty state', () => { + expectEmptyState(); + }); + + describe('after clicking the project selector toggle button', () => { + beforeEach(clickProjectSelectorToggleButton); + + it('renders the project selector', () => { + expectProjectSelectorState(); + }); + }); + }); + + describe('given there are projects', () => { + beforeEach(() => { + factory({ projects: [{ name: 'foo', id: 1 }], isInitialized: true }); + }); + + it('renders the security dashboard', () => { + expectDashboardState(); + }); + + describe('after clicking the project selector toggle button', () => { + beforeEach(clickProjectSelectorToggleButton); + + it('renders the project selector', () => { + expectProjectSelectorState(); + }); + }); + }); +}); -- GitLab From e897d564476f60bbef8d9d1e9431912a9e62ca3a Mon Sep 17 00:00:00 2001 From: Mark Florian Date: Wed, 25 Sep 2019 18:24:35 +0100 Subject: [PATCH 03/10] Prefer slot directive shorthand --- .../components/instance_security_dashboard.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index f020659a0905d2..54bdee33fac380 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue @@ -107,7 +107,7 @@ export default { :title="s__('SecurityDashboard|Add a project to your dashboard')" :svg-path="emptyStateSvgPath" > -